Nightly release preparation #2
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Nightly release preparation | |
| on: | |
| schedule: | |
| # Avoid the start-of-hour congestion window while still running near | |
| # midnight in China. GitHub evaluates this with the explicit IANA zone. | |
| - cron: '7 0 * * *' | |
| timezone: 'Asia/Shanghai' | |
| workflow_dispatch: | |
| permissions: | |
| actions: write | |
| contents: write | |
| pull-requests: write | |
| concurrency: | |
| group: nightly-release-preparation | |
| cancel-in-progress: false | |
| jobs: | |
| prepare: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| - name: Find unreleased commits | |
| id: release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| latest_tag="$(gh release view --json tagName --jq .tagName)" | |
| if ! git rev-parse --verify --quiet "refs/tags/$latest_tag" >/dev/null; then | |
| git fetch origin "refs/tags/$latest_tag:refs/tags/$latest_tag" | |
| fi | |
| if git merge-base --is-ancestor "$latest_tag" HEAD; then | |
| unreleased="$(git rev-list --count "$latest_tag"..HEAD)" | |
| else | |
| echo "::error::Latest published release $latest_tag is not an ancestor of main" | |
| exit 1 | |
| fi | |
| echo "latest_tag=$latest_tag" >> "$GITHUB_OUTPUT" | |
| echo "unreleased=$unreleased" >> "$GITHUB_OUTPUT" | |
| if [ "$unreleased" -eq 0 ]; then | |
| echo "No commits after $latest_tag" | |
| echo "prepare=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| next_version="$(node scripts/prepare-release.mjs next "$latest_tag")" | |
| current_version="$(node scripts/prepare-release.mjs current)" | |
| echo "next_version=$next_version" >> "$GITHUB_OUTPUT" | |
| echo "branch=automation/release-v$next_version" >> "$GITHUB_OUTPUT" | |
| if [ "$current_version" = "${latest_tag#v}" ]; then | |
| echo "prepare=true" >> "$GITHUB_OUTPUT" | |
| elif [ "$current_version" = "$next_version" ]; then | |
| node scripts/prepare-release.mjs verify-current | |
| echo "v$current_version is already prepared on main and is waiting to be released." | |
| echo "prepare=false" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "::error::App version $current_version is neither latest release ${latest_tag#v} nor next patch $next_version" | |
| exit 1 | |
| fi | |
| - name: Stop when main is already fully released | |
| if: steps.release.outputs.unreleased == '0' | |
| run: echo "main matches ${{ steps.release.outputs.latest_tag }}; no release PR is needed." | |
| - name: Recover or refuse an existing release branch | |
| id: branch | |
| if: steps.release.outputs.prepare == 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| RELEASE_BRANCH: ${{ steps.release.outputs.branch }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if git ls-remote --exit-code --heads origin "$RELEASE_BRANCH" >/dev/null 2>&1; then | |
| pr_url="$(gh pr list --state open --head "$RELEASE_BRANCH" --json url --jq '.[0].url // empty')" | |
| if [ -n "$pr_url" ]; then | |
| echo "Release PR already exists: $pr_url" | |
| echo "exists=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| # A previous run may have pushed the prepared commit and then | |
| # failed before `gh pr create`. Recover only that exact state: | |
| # one generated release commit directly on the current main, with | |
| # internally consistent version metadata. Anything else remains a | |
| # hard stop so automation never adopts an unrelated branch. | |
| git fetch origin "refs/heads/$RELEASE_BRANCH:refs/remotes/origin/$RELEASE_BRANCH" | |
| branch_ref="refs/remotes/origin/$RELEASE_BRANCH" | |
| branch_sha="$(git rev-parse "$branch_ref")" | |
| parent_sha="$(git rev-parse "$branch_ref^")" | |
| version="${RELEASE_BRANCH#automation/release-v}" | |
| subject="$(git log -1 --format=%s "$branch_ref")" | |
| if [ "$parent_sha" != "$GITHUB_SHA" ] || [ "$subject" != "chore(release): prepare v$version" ]; then | |
| echo "::error::Remote branch $RELEASE_BRANCH is not the expected release commit on current main" | |
| exit 1 | |
| fi | |
| recovery_dir="$(mktemp -d)" | |
| git worktree add --detach "$recovery_dir" "$branch_sha" | |
| if ! (cd "$recovery_dir" && node scripts/prepare-release.mjs verify "v$version"); then | |
| git worktree remove --force "$recovery_dir" | |
| echo "::error::Remote branch $RELEASE_BRANCH failed release metadata validation" | |
| exit 1 | |
| fi | |
| git worktree remove --force "$recovery_dir" | |
| body_file="$(mktemp)" | |
| cat > "$body_file" <<EOF | |
| ## Summary | |
| Nightly automation recovered the already-prepared patch release \`v$version\` after an earlier run pushed the branch but failed before creating its pull request. | |
| Review the generated CHANGELOG and in-app release notes before merging. | |
| ## Automated checks | |
| - release branch is exactly one generated commit on the current \`main\` | |
| - release metadata consistency validation | |
| EOF | |
| gh pr create \ | |
| --base main \ | |
| --head "$RELEASE_BRANCH" \ | |
| --title "chore(release): prepare v$version" \ | |
| --body-file "$body_file" \ | |
| --draft | |
| gh workflow run test.yml --ref "$RELEASE_BRANCH" | |
| echo "Recovered release PR for $RELEASE_BRANCH" | |
| echo "exists=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| echo "exists=false" >> "$GITHUB_OUTPUT" | |
| - name: Prepare patch release | |
| if: steps.release.outputs.prepare == 'true' && steps.branch.outputs.exists != 'true' | |
| env: | |
| LATEST_TAG: ${{ steps.release.outputs.latest_tag }} | |
| NEXT_VERSION: ${{ steps.release.outputs.next_version }} | |
| run: | | |
| node scripts/prepare-release.mjs prepare \ | |
| --from "$LATEST_TAG" \ | |
| --version "$NEXT_VERSION" \ | |
| --date "$(TZ=Asia/Shanghai date +%F)" | |
| - name: Test release automation | |
| if: steps.release.outputs.prepare == 'true' && steps.branch.outputs.exists != 'true' | |
| working-directory: scripts | |
| run: | | |
| npm ci --no-audit --no-fund | |
| npm test | |
| - name: Commit release preparation | |
| if: steps.release.outputs.prepare == 'true' && steps.branch.outputs.exists != 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| LATEST_TAG: ${{ steps.release.outputs.latest_tag }} | |
| NEXT_VERSION: ${{ steps.release.outputs.next_version }} | |
| RELEASE_BRANCH: ${{ steps.release.outputs.branch }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| git switch -c "$RELEASE_BRANCH" | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git add \ | |
| CHANGELOG.md \ | |
| crates/iec104sim-app/Cargo.toml \ | |
| crates/iec104sim-app/tauri.conf.json \ | |
| crates/iec104master-app/Cargo.toml \ | |
| crates/iec104master-app/tauri.conf.json \ | |
| frontend/src/releaseNotes.ts \ | |
| master-frontend/src/releaseNotes.ts | |
| git commit -m "chore(release): prepare v$NEXT_VERSION" | |
| git push origin "$RELEASE_BRANCH" | |
| body_file="$(mktemp)" | |
| cat > "$body_file" <<EOF | |
| ## Summary | |
| Nightly automation found ${{ steps.release.outputs.unreleased }} commit(s) on \`main\` after \`$LATEST_TAG\` and prepared patch release \`v$NEXT_VERSION\`. | |
| The generated CHANGELOG and in-app release notes are intentionally editable. Review their wording before merging. | |
| ## Release behavior | |
| After this PR is merged and normal CI succeeds, the release-on-merge workflow will validate the version metadata, create tag \`v$NEXT_VERSION\`, and explicitly dispatch the multi-platform Release workflow. | |
| ## Automated checks | |
| - \`npm test\` in \`scripts/\` | |
| - release metadata consistency validation | |
| EOF | |
| gh pr create \ | |
| --base main \ | |
| --head "$RELEASE_BRANCH" \ | |
| --title "chore(release): prepare v$NEXT_VERSION" \ | |
| --body-file "$body_file" \ | |
| --draft | |
| # Workflow-created PR runs require manual approval, so explicitly | |
| # attach the normal Test workflow to the release branch without an | |
| # approval gate. | |
| gh workflow run test.yml --ref "$RELEASE_BRANCH" |