Skip to content

Commit 03bbb5f

Browse files
authored
ci: add release workflow for automated CLI releases (#403)
## Summary - Adds `.github/workflows/release.yml` that automates CLI releases end-to-end - Triggered by `cli-v*` tag push: validates tag is on main, builds CLI tarball in Docker, publishes to npm via OIDC trusted publishing, creates GitHub Release with tarball + changelog ## Tested Temporarily triggered the workflow on push (dry-run mode) — all steps passed: - Docker build, tarball extraction, SHA256 computation - npm ci + wasm build - Changelog extraction - `npm publish --dry-run` ## How to use ```bash # After merging a release PR: git tag cli-v<version> git push origin cli-v<version> ``` ## Key decisions - **OIDC trusted publishing** instead of NPM_TOKEN — no secrets to manage - **Single job** for simplicity (can split build/publish later if needed) - All `${{ }}` expressions passed through `env:` to prevent expression injection - npm pinned to 11.4.0 (minimum for trusted publishing) - `--ignore-scripts` on `npm publish` avoids redundant `prepare` run - Changelog extraction via awk (avoids tsx/node dependency for a simple text extraction)
1 parent 025b633 commit 03bbb5f

2 files changed

Lines changed: 178 additions & 65 deletions

File tree

.github/workflows/release.yml

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
name: Release CLI
2+
3+
on:
4+
push:
5+
tags:
6+
- "cli-v*"
7+
8+
permissions:
9+
contents: write
10+
id-token: write
11+
12+
concurrency:
13+
group: release-cli
14+
cancel-in-progress: false
15+
16+
jobs:
17+
release:
18+
runs-on: ubuntu-22.04
19+
timeout-minutes: 30
20+
steps:
21+
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
22+
with:
23+
fetch-depth: 0
24+
25+
- name: Abort if tag is not on main
26+
run: |
27+
set -euo pipefail
28+
git fetch origin main
29+
if git merge-base --is-ancestor "${GITHUB_SHA}" "origin/main"; then
30+
echo "Tag commit ${GITHUB_SHA} is reachable from main."
31+
else
32+
echo "::error::Tag commit ${GITHUB_SHA} is not on main. Aborting."
33+
exit 1
34+
fi
35+
36+
- name: Extract version from tag
37+
id: version
38+
run: |
39+
VERSION="${GITHUB_REF_NAME#cli-v}"
40+
if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$'; then
41+
echo "::error::Could not extract a valid version from tag '${GITHUB_REF_NAME}'"
42+
exit 1
43+
fi
44+
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
45+
echo "Version: $VERSION"
46+
47+
- name: Validate version matches package.json
48+
working-directory: cli
49+
env:
50+
TAG_VERSION: ${{ steps.version.outputs.version }}
51+
run: |
52+
PKG_VERSION=$(node -p "require('./package.json').version")
53+
if [ "$PKG_VERSION" != "$TAG_VERSION" ]; then
54+
echo "::error::Tag version ($TAG_VERSION) does not match package.json ($PKG_VERSION)"
55+
exit 1
56+
fi
57+
58+
# --- Docker build ---
59+
60+
- name: Set up Docker
61+
uses: docker/setup-docker-action@efe9e3891a4f7307e689f2100b33a155b900a608 # v4.5.0
62+
63+
- name: Build CLI tarball using Docker
64+
working-directory: cli
65+
env:
66+
COMMIT_HASH: ${{ github.sha }}
67+
MOPS_VERSION: ${{ steps.version.outputs.version }}
68+
run: |
69+
docker build . \
70+
--build-arg COMMIT_HASH="$COMMIT_HASH" \
71+
--build-arg MOPS_VERSION="$MOPS_VERSION" \
72+
-t mops
73+
74+
- name: Extract tarball from container
75+
working-directory: cli
76+
run: |
77+
cid=$(docker create mops)
78+
mkdir -p bundle
79+
docker cp "$cid:/mops/cli/bundle/cli.tgz" ./bundle/cli.tgz
80+
docker rm "$cid"
81+
82+
- name: Compute SHA256
83+
id: hash
84+
working-directory: cli
85+
env:
86+
VERSION: ${{ steps.version.outputs.version }}
87+
run: |
88+
HASH=$(sha256sum bundle/cli.tgz | awk '{print $1}')
89+
echo "hash=$HASH" >> "$GITHUB_OUTPUT"
90+
echo "### CLI Release v${VERSION}" >> "$GITHUB_STEP_SUMMARY"
91+
echo "**SHA256:** \`$HASH\`" >> "$GITHUB_STEP_SUMMARY"
92+
93+
# --- npm publish ---
94+
95+
- name: Set up Node.js
96+
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
97+
with:
98+
node-version: 22
99+
registry-url: "https://registry.npmjs.org"
100+
101+
- name: Upgrade npm for trusted publishing
102+
run: npm install -g npm@11.4.0
103+
104+
- uses: ./.github/actions/cache-rust-wasm
105+
106+
- name: Install dependencies
107+
working-directory: cli
108+
run: npm ci
109+
110+
- name: Extract changelog
111+
working-directory: cli
112+
env:
113+
VERSION: ${{ steps.version.outputs.version }}
114+
BUILD_HASH: ${{ steps.hash.outputs.hash }}
115+
COMMIT_HASH: ${{ github.sha }}
116+
run: |
117+
ESCAPED=$(echo "$VERSION" | sed 's/\./\\./g')
118+
awk "/^##? ${ESCAPED}$/{found=1; next} found && /^##? [0-9]/{exit} found && /^##? Next/{exit} found" \
119+
CHANGELOG.md > /tmp/release-notes.md
120+
121+
if [ ! -s /tmp/release-notes.md ]; then
122+
echo "::error::No changelog entry found for version ${VERSION}"
123+
exit 1
124+
fi
125+
126+
{
127+
echo ""
128+
echo "---"
129+
echo "**SHA256:** \`${BUILD_HASH}\`"
130+
echo ""
131+
echo "**Verify build:**"
132+
echo '```bash'
133+
echo "cd cli"
134+
echo "docker build . --build-arg COMMIT_HASH=${COMMIT_HASH} --build-arg MOPS_VERSION=${VERSION} -t mops"
135+
echo "docker run --rm --env SHASUM=${BUILD_HASH} mops"
136+
echo '```'
137+
} >> /tmp/release-notes.md
138+
139+
- name: Publish to npm
140+
working-directory: cli
141+
run: npm publish --provenance --ignore-scripts
142+
143+
# --- GitHub Release ---
144+
145+
- name: Create GitHub Release
146+
env:
147+
GH_TOKEN: ${{ github.token }}
148+
TAG: ${{ github.ref_name }}
149+
VERSION: ${{ steps.version.outputs.version }}
150+
run: |
151+
gh release create "$TAG" \
152+
cli/bundle/cli.tgz#cli.tgz \
153+
--title "CLI v${VERSION}" \
154+
--notes-file /tmp/release-notes.md

cli/RELEASE.md

Lines changed: 24 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -2,34 +2,17 @@
22

33
## Prerequisites
44

5-
### macOS: GNU tar
6-
7-
```bash
8-
brew install gnu-tar
9-
```
10-
11-
Add to `~/.zshrc` or `~/.bashrc`:
12-
```bash
13-
export PATH="$HOMEBREW_PREFIX/opt/gnu-tar/libexec/gnubin:$PATH"
14-
```
15-
165
### Docker
176

18-
Docker (or OrbStack) must be installed and running. The CLI build happens inside a Docker container (`zenvoich/mops-builder:1.1.0`) for reproducibility.
7+
Docker (or OrbStack) must be installed and running. The on-chain release step builds the CLI inside a Docker container (`zenvoich/mops-builder:1.1.0`) for reproducibility.
198

209
### dfx
2110

2211
`dfx` must be installed with the `mops` identity configured (see [Adding the `mops` identity to dfx](#adding-the-mops-identity-to-dfx)).
2312

2413
## Release Steps
2514

26-
### 1. Install dependencies
27-
28-
```bash
29-
cd cli && bun install
30-
```
31-
32-
### 2. Update changelog
15+
### 1. Update changelog
3316

3417
Move items from the `## Next` section in `CHANGELOG.md` into a new version heading:
3518

@@ -41,28 +24,19 @@ Move items from the `## Next` section in `CHANGELOG.md` into a new version headi
4124
- Change 2
4225
```
4326

44-
The heading must contain the exact version string — `release-cli.ts` parses it to extract release notes.
45-
46-
### 3. Create a release branch and PR
47-
48-
Direct pushes to `main` are not allowed. Create a branch and PR:
49-
50-
```bash
51-
git checkout -b <username>/release-X.Y.Z
52-
```
53-
54-
### 4. Bump version
27+
The heading must contain the exact version string — the release workflow parses it to extract release notes for the GitHub Release.
5528

56-
Use `--no-git-tag-version` since the version bump will be committed as part of the PR (not directly on `main`):
29+
### 2. Bump version
5730

5831
```bash
5932
cd cli
6033
npm version minor --no-git-tag-version # or: patch / major
6134
```
6235

63-
### 5. Commit, push, and open PR
36+
### 3. Create a release branch and PR
6437

6538
```bash
39+
git checkout -b <username>/release-X.Y.Z
6640
git add cli/CHANGELOG.md cli/package.json cli/package-lock.json
6741
git commit -m "release: CLI vX.Y.Z"
6842
git push -u origin <username>/release-X.Y.Z
@@ -71,41 +45,26 @@ gh pr create --title "release: CLI vX.Y.Z" --body "..."
7145

7246
Wait for CI to pass, then merge the PR.
7347

74-
### 6. Check reproducibility of the build
75-
76-
After the PR is merged to `main`, check the SHA256 hash from the latest [build-hash workflow](https://github.com/caffeinelabs/mops/actions/workflows/build-hash.yml) run.
48+
### 4. Tag and push
7749

78-
**Important**: The CI hash is written to the GitHub Actions **Step Summary** — it is only visible on the **Summary tab** of the workflow run page. It does **not** appear in the downloadable logs (`gh run view --log` will not find it). You must open the run URL in a browser to see it.
50+
After the PR is merged to `main`:
7951

80-
Build the same version locally:
8152
```bash
82-
cd cli
83-
MOPS_VERSION=0.0.0 ./build.sh
53+
git checkout main && git pull
54+
git tag cli-vX.Y.Z
55+
git push origin cli-vX.Y.Z
8456
```
8557

86-
`build.sh` does the following:
87-
1. Reads `COMMIT_HASH` (defaults to `git rev-parse HEAD`) and `MOPS_VERSION`
88-
2. Runs `docker build` using `cli/Dockerfile` with those as build args
89-
3. The Dockerfile clones the repo at that commit, runs `bun install`, sets the version, and runs `npm run build`
90-
4. Prints the SHA256 hash of `cli.tgz`
91-
5. Copies `cli.tgz` out of the container into `cli/bundle/cli.tgz`
58+
This triggers the [`release.yml`](../.github/workflows/release.yml) workflow which automatically:
59+
1. Validates the tag is on `main` and version matches `package.json`
60+
2. Builds the CLI tarball in Docker (reproducible build)
61+
3. Computes and reports the SHA256 hash (visible in the workflow Step Summary)
62+
4. Publishes to npm via OIDC trusted publishing
63+
5. Creates a GitHub Release with the tarball attached and changelog as release notes
9264

93-
Compare the locally printed hash with the one from the CI Step Summary. If they don't match, do not proceed.
94-
95-
**Notes on the local build output:**
96-
- The "Verification failed" message at the end is **expected** — it happens because no `SHASUM` env var is passed for comparison. The important output is the `Actual shasum: <hash>` line.
97-
98-
### 7. Publish to npm
99-
100-
After the PR is merged and the build hash is verified:
101-
102-
```bash
103-
git checkout main && git pull
104-
cd cli
105-
npm publish
106-
```
65+
Monitor the workflow run at [Actions → Release CLI](https://github.com/caffeinelabs/mops/actions/workflows/release.yml).
10766

108-
### 8. Prepare on-chain release
67+
### 5. Prepare on-chain release
10968

11069
Run from the **repo root** (not `cli/`), with Docker running:
11170

@@ -122,25 +81,25 @@ This runs `cli/release-cli.ts`, which:
12281
6. Updates `cli-releases/tags/latest` to the new version
12382
7. Updates `cli-releases/releases.json` with metadata (timestamp, size, hash, commit hash, download URL, release notes)
12483

125-
### 9. Deploy the canister
84+
### 6. Deploy the canister
12685

12786
```bash
12887
dfx deploy --network ic --no-wallet cli --identity mops
12988
```
13089

13190
This deploys the `cli-releases` canister (serving `cli.mops.one`) to the Internet Computer mainnet.
13291

133-
### 10. Deploy the docs canister
92+
### 7. Deploy the docs canister
13493

13594
```bash
13695
dfx deploy --network ic --no-wallet docs --identity mops
13796
```
13897

13998
This builds the Docusaurus site (`docs/`) and deploys the `docs` assets canister (serving `docs.mops.one`). Docs are not auto-deployed, so this step ensures any documentation changes from the release are published.
14099

141-
### 11. Commit and push release artifacts
100+
### 8. Commit and push release artifacts
142101

143-
Step 8 generates files in `cli-releases/` that must be committed and pushed:
102+
Step 5 generates files in `cli-releases/` that must be committed and pushed:
144103

145104
```bash
146105
git add cli-releases/
@@ -159,7 +118,7 @@ Merge this PR after approval.
159118

160119
## Verify build
161120

162-
Anyone can verify a released version by rebuilding from source:
121+
Anyone can verify a released version by rebuilding from source. The SHA256 hash and verification instructions are included in each [GitHub Release](https://github.com/caffeinelabs/mops/releases).
163122

164123
```bash
165124
cd cli

0 commit comments

Comments
 (0)