Skip to content

feat(release): release on merge to stable, App credentials from the release environment - #12

Closed
pjhartlieb wants to merge 4 commits into
mainfrom
fix/release-on-stable
Closed

pjhartlieb wants to merge 4 commits into
mainfrom
fix/release-on-stable

Conversation

@pjhartlieb

@pjhartlieb pjhartlieb commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Supersedes #11. It keeps @singlerider's commit (4894c4a) and adds the fixes from the review on #11 in f6816f0 and 6fc2cf5.

Draft until the release flow has been rehearsed end to end in blacklanternsecurity/release-sandbox against this commit, and until the policy amendment (.github-config#17, v1.2.0) is merged.

What changes on top of #11

  • App credentials from the environment (H1). The release App's ID and key come from the caller repository's release environment (vars.BLS_RELEASE_APP_ID, secrets.BLS_RELEASE_PRIVATE_KEY). Callers pass nothing, and the release-app-id input and release-app-key secret are removed, so the key can't end up at repository or organization level. docker.yml reads DOCKER_USERNAME/DOCKER_TOKEN the same way, so callers never need secrets: inherit.
  • Environment guard (6fc2cf5). A job that names a missing environment makes GitHub create it with no protection, and the sandbox's first run did exactly that. The check job now reads release from outside it, before any job enters it, and fails unless it exists, admins can't bypass it, deployments are limited to stable only, and (on public repositories) it has required reviewers with prevent self-review. Anything unreadable fails the run. The check job needs actions: read, so callers must grant it. Verified live in release-sandbox: an unprotected environment stopped the run at check, and the environment wasn't touched.
  • Fail fast. A preflight job in release-check.yml runs inside release. It checks that the variable and secret exist and mints a token, which proves the key and the App installation, before anything is built or published.
  • Strict versions (M1). Exactly MAJOR.MINOR.PATCH. 1.2, 1.2.3.4, .postN, epochs, +local, leading zeros and a v prefix are refused, each with a test.
  • What "already released" means (M2). The immutable v<version> tag. A version already on PyPI or crates.io without its tag fails the check, rather than tagging a commit the artifacts weren't built from (an interrupted release, or a version from before this workflow).
  • stable only (M3). refs/heads/stable explicitly, not "the default branch".

Callers

Callers drop the App lines, pass no secrets, and grant actions: read to the job that calls release-check.yml. A publish.yml job that calls publish.yml needs only version, tag and the optional inputs.

Validation

  • uv run --group dev pytest -q tests: 24 passed (adds 7 version cases and a crate-name test)
  • actionlint ok, ruff check ok, zizmor: no findings

Before this merges

  • The rehearsal passes in release-sandbox (a release with approval by someone other than the merger, a tag created by the App, tags that can't be moved, an unbumped push that releases nothing, a non-stable run refused, a re-run after a failure).
  • main gets required status checks and a CODEOWNERS file requiring bbot-leads: every release in the org runs this code.

Refs blacklanternsecurity/.github-config#16

singlerider and others added 4 commits October 21, 2026 05:43
Implements the approved publishing policy (.github-config#17) in the
shared workflows:

- release-check runs on the trunk only, reads the version from the
  merged manifest, and reports release=false when that version already
  has a GitHub release, so a push that keeps the version publishes
  nothing. The new manifest.py release command refuses prerelease
  versions, a Helm chart that disagrees with the manifest, and git
  dependencies, so stable pins released versions.
- publish creates the tag and the GitHub release last, on the released
  commit, with a token from the release GitHub App, the only actor the
  release-tags-create ruleset lets create a v* tag.
- crates, docker and publish run in the release environment, which a
  bbot-leads member approves. crates.io uses trusted publishing and
  skips a version already published. docker drops the dev tag; dev
  images go to GHCR.
- The tag subcommand and release.sh, which cut tags by hand, are gone.
…release check

Builds on 4894c4a (CLA#11). Addresses the review on CLA#11:

- H1: the release App ID and key are read from the caller repository's
  release environment (vars.BLS_RELEASE_APP_ID,
  secrets.BLS_RELEASE_PRIVATE_KEY). Callers pass nothing; the
  release-app-id input and release-app-key secret are removed, so the
  key can't end up at repo or org level. docker.yml reads DOCKER_* the
  same way, so callers don't need secrets: inherit.
- Fail fast: a preflight job in release-check.yml, inside the release
  environment, checks the variable and secret exist and mints a token
  (proving the key and the installation) before anything publishes.
- M1: the manifest version must be exactly MAJOR.MINOR.PATCH (rejects
  1.2, 1.2.3.4, .postN, epochs, +local, leading zeros, a v prefix).
- M2: "already released" is the immutable v<version> tag, not the
  GitHub release. A version already on PyPI or crates.io without its
  tag fails the check instead of tagging a commit the artifacts
  weren't built from.
- M3: releases run from refs/heads/stable explicitly, not "the default
  branch".

Refs blacklanternsecurity/.github-config#16

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A job that names a missing environment makes GitHub create it with no
protection. The sandbox's first publish run did exactly that (preflight
failed closed and nothing was published, but release was created with
no reviewers, no branch policy and admin bypass on).

The check job now reads the release environment from outside it,
before any job enters it, and fails unless: it exists; admins can't
bypass it; deployments are limited to the stable branch only; and, on
public repositories, it has required reviewers with prevent self-review
(private repositories on Team can't have environment reviewers).
Anything unreadable fails the run.

The check job needs actions: read; callers must grant it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Before a release, check each commit since the last v* tag against the
pull requests it belongs to (GitHub's commit-to-PR association), not its
author, which anyone can set. A commit that didn't arrive through a
merged pull request may touch only the paths in the new
direct-push-paths input (cloudcheck: its two daily data files); with no
paths given, the default, any such commit fails the release. A first
release (no earlier v* tag) is skipped with a notice.

This is the release-time backstop the publishing policy requires for the
cloudcheck-update-bot exception, and a free extra control everywhere
else, where direct pushes to stable shouldn't exist at all.

The check job needs pull-requests: read; callers must grant it.

Verified: 8 new tests (32 in total); 3 mutations of the rule each fail
them; on cloudcheck's real v11.1.0..v11.2.0 history (158 commits: 112
bot data pushes, 46 through PRs) it passes with the data paths allowed,
and on stable it fails with none allowed.

Refs blacklanternsecurity/.github-config#16, blacklanternsecurity/bbot-enterprise#406

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@singlerider singlerider added the enhancement New feature or request label Oct 22, 2026
singlerider pushed a commit that referenced this pull request Oct 26, 2026
A job that names a missing environment makes GitHub create it with no
protection. Before any job enters release, the check job now reads the
environment and fails unless it exists, admins can't bypass it,
deployments are limited to stable only, and, on public repositories, it
has required reviewers with prevent self-review. Anything unreadable
fails the run.

The check job needs actions: read; callers must grant it.

From pjhartlieb's 6fc2cf5 on #12.
singlerider pushed a commit that referenced this pull request Oct 26, 2026
Before a release, check each commit since the last v* tag against the
pull requests it belongs to (GitHub's commit-to-PR association), not
its author, which anyone can set. A commit that didn't arrive through a
merged pull request may touch only the paths in the new
direct-push-paths input (cloudcheck: its daily data files). With no
paths given, the default, any such commit fails the release. A first
release (no earlier v* tag) is skipped with a notice.

This is the release-time backstop the publishing policy requires for
the cloudcheck-update-bot exception.

The check job needs pull-requests: read; callers must grant it.

From pjhartlieb's 31d7219 on #12.

Refs blacklanternsecurity/.github-config#16, blacklanternsecurity/bbot-enterprise#406
@pjhartlieb

Copy link
Copy Markdown
Contributor Author

Superseded by #11, which carries this code with F1 fixed at 9c75e85c. Closing.

@pjhartlieb pjhartlieb closed this Oct 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants