Safe Public GitHub Release
Core Rule
Protect public trust before speed. Prefer a clean public snapshot, a release-branch PR/CI gate, verified GitHub Release artifacts, checksums, and explicit maintainer approval for irreversible operations.
Never publish from private history in place. A repo that was created as a clean public snapshot and kept private only for final polishing may be flipped public after the final scrub gate. Use PyPI/TestPyPI only when Python package distribution is actually in scope.
Workflow
- Inspect first:
git status -sb, remotes, current branch, tags, and relevant workflows.- Public repo visibility, default branch, release/tag state, and GitHub
environments with
gh. - Existing docs that mention install paths, publishing, Homebrew, TestPyPI, PyPI, private repos, local paths, or handoff files.
- Scope the public surface:
- Public docs and package metadata may mention the public owner/repo, package name, public tags, and official install commands.
- Public docs must not mention private repo names, local home paths,
HANDOFF.md,CONTINUITY.md,.local,.codex, tokens, keys, raw internal transcripts, or maintainer-only instructions unless rewritten as public-facing guidance. - Prefer relative README links for repo-local docs and media. Absolute GitHub owner/repo URLs are fine in release notes, but in README they can create avoidable owner-coupling and false positives in public-surface scrub checks.
- Prepare GitHub release gates:
- Build/test locally before remote mutation.
- Use
release/vX.Y.Z-> PR -> CI -> merge by default before tagging. - Require Aki's GitHub-rendered README visual approval before merge when README or localized README files change.
- Verify Release assets before upload: expected filenames, version metadata, checksum, archive contents, and platform-specific signature/notarization checks when applicable.
- For macOS
.appzip assets, build the archive without AppleDouble or Finder metadata. PreferCOPYFILE_DISABLE=1 zipor an equivalent archive command, and reject archives containing__MACOSX,.DS_Store, or._*files. Those extra files can make a signed app fail verification after unzip. - Always verify the exact archive that will be uploaded by extracting it to a temporary directory, checking app/package version metadata, and running the relevant signature/notarization verification on the extracted payload.
- Publish the GitHub release in this order:
- Re-fetch
main, verify latest CI is green, and confirm local worktree is clean. - Confirm the target tag and GitHub Release do not already exist.
- Create the tag on the verified
maincommit. - Push the tag.
- Create checksum files from the final upload files using release-facing
basenames, not local paths such as
dist/.... - Create the GitHub Release with intentional notes and only intentional assets.
- Upload checksum files when useful for the asset type, and include the checksum in the Release notes when it helps users verify the download.
- Re-fetch
- Verify after GitHub publishing:
- The tag points to the expected commit.
- The GitHub Release is visible at the expected URL.
- Release assets and checksums match local verified files.
- Download the published Release assets into a temporary directory and verify
them from the downloaded copies. For checksum-backed archives, run
shasum -a 256 -c <asset>.sha256; for macOS.appzips, unzip the downloaded asset and run the app version andcodesign --verify --strict --deepchecks on the extracted app. - Public README/docs match the live release version and asset names.
Optional PyPI / TestPyPI Subsection
Use this subsection only when the GitHub release also publishes a Python
package, CLI, or uvx install path.
- Ensure GitHub environments exist for
testpypiandpypi. - Keep
pypibehind maintainer approval. - Use
id-token: writeand PyPI Trusted Publishers instead of stored PyPI tokens. - Publish in this order:
- Build-only workflow from the intended tag.
- GitHub Release archive with
SHA256SUMSand artifact attestations. - TestPyPI publish, then install smoke.
- PyPI publish only after TestPyPI shows the version and install smoke passes.
- Docs update to make the live install path primary.
- Verify after publishing:
- PyPI/TestPyPI JSON contains the expected version.
uvx --from <package> <command> --helpworks.- Re-uploading the same version is blocked by readiness checks.
- Public README/docs match the live install path.
GitHub-Only Public Snapshot Checklist
Use this when the target is a GitHub repository or app release without PyPI.
- Keep the repo private until the final gate passes. If the public target is a separate already-public repository, never copy private history into it; move only the scrubbed snapshot content.
- Use a release branch and PR by default for public snapshots:
- Create or update
release/vX.Y.Zin the public repository. - Commit the prepared snapshot and public docs on that branch.
- Open a PR into
mainand let CI run there. - When README or release-facing docs change, also use
readme-release-syncso public copy stays readable, bilingual, and aligned with the shipped behavior. - Give Aki GitHub-rendered README links for the PR/branch and wait for explicit visual approval before merge. Local Markdown previews, raw file views, screenshots, or automated link checks do not replace this gate.
- Merge only after CI is green and the public-surface scrub has passed.
- Push directly to
mainonly when Aki explicitly asks to skip the PR gate.
- Create or update
- Scrub tracked files, not just the working tree:
- secrets, local paths, maintainer names, old private repo names, handoff files, generated archives, and stale product names.
git ls-filesfor accidental.mov,.mp4, build outputs, caches, and large binary files.
- Treat README media as part of the public surface:
- Inline GIFs are the most reliable GitHub README demo format; keep them under GitHub's practical image/GIF limit.
- Local README
<video>embeds and repo-local MP4 preview links may fail or open unstable GitHub file-preview pages. - Put optional MP4 demos in GitHub Release assets only when they are truly needed, and remove them plus release-note references if the product does not need them.
- Before creating the tag or GitHub Release, re-fetch:
- PR merge state and latest CI status on
mainafter merge git status -sb,git log --oneline -1,git tag --list, and current release assets- README and referenced image paths through the GitHub contents API when the repository is already public
- archive contents and extracted-asset verification results for every binary upload
- PR merge state and latest CI status on
- Before flipping visibility, re-fetch:
gh repo view ... --json visibility,isPrivate,url,defaultBranchRef- latest Actions status for the default branch
- Release assets and release notes
- README and referenced image paths through the GitHub contents API
- Flip visibility only after Aki explicitly says to publish, then verify
visibility=PUBLIC, public README URL availability, Release assets, and a clean local worktree.
Clean Snapshot To Public Repo Flow
Use this flow when a private/internal repository feeds a separate public repository.
- Finish and validate the feature in the private repository.
- Create a temporary snapshot folder from an allowlist of public files.
- Apply public naming and remove private-only surfaces in the snapshot.
- Scrub the snapshot for secrets, private paths, handoff files, old product names, generated archives, and large accidental files.
- Copy the scrubbed snapshot into the public repository working tree.
- Create or switch to
release/vX.Y.Z. - Update public README, localized README, changelog, version metadata, release draft notes, and README media references on that branch.
- Inspect
git diff, README media links, bilingual parity, stale wording, and public-surface scan results. - Run the relevant local build/test/package checks.
- Commit the release-prep changes on
release/vX.Y.Zand push the branch. - Open a PR into
main; do not tag or create a GitHub Release yet. - Wait for CI, fix failures on the release branch, and re-run checks.
- Send Aki the GitHub-rendered README URLs for
README.mdand any localized README files on the PR/branch. Wait for explicit visual approval that the README looks correct on GitHub. - Merge the PR only after CI, scrub gates, and Aki's GitHub-rendered README visual approval all pass.
- Re-fetch
mainand verify the merged commit is clean and green. - Stop for Aki approval before creating
vX.Y.Z, uploading assets, or creating/publishing the GitHub Release.
No Public Repository Yet Flow
Use this route when the project has never had a public repository.
- Do not flip the private/internal working repository public.
- Create a new clean publishing repository and keep it private at first.
- Initialize it from the scrubbed public snapshot only, not from private history.
- Use public-facing product names, package metadata, README files, license, changelog, release draft notes, screenshots/GIFs, and workflows from the start.
- Run the same secret/private-path/large-file/public-surface scrub on tracked files.
- Use
release/vX.Y.Z-> PR -> CI -> Aki GitHub-rendered README visual approval -> merge, even though the repository is still private. - Create the intended tag and GitHub Release draft while the repo is still private when possible, and verify assets/checks there.
- Stop for Aki approval before changing visibility.
- Flip visibility to public only after final scrub, green CI, intentional release assets, and explicit Aki approval.
- After the visibility flip, re-fetch and verify public README availability, Release assets, tags, latest Actions status, and local clean worktree.
Important Lessons
- GitHub Actions workflow definitions are taken from the ref used to dispatch the workflow. If a tag was created before a new workflow input or guard was added, dispatching from that old tag will not know about the new input.
- Future releases should land release guards before creating the tag. Then dispatch from that tag with the guarded inputs.
- If a one-time historical tag predates the guard, do not guess. Run local readiness checks, prove TestPyPI first, verify artifacts/attestations, and document the exception.
- Release asset hygiene matters: after exploratory uploads, the final public Release should contain only intentional artifacts. Delete unwanted assets and remove their SHA lines or release-note mentions before publishing.
Detailed Runbook
Read references/pypi-trusted-release-runbook.md for the step-by-step
checklist, command patterns, and public-doc audit patterns.