Skip to content

ci: publish Anton staging release candidates (ENG-1159) - #298

Open
pnewsam wants to merge 2 commits into
stagingfrom
paul/eng-1159-anton-staging-rc
Open

ci: publish Anton staging release candidates (ENG-1159)#298
pnewsam wants to merge 2 commits into
stagingfrom
paul/eng-1159-anton-staging-rc

Conversation

@pnewsam

@pnewsam pnewsam commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Problem

cowork-server staging needs Anton changes before they reach anton/main, but Anton only publishes stable packages from main (release.yml). Downstream staging has had to install Anton from a mutable git branch or a hand-pinned commit, so tests, images, wheels, and lockfiles can observe different Anton snapshots. This is the producer half of ENG-1159.

The consumer half already shipped in cowork-server #225 (ENG-958): its publish-staging.yml ("Pin anton-agent to its latest staging rc" step) pins anton-agent==<latest rc> and falls back to a loose constraint otherwise. It hits that fallback on every run today because Anton publishes no rc's. This PR closes the loop.

Design — mirrors the cowork-server pattern

  • publish-staging.yml (new) — staging only: push to staging → validate → cut a PEP 440 pre-release (2.YY.M.DD.SEQrcN) via the shared calver-release.yml reusable → publish to PyPI via Trusted Publishing → notify.
  • release.yml — same intent, restructured to match cowork-server's publish.yml: gate on the reusable tests, scope permissions per job (only the release job gets contents: write), guard on refs/heads/main, and fold the test job into notify.
  • tests.yml — now a workflow_call reusable invoked by both publishers; drops its standalone push: [main, staging] run and push-only notify. Branch-push validation now happens inside the release run. PR gating is unchanged.

Simpler than cowork-server's: Anton doesn't depend on itself, so there's nothing to self-pin — publish-staging.yml builds from a clean checkout and hatch-vcs derives the rc version directly. No pin step, no SETUPTOOLS_SCM_PRETEND_VERSION override, and no build-deploy job (Anton ships no image).

Prod safety: resolvers ignore pre-releases unless a specifier names one, and PyPI's info.version (read by the prod desktop updater) excludes them — so prod can never pick up an rc.

⚠️ One-time PyPI setup required before merge

Register a second Trusted Publisher on the anton-agent project:

  • Workflow filename: publish-staging.yml
  • Environment: pypi

Until this is added, the publish job fails OIDC auth. The tag/release still cut, so it's safe to add the publisher and re-run.

Notes

  • Supersedes the self-closed anton#289 / cowork-server#255. The consumer side (cowork-server#225) is already merged on main, so only this producer workflow is needed.
  • Follow-up (not in this PR): cowork-server pins "latest by upload time, only if rc", so once Anton publishes both streams a newer stable can win the race and drop it back to the loose constraint. Tightening to "latest rc specifically" would make the pairing strict.

🤖 Generated with Claude Code

@pnewsam
pnewsam requested a review from a team as a code owner July 31, 2026 18:53

@pnewsam pnewsam left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Code Review

Verdict: REQUEST CHANGES

The core producer design is sound, but this head is not merge-ready. It is 36 commits behind staging, GitHub reports it as non-mergeable, and the conflict in .github/workflows/tests.yml overlaps meaningful changes: current staging added uv lock --check and moved development dependencies from --extra dev to --group dev. Rebase and preserve those current test steps while adding workflow_call and removing the standalone push notification.

The PyPI Trusted Publisher for publish-staging.yml with environment pypi also needs to be configured before merge, because the merge push immediately enters the tag-and-publish pipeline.

Non-blocking integration caveat: cowork-server still selects the most recently uploaded Anton version and pins it only when that version is an RC. If ENG-1159 requires strict RC pairing, update the consumer to select the latest RC specifically rather than deferring that change.

Validation

  • Inspected all three workflow patches at head 1a9301b and the current staging versions.
  • Confirmed the PR CI run passed unit and stub E2E tests.
  • Confirmed the branch is 36 commits behind staging and conflicts in tests.yml.
  • Did not exercise PyPI OIDC publishing or verify the Trusted Publisher configuration.

Comment thread .github/workflows/publish-staging.yml
Add a staging rc pre-release stream mirroring cowork-server's
publish-staging.yml, so cowork-server staging consumes an immutable,
versioned anton-agent artifact instead of a mutable git branch or a
hand-pinned commit.

- publish-staging.yml (new): push to staging -> validate -> PEP 440 rc
  release (2.YY.M.DD.SEQrcN) -> PyPI trusted publish -> notify.
- release.yml: gate the stable stream on the same reusable tests and
  scope permissions/notify per job, matching cowork-server's publish.yml.
- tests.yml: becomes a reusable (workflow_call) called by both publishers
  and drops the standalone branch-push run + notify, so a push validates
  once inside its release run tree.

Unlike cowork-server, anton has nothing to self-pin (it does not depend on
itself), so this needs no anton-agent pin step or version-pretend override:
hatch-vcs derives the rc version from the tag on a clean checkout.

Requires a one-time PyPI Trusted Publisher registration for
publish-staging.yml (environment: pypi) on the anton-agent project before
the publish job can succeed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@pnewsam
pnewsam force-pushed the paul/eng-1159-anton-staging-rc branch from 1a9301b to 549cf3e Compare August 11, 2026 18:15
@pnewsam

pnewsam commented Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review. Addressed the blocking item and confirmed the rest.

Rebased onto current staging (was 36 behind, non-mergeable) — now 0 commits behind at 549cf3e4. The only conflict was .github/workflows/tests.yml; release.yml (untouched by staging) and the new publish-staging.yml applied cleanly.

tests.yml conflict resolved to preserve current staging test steps while keeping this PR's reusable-workflow change:

  • Kept staging's Check uv.lock matches pyproject.toml (uv lock --check) step.
  • Kept staging's --group dev on both the unit and stub-e2e pytest steps (this PR had branched from the older --extra dev).
  • Applied workflow_call: and removed the standalone push: trigger.
  • Removed the in-file notify job — failure notification is now carried by the calling publishers (release.yml needs [unit-tests, auto-release, publish, e2e]; publish-staging.yml needs [unit-tests, release, publish]), so direct-push alerting isn't lost, just relocated per the reusable pattern.

All three workflows pass YAML validation, and release.yml's reference to tests_e2e_release.yml resolves against staging.

PyPI Trusted Publisher for publish-staging.yml (environment pypi): still needs to be registered on the anton-agent project before merge — flagging so it's done ahead of the merge push entering the tag/publish pipeline.

Strict RC pairing (non-blocking): agreed this is a cowork-server consumer change ("latest by upload time, pin only if RC" → "latest RC specifically"). Tracking it as a follow-up rather than folding it into this producer PR.

The staging CI notify job previously passed
freeze-scoped: ${{ github.ref_name == 'staging' }} (true on staging), so
release-blocking escalation was reserved for the freeze window. Moving the
alert into publish-staging.yml dropped that flag, which would page the eng
channel on every midweek staging failure. This publisher is staging-only, so
hardcode freeze-scoped: true.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@pnewsam

pnewsam commented Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the freeze-scoped comment in 4cdd4f64.

publish-staging.yml's notify job now passes freeze-scoped: true. Since this publisher only runs on staging (if: github.ref == 'refs/heads/staging'), that hardcodes the same policy the shared CI notify previously applied via freeze-scoped: ${{ github.ref_name == 'staging' }} — release-blocking escalation stays reserved for the freeze window instead of paging on every midweek staging failure. release.yml (main) intentionally omits the flag, since main failures always block.

@lucas-koontz

Copy link
Copy Markdown
Contributor

Thanks for the review. Addressed the blocking item and confirmed the rest.

Rebased onto current staging (was 36 behind, non-mergeable) — now 0 commits behind at 549cf3e4. The only conflict was .github/workflows/tests.yml; release.yml (untouched by staging) and the new publish-staging.yml applied cleanly.

tests.yml conflict resolved to preserve current staging test steps while keeping this PR's reusable-workflow change:

* Kept staging's `Check uv.lock matches pyproject.toml` (`uv lock --check`) step.

* Kept staging's `--group dev` on both the unit and stub-e2e pytest steps (this PR had branched from the older `--extra dev`).

* Applied `workflow_call:` and removed the standalone `push:` trigger.

* Removed the in-file `notify` job — failure notification is now carried by the calling publishers (`release.yml` needs `[unit-tests, auto-release, publish, e2e]`; `publish-staging.yml` needs `[unit-tests, release, publish]`), so direct-push alerting isn't lost, just relocated per the reusable pattern.

All three workflows pass YAML validation, and release.yml's reference to tests_e2e_release.yml resolves against staging.

PyPI Trusted Publisher for publish-staging.yml (environment pypi): still needs to be registered on the anton-agent project before merge — flagging so it's done ahead of the merge push entering the tag/publish pipeline.

Strict RC pairing (non-blocking): agreed this is a cowork-server consumer change ("latest by upload time, pin only if RC" → "latest RC specifically"). Tracking it as a follow-up rather than folding it into this producer PR.

image

@lucas-koontz lucas-koontz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Read the whole diff at 4cdd4f6, plus calver-release.yml and notify-main-failure.yml against it. The permission grants match the called workflows' declarations exactly, the notify aggregation handles skipped needs correctly, and the recovered lookup keys on this file's path, so the first green run stays quiet. That all checks out.

Nothing blocks. Two notes inline: a version-skew edge when one commit carries two rc tags (with a one-env-var fix), and the two release docs that still describe main as the only publish stream.

The PyPI side is ready: I registered publish-staging.yml (environment pypi) on anton-agent, so the pre-merge gate in the body is cleared. Workflow naming across repos (release.yml here vs publish.yml in cowork-server) is a follow-up I'll take; the rename has to move the trusted-publisher registration in lockstep anyway.

Comment on lines +72 to +73
- name: Build package
run: uv build

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue: If someone clicks 'Re-run all jobs', or dispatches this workflow on an unchanged staging head, calver-release mints a second rc tag on a commit that already has one. The build step asks git which version to build, and git describe answers with the older tag: the run that minted rc2 builds rc1, and PyPI rejects the upload as a duplicate. Pinning the build to the version the release job just minted removes the guesswork (the lines 15-18 header comment would need updating to match). Same edge on release.yml re-runs.

Suggested change
- name: Build package
run: uv build
- name: Build package
env:
# A re-run or re-dispatch on an already-tagged head leaves two CalVer
# tags on one commit and `git describe` resolves the older one; build
# exactly the version the release job minted.
SETUPTOOLS_SCM_PRETEND_VERSION: ${{ needs.release.outputs.version }}
run: uv build

# consumes an immutable, versioned Anton artifact instead of a mutable git branch
# or a hand-pinned commit (ENG-1159). Resolvers ignore pre-releases unless a
# specifier names one, and PyPI's `info.version` (which the prod desktop updater
# reads) excludes them, so prod installs can never pick these up.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue: README's 'Releasing' section and docs/docs/developer/release-and-versioning.md still describe push-to-main as the only publish path and say only the release workflow creates v* tags. The first staging push after merge contradicts both: rc tag, GitHub pre-release, PyPI upload. Worth a short staging-rc paragraph in both while this is fresh.

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.

2 participants