agentleFS
stephengpopestephengpope/shockwave/.github/workflows/CLAUDE.md

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

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
  1. Very specific:

    Not a source directory; nothing imports it.

    line 52

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
CLAUDE.md

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:

JobRunsWaits on
releasedesktop installers, matrix over macOS + Windows + Linux → uploaded to a draft release—
imagethe companion container → ghcr.io—
publishflips 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 the v* 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 let publish gate 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.70prerelease 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.
  • publish builds its flags as a bash array, not a string. gh release edit … $FLAGS would 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 -verbose

The release job — desktop installers

  • npm run cli-tools is its own top-level step, not a postinstall inside npm ci. Nested, it dies with EPERM on the Windows runner. It provisions the bundled CLIs (cli-tools/, see resources/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 dist step is separate solely to scope the signing secrets. CSC_LINK / CSC_KEY_PASSWORD are 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 publish job buys. package.json's build.publish.releaseType is draft, so every platform uploads into a draft; publish (with needs: [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 be releaseType: "release" and went public the moment the first platform finished — while latest-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.
  • publish waits on the container too. A desktop release that goes public without its matching companion image tells users to upgrade to a companion version ghcr.io doesn'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.plist is applied both as entitlements and entitlementsInherit, because hardenedRuntime: true. Two keys, each load-bearing: allow-jit (V8 will not start under the hardened runtime without it) and device.audio-input — without that second one the hardened runtime kills the process on the first getUserMedia call, so voice input works in dev and takes down the packaged app. The paired NSMicrophoneUsageDescription lives in mac.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.js is a one-shot generator, run by hand (node build/make-icons.js), not part of any build: it masks icon-source.png into the macOS Big Sur squircle spec (1024 canvas, 824 artwork, 185 radius) and emits the per-size PNGs the .icns/.ico are assembled from. It needs sharp, 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, not releases/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 permissions block (contents: read, packages: write) because it pushes to the package registry, while the workflow default is the contents: write the other two jobs need for the Release. A job-level permissions replaces the workflow-level one outright rather than merging, so that block has to restate contents even though it only narrows it.
  • Build context is the repo root, file: api/Dockerfile, because the image needs agent-core/ and resources/built-in-skills/ from outside api/.
  • VERSION=${{ github.ref_name }} is baked in as APP_VERSION and surfaced by GET /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 pins SHOCKWAVE_TAG to an exact tag so the compose files and the image are always the same release. See "Remote upgrade" in api/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.

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.