Repository navigation
voice-agents-python: a hand publish runs on the release tag; dry runs for every registry, apart from real publishes #8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,23 +1,77 @@ | ||
| # Runs when release-please publishes a release, and by hand to publish again if a run failed. | ||
| # Runs when release-please publishes a release, and by hand to publish again if a run failed. By hand with | ||
| # `dry_run`, it only exchanges the job's OIDC token with PyPI, which checks the trusted publisher without | ||
| # publishing. A hand publish runs on the release tag, never on a branch: `gh workflow run publish-pypi.yml | ||
| # --ref v<version>`. | ||
| # | ||
| # PyPI is pushed by trusted publishing: the job's OIDC token is exchanged for a short-lived upload token against | ||
| # the publisher registered on PyPI (repository sentdm/sent-dm-voice-agents-python, workflow publish-pypi.yml, | ||
| # environment blank). No long-lived key exists anywhere. | ||
| name: Publish PyPI | ||
| on: | ||
| workflow_dispatch: | ||
| inputs: | ||
| dry_run: | ||
| description: Exchange the OIDC token with PyPI and stop, to check the trusted publisher without publishing | ||
| type: boolean | ||
| default: false | ||
| release: | ||
| types: [published] | ||
|
|
||
| concurrency: | ||
| # A dry run never queues behind, or displaces, a real publish. | ||
| group: publish-pypi${{ inputs.dry_run && '-dry-run' || '' }} | ||
| cancel-in-progress: false | ||
|
|
||
| jobs: | ||
| publish: | ||
| name: publish | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| contents: read | ||
| id-token: write # PyPI trusted publishing | ||
| id-token: write | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| with: | ||
| persist-credentials: false | ||
| - name: The version to publish | ||
| # 0.0.0 is the template's placeholder: the repository has no release yet. A hand run publishes the | ||
| # release tag whose version pyproject.toml carries, so main's later commits never ship under an old number. | ||
| env: | ||
| DRY_RUN: ${{ inputs.dry_run }} | ||
| run: | | ||
| version="$(sed -n 's/^version = "\([^"]*\)".*/\1/p' pyproject.toml | head -1)" | ||
| echo "version=$version" | ||
| [ "$DRY_RUN" = "true" ] && exit 0 | ||
| if [ "$version" = "0.0.0" ]; then | ||
| echo "no release yet: pyproject.toml still carries 0.0.0" >&2 | ||
| exit 1 | ||
| fi | ||
| if [ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ] && { [ "$GITHUB_REF_TYPE" != "tag" ] || [ "$GITHUB_REF_NAME" != "v$version" ]; }; then | ||
| echo "a hand run publishes the release tag: gh workflow run publish-pypi.yml --ref v$version" >&2 | ||
| exit 1 | ||
|
Comment on lines
+48
to
+51
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 (optional) If a publish fails because of this workflow, a maintainer can no longer re-publish that release with a fixed workflow. Only a new release gets the package out. A hand run must use Why this was flaggedFor workflow_dispatch, GitHub reads the workflow file from the dispatched ref. The check at publish-pypi.yml:48 exits 1 unless GITHUB_REF_TYPE is tag and GITHUB_REF_NAME is v$version. So every allowed hand run uses the workflow as it stood at the release tag. Suppose a release's publish fails because of the workflow itself, for example a broken setup-uv/uv publish step or a bad flag. The fix lands on main as a Verification: Trigger: a release's publish fails because of the workflow file itself, and the fix then lands on main. The "The version to publish" step refuses any non-dry workflow_dispatch run unless the ref is the release tag. A run from main, where the fix lives, exits 1 at that check. On the base, nothing gated the ref, so a dispatch from main ran main's fixed workflow. |
||
| fi | ||
| - name: Check the trusted publisher | ||
| # The exchange PyPI makes on upload: the job's OIDC token for a short-lived upload token. A 2xx means the | ||
| # publisher on PyPI names this repository and workflow. The token is never printed. | ||
| if: ${{ inputs.dry_run }} | ||
| run: | | ||
| id_token="$(curl -fsS -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=pypi" | python3 -c 'import json, sys; print(json.load(sys.stdin).get("value", ""))')" | ||
| if [ -z "$id_token" ]; then | ||
| echo "GitHub issued no OIDC token for this job" >&2 | ||
| exit 1 | ||
| fi | ||
| code="$(curl -sS -o /dev/null -w '%{http_code}' -X POST -H 'Content-Type: application/json' -d "{\"token\": \"$id_token\"}" https://pypi.org/_/oidc/mint-token)" | ||
| case "$code" in | ||
| 2*) echo "PyPI exchanged the token (HTTP $code): the trusted publisher matches this repository and workflow" ;; | ||
| *) | ||
| echo "PyPI refused the exchange with HTTP $code: no trusted publisher matches this repository and workflow" >&2 | ||
| exit 1 ;; | ||
| esac | ||
| - uses: astral-sh/setup-uv@v6 | ||
| if: ${{ !inputs.dry_run }} | ||
| - name: Build | ||
| if: ${{ !inputs.dry_run }} | ||
| run: uv build | ||
| - name: Publish | ||
| if: ${{ !inputs.dry_run }} | ||
| run: uv publish --trusted-publishing always | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 (optional) A real release publish can now be cancelled without anyone noticing, when three real publish runs overlap. On the base branch every run went ahead. All real runs share the repo-wide group
publish-pypi. GitHub keeps one running and one pending run per group and cancels the pending one when a third arrives, whatevercancel-in-progresssays. Fix: include the version or ref in the real group, e.g.publish-pypi-${{ github.ref_name }}. Runs for different releases then never displace each other, and repeat runs of one tag stay serialized.Why this was flagged
The trigger needs three runs in the same window, for example a release event plus hand re-publish dispatches. The concurrency block at publish-pypi.yml:20-23 puts every non-dry run (release events and hand publishes) in one group,
publish-pypi, whatever the tag. GitHub concurrency allows one in-progress run and one pending run per group. A newly queued run cancels the existing pending run even with cancel-in-progress: false. Example: run A (a hand re-publish of v0.1.1) is running and run B (the release event for v0.1.2) is pending. A third dispatch C then cancels B, and v0.1.2 never publishes unless someone notices the cancelled run. The base branch had no concurrency block, so each run went ahead on its own. The comment at line 21 only says a dry run never displaces a real publish. It does not cover real publishes displacing each other, which this change introduces.Verification: Lines 20-23 put every non-dry run in one fixed group, whatever its tag or release. When a new run is queued, the existing pending run is cancelled.
cancel-in-progress: falseonly protects the run that is already in progress, not the pending one. The base workflow had noconcurrencyblock, so every run went ahead.