Imported from 4gray/yesterlog (
.codex/skills/release/SKILL.md). Install upstream withnpx skills add 4gray/yesterlog --skill release. Copyright stays with the author.
Release Yesterlog
End-to-end release flow for the public 4gray/yesterlog Electron + Vite + React app (npm package and Snap name yesterlog). Pushing a vX.Y.Z tag triggers .github/workflows/release.yml, which tests, builds and code-signs/notarizes macOS, builds Windows/Linux, creates a DRAFT GitHub Release, and uploads the Snap to edge. You then verify every artifact, curate the notes, promote the verified Snap revision, and hand the user the draft URL. Do not publish the GitHub Release unless the user explicitly asks.
The bump type comes from $ARGUMENTS ($1): patch, minor, or major. Default to patch when none is given.
Procedure
1. Preconditions
- Release from
main. Thechore(release)commits live onmain; this project does not use release branches. - Confirm
git remote get-url originresolves to4gray/yesterlog, the Snap name isyesterlog, and Pages is configured asmain:/docsathttps://4gray.github.io/yesterlog/. - Confirm the bump type. Map
$1→ script:patch→release:patch,minor→release:minor,major→release:major. If$1is empty, usepatch. - The working tree must be clean before bumping —
npm versionaborts on a dirty tree. So commit the actual fix/feature FIRST (step 2). - The latest
mainCI run must be GREEN before you tag. The release workflow re-runs the sametestjob (unit tests + renderer E2E + build), so a red pipeline onmainfails the release too. If any commits are already onmain, verify their CI passed before releasing:
⚠️gh run list --workflow=ci.yml --branch main --limit 1 --json headSha,status,conclusion -q '.[0]' # Require: status == "completed", conclusion == "success", and headSha == the commit you're about to tag. # If it's failing or still in progress, fix/wait BEFORE bumping — never tag on a red main.npm run testdoes not run the renderer E2E (e2e/renderer.e2e.mjs); a UI change can passtestlocally yet breake2e:rendererin CI. Always gate on the real pipeline (above) and/orrelease:dry-run(below), notnpm run testalone. - (Recommended) Validate locally before tagging — this mirrors the CI
testjob exactly:
(= brand audit +npm run release:dry-runnpm run test && npm run e2e:renderer && npm run build)
2. Commit the change
Commit the real fix/feature with a conventional-commit subject and a body explaining the why:
git add -A
git commit -m "fix: <subject>" -m "<why this change>"
Verify the tree is now clean (must print nothing):
git status --porcelain
2b. Refresh release screenshots (feature releases)
If the release changes a primary view, refresh its screenshot for the notes/docs before bumping — the tag must include it, and GitHub Pages (main:/docs, public) serves it for the notes. Capture with the demo seed:
npm run screenshots -- --seed release --today 2026-06-17 --views today --out docs/screenshots/v<NEW>
# omit --views to refresh the full set; add a second run with --themes light for light-<view> shots
- Screenshots are auto-compressed to a palette PNG by
scripts/capture-screenshots.mjs(a full-view 1440×1000 shot is ~30 KB instead of ~120 KB), so committing them todocs/stays cheap. Do not hand-optimize. - Commit the PNG(s) under
docs/screenshots/v<NEW>/. If you refreshed the whole set, also bump thescreenshots/v…/paths inREADME.mdanddocs/index.htmlso they don't point at the old version. - Reference the shot in the notes (step 7) via its public Pages URL — it renders inline:
https://4gray.github.io/yesterlog/screenshots/v<NEW>/<theme>-<view>.png(e.g..../v2.1.0/dark-today.png). Confirm it's live withcurl -sI <url>(expect200) oncemainis pushed.
3. Bump version + create the tag
Run the project script for the chosen bump type (default patch):
npm run release:patch # or: npm run release:minor / npm run release:major
Each expands to npm version <type> -m "chore(release): v%s", which in one step:
- updates
"version"inpackage.jsonandpackage-lock.json, - creates a
chore(release): vX.Y.Zcommit, and - creates the annotated tag
vX.Y.Z.
Capture the new version and tag for later steps (read it from package.json — don't hardcode a version, it drifts every release):
NEW=$(node -p "require('./package.json').version") # e.g. 2.1.0
TAG="v$NEW" # e.g. v2.1.0
echo "$TAG"
4. Push the commit and the tag
npm run release:push # = git push && git push --tags
The tag push (vX.Y.Z, matching on: push: tags: "v*.*.*") is what triggers the Release workflow.
5. Find the pipeline run, then watch it in the BACKGROUND
Resolve the run id for this tag (the workflow runs against the tag ref):
RUN_ID=$(gh run list --workflow=release.yml --branch "$TAG" --limit 1 --json databaseId -q '.[0].databaseId')
echo "$RUN_ID"
Watch it to completion in the background (the build takes ~5-6 min) so you're notified when it finishes:
gh run watch "$RUN_ID" --exit-status --interval 20
Run that gh run watch command with run_in_background: true.
6. What the pipeline does
Jobs run in order, all from the tagged commit:
test(ubuntu-latest) —npm ci,npm run test,npx playwright install --with-deps chromium,npm run e2e:renderer,npm run build.build(needs: test, matrix,fail-fast: false) — three platforms in parallel:- macOS (
macos-latest, artifactyesterlog-macos): runsnpm run dist:mac -- -c.mac.forceCodeSigning=truewith code-signing + notarization (themacbuild config hasnotarize: trueandhardenedRuntime: true). Producesrelease/*.dmg,release/*.zip. - Windows (
windows-latest, artifactyesterlog-windows):npm run dist:win, unsigned. Producesrelease/*.exe,release/*.zip. - Linux (
ubuntu-latest, artifactyesterlog-linux):npm run dist:linux, unsigned. Producesrelease/*.AppImage,release/*.deb,release/*.tar.gz.
- macOS (
snap(needs: test, ubuntu-latest) — buildsyesterlog_<version>_amd64.snapand uploads the workflow artifactyesterlog-snap.publish_snap(needs: snap, ubuntu-latest, release tags only) — uploads the exact Snap artifact to SnapcraftedgewhenSNAP_STORE_PUBLISH_ENABLEDis enabled.publish(needs: [build, snap], ubuntu-latest, only whenstartsWith(github.ref, 'refs/tags/v')) — downloads all artifacts intorelease-assets/, then creates the release if it doesn't exist, orgh release upload --clobberif it does:gh release create "$TAG_NAME" "${assets[@]}" \ --draft \ --title "$TAG_NAME" \ --generate-notes \ --verify-tag
The result is a DRAFT GitHub Release titled vX.Y.Z with .dmg/.zip/.exe/.AppImage/.deb/.tar.gz attached and auto-generated notes.
7. Confirm the draft and curate the release notes
After the run succeeds, confirm the draft exists:
gh release view "$TAG" --json isDraft,url,assets -q '{isDraft, url, assets: [.assets[].name]}'
Replace the auto-generated notes with a curated, user-facing changelog. Determine the previous tag for the compare link:
PREV=$(git tag --sort=-v:refname | grep -v "^$TAG$" | head -1) # e.g. v1.3.2
Use the generated draft to identify the included pull requests, then inspect each pull request's ## Release note section:
gh release view "$TAG" --json body -q .body
PR_NUMBER=37 # replace with each pull request number from the generated notes
gh pr view "$PR_NUMBER" --repo 4gray/yesterlog --json title,body,url
- Treat a non-
None## Release noteas the primary user-facing input. Combine and polish entries for a coherent release; do not copy raw commit subjects or implementation details into the final notes. - Omit entries marked exactly
None. For older pull requests without the section, fall back to their title and body. - Cross-check
git log "$PREV..$TAG"and the full compare link so direct commits, stale pull request descriptions, and meaningful changes omitted by generated notes are caught. Resolve mismatches from the actual shipped diff rather than blindly trusting the pull request field.
Write /tmp/notes.md (group changes under headings like Highlights, Fixes, Improvements — describe user-visible impact, not raw commit subjects). For a feature release, embed the screenshot captured in step 2b via its public Pages URL (it renders inline; a committed private-repo raw/asset URL would NOT). ALWAYS end with the compare link:
## What's new in vX.Y.Z
### Highlights
- ...

### Fixes
- ...
**Full changelog:** https://github.com/4gray/yesterlog/compare/vPREV...vX.Y.Z
Apply the notes to the draft (this does not publish it):
gh release edit "$TAG" --title "$TAG" --notes-file /tmp/notes.md
8. Verify and promote the Snap revision
On Ubuntu, install the exact edge revision produced by the workflow and verify startup, the clean first-run profile, Jira/network access, browser links, notifications, and desktop integration:
snap info yesterlog
sudo snap install yesterlog --edge
# If already installed:
sudo snap refresh yesterlog --edge
After verification, promote that same revision without rebuilding it:
snapcraft promote yesterlog --from-channel=edge --to-channel=candidate
snapcraft promote yesterlog --from-channel=candidate --to-channel=stable
snap info yesterlog
Never release a different revision to candidate or stable. For the first Yesterlog release, confirm the public stable listing before making the legacy Snap private.
9. Hand the URL to the user — leave it as a DRAFT
Give the user the draft release URL and tell them it's ready for review:
gh release view "$TAG" --json url -q .url
Stop here. The release stays a DRAFT for the user to review and Publish themselves. Only publish when the user explicitly asks, with:
gh release edit "$TAG" --draft=false
Gotchas
- The shipped version comes from the BUILD, not from any runtime string. In the renderer,
import.meta.env.VITE_APP_VERSIONis injected at build time by a Vitedefineinvite.config.ts("import.meta.env.VITE_APP_VERSION": JSON.stringify(packageJson.version)), read insrc/App.tsxandsrc/api/native.ts. In the Electron main process,app.getVersion()(electron/main.ts) reads the bundledpackage.json. CI builds from the tagged commit, so the version bump must be committed + tagged BEFORE the build — which is exactly why the tag push is the trigger. Never hand-edit a version string anywhere else; bump only vianpm version(step 3). - Clean tree is mandatory.
npm version(insiderelease:patch/minor/major) refuses a dirty working tree and aborts. Commit your real change first (step 2). - A local
npm run dist:macis UNSIGNED (it runs with--publish neverand no signing secrets) and is stamped with whateverpackage.jsonversion exists at that moment. Use it only for quick personal testing — never as the release artifact. The signed/notarized macOS build only happens in CI via repo secrets (MAC_CSC_LINK,MAC_CSC_KEY_PASSWORD,APPLE_API_KEY_BASE64,APPLE_API_KEY_ID,APPLE_API_ISSUER,APPLE_TEAM_ID). If any macOS signing secret is missing, the macOS build job fails fast at the "Check macOS signing secrets" step; Windows and Linux need no signing. - The tag pattern must be semver
vX.Y.Z. Onlyv*.*.*triggers the release on push. A manualworkflow_dispatchrun executestest+buildbut skipspublish(gated onstartsWith(github.ref, 'refs/tags/v')), so no release is created from a branch dispatch. - The publish job is idempotent. Re-running it when the release already exists uses
gh release upload --clobberrather than recreating it, so re-runs overwrite assets instead of duplicating the release. - Identity is consistent: GitHub repo
4gray/yesterlog, npm packageyesterlog, Snapyesterlog, electron-builderproductNameYesterlog, and Pageshttps://4gray.github.io/yesterlog/. - Snap credentials are least-privilege.
SNAPCRAFT_STORE_CREDENTIALSmust be restricted to Snapyesterlogand channeledge; promotion is a separate deliberate post-verification action. - Validate before tagging with
npm run release:dry-run(= brand audit +npm run test && npm run e2e:renderer && npm run build) to avoid pushing a tag that fails CI.