Skip to content

Commit 0904859

Browse files
authored
Release component from CI with Changesets (#81)
1 parent 545b53b commit 0904859

9 files changed

Lines changed: 895 additions & 34 deletions

File tree

‎.changeset/README.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Release notes
2+
3+
See [CONTRIBUTING.md](../CONTRIBUTING.md#releases) for contributing release
4+
notes and preparing a release.

‎.changeset/config.json‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"$schema": "https://unpkg.com/@changesets/config@4.0.1/schema.json",
3+
"baseBranch": "main",
4+
"access": "public",
5+
"changelog": "@changesets/cli/changelog",
6+
"commit": false,
7+
"fixed": [],
8+
"linked": [],
9+
"updateInternalDependencies": "patch",
10+
"ignore": []
11+
}

‎.github/workflows/lint-workflows.yml‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@ jobs:
2525
runs-on: ubuntu-latest
2626
timeout-minutes: 5
2727
steps:
28-
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
28+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
2929
with:
3030
persist-credentials: false
31-
- uses: raven-actions/actionlint@205b530c5d9fa8f44ae9ed59f341a0db994aa6f8 # v2.1.2
31+
- uses: kjanat/actionlint@08bb2c4f0d039744455b87aef1c647fb8b66d37b # v1.17.0
3232

3333
zizmor:
3434
name: zizmor
@@ -37,7 +37,7 @@ jobs:
3737
permissions:
3838
security-events: write
3939
steps:
40-
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
40+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
4141
with:
4242
persist-credentials: false
43-
- uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6
43+
- uses: zizmorcore/zizmor-action@53290badfa369cb0bf7ad16342cb62c2fa47ac7b # v0.8.0

‎.github/workflows/release.yml‎

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
branches: [main]
6+
workflow_dispatch:
7+
inputs:
8+
commit:
9+
description:
10+
"Optional 40-character version-bump commit SHA reachable from the
11+
selected ref"
12+
required: false
13+
type: string
14+
15+
concurrency:
16+
group: ${{ github.workflow }}
17+
queue: max
18+
cancel-in-progress: false
19+
20+
permissions: {}
21+
22+
env:
23+
NODE_VERSION: "24.22.1"
24+
25+
jobs:
26+
plan:
27+
if: github.repository_owner == 'convex'
28+
runs-on: ubuntu-latest
29+
timeout-minutes: 5
30+
permissions:
31+
contents: read
32+
outputs:
33+
sha: ${{ steps.select.outputs.sha }}
34+
name: ${{ steps.select.outputs.name }}
35+
version: ${{ steps.select.outputs.version }}
36+
channel: ${{ steps.select.outputs.channel }}
37+
steps:
38+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
39+
with:
40+
fetch-depth: 0
41+
persist-credentials: false
42+
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
43+
with:
44+
node-version: ${{ env.NODE_VERSION }}
45+
package-manager-cache: false
46+
- id: select
47+
name: Select the latest version-bump commit
48+
env:
49+
RELEASE_COMMIT: ${{ inputs.commit }}
50+
run: |
51+
if [[ "$GITHUB_EVENT_NAME" == workflow_dispatch ]]; then
52+
source="${RELEASE_COMMIT:-$GITHUB_SHA}"
53+
[[ "$source" =~ ^[0-9a-f]{40}$ ]] || { echo 'Expected a 40-character commit SHA' >&2; exit 1; }
54+
git merge-base --is-ancestor "$source" "$GITHUB_SHA"
55+
else
56+
before=$(jq -r .before "$GITHUB_EVENT_PATH")
57+
source=''
58+
candidates=$(git rev-list --first-parent "$before..$GITHUB_SHA" -- package.json)
59+
while read -r candidate && [[ -n "$candidate" ]]; do
60+
current=$(git show "$candidate:package.json" | jq -r .version)
61+
previous=$(git show "$candidate^1:package.json" | jq -r .version)
62+
if [[ "$current" != "$previous" ]]; then source="$candidate"; break; fi
63+
done <<< "$candidates"
64+
if [[ -z "$source" ]]; then echo 'No version bump'; exit 0; fi
65+
fi
66+
name=$(git show "$source:package.json" | jq -r .name)
67+
VERSION=$(git show "$source:package.json" | jq -r .version)
68+
PREVIOUS_VERSION=$(git show "$source^1:package.json" | jq -r .version)
69+
export VERSION PREVIOUS_VERSION
70+
node --input-type=module <<'NODE'
71+
import { execFileSync } from 'node:child_process';
72+
import { appendFileSync } from 'node:fs';
73+
import { createRequire } from 'node:module';
74+
// Use the standard SemVer library bundled with the pinned npm.
75+
const npmRoot = execFileSync('npm', ['root', '-g'], { encoding: 'utf8' }).trim();
76+
const semver = createRequire(`${npmRoot}/npm/package.json`)('semver');
77+
const { VERSION: version, PREVIOUS_VERSION: previous, GITHUB_REF: ref } = process.env;
78+
if (semver.valid(version) !== version || !semver.gt(version, previous)) throw new Error('Selected commit must increase the package version');
79+
const pre = semver.prerelease(version);
80+
const channel = pre ? String(pre[0]) : 'latest';
81+
if (pre && (!/^[a-z][a-z0-9-]*$/.test(channel) || channel === 'latest' || semver.validRange(channel))) throw new Error('Invalid prerelease channel');
82+
if (!pre && ref !== 'refs/heads/main') throw new Error('Stable releases require main');
83+
appendFileSync(process.env.GITHUB_OUTPUT, `channel=${channel}\n`);
84+
NODE
85+
tag="v$VERSION"
86+
if git show-ref --verify --quiet "refs/tags/$tag"; then
87+
if [[ "$GITHUB_EVENT_NAME" != workflow_dispatch ]]; then
88+
echo "$tag was already staged; skipping automatic release"
89+
exit 0
90+
fi
91+
[[ "$(git rev-parse "$tag^{commit}")" == "$source" ]] || { echo "$tag points elsewhere" >&2; exit 1; }
92+
fi
93+
{
94+
echo "sha=$source"
95+
echo "name=$name"
96+
echo "version=$VERSION"
97+
} >> "$GITHUB_OUTPUT"
98+
99+
build:
100+
needs: plan
101+
if: needs.plan.outputs.sha != ''
102+
runs-on: ubuntu-latest
103+
timeout-minutes: 30
104+
permissions:
105+
contents: read
106+
outputs:
107+
artifact_id: ${{ steps.upload.outputs.artifact-id }}
108+
sha256: ${{ steps.digest.outputs.sha256 }}
109+
steps:
110+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
111+
with:
112+
ref: ${{ needs.plan.outputs.sha }}
113+
persist-credentials: false
114+
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
115+
with:
116+
node-version: ${{ env.NODE_VERSION }}
117+
package-manager-cache: false
118+
- run: npm ci
119+
- name: Validate release notes
120+
run: |
121+
# Compare against this checkout while reading all notes, including prerelease state.
122+
git update-ref refs/heads/main HEAD
123+
npx changeset status --output "$RUNNER_TEMP/changesets.json"
124+
jq -e '.changesets | length == 0' "$RUNNER_TEMP/changesets.json"
125+
version=$(jq -r .version package.json)
126+
awk -v version="$version" '/^## / { active=($0 == "## " version); next } active && !/^#/ && /[[:alnum:]]/ { found=1 } END { exit !found }' CHANGELOG.md
127+
- run: npm run build
128+
- run: npm run check
129+
- name: Run E2E tests and verify generated code
130+
env:
131+
CONVEX_AGENT_MODE: anonymous
132+
CONVEX_DISABLE_TELEMETRY: "1"
133+
run: |
134+
npm run test:e2e
135+
git diff --exit-code -- src/component/_generated
136+
test -z "$(git ls-files --others --exclude-standard src/component/_generated)"
137+
- name: Pack the tested package
138+
run: |
139+
mkdir -p "$RUNNER_TEMP/npm-release"
140+
npm pack --ignore-scripts --pack-destination "$RUNNER_TEMP/npm-release"
141+
- id: digest
142+
run:
143+
echo "sha256=$(sha256sum "$RUNNER_TEMP"/npm-release/*.tgz | cut -d ' '
144+
-f 1)" >> "$GITHUB_OUTPUT"
145+
- id: upload
146+
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
147+
with:
148+
name: npm-release-${{ needs.plan.outputs.sha }}
149+
path: ${{ runner.temp }}/npm-release/*.tgz
150+
if-no-files-found: error
151+
152+
stage:
153+
needs: [plan, build]
154+
runs-on: ubuntu-latest
155+
timeout-minutes: 10
156+
permissions:
157+
contents: read
158+
id-token: write
159+
steps:
160+
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
161+
with:
162+
node-version: ${{ env.NODE_VERSION }}
163+
package-manager-cache: false
164+
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8
165+
with:
166+
artifact-ids: ${{ needs.build.outputs.artifact_id }}
167+
path: ${{ runner.temp }}/npm-release
168+
merge-multiple: true
169+
- name: Stage the tested artifact
170+
env:
171+
SOURCE_SHA: ${{ needs.plan.outputs.sha }}
172+
PACKAGE_NAME: ${{ needs.plan.outputs.name }}
173+
VERSION: ${{ needs.plan.outputs.version }}
174+
CHANNEL: ${{ needs.plan.outputs.channel }}
175+
EXPECTED_SHA256: ${{ needs.build.outputs.sha256 }}
176+
run: |
177+
set -o pipefail
178+
set -- "$RUNNER_TEMP"/npm-release/*.tgz
179+
test "$#" -eq 1
180+
TARBALL="$1"
181+
echo "$EXPECTED_SHA256 $TARBALL" | sha256sum -c -
182+
{
183+
echo "$PACKAGE_NAME@$VERSION — source $SOURCE_SHA — npm channel $CHANNEL"
184+
echo "Review on npm's Staged Packages page, then approve with 2FA or npm stage approve <stage-id>."
185+
} >> "$GITHUB_STEP_SUMMARY"
186+
# npm reads GITHUB_SHA for provenance; checkout alone does not change it.
187+
GITHUB_SHA="$SOURCE_SHA" npm stage publish "$TARBALL" \
188+
--ignore-scripts --provenance --access public --tag "$CHANNEL" \
189+
2>&1 | tee "$RUNNER_TEMP/npm-stage.log"
190+
- name: Preserve staging output and stage ID
191+
if: always()
192+
run: |
193+
if test -f "$RUNNER_TEMP/npm-stage.log"; then
194+
cat "$RUNNER_TEMP/npm-stage.log" >> "$GITHUB_STEP_SUMMARY"
195+
fi
196+
197+
tag:
198+
needs: [plan, stage]
199+
runs-on: ubuntu-latest
200+
timeout-minutes: 5
201+
permissions:
202+
contents: write
203+
steps:
204+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
205+
with:
206+
ref: ${{ needs.plan.outputs.sha }}
207+
fetch-depth: 0
208+
persist-credentials: false
209+
- name: Tag the staged source
210+
env:
211+
GH_TOKEN: ${{ github.token }}
212+
SOURCE_SHA: ${{ needs.plan.outputs.sha }}
213+
VERSION: ${{ needs.plan.outputs.version }}
214+
run: |
215+
tag="v$VERSION"
216+
if git show-ref --verify --quiet "refs/tags/$tag"; then
217+
test "$(git rev-parse "$tag^{commit}")" = "$SOURCE_SHA"
218+
else
219+
git tag "$tag" "$SOURCE_SHA"
220+
gh auth setup-git
221+
git push origin "refs/tags/$tag"
222+
fi

‎.github/workflows/test.yml‎

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,29 +16,48 @@ jobs:
1616
name: Test and lint
1717
runs-on: ubuntu-latest
1818
timeout-minutes: 30
19+
permissions:
20+
contents: read
1921

2022
steps:
21-
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
23+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
2224
with:
25+
fetch-depth: 0
2326
persist-credentials: false
2427

2528
- name: Node setup
26-
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
29+
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
2730
with:
2831
cache-dependency-path: package.json
29-
node-version: "24.x"
32+
node-version: "24.22.1"
3033
cache: "npm"
3134

3235
- name: Install and build
3336
run: |
34-
npm i
37+
npm ci
3538
npm run build
36-
- name: Publish package for testing branch
37-
run:
38-
npx pkg-pr-new publish || echo "Have you set up pkg-pr-new for this
39-
repo?"
39+
- name: Validate release notes when the proposed merge changes the version
40+
env:
41+
BASE_SHA:
42+
${{ github.event.pull_request.base.sha || github.event.before }}
43+
run: |
44+
version=$(jq -r .version package.json)
45+
previous=$(git show "$BASE_SHA:package.json" | jq -r .version)
46+
if [[ "$version" != "$previous" ]]; then
47+
git update-ref refs/heads/main HEAD
48+
npx changeset status --output "$RUNNER_TEMP/changesets.json"
49+
jq -e '.changesets | length == 0' "$RUNNER_TEMP/changesets.json"
50+
awk -v version="$version" '/^## / { active=($0 == "## " version); next } active && !/^#/ && /[[:alnum:]]/ { found=1 } END { exit !found }' CHANGELOG.md
51+
fi
4052
- name: Test
53+
run: npm run check
54+
- name: Run E2E smoke test and verify generated code
55+
env:
56+
CONVEX_AGENT_MODE: anonymous
57+
CONVEX_DISABLE_TELEMETRY: "1"
4158
run: |
42-
npm run test
43-
npm run typecheck
44-
npm run lint
59+
npm run test:e2e
60+
git diff --exit-code -- src/component/_generated
61+
test -z "$(git ls-files --others --exclude-standard src/component/_generated)"
62+
- name: Publish package for testing branch
63+
run: npx pkg-pr-new publish || echo "pkg-pr-new failed"

‎.github/workflows/version.yml‎

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
name: Update release PR
2+
3+
concurrency:
4+
group: ${{ github.workflow }}
5+
cancel-in-progress: false
6+
7+
on:
8+
push:
9+
branches: [main]
10+
workflow_dispatch:
11+
12+
permissions: {}
13+
14+
jobs:
15+
version:
16+
name: Update release PR
17+
if: github.repository_owner == 'convex' && github.ref == 'refs/heads/main'
18+
runs-on: ubuntu-latest
19+
timeout-minutes: 30
20+
permissions:
21+
contents: write
22+
pull-requests: write
23+
steps:
24+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
25+
with:
26+
ref: ${{ github.sha }}
27+
fetch-depth: 0
28+
persist-credentials: false
29+
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
30+
with:
31+
node-version: "24.22.1"
32+
package-manager-cache: false
33+
- run: npm ci --ignore-scripts
34+
- name: Check whether a version PR is needed
35+
id: mode
36+
uses: changesets/action/select-mode@ae32849d5ba541f9ae29e40e22a623bc13562f51 # v2.1.2
37+
- name: Check main is still at this run's commit
38+
if: steps.mode.outputs.mode == 'version'
39+
id: freshness
40+
env:
41+
GH_TOKEN: ${{ github.token }}
42+
run: |
43+
main_sha="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/heads/main" --jq '.object.sha')"
44+
if [[ "$main_sha" == "$GITHUB_SHA" ]]; then
45+
echo "current=true" >> "$GITHUB_OUTPUT"
46+
else
47+
echo "current=false" >> "$GITHUB_OUTPUT"
48+
echo "Skipping release PR update: main has moved beyond this run's commit."
49+
fi
50+
- name: Create or update the release PR
51+
if:
52+
steps.mode.outputs.mode == 'version' &&
53+
steps.freshness.outputs.current == 'true'
54+
uses: changesets/action/version@ae32849d5ba541f9ae29e40e22a623bc13562f51 # v2.1.2
55+
with:
56+
github-token: ${{ github.token }}
57+
pr-title: "Prepare next release"
58+
commit-message: "Version packages"
59+
pr-base-branch: main

0 commit comments

Comments
 (0)