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
16 changes: 16 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,23 @@ jobs:
with:
# Build the tag being released, never whatever the default branch is at.
ref: ${{ github.event.release.tag_name || inputs.tag }}
# Full history so the tagged commit can be checked against main below.
fetch-depth: 0
persist-credentials: false
- name: Refuse to publish a commit that is not on main
env:
TAG: ${{ github.event.release.tag_name || inputs.tag }}
run: |
# Branch protection governs main, not tags. Without this, anyone who can
# push a tag and publish a Release can ship a commit that never passed
# review -- the version guard below only checks the number, not the history.
git fetch --no-tags --quiet origin main
if ! git merge-base --is-ancestor HEAD origin/main; then
echo "::error::$TAG points at $(git rev-parse --short HEAD), which is not reachable from main. Refusing to publish an unreviewed commit."
exit 1
fi
echo "$TAG is on main ($(git rev-parse --short HEAD))"

- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ All notable changes to CodeSnake are documented here. The format follows

## [Unreleased]

### Security

- **The release workflow refuses a tag that is not reachable from `main`.** Branch
protection governs `main`, not tags, so anyone able to push a tag and publish a
GitHub Release could previously ship a commit that never passed review — the
existing guard compared the tag to `_version.py` and said nothing about history.
Paired with a repository ruleset that blocks deleting or force-updating `v*` tags,
and `can_admins_bypass` turned off on the `pypi` environment so the approval applies
to the owner as well.

## [1.3.0] - 2026-09-03

A security fix for anyone using CodeSnake as a CI gate. Upgrade if you run it against
Expand Down
10 changes: 9 additions & 1 deletion docs/RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,15 @@ The build job refuses to publish when the release tag and `_version.py` disagree
## What protects the release path

- **Trusted Publishing** — no long-lived credential exists to steal.
- **The `pypi` environment** — deployments are restricted to `v*` tags, and the publish job is the only thing granted `id-token: write`.
- **The tagged commit must be on `main`.** Branch protection governs `main`, not tags, so
without this anyone able to push a tag and publish a Release could ship a commit that
never passed review — the version guard only checks the number, not the history. The
build refuses a tag that is not reachable from `main`.
- **Release tags are immutable** — a ruleset blocks deleting or force-updating `v*`, so a
published tag cannot be repointed at different code after the fact.
- **The `pypi` environment** — deployments are restricted to `v*` tags, the publish job is
the only thing granted `id-token: write`, and approval is required with
`can_admins_bypass` off, so the wait applies to the owner too.
- **Split jobs** — the build job has `contents: read` and no OIDC; the publish job has OIDC and never checks out the repository. Code from the repo and the ability to publish never sit in the same job.
- **Pinned actions** — every action is pinned to a commit SHA, not a mutable tag.

Expand Down
Loading