Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
version: 2
updates:
# Keep GitHub Actions pinned to commit SHAs (supply-chain hardening) while
# still receiving updates: Dependabot opens a PR whenever a pinned action has
# a newer release, bumping the SHA and the trailing "# vX.Y.Z" comment.
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: weekly
groups:
# One consolidated PR per week instead of one per action.
github-actions:
patterns:
- "*"
131 changes: 131 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
name: Release

# Two-stage trusted-publishing pipeline, driven by git tags:
#
# git tag v0.2.1.dev1 && git push origin v0.2.1.dev1 -> build + TestPyPI only
# git tag v0.2.1 && git push origin v0.2.1 -> build + TestPyPI + PyPI
#
# The SAME artifacts built once in `build` are promoted through both indexes, so
# what lands on PyPI is byte-identical to what you smoke-tested on TestPyPI.
#
# Auth is OIDC trusted publishing (no API tokens stored). The manual approval
# gates are GitHub Environment "required reviewers", configured in
# Settings -> Environments (NOT in this file). The trusted-publisher registration
# on each index must match, exactly:
# owner=thad0ctor repo=Gefen-X workflow=release.yml environment=testpypi / pypi
# Renaming this file breaks the OIDC handshake -- keep it release.yml.

on:
push:
tags:
- "v*"

# Only the publish jobs need privilege (id-token), and they widen it locally.
permissions:
contents: read

# Never cancel a release that is mid-flight (e.g. waiting on an approval gate).
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false

jobs:
build:
name: Build & verify artifacts
runs-on: ubuntu-latest
outputs:
version: ${{ steps.ver.outputs.version }}
prerelease: ${{ steps.ver.outputs.prerelease }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
persist-credentials: false

- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.12"
# No pip cache in this privileged publish workflow: GitHub caches are
# branch-scoped and writable by less-trusted runs, so a poisoned cache
# could taint the wheel uploaded to PyPI. Build tooling installs fast.

- name: Install build tooling
run: python -m pip install --upgrade pip build twine

- name: Build sdist + wheel
# Pure-Python build (kernels JIT at runtime), so no CUDA toolchain needed.
run: python -m build

- name: twine check
run: python -m twine check dist/*

- name: Verify tag matches package version
id: ver
# Guards against tagging vX.Y.Z while pyproject.toml still says the old
# version (the version is hard-coded there, so this is easy to forget).
run: |
TAG="${GITHUB_REF_NAME#v}"
WHEEL=$(ls dist/*.whl)
PKG_VER=$(basename "$WHEEL" | sed -E 's/^gefen_x-([^-]+)-py3.*/\1/')
echo "tag=$TAG package=$PKG_VER"
if [ "$TAG" != "$PKG_VER" ]; then
echo "::error::Tag v$TAG does not match built package version $PKG_VER -- bump version in pyproject.toml"
exit 1
fi
echo "version=$PKG_VER" >> "$GITHUB_OUTPUT"
# PEP 440 prerelease markers (.devN / aN / bN / rcN) => TestPyPI only.
if echo "$PKG_VER" | grep -Eq '(\.dev|a|b|rc)[0-9]+$'; then
echo "prerelease=true" >> "$GITHUB_OUTPUT"
else
echo "prerelease=false" >> "$GITHUB_OUTPUT"
fi

- name: Upload artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: dist
path: dist/

testpypi:
name: Publish to TestPyPI (gate 1)
needs: build
runs-on: ubuntu-latest
# Approval gate #1: the `testpypi` environment's required reviewers.
environment:
name: testpypi
url: https://test.pypi.org/project/gefen-x/${{ needs.build.outputs.version }}/
permissions:
id-token: write # mint the short-lived OIDC token; no stored secret
steps:
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: dist
path: dist/
- name: Publish to TestPyPI
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
with:
repository-url: https://test.pypi.org/legacy/
# A real release also passes through here; if that version was already
# tested on TestPyPI, don't hard-fail on the duplicate.
skip-existing: true

pypi:
name: Publish to PyPI (gate 2)
needs: [build, testpypi]
# Prerelease tags stop at TestPyPI; only clean vX.Y.Z tags reach PyPI.
if: needs.build.outputs.prerelease == 'false'
runs-on: ubuntu-latest
# Approval gate #2: the `pypi` environment's required reviewers.
environment:
name: pypi
url: https://pypi.org/project/gefen-x/${{ needs.build.outputs.version }}/
permissions:
id-token: write
steps:
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: dist
path: dist/
- name: Publish to PyPI
# Default index is PyPI. No skip-existing: re-releasing an existing
# version should hard-fail, not silently no-op.
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
Loading