Skip to content

Commit fdad55e

Browse files
committed
feat: let the release pick its own version
Preflight computes the release version instead of requiring a manual bump: a patch bump past the highest of the latest v* tag and the npm-published version, or cli/package.json's version when a PR raised it higher (minor/ major bumps). The version threads into publish-cli as a workflow input and is applied with npm version before publishing. Basing the bump on npm as well as tags means a rerun after a half-finished release always advances past the orphaned npm version, so a tag can never point at code the published tarball does not contain.
1 parent fa99a75 commit fdad55e

4 files changed

Lines changed: 47 additions & 15 deletions

File tree

.github/workflows/publish-cli.yml

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ on:
77
description: Commit SHA whose published images the package should pin
88
required: false
99
type: string
10+
version:
11+
description: Version to publish, overriding cli/package.json
12+
required: false
13+
type: string
1014
outputs:
1115
manifest:
1216
description: The image manifest the published package pins
@@ -16,6 +20,9 @@ on:
1620
images_ref:
1721
description: Commit SHA whose published images the package should pin
1822
required: false
23+
version:
24+
description: Version to publish, overriding cli/package.json
25+
required: false
1926

2027
permissions:
2128
contents: read
@@ -77,8 +84,14 @@ jobs:
7784
printf 'manifest=%s\n' "$(jq -c . cli/manifest.json)" >> "$GITHUB_OUTPUT"
7885
- name: Publish
7986
working-directory: cli
87+
env:
88+
VERSION: ${{ inputs.version }}
89+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
8090
run: |
8191
set -euo pipefail
92+
if [ -n "$VERSION" ]; then
93+
npm version "$VERSION" --no-git-tag-version --allow-same-version > /dev/null
94+
fi
8295
version=$(jq -r .version package.json)
8396
if npm view "@yc-software/qm@$version" version > /dev/null 2>&1; then
8497
published=$(mktemp -d)
@@ -95,5 +108,3 @@ jobs:
95108
if [ "$version" = 0.1.5 ]; then
96109
npm deprecate @yc-software/qm@1.0.5 "Published with an incorrect version number; use 0.1.5."
97110
fi
98-
env:
99-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

.github/workflows/release.yml

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ jobs:
1515
contents: read
1616
outputs:
1717
tag: ${{ steps.version.outputs.tag }}
18+
version: ${{ steps.version.outputs.version }}
1819
steps:
1920
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
2021
with:
@@ -29,17 +30,27 @@ jobs:
2930
echo "releases are cut from main; this run is on $GITHUB_REF" >&2
3031
exit 1
3132
fi
32-
version=$(jq -r .version cli/package.json)
33-
case "$version" in
33+
pkg=$(jq -r .version cli/package.json)
34+
case "$pkg" in
3435
[0-9]*.[0-9]*.[0-9]*) ;;
35-
*) echo "cli/package.json version must be semver, got $version" >&2; exit 1 ;;
36+
*) echo "cli/package.json version must be semver, got $pkg" >&2; exit 1 ;;
3637
esac
38+
tagged=$(gh api --paginate "repos/$GITHUB_REPOSITORY/git/matching-refs/tags/v" -q '.[].ref' \
39+
| sed 's|^refs/tags/||' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sed 's/^v//' | sort -V | tail -1 || true)
40+
published=$(npm view @yc-software/qm version 2>/dev/null || true)
41+
base=$(printf '%s\n%s\n' "${tagged:-0.0.0}" "${published:-0.0.0}" | sort -V | tail -1)
42+
if [ "$(printf '%s\n%s\n' "$base" "$pkg" | sort -V | tail -1)" = "$pkg" ] && [ "$pkg" != "$base" ]; then
43+
version=$pkg
44+
else
45+
IFS=. read -r major minor patch <<< "$base"
46+
version="$major.$minor.$((patch + 1))"
47+
fi
3748
tag="v$version"
3849
if gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$tag" > /dev/null 2>&1; then
39-
echo "$tag is already released; bump cli/package.json before releasing again" >&2
50+
echo "$tag already exists; refusing to move it" >&2
4051
exit 1
4152
fi
42-
printf 'tag=%s\n' "$tag" >> "$GITHUB_OUTPUT"
53+
printf 'tag=%s\nversion=%s\n' "$tag" "$version" >> "$GITHUB_OUTPUT"
4354
4455
images:
4556
name: Images
@@ -52,12 +63,16 @@ jobs:
5263

5364
cli:
5465
name: CLI
55-
needs: images
66+
needs:
67+
- preflight
68+
- images
5669
permissions:
5770
contents: read
5871
id-token: write
5972
secrets: inherit
6073
uses: ./.github/workflows/publish-cli.yml
74+
with:
75+
version: ${{ needs.preflight.outputs.version }}
6176

6277
release:
6378
name: Tag and publish the release

cli/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ This package is published to npm as `@yc-software/qm`, with npm provenance attes
2222
building workflow. A release is one dispatch of `.github/workflows/release.yml` from
2323
`main`: it signs and pushes the first-party images, publishes the package pinning their
2424
digests, and then tags `v<version>` and creates the GitHub release with the resolved
25-
digests attached. The version comes from `cli/package.json`, bumped in a PR before dispatching a release
26-
rather than by every pull request that touches the package; a tag that already exists
27-
stops the release rather than moving. The checked-in image manifest is a sentinel that
25+
digests attached. Each release picks its own version: a patch bump past the latest released version, or
26+
`cli/package.json`'s version when a PR raised it higher (for a minor or major bump); a
27+
tag that already exists stops the release rather than moving. The checked-in image manifest is a sentinel that
2828
a deployment overrides with real digests. The packed-artifact test exercises the consumer
2929
path locally.
3030

test/release-workflows.test.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,18 +126,24 @@ test("one dispatchable workflow drives the whole release, main-only and in order
126126
workflow,
127127
/^ {2}images:\n[\s\S]*?needs: preflight\n[\s\S]*?uses: \.\/\.github\/workflows\/release-package\.yml$/m,
128128
);
129-
assert.match(workflow, /^ {2}cli:\n[\s\S]*?needs: images\n[\s\S]*?uses: \.\/\.github\/workflows\/publish-cli\.yml$/m);
129+
assert.match(
130+
workflow,
131+
/^ {2}cli:\n[\s\S]*?needs:\n {6}- preflight\n {6}- images\n[\s\S]*?uses: \.\/\.github\/workflows\/publish-cli\.yml\n {4}with:\n {6}version: \$\{\{ needs\.preflight\.outputs\.version \}\}$/m,
132+
);
130133
assert.match(workflow, /^ {2}release:\n[\s\S]*?needs:\n {6}- preflight\n {6}- cli$/m);
131134
assert.match(workflow, /concurrency:\n {2}group: release\n {2}cancel-in-progress: false/);
132135
});
133136

134-
test("the release refuses a tag it already published and writes the tag last", () => {
137+
test("the release bumps its own version past everything already released", () => {
135138
const workflow = readFileSync(".github/workflows/release.yml", "utf8");
136139

137-
assert.match(workflow, /version=\$\(jq -r \.version cli\/package\.json\)/);
140+
assert.match(workflow, /pkg=\$\(jq -r \.version cli\/package\.json\)/);
138141
assert.match(workflow, /cli\/package\.json version must be semver/);
142+
assert.match(workflow, /matching-refs\/tags\/v/);
143+
assert.match(workflow, /npm view @yc-software\/qm version/);
144+
assert.match(workflow, /version="\$major\.\$minor\.\$\(\(patch \+ 1\)\)"/);
139145
assert.match(workflow, /tag="v\$version"/);
140-
assert.match(workflow, /is already released; bump cli\/package\.json before releasing again/);
146+
assert.match(workflow, /already exists; refusing to move it/);
141147
assert.ok(
142148
workflow.indexOf("already released") < workflow.indexOf("gh release create"),
143149
"the tag gate runs before anything is published",

0 commit comments

Comments
 (0)