Releasing lgtmaybe (Maintainers)¶
release-please automates releases
(.github/workflows/release-please.yml). It reads the conventional commits
merged to main. From them it keeps a Release PR open that bumps the version
and regenerates CHANGELOG.md.
Merging that PR is the release. release-please creates the
lgtmaybe-vX.Y.Z tag and the GitHub release. The same run then publishes
everywhere:
- PyPI, through trusted publishing (OIDC) in an inline job.
- GHCR image and Action tags, through the reusable
.github/workflows/release.yml, using the built-inGITHUB_TOKEN.- It pushes a multi-arch (amd64, arm64) image tagged
X.Y.Z,vX(currentlyv2), andlatest. - It moves the git tags
vX.Y.Z,vX, andlgtmaybe-vXto the release commit, so workflows can useMattJColes/lgtmaybe@v2.
- It pushes a multi-arch (amd64, arm64) image tagged
- Homebrew, through
.github/workflows/homebrew.yml, after the PyPI job succeeds (described below). -
Windows, through two workflows:
.github/workflows/windows-exe.ymlbuilds and smoke-tests a portable executable and attaches it to the release..github/workflows/winget.ymlthen submits that asset to winget. The winget job waits for the executable job.
You can dispatch both workflows by hand for recovery.
PyPI and GHCR need no publish tokens in secrets. Homebrew and winget each need one PAT (personal access token), set up below.
Commit messages must follow the conventional-commit format.
.github/workflows/commitlint.yml enforces it on PRs, so release-please can
work out the next version.
Contents¶
- Stacked Pull Requests
- The Homebrew Formula
- One-Time Setup
- Each Release
- Rotate the Public App Private Key
- Before Going Public
Stacked Pull Requests¶
release-please only sees commits that reach main. A stacked PR targets the
branch of the PR below it, so it can miss main entirely:
- The bottom PR squash-merges into
main. Its branch stays behind. - GitHub retargets the PRs above it only when that branch is deleted.
- An upper PR merged now lands on the old branch, not
main. Its conventional commit never reaches the changelog.
.github/workflows/stack-retarget.yml stops this. When a PR merges, it moves
every open PR based on the merged branch onto the merged PR's own base, and
comments on each one. It runs main's copy of the workflow, so it also covers
layers whose branches were cut before it existed. Merge the new base into a retargeted branch before you
merge it, so its diff shows only that layer.
GitHub starts no workflow for a merge made with another workflow's
GITHUB_TOKEN. After a merge like that, check the base of the layers above it.
If a layer has already landed on a dead branch, open one PR that brings that
branch into main. Its squash commit is a single commit, so list every
stranded layer in the PR body between BEGIN_COMMIT_OVERRIDE and
END_COMMIT_OVERRIDE. release-please then reads those lines instead of the
squash message:
BEGIN_COMMIT_OVERRIDE
fix(engine): keep scan findings on files triage dropped (#789)
fix: escape the describe fallback text before posting it (#791)
END_COMMIT_OVERRIDE
Write the marker words only in the block itself. The tool reads from the first
BEGIN_COMMIT_OVERRIDE in the body. If the words appear earlier in the
description, it parses the wrong text and skips the commit.
You can edit the override after the merge. release-please reads it on its next
run, which is the next push to main.
The Homebrew Formula¶
homebrew.yml regenerates the formula in the tap repo
(MattJColes/homebrew-tap, which is the tap MattJColes/tap). That keeps
brew install MattJColes/tap/lgtmaybe on the latest version.
scripts/update-homebrew-formula.sh writes a small formula. The formula
creates a venv and pip installs lgtmaybe and its dependencies from PyPI
wheels. It has no per-dependency resource stanzas. Building from source is a
dead end: litellm's dependency tree includes Rust sdists (tokenizers, hf-xet)
that can't build in Homebrew's sandbox.
The formula declares preserve_rpath. Homebrew then keeps the wheels'
@rpath extension-dylib ids instead of failing to rewrite them ("Failed to fix
install linkage"). It is a plain source formula with no bottle (no prebuilt
package), so it installs on any architecture and macOS version.
The workflow:
- Is called by
release-please.yml(workflow_call) once the PyPI publish succeeds. GitHub does not deliver arelease: publishedevent for a release cut with the built-inGITHUB_TOKEN. It suppresses downstream triggers to prevent recursion. - Also runs:
- on
release: published, for a release a human cuts; - on a daily
schedule; - on a manual
workflow_dispatch, with an optionalforceto re-publish the same version.
- on
- Installs the regenerated formula in CI before committing it. A real
brew trust+brew installgates the push, so a formula that doesn't install is never published. The gate trusts the tap rather than settingHOMEBREW_NO_REQUIRE_TAP_TRUST. That way it runs the same steps the install guide gives users.
A new version reaches the tap within minutes of release. There is no PyPI
cooldown to wait out, because the workflow doesn't use
brew update-python-resources (which imposes one).
One-Time Setup¶
- PyPI: add a trusted publisher for this repo, with workflow
release-please.ymland environmentpypi. There is noPYPI_TOKENsecret; auth is via OIDC. The publish job is inline in that workflow on purpose. PyPI trusted publishing requires the OIDCjob_workflow_refto equal the top-level workflow. - Create the repo environment named
pypi(Settings → Environments). - After the first release, set the GHCR package visibility to public
(Packages → lgtmaybe → Package settings). Consumers can then
docker pullthe image. - First release only: from the GitHub release page, tick "Publish this Action
to the GitHub Marketplace" and accept the terms. Pick the categories
code-reviewandcontinuous-integration. - Homebrew tap: create the repo
MattJColes/homebrew-tapwith aFormula/directory.- It can start empty; the workflow writes the formula.
- Homebrew strips the
homebrew-prefix. So that repo is the tapMattJColes/tap, and the formula inside it isMattJColes/tap/lgtmaybe. - Name the repo
homebrew-tap, nothomebrew-lgtmaybe, or the formula reads asMattJColes/lgtmaybe/lgtmaybe. - Add a repo secret
HOMEBREW_TAP_TOKENto this repo. It is a fine-grained PAT withcontents: writeon the tap repo. The defaultGITHUB_TOKENcannot push to another repository. - To seed or verify the formula by hand on a Mac, run
scripts/update-homebrew-formula.sh <version> path/to/homebrew-tap/Formula/lgtmaybe.rb.
-
winget: fork
microsoft/winget-pkgs. Create a classic GitHub PAT with thepublic_repoandworkflowscopes. Add it to this repo asWINGET_TOKEN. Here is why it needsworkflow:wingetcreatesyncs the fork with upstream before it opens each PR.microsoft/winget-pkgsoften changes files under.github/workflows/.- GitHub refuses a fork sync that brings in workflow-file changes unless the
token has the
workflowscope.
With
public_repoalone, the submission fails on almost every release. -
First winget release: build and upload the Windows asset. Then run
wingetcreate new <asset-url>by hand with:- package id
MattJColes.lgtmaybe; - installer type
portable; - command alias
lgtmaybe; - MIT licence metadata.
Microsoft moderates a new package before it is published, which can take days or weeks. Until that first manifest is accepted, the release job warns and skips the winget update. It does not fail the other release artefacts. After that, the workflow runs
wingetcreate updatefor every new version. - package id
Each Release¶
- Merge feature/fix PRs to
mainusing conventional-commit messages (feat:,fix:,feat!:/BREAKING CHANGE:for a major bump). - release-please opens or updates the Release PR automatically. Review the
proposed version + changelog, then merge it to publish.
- Once the release is cut, the
contributorsjob appends a Contributors section to the GitHub release. - That section names the human authors of the release's pull requests and welcomes first-time contributors. Bots are left out.
scripts/release_contributors.pybuilds it, fed by GitHub's generate-notes API.CHANGELOG.mdis not touched.- The merge also republishes the docs, so the homepage's What's New list picks up the release.
- Once the release is cut, the
- To (re)publish an existing tag to PyPI without a new release, run the
release-pleaseworkflow via workflow_dispatch with the tag name (for examplelgtmaybe-v2.11.1). To rebuild the GHCR image and move the Action tags for an existing tag, dispatchreleasethe same way. - If Windows publication needs recovery, dispatch
windows-exefor the tag. Setforceto replace an existing asset. Then dispatchwingetonce the release asset is visible. - The
wingetjob may fail with "The forked repository could not be synced with the upstream commits". If so, syncMattJColes/winget-pkgswith upstream. Use Sync fork on itsmaster, or rungh api -X POST repos/MattJColes/winget-pkgs/merge-upstream -f branch=master. Then dispatchwingetagain with the version. If it keeps recurring, check thatWINGET_TOKENstill carries theworkflowscope.
Rotate the Public App Private Key¶
Rotate the public lgtmaybe GitHub App key:
- quarterly;
- whenever maintainer access changes;
- immediately after any suspected exposure.
Keep the old key active until the replacement has passed a brokered smoke test. That way rotation does not interrupt reviews.
- In the GitHub App settings, generate a new private key. Do not delete the old key yet.
-
From the directory containing the downloaded PEM, replace the retained Secrets Manager value without printing the key:
aws secretsmanager put-secret-value \ --secret-id lgtmaybe/github-app/private-key \ --secret-string file://lgtmaybe.private-key.pem \ --region ap-southeast-2 -
Refresh the Lambda configuration so every execution environment drops its cached copy of the old key:
FUNCTION_NAME="$( aws cloudformation describe-stack-resources \ --stack-name LgtmaybeGithubAppIdentity \ --region ap-southeast-2 \ --query "StackResources[?ResourceType=='AWS::Lambda::Function'].PhysicalResourceId | [0]" \ --output text )" aws lambda update-function-configuration \ --function-name "$FUNCTION_NAME" \ --description "GitHub App key rotated $(date -u +%Y-%m-%dT%H:%M:%SZ)" \ --region ap-southeast-2 aws lambda wait function-updated-v2 \ --function-name "$FUNCTION_NAME" \ --region ap-southeast-2 -
Run a brokered dogfood review and confirm GitHub attributes it to
lgtmaybe[bot]. - Delete the old key in the GitHub App settings. Then delete the downloaded PEM from the maintainer machine. Never paste the PEM into a command argument, issue, workflow log, or repository file.
If the smoke test fails:
- Leave the old GitHub key active.
- Put its PEM back into the same Secrets Manager secret.
- Refresh the Lambda configuration again.
- Investigate before retrying.