Skip to content

ci(desktop): resume a release that reached PyPI but not its published release - #1417

Merged
abrichr merged 3 commits into
mainfrom
fix/desktop-publish-resume
Oct 20, 2026
Merged

abrichr merged 3 commits into
mainfrom
fix/desktop-publish-resume

Conversation

@abrichr

@abrichr abrichr commented Oct 20, 2026

Copy link
Copy Markdown
Member

Summary

Desktop 0.22.0 run 39374653331 uploaded the wheel and sdist to PyPI (PyPI serves 0.22.0), then failed "Require PyPI to serve exactly these bytes" with unexpected distribution set: [... .whl.publish.attestation, ... .tar.gz.publish.attestation]. With attestations on, pypa/gh-action-pypi-publish writes those files into the dist directory. "Publish the immutable release" was skipped, so the release is still a draft with all 18 assets and the tag at a024869. A rerun would replay the same bug, and a new run refuses the partly published version.

  1. The byte check uses the attested SHA256SUMS. release_publication.py verify-pypi now takes --checksums. It requires SHA256SUMS to name the wheel and sdist, and requires the local files to be exactly those bytes. It ignores *.publish.attestation files, refuses any other extra file, and then requires PyPI to serve those digests.

  2. A dispatch resumes this exact state. The gate checks four conditions:

    • PyPI serves the version.
    • The tag exists, and its commit is on main.
    • No published release exists for it.
    • The event is workflow_dispatch.

    When all hold, the gate outputs resume=true with the tag's commit, and nothing is built. The publish job then works with the release App's token, the only token here that can see drafts:

    • It requires the release to be a draft and downloads it.
    • It runs reverify, the same check the stage job ran. That requires SHA256SUMS to name exactly this file set, and every attestation to name desktop-release.yml@refs/heads/main and the tag's commit.
    • It requires PyPI to serve the attested wheel and sdist.
    • It runs the unchanged publish step, which verifies the draft again, publishes it once and verifies the immutable release.

    Every other partly published state still refuses.

  3. The publish job now also checks the release App identity, as the stage job does.

GitHub's releases/tags/{tag} endpoint never returns a draft. The gate's github.token therefore reads a staged draft as none, so the resume condition is "no published release", and the publish job confirms the draft.

Rehearsal on the real 0.22.0 draft (read-only)

I ran the resume commands locally against the live draft and PyPI:

  • gh release download fetched 18 files.
  • reverify --source-commit a02486937… printed "Reverified 18 release files".
  • python-dist copied the wheel and sdist.
  • With fake .publish.attestation files beside them, verify-pypi --checksums upload/SHA256SUMS passed: "PyPI serves exactly [whl, tar.gz]".
  • verify_github_release.py --state draft --expected-dir upload passed against the draft.

I also confirmed the gate's inputs on main: version 0.22.0, PyPI HTTP 200, tag openadapt-desktop-v0.22.0 at a024869, and releases/tags returning 404.

Tests

  • packages/desktop/tests/test_release_publication.py:
    • a fixture dist directory with *.publish.attestation files passes;
    • a swapped wheel refuses;
    • any other extra file (including *.attestation without publish) refuses;
    • a SHA256SUMS missing the wheel or sdist refuses;
    • the CLI requires --checksums.
  • tests/test_desktop_release_resume.py:
    • the resume branch is dispatch-only and requires the tag;
    • it sets release=false and names the tag's commit, and the builds stay skipped;
    • the other partly published states still refuse;
    • publish's if, permissions and App identity check;
    • the resume step's draft, reverify and PyPI checks;
    • the publish-pypi call uses --checksums.
  • Locally: 27 root release tests and 108 Desktop release tests pass. actionlint with shellcheck is clean.

The commit type is ci(desktop), so no version bump is needed. Nothing was dispatched.

🤖 Generated with Claude Code

abrichr and others added 3 commits October 20, 2026 02:08
… release

Desktop 0.22.0 run 39374653331 uploaded the wheel and sdist to PyPI, then
failed "Require PyPI to serve exactly these bytes": with attestations on,
pypa/gh-action-pypi-publish writes *.publish.attestation files into the
dist directory, and the check refused them as an unexpected distribution
set. The draft release, its installers and the tag were never published,
and a rerun replays the same workflow.

verify-pypi now takes the expected digests from the attested SHA256SUMS
(--checksums), requires the local wheel and sdist to be those bytes,
ignores *.publish.attestation files and refuses anything else.

A dispatch on main now resumes that exact state. When PyPI serves the
version, the tag exists on main's history and no published release does,
the gate builds nothing and names the tag's commit. The publish job then
requires the release to be a draft (with the release App's token, the
only one that can see drafts), downloads it, reverifies every file against
SHA256SUMS and its attestations for that commit, requires PyPI to serve
the attested wheel and sdist, and publishes the draft as before. The
publish job now also checks the release App identity. Every other partly
published state still refuses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1414 starts Desktop releases from workflow_run. A dispatch is always a
candidate, so it reaches the release-state check and resumes. Any other
run that finds the resumable state (PyPI serves the version, the tag
exists, no published release) builds nothing and leaves a warning to
dispatch, instead of failing every Desktop commit until someone does.
The gate's release output stays with the main-unchanged step, which a
resume skips.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@abrichr

abrichr commented Oct 20, 2026

Copy link
Copy Markdown
Member Author

Merged main into this branch: #1414's workflow_run trigger is now in place. There was no force-push.

  • Dispatch: the gate classifies a dispatch as a candidate, so it reaches the release-state check and resumes.
  • workflow_run: a run that finds the resumable state (PyPI serves the version, the tag exists, and no release is published) builds nothing. It ends with a ::warning:: to dispatch. A non-Desktop commit still stops earlier, at "does not change packages/desktop".
  • Main-unchanged check: the gate's release output still comes from the main-unchanged step, which a resume skips. The resume is bound to the tag's commit, independent of main's tree.

Rehearsed on the merged code against the live 0.22.0 draft and PyPI. Each step was extracted from the workflow YAML and run read-only:

  • Gate state step as workflow_dispatch: outputs release=false, resume=true, resume-commit=a02486937dbdb62a8fdd3024730643cb69f1c6c8.
  • Gate state step as workflow_run: emits the ::warning::, outputs release=false, exits 0.
  • Publish job's resume step: "Reverified 18 release files", then "PyPI serves exactly ['openadapt_desktop-0.22.0-py3-none-any.whl', 'openadapt_desktop-0.22.0.tar.gz']", exit 0.
  • The publish step's draft check (verify_github_release.py --state draft --expected-dir upload): exit 0.

Locally, 42 root release tests and 108 Desktop release tests pass, and actionlint with shellcheck is clean.

@abrichr
abrichr merged commit fb19f8c into main Oct 20, 2026
42 checks passed
@abrichr
abrichr deleted the fix/desktop-publish-resume branch October 20, 2026 06:37
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