Floor the optimal-design budget at k + 1 on the pyoptex path (deflake… #122
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Publish to PyPI | |
| # ENG-21 (#303): publishing requires an explicit human action -- either a | |
| # pushed tag matching ``v*`` or a manual ``workflow_dispatch`` run -- so a | |
| # typo in pyproject.toml on main can no longer ship a release. Releases | |
| # carry a sigstore attestation (PEP 740) and a CycloneDX SBOM via the | |
| # corresponding GitHub release page. | |
| # | |
| # The workflow_dispatch path is a republish path only (#507): the checked-out | |
| # ref must already carry a ``v<version>`` tag matching pyproject.toml, so a | |
| # dispatch run cannot ship an untagged version either. | |
| on: | |
| push: | |
| tags: | |
| - "v*" | |
| workflow_dispatch: | |
| inputs: | |
| ref: | |
| description: "Git ref to publish (tag, branch, or sha). Defaults to the current default branch." | |
| required: false | |
| default: "" | |
| # No workflow-level grants (#507): the build job executes repository code | |
| # through PEP 517 build hooks, so each job declares only what it needs. | |
| permissions: {} | |
| jobs: | |
| build: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| outputs: | |
| version: ${{ steps.read_version.outputs.version }} | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| ref: ${{ github.event.inputs.ref || github.ref }} | |
| fetch-depth: 0 | |
| - uses: actions/setup-python@v7 | |
| with: | |
| python-version: "3.11" | |
| - name: Read version from pyproject.toml | |
| id: read_version | |
| run: | | |
| VERSION=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])") | |
| echo "version=${VERSION}" >> "$GITHUB_OUTPUT" | |
| echo "Detected version: ${VERSION}" | |
| - name: Verify the ref matches pyproject.toml version | |
| # Runs on both triggers (#507). On a tag push the tag must equal | |
| # v<version>. On workflow_dispatch the checked-out commit must already | |
| # be tagged v<version>, which keeps the ENG-21 promise: no tag, no | |
| # release, whatever pyproject.toml happens to hold on the chosen ref. | |
| run: | | |
| VERSION="${{ steps.read_version.outputs.version }}" | |
| EXPECTED="v${VERSION}" | |
| if [ "${GITHUB_EVENT_NAME}" = "push" ]; then | |
| TAG="${GITHUB_REF#refs/tags/}" | |
| if [ "${TAG}" != "${EXPECTED}" ]; then | |
| echo "::error::Tag ${TAG} does not match pyproject.toml version ${EXPECTED}." | |
| exit 1 | |
| fi | |
| else | |
| if ! TAG_COMMIT=$(git rev-parse --verify --quiet "refs/tags/${EXPECTED}^{commit}"); then | |
| echo "::error::No tag ${EXPECTED} exists for pyproject.toml version ${VERSION}. Push the tag first; workflow_dispatch only republishes an already-tagged release." | |
| exit 1 | |
| fi | |
| HEAD_COMMIT=$(git rev-parse HEAD) | |
| if [ "${TAG_COMMIT}" != "${HEAD_COMMIT}" ]; then | |
| echo "::error::Tag ${EXPECTED} points at ${TAG_COMMIT} but the checked-out ref is ${HEAD_COMMIT}. Dispatch the workflow against the tag itself." | |
| exit 1 | |
| fi | |
| fi | |
| - name: Verify CHANGELOG.md has an entry for this version | |
| # Mirrors the release-notes extraction regex in the publish job, so a | |
| # missing changelog section fails here, before anything is published. | |
| run: | | |
| python - "${{ steps.read_version.outputs.version }}" <<'PYEOF' | |
| import pathlib | |
| import re | |
| import sys | |
| version = sys.argv[1] | |
| changelog = pathlib.Path("CHANGELOG.md").read_text(encoding="utf-8") | |
| pattern = re.compile(rf"^## \[{re.escape(version)}\][^\n]*$", re.MULTILINE) | |
| if not pattern.search(changelog): | |
| print(f"::error::CHANGELOG.md has no '## [{version}]' heading; " | |
| "add the release entry before publishing.") | |
| sys.exit(1) | |
| print(f"CHANGELOG.md contains an entry for {version}.") | |
| PYEOF | |
| - name: Build sdist and wheel | |
| run: | | |
| pip install build | |
| python -m build | |
| - name: Generate CycloneDX SBOM | |
| # ENG-21 (#303) sub-item 3: SBOM ships alongside every release. The | |
| # SBOM must mirror the wheel's runtime dependency closure only (#507), | |
| # so it is taken from a fresh virtualenv holding nothing but the built | |
| # wheel and its runtime dependencies. cyclonedx-bom runs from a | |
| # separate tooling venv, so neither it nor ``build`` can appear as | |
| # components; the installers are removed before the scan. | |
| run: | | |
| python -m venv /tmp/sbom-target | |
| /tmp/sbom-target/bin/python -m pip install --quiet dist/*.whl | |
| /tmp/sbom-target/bin/python -m pip uninstall --quiet -y setuptools wheel || true | |
| /tmp/sbom-target/bin/python -m pip uninstall --quiet -y pip | |
| python -m venv /tmp/sbom-tool | |
| /tmp/sbom-tool/bin/python -m pip install --quiet "cyclonedx-bom>=5.0" | |
| /tmp/sbom-tool/bin/cyclonedx-py environment /tmp/sbom-target/bin/python \ | |
| --output-format JSON \ | |
| --output-file "dist/sbom-${{ steps.read_version.outputs.version }}.cdx.json" | |
| - name: Upload built artifacts | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: dist | |
| path: dist/ | |
| retention-days: 7 | |
| publish: | |
| needs: build | |
| runs-on: ubuntu-latest | |
| environment: pypi | |
| permissions: | |
| id-token: write # Required for trusted PyPI publishing and sigstore attestations. | |
| contents: write # Required to create / update the GitHub release. | |
| attestations: write | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| ref: ${{ github.event.inputs.ref || github.ref }} | |
| fetch-depth: 0 | |
| - name: Download built artifacts | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: dist | |
| path: dist/ | |
| - name: Separate SBOM from PyPI artifacts | |
| run: | | |
| mkdir -p sbom | |
| mv dist/sbom-*.cdx.json sbom/ | |
| - name: Publish to PyPI with sigstore attestations | |
| # ``attestations: true`` is the default on recent action versions, but | |
| # we set it explicitly so the SEC posture is visible in the workflow. | |
| # Pinned to the commit that release/v1 resolved to (v1.14.2) so a | |
| # moved branch cannot swap the publish code underneath us (#507). | |
| uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 | |
| with: | |
| attestations: true | |
| - name: Extract release notes from CHANGELOG.md | |
| # ENG-21 (#303) sub-item 4: the GitHub release page mirrors the | |
| # CHANGELOG entry for the same version. The build job already verified | |
| # the heading exists; a miss here is therefore a hard failure, not a | |
| # silent fall-back to auto-generated notes (#507). | |
| run: | | |
| VERSION="${{ needs.build.outputs.version }}" | |
| NOTES_FILE="release-notes.md" | |
| python - "${VERSION}" "${NOTES_FILE}" <<'PYEOF' | |
| import sys | |
| import re | |
| import pathlib | |
| version = sys.argv[1] | |
| out_path = pathlib.Path(sys.argv[2]) | |
| changelog = pathlib.Path("CHANGELOG.md").read_text(encoding="utf-8") | |
| # Match "## [X.Y.Z] - YYYY-MM-DD" up to (but not including) the next | |
| # "## [" heading. | |
| pattern = re.compile( | |
| rf"^## \[{re.escape(version)}\][^\n]*\n(.*?)(?=^## \[|\Z)", | |
| re.DOTALL | re.MULTILINE, | |
| ) | |
| m = pattern.search(changelog) | |
| if not m: | |
| print(f"::error::CHANGELOG.md has no entry for v{version}; " | |
| "refusing to create a release without notes.") | |
| sys.exit(1) | |
| out_path.write_text(m.group(1).strip() + "\n", encoding="utf-8") | |
| print(f"Extracted release notes for v{version} ({len(m.group(1))} chars)") | |
| PYEOF | |
| - name: Create or update GitHub release | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| TAG="v${{ needs.build.outputs.version }}" | |
| # Target the checked-out commit explicitly (#507): without it a | |
| # release-created tag would land on the default branch, not on the | |
| # commit that was actually published. git rev-parse HEAD is used | |
| # rather than GITHUB_SHA because a dispatch run may check out an | |
| # inputs.ref that differs from github.ref. | |
| TARGET="$(git rev-parse HEAD)" | |
| if gh release view "${TAG}" > /dev/null 2>&1; then | |
| echo "Release ${TAG} already exists; updating notes and uploading SBOM." | |
| gh release edit "${TAG}" --notes-file release-notes.md | |
| else | |
| gh release create "${TAG}" \ | |
| --target "${TARGET}" \ | |
| --title "${TAG}" \ | |
| --notes-file release-notes.md | |
| fi | |
| gh release upload "${TAG}" sbom/sbom-*.cdx.json --clobber |