Skip to content

Commit 96b09b6

Browse files
committed
ci(publish): trust main CI as the release gate instead of re-running it
The publish workflow re-ran the full 8-combo test matrix on every release — a duplicate of the CI that already ran on main at that exact commit, costing ~8 jobs of Actions time per release for no new signal. Publishing now verifies instead of re-testing: - the release tag must exist (clear error on a bogus dispatch, which is how today's run failed: v0.3.0 was dispatched before any tag existed), - the tagged commit must be reachable from main, - the CI workflow run at that commit must have concluded success, - tag and pyproject versions must match (unchanged), - the built artifacts get twine check --strict plus an install-and- import smoke test in a bare venv — validating the wheel itself, which main CI never sees. Publishing remains automatic on every published GitHub release; the OIDC publish step is unchanged and still pinned by SHA.
1 parent 8ab7885 commit 96b09b6

1 file changed

Lines changed: 63 additions & 44 deletions

File tree

.github/workflows/publish-pypi.yml

Lines changed: 63 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -18,60 +18,60 @@ concurrency:
1818
cancel-in-progress: false
1919

2020
jobs:
21-
test:
22-
name: test (py${{ matrix.python-version }}, ${{ matrix.extras }})
21+
build:
22+
name: Verify and build distributions
2323
runs-on: ubuntu-latest
24+
permissions:
25+
contents: read
26+
actions: read
2427
env:
2528
RELEASE_TAG: ${{ github.event_name == 'release' && github.event.release.tag_name || inputs.tag }}
26-
strategy:
27-
fail-fast: false
28-
matrix:
29-
# Re-run the full CI matrix at the release ref, not a 2-combo subset, so a
30-
# release cannot ship a combination that main CI would have failed.
31-
python-version: ["3.10", "3.11", "3.12", "3.13"]
32-
extras: [core, all]
3329

3430
steps:
35-
- name: Check out release tag
36-
uses: actions/checkout@v4
37-
with:
38-
ref: ${{ env.RELEASE_TAG }}
39-
40-
- name: Install uv
41-
uses: astral-sh/setup-uv@v5
42-
with:
43-
python-version: ${{ matrix.python-version }}
44-
45-
- name: Sync dependencies
46-
# --locked: a release must be tested against exactly the committed lockfile.
31+
- name: Verify the tag exists (manual dispatch)
32+
if: github.event_name == 'workflow_dispatch'
33+
env:
34+
GH_TOKEN: ${{ github.token }}
4735
run: |
48-
if [ "${{ matrix.extras }}" = "all" ]; then
49-
uv sync --locked --all-extras
50-
else
51-
uv sync --locked
36+
if ! gh api "repos/${GITHUB_REPOSITORY}/git/ref/tags/${RELEASE_TAG}" --silent; then
37+
echo "::error::tag ${RELEASE_TAG} does not exist; create the release first:"
38+
echo "::error:: gh release create ${RELEASE_TAG} --target main --generate-notes"
39+
exit 1
5240
fi
5341
54-
- name: Ruff
55-
run: uv run ruff check .
56-
57-
- name: Mypy
58-
run: uv run mypy
59-
60-
- name: Pytest
61-
run: uv run pytest -q
62-
63-
build:
64-
name: Build distributions
65-
needs: test
66-
runs-on: ubuntu-latest
67-
env:
68-
RELEASE_TAG: ${{ github.event_name == 'release' && github.event.release.tag_name || inputs.tag }}
69-
70-
steps:
7142
- name: Check out release tag
7243
uses: actions/checkout@v4
7344
with:
7445
ref: ${{ env.RELEASE_TAG }}
46+
fetch-depth: 0
47+
48+
# The full test matrix is deliberately NOT re-run here. Releases may only
49+
# be cut from commits on main, and main's CI already ran the exact matrix
50+
# at that commit — re-running it on publish doubles the Actions spend for
51+
# zero new signal. The two checks below encode that gate; the smoke test
52+
# afterward validates the one thing main CI never sees: the built wheel.
53+
- name: Require the release commit to be on main
54+
run: |
55+
git fetch --quiet origin main
56+
if ! git merge-base --is-ancestor HEAD origin/main; then
57+
echo "::error::${RELEASE_TAG} points at a commit that is not on main;" \
58+
"releases are cut from main because its CI is the release gate"
59+
exit 1
60+
fi
61+
62+
- name: Require green CI on the release commit
63+
env:
64+
GH_TOKEN: ${{ github.token }}
65+
run: |
66+
sha=$(git rev-parse HEAD)
67+
conclusion=$(gh api \
68+
"repos/${GITHUB_REPOSITORY}/actions/workflows/ci.yml/runs?head_sha=${sha}&per_page=10" \
69+
--jq '[.workflow_runs[] | select(.status == "completed")][0].conclusion // "missing"')
70+
if [ "${conclusion}" != "success" ]; then
71+
echo "::error::CI at ${sha} is '${conclusion}', not 'success'." \
72+
"Wait for CI on main to finish (or fix it) before releasing."
73+
exit 1
74+
fi
7575
7676
- name: Set up Python
7777
uses: actions/setup-python@v5
@@ -97,12 +97,31 @@ jobs:
9797
)
9898
PY
9999
100-
- name: Install build backend
101-
run: python -m pip install --upgrade build
100+
- name: Install build tools
101+
run: python -m pip install --upgrade build twine
102102

103103
- name: Build wheel and sdist
104104
run: python -m build
105105

106+
- name: Check distribution metadata
107+
run: python -m twine check --strict dist/*
108+
109+
- name: Smoke test the built wheel
110+
# The dependency-free core promise, checked on the artifact itself: the
111+
# wheel installs into a bare venv and imports with zero dependencies.
112+
run: |
113+
python -m venv /tmp/smoke
114+
/tmp/smoke/bin/pip install --quiet dist/*.whl
115+
/tmp/smoke/bin/python - <<'PY'
116+
import importlib.metadata
117+
118+
import agent_runtime_kit as ark
119+
120+
task = ark.AgentTask(goal="smoke")
121+
assert task.permissions.mode is ark.PermissionMode.DEFAULT
122+
print("wheel imports ok:", importlib.metadata.version("agent-runtime-kit"))
123+
PY
124+
106125
- name: Upload distributions
107126
uses: actions/upload-artifact@v4
108127
with:

0 commit comments

Comments
 (0)