ci: add pull-request CI for the release codepath - #17
Merged
Conversation
Adds a three-job GitHub Actions workflow that runs on every pull request, on pushes to main, and on manual dispatch: - Frontend (ubuntu): npm ci, lint, format check, tests - Collector (ubuntu): gofmt + go vet via a new `make check`, go test - Release (macOS): the real `make release`, then a check that staging the frontend leaves the worktree clean, then a smoke test of the resulting binary, then the same for a dev binary, then artifact upload The release job is the point of the change: it exercises the same embedded-frontend build path a human release uses, rather than approximating it. scripts/smoke.sh boots a built binary under an empty HOME and asserts its HTTP contract in two modes. Embedded: / serves the app document, a referenced /assets/*.js resolves, /coslash survives a direct hit, a missing asset is a 404 rather than the SPA fallback, an unrouted /api path is a 404 rather than the app document, /api/sessions returns JSON, and --version is not "dev". Bare: / is 503 and /api/sessions still answers. The 404 and 503 assertions lock in the behavior from 0cb5405. Also reformats CoslashPage.tsx, which was the only file failing prettier and would have made the new format gate red on its first run. The change is whitespace and Tailwind class ordering only.
`go vet ./...` on the new ubuntu CI job could not compile the collector: Birthtimespec exists only on darwin's syscall.Stat_t, so internal/vendors/claude/fork.go failed to build on Linux. fileCreationTime already fell back to ModTime when the Stat_t assertion failed, so the intent was there — it just could not compile where the field is absent. Move the darwin-only read behind a build-tagged birthtime helper and keep the existing fallback for everything else. Fork ordering only uses birthtime to break ties between transcripts, and the collector ships for macOS, so the fallback costs nothing in practice. No behavior change on darwin: the expression is unchanged. Verified: `make check` and `make test` pass natively, and GOOS=linux (amd64 and arm64) now builds and vets clean.
The production build only ran inside `make stage` on the macOS Release job, so a TypeScript error failed Release while Frontend stayed green — a source error reported as release-infrastructure breakage, on the slowest and most expensive runner. Runs `npm run build`, the same script the release path invokes, rather than a separate typecheck script, so both paths check the identical build. Placed last because it is the slowest gate (~3s locally against under a second for the others) and nothing earlier depends on it. Verified the step earns its place: with a deliberate type error present, lint, format:check, and `vitest run` all exit 0 and only `npm run build` fails (TS2322). `vite build` alone also exits 0 — esbuild strips types without checking them, so `tsc -b` is doing the work. No worktree impact: both tsconfigs set noEmit and point tsBuildInfoFile into node_modules/.tmp, so the Release job's clean gate is unaffected.
milanshen
reviewed
Aug 4, 2026
milanshen
left a comment
Contributor
There was a problem hiding this comment.
Two recommended workflow changes before merge.
milanshen
approved these changes
Aug 5, 2026
This was referenced Aug 5, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Context
The repository had no CI. Nothing checked a pull request, and nothing verified that the release build — the one that embeds the built frontend into the Go binary via
go:embed— still produced a working executable. A broken release was only discoverable by hand.This adds both, with the release path exercised the same way a person would run it rather than approximated by a separate set of steps.
Changes
.github/workflows/ci.yml— three jobs, on every pull request, on pushes tomain, and on manual dispatch. In-progress runs are cancelled for pull requests but not formain.Frontendnpm ci, lint, format check, tests, production buildCollectormake check(gofmt +go vet),make testReleasemake release, worktree-clean check, smoke test, dev build, dev smoke test, artifact uploadThe
Releasejob is the point of the change. It runs the samemake releasea human release uses, then boots the resulting binary and asserts its behavior over HTTP. Step order matters: the embedded binary is copied aside beforemake buildoverwritesbin/coslash, so the uploaded artifact is the embedded one and not the bare development build.collector/scripts/smoke.shboots a built binary under an emptyHOMEand checks its HTTP contract in two modes./serves the application document; a/assets/*.jsfile referenced by that document resolves;/coslashsurvives a direct hit rather than 404ing; a missing asset returns 404 rather than falling back to the SPA document; an unrouted/apipath returns 404 rather than the application document;/api/sessionsreturns JSON;--versionis notdev./returns 503 and/api/sessionsstill answers.The 404 and 503 assertions lock in the behavior from
0cb5405, which was previously only verifiable by hand.make check,make smoke,make smoke-devgive CI stable entry points instead of shell inlined into YAML, and stay usable locally.The collector now builds on non-darwin platforms. The new Linux job caught this immediately:
Birthtimespecexists only on darwin'ssyscall.Stat_t, sointernal/vendors/claude/fork.gohad never compiled anywhere else — nothing had ever tried.fileCreationTimealready fell back toModTime()when theStat_ttype assertion failed, so the intent was present; it simply could not compile where the field is absent. The darwin-only read now sits behind a build-taggedbirthtimehelper. No behavior change on darwin — the expression is unchanged — and fork ordering only uses birthtime to break ties between transcripts.CoslashPage.tsxreformatted so the new formatting gate is not red on its first run. Whitespace and Tailwind class ordering only; nothing renders differently.Plus a CI badge in the README.
Test
All three jobs pass:
Frontend23s,Collector23s,Release48s.The uploaded artifact is the embedded binary — the one failure mode that could ship behind a green check. Downloaded
coslash-macos(10.1 MB),--versionprinted a real commit SHA rather thandev, and booting it under an emptyHOMEgave/→ 200 with<title>coSlashand oneid="root"div,/api/sessions→ 200. A 503 there would have meant the wrong binary was uploaded.The frontend build step earns its slot. With a deliberate type error present,
npm run lint,npm run format:check,vitest run, andvite buildall exit 0; onlynpm run buildfails, withTS2322.vite buildpassing on its own confirms why — esbuild strips types without checking them, sotsc -bis doing the work. It uses the same script the release path invokes, so both check the identical build.Every CI step also replayed locally, in the workflow's order, on darwin-arm64 / Node 23.5.0 / Go 1.26.5:
make check,make test,make release(worktree clean afterwards),make smoke,make build,make smoke-dev, and the full frontend sequence from a coldnode_modules.GOOS=linux go build ./...andgo vet ./...pass on amd64 and arm64 after the portability fix.Not yet done: deliberately breaking each gate to watch it go red — though the gofmt/vet gate proved itself for real via the Linux bug above, and the formatting gate was genuinely red before the
CoslashPage.tsxfix — and the smoke script's port-fallback and argument-validation edge cases.Note for reviewers
go test ./...currently has no test files, so that step passes vacuously. The wiring is correct and picks up tests as soon as they exist, andsmoke.shis doing the real behavioral verification in the meantime, but a greenCollectorcheck should not be read as coverage.internal/web's routing rules are pure functions of anfs.FSand are the obvious first candidate.