Skip to content

Cut releases from a reviewed PR, and make CI run the thing it ships - #64

Merged
davidmckayv merged 2 commits into
mainfrom
feat/release-process
Aug 21, 2026
Merged

Cut releases from a reviewed PR, and make CI run the thing it ships#64
davidmckayv merged 2 commits into
mainfrom
feat/release-process

Conversation

@davidmckayv

Copy link
Copy Markdown
Contributor

What this changes

Releases were cut by hand, and nothing in CI ran the artefact people deploy.

A release is one choice and one review. Create release PR bumps the version and promotes
## Unreleased to a numbered section. Merging the pull request it opens is what publishes: one image
to ghcr.io/copilotkit/openbot, a build provenance attestation signed for its digest, the tag, and a
Release carrying container-images.json so a deployment names a digest rather than a tag somebody
could move. Notes are the section a person wrote. Nothing is generated from commit subjects.

Publishing trusts the pull request, not the commit message. The head branch must match
release/publish/vX.Y.Z, be in this repository, and carry the release label. A fork can name a
branch anything; it cannot add a label.

Three new checks, because the existing ones cannot see the image. migrations refuses a schema
change with no migration and a drifted snapshot. image builds the container, boots it, and fails if
it does not answer or if a supervised service is respawning. smoke runs tests/smoke, which
existed and ran nowhere, against a real deployment. One verify check covers every job.

Modelled on OpenTag's release plumbing, with two deliberate departures: its changelog is generated
from git log, which would bulldoze the hand-written one here; and its own e2e harness is wired into
nothing, which is the gap this closes rather than copies.

Where it runs

  • New state that outlives a request? None. Nothing here is application code.
  • What happens on the second replica? Unchanged. No runtime behaviour is touched.
  • Anything serialised? The release PR: a second one is refused, because two would each carry
    a version computed before the other existed.
  • Anything fanned out to a browser? No.
  • New listener, port, or schedule? None. Both workflows are triggered, not scheduled.

Boundary and audit

  • No acting path changed. No server or app code in this PR.
  • Nothing new is trusted from outside: publishing verifies branch, repository and label through
    the API, and the tag is created through the API so no git credential is persisted in a runner.

Changelog

  • Entry under Unreleased.

Proof

Checked against current guidance rather than copied: every action is at its latest release and
SHA-pinned, verified against the GitHub API. actions/attest-build-provenance is v4.2.2, not the v3
that search results suggest; docker/setup-buildx-action was bumped to v4.3.0. Attestation binds the
digest, never a tag.

Run locally:

  • zizmor --min-severity=low on all four workflows: no findings. It caught two of mine first, a
    cache-poisoning vector and a persisted git credential. Both are fixed rather than suppressed: the
    smoke job no longer caches the bun binary, and the tag is created through the API.
  • The version bump was wrong and is now tested. bun -e passes argv as [bun, ...args], so
    reading it positionally from the front yielded undefined and every bump silently became a patch.
    Verified: 0.0.1 -> patch 0.0.2, minor 0.1.0, major 1.0.0.
  • Changelog promotion produces ## Unreleased above ## 0.0.2, and the notes extractor pulls 40
    non-blank lines back out for the release body.
  • drizzle-kit check clean and generate reports no unwritten migration, so both new gates pass on
    this tree rather than failing on arrival.
  • bun run typecheck exit 0, biome lint and biome format clean, bun test 745 pass 0 fail,
    bun run test:ci meets its floor.

Not verified: I cannot execute GitHub Actions locally, so the workflows are validated by YAML
parse, zizmor, pinned-version checks and by running their underlying commands. The first Create release PR should be run with dry_run first.

Two things are needed before the smoke job can pass: COPILOTKIT_LICENSE_TOKEN and
INTELLIGENCE_API_KEY as repository secrets, and DEVOPS_BOT_CLIENT_ID / DEVOPS_BOT_PRIVATE_KEY
for the release PR. Without the licence, smoke skips with a warning rather than failing.

Releases were cut by hand, and nothing in CI ran the artefact people deploy.

A release is now one choice and one review. `Create release PR` bumps the
version and promotes `## Unreleased` to a numbered section; merging the pull
request it opens is what publishes. Merging builds one image, signs a build
provenance attestation for its digest, tags the commit and creates the Release
with `container-images.json` so a deployment names a digest rather than a tag
somebody could move. The notes are the section a person wrote: nothing is
generated from commit subjects, because a commit subject is written for the
reader of a diff and these are for somebody deciding whether to upgrade.

Publishing checks the pull request rather than the commit message. The head
branch must match `release/publish/vX.Y.Z`, be in this repository, and carry
the `release` label. A fork can name a branch anything; it cannot add a label.

Three checks were added, because the existing ones cannot see the image at all.
`migrations` refuses a schema change with no migration and a snapshot that has
drifted. `image` builds the container, boots it, and fails if it does not answer
or if a supervised service is respawning. `smoke` runs the journey in
tests/smoke, which existed and ran nowhere, against a real deployment. A single
`verify` check covers every job, so branch protection needs one entry and a new
job is covered without anybody updating a list.

The smoke journey skips itself with a warning when no licence is configured. A
check that fails for a reason nobody can fix is a check people learn to ignore.
…e journey

Three corrections to how this was first written, all of them found by running it.

The image check under-configured the container and it died at start-up.
`loadConfig` refuses to run without Intelligence and a licence, so the boot now
supplies both as placeholders: it only checks they are present and well-formed,
nothing is contacted at start-up, and /api/capabilities reads config alone. The
wait is 150s rather than 60s, which a cold embedded PostgreSQL plus migrations
plus Chromium needs.

The smoke journey is gone from CI, and will not be coming back. It asserts
`licenseStatus` is valid, and a licence is signed for the machine it was issued
for, so no hosted runner can satisfy it and no placeholder can fake it. It is
now the one step a person takes before merging a release, on a machine that has
a licence, and the release PR asks for the result. A check that cannot pass is
worse than an absent one.

There is no GitHub App for this organisation, so the release PR is opened with
the built-in token. That means the PR arrives without its own checks, because a
pull request opened by a workflow does not trigger them. Rather than work around
that, publishing now runs the whole suite against the release commit before it
builds anything. The checks gate the release instead of the proposal for one,
which is where they were always more useful.

No secrets are needed by any of this. GITHUB_TOKEN is the only one referenced.
@davidmckayv
davidmckayv merged commit 675ec47 into main Aug 21, 2026
7 checks passed
@davidmckayv
davidmckayv deleted the feat/release-process branch August 21, 2026 01:14
davidmckayv added a commit that referenced this pull request Aug 21, 2026
Audited every markdown file against everything that landed today, including the
work that was not mine.

`docs/coworkers.md` still told people to point `MANAGED_AGENT_AG_UI_URL` at
`4200`. #33 made `agent-langgraph` on `4201` the default precisely because the
proof-of-concept hand-writes the protocol and leaves the tool loop to whatever is
watching, so following that page produced the shape the change moved away from.

Three environment variables the server reads were in `.env.example` and nowhere
in the configuration reference: `AGENT_STALL_TIMEOUT_MS` from #19, which is the
only thing that notices a Bot's stream going silent; `AGENT_TOOL_TOKEN` from #34,
without which no framework Bot may call a granted tool back; and `APP_DIST_DIR`,
which the container sets so one process serves both halves.

Both documentation indexes had fallen behind their own directory and listed
neither `deployment.md` nor `releasing.md`.

`docs/development.md` gains the migration workflow the checks in #64 now enforce:
never hand-edit a generated migration, write a data step with `--custom`, and
what to do when `drizzle-kit migrate` hangs and exits non-zero with nothing
printed, which is the journal naming a file a rebase renamed. `drizzle-kit check`
calls that state fine, because it compares schemas rather than asking whether the
journal and the directory agree.

The README keeps its shape: what this is, how to run it, how to deploy it, and
where to read the rest.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant