Repository navigation
fix(infra/snapshot): des points de reprise externes, et un « required… #58
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
| # Publish dsoxlab to PyPI when a version tag (vX.Y.Z) is pushed. | |
| # | |
| # Security model: | |
| # - PyPI Trusted Publishing (OIDC). No API token or long-lived secret: the | |
| # `publish` job mints a short-lived id-token that PyPI trusts. Configure the | |
| # trusted publisher on PyPI (project: dsoxlab, workflow: release.yml, | |
| # environment: pypi) before the first release. | |
| # - Privilege separation: `build` compiles the artifacts with read-only access | |
| # and runs project code; `publish` runs no project code, only downloads the | |
| # built artifacts and hands them to PyPI with `id-token: write`. | |
| # - The `pypi` GitHub Environment gates the publish step (add required | |
| # reviewers / tag protection there for a manual approval before upload). | |
| # - Every action is pinned to a full commit SHA; the default token has no | |
| # permissions; no build cache in the publishing path (cache-poisoning | |
| # hardening). | |
| name: Release | |
| on: | |
| push: | |
| tags: | |
| - 'v*' | |
| permissions: {} | |
| concurrency: | |
| group: release-${{ github.ref }} | |
| cancel-in-progress: false | |
| jobs: | |
| build: | |
| name: Build distribution | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: read | |
| id-token: write # mint the OIDC token used to sign the provenance | |
| attestations: write # record the SLSA build provenance for the artifacts | |
| steps: | |
| - name: Harden the runner | |
| uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 | |
| with: | |
| egress-policy: audit | |
| - name: Checkout | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@ae62891fec2bb8e7d6c99fc78c9fec3a63790f8d # v10.0.0 | |
| with: | |
| version: "0.11.26" | |
| enable-cache: false # no cache in the publishing path (cache-poisoning hardening) | |
| # The tag must match the version inside pyproject.toml. Twice now, a tag | |
| # was pushed at a commit whose version had already moved on: v0.1.22 built | |
| # the previous version, and v0.1.25 built 0.1.26 and published it under | |
| # the wrong tag, so PyPI never received a 0.1.25 at all. Nothing in the | |
| # pipeline objected, because the build reads pyproject and the tag is only | |
| # used for the release notes. Fail loudly instead. | |
| - name: Tag must match the packaged version | |
| env: | |
| TAG: ${{ github.ref_name }} | |
| run: | | |
| set -euo pipefail | |
| packaged=$(grep -m1 '^version = ' pyproject.toml | cut -d'"' -f2) | |
| if [ "${TAG#v}" != "$packaged" ]; then | |
| echo "::error::Tag $TAG does not match pyproject version $packaged." \ | |
| "Tag the commit that carries the matching version bump," \ | |
| "or bump pyproject to ${TAG#v} before tagging." | |
| exit 1 | |
| fi | |
| echo "Tag $TAG matches the packaged version $packaged." | |
| - name: Build sdist and wheel | |
| run: uv build | |
| - name: Check distribution metadata | |
| run: uvx twine check dist/* | |
| # SLSA build provenance for the wheel and sdist, verifiable with | |
| # `gh attestation verify <artifact> --repo stephrobert/dsoxlab` or | |
| # slsa-verifier. Complements the PEP 740 attestations PyPI records. | |
| # | |
| # The two artifacts are listed explicitly rather than globbed with | |
| # `dist/*`: `uv build` also drops a one-byte `dist/.gitignore`, and this | |
| # action's glob matches dotfiles (unlike the shell glob that feeds | |
| # `gh release create` below). v0.1.11 therefore attested `.gitignore` | |
| # alongside the distributions — harmless, but an attestation should name | |
| # exactly what is published, nothing more. | |
| - name: Attest build provenance | |
| id: attest | |
| uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 | |
| with: | |
| subject-path: | | |
| dist/*.whl | |
| dist/*.tar.gz | |
| # The attestation above is recorded on GitHub's attestation API, which is | |
| # what `gh attestation verify` reads. It is NOT what OpenSSF Scorecard's | |
| # Signed-Releases control looks at: that check wants a `*.intoto.jsonl` | |
| # file attached as a release asset — a distinct artifact. Copy the bundle | |
| # out under the expected name so `github_release` can attach it. | |
| # | |
| # The bundle path goes through `env:` rather than being interpolated into | |
| # the script body, so no step output is ever expanded into shell code. | |
| - name: Stage the provenance bundle as a release asset | |
| env: | |
| BUNDLE_PATH: ${{ steps.attest.outputs.bundle-path }} | |
| run: | | |
| set -euo pipefail | |
| cp "$BUNDLE_PATH" provenance.intoto.jsonl | |
| echo "Provenance bundle staged ($(wc -c < provenance.intoto.jsonl) bytes)" | |
| - name: Upload distribution artifact | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: dist | |
| path: dist/ | |
| if-no-files-found: error | |
| retention-days: 1 | |
| - name: Upload provenance artifact | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: provenance | |
| path: provenance.intoto.jsonl | |
| if-no-files-found: error | |
| retention-days: 1 | |
| publish: | |
| name: Publish to PyPI | |
| needs: build | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 10 | |
| environment: | |
| name: pypi | |
| url: https://pypi.org/p/dsoxlab | |
| permissions: | |
| id-token: write # OIDC token for Trusted Publishing — the only scope needed | |
| steps: | |
| - name: Harden the runner | |
| uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 | |
| with: | |
| egress-policy: audit | |
| - name: Download distribution artifact | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: dist | |
| path: dist/ | |
| - name: Publish to PyPI (Trusted Publishing) | |
| uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 | |
| with: | |
| attestations: true | |
| print-hash: true | |
| github_release: | |
| name: Create GitHub Release | |
| # Only after PyPI succeeds — never publish a GitHub Release for a version | |
| # that failed to reach PyPI. | |
| needs: [build, publish] | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: write # create the Release and upload the built artifacts | |
| steps: | |
| - name: Harden the runner | |
| uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 | |
| with: | |
| egress-policy: audit | |
| - name: Checkout | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Download distribution artifact | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: dist | |
| path: dist/ | |
| - name: Download provenance artifact | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: provenance | |
| path: . | |
| # Extract the CHANGELOG section for this tag. The tag is passed through an | |
| # env var (never interpolated into the script body) to avoid injection. | |
| - name: Extract changelog notes | |
| env: | |
| TAG: ${{ github.ref_name }} | |
| run: | | |
| version="${TAG#v}" | |
| python3 - "$version" > release-notes.md <<'PY' | |
| import pathlib, re, sys | |
| version = sys.argv[1] | |
| text = pathlib.Path("CHANGELOG.md").read_text(encoding="utf-8") | |
| m = re.search( | |
| r"^## \[" + re.escape(version) + r"\].*?(?=^## \[|\Z)", | |
| text, re.S | re.M, | |
| ) | |
| body = m.group(0).strip() if m else f"Release {version}" | |
| lines = body.splitlines() | |
| if lines and lines[0].startswith("## "): | |
| lines = lines[1:] # drop the version heading (title carries it) | |
| print("\n".join(lines).strip() or f"Release {version}") | |
| PY | |
| - name: Create GitHub Release | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| TAG: ${{ github.ref_name }} | |
| REPO: ${{ github.repository }} | |
| run: | | |
| gh release create "$TAG" \ | |
| --repo "$REPO" \ | |
| --title "$TAG" \ | |
| --notes-file release-notes.md \ | |
| dist/* provenance.intoto.jsonl |