mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
277 lines
12 KiB
YAML
277 lines
12 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
|
|
|
|
concurrency:
|
|
group: publish-prerelease-${{ inputs.tag }}
|
|
cancel-in-progress: false
|
|
|
|
jobs:
|
|
gate:
|
|
name: "Гейт: зелёная Проверка и релизный контракт"
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
sha: ${{ steps.candidate.outputs.sha }}
|
|
tag: ${{ steps.candidate.outputs.tag }}
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
with:
|
|
ref: ${{ github.sha }}
|
|
fetch-depth: 0
|
|
- uses: actions/setup-node@v7
|
|
with: { node-version: 22 }
|
|
- name: Pin the current dev candidate
|
|
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
|
|
}
|
|
SHA=$(git rev-parse HEAD)
|
|
git fetch origin dev
|
|
test "$(git rev-parse origin/dev)" = "$SHA" || {
|
|
echo "::error::The dispatched SHA is no longer the origin/dev tip"
|
|
exit 1
|
|
}
|
|
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"
|
|
|
|
publish:
|
|
name: Публикация тега и релиза
|
|
needs: gate
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
url: ${{ steps.verify.outputs.url }}
|
|
newly_published: ${{ steps.release.outputs.newly_published }}
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
with:
|
|
ref: ${{ needs.gate.outputs.sha }}
|
|
fetch-depth: 0
|
|
- uses: actions/setup-node@v7
|
|
with: { node-version: 22 }
|
|
- name: Build and verify both release assets before publication
|
|
env:
|
|
TAG: ${{ needs.gate.outputs.tag }}
|
|
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
|
|
(cd custom_components/houseplan && zip -qr ../../houseplan.zip .)
|
|
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
|
|
- 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"
|
|
gh release upload "$TAG" dist/houseplan-card.js houseplan.zip \
|
|
--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']) {
|
|
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']) {
|
|
if (!(Number(assets.get(name)?.size) > 0)) throw new Error(`${name} is missing or empty`);
|
|
}
|
|
NODE
|
|
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- assets: `houseplan-card.js`, `houseplan.zip`\n' \
|
|
"$TAG" "$SHA" "$URL" >> "$GITHUB_STEP_SUMMARY"
|
|
- name: Verify HACS prerelease discovery order
|
|
uses: actions/github-script@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,
|
|
);
|
|
}
|
|
|
|
# PROCESS.md 10.2 item 10: closing issues and stripping status labels happens
|
|
# because a beta was published, not because someone remembered to do it. The
|
|
# manual step was skipped twice, and both times it broke the invariant that a
|
|
# closed issue carries no status label — the one thing `verify` relies on.
|
|
#
|
|
# A manual step after a successful release is the worst kind: by the time it is
|
|
# due, the work already looks finished, which is exactly why it gets forgotten.
|
|
close-merged:
|
|
name: Закрытие вошедших issue
|
|
needs: [gate, publish]
|
|
if: ${{ needs.publish.outputs.newly_published == 'true' }}
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: 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:
|
|
- name: Close the S8-merged queue and strip status labels
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
REPO: ${{ github.repository }}
|
|
TAG: ${{ needs.gate.outputs.tag }}
|
|
URL: ${{ needs.publish.outputs.url }}
|
|
run: |
|
|
set -euo pipefail
|
|
# Only the owner's issues take part in the process; issues filed by
|
|
# anyone else never carry status labels and are not ours to close.
|
|
numbers=$(gh issue list --repo "$REPO" --state open --label S8-merged \
|
|
--author Matysh --limit 100 --json number --jq '.[].number')
|
|
if [ -z "$numbers" ]; then
|
|
echo "the S8-merged queue is empty, nothing to close"
|
|
else
|
|
for n in $numbers; do
|
|
gh issue comment "$n" --repo "$REPO" \
|
|
--body "Выпущено в \`$TAG\` · [релиз]($URL)"
|
|
# Label first, then close. If the run dies between the two steps an
|
|
# open issue without a status is visible and fixable in the flow;
|
|
# the reverse order would recreate the exact breakage this job is
|
|
# here to prevent.
|
|
gh issue edit "$n" --repo "$REPO" --remove-label S8-merged
|
|
gh issue close "$n" --repo "$REPO" --reason completed
|
|
echo "closed #$n"
|
|
done
|
|
fi
|
|
# Targeted at the defect that actually recurs, not at the invariant in
|
|
# general: no closed issue may still carry S8-merged.
|
|
leftover=$(gh issue list --repo "$REPO" --state closed --label S8-merged \
|
|
--limit 100 --json number --jq 'length')
|
|
test "$leftover" = "0" || {
|
|
echo "::error::$leftover closed issues still carry S8-merged"
|
|
exit 1
|
|
}
|
|
|
|
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
|