mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-30 03:38:47 +00:00
show/ship stop paying for diff mutants and for every move of dev: - scripts/process-track.mjs resolves the track from the current labels and the diff (show for unlabelled infra, ask for unlabelled product work) and checks the mechanical ship limits; outside them the pipeline comments and relabels track:ship -> track:show in the same round. - Validate on the review material is light on show/ship: a completed push run on the exact SHA is proof, a dispatch asks mutants=false. ask and the ci:mutants label keep the mutant dispatch. - show/ship skip the pre-review rebase when git merge-tree with dev is clean; the candidate is rebased once at merge and still passes Validate before the push to dev. The light merge waits for the push run of the candidate and dispatches only when none appears. - ship inside the limits merges after the light Validate without a model review; the issue gets a machine marker hp:ship-merge. - ship-review.yml + scripts/ship-review.mjs read the code of all ship tasks of a beta range in one model session and publish docs/reviews/SHIP-REVIEW-<tag>.md; both beta publication paths refuse a range with ship tasks the document does not cover or that carries a High. - show reviews judge correctness and AC; the spec review installs neither npm ci nor Chromium, the show review installs Chromium only when the issue names a smoke. Canon: PROCESS.md §5, §5.1, §10.4, new §11.7; REVIEWER.md, AUTHOR.md and AGENTS.md digests. Issue: #696 User-Visible: no Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
348 lines
16 KiB
YAML
348 lines
16 KiB
YAML
name: Публикация пре-релиза (ручная)
|
|
run-name: Publish ${{ inputs.tag }}
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
tag:
|
|
description: "Exact prerelease tag, for example v1.61.0-beta.4"
|
|
required: true
|
|
type: string
|
|
|
|
permissions:
|
|
contents: write
|
|
actions: read
|
|
issues: read
|
|
|
|
concurrency:
|
|
group: publish-prerelease-${{ inputs.tag }}
|
|
cancel-in-progress: false
|
|
|
|
jobs:
|
|
gate:
|
|
name: "Гейт: зелёная Проверка и релизный контракт"
|
|
runs-on: ubuntu-24.04
|
|
# Больше ожидания зелёного Validate в release-gate.mjs (до 60 мин) (#658).
|
|
timeout-minutes: 75
|
|
outputs:
|
|
sha: ${{ steps.candidate.outputs.sha }}
|
|
tag: ${{ steps.candidate.outputs.tag }}
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
with:
|
|
ref: ${{ github.sha }}
|
|
fetch-depth: 0
|
|
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
|
with: { node-version: 22 }
|
|
- name: Pin the dev candidate or the existing annotated tag
|
|
id: candidate
|
|
env:
|
|
TAG: ${{ inputs.tag }}
|
|
REF_NAME: ${{ github.ref_name }}
|
|
run: |
|
|
set -euo pipefail
|
|
test "$REF_NAME" = "dev" || {
|
|
echo "::error::Prereleases must be dispatched from the dev branch, got $REF_NAME"
|
|
exit 1
|
|
}
|
|
DISPATCHED_SHA=$(git rev-parse HEAD)
|
|
git fetch --force origin dev --tags
|
|
REMOTE=$(git ls-remote --tags origin "refs/tags/$TAG" "refs/tags/$TAG^{}")
|
|
if [ -n "$REMOTE" ]; then
|
|
SHA=$(printf '%s\n' "$REMOTE" | awk -v ref="refs/tags/$TAG^{}" '$2 == ref {print $1}')
|
|
test -n "$SHA" || {
|
|
echo "::error::Existing remote tag $TAG is not annotated"
|
|
exit 1
|
|
}
|
|
else
|
|
SHA=$DISPATCHED_SHA
|
|
test "$(git rev-parse origin/dev)" = "$SHA" || {
|
|
echo "::error::The dispatched SHA is no longer the origin/dev tip"
|
|
exit 1
|
|
}
|
|
fi
|
|
git checkout --detach "$SHA"
|
|
echo "sha=$SHA" >> "$GITHUB_OUTPUT"
|
|
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
|
|
- name: Verify version, changelogs and bilingual release notes
|
|
env:
|
|
TAG: ${{ inputs.tag }}
|
|
run: node scripts/release-contract.mjs "$TAG" --repo="$GITHUB_REPOSITORY"
|
|
# #479: тяжёлые job Validate идут только на коммите с трейлером `Release:`.
|
|
# Зелёный Validate без трейлера означал бы прогон без смоков и golden —
|
|
# класс тихого пропуска #171/#207, поэтому трейлер проверяется здесь явно.
|
|
- name: Require the Release trailer on the candidate commit
|
|
env:
|
|
SHA: ${{ steps.candidate.outputs.sha }}
|
|
run: |
|
|
set -euo pipefail
|
|
git log -1 --format=%B "$SHA" > /tmp/head-message.txt
|
|
if ! grep -Eq '^Release:[[:space:]]*v?[0-9]+\.[0-9]+\.[0-9]+' /tmp/head-message.txt; then
|
|
echo "::error::Candidate $SHA has no Release: trailer — Validate ran without the heavy gates (#479)"
|
|
exit 1
|
|
fi
|
|
# #479: свежесть скриншотов на обычном пуше — предупреждение; на
|
|
# кандидате она обязана быть доказана строгим режимом.
|
|
- name: Documentation screenshots are fresh for the candidate
|
|
run: node scripts/check-docs.mjs --screenshots=strict
|
|
- name: Require green Validate for this exact SHA
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
REPO: ${{ github.repository }}
|
|
SHA: ${{ steps.candidate.outputs.sha }}
|
|
run: node scripts/release-gate.mjs "$SHA"
|
|
# #696, PROCESS.md §11.7: задачи track:ship слиты без ревью модели — бета
|
|
# публикуется, только когда их код прочитало пакетное ревью диапазона.
|
|
- name: Ship tasks of the range are batch-reviewed
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
TAG: ${{ steps.candidate.outputs.tag }}
|
|
SHA: ${{ steps.candidate.outputs.sha }}
|
|
run: node scripts/ship-review.mjs check --tag="$TAG" --candidate="$SHA" --repo="$GITHUB_REPOSITORY"
|
|
- name: Bind issue membership to the exact candidate
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
TAG: ${{ steps.candidate.outputs.tag }}
|
|
SHA: ${{ steps.candidate.outputs.sha }}
|
|
run: |
|
|
set -euo pipefail
|
|
mkdir -p release-membership
|
|
if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
|
|
gh release download "$TAG" --repo "$GITHUB_REPOSITORY" \
|
|
--dir release-membership --pattern RELEASE-MEMBERSHIP.json --clobber || true
|
|
fi
|
|
if [ -s release-membership/RELEASE-MEMBERSHIP.json ]; then
|
|
node scripts/release-membership.mjs verify --tag="$TAG" --candidate="$SHA" \
|
|
--input=release-membership/RELEASE-MEMBERSHIP.json
|
|
else
|
|
ISSUES=$(gh issue list --repo "$GITHUB_REPOSITORY" --state open \
|
|
--label S8-merged --limit 1000 --json number --jq 'map(.number)|join(",")')
|
|
node scripts/release-membership.mjs create --tag="$TAG" --candidate="$SHA" \
|
|
--issues="$ISSUES" --allow-unmatched \
|
|
--output=release-membership/RELEASE-MEMBERSHIP.json
|
|
fi
|
|
- name: Preserve candidate membership for publication
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
with:
|
|
name: release-membership
|
|
path: release-membership/RELEASE-MEMBERSHIP.json
|
|
if-no-files-found: error
|
|
retention-days: 7
|
|
|
|
publish:
|
|
name: Публикация тега и релиза
|
|
needs: gate
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 20
|
|
outputs:
|
|
url: ${{ steps.verify.outputs.url }}
|
|
newly_published: ${{ steps.release.outputs.newly_published }}
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
with:
|
|
ref: ${{ needs.gate.outputs.sha }}
|
|
fetch-depth: 0
|
|
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
|
with: { node-version: 22 }
|
|
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
|
with:
|
|
name: release-membership
|
|
path: release-assets
|
|
- name: Build and verify both release assets before publication
|
|
env:
|
|
TAG: ${{ needs.gate.outputs.tag }}
|
|
SHA: ${{ needs.gate.outputs.sha }}
|
|
run: |
|
|
set -euo pipefail
|
|
npm ci
|
|
npm run build
|
|
node scripts/bundle-tree.mjs dist custom_components/houseplan/frontend
|
|
npm run bundle:budget
|
|
VERSION=${TAG#v}
|
|
grep -RFq "$VERSION" dist
|
|
# #540: тот же способ, что у release.yml и release-prerelease.mjs —
|
|
# архив закоммиченного дерева точного коммита, детерминированный.
|
|
git -c core.autocrlf=false archive --format=zip --output=houseplan.zip \
|
|
"$SHA:custom_components/houseplan"
|
|
node scripts/verify-houseplan-zip.mjs houseplan.zip \
|
|
custom_components/houseplan/frontend "$VERSION"
|
|
test -s dist/houseplan-card.js
|
|
test -s dist/houseplan-panel.js
|
|
test -s houseplan.zip
|
|
mkdir -p release-assets
|
|
cp dist/houseplan-card.js houseplan.zip release-assets/
|
|
node scripts/release-membership.mjs verify --tag="$TAG" --candidate="$SHA" \
|
|
--input=release-assets/RELEASE-MEMBERSHIP.json
|
|
node scripts/release-assets.mjs sums release-assets --include-membership
|
|
- name: Create or verify the annotated tag
|
|
env:
|
|
TAG: ${{ needs.gate.outputs.tag }}
|
|
SHA: ${{ needs.gate.outputs.sha }}
|
|
run: |
|
|
set -euo pipefail
|
|
REMOTE=$(git ls-remote --tags origin "refs/tags/$TAG" "refs/tags/$TAG^{}")
|
|
if [ -n "$REMOTE" ]; then
|
|
PEELED=$(printf '%s\n' "$REMOTE" | awk -v ref="refs/tags/$TAG^{}" '$2 == ref {print $1}')
|
|
test -n "$PEELED" || {
|
|
echo "::error::Existing remote tag $TAG is not annotated"
|
|
exit 1
|
|
}
|
|
test "$PEELED" = "$SHA" || {
|
|
echo "::error::Existing tag $TAG points to $PEELED, expected $SHA"
|
|
exit 1
|
|
}
|
|
git fetch --force origin "refs/tags/$TAG:refs/tags/$TAG"
|
|
test "$(git cat-file -t "refs/tags/$TAG")" = "tag"
|
|
else
|
|
git config user.name "github-actions[bot]"
|
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
git tag -a "$TAG" "$SHA" -m "$TAG"
|
|
git push origin "$TAG"
|
|
fi
|
|
- name: Stage, verify and publish the prerelease
|
|
id: release
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
TAG: ${{ needs.gate.outputs.tag }}
|
|
run: |
|
|
set -euo pipefail
|
|
if ! gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
|
|
gh release create "$TAG" --repo "$GITHUB_REPOSITORY" --verify-tag \
|
|
--draft --prerelease --title "$TAG" --notes-file docs/RELEASE-NOTES.md
|
|
fi
|
|
WAS_DRAFT=$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json isDraft --jq .isDraft)
|
|
echo "newly_published=$WAS_DRAFT" >> "$GITHUB_OUTPUT"
|
|
if [ "$WAS_DRAFT" = "false" ]; then
|
|
mkdir -p existing-public
|
|
if gh release download "$TAG" --repo "$GITHUB_REPOSITORY" --dir existing-public \
|
|
--pattern houseplan-card.js --pattern houseplan.zip \
|
|
--pattern RELEASE-MEMBERSHIP.json --pattern SHA256SUMS --clobber \
|
|
&& diff -u release-assets/SHA256SUMS existing-public/SHA256SUMS \
|
|
&& node scripts/release-assets.mjs check existing-public release-assets/SHA256SUMS \
|
|
&& node scripts/release-membership.mjs verify --tag="$TAG" --candidate="${{ needs.gate.outputs.sha }}" \
|
|
--input=existing-public/RELEASE-MEMBERSHIP.json; then
|
|
echo "release is already public and byte-identical; publication skipped"
|
|
exit 0
|
|
fi
|
|
echo "existing public assets need recovery; verified files will be uploaded again"
|
|
fi
|
|
gh release upload "$TAG" release-assets/houseplan-card.js release-assets/houseplan.zip \
|
|
release-assets/RELEASE-MEMBERSHIP.json release-assets/SHA256SUMS \
|
|
--repo "$GITHUB_REPOSITORY" --clobber
|
|
RELEASE_JSON=$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" \
|
|
--json tagName,isDraft,isPrerelease,assets,url)
|
|
export RELEASE_JSON TAG
|
|
node <<'NODE'
|
|
const release = JSON.parse(process.env.RELEASE_JSON);
|
|
if (release.tagName !== process.env.TAG) throw new Error('release tag mismatch');
|
|
const assets = new Map(release.assets.map((asset) => [asset.name, asset]));
|
|
for (const name of ['houseplan-card.js', 'houseplan.zip', 'RELEASE-MEMBERSHIP.json', 'SHA256SUMS']) {
|
|
if (!(Number(assets.get(name)?.size) > 0)) throw new Error(`${name} is missing or empty`);
|
|
}
|
|
NODE
|
|
gh release edit "$TAG" --repo "$GITHUB_REPOSITORY" --draft=false --prerelease \
|
|
--title "$TAG" --notes-file docs/RELEASE-NOTES.md
|
|
- name: Verify the public release and assets
|
|
id: verify
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
TAG: ${{ needs.gate.outputs.tag }}
|
|
SHA: ${{ needs.gate.outputs.sha }}
|
|
run: |
|
|
set -euo pipefail
|
|
RELEASE_JSON=$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" \
|
|
--json tagName,isDraft,isPrerelease,assets,url)
|
|
export RELEASE_JSON TAG
|
|
node <<'NODE'
|
|
const release = JSON.parse(process.env.RELEASE_JSON);
|
|
if (release.tagName !== process.env.TAG || release.isDraft || !release.isPrerelease)
|
|
throw new Error('release is not a public prerelease for the requested tag');
|
|
const assets = new Map(release.assets.map((asset) => [asset.name, asset]));
|
|
for (const name of ['houseplan-card.js', 'houseplan.zip', 'RELEASE-MEMBERSHIP.json', 'SHA256SUMS']) {
|
|
if (!(Number(assets.get(name)?.size) > 0)) throw new Error(`${name} is missing or empty`);
|
|
}
|
|
NODE
|
|
# #540: публичные байты — ровно те, что собраны и проверены выше.
|
|
mkdir -p public
|
|
gh release download "$TAG" --repo "$GITHUB_REPOSITORY" --dir public \
|
|
--pattern houseplan-card.js --pattern houseplan.zip \
|
|
--pattern RELEASE-MEMBERSHIP.json --pattern SHA256SUMS --clobber
|
|
diff -u release-assets/SHA256SUMS public/SHA256SUMS
|
|
node scripts/release-assets.mjs check public release-assets/SHA256SUMS
|
|
node scripts/release-membership.mjs verify --tag="$TAG" --candidate="$SHA" \
|
|
--input=public/RELEASE-MEMBERSHIP.json
|
|
test "$(git rev-list -n 1 "$TAG")" = "$SHA"
|
|
URL=$(node -p "JSON.parse(process.env.RELEASE_JSON).url")
|
|
echo "url=$URL" >> "$GITHUB_OUTPUT"
|
|
printf '### Published %s\n\n- exact SHA: `%s`\n- [GitHub prerelease](%s)\n\n```\n%s```\n' \
|
|
"$TAG" "$SHA" "$URL" "$(cat public/SHA256SUMS)" >> "$GITHUB_STEP_SUMMARY"
|
|
- name: Verify HACS prerelease discovery order
|
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
|
env:
|
|
EXPECTED_TAG: ${{ needs.gate.outputs.tag }}
|
|
with:
|
|
script: |
|
|
const releases = await github.paginate(github.rest.repos.listReleases, {
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
per_page: 100,
|
|
});
|
|
const first = releases.find((release) => release.prerelease && !release.draft);
|
|
if (first?.tag_name !== process.env.EXPECTED_TAG) {
|
|
core.setFailed(
|
|
`HACS prerelease discovery is stale: ${first?.tag_name ?? 'none'} precedes ` +
|
|
process.env.EXPECTED_TAG,
|
|
);
|
|
}
|
|
|
|
# #547: bookkeeping is driven by the immutable candidate manifest, not by the
|
|
# mutable S8 queue. It also runs on a verified retry of an already-public beta.
|
|
close-merged:
|
|
name: Закрытие вошедших issue
|
|
needs: [gate, publish]
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 15
|
|
permissions:
|
|
contents: read
|
|
actions: read
|
|
# Deliberately the stock token, not a PAT: events caused by GITHUB_TOKEN do
|
|
# not start workflows, so removing the label cannot wake the review
|
|
# pipeline. A PAT here would build a cascade out of a bookkeeping step.
|
|
issues: write
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
with:
|
|
ref: ${{ needs.gate.outputs.sha }}
|
|
fetch-depth: 0
|
|
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
|
with: { node-version: 22 }
|
|
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
|
with:
|
|
name: release-membership
|
|
path: release-membership
|
|
- name: Finish only the issues proven in the candidate manifest
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
REPO: ${{ github.repository }}
|
|
TAG: ${{ needs.gate.outputs.tag }}
|
|
URL: ${{ needs.publish.outputs.url }}
|
|
run: |
|
|
set -euo pipefail
|
|
node scripts/release-bookkeeping.mjs --repo="$REPO" --tag="$TAG" \
|
|
--candidate="${{ needs.gate.outputs.sha }}" --url="$URL" \
|
|
--membership=release-membership/RELEASE-MEMBERSHIP.json
|
|
|
|
announce:
|
|
name: Комментарий о публикации
|
|
needs: [gate, publish]
|
|
if: ${{ needs.publish.outputs.newly_published == 'true' }}
|
|
uses: ./.github/workflows/announce.yml
|
|
with:
|
|
reusable: true
|
|
tag: ${{ needs.gate.outputs.tag }}
|
|
release_name: ${{ needs.gate.outputs.tag }}
|
|
url: ${{ needs.publish.outputs.url }}
|
|
prerelease: true
|
|
ref: ${{ needs.gate.outputs.tag }}
|
|
secrets: inherit
|