shockwave / workflows
CLAUDE.md192 starsChanged 4 months agoMITView on GitHub
One workflow, release.yml, triggered by a v* tag. A desktop release and the companion image it talks to are one version by construction, so the desktop's "your companion is behind" check (versionCompare.ts) compares two numbers that came from the same commit. release and image declare no dependency on each other, so they start together and the run costs the slower one, not the sum. Cutting a release is therefore: bump package.json version, commit, git tag vX.Y.Z && git push --tags.…
- Reads credentials
- Commits and pushes
In this repository7 other files it gives its agents.
- agent-core/CLAUDE.mdCLAUDE.md
- api/CLAUDE.mdCLAUDE.md
- CLAUDE.mdCLAUDE.md
- docs/CLAUDE.mdCLAUDE.md
- tests/CLAUDE.mdCLAUDE.md
- electron-devSkill
- shadcnSkill
The file
We split this CLAUDE.md into the rules it gives an agent
1 rule. About 0 are common, 0 are distinctive, and 1 is very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 52
Not a source directory; nothing imports it.
≈ “Common” is an estimate: at least 1 in 5 of the 26,980 CLAUDE.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
CLAUDE.md — release workflows
One workflow, release.yml, triggered by a v* tag. A desktop release and the companion image it talks to are one version by construction, so the desktop's "your companion is behind" check (versionCompare.ts) compares two numbers that came from the same commit.
Three jobs:
| Job | Runs | Waits on |
release | desktop installers, matrix over macOS + Windows + Linux → uploaded to a draft release | — |
image | the companion container → ghcr.io | — |
publish | flips the draft public, with notes | [release, image] |
release and image declare no dependency on each other, so they start together and the run costs the slower one, not the sum.
Cutting a release is therefore: bump package.json version, commit, git tag vX.Y.Z && git push --tags. Nothing else is manual. Keep package.json's version in sync with the tag you push — electron-builder names artifacts from the manifest, not the tag, and a mismatch produces installers whose auto-update feed points at a version that doesn't exist.
This was two workflows (
release.yml+companion-image.yml) until v1.0.69. They shared thev*trigger, so every tag produced two runs that the Actions list titled identically — both take their title from the tag commit's message — which read as an accidental double-trigger. Merging them also letpublishgate on the image, which two separate workflows structurally could not do.
Prereleases — how to rehearse without shipping
A tag containing a hyphen is a prerelease (semver's own rule). That single test appears twice, and both spots exist so the pipeline can be exercised end to end without any user seeing it:
release tag v1.0.70 | prerelease tag v1.0.70-rc1 | |
image job pushes | :v1.0.70 and :latest | :v1.0.70-rc1 only |
publish job flags | --draft=false --latest | --draft=false --prerelease --latest=false |
Both halves are load-bearing. Fresh companion installs pull :latest, so an rc that moved it would hand every new install a test image; electron-updater reads the latest published release, so an rc marked latest would be offered to every desktop client as an update.
package.json version must carry the prerelease suffix too (1.0.70-rc1), not just the tag. electron-builder derives its target release from the manifest version rather than GITHUB_REF — tag v1.0.70-rc1 against version 1.0.70 uploads into a stray v1.0.70 release, and the rc tag you pushed ends up testing nothing.
Two implementation details that bit during the merge and will bite again:
- The image tag list is built with
printf, not an embedded newline. A literal newline inside a YAML block scalar has to start at the block's indentation; written at column 0 it terminates the block and the file stops parsing. publishbuilds its flags as a bash array, not a string.gh release edit … $FLAGSwould depend on word splitting, which shellcheck flags (SC2086) and actionlint surfaces as a workflow error.
actionlint is the local gate for all of this — there is no dry run for a tag push:
docker run --rm -v "$PWD":/repo --workdir /repo rhysd/actionlint:latest -verboseThe release job — desktop installers
npm run cli-toolsis its own top-level step, not a postinstall insidenpm ci. Nested, it dies with EPERM on the Windows runner. It provisions the bundled CLIs (cli-tools/, seeresources/built-in-skills/CLAUDE.md) and removes npm's recursive self-link, which Windows' 7za can't resolve during NSIS packaging.- The macOS
npm run diststep is separate solely to scope the signing secrets.CSC_LINK/CSC_KEY_PASSWORDare electron-builder's cross-platform signing vars — exposed on the Windows runner they'd make it try, and fail, to sign the NSIS installer with the Apple certificate. Notarization (mac.notarize: true) needs all five Apple secrets. - Publishing is atomic, and that's what the
publishjob buys.package.json'sbuild.publish.releaseTypeisdraft, so every platform uploads into a draft;publish(withneeds: [release, image]) flips it public. electron-updater only reads published releases, so clients cannot see a release until every artifact is there. It used to bereleaseType: "release"and went public the moment the first platform finished — whilelatest-mac.yml, written last after ~10 minutes of notarization, was still missing, so Mac clients polling in that window got a 404 on the update feed. A failure anywhere now leaves an unpublished draft to inspect rather than a half-published release. publishwaits on the container too. A desktop release that goes public without its matching companion image tells users to upgrade to a companion versionghcr.iodoesn't have. The cost is that a Docker-only failure holds back a set of perfectly good installers — recoverable by re-running the failed job, since the draft and its artifacts survive.
build/ — what electron-builder reads
Not a source directory; nothing imports it. It holds the per-platform packaging assets named in package.json's build block, and two of them carry decisions:
entitlements.mac.plistis applied both asentitlementsandentitlementsInherit, becausehardenedRuntime: true. Two keys, each load-bearing:allow-jit(V8 will not start under the hardened runtime without it) anddevice.audio-input— without that second one the hardened runtime kills the process on the firstgetUserMediacall, so voice input works in dev and takes down the packaged app. The pairedNSMicrophoneUsageDescriptionlives inmac.extendInfo; the entitlement grants the capability, the usage string is what macOS shows in the prompt, and a missing one is its own crash.- One icon per platform, three formats —
icon.icns(mac),icon.ico(windows),icon-linux.png.make-icons.jsis a one-shot generator, run by hand (node build/make-icons.js), not part of any build: it masksicon-source.pnginto the macOS Big Sur squircle spec (1024 canvas, 824 artwork, 185 radius) and emits the per-size PNGs the.icns/.icoare assembled from. It needssharp, which is deliberately not a dependency of the app — regenerating icons is a design act, not a build step.
build/icon.png is also listed in build.files because it is loaded at runtime, unlike the rest of the folder.
Release notes are commit subjects
electron-builder creates the draft with an empty body, and nothing filled it in through v1.0.36 — invisible on github.com, fatal to the app's "What's new" dialog, which reads body over the API (see "App updates" in src/main/CLAUDE.md). The publish job writes them before flipping the draft.
Two choices worth not re-litigating:
- Not GitHub's
generate-notes. That endpoint summarizes merged pull requests and this repo commits straight to main — asked for v1.0.36 it returns a compare link and nothing else. Commit subjects here are already written as changelog lines, so they are the notes (Release v*commits are filtered out). - The previous tag comes from
git tag --sort=-v:refname, notreleases/latest. At that point in the run our own release is still a draft, so "latest" happens to mean the previous one — true today and quietly wrong the first time a release doesn't get published.
The whole step is best-effort (|| true plus an emptiness check before --notes-file) on purpose: a hiccup writing notes must never strand a fully-built release as a draft. Shipping beats annotating. Needs fetch-depth: 0 for the tag history.
The image job — the companion container
Publishes ghcr.io/stephengpope/shockwave-companion:<tag> and :latest, multi-arch (linux/amd64,linux/arm64, hence the QEMU step — VPSes are amd64, some boxes aren't).
- It carries its own
permissionsblock (contents: read,packages: write) because it pushes to the package registry, while the workflow default is thecontents: writethe other two jobs need for the Release. A job-levelpermissionsreplaces the workflow-level one outright rather than merging, so that block has to restatecontentseven though it only narrows it. - Build context is the repo root,
file: api/Dockerfile, because the image needsagent-core/andresources/built-in-skills/from outsideapi/. VERSION=${{ github.ref_name }}is baked in asAPP_VERSIONand surfaced byGET /health— that's the number the desktop's upgrade check reads, and it's'dev'for local builds.- Both tags matter: fresh installs pull
:latest, while a remote upgrade pinsSHOCKWAVE_TAGto an exact tag so the compose files and the image are always the same release. See "Remote upgrade" inapi/CLAUDE.md.
What is not automated
The install-script path. api/install.sh and updater/apply.sh fetch runtime files from raw.githubusercontent.com at the tag, so a release adding a host artifact needs it in both lists — tests/hostArtifacts.test.js fails if they disagree, which is the guard, not the workflow. A release adding a required .env secret still needs users to re-run the install one-liner; the updater deliberately doesn't invent secrets.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

