Skip to content

Publish release images to GHCR and a CLI that defaults to them - #71

Merged
Tomer Glottmann (tomergee) merged 10 commits into
mainfrom
release-images
Oct 13, 2026
Merged

Tomer Glottmann (tomergee) merged 10 commits into
mainfrom
release-images

Conversation

@tomergee

@tomergee Tomer Glottmann (tomergee) commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

What

Make the images of this repo something a customer downloads rather than builds.

  • .github/workflows/release.yaml: on every v* tag, or by hand for an existing tag, builds ate-env-api and ate-env-guest with ko for linux/amd64 and linux/arm64, pushes them to ghcr.io/agent-substrate/env/<image> tagged with the release and latest, signs them with cosign keyless signing (bound to this repo's Actions identity), builds the ate-env CLI for linux and darwin on amd64 and arm64 with the two digests baked in, and uploads binaries, images_<tag>.txt and checksums as release assets. The release notes get an ## Images section with the digests and the verify command; a republish replaces it instead of duplicating it.

  • CLI defaults: a release binary's ate-env manifest --api-image and ate-env manifest template --guest-image default to the release's images, so the quickstart needs no image flags for this repo. ate-env --version prints the tag. A source build keeps the flags required, with messages that explain the two cases. make build-cli reproduces a release-style binary locally.

  • --worker-image stays required. ateom-gvisor is a Substrate image and must match the Substrate version on the cluster; Substrate publishes nothing yet, so the message points at building it there. That is the remaining piece for a zero-build customer experience and belongs to the Substrate repo.

  • .github/workflows/ci.yaml: Go build, vet, tests, gofmt (make verify-gofmt over tracked Go files), license headers, and the Python client tests on PRs and main. The repo had no checks before. ate-env-api: create the default atespace at startup #78 landed with two space-indented files because nothing checked formatting; this PR formats them and adds the check.

  • .ko.yaml: the guest's bash base pinned by digest, so a published guest image is reproducible.

  • docs/release.md: artifacts, pinning by digest, signature verification, cutting and republishing a release, the one-time organization setup for GHCR. README: Installation and a new Images section; quickstart commands updated.

  • Workflow hardening (what the organization's zizmor scan requires): every action pinned by commit SHA with the version in a trailing comment, checkout with persist-credentials: false, top-level permissions: {} with the release job asking for contents, packages and id-token itself, no expression interpolated into a run: block (inputs and step outputs reach the shell through env), Go module cache off for the release build, and concurrency groups (a new push supersedes running CI; one publish per tag at a time). docs/release.md explains how to bump a pin.

  • make images builds, pushes and prints the digests. It no longer rewrites the README, which no longer pins this repo's images.

Why this shape

  • Digests, not tags, everywhere a customer copies something: Substrate requires pinned images in templates and latest moves.
  • Baking the digests into the CLI makes "download the binary, run ate-env manifest" work without reading release notes, and keeps the CLI and the images of one release consistent by construction.
  • Keyless signing costs one step and gives customers a way to check provenance that does not depend on us managing keys.

First-time setup (maintainers, once)

  1. Organization settings → Packages: allow GitHub Actions to create packages.
  2. Push a tag (git tag -a v0.0.11 -m v0.0.11 && git push origin v0.0.11), watch the release workflow.
  3. Open the two packages that appear, confirm they are public and linked to this repository.

Testing

  • go test ./... passes (12 packages); the new test covers the source-build and release-build flag defaults and the worker-image message. hack/verify/boilerplate.sh passes.
  • make build-cli ATE_ENV_API_IMAGE=... ATE_ENV_GUEST_IMAGE=... VERSION=v9.9.9 produces a binary whose --version and flag defaults carry the values, which is exactly what the workflow does. A source build keeps the flags required with the explaining message.
  • zizmor 1.30.1 reports no findings on either workflow. Every pinned SHA was resolved against its tag: actions/checkout v5.1.0, actions/setup-go v5.6.0, actions/setup-python v5.6.0, ko-build/setup-ko v0.9, sigstore/cosign-installer v3.10.1.
  • Both workflow files parse as YAML; every run: block passes bash -n. The Python CI steps were run locally as written.
  • make verify-gofmt passes on the branch and fails on a planted unformatted file.
  • The release workflow itself cannot run before it is on main and a tag is pushed; the first tag is the real test, and workflow_dispatch exists to rerun it without a new tag.

Relation to other PRs

@tomergee

Copy link
Copy Markdown
Collaborator Author

After #77 (Substrate bump to f03327bd) merges, this branch needs a rebase for one conflict: README.md. #77 rewrote the quickstart --api-image / --guest-image digests via make images and added a compatibility note under "Quickstart" (Substrate at or after f03327bd; --worker-image must be the ateom published by that install). Keep the note and let this branch's GHCR flow own the digests. Makefile, cmd/ate-env/main.go, manifest.go and manifest_test.go auto-merge.

Nothing published images for this repo: the Makefile pushed to a personal
registry, releases carried no assets, and the manifest command's error
message pointed at "images published by the latest release" that did not
exist. Every user, including the quickstart, had to build and push
ate-env-api and ate-env-guest first.

A release workflow now runs on every v* tag (and by hand for an existing
tag): ko builds both images for linux/amd64 and linux/arm64, pushes them to
ghcr.io/<org>/<repo>/ate-env-api and ate-env-guest tagged with the release
and latest, signs them with cosign keyless signing bound to the workflow's
identity, then builds the ate-env CLI for linux and darwin on amd64 and
arm64 with the two digests and the version baked in, uploads the binaries,
an images_<tag>.txt and checksums as release assets, and writes an Images
section with the digests and the verify command into the release notes.

The CLI gains build-time defaults: a release binary's `manifest
--api-image` and `manifest template --guest-image` default to the images
published with it, `ate-env --version` prints the tag, and a source build
keeps the flags required with messages that say why. The worker image
stays a required flag, since ateom-gvisor comes from the Substrate repo
and must match the Substrate version on the cluster; the message says so.
`make build-cli` reproduces a release-style binary locally.

A ci workflow runs the Go build, vet and tests, the license-header check,
and the Python client tests on pull requests and main; the repo had no
checks before. The guest's bash base image is pinned by digest in .ko.yaml
so a published guest is reproducible. docs/release.md describes the
artifacts, pinning, verification, cutting and republishing a release, and
the one-time organization setup for GitHub Container Registry; the README
quickstart no longer needs image flags with a release binary.
…t of shell

What the organization's workflow security scan (zizmor) requires, and
what a release pipeline should do anyway:

- every action pinned by commit SHA, with the version in a trailing
  comment (each SHA checked against its tag)
- checkout with persist-credentials off
- top-level permissions empty; the release job asks for contents,
  packages and id-token itself
- no expression interpolated into a run block: inputs and step outputs
  reach the shell through env
- Go module cache off for the release build, so a release cannot consume
  a cache another run seeded
- concurrency: a new push supersedes running CI; one publish per tag at
  a time

docs/release.md explains the pinning convention and how to bump a pin.
zizmor reports no findings on both workflows.
The quickstart no longer pins this repo's images, release binaries carry
the defaults, so the regex rewrite had nothing to match and printed a
misleading "updated README" line. Build, push, print.
Comment thread cmd/ate-env/manifest.go Resolved
Comment thread .github/workflows/ci.yaml Resolved
Comment thread README.md Resolved
runSidecar used exec.CommandContext, whose cancel kills only the direct
child. A sidecar that forks (a wrapper script, which RUNTIMES.md recommends
for quoted arguments) died alone while its child kept the output pipes
open, and cmd.Wait blocked until that child exited. The guest's shutdown
hung for as long as the orphan lived.

TestRunSidecarRestartsAndStopsWithContext caught this on Linux and not on
macOS: `sh -c "sleep 60"` is dash on Linux, which forks sleep, and bash on
macOS, which execs it. First seen in the ci run on #71
(https://github.com/agent-substrate/env/actions/runs/38440900526), the
first Linux run of this code.

Each sidecar now runs in its own process group. On cancel the group gets
SIGTERM, cmd.WaitDelay (sidecarStopGrace, 5 s) bounds the wait for the
pipes, and the group gets SIGKILL for anything still alive. Verified in a
golang:1.27 container, the test 3/3 and the guest packages green, and on
macOS.

(cherry picked from commit 93c5fb2)
ubuntu-latest migrates to Ubuntu 26.04 from 2026-10-19 (actions/runner-images#14748).
A release pipeline must not change its toolchain image underneath a tag; pin
the image and bump it on purpose.
… platform

Review follow-ups on the process-group fix:

- main waits for the supervisors after Serve returns (bounded by
  sidecarStopGrace plus a second), so the SIGTERM, grace and SIGKILL
  sequence runs before the process exits. Inside an actor the guest is
  PID 1 and the container ends with it anyway; outside one, this is what
  stops the sidecars.
- The old process group is SIGKILLed before a restart as well, so a
  restarted sidecar does not compete with its own orphans for ports.
- A stop is logged.
- The test forks explicitly (`sleep 60 & wait`): a bare `sh -c "sleep 60"`
  execs on macOS and did not exercise the group kill there. The bound is
  derived from sidecarStopGrace and explained: the forked sleep holds the
  pipe, so returning within it proves the child died.

(cherry picked from commit 922a95b)
…ign identity, strip CR before editing notes

- The --worker-image message, the README quickstart and docs/release.md
  name `go run ./cmd/ate-setup publish worker-images` (which pushes the
  ateom images and prints their digest-pinned refs) instead of "build it
  with ko", and warn that a registry's :latest is whatever was built last
  and a stale worker image registers workers with no capacity.
- The cosign identity regexp in the release notes and the docs is anchored
  to this repository's release workflow at a refs/tags/ ref, not to any
  identity under the repository.
- The republish path strips carriage returns from the release body before
  looking for the previous "## Images" block; with CRLF from the API the
  block was appended twice instead of replaced.
…p logs

Review follow-up on #79. stopGroup is now the only way a sidecar's group is
taken down: SIGTERM, up to sidecarStopGrace for the group to disappear,
then SIGKILL for what is left. Both paths use it: after a cancel, where it
finishes what cmd.Cancel started, and before a restart, where it clears
what a crashed sidecar left behind so the new one does not compete with
its own orphans for ports, pipes or lock files. Each step is logged:
SIGTERM sent, group exited, still alive after the grace and SIGKILL sent,
or already gone (ESRCH counts as success).

main's shutdown wait covers both grace periods. The test asserts the
logged sequence.

(cherry picked from commit 8eb8274)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/lgtm

@sunnylovestiramisu

Copy link
Copy Markdown
Collaborator

/lgtm

@tomergee
Tomer Glottmann (tomergee) merged commit 077ae95 into main Oct 13, 2026
10 checks passed
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.

3 participants