-
Notifications
You must be signed in to change notification settings - Fork 13
213 lines (186 loc) · 8.68 KB
/
Copy pathnightly-release.yml
File metadata and controls
213 lines (186 loc) · 8.68 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
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"