-
Notifications
You must be signed in to change notification settings - Fork 0
feat(release): MVP — release-please + v1.0.0 manifest + hybrid pin guidance #29
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,260 @@ | ||||||||||
| --- | ||||||||||
| title: muntianus/.github release strategy — v1.0.0 and beyond | ||||||||||
| audit_findings: F-002 (consumer @main migration → @sha hybrid) | ||||||||||
| status: design / proposed | ||||||||||
| authored: 2026-05-30 | ||||||||||
| --- | ||||||||||
|
|
||||||||||
| # `muntianus/.github` release strategy — v1.0.0 and beyond | ||||||||||
|
|
||||||||||
| ## TL;DR | ||||||||||
|
|
||||||||||
| Consumers currently pin org-reusable workflows by raw commit SHA after | ||||||||||
| the F-002 sweep: | ||||||||||
|
|
||||||||||
| ```yaml | ||||||||||
| uses: muntianus/.github/.github/workflows/reusable-deploy-vps.yml@deb9a4d... # main (post #27 SHA-pinning) | ||||||||||
| ``` | ||||||||||
|
|
||||||||||
| That defends supply-chain risk but is unreadable, ungroupable, and | ||||||||||
| upgrades are manual. Cutting a proper `v1.0.0` release plus a floating | ||||||||||
| `v1` major tag, and switching consumers to the **hybrid pin** | ||||||||||
| `@<sha> # v1.0.0`, gives us: | ||||||||||
|
|
||||||||||
| - Immutable execution (SHA wins at runtime — tag rewrites are | ||||||||||
| ineffective). | ||||||||||
| - Human-readable diffs (`# v1.0.0` says what the SHA *is*). | ||||||||||
| - Automatic upgrade PRs via Dependabot, which since | ||||||||||
| [dependabot-core PR #5951](https://github.com/dependabot/dependabot-core/pull/5951) | ||||||||||
| rewrites BOTH the SHA and the trailing tag comment. | ||||||||||
| - Group bumps so 5 consumer PRs become 1 reviewable batch. | ||||||||||
|
|
||||||||||
| ## Strategy comparison | ||||||||||
|
|
||||||||||
| | Strategy | Immutable execution | Readable diff | Dependabot friendly | Tag-rewrite resistant | | ||||||||||
| |---|---|---|---|---| | ||||||||||
| | Raw SHA only (current) | yes | **no** | yes (rewrites SHA) | yes | | ||||||||||
| | `@v1` floating major | no | yes | yes | **no** | | ||||||||||
| | `@v1.0.0` exact tag | no | yes | yes | no | | ||||||||||
| | **Hybrid `@<sha> # v1.0.0`** | yes | yes | yes (PR #5951) | yes | | ||||||||||
|
|
||||||||||
| The "hybrid" column is the only one that's yes across the board. It | ||||||||||
| exists in the wild — Renovate has supported it for years, Dependabot | ||||||||||
| caught up in 2024. For an internal repo with 5 consumers and one | ||||||||||
| admin, this is the right answer. | ||||||||||
|
|
||||||||||
| ## Why not just `@v1` | ||||||||||
|
|
||||||||||
| For an internal-only repo it's tempting — five consumers, one admin, | ||||||||||
| small blast radius. The trade is real: a malicious or buggy force-push | ||||||||||
| to `v1` immediately runs in every consumer's CI with their secrets | ||||||||||
| (`VPS_SSH_KEY`, `ANTHROPIC_API_KEY`). The | ||||||||||
| [`tj-actions/changed-files` March 2025 incident](https://www.stepsecurity.io/blog/pinning-github-actions-for-enhanced-security-a-complete-guide) | ||||||||||
| proved this is a real attack class, not theoretical: SHA-pinned | ||||||||||
| consumers survived; tag-pinned consumers ran the attacker's payload. | ||||||||||
|
|
||||||||||
| For workflows that handle secrets (`reusable-deploy-vps`, | ||||||||||
| `reusable-security-gate`), raw `@v1` is unacceptable. For | ||||||||||
| `openapi-sync` (no secrets, no privileged operations), it would be | ||||||||||
| acceptable — but mixing pin strategies across workflows in the same | ||||||||||
| repo creates a footgun where someone copy-pastes the `openapi-sync` | ||||||||||
| pattern onto `deploy-vps`. Hybrid pin every workflow uniformly and | ||||||||||
| avoid the discipline tax. | ||||||||||
|
|
||||||||||
| ## What constitutes a "breaking change" for a reusable workflow | ||||||||||
|
|
||||||||||
| Reusable workflows are functions consumers call. The contract surface | ||||||||||
| is: | ||||||||||
|
|
||||||||||
| 1. **Inputs** in `on.workflow_call.inputs` — adding required input, | ||||||||||
| removing input, changing type, narrowing accepted values | ||||||||||
| → MAJOR. | ||||||||||
| 2. **Secrets** in `on.workflow_call.secrets` — adding required | ||||||||||
| secret, removing secret, renaming → MAJOR. | ||||||||||
| 3. **Outputs** in `on.workflow_call.outputs` — removing, renaming | ||||||||||
| → MAJOR. | ||||||||||
| 4. **Required permissions** in `permissions:` — broadening (consumer | ||||||||||
| must grant) → MAJOR. Narrowing → MINOR. | ||||||||||
| 5. **Observable side effects** — artifacts emitted under a different | ||||||||||
| name, env vars set, files written, deploy targets changed → | ||||||||||
| MAJOR if relied upon. | ||||||||||
| 6. **Behavioural changes that don't break the contract** but change | ||||||||||
| outcomes (e.g. tightening security checks, bumping Go) → MINOR. | ||||||||||
| 7. **Pure bug fixes / docs / dependency SHA bumps** → PATCH. | ||||||||||
|
|
||||||||||
| Adopt [Conventional Commits](https://www.conventionalcommits.org/) | ||||||||||
| so release-please can infer the bump: | ||||||||||
|
|
||||||||||
| - `feat:` → MINOR | ||||||||||
| - `fix:` → PATCH | ||||||||||
| - `feat!:` / `BREAKING CHANGE:` footer → MAJOR | ||||||||||
|
|
||||||||||
| ## Release toolchain | ||||||||||
|
|
||||||||||
| [`release-please`](https://github.com/googleapis/release-please) with | ||||||||||
| `release-type: simple` fits a workflows-only repo (no `package.json`, | ||||||||||
| no `pyproject.toml` to bump). It maintains `version.txt` + | ||||||||||
| `CHANGELOG.md` from Conventional Commits. | ||||||||||
|
|
||||||||||
| `.github/workflows/release-please.yml`: | ||||||||||
|
|
||||||||||
| ```yaml | ||||||||||
| name: release-please | ||||||||||
| on: | ||||||||||
| push: | ||||||||||
| branches: [main] | ||||||||||
| permissions: | ||||||||||
| contents: write | ||||||||||
| pull-requests: write | ||||||||||
| jobs: | ||||||||||
| release-please: | ||||||||||
| runs-on: ubuntu-latest | ||||||||||
| steps: | ||||||||||
| - uses: googleapis/release-please-action@db8f2c60ee802b3748b512940dde88eabd7b7e01 # v4.4.0 | ||||||||||
| id: release | ||||||||||
| with: | ||||||||||
| release-type: simple | ||||||||||
| # generates: version.txt, CHANGELOG.md, GitHub Release | ||||||||||
| - if: ${{ steps.release.outputs.release_created }} | ||||||||||
| name: Move floating v1 tag | ||||||||||
| run: | | ||||||||||
| git tag -f v1 "${{ steps.release.outputs.tag_name }}" | ||||||||||
| git push -f origin v1 | ||||||||||
| ``` | ||||||||||
|
|
||||||||||
| The `v1` tag move runs only on a release commit, not on every push. | ||||||||||
| After `v1.0.0` ships, every subsequent `v1.X.Y` release advances `v1` | ||||||||||
| in lockstep. Consumers on `@<sha> # v1.0.0` are unaffected by the | ||||||||||
| floating tag — they upgrade through Dependabot, which sees the new | ||||||||||
| release on the Releases API and proposes a new pinned SHA + comment. | ||||||||||
|
|
||||||||||
| When we cut `v2.0.0`, `v1` stops moving forward; consumers still on | ||||||||||
| `v1.X.Y` keep working. New consumers can opt into `@<sha> # v2` or | ||||||||||
| stay on `v1` until ready. | ||||||||||
|
|
||||||||||
| ## Tag immutability — rulesets | ||||||||||
|
|
||||||||||
| GitHub's legacy "tag protection rules" are deprecated. Use rulesets: | ||||||||||
|
|
||||||||||
| `Settings → Rules → Rulesets → New ruleset`: | ||||||||||
|
|
||||||||||
| - Name: `protect-version-tags` | ||||||||||
| - Enforcement status: Active | ||||||||||
| - Target: Tags → name pattern `v*` | ||||||||||
| - Rules: | ||||||||||
| - Restrict creations (allow only via release-please) | ||||||||||
| - Restrict updates | ||||||||||
| - Restrict deletions | ||||||||||
| - Bypass list: repository admins only, with audit logging. | ||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since the You should update the bypass list to also allow the GitHub Actions bypass actor (or the specific integration/app used for the workflow) to bypass the ruleset for tag updates.
Suggested change
|
||||||||||
|
|
||||||||||
| This makes a malicious `git push -f origin v1.0.0` from a compromised | ||||||||||
| collaborator account fail. A compromised admin account is still | ||||||||||
| game-over for the repo, but that's defended by the existing 2FA + | ||||||||||
| SSH-key audit posture. | ||||||||||
|
|
||||||||||
| ## Dependabot config — one file per consumer | ||||||||||
|
|
||||||||||
| `.github/dependabot.yml` in each of the 5 consumer repos | ||||||||||
| (polymarket-arb, launchgate, benchmark-apps, architecture, | ||||||||||
| chaos-scenarios): | ||||||||||
|
|
||||||||||
| ```yaml | ||||||||||
| version: 2 | ||||||||||
| updates: | ||||||||||
| - package-ecosystem: github-actions | ||||||||||
| directory: / | ||||||||||
| schedule: | ||||||||||
| interval: weekly | ||||||||||
| day: monday | ||||||||||
| groups: | ||||||||||
| muntianus-workflows: | ||||||||||
| patterns: | ||||||||||
| - muntianus/.github/* | ||||||||||
|
Comment on lines
+171
to
+172
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In Dependabot, the dependency name for a reusable workflow is the repository name (e.g., To have the group correctly match and group updates for these workflows, the pattern should be updated to
Suggested change
|
||||||||||
| commit-message: | ||||||||||
| prefix: ci | ||||||||||
| include: scope | ||||||||||
| labels: | ||||||||||
| - dependencies | ||||||||||
| - github-actions | ||||||||||
| open-pull-requests-limit: 5 | ||||||||||
| ``` | ||||||||||
|
|
||||||||||
| The `groups` block collapses all `muntianus/.github/*` bumps into one | ||||||||||
| PR per week, so reviewers don't drown in five identical bumps. | ||||||||||
| `commit-message.prefix: ci` keeps the merged history clean — | ||||||||||
| Conventional-Commits-shaped, doesn't trigger a release-please bump on | ||||||||||
| the consumer. | ||||||||||
|
|
||||||||||
| Dependabot reads the consumer's existing `@<sha> # v1.0.0` lines and, | ||||||||||
| on a release, rewrites both: | ||||||||||
|
|
||||||||||
| ```diff | ||||||||||
| - uses: muntianus/.github/.github/workflows/reusable-ci-go.yml@deb9a4da82fa78369ab44819a6fbfa12d231f064 # v1.0.0 | ||||||||||
| + uses: muntianus/.github/.github/workflows/reusable-ci-go.yml@2a1b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b # v1.1.0 | ||||||||||
| ``` | ||||||||||
|
|
||||||||||
| If the comment is malformed, Dependabot falls back to bumping just | ||||||||||
| the SHA, which is degraded but not broken. | ||||||||||
|
|
||||||||||
| ## One-shot rollout checklist | ||||||||||
|
|
||||||||||
| 1. **Adopt Conventional Commits** for this repo (one-line update to | ||||||||||
| `AGENTS.md`). | ||||||||||
| 2. **Land `release-please.yml`** workflow (above). | ||||||||||
| 3. **Tag `v1.0.0` manually for the first release** — `git tag | ||||||||||
| v1.0.0; git push origin v1.0.0`. Future releases come from | ||||||||||
| release-please automatically. | ||||||||||
| 4. **Hand-author `CHANGELOG.md`** for v1.0.0 describing each | ||||||||||
| workflow's `inputs`/`secrets`/`outputs` contract — this is the | ||||||||||
| reference downstream consumers cite when judging "is this bump | ||||||||||
| safe for me?". | ||||||||||
| 5. **Push floating `v1`** — `git tag -f v1 v1.0.0; git push -f | ||||||||||
| origin v1`. | ||||||||||
| 6. **Configure ruleset** on `v*` per the previous section. | ||||||||||
| 7. **PR each of the 5 consumer repos** swapping | ||||||||||
| `@<sha>` → `@<sha> # v1.0.0` and dropping the | ||||||||||
| `# main (post #27 SHA-pinning)` comments. | ||||||||||
| 8. **Enable Dependabot** on each consumer per the config above. | ||||||||||
|
|
||||||||||
| ## Ongoing process | ||||||||||
|
|
||||||||||
| - Land changes on this repo via Conventional-Commits PRs. | ||||||||||
| - `release-please` opens a release PR with computed version + draft | ||||||||||
| CHANGELOG. Reviewers can hand-edit before merging. | ||||||||||
| - Merging the release PR cuts the tag, the GitHub Release, and | ||||||||||
| re-points `v1`. | ||||||||||
| - Dependabot picks up the new release within 7 days and fans out | ||||||||||
| hybrid-pin bumps to consumers. | ||||||||||
| - Consumers review the grouped PR (typically one of 5 line edits + | ||||||||||
| a CHANGELOG link) and merge. Their next CI run is on the new SHA. | ||||||||||
|
|
||||||||||
| ## Honest critique — when SHA-only stays the right answer | ||||||||||
|
|
||||||||||
| For workflows that touch secrets we *don't* control (third-party | ||||||||||
| actions like `actions/checkout`), SHA-pin always — the upstream | ||||||||||
| maintainer's `v4` can change under us, and we have no operational | ||||||||||
| relationship with them. | ||||||||||
|
|
||||||||||
| For our own org-internal workflows the hybrid pin is right because we | ||||||||||
| control both ends of the supply chain: a malicious `v1.0.0` rewrite | ||||||||||
| requires admin-level repo compromise (rulesets) AND a 7-day window | ||||||||||
| where Dependabot eventually picks up the rewrite. Detection is high, | ||||||||||
| attack window is narrow, and the readability + automation wins are | ||||||||||
| real. | ||||||||||
|
|
||||||||||
| For a one-consumer repo, raw SHA without ceremony is fine — the | ||||||||||
| upgrade volume doesn't justify the toolchain. The break-even point | ||||||||||
| is around 3+ consumers and weekly upstream activity. | ||||||||||
|
|
||||||||||
| ## References | ||||||||||
|
|
||||||||||
| - [GitHub Secure-use docs — pinning](https://docs.github.com/en/actions/reference/security/secure-use) (full-length SHA is the only immutable form) | ||||||||||
| - [Well-Architected — Securing GHA](https://wellarchitected.github.com/library/application-security/recommendations/actions-security/) (org-level guidance) | ||||||||||
| - [StepSecurity — pinning for security](https://www.stepsecurity.io/blog/pinning-github-actions-for-enhanced-security-a-complete-guide) (tj-actions incident analysis) | ||||||||||
| - [GitHub changelog Aug-2025 — SHA-pinning policy enforcement](https://github.blog/changelog/2025-08-15-github-actions-policy-now-supports-blocking-and-sha-pinning-actions/) (org-wide enforcement option) | ||||||||||
| - [Community discussion #30049 — reusable workflow semver](https://github.com/orgs/community/discussions/30049) (no official semver policy yet) | ||||||||||
| - [Dependabot github-actions ecosystem](https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file) (config reference) | ||||||||||
| - [dependabot-core PR #5951 — hybrid pin support](https://github.com/dependabot/dependabot-core/pull/5951) (rewrites trailing tag comment) | ||||||||||
| - [Available rules for rulesets](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/available-rules-for-rulesets) (tag rulesets — legacy protection deprecated) | ||||||||||
| - [release-please](https://github.com/googleapis/release-please) (`release-type: simple` is the right shape) | ||||||||||
| - [Xebia — How GitHub Actions versioning works](https://xebia.com/blog/how-github-actions-versioning-system-works/) (semver tradeoffs in practice) | ||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The workflow steps do not clone the repository. Without a cloned repository, the runner's workspace is empty, and the subsequent
git tagandgit pushcommands in theMove floating v1 tagstep will fail with anot a git repositoryerror.Please add the
actions/checkoutstep at the beginning of the job, using a SHA-pinned reference to adhere to the repository's security guidelines.