From caf205dd822d482bd6c54626464557e2deafb875 Mon Sep 17 00:00:00 2001 From: rabii-chaarani Date: Thu, 28 May 2026 15:40:40 +0930 Subject: [PATCH 1/2] fix(release): download release assets without git checkout The PyPI publish job intentionally does not check out the repository, but gh release download tried to infer the repository from .git and failed before publishing. Pass GITHUB_REPOSITORY explicitly so the job can retrieve release assets in a checkout-free workspace. Constraint: Publish job should stay minimal and use trusted publishing without requiring a checkout. Rejected: Add actions/checkout to publish-pypi | unnecessary workspace state for a release asset download. Confidence: high Scope-risk: narrow Directive: Keep checkout-free release jobs explicit about their repository when using gh. Tested: .venv/bin/python -m pytest tests/test_release_workflows.py -q Tested: .venv/bin/ruff check tests/test_release_workflows.py Tested: .venv/bin/python scripts/check_release_gate.py Tested: .venv/bin/ruff check . Tested: .venv/bin/python -m pytest -q Not-tested: Live PyPI publish rerun after merge. --- .github/workflows/release.yml | 2 +- tests/test_release_workflows.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9e534bc..2da3a61 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -276,7 +276,7 @@ jobs: shell: bash run: | mkdir -p dist - gh release download "$RELEASE_TAG" --dir dist --pattern "*.whl" --pattern "*.tar.gz" + gh release download "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --dir dist --pattern "*.whl" --pattern "*.tar.gz" python - <<'PY' from pathlib import Path diff --git a/tests/test_release_workflows.py b/tests/test_release_workflows.py index 370e62f..154d447 100644 --- a/tests/test_release_workflows.py +++ b/tests/test_release_workflows.py @@ -175,7 +175,7 @@ def test_workflows_avoid_node20_artifact_actions() -> None: def test_release_workflow_downloads_distributions_from_github_release() -> None: text = Path(".github/workflows/release.yml").read_text(encoding="utf-8") - assert 'gh release download "$RELEASE_TAG" --dir dist' in text + assert 'gh release download "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --dir dist' in text assert "release {artifacts=} does not include a wheel" in text assert "release {artifacts=} does not include a source distribution" in text From 969b4dd291594dad49c308f4204e84cfe88c1f9c Mon Sep 17 00:00:00 2001 From: rabii-chaarani Date: Thu, 28 May 2026 15:50:01 +0930 Subject: [PATCH 2/2] fix(release): allow publishing existing release tags A failed publish job cannot reliably be recovered by rerunning an old workflow attempt after the workflow file is fixed. Add a protected manual path for strict vX.Y.Z tags so an existing GitHub Release can be gated and published from the current workflow without creating another release. Constraint: The v0.2.0 release already exists with uploaded artifacts, but the publish job failed before PyPI upload. Rejected: Rely on rerunning the old failed job | it uses the original workflow attempt and still lacks the repository-explicit gh call. Confidence: high Scope-risk: moderate Directive: Existing-tag publishing must remain behind the pypi environment and production release gate. Tested: .venv/bin/python -m pytest tests/test_release_workflows.py -q Tested: .venv/bin/ruff check tests/test_release_workflows.py Tested: .venv/bin/python scripts/check_release_gate.py Tested: .venv/bin/ruff check . Tested: .venv/bin/python -m pytest -q Not-tested: Live workflow_dispatch publish-existing-tag=v0.2.0. --- .github/workflows/release.yml | 100 +++++++++++++++++++++++++++++++- tests/test_release_workflows.py | 16 ++++- 2 files changed, 114 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2da3a61..62c8ed0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,6 +11,11 @@ on: required: false type: boolean default: false + publish-existing-tag: + description: Publish an existing GitHub release tag to PyPI without creating a new release. + required: false + type: string + default: "" permissions: contents: read @@ -78,7 +83,7 @@ jobs: release-please: name: release please - if: ${{ !inputs.pypi-environment-smoke }} + if: ${{ github.event_name != 'workflow_dispatch' || (!inputs.pypi-environment-smoke && inputs.publish-existing-tag == '') }} runs-on: ubuntu-latest timeout-minutes: 10 permissions: @@ -289,3 +294,96 @@ jobs: - name: Publish distributions to PyPI uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1 + + publish-existing-release-gate: + name: existing release gate + if: ${{ github.event_name == 'workflow_dispatch' && !inputs.pypi-environment-smoke && inputs.publish-existing-tag != '' }} + runs-on: ubuntu-latest + timeout-minutes: 10 + environment: + name: pypi + permissions: + contents: read + + env: + RELEASE_TAG: ${{ inputs.publish-existing-tag }} + CODEBASE_GRAPH_CONFIRM_TRUSTED_PUBLISHER: ${{ vars.CODEBASE_GRAPH_CONFIRM_TRUSTED_PUBLISHER }} + CODEBASE_GRAPH_CONFIRM_PYPI_ENVIRONMENT: ${{ vars.CODEBASE_GRAPH_CONFIRM_PYPI_ENVIRONMENT }} + CODEBASE_GRAPH_CONFIRM_HOSTED_CI_GREEN: ${{ vars.CODEBASE_GRAPH_CONFIRM_HOSTED_CI_GREEN }} + CODEBASE_GRAPH_CONFIRM_PRIVATE_VULNERABILITY_REPORTING: ${{ vars.CODEBASE_GRAPH_CONFIRM_PRIVATE_VULNERABILITY_REPORTING }} + CODEBASE_GRAPH_REQUIRE_CONDA: ${{ vars.CODEBASE_GRAPH_REQUIRE_CONDA }} + + steps: + - name: Validate release tag + shell: bash + run: | + python - <<'PY' + import os + import re + + tag = os.environ["RELEASE_TAG"] + if re.fullmatch(r"v\d+\.\d+\.\d+", tag) is None: + raise SystemExit(f"release tag must match vX.Y.Z, got {tag!r}") + PY + + - name: Check out release tag + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ env.RELEASE_TAG }} + fetch-depth: 0 + + - name: Run production release gate + shell: bash + run: | + args=(--production) + if [[ "${CODEBASE_GRAPH_CONFIRM_TRUSTED_PUBLISHER}" == "true" ]]; then + args+=(--confirm trusted-publisher) + fi + if [[ "${CODEBASE_GRAPH_CONFIRM_PYPI_ENVIRONMENT}" == "true" ]]; then + args+=(--confirm pypi-environment) + fi + if [[ "${CODEBASE_GRAPH_CONFIRM_HOSTED_CI_GREEN}" == "true" ]]; then + args+=(--confirm hosted-ci-green) + fi + if [[ "${CODEBASE_GRAPH_CONFIRM_PRIVATE_VULNERABILITY_REPORTING}" == "true" ]]; then + args+=(--confirm private-vulnerability-reporting) + fi + if [[ "${CODEBASE_GRAPH_REQUIRE_CONDA}" == "true" ]]; then + args+=(--require-conda) + fi + python scripts/check_release_gate.py "${args[@]}" + + publish-existing-pypi: + name: publish existing release to PyPI + needs: publish-existing-release-gate + if: ${{ github.event_name == 'workflow_dispatch' && !inputs.pypi-environment-smoke && inputs.publish-existing-tag != '' }} + runs-on: ubuntu-latest + timeout-minutes: 10 + environment: + name: pypi + url: https://pypi.org/p/cbasegraph + permissions: + contents: read + id-token: write + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ inputs.publish-existing-tag }} + + steps: + - name: Download distributions from GitHub release + shell: bash + run: | + mkdir -p dist + gh release download "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --dir dist --pattern "*.whl" --pattern "*.tar.gz" + python - <<'PY' + from pathlib import Path + + artifacts = sorted(path.name for path in Path("dist").iterdir()) + if not any(name.endswith(".whl") for name in artifacts): + raise SystemExit(f"release {artifacts=} does not include a wheel") + if not any(name.endswith(".tar.gz") for name in artifacts): + raise SystemExit(f"release {artifacts=} does not include a source distribution") + PY + + - name: Publish distributions to PyPI + uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1 diff --git a/tests/test_release_workflows.py b/tests/test_release_workflows.py index 154d447..b9ed19a 100644 --- a/tests/test_release_workflows.py +++ b/tests/test_release_workflows.py @@ -74,7 +74,21 @@ def test_release_workflow_can_smoke_test_pypi_environment_without_publishing() - def test_release_please_is_skipped_during_pypi_environment_smoke() -> None: text = Path(".github/workflows/release.yml").read_text(encoding="utf-8") - assert "release-please:\n name: release please\n if: ${{ !inputs.pypi-environment-smoke }}" in text + assert "release-please:\n name: release please" in text + assert "!inputs.pypi-environment-smoke" in text + assert "inputs.publish-existing-tag == ''" in text + + +def test_release_workflow_can_publish_existing_release_tag() -> None: + text = Path(".github/workflows/release.yml").read_text(encoding="utf-8") + + assert "publish-existing-tag:" in text + assert "existing release gate" in text + assert "publish existing release to PyPI" in text + assert "inputs.publish-existing-tag != ''" in text + assert 'RELEASE_TAG: ${{ inputs.publish-existing-tag }}' in text + assert 'gh release download "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --dir dist' in text + assert "release tag must match vX.Y.Z" in text def test_release_please_uses_strict_semver_tags() -> None: