Repository navigation
Conversation
Push-to-master now computes a semver bump from squash-merged PR titles (feat/fix/perf/breaking!), commits and tags the release, then builds, publishes to PyPI and pushes the multi-arch image, no manual GitHub release step. Image build/push is factored into a reusable images.yml called from both ci.yml (edge, PR smoke test) and release.yml (semver + latest). A new PR title lint enforces the conventional-commit prefixes the bump logic depends on.
Declaring packages: write on image-build/merge unconditionally made GitHub reject any call site that only grants contents: read (PR builds), since the callee's requested permission exceeded the caller's ceiling for every invocation, not just the offending one. Omitting job-level permissions lets each job use whatever the calling job actually granted for that specific call.
docker/metadata-action produced zero tags for PR builds since all three explicit rules were disabled (no push, no version); the local build didn't fail (it uses a hardcoded tag) but emitted a warning and left BUILD_VERSION/labels empty.
Address adversarial review findings on the auto-release design:
- Split the tag/release job into a read-only "prepare" job (runs
release.py apply, uploads the diff as an artifact) and a
"validate-and-push" job that holds the only push credential. The
latter never runs repo code; it re-validates the raw patch text with
inline shell (file allow-list, version-digits-only hunks, changelog
additions-only) before applying, committing and pushing.
- Push/tag via an SSH deploy key (RELEASE_DEPLOY_KEY, environment
"release", persist-credentials: false) instead of a ruleset bypass
for the GitHub Actions app - that bypass would apply to every
workflow's default token, not just this one job. GH_TOKEN is used
for exactly one step (gh release create).
- scripts/setup_tag_ruleset.sh: protect refs/tags/v* so only that
deploy key can create/update/delete release tags. Documented owner
setup (ssh-keygen, deploy key, secret, ruleset) in RELEASING.md; no
keys or secrets are created by any script.
- republish_tag now requires the tag to be an ancestor of
origin/master and its commit to be a release: X.Y.Z commit from the
release identity, never moves :latest, and refuses to overwrite an
existing GHCR version tag unless force_overwrite_image is set
(images.yml gained move_latest/allow_overwrite inputs and an
overwrite guard).
- Race handling: fail fast and rely on the next push if master moved
while the diff was being computed (documented as the chosen,
simplest option); detect and recover an untagged release commit
(commit pushed, tag push failed) via pending-retag.
- release.py: reject non-X.Y.Z versions in apply; skip non-numeric
v* tags instead of crashing bump_version; sanitize changelog/release
note text (strip images/HTML, escape @mentions).
- check_version.py: fix ROOT override falling through even when set,
since Path("") is truthy, not falsy.
- Pin every action in release.yml and images.yml to a commit SHA with
a version comment (pr-title-lint.yml was already pinned).
plain `git diff` ignores untracked files, so the first release ever (when CHANGELOG.md doesn't exist yet) would silently drop it from the uploaded patch. git add -N (intent-to-add) makes new files show up in the diff without staging their content. Verified against a scratch repo with no pre-existing CHANGELOG.md: before the fix, git diff only showed pyproject.toml/config.yaml; after, it includes the new CHANGELOG.md, and the validate-and-push job's hunk checks still pass.
Owner
Author
|
Dropped: rescoped as over-engineered for this repo. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Releases are now automatic: every push to
masterthat carries a releasablecommit is versioned, tagged, published to PyPI and shipped as a multi-arch
image, with no manual tag or GitHub release step. See
RELEASING.mdfor thefull walkthrough (prefix table, forcing a bump, re-publishing, recovery, and
the deploy-key setup needed for the release push).
This PR went through an adversarial security review after the first version;
the current design reflects that (see "Security hardening" below).
Flow
Image build/push moved into a reusable
.github/workflows/images.yml(
workflow_call, inputsref/push/version/move_latest/allow_overwrite), called byci.ymlfor PR builds (no push, local smoketest) and master pushes (
:edge), and byrelease.ymlfor the taggedrelease (
:X.Y.Z+:latest). A newpr-title-lint.yml(read-onlypull_request_target) enforces the conventional-commit prefix the bumplogic depends on.
Bump table
<type>!:or aBREAKING CHANGE:footerfeat:fix:,perf:docs:,chore:,ci:,test:,refactor:,style:,build:,release:Security hardening
An adversarial review of an earlier revision returned BLOCK. Addressed:
to every workflow's default
GITHUB_TOKEN, not just this job. Push/taginstead uses a dedicated SSH deploy key (
RELEASE_DEPLOY_KEY, environmentrelease, restricted to branchmaster);GITHUB_TOKENis used forexactly one step (
gh release create).scripts/setup_tag_ruleset.shprotects
refs/tags/v*so only that key can create/update/delete releasetags. Owner setup steps (
ssh-keygen,gh repo deploy-key add,gh secret set) are inRELEASING.md; no keys or secrets are created byany script or workflow.
prepare(read-only)runs
release.py applyand uploads the diff as an artifact.validate-and-pushdownloads it and validates the raw diff text withinline shell only: file allow-list, and per-file, every changed line
mentions
version/evmqtt==and normalising awayX.Y.Zdigits makesthe added/removed line sets identical (only version numbers changed);
CHANGELOG.mdmust be pure additions. Verified against real patches in ascratch repo, including that it rejects an appended stray line, a wholly
unrelated new file, and a changelog line rewrite.
republish_taghardened: requires the tag to be an ancestor oforigin/masterand its commit to be arelease: X.Y.Z [skip ci]commitfrom the release identity matching that version; never moves
:latest;refuses to overwrite an existing GHCR version tag unless
force_overwrite_image: true.mastermoves betweenversioncomputing a diffand
validate-and-pushapplying it, the job fails fast and relies on thenext push to recompute from the new tip (documented as the deliberately
simplest option, rather than blindly reapplying a stale diff). If the
release commit pushes but the tag push fails, the next run detects the
untagged commit (
scripts/release.py pending-retag) and only re-tags.release.py applyrejects non-X.Y.Zversions; anon-numeric tag (
v1.2.0-rc1) is skipped instead of crashingbump_version; changelog/release-note text strips images/HTML andescapes
@mentions;check_version.py's root override no longersilently no-ops (
Path("")is truthy, not falsy); every action inrelease.yml/images.ymlis now pinned to a commit SHA with a versioncomment (
pr-title-lint.ymlalready was).What merging this PR releases
Checked with
scripts/release.py next-versionagainst this repo's realhistory: from
v2.1.0toorigin/master's tip, the only commits areci:type - non-releasing - so merging this PR by itself ships no release
(its own squash commit title,
ci: automatic releases on merge to master,is also type
ci).If PR #19 (
2.0: HACS integration (4c)) merges first or alongside this one:its current title doesn't carry a conventional prefix, so as titled it also
computes to no release. It contains a
feat(hacs)!:breaking-change commit,so once retitled with a conventional prefix it would trigger a major
release - confirmed by dry-running
release.pyagainst a squashed copy ofthat branch with a conventional title.
Risks / unverified
masteris being built by another agent in parallel; this PR documents(in
RELEASING.md) the deploy-key bypass setup needed for both themasterruleset and the new tag ruleset, and shipsscripts/setup_tag_ruleset.shfor the tag side, but doesn't touch thatother agent's files. Verified no ruleset exists yet on the live repo.
it'll need a conventional prefix before it can land.
release.ymljob graph is actionlint-clean, unit-tested at therelease.pylevel, and its patch-validation shell was tested standaloneagainst real generated patches (including untracked-file and attack
cases) - but it only triggers on push-to-master/workflow_dispatch, so it
couldn't be exercised end-to-end on GitHub without touching
master.actwasn't available to dry-run it locally either.gh release createfails after a tag already exists (rather than thecommit-pushed-but-untagged case, which is handled), there's no automatic
recovery for the missing GitHub Release page; documented as a manual step
in
RELEASING.md.edge's baked-in version string lags one release behind until the nextmaster push rebuilds it (cosmetic, documented in
RELEASING.md).https://claude.ai/code/session_01TJskTZQqRHiHQBc8kE5Lr2