-
Notifications
You must be signed in to change notification settings - Fork 0
ci: codify release-on-tag + PyPI provenance pipeline (fixes VER-001, SUPPLY-001) #47
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 |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| name: release-drift | ||
|
|
||
| # Standing drift gate: fails loud the moment the tag, pyproject.toml version, | ||
| # PyPI's latest published version, GitHub Release existence, or PyPI | ||
| # attestation coverage disagree with each other. This is the mechanism behind | ||
| # Jake's 2026-09-05 decision -- "always codified workflows so we don't ever | ||
| # have drifts" -- it is the check that would have caught VER-001 (no GitHub | ||
| # Release for the current tag, PyPI a version behind) and SUPPLY-001 (no | ||
| # provenance/attestation on the published package) on day one instead of | ||
| # waiting for an external GA validator to find them. | ||
| # | ||
| # All comparison logic lives in scripts/release/check_drift.py so it is | ||
| # testable locally with zero CI round-trip: | ||
| # python3 scripts/release/check_drift.py | ||
| # | ||
| # Exit codes (from the script, passed straight through): | ||
| # 0 in sync -> job succeeds | ||
| # 1 drift detected -> job fails (this is a REAL, verified disagreement) | ||
| # 2 a source was unreadable -> job fails (network/API blip is NEVER | ||
| # silently treated as "in sync" -- that would hide a real drift behind | ||
| # a flaky read) | ||
|
|
||
| on: | ||
| push: | ||
| branches: [main] | ||
| schedule: | ||
| - cron: "17 6 * * *" # daily, off the hour to avoid GitHub Actions' top-of-hour scheduling crunch | ||
| workflow_dispatch: | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| concurrency: | ||
|
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. P3: All three triggers share the concurrency group because each resolves Prompt for AI agents |
||
| group: release-drift-${{ github.ref }} | ||
| cancel-in-progress: true | ||
|
|
||
| jobs: | ||
| drift-check: | ||
| name: tag / pyproject / PyPI / release / attestation agreement | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 10 | ||
| steps: | ||
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | ||
| with: | ||
| fetch-depth: 0 # need full tag history, not just the push's shallow clone | ||
| persist-credentials: false | ||
|
|
||
| - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 | ||
| with: | ||
| python-version: "3.12" | ||
|
|
||
| - name: Run the drift check | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| run: python3 scripts/release/check_drift.py --repo-root . | ||
|
Comment on lines
+52
to
+55
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. 🩺 Stability & Availability | 🔵 Trivial Make scheduled drift failures produce an actionable notification. The scheduled job runs 🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,136 +1,264 @@ | ||||||||||
| name: release | ||||||||||
|
|
||||||||||
| # Builds sdist + wheel and publishes to PyPI on `v*` tags using PyPI Trusted | ||||||||||
| # Publishing (OIDC) — no long-lived API token stored in this repo. On pull | ||||||||||
| # requests the same build runs as a dry-run: build, `twine check`, install | ||||||||||
| # the wheel into a throwaway venv, and import `Wave` from it. That dry-run | ||||||||||
| # job never touches PyPI (no `id-token` permission, no publish step). | ||||||||||
| # Codified release-on-tag pipeline for wave-sdk (PyPI: wave-sdk, PyPI Trusted | ||||||||||
| # Publishing, no stored token -- decision IGV-D-010: no per-repo publish | ||||||||||
| # secrets). Four jobs, each with its own minimal `permissions:`: | ||||||||||
| # | ||||||||||
| # One-time setup before the first `v*` tag: register this repo + workflow as | ||||||||||
| # a Trusted Publisher on the `wave-sdk` PyPI project (see AGENTS.md / PR body | ||||||||||
| # for the exact fields — this workflow cannot self-register). | ||||||||||
| # resolve-ref -- resolves + strictly validates the tag (push ref or | ||||||||||
| # dispatch input) BEFORE it reaches any git/python/gh command as | ||||||||||
| # text: format (v<semver>), existence (git rev-parse), and trust | ||||||||||
| # (the tag's commit must be an ancestor of origin/main) -- every | ||||||||||
| # later job checks out that commit BY SHA, not by the (mutable) | ||||||||||
| # tag name. A workflow_dispatch `tag` input is | ||||||||||
| # attacker-influencable text; requiring ancestry means every | ||||||||||
| # downstream job only ever builds/publishes/releases code that | ||||||||||
| # was already merged and reviewed on the default branch. | ||||||||||
| # verify -- checks out the resolved sha, asserts tag == pyproject.toml | ||||||||||
| # version == wave_sdk.__version__ (fails loud on any mismatch), | ||||||||||
| # installs, runs the full pytest suite, builds sdist+wheel, | ||||||||||
| # and `twine check`s them. Nothing downstream runs on a red verify. | ||||||||||
| # publish -- id-token: write only. Publishes via PyPI Trusted Publishing | ||||||||||
| # (pypa/gh-action-pypi-publish, OIDC, NO API token) with PEP 740 | ||||||||||
| # attestations enabled. If PyPI already has this exact version | ||||||||||
| # (checked live against pypi.org before publishing), the publish | ||||||||||
| # step is skipped with a clear log line and the job still | ||||||||||
| # succeeds -- a re-run must be idempotent, not a hard failure. | ||||||||||
| # release -- contents: write only. Creates the GitHub Release for the tag | ||||||||||
| # with generated notes and the sdist+wheel attached. Idempotent: | ||||||||||
| # if the release already exists, it edits/uploads onto it | ||||||||||
| # instead of failing. | ||||||||||
| # | ||||||||||
| # Triggers on `v*` tag pushes AND `workflow_dispatch` with a `tag` input, so | ||||||||||
| # an existing tag (e.g. the current v2.1.0, pushed before this workflow | ||||||||||
| # existed) can be backfilled on demand: | ||||||||||
| # gh workflow run release.yml --repo wave-av/sdk-python --ref main -f tag=v2.1.0 | ||||||||||
| # | ||||||||||
| # Every `uses:` below is pinned to a 40-character commit SHA with the human | ||||||||||
| # version in a trailing comment -- a tag ref (`@v6`) is a mutable pointer an | ||||||||||
| # upstream maintainer (or an attacker who compromises their account) can | ||||||||||
| # repoint without your review ever seeing a new commit. | ||||||||||
| # | ||||||||||
| # One-time operator setup (this workflow cannot self-register): add this repo | ||||||||||
| # + workflow as a PyPI Trusted Publisher for the `wave-sdk` project. See the | ||||||||||
| # PR body for the exact pypi.org settings path. | ||||||||||
|
|
||||||||||
| on: | ||||||||||
| pull_request: | ||||||||||
| push: | ||||||||||
| tags: ["v*"] | ||||||||||
| workflow_dispatch: | ||||||||||
| inputs: | ||||||||||
| tag: | ||||||||||
| description: "Tag to verify/publish/release, e.g. v2.1.0 (backfill an existing tag)" | ||||||||||
| required: true | ||||||||||
| type: string | ||||||||||
|
|
||||||||||
| permissions: | ||||||||||
| contents: read | ||||||||||
|
|
||||||||||
| concurrency: | ||||||||||
| group: release-${{ github.ref }} | ||||||||||
| cancel-in-progress: true | ||||||||||
| group: release-${{ github.event.inputs.tag || github.ref }} | ||||||||||
|
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. P1: Key the concurrency group off the bare tag name for both trigger types. Otherwise a tag push and a workflow-dispatch backfill for the same tag can run concurrently and defeat the publish and release idempotency checks. Prompt for AI agents
Suggested change
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. P2: A tag push and a manual backfill for the same tag use different concurrency groups, so they can publish concurrently and one will fail on PyPI's duplicate-file rejection. Normalize both events to the tag name, such as Prompt for AI agents
Suggested change
|
||||||||||
| cancel-in-progress: false | ||||||||||
|
Comment on lines
58
to
+60
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.
|
||||||||||
|
|
||||||||||
| jobs: | ||||||||||
| build: | ||||||||||
| name: build + dry-run check | ||||||||||
| # --------------------------------------------------------------------------- | ||||||||||
| # Resolve + strictly validate the tag before it reaches any command. A | ||||||||||
| # dispatch input is attacker-influencable text; a full-anchored regex (not a | ||||||||||
| # glob) refuses anything that isn't exactly `v<semver>`. Beyond format, this | ||||||||||
| # job also proves the tag's commit EXISTS and is an ANCESTOR of origin/main | ||||||||||
| # before any downstream job checks it out. Outputs both `tag` (the name, for | ||||||||||
| # assert_version.py/gh release) and `sha` (the validated commit, used for | ||||||||||
| # every downstream checkout) -- never interpolated into a shell body, only | ||||||||||
| # passed via env:/outputs. | ||||||||||
| # --------------------------------------------------------------------------- | ||||||||||
| resolve-ref: | ||||||||||
| name: Resolve release tag | ||||||||||
| runs-on: ubuntu-latest | ||||||||||
| timeout-minutes: 10 | ||||||||||
| timeout-minutes: 5 | ||||||||||
| permissions: | ||||||||||
| contents: read | ||||||||||
| outputs: | ||||||||||
| tag: ${{ steps.resolve.outputs.tag }} | ||||||||||
| sha: ${{ steps.resolve.outputs.sha }} | ||||||||||
| steps: | ||||||||||
| # Full history + all tags -- needed so `git rev-parse`/`git merge-base` | ||||||||||
| # below can see the tag and `origin/main`. No `ref:` override: this | ||||||||||
| # checks out whatever triggered the run (the pushed tag, or the branch a | ||||||||||
| # dispatch was run from) -- never the untrusted dispatch input -- and no | ||||||||||
| # step here executes anything from that tree (metadata reads only). | ||||||||||
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | ||||||||||
| with: | ||||||||||
| fetch-depth: 0 | ||||||||||
| persist-credentials: false | ||||||||||
|
|
||||||||||
| - name: Resolve and validate the tag to release | ||||||||||
| id: resolve | ||||||||||
| env: | ||||||||||
| DISPATCH_TAG: ${{ github.event.inputs.tag }} | ||||||||||
| PUSH_TAG: ${{ github.ref_name }} | ||||||||||
| run: | | ||||||||||
| set -euo pipefail | ||||||||||
| TAG="${DISPATCH_TAG:-$PUSH_TAG}" | ||||||||||
|
|
||||||||||
| # 1. Format -- full-anchored regex, not a glob -- refuses anything | ||||||||||
| # that isn't exactly `v<semver>`. This value flows into a `ref:` | ||||||||||
| # on downstream checkout steps (as the resolved sha, never the | ||||||||||
| # tag text itself), so it is validated BEFORE use. | ||||||||||
| if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$ ]]; then | ||||||||||
| echo "::error::refusing to process ref '$TAG' — expected v<semver>" | ||||||||||
| exit 1 | ||||||||||
| fi | ||||||||||
|
|
||||||||||
| # 2. Existence -- the tag must resolve to a real commit object in | ||||||||||
| # THIS repo, not just text that happens to look right. | ||||||||||
| if ! SHA="$(git rev-parse --verify --quiet "refs/tags/${TAG}^{commit}")"; then | ||||||||||
| echo "::error::tag '$TAG' does not exist in this repository (or does not point at a commit)" | ||||||||||
| exit 1 | ||||||||||
| fi | ||||||||||
|
|
||||||||||
| # 3. Trust -- a `workflow_dispatch` input is attacker-influencable | ||||||||||
| # text, and a dispatch run's ambient GITHUB_REF/Actions-cache | ||||||||||
| # scope is whatever branch it was dispatched on (normally | ||||||||||
| # `main`), even though the CODE this workflow goes on to execute | ||||||||||
| # (pytest, build, twine, PyPI publish) is the resolved tag's | ||||||||||
| # tree. Format-validity alone does not prove that tree was ever | ||||||||||
| # reviewed -- a tag pushed off an arbitrary/unmerged branch | ||||||||||
| # passes the regex above just as easily. Requiring the tag's | ||||||||||
| # commit to be an ANCESTOR of origin/main means every later job | ||||||||||
| # only ever runs code that was already merged (and reviewed) on | ||||||||||
| # the default branch -- never a bespoke, unmerged payload | ||||||||||
| # smuggled in via a crafted tag. | ||||||||||
| if ! git merge-base --is-ancestor "$SHA" origin/main; then | ||||||||||
| echo "::error::tag '$TAG' (commit $SHA) is not reachable from origin/main — refusing to build/publish/release an unmerged or unrecognized ref" | ||||||||||
| exit 1 | ||||||||||
| fi | ||||||||||
|
|
||||||||||
| echo "resolved tag: $TAG -> $SHA (verified: v<semver> shape, exists, ancestor of origin/main)" | ||||||||||
| echo "tag=$TAG" >> "$GITHUB_OUTPUT" | ||||||||||
| echo "sha=$SHA" >> "$GITHUB_OUTPUT" | ||||||||||
|
|
||||||||||
| verify: | ||||||||||
| name: verify (version match, tests, build) | ||||||||||
| needs: [resolve-ref] | ||||||||||
| runs-on: ubuntu-latest | ||||||||||
| timeout-minutes: 15 | ||||||||||
| permissions: | ||||||||||
| contents: read | ||||||||||
| outputs: | ||||||||||
| version: ${{ steps.version.outputs.version }} | ||||||||||
| steps: | ||||||||||
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | ||||||||||
| with: | ||||||||||
| ref: ${{ needs.resolve-ref.outputs.sha }} | ||||||||||
| persist-credentials: false | ||||||||||
|
|
||||||||||
| - name: Derive version from the resolved tag | ||||||||||
| id: version | ||||||||||
| env: | ||||||||||
| TAG: ${{ needs.resolve-ref.outputs.tag }} | ||||||||||
| run: echo "version=${TAG#v}" >> "$GITHUB_OUTPUT" | ||||||||||
|
|
||||||||||
| - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 | ||||||||||
| with: | ||||||||||
| python-version: "3.12" | ||||||||||
|
|
||||||||||
| - name: Install build tooling | ||||||||||
| - name: Install the SDK with every extra its tests need | ||||||||||
| run: | | ||||||||||
| python -m pip install --upgrade pip | ||||||||||
| pip install -e ".[dev,realtime,x402]" | ||||||||||
| pip install build twine | ||||||||||
|
|
||||||||||
| - name: Assert tag == pyproject.toml version == wave_sdk.__version__ | ||||||||||
|
Comment on lines
+164
to
+170
|
||||||||||
| run: python3 scripts/release/assert_version.py "${{ needs.resolve-ref.outputs.tag }}" | ||||||||||
|
|
||||||||||
| - name: pytest (full suite, the checked-out tag) | ||||||||||
|
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. P2: The release gate tests the editable checkout, not the wheel it uploads, so a packaging omission can pass pytest and Prompt for AI agents
Comment on lines
+170
to
+173
|
||||||||||
| run: python -m pytest -q | ||||||||||
|
|
||||||||||
| - name: Build sdist + wheel | ||||||||||
Check failureCode scanning / CodeQL Cache Poisoning via execution of untrusted code High
Potential cache poisoning in the context of the default branch due to privilege checkout of untrusted code from
needs.resolve-ref.outputs.sha Error loading related location Loading workflow_dispatch Error loading related location Loading |
||||||||||
| run: python -m build | ||||||||||
|
|
||||||||||
| - name: twine check | ||||||||||
Check failureCode scanning / CodeQL Cache Poisoning via execution of untrusted code High
Potential cache poisoning in the context of the default branch due to privilege checkout of untrusted code from
needs.resolve-ref.outputs.sha Error loading related location Loading workflow_dispatch Error loading related location Loading |
||||||||||
| run: twine check dist/* | ||||||||||
|
|
||||||||||
| - name: Create fresh venv (no repo on sys.path) | ||||||||||
| run: python -m venv "$RUNNER_TEMP/dry-run" | ||||||||||
|
|
||||||||||
| - name: Install the built wheel | ||||||||||
| run: | | ||||||||||
| WHEEL=$(ls dist/*.whl) | ||||||||||
| "$RUNNER_TEMP/dry-run/bin/pip" install --upgrade pip | ||||||||||
| "$RUNNER_TEMP/dry-run/bin/pip" install "$WHEEL" | ||||||||||
|
|
||||||||||
| - name: Import check (installed wheel, run away from the repo) | ||||||||||
| working-directory: ${{ runner.temp }}/dry-run | ||||||||||
| run: | | ||||||||||
| bin/python -c " | ||||||||||
| from wave_sdk import Wave | ||||||||||
| import wave_sdk | ||||||||||
| print('wave_sdk', wave_sdk.__version__, 'imported OK from', wave_sdk.__file__) | ||||||||||
| print('Wave facade:', Wave) | ||||||||||
| " | ||||||||||
|
|
||||||||||
| - name: Upload dist | ||||||||||
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | ||||||||||
| with: | ||||||||||
| name: dist | ||||||||||
| name: dist-${{ needs.resolve-ref.outputs.tag }} | ||||||||||
| path: dist/ | ||||||||||
| retention-days: 7 | ||||||||||
| retention-days: 14 | ||||||||||
|
|
||||||||||
| publish: | ||||||||||
| name: publish to PyPI | ||||||||||
| needs: build | ||||||||||
| if: startsWith(github.ref, 'refs/tags/v') | ||||||||||
| needs: [resolve-ref, verify] | ||||||||||
| runs-on: ubuntu-latest | ||||||||||
| timeout-minutes: 10 | ||||||||||
| environment: | ||||||||||
| name: pypi | ||||||||||
| url: https://pypi.org/project/wave-sdk/ | ||||||||||
| permissions: | ||||||||||
| # OIDC token for PyPI Trusted Publishing — no API token secret needed. | ||||||||||
| id-token: write | ||||||||||
| contents: read | ||||||||||
| steps: | ||||||||||
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | ||||||||||
| with: | ||||||||||
| ref: ${{ needs.resolve-ref.outputs.sha }} | ||||||||||
| persist-credentials: false | ||||||||||
|
|
||||||||||
| - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 | ||||||||||
| with: | ||||||||||
| python-version: "3.12" | ||||||||||
|
|
||||||||||
| - name: Install build tooling | ||||||||||
| run: | | ||||||||||
| python -m pip install --upgrade pip | ||||||||||
| pip install build | ||||||||||
|
|
||||||||||
| - name: Build sdist + wheel | ||||||||||
| run: python -m build | ||||||||||
| - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | ||||||||||
| with: | ||||||||||
| name: dist-${{ needs.resolve-ref.outputs.tag }} | ||||||||||
| path: dist | ||||||||||
|
|
||||||||||
| - name: Verify tag matches package version | ||||||||||
| - name: Check whether PyPI already has this exact version | ||||||||||
| id: pypi_check | ||||||||||
| run: | | ||||||||||
| TAG_VERSION="${GITHUB_REF_NAME#v}" | ||||||||||
| PKG_VERSION=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])") | ||||||||||
| if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then | ||||||||||
| echo "tag v$TAG_VERSION does not match pyproject.toml version $PKG_VERSION" | ||||||||||
| exit 1 | ||||||||||
| fi | ||||||||||
| set -e | ||||||||||
| RESULT=$(python3 scripts/release/pypi_version_exists.py "${{ needs.verify.outputs.version }}") | ||||||||||
| echo "exists=$RESULT" >> "$GITHUB_OUTPUT" | ||||||||||
|
|
||||||||||
| - name: Publish to PyPI (Trusted Publishing, OIDC) | ||||||||||
| - name: Publish to PyPI (Trusted Publishing, OIDC, PEP 740 attestations) | ||||||||||
| if: steps.pypi_check.outputs.exists == 'false' | ||||||||||
|
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. P1: When the existing PyPI version lacks PEP 740 provenance, this condition skips the only attestation-enabled upload and lets the release succeed. Check provenance for the existing files and fail loudly when it is missing instead of treating version existence alone as success. Prompt for AI agentsThere 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. P1: When the target version already exists without provenance, this condition skips the only attested upload while the Prompt for AI agents |
||||||||||
| uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 | ||||||||||
|
Comment on lines
+219
to
224
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. Suggestion: If the version exists on PyPI without provenance, this check skips publishing and cannot repair the missing attestation, leaving the drift unresolved. [incomplete implementation] Assessment: 🟠 Prompt for AI Agent 🤖This is a comment left during a code review.
**Path:** .github/workflows/release.yml
**Line:** 219:224
**Comment:**
*Incomplete Implementation: If the version exists on PyPI without provenance, this check skips publishing and cannot repair the missing attestation, leaving the drift unresolved.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix |
||||||||||
| with: | ||||||||||
| attestations: true | ||||||||||
|
|
||||||||||
| - name: Skip publish (already on PyPI) | ||||||||||
| if: steps.pypi_check.outputs.exists == 'true' | ||||||||||
| run: echo "::notice::wave-sdk ${{ needs.verify.outputs.version }} is already on PyPI -- skipping publish, proceeding to release job" | ||||||||||
|
Comment on lines
189
to
+230
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. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift Handle pre-existing PyPI versions without PEP 740 provenance When the exact version exists, 🧰 Tools🪛 zizmor (1.29.0)[info] 149-149: code injection via template expansion (template-injection): may expand into attacker-controllable code (template-injection) [info] 160-160: code injection via template expansion (template-injection): may expand into attacker-controllable code (template-injection) [warning] 128-128: permissions without explanatory comments (undocumented-permissions): needs an explanatory comment (undocumented-permissions) 🤖 Prompt for AI Agents |
||||||||||
|
|
||||||||||
| - name: Post-publish verification (PyPI + fresh install) | ||||||||||
| release: | ||||||||||
| name: create GitHub Release | ||||||||||
| needs: [resolve-ref, publish] | ||||||||||
| runs-on: ubuntu-latest | ||||||||||
| timeout-minutes: 10 | ||||||||||
| permissions: | ||||||||||
| contents: write | ||||||||||
| steps: | ||||||||||
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | ||||||||||
| with: | ||||||||||
| ref: ${{ needs.resolve-ref.outputs.sha }} | ||||||||||
| persist-credentials: false | ||||||||||
|
|
||||||||||
| - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | ||||||||||
| with: | ||||||||||
| name: dist-${{ needs.resolve-ref.outputs.tag }} | ||||||||||
| path: dist | ||||||||||
|
|
||||||||||
| - name: Create or update the GitHub Release for this tag (idempotent) | ||||||||||
| env: | ||||||||||
| GH_TOKEN: ${{ github.token }} | ||||||||||
| TAG: ${{ needs.resolve-ref.outputs.tag }} | ||||||||||
| run: | | ||||||||||
| TAG_VERSION="${GITHUB_REF_NAME#v}" | ||||||||||
| for i in 1 2 3 4 5 6 7 8; do | ||||||||||
| AVAILABLE=$(pip index versions wave-sdk 2>/dev/null | grep -o "$TAG_VERSION" || true) | ||||||||||
| if [ -n "$AVAILABLE" ]; then break; fi | ||||||||||
| echo "waiting for PyPI to index wave-sdk==$TAG_VERSION (attempt $i)" | ||||||||||
| sleep 15 | ||||||||||
| done | ||||||||||
| python -m venv "$RUNNER_TEMP/verify" | ||||||||||
| "$RUNNER_TEMP/verify/bin/pip" install --upgrade pip | ||||||||||
| "$RUNNER_TEMP/verify/bin/pip" install "wave-sdk==$TAG_VERSION" | ||||||||||
| "$RUNNER_TEMP/verify/bin/python" -c " | ||||||||||
| from wave_sdk import Wave | ||||||||||
| import wave_sdk | ||||||||||
| assert wave_sdk.__version__ == '$TAG_VERSION', wave_sdk.__version__ | ||||||||||
| print('verified wave-sdk', wave_sdk.__version__, 'installed from PyPI, Wave facade OK') | ||||||||||
| " | ||||||||||
| set -e | ||||||||||
| if gh release view "$TAG" --repo "${{ github.repository }}" >/dev/null 2>&1; then | ||||||||||
| echo "::notice::release $TAG already exists -- uploading dist assets (clobber) instead of creating" | ||||||||||
| gh release upload "$TAG" dist/* --repo "${{ github.repository }}" --clobber | ||||||||||
| else | ||||||||||
| gh release create "$TAG" dist/* \ | ||||||||||
| --repo "${{ github.repository }}" \ | ||||||||||
| --title "wave-sdk $TAG" \ | ||||||||||
| --generate-notes | ||||||||||
|
Comment on lines
+256
to
+263
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. Suggestion: Concurrent runs can both observe no release and call Assessment: 🟠 Prompt for AI Agent 🤖This is a comment left during a code review.
**Path:** .github/workflows/release.yml
**Line:** 256:263
**Comment:**
*Race Condition: Concurrent runs can both observe no release and call `gh release create`; one then fails, breaking the claimed idempotent release behavior.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix |
||||||||||
| fi | ||||||||||
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.
P2: Add an idempotent failure notification for scheduled drift checks so a detected release drift reaches the responsible maintainers instead of remaining visible only in Actions.
Prompt for AI agents