Repository navigation
303 lines (278 loc) · 15.1 KB
/
Copy pathdev-version-bump.yml
File metadata and controls
303 lines (278 loc) · 15.1 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
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
name: Dev version bump
# Before a release publishes, open a pull request that moves `dev` past the intended
# version. Merge that pull request before promoting and publishing so `dev` and pull
# requests based on it never inherit a version-line failure from the new tag.
#
# That has been repaired by hand four times: 32529c2b2, e4a85d134, 076ad3036, befcac3e1.
# The workflow now prepares the move before publication. Explicit repair mode retains
# the old catch-up capability if a release somehow publishes without the pre-move.
#
# WHAT THIS DOES NOT DO. It does not push to `dev`. It opens a pull request and a human
# merges it, because ruleset `Protect dev` requires an approving review and code-owner
# sign-off that a bot cannot supply. `release.yml` independently refuses publication
# until `dev` already outranks the intended version.
#
# WHY THIS IS DISPATCHED. The intended version is known before publication, and this
# workflow's purpose is to queue the reviewed `dev` move first. It is not called by the
# release workflow after an irreversible publish, and it does not react to release events.
#
# A branch-selected dispatch executes that branch's workflow body with write permission.
# The in-job guard therefore rejects accidental non-default-ref dispatches. It is an early
# warning, not a security boundary: a writer could remove it on their branch. Protected
# release branches and the required review on `dev` remain the enforcement boundaries.
on:
workflow_dispatch:
inputs:
intended-version:
description: "Version about to be released (pre-move), or one already published (repair)"
required: true
type: string
mode:
description: "pre-move (default) or repair — repair allows an already-published version"
required: false
default: pre-move
type: choice
options:
- pre-move
- repair
permissions: {}
concurrency:
group: dev-version-bump
cancel-in-progress: false
jobs:
open-bump-pr:
runs-on: ubuntu-latest
permissions:
# Push the new codex/dev-version-* branch. Ruleset `Protect dev` covers only
# refs/heads/dev, so the bump branch is unprotected and this token cannot
# bypass dev review. It is the ruleset that keeps this job off dev, not the
# permission name.
contents: write
# Open the pull request.
pull-requests: write
steps:
# Keep every executable file in the privileged job pinned to the audited
# release revision. The later dev checkout is input data only: none of its
# actions, dependencies, scripts, or tests run with this job's token.
- name: Checkout trusted automation
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
ref: ${{ github.sha }}
# Tags are load-bearing, not decoration: the freeness gate below is a bun
# test that reads the local tag set, and release-version-line.test.ts
# returns EARLY on an empty set. A shallow checkout would make that gate
# silently vacuous instead of failing loudly.
fetch-depth: 0
persist-credentials: false
- name: Checkout dev as data
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
ref: dev
path: dev-tree
fetch-depth: 0
persist-credentials: false
# The repository-owned composite action, not a hand-pinned setup-bun SHA: it
# selects package.json's test-runner pin even for this single-file test so
# validation follows the CI runner contract (see setup-project-bun/action.yml).
# This job edits version metadata; it does not compile a shipped runtime.
- name: Setup project Bun
uses: ./.github/actions/setup-project-bun
with:
role: test-runner
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Refuse a dispatch from a non-default ref
run: |
test "$GITHUB_REF" = "refs/heads/${{ github.event.repository.default_branch }}" || {
echo "::error::this workflow may only be dispatched from the default branch"
exit 1
}
- name: Resolve the target version
id: target
env:
INTENDED: ${{ inputs.intended-version }}
MODE: ${{ inputs.mode }}
run: |
set -euo pipefail
target="${INTENDED:-}"
if [ -z "$target" ]; then
echo "::error::intended-version was not supplied"
exit 1
fi
echo "version=${target}" >> "$GITHUB_OUTPUT"
if [ "${MODE:-pre-move}" = "repair" ]; then
echo "mode=repair" >> "$GITHUB_OUTPUT"
else
echo "mode=pre-move" >> "$GITHUB_OUTPUT"
fi
- name: Decide the version dev should carry
id: decide
env:
RELEASED_VERSION: ${{ steps.target.outputs.version }}
run: |
set -euo pipefail
bun scripts/bump-dev-version.ts "${RELEASED_VERSION}" dev-tree/package.json
- name: Prove the intended version is not already released
if: ${{ steps.target.outputs.mode == 'pre-move' }}
env:
INTENDED: ${{ steps.target.outputs.version }}
run: |
set -euo pipefail
git fetch --force --tags origin
if git rev-parse -q --verify "refs/tags/v${INTENDED#v}" >/dev/null; then
echo "::error::v${INTENDED#v} already exists; this is a catch-up, not a pre-move"
exit 1
fi
if npm view "@bitkyc08/opencodex@${INTENDED#v}" version >/dev/null 2>&1; then
echo "::error::${INTENDED#v} is already on npm"
exit 1
fi
- name: Prove the chosen version is unused
if: ${{ steps.decide.outputs.changed == 'true' }}
env:
NEXT_VERSION: ${{ steps.decide.outputs.version }}
# The script decides the candidate from the target version SHAPE, which is all
# a pure function can see. Whether that candidate is actually FREE is a property
# of the tag set, so it is settled here by the detector that already owns the
# question. If this fails, no pull request is opened and the job goes red asking
# for a human decision - which is the correct outcome, not a fallback.
#
# The release-commit exception does not apply here. release-version-line.test.ts
# lets an in-tree version equal the highest tag when that tag names HEAD, which
# is correct on the release commit itself. In this job HEAD is the workflow's own
# main checkout while only package.json came from dev, so a chosen version that
# already carries a v tag would pass the shared detector and open a pull request
# claiming a published version. Refuse that tag explicitly first.
run: |
set -euo pipefail
git fetch --force --tags origin
if git rev-parse -q --verify "refs/tags/v${NEXT_VERSION#v}" >/dev/null; then
echo "::error::v${NEXT_VERSION#v} already exists; the chosen version needs a human decision"
exit 1
fi
# Exercise the trusted detector against the candidate package metadata.
cp dev-tree/package.json package.json
bun test tests/ci-workflows/release-version-line.test.ts
- name: Open the bump pull request
if: ${{ steps.decide.outputs.changed == 'true' }}
working-directory: dev-tree
env:
GH_TOKEN: ${{ github.token }}
MODE: ${{ steps.target.outputs.mode }}
NEXT_VERSION: ${{ steps.decide.outputs.version }}
TARGET_VERSION: ${{ steps.target.outputs.version }}
run: |
set -euo pipefail
branch="codex/dev-version-${NEXT_VERSION}"
if [ "${MODE}" = "repair" ]; then
subject="fix(release): move dev to ${NEXT_VERSION} after ${TARGET_VERSION}"
reason="\`${TARGET_VERSION}\` has published, so \`dev\` is carrying a version at or behind a released one and \`tests/ci-workflows/release-version-line.test.ts\` fails on \`dev\` and on every pull request opened against it. This is the post-publish repair."
freeness="\`bun test tests/ci-workflows/release-version-line.test.ts\` proved the chosen development version is unused."
else
subject="chore(release): open dev at ${NEXT_VERSION} before releasing ${TARGET_VERSION}"
reason="\`${TARGET_VERSION}\` is about to be released. Merging this first means \`dev\` already outranks the new tag when it lands, so neither \`dev\` nor any open pull request ever inherits the version-line failure. \`release.yml\` refuses to publish until this has merged."
freeness="The workflow proved \`${TARGET_VERSION}\` has neither a Git tag nor an npm publication, and \`bun test tests/ci-workflows/release-version-line.test.ts\` proved the chosen development version is unused."
fi
# Idempotent: a repeated dispatch, a re-run, or a manual repair must not turn
# an already-queued version move into a red job.
#
# Check the PULL REQUEST as well as the branch, not just the branch. A security
# review caught that: an open bump pull request whose head branch was deleted
# leaves the branch check passing, so the job would recreate the branch and then
# fail on `gh pr create` with "already exists" — turning a successful run red
# for a move that was already queued.
# Apply the repository owner and branch filter on the server. Filtering a
# paginated `gh pr list` result locally can miss this repository's pull request
# when newer same-named fork pull requests fill the fetched page (#3325).
open_prs="$(
gh api --method GET "repos/${GITHUB_REPOSITORY}/pulls" \
-f state=open \
-f base=dev \
-f "head=${GITHUB_REPOSITORY_OWNER}:${branch}" \
-F per_page=1 \
--jq 'length'
)"
if [ "${open_prs}" != "0" ]; then
echo "::notice::a bump pull request for ${branch} is already open; nothing to do"
exit 0
fi
# An existing branch is NOT terminal. If a previous run pushed the branch and then
# failed at `gh pr create`, exiting here would leave the move permanently unqueued
# while every rerun reports success - the exact failure mode a reviewer caught. So
# reuse the branch and fall through to pull-request creation instead.
if git ls-remote --exit-code --heads origin "${branch}" >/dev/null 2>&1; then
echo "::notice::${branch} exists without an open pull request; validating it"
git fetch origin "${branch}"
# Fail closed on unexpected content. The branch carries the bot's own version move,
# which touches only the four version sources that scripts/release-version-sources.ts
# owns, and always package.json. Anything else on it means a human or another job is
# using that name and this job must not push to it or open a pull request from it.
# --no-renames lists a rename as its deletion plus its addition, so a file renamed onto
# an allowed name still shows the path it removed. The case arms are literal paths.
changed_files="$(git diff --no-renames --name-only "origin/dev...origin/${branch}")"
touches_package_json=false
while IFS= read -r changed_file; do
case "${changed_file}" in
package.json) touches_package_json=true ;;
desktop/src-tauri/tauri.conf.json|desktop/src-tauri/Cargo.toml|desktop/src-tauri/Cargo.lock) ;;
*)
echo "::error::${branch} touches unexpected files: ${changed_files:-<none>}"
exit 1
;;
esac
done <<< "${changed_files}"
if [ "${touches_package_json}" != "true" ]; then
echo "::error::${branch} does not move package.json: ${changed_files:-<none>}"
exit 1
fi
# The decide step already rewrote the version sources in this working tree. Put them
# back first: git refuses to switch over local edits, and the check below must read
# what the branch commits, not what this run wrote.
git checkout -- package.json desktop/src-tauri/tauri.conf.json desktop/src-tauri/Cargo.toml desktop/src-tauri/Cargo.lock
git checkout -B "${branch}" "origin/${branch}"
# The diff above proved every other file matches the merge base with dev, so this is
# the merge base's checker reading the branch's four committed version sources.
(cd .. && env -u GH_TOKEN bun scripts/release-version-sources.ts check "${NEXT_VERSION}" --root dev-tree) || {
echo "::error::${branch} does not carry ${NEXT_VERSION} in every version source"
exit 1
}
else
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git checkout -b "${branch}"
# The trusted checkout's checker reads this tree's four version sources.
(cd .. && env -u GH_TOKEN bun scripts/release-version-sources.ts check "${NEXT_VERSION}" --root dev-tree) || {
echo "::error::the bump did not move every version source to ${NEXT_VERSION}"
exit 1
}
git add -- package.json desktop/src-tauri/tauri.conf.json desktop/src-tauri/Cargo.toml desktop/src-tauri/Cargo.lock
git commit -m "${subject}"
# Supply the write credential only to this trusted push invocation. In
# particular, never persist it in the dev checkout while dev-controlled
# files could execute.
auth_header="$(printf 'x-access-token:%s' "${GH_TOKEN}" | base64 -w0)"
# The raw token is masked automatically; its base64 form is not. Mask it so a
# verbose git or curl trace cannot leak a usable credential into the log.
echo "::add-mask::${auth_header}"
git -c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth_header}" push origin "${branch}"
fi
gh pr create \
--base dev \
--head "${branch}" \
--title "${subject}" \
--body "$(cat <<BODY
## Summary
${reason}
This moves \`dev\` to \`${NEXT_VERSION}\` in \`package.json\` and the desktop version
sources (\`tauri.conf.json\`, \`Cargo.toml\`, and the \`opencodex-desktop\` entry of \`Cargo.lock\`).
Opened automatically by \`.github/workflows/dev-version-bump.yml\`. The same
version-line move was previously done by hand in 32529c2b2, e4a85d134, 076ad3036, and
befcac3e1.
## Verification
${freeness}
## Checklist
- [x] Scope stays focused and avoids unrelated cleanup.
- [x] Docs or release notes were updated when needed.
- [x] Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults.
BODY
)"