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
175 changes: 122 additions & 53 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ permissions:

# Only a superseded pull_request run is cancelled: every other event (push to main,
# whose run authorizes publication) gets its own run_id group and is never cancelled.
# Pull requests (same-repo, fork, dependabot) run on GitHub-hosted ubuntu-24.04 only;
# push keeps the pool. Docs-only PRs skip the four runtime jobs (docker, database-e2e,
# Every job runs on the pf-ci pool (plain labels, no hosted fallback); dependabot counts as ours.
# Fork PRs fail authz. Docs-only PRs skip the four runtime jobs (docker, database-e2e,
# contract, distribution); the Python matrix always runs because tests/test_documentation.py
# checks the links in README.md, CONTRIBUTING.md and docs/. release/ready then accepts
# those four as skipped and writes no release evidence; release PRs change code, so the
Expand All @@ -22,9 +22,73 @@ concurrency:
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
authz:
name: authz
# Pool label (needs no toolchain or Docker). Switch to pf-ci-crj once laneboss's
# bulletin says the Cloud Run class is live.
runs-on: ["self-hosted", "pf-ci", "pf-ci-x64"]
permissions: {}
# No timeout-minutes: the owner's no-time-limits rule (check-no-time-limits refuses new ones).
steps:
- name: Only our own pull requests and our own events run CI
env:
EVENT: ${{ github.event_name }}
REPO: ${{ github.repository }}
HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
HEAD_IS_FORK: ${{ github.event.pull_request.head.repo.fork }}
ASSOCIATION: ${{ github.event.pull_request.author_association }}
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
WR_HEAD_REPO: ${{ github.event.workflow_run.head_repository.full_name }}
WR_ACTOR: ${{ github.event.workflow_run.triggering_actor.login }}
GH_TOKEN: ${{ github.token }} # read-only use: one permission lookup for workflow_run
# Bot logins whose same-repository pull requests count as ours: dependabot[bot] (CI-DEPBOT A)
# and the org's own GitHub App bot logins by name (space-separated), nothing else.
ALLOWED_BOTS: "dependabot[bot] pineforge-app[bot]"
run: |
set -eu
fail() { echo "::error title=authz::$1"; echo "authz: REFUSED: $1" >> "$GITHUB_STEP_SUMMARY"; exit 1; }
case "$EVENT" in
push|workflow_dispatch|schedule|merge_group|release|workflow_call|repository_dispatch)
# Only someone with write access to this repository (or our App's token) can cause these
# events; none of them runs pull-request code.
;;
workflow_run)
# The pwn-request path: it runs in THIS repository with secrets even when the triggering run
# came from a fork. Refuse ALWAYS unless the triggering run's head is this repository, and
# apply the same identity rule to its triggering actor as to a pull-request author.
[ "$WR_HEAD_REPO" = "$REPO" ] || fail "workflow_run triggered from another repository (${WR_HEAD_REPO:-unknown})"
case " $ALLOWED_BOTS " in
*" $WR_ACTOR "*) ;;
*)
# No author_association exists on workflow_run: ask GitHub for the actor's permission on
# this repository; anything but admin/maintain/write (or a failed call) refuses.
perm=$(gh api "repos/$REPO/collaborators/$WR_ACTOR/permission" --jq .permission 2>/dev/null || echo none)
case "$perm" in admin|maintain|write) ;; *) fail "workflow_run triggering actor ${WR_ACTOR:-unknown} has permission '$perm', not write" ;; esac ;;
esac ;;
pull_request)
[ "$HEAD_REPO" = "$REPO" ] || fail "pull request from another repository (${HEAD_REPO:-unknown})"
[ "$HEAD_IS_FORK" = "false" ] || fail "pull request from a fork"
case "$ASSOCIATION" in
OWNER|MEMBER|COLLABORATOR) ;;
*)
case " $ALLOWED_BOTS " in
*" $PR_AUTHOR "*) ;;
*)
# A member whose org membership is private shows as CONTRIBUTOR: ask GitHub for the
# author's permission on this repository; anything but admin/maintain/write (or a
# failed call) refuses.
perm=$(gh api "repos/$REPO/collaborators/$PR_AUTHOR/permission" --jq .permission 2>/dev/null || echo none)
case "$perm" in admin|maintain|write) ;; *) fail "author ${PR_AUTHOR:-unknown} (${ASSOCIATION:-NONE}) has permission '$perm', not write, and is not an allowlisted bot" ;; esac ;;
esac ;;
esac ;;
*) fail "event $EVENT does not run CI (pull_request_target and other events are not allowed)" ;;
esac
echo "authz: allowed ($EVENT${PR_AUTHOR:+ by $PR_AUTHOR})" >> "$GITHUB_STEP_SUMMARY"

changes:
needs: authz
if: github.event_name == 'pull_request'
runs-on: ubuntu-24.04
runs-on: ["self-hosted", "pf-ci", "pf-ci-x64"]
timeout-minutes: 5
outputs:
code: ${{ steps.paths.outputs.code }}
Expand All @@ -50,13 +114,8 @@ jobs:

test:
name: Python ${{ matrix.python-version }}
runs-on: >-
${{ github.event_name != 'pull_request' && github.event_name != 'pull_request_target' &&
github.actor != 'dependabot[bot]' && github.triggering_actor != 'dependabot[bot]' &&
contains(fromJSON('["push","pull_request","workflow_dispatch","schedule","release"]'), github.event_name) &&
(github.event_name != 'pull_request' ||
(github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.head.repo.fork == false)) &&
fromJSON('["self-hosted","pf-ci","pf-ci-x64"]') || 'ubuntu-24.04' }}
needs: authz
runs-on: ["self-hosted", "pf-ci", "pf-ci-x64"]
env:
PF_CI_LABELS: '["self-hosted","pf-ci","pf-ci-x64"]'
strategy:
Expand Down Expand Up @@ -88,47 +147,44 @@ jobs:

docker:
name: Docker runtime
needs: changes
needs: [authz, changes]
if: >-
!cancelled() &&
needs.authz.result == 'success' &&
(github.event_name != 'pull_request' ||
(needs.changes.result == 'success' && needs.changes.outputs.code == 'true'))
runs-on: >-
${{ github.event_name != 'pull_request' && github.event_name != 'pull_request_target' &&
github.actor != 'dependabot[bot]' && github.triggering_actor != 'dependabot[bot]' &&
contains(fromJSON('["push","pull_request","workflow_dispatch","schedule","release"]'), github.event_name) &&
(github.event_name != 'pull_request' ||
(github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.head.repo.fork == false)) &&
fromJSON('["self-hosted","pf-ci","pf-ci-x64"]') || 'ubuntu-24.04' }}
runs-on: ["self-hosted", "pf-ci", "pf-ci-x64"]
env:
PF_CI_LABELS: '["self-hosted","pf-ci","pf-ci-x64"]'
steps:
- name: authz must have passed
if: needs.authz.result != 'success'
run: exit 1
- uses: actions/checkout@v7
if: needs.authz.result == 'success'
- uses: actions/setup-python@v6
with:
python-version: "3.13"
cache: pip
- run: python -m pip install -e '.[dev]'
if: needs.authz.result == 'success'
- run: PINEFORGE_DOCKER_TEST=1 pytest tests/test_docker_integration.py --junitxml=docker.xml
if: needs.authz.result == 'success'
- name: Require every real runtime test with zero skips
if: needs.authz.result == 'success'
env:
PYTHONPATH: scripts
run: python -m release_chain.evidence junit --suite docker --path docker.xml

database-e2e:
name: Database E2E
needs: changes
needs: [authz, changes]
if: >-
!cancelled() &&
needs.authz.result == 'success' &&
(github.event_name != 'pull_request' ||
(needs.changes.result == 'success' && needs.changes.outputs.code == 'true'))
runs-on: >-
${{ github.event_name != 'pull_request' && github.event_name != 'pull_request_target' &&
github.actor != 'dependabot[bot]' && github.triggering_actor != 'dependabot[bot]' &&
contains(fromJSON('["push","pull_request","workflow_dispatch","schedule","release"]'), github.event_name) &&
(github.event_name != 'pull_request' ||
(github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.head.repo.fork == false)) &&
fromJSON('["self-hosted","pf-ci","pf-ci-x64"]') || 'ubuntu-24.04' }}
runs-on: ["self-hosted", "pf-ci", "pf-ci-x64"]
services:
mysql:
image: mysql:8.4
Expand Down Expand Up @@ -166,83 +222,96 @@ jobs:
PINEFORGE_MYSQL_URL: mysql+pymysql://pineforge:pineforge@127.0.0.1:3306/pineforge
PINEFORGE_POSTGRES_URL: postgresql+psycopg://pineforge:pineforge@127.0.0.1:5432/pineforge
steps:
- name: authz must have passed
if: needs.authz.result != 'success'
run: exit 1
- uses: actions/checkout@v7
if: needs.authz.result == 'success'
- uses: actions/setup-python@v6
with:
python-version: "3.13"
cache: pip
- run: python -m pip install --upgrade pip
if: needs.authz.result == 'success'
- run: python -m pip install -e '.[dev,database-e2e]'
if: needs.authz.result == 'success'
- run: pytest tests/test_database_e2e.py --junitxml=database.xml
if: needs.authz.result == 'success'
- name: Require the disposable database E2E inventory with zero skips
if: needs.authz.result == 'success'
env:
PYTHONPATH: scripts
run: python -m release_chain.evidence junit --suite database --path database.xml

contract:
name: Release contract
needs: changes
needs: [authz, changes]
if: >-
!cancelled() &&
needs.authz.result == 'success' &&
(github.event_name != 'pull_request' ||
(needs.changes.result == 'success' && needs.changes.outputs.code == 'true'))
runs-on: >-
${{ github.event_name != 'pull_request' && github.event_name != 'pull_request_target' &&
github.actor != 'dependabot[bot]' && github.triggering_actor != 'dependabot[bot]' &&
contains(fromJSON('["push","pull_request","workflow_dispatch","schedule","release"]'), github.event_name) &&
(github.event_name != 'pull_request' ||
(github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.head.repo.fork == false)) &&
fromJSON('["self-hosted","pf-ci","pf-ci-x64"]') || 'ubuntu-24.04' }}
runs-on: ["self-hosted", "pf-ci", "pf-ci-x64"]
env:
PF_CI_LABELS: '["self-hosted","pf-ci","pf-ci-x64"]'
steps:
- name: authz must have passed
if: needs.authz.result != 'success'
run: exit 1
- uses: actions/checkout@v7
if: needs.authz.result == 'success'
with:
persist-credentials: false
- uses: actions/setup-python@v6
with:
python-version: '3.13'
- name: Verify synchronized defaults, immutable manifest, source, facts and both OCI children
if: needs.authz.result == 'success'
env:
GH_TOKEN: ${{ github.token }}
run: python scripts/release_adapter.py local

distribution:
name: Distribution
needs: changes
needs: [authz, changes]
if: >-
!cancelled() &&
needs.authz.result == 'success' &&
(github.event_name != 'pull_request' ||
(needs.changes.result == 'success' && needs.changes.outputs.code == 'true'))
runs-on: >-
${{ github.event_name != 'pull_request' && github.event_name != 'pull_request_target' &&
github.actor != 'dependabot[bot]' && github.triggering_actor != 'dependabot[bot]' &&
contains(fromJSON('["push","pull_request","workflow_dispatch","schedule","release"]'), github.event_name) &&
(github.event_name != 'pull_request' ||
(github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.head.repo.fork == false)) &&
fromJSON('["self-hosted","pf-ci","pf-ci-x64"]') || 'ubuntu-24.04' }}
runs-on: ["self-hosted", "pf-ci", "pf-ci-x64"]
env:
PF_CI_LABELS: '["self-hosted","pf-ci","pf-ci-x64"]'
steps:
- name: authz must have passed
if: needs.authz.result != 'success'
run: exit 1
- uses: actions/checkout@v7
if: needs.authz.result == 'success'
with:
persist-credentials: false
- uses: actions/setup-python@v6
with:
python-version: '3.13'
cache: pip
- run: python -m pip install -e '.[release]'
if: needs.authz.result == 'success'
- name: Set reproducible archive timestamps
if: needs.authz.result == 'success'
run: echo "SOURCE_DATE_EPOCH=$(git show -s --format=%ct HEAD)" >> "$GITHUB_ENV"
- run: python -m build
if: needs.authz.result == 'success'
- run: python -m twine check --strict dist/*
if: needs.authz.result == 'success'
- name: The canonical sdist does not ship the native feed
if: needs.authz.result == 'success'
run: |
if tar -tzf dist/*.tar.gz | grep -E '^[^/]+/native/'; then
echo "native/ must stay out of the Python sdist" >&2
exit 1
fi
- name: Install and backtest both built distributions without importing checkout source
if: needs.authz.result == 'success'
env:
PYTHONPATH: scripts
run: |
Expand Down Expand Up @@ -277,20 +346,19 @@ jobs:
ready:
name: release/ready
if: always()
needs: [changes, test, contract, docker, database-e2e, distribution]
runs-on: >-
${{ github.event_name != 'pull_request' && github.event_name != 'pull_request_target' &&
github.actor != 'dependabot[bot]' && github.triggering_actor != 'dependabot[bot]' &&
contains(fromJSON('["push","pull_request","workflow_dispatch","schedule","release"]'), github.event_name) &&
(github.event_name != 'pull_request' ||
(github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.head.repo.fork == false)) &&
fromJSON('["self-hosted","pf-ci","pf-ci-x64"]') || 'ubuntu-24.04' }}
needs: [authz, changes, test, contract, docker, database-e2e, distribution]
runs-on: ["self-hosted", "pf-ci", "pf-ci-x64"]
env:
PF_CI_LABELS: '["self-hosted","pf-ci","pf-ci-x64"]'
steps:
- name: authz must have passed
if: needs.authz.result != 'success'
run: exit 1
- name: Require the path decision and every expected job result
id: gate
if: needs.authz.result == 'success'
env:
AUTHZ_RESULT: ${{ needs.authz.result }}
EVENT_NAME: ${{ github.event_name }}
CHANGES_RESULT: ${{ needs.changes.result }}
CODE_CHANGED: ${{ needs.changes.outputs.code }}
Expand All @@ -299,6 +367,7 @@ jobs:
${{ needs.contract.result }} ${{ needs.docker.result }}
${{ needs.database-e2e.result }} ${{ needs.distribution.result }}
run: |
if [ "$AUTHZ_RESULT" != "success" ]; then echo "authz: $AUTHZ_RESULT"; exit 1; fi
evidence=true
if [ "$EVENT_NAME" = pull_request ]; then
if [ "$CHANGES_RESULT" != success ]; then
Expand Down Expand Up @@ -335,11 +404,11 @@ jobs:
fi
echo "evidence=$evidence" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@v7
if: steps.gate.outputs.evidence == 'true'
if: needs.authz.result == 'success' && steps.gate.outputs.evidence == 'true'
with:
persist-credentials: false
- uses: actions/setup-python@v6
if: steps.gate.outputs.evidence == 'true'
if: needs.authz.result == 'success' && steps.gate.outputs.evidence == 'true'
with:
python-version: '3.13'
- uses: actions/download-artifact@v6
Expand All @@ -348,7 +417,7 @@ jobs:
name: python-package-distributions-${{ github.run_attempt }}
path: dist/
- name: Require the complete job inventory and bind evidence to this head/base/tree
if: steps.gate.outputs.evidence == 'true'
if: needs.authz.result == 'success' && steps.gate.outputs.evidence == 'true'
env:
PYTHONPATH: scripts
NEEDS_JSON: >-
Expand Down
Loading
Loading