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
84 changes: 84 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Publishes a tagged dbprofiler.py, unmodified, with a checksum and a signed
# provenance attestation.
#
# The whole value of a single-file tool is that a security reviewer can read
# what they are about to run. That only holds if the download is byte-identical
# to the tag, so no step here rewrites the script -- not even to stamp a version
# into it. The version is committed before the tag, and the job refuses to
# publish if the two disagree.
#
# To cut a release:
#
# 1. Edit VERSION in dbprofiler.py, commit, merge.
# 2. git tag -a v1.2.3 -m 'v1.2.3' && git push origin v1.2.3
#
# Anything else -- a tag on an unmerged branch, a tag that does not match
# VERSION, a file that fails the safety audit -- fails here rather than reaching
# a customer.

name: Release

on:
push:
tags: ["v*"]

permissions:
contents: read

jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write # create the release and attach its assets
id-token: write # sign the provenance attestation
attestations: write # record it against this repository
steps:
- uses: actions/checkout@v4

# The oldest version the tool claims to support. If the release artifact
# only works on something newer, that is a release-blocking bug.
- uses: actions/setup-python@v5
with:
python-version: "3.9"

- name: Safety audit
run: python dbprofiler.py --check-safety

- name: Unit tests
run: python -m unittest

- name: Tag must agree with the version in the script
run: |
set -euo pipefail
version="$(python dbprofiler.py --version)"
if [ "v${version}" != "${GITHUB_REF_NAME}" ]; then
echo "tag ${GITHUB_REF_NAME} does not match VERSION ${version}" >&2
echo "bump VERSION in dbprofiler.py, merge, then re-tag" >&2
exit 1
fi

# Written with a bare filename so `sha256sum -c` works from whatever
# directory the customer downloaded into.
- name: Checksum
run: sha256sum dbprofiler.py > dbprofiler.py.sha256

- name: Verify the checksum the way a customer will
run: sha256sum -c dbprofiler.py.sha256

- name: Attest build provenance
uses: actions/attest-build-provenance@v4
with:
subject-path: dbprofiler.py

# --verify-tag refuses to invent a tag that is not already pushed, so a
# typo cannot create a release pointing at the wrong commit.
- name: Publish
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
gh release create "${GITHUB_REF_NAME}" \
--title "${GITHUB_REF_NAME}" \
--verify-tag \
--generate-notes \
dbprofiler.py dbprofiler.py.sha256
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,22 @@ curl -LO https://github.com/cockroachlabs/dbprofiler/releases/latest/download/db
sha256sum -c dbprofiler.py.sha256
```

On macOS, `shasum -a 256 -c dbprofiler.py.sha256` does the same thing.

The checksum proves the file survived the transfer. To prove it came from this
repository's release workflow and not from someone who could write to the release page,
verify the provenance attestation as well:

```bash
gh attestation verify dbprofiler.py --repo cockroachlabs/dbprofiler
```

That checks a signature made by GitHub's own OIDC identity for this repository, recording
which workflow built the artifact and from which commit. The release workflow publishes
the tagged file unmodified — no version stamping, no rewriting — so the bytes you verify
are the bytes in the tag, and `git show <tag>:dbprofiler.py | diff - dbprofiler.py` is
empty.

It is a single file with no dependencies, so you can read all of it before you run it.

## Usage
Expand Down Expand Up @@ -151,6 +167,25 @@ python3 -m unittest integration_test -v

See `docs/TESTING.md` for the server and the configuration it needs.

## Releasing

```bash
# 1. Bump VERSION in dbprofiler.py, commit, merge to main.
# 2. Tag the merged commit.
git tag -a v1.2.3 -m 'v1.2.3'
git push origin v1.2.3
```

`.github/workflows/release.yaml` takes it from there: it runs the safety audit and the
unit suite on Python 3.9, refuses to continue if the tag disagrees with `VERSION`,
computes and re-verifies the checksum, attests build provenance, and creates the release
with `dbprofiler.py` and `dbprofiler.py.sha256` attached. It never edits the script, so
the asset is byte-identical to the tag.

`test_dbprofiler.py` holds guard tests over that workflow — trigger, step order,
permissions scope, action pinning, and the absence of any step that could rewrite the
script — so the release path is covered by the same suite as the tool.

## License

MIT. See [LICENSE](LICENSE).
50 changes: 45 additions & 5 deletions docs/superpowers/plans/2026-08-21-dbprofile-mvp-python.md
Original file line number Diff line number Diff line change
Expand Up @@ -801,11 +801,51 @@ satisfied while still testing the parser.

**Files:** `.github/workflows/release.yaml`.

- [ ] Trigger on `v*` tags.
- [ ] Compute `sha256sum dbprofiler.py > dbprofiler.py.sha256`.
- [ ] Upload `dbprofiler.py` + `dbprofiler.py.sha256` to the GitHub Release for that tag.
- [ ] README documents the download-and-verify flow: `curl -O <url>/dbprofiler.py`, `curl -O <url>/dbprofiler.py.sha256`, `sha256sum -c dbprofiler.py.sha256`.
- [ ] Optional: also publish a signed provenance attestation via GitHub's built-in `actions/attest-build-provenance` — cheap to add, meaningful for security reviewers.
- [x] Trigger on `v*` tags.
- [x] Compute `sha256sum dbprofiler.py > dbprofiler.py.sha256`.
- [x] Upload `dbprofiler.py` + `dbprofiler.py.sha256` to the GitHub Release for that tag.
- [x] README documents the download-and-verify flow: `curl -O <url>/dbprofiler.py`, `curl -O <url>/dbprofiler.py.sha256`, `sha256sum -c dbprofiler.py.sha256`.
- [x] Optional: also publish a signed provenance attestation via GitHub's built-in `actions/attest-build-provenance` — cheap to add, meaningful for security reviewers.

**Deviations and additions beyond the checklist**

- **The download is byte-identical to the tag.** The obvious thing to do at release time
is stamp the version into `dbprofiler.py`, and that is exactly what this must not do:
the tool's value is that a reviewer reads the file before running it, which only holds
if the file they read and the file they ran are the same bytes. `VERSION` is committed
before the tag instead, and the workflow refuses to publish when
`v$(python dbprofiler.py --version)` disagrees with `GITHUB_REF_NAME`. A guard test
fails if any step could rewrite the script — `sed -i`, `tee`, `patch`, or a redirect
onto it, with a lookahead sparing the `> dbprofiler.py.sha256` that writes the checksum.
- **The safety audit gates the release, not just CI.** A tag is the one moment the
boundary stops being reviewable by reading the repository, so `--check-safety` and the
unit suite run again before anything is uploaded, on Python 3.9 — the oldest version the
tool claims to support. A guard test asserts the ordering by line index, so moving the
publish step above the audit turns the unit suite red.
- **11 guard tests over the workflow, written first and mutation-tested.** Trigger shape
(`v*` tags only, no `branches:`, no `workflow_dispatch:` back door), step ordering,
the tag/version gate, checksum generation and re-verification, both assets attached,
no rewriting step, provenance attested with the token permissions it needs,
`contents: read` by default and `contents: write` only on the publishing job, no
`secrets.` reference beyond `github.token`, every action pinned to a major version, and
the README documenting the filenames the workflow actually produces. Six mutations
tried — a `workflow_dispatch` back door, a version-stamping `sed -i`, dropping the
tag/version gate, unpinning the attest action to `@main`, publishing before the audit,
and widening the top-level permission to `contents: write` — each caught by the test
written for it and by no other.
- **`--verify-tag` on `gh release create`.** Without it, `gh` will happily create the tag
it was asked to release, so a typo becomes a release pointing at whatever `main` was.
- **`actions/attest-build-provenance@v4`.** v4 is a thin wrapper over `actions/attest`,
which upstream now recommends directly, but the wrapper remains the documented path for
build provenance and needs no predicate wiring.
- **Two additions to the README.** `gh attestation verify` alongside the checksum — the
checksum proves the file survived the transfer, the attestation proves it came from this
repository's workflow — plus `shasum -a 256` for macOS, and a note that
`git show <tag>:dbprofiler.py | diff - dbprofiler.py` is empty. A `## Releasing` section
records the bump-then-tag ritual the version gate requires.
- **Both workflow files were parsed before commit.** `ruff` and the unit suite do not read
YAML, and a syntax error in a workflow does not fail CI — it silently means the workflow
never runs, which for a release path would only be discovered at the tag.

### Task 14: Verification and uncommitted handoff

Expand Down
94 changes: 94 additions & 0 deletions test_dbprofiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -3195,5 +3195,99 @@ def test_it_never_prints_a_configured_value(self):
self.assertNotRegex(self.text, r"print\(")


# --- release plumbing -------------------------------------------------------
#
# What a customer downloads is what a reviewer read. These tests exist to keep
# that sentence true: the release workflow publishes the tagged file unmodified,
# refuses to publish one that has not passed the safety audit, and refuses to
# publish under a tag the file does not claim.

RELEASE_WORKFLOW = REPO / ".github" / "workflows" / "release.yaml"
SCRIPT_NAME = "dbprofiler.py"
CHECKSUM_NAME = "dbprofiler.py.sha256"


class TestReleaseWorkflow(unittest.TestCase):
def setUp(self):
self.text = RELEASE_WORKFLOW.read_text()
self.lines = self.text.splitlines()

def index_of(self, needle):
for number, line in enumerate(self.lines):
if needle in line:
return number
raise AssertionError(f"{needle!r} not found in {RELEASE_WORKFLOW.name}")

def test_it_triggers_on_version_tags_only(self):
self.assertRegex(self.text, r"tags:\s*\[\s*[\"']v\*[\"']\s*\]")
self.assertNotIn("branches:", self.text)
self.assertNotIn("workflow_dispatch", self.text)

def test_nothing_ships_before_the_safety_audit(self):
"""A release is the one moment the boundary stops being reviewable by
reading the repository, so the audit gates it."""
self.assertLess(self.index_of("--check-safety"), self.index_of("gh release create"))
self.assertLess(self.index_of("unittest"), self.index_of("gh release create"))

def test_the_tag_must_agree_with_the_version_in_the_script(self):
"""Publishing v1.2.0 from a file that reports 1.1.0 would put the wrong
tool_version in every manifest produced by that download."""
self.assertIn("--version", self.text)
self.assertIn("GITHUB_REF_NAME", self.text)
self.assertLess(
self.index_of("GITHUB_REF_NAME"), self.index_of("gh release create")
)

def test_the_checksum_is_taken_over_the_script_and_verified(self):
self.assertIn(f"sha256sum {SCRIPT_NAME} > {CHECKSUM_NAME}", self.text)
self.assertIn(f"sha256sum -c {CHECKSUM_NAME}", self.text)

def test_both_assets_are_uploaded(self):
publish = self.lines[self.index_of("gh release create"):]
uploaded = "\n".join(publish)
self.assertIn(SCRIPT_NAME, uploaded)
self.assertIn(CHECKSUM_NAME, uploaded)

def test_the_published_file_is_the_tagged_file(self):
"""No step may rewrite the script on its way out. The download has to be
byte-identical to what is in the tag, or reading the repository tells a
reviewer nothing about what they ran."""
for rewrite in (r"sed\s+-i", r"tee\s+dbprofiler\.py", r"\bpatch\b", r"\bapply\b"):
self.assertNotRegex(self.text, rewrite)
# A redirect onto the script itself. The negative lookahead spares
# `> dbprofiler.py.sha256`, which is the checksum, not the script.
self.assertNotRegex(self.text, r">>?\s*dbprofiler\.py(?!\.)")

def test_provenance_is_attested(self):
self.assertIn("actions/attest-build-provenance@", self.text)
self.assertIn("id-token: write", self.text)
self.assertIn("attestations: write", self.text)

def test_write_permission_is_scoped_to_the_publishing_job(self):
"""Default read-only at the top of the file, widened only where the
release is actually created."""
jobs = self.index_of("jobs:")
self.assertIn("contents: read", "\n".join(self.lines[:jobs]))
self.assertNotIn("contents: write", "\n".join(self.lines[:jobs]))
self.assertIn("contents: write", "\n".join(self.lines[jobs:]))

def test_it_uses_no_secret_beyond_the_workflow_token(self):
"""A single-file tool needs no signing key and no registry credential.
Anything referencing secrets. here would be a new thing to trust."""
self.assertNotIn("secrets.", self.text)
self.assertIn("github.token", self.text)

def test_every_action_is_pinned_to_a_major_version(self):
for match in re.finditer(r"uses:\s*(\S+)", self.text):
with self.subTest(action=match.group(1)):
self.assertRegex(match.group(1), r"@v\d+$")

def test_the_readme_documents_the_flow_it_actually_publishes(self):
readme = (REPO / "README.md").read_text()
self.assertIn(f"releases/latest/download/{SCRIPT_NAME}", readme)
self.assertIn(f"releases/latest/download/{CHECKSUM_NAME}", readme)
self.assertIn(f"sha256sum -c {CHECKSUM_NAME}", readme)


if __name__ == "__main__":
unittest.main()
Loading