Skip to content

ci: tag-driven release with native GitHub workflows#154

Merged
thepagent merged 20 commits intomainfrom
feat/tag-driven-release
Apr 11, 2026
Merged

ci: tag-driven release with native GitHub workflows#154
thepagent merged 20 commits intomainfrom
feat/tag-driven-release

Conversation

@neilkuan
Copy link
Copy Markdown
Contributor

@neilkuan neilkuan commented Apr 9, 2026

(Updated and co-authored by @thepagent)

問題

原本的 CI 只有 push-to-main 觸發的 build workflow,沒有:

  1. PR 檢查 — 壞掉的程式碼可以直接 merge 到 main(例如 #158 的 duplicate import 導致 main build 全部失敗)
  2. 版本化 release 流程 — 每次 push main 都 build,沒有版本概念,無法區分 pre-release 和 stable

我們曾考慮使用第三方 action Songmu/tagpr 來自動化 release,但評估後認為其僅 292 stars、需要授予 GitHub App 廣泛的 repo 權限,存在供應鏈風險,因此改用原生 GitHub workflows 實現。

設計

採用 tag-driven release 流程,核心原則:測過什麼就發什麼

  • stable release 不重新 build
  • 直接 promote 驗證過的 pre-release image

零第三方依賴,只使用 GitHub 官方 actions + shell 實現:

PR merge to main(日常開發,不觸發 release)
        │
        ▼
Maintainer 觸發 release-pr.yml (workflow_dispatch)
  → 自動算版本號(patch/minor/major)或手動指定
  → 建立 Release PR,更新 Cargo.toml + Chart.yaml
        │
        ▼
Merge Release PR
  → tag-on-merge.yml 自動打 tag (e.g. v0.7.0-rc.1)
  → build.yml 觸發:4 variants × 2 platforms
  → 部署測試
        │
        ▼
測試通過 → 再跑一次,指定 stable version (e.g. 0.7.0)
  → promote-stable: re-tag rc image → 0.7.0 / 0.7 / latest
  ⚠️ 不 rebuild,同一個 artifact

實現

檔案 說明
ci.yml 新增 — PR check: cargo check + clippy + test,防止壞程式碼進 main
release-pr.yml 新增 — workflow_dispatch 建立 Release PR,自動/手動版本號
tag-on-merge.yml 新增 — release/ PR merge 後用 App token 自動打 tag
build.yml 改為 tag-driven:pre-release 完整 build / stable promote
release.yml 簡化,保留 chart-releaser
RELEASING.md 完整重寫

GitHub App 權限從 8 項簡化為 3 項(Contents、Metadata、Pull requests)。

@neilkuan neilkuan requested a review from thepagent as a code owner April 9, 2026 00:24
@neilkuan neilkuan changed the title ci: switch Build & Release to git tag driven flow ci: tag-driven release with tagpr automation Apr 9, 2026
@neilkuan neilkuan requested a review from thepagent April 9, 2026 03:21
@neilkuan neilkuan changed the title ci: tag-driven release with tagpr automation ci: tag-driven release with tagpr + promote-stable Apr 9, 2026
@the3mi
Copy link
Copy Markdown
Contributor

the3mi commented Apr 9, 2026

Code Review: tagpr + promote-stable PR

Logic Test Results ✅

Tested the core flow — tag validation, pre-release detection, and promote-stable tag lookup all work correctly.

⚠️ One Issue Found: appVersion in postVersionCommand

.tagpr

When TAGPR_NEXT_VERSION=0.7.0 (stable), appVersion gets set to 0.7.0, but the promoted stable image was actually built from 0.7.0-rc.2.

Result: users see appVersion: 0.7.0 but the binary is from the rc build.

Suggested fix: Set appVersion to the actual pre-release tag that was promoted.

✅ Design Looks Good

  • promote-stable using git tag to find image (cross-commit promote) ✅
  • DAG split between pre-release build and stable promote ✅
  • versionFile sync across Cargo.toml + Chart.yaml ✅
  • RELEASING.md is thorough ✅

Questions

  1. APP_ID / APP_PRIVATE_KEY secrets — are these already set up in the repo? The workflow will fail without them.

@pahud pahud changed the title ci: tag-driven release with tagpr + promote-stable ci: tag-driven release with native GitHub workflows Apr 11, 2026
@chaodu-agent
Copy link
Copy Markdown
Collaborator

PR Review

1. What problem does this solve?

The original CI had two pain points:

2. How does it solve it?

Tag-driven release flow with the core principle: what you tested is what you ship.

  • ci.yml — PR-level cargo check + clippy + test to gate bad code
  • release-pr.yml — workflow_dispatch creates a Release PR, auto or manual version bump, updates Cargo.toml + Chart.yaml
  • tag-on-merge.yml — auto-tags on release/ branch merge using App token
  • build.yml — tag-driven: pre-release does full build (4 variants × 2 platforms), stable uses imagetools create to re-tag the validated pre-release image (no rebuild)
  • release-chart job — unified helm push to OCI registry inside build.yml
  • release.yml — simplified to chart-releaser only (GitHub Pages index)

Zero third-party dependencies (only GitHub official actions). GitHub App permissions reduced from 8 to 3.

3. Were alternatives considered?

Yes — Songmu/tagpr was implemented (visible in commits 6–10) but ultimately replaced with native GitHub workflows in the final commit by @thepagent. Reason: tagpr has only 292 stars and requires broad repo permissions, posing supply chain risk.

4. Is this the best approach? Issues & suggestions

Overall: solid design. A few things to address:

⚠️ release-chart job if condition should also check resolve-tag

The always() means if resolve-tag fails, chart_version will be empty and helm package will break. Suggest:

if: >-
  ${{ always() && inputs.dry_run != true &&
      needs.resolve-tag.result == 'success' &&
      (needs.merge-manifests.result == 'success' || needs.promote-stable.result == 'success') }}

⚠️ release-pr.yml does not update Cargo.lock

sed -i updates Cargo.toml version but never runs cargo generate-lockfile. After merge, Cargo.lock will have a stale version. Should add a step to regenerate it.

💡 tag-on-merge.yml — no version format validation

If someone creates and merges a release/something-weird branch, it will push a garbage tag. build.yml resolve-tag will reject it, but the tag still exists. Consider adding a simple format check before git tag.

💡 build.yml workflow_dispatch tag input has no placeholder

Easy to forget what format to use when triggering manually. A placeholder like v0.7.0-rc.1 would help.

✅ appVersion issue raised by @the3mi is resolved

The final version uses semver directly for appVersion (e.g. 0.6.0) instead of commit SHA, so promote-stable no longer causes a mismatch. 👍


Verdict: Good to merge after fixing the release-chart if-condition and the Cargo.lock sync issue. The rest are minor improvements that can be follow-ups.

@thepagent
Copy link
Copy Markdown
Collaborator

All four items addressed in 9e06b1d:

  1. release-chart if-condition — added needs.resolve-tag.result == 'success' guard
  2. Cargo.lock sync — added dtolnay/rust-toolchain + cargo generate-lockfile after version bump in release-pr.yml
  3. tag-on-merge version validation — added ^v[0-9]+\.[0-9]+\.[0-9]+ format check before git tag
  4. build.yml tag placeholder — added default: 'v' for the workflow_dispatch input

Thanks for the thorough review @chaodu-agent!

@pahud pahud force-pushed the feat/tag-driven-release branch from c99ba9d to 2e627ac Compare April 11, 2026 06:32
neilkuan and others added 18 commits April 11, 2026 14:32
- trigger: push tags v* (instead of push to main with paths filter)
- version: parsed from tag (v0.7.0-beta.1 → 0.7.0-beta.1)
- docker tags: sha + semver + major.minor + latest (stable only)
- bump-chart: version comes directly from tag, no more GITHUB_RUN_NUMBER
- workflow_dispatch: kept for manual trigger with explicit tag input
Beta (tag contains '-'):
  → release-chart-beta: helm package + push to OCI registry
  → no PR, main branch untouched

Stable (tag without '-'):
  → bump-chart-stable: update Chart.yaml + values.yaml → PR → auto merge
  → release.yml picks up Chart.yaml change → publish to GitHub Pages + OCI
- Add resolve-tag job: validates tag format, parses chart_version,
  image_sha, is_beta — single source of truth for all downstream jobs
- Fix: workflow_dispatch now uses inputs.tag instead of github.ref_name
  for beta/stable branching (github.ref_name is branch name, not tag)
- Remove 3× duplicated 'Resolve version tag' steps
- IMAGE_SHA computed once in resolve-tag, not repeated per job
- build-image + merge-manifests: only run for beta tags
- promote-stable: retag existing beta image with stable tags
  (version, major.minor, latest) using imagetools create
- verify beta image exists before promoting — fail fast if not
- bump-chart-stable now depends on promote-stable

This ensures "what you tested is what you ship" — the stable
release uses the exact same image artifact validated during beta.
- Add tagpr.yml with GitHub App token (so tags trigger build.yml)
- Simplify build.yml: remove beta/stable two-stage, all tags do full build
- Add pre-release support: manual tags like v0.7.0-rc.1 won't overwrite latest
- Configure .tagpr to sync Cargo.toml + Chart.yaml version/appVersion
- Simplify release.yml: keep chart-releaser + install instructions
- Align Cargo.toml and Chart.yaml versions to 0.6.0
- Rewrite RELEASING.md with new tag-driven flow
- Pre-release tag (v0.7.0-rc.1): full build, image tags = sha + version
- Stable tag (v0.7.0): promote (re-tag) pre-release image, no rebuild
- Verify pre-release image exists before promote, fail if not found
- Update RELEASING.md with two-path flow diagram
… SHA

- promote-stable uses git tag -l to find latest pre-release tag (e.g. v0.7.0-rc.*)
- re-tags pre-release image to stable tags (no rebuild, same artifact)
- removes commit SHA dependency — pre-release and stable can be on different commits
- natural flow: pre-release first → test → merge Release PR → auto promote
- Remove tagpr.yml and .tagpr (third-party dependency)
- Add release-pr.yml: workflow_dispatch with auto/manual version bump
- Add tag-on-merge.yml: auto-tag on release PR merge
- Update RELEASING.md: document new flow, simplify App permissions
- release-chart: add resolve-tag.result == 'success' to if-condition
- release-pr: add rust-toolchain + cargo generate-lockfile for Cargo.lock sync
- tag-on-merge: validate version format before pushing tag
- build: add default placeholder for workflow_dispatch tag input
@pahud pahud force-pushed the feat/tag-driven-release branch from 2e627ac to fef071a Compare April 11, 2026 06:33
@thepagent thepagent merged commit beeb42e into main Apr 11, 2026
1 check passed
Reese-max pushed a commit to Reese-max/openab that referenced this pull request Apr 12, 2026
* ci: switch Build & Release to git tag driven flow

- trigger: push tags v* (instead of push to main with paths filter)
- version: parsed from tag (v0.7.0-beta.1 → 0.7.0-beta.1)
- docker tags: sha + semver + major.minor + latest (stable only)
- bump-chart: version comes directly from tag, no more GITHUB_RUN_NUMBER
- workflow_dispatch: kept for manual trigger with explicit tag input

* ci: mark beta chart releases as pre-release

* ci: split chart release — beta pushes OCI only, stable opens PR

Beta (tag contains '-'):
  → release-chart-beta: helm package + push to OCI registry
  → no PR, main branch untouched

Stable (tag without '-'):
  → bump-chart-stable: update Chart.yaml + values.yaml → PR → auto merge
  → release.yml picks up Chart.yaml change → publish to GitHub Pages + OCI

* ci: add resolve-tag job, fix workflow_dispatch bug, deduplicate

- Add resolve-tag job: validates tag format, parses chart_version,
  image_sha, is_beta — single source of truth for all downstream jobs
- Fix: workflow_dispatch now uses inputs.tag instead of github.ref_name
  for beta/stable branching (github.ref_name is branch name, not tag)
- Remove 3× duplicated 'Resolve version tag' steps
- IMAGE_SHA computed once in resolve-tag, not repeated per job

* ci: stable release promotes beta image instead of rebuilding

- build-image + merge-manifests: only run for beta tags
- promote-stable: retag existing beta image with stable tags
  (version, major.minor, latest) using imagetools create
- verify beta image exists before promoting — fail fast if not
- bump-chart-stable now depends on promote-stable

This ensures "what you tested is what you ship" — the stable
release uses the exact same image artifact validated during beta.

* ci: add tagpr workflow and config for automated release PR

* ci: pin tagpr action to commit hash and bump checkout to v6

* ci: add tagpr workflow and config for automated release PR

- Add tagpr.yml with GitHub App token (so tags trigger build.yml)
- Simplify build.yml: remove beta/stable two-stage, all tags do full build
- Add pre-release support: manual tags like v0.7.0-rc.1 won't overwrite latest
- Configure .tagpr to sync Cargo.toml + Chart.yaml version/appVersion
- Simplify release.yml: keep chart-releaser + install instructions
- Align Cargo.toml and Chart.yaml versions to 0.6.0
- Rewrite RELEASING.md with new tag-driven flow

* ci: add promote-stable to guarantee pre-release = stable image

- Pre-release tag (v0.7.0-rc.1): full build, image tags = sha + version
- Stable tag (v0.7.0): promote (re-tag) pre-release image, no rebuild
- Verify pre-release image exists before promote, fail if not found
- Update RELEASING.md with two-path flow diagram

* docs: rewrite RELEASING.md with complete release flow and constraints

* ci: promote-stable finds pre-release image by version tag, not commit SHA

- promote-stable uses git tag -l to find latest pre-release tag (e.g. v0.7.0-rc.*)
- re-tags pre-release image to stable tags (no rebuild, same artifact)
- removes commit SHA dependency — pre-release and stable can be on different commits
- natural flow: pre-release first → test → merge Release PR → auto promote

* chore: update openab version to 0.6.0 in Cargo.lock

* ci: upgrade tagpr to v1.18.1 and use vPrefix config

* ci: replace deprecated app-id with client-id in create-github-app-token

* ci: add CI workflow for PR validation on source and Dockerfile changes

* docs: add GitHub App permissions to RELEASING.md

* ci: replace tagpr with native GitHub workflows

- Remove tagpr.yml and .tagpr (third-party dependency)
- Add release-pr.yml: workflow_dispatch with auto/manual version bump
- Add tag-on-merge.yml: auto-tag on release PR merge
- Update RELEASING.md: document new flow, simplify App permissions

* ci: address review feedback

- release-chart: add resolve-tag.result == 'success' to if-condition
- release-pr: add rust-toolchain + cargo generate-lockfile for Cargo.lock sync
- tag-on-merge: validate version format before pushing tag
- build: add default placeholder for workflow_dispatch tag input

* fix: replace map_or with is_some_and to satisfy clippy

* chore: use beta instead of rc for pre-release naming

---------

Co-authored-by: thepagent <thepagent@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants