-
Notifications
You must be signed in to change notification settings - Fork 3
ci: staging RC releases + automated back/forward-merge PRs #713
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
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,154 @@ | ||
| name: forward-merge | ||
|
|
||
| # Keeps the release branches converged automatically: | ||
| # - source_branch = production -> open a back-merge PR production -> staging, | ||
| # syncing .release-please-manifest.staging.json to the released stable | ||
| # version so the next staging RC computes from the new baseline. | ||
| # - source_branch = hotfix/vX.Y.Z -> open a forward-merge PR into production. | ||
| # (After that PR merges, run this workflow manually with | ||
| # source_branch=production to propagate the hotfix on to staging — no | ||
| # release is cut on production by the hotfix merge, so the automatic | ||
| # trigger doesn't fire.) | ||
| # | ||
| # Invoked via workflow_call from release-please.yml after a release is cut, or | ||
| # manually via workflow_dispatch. | ||
| # | ||
| # NOTE: PRs created with the default GITHUB_TOKEN do not trigger | ||
| # `pull_request` workflows (tests will not run on the PR). Set the | ||
| # FORWARD_MERGE_TOKEN secret (fine-grained PAT or GitHub App token with | ||
| # contents:write + pull-requests:write) to get CI on these PRs; without it the | ||
| # workflow falls back to GITHUB_TOKEN and you can close/reopen the PR to kick | ||
| # CI. | ||
|
|
||
| on: | ||
| workflow_call: | ||
| inputs: | ||
| tag_name: | ||
| description: Release tag that was just cut (e.g. v1.2.0) | ||
| required: true | ||
| type: string | ||
| source_branch: | ||
| description: Branch the release was cut on (production or hotfix/vX.Y.Z) | ||
| required: true | ||
| type: string | ||
| workflow_dispatch: | ||
| inputs: | ||
| tag_name: | ||
| description: Release tag driving the merge (e.g. v1.2.0) | ||
| required: true | ||
| type: string | ||
| source_branch: | ||
| description: Branch to merge from (production or hotfix/vX.Y.Z) | ||
| required: true | ||
| type: string | ||
|
|
||
| permissions: | ||
| contents: write | ||
| pull-requests: write | ||
|
|
||
| jobs: | ||
| back-merge-to-staging: | ||
| if: ${{ inputs.source_branch == 'production' }} | ||
| runs-on: ubuntu-latest | ||
| env: | ||
| GH_TOKEN: ${{ secrets.FORWARD_MERGE_TOKEN || github.token }} | ||
| TAG: ${{ inputs.tag_name }} | ||
| steps: | ||
| - uses: actions/checkout@v6.0.3 | ||
| with: | ||
| fetch-depth: 0 | ||
| token: ${{ secrets.FORWARD_MERGE_TOKEN || github.token }} | ||
|
|
||
| - name: Set up git user | ||
| run: | | ||
| git config user.name "github-actions[bot]" | ||
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | ||
|
|
||
| - name: Create merge branch off staging and merge production | ||
| id: merge | ||
| run: | | ||
| VERSION="${TAG#v}" | ||
| BRANCH="merge/production-into-staging-${VERSION}" | ||
| echo "branch=$BRANCH" >> "$GITHUB_OUTPUT" | ||
| git checkout -b "$BRANCH" origin/staging | ||
| if ! git merge --no-ff "origin/production" -m "merge: production ${TAG} into staging"; then | ||
| { | ||
| echo "### Back-merge conflict" | ||
| echo "" | ||
| echo "Merging \`production\` (${TAG}) into \`staging\` conflicts." | ||
| echo "Resolve manually:" | ||
| echo '```' | ||
| echo "git checkout -b $BRANCH origin/staging" | ||
| echo "git merge origin/production # resolve conflicts" | ||
| echo "git push origin $BRANCH" | ||
| echo "gh pr create --base staging --head $BRANCH --title 'chore: merge production ${TAG} into staging'" | ||
| echo '```' | ||
| } >> "$GITHUB_STEP_SUMMARY" | ||
| exit 1 | ||
| fi | ||
|
|
||
| - name: Sync staging release-please manifest to released version | ||
| run: | | ||
| VERSION="${TAG#v}" | ||
| jq --arg v "$VERSION" '."." = $v' .release-please-manifest.staging.json > manifest.tmp | ||
| mv manifest.tmp .release-please-manifest.staging.json | ||
| if ! git diff --quiet -- .release-please-manifest.staging.json; then | ||
| git add .release-please-manifest.staging.json | ||
| git commit -m "chore: sync staging release-please manifest to ${TAG}" | ||
| fi | ||
|
|
||
| # Retry-safe: skip if a PR is already open for this merge; force-with-lease | ||
| # handles a branch left behind by a previous partial run. | ||
| - name: Push branch and open PR | ||
| run: | | ||
| BRANCH="${{ steps.merge.outputs.branch }}" | ||
| EXISTING="$(gh pr list --base staging --head "$BRANCH" --state open --json number --jq '.[0].number // empty')" | ||
| if [ -n "$EXISTING" ]; then | ||
| echo "Back-merge PR #$EXISTING already open for $BRANCH; nothing to do." | ||
| exit 0 | ||
| fi | ||
| git push --force-with-lease origin "$BRANCH" | ||
| { | ||
| echo "Automated back-merge after release \`${TAG}\`." | ||
| echo "" | ||
| echo "- Brings the release commit and tag history into \`staging\`." | ||
| echo "- Syncs \`.release-please-manifest.staging.json\` to \`${TAG#v}\` so the next staging RC versions from the new stable baseline." | ||
| echo "" | ||
| echo "If version files conflict with in-flight staging work, accept either side — release-please rewrites them on the next Release PR." | ||
| } > pr-body.md | ||
| gh pr create \ | ||
| --base staging \ | ||
| --head "$BRANCH" \ | ||
| --title "chore: merge production ${TAG} into staging" \ | ||
| --body-file pr-body.md | ||
|
|
||
| forward-merge-to-production: | ||
| if: ${{ startsWith(inputs.source_branch, 'hotfix/') }} | ||
| runs-on: ubuntu-latest | ||
| env: | ||
| GH_TOKEN: ${{ secrets.FORWARD_MERGE_TOKEN || github.token }} | ||
| TAG: ${{ inputs.tag_name }} | ||
| SOURCE: ${{ inputs.source_branch }} | ||
| steps: | ||
| - uses: actions/checkout@v6.0.3 | ||
|
|
||
| # Retry-safe: skip if a PR is already open from this hotfix branch. | ||
| - name: Open PR hotfix -> production | ||
| run: | | ||
| EXISTING="$(gh pr list --base production --head "$SOURCE" --state open --json number --jq '.[0].number // empty')" | ||
| if [ -n "$EXISTING" ]; then | ||
| echo "Forward-merge PR #$EXISTING already open for $SOURCE; nothing to do." | ||
| exit 0 | ||
| fi | ||
| { | ||
| echo "Automated forward-merge of hotfix release \`${TAG}\`." | ||
| echo "" | ||
| echo "- Brings the hotfix commits and release bookkeeping into \`production\`." | ||
| echo "- No new release is cut by this merge (the \`${TAG}\` release commit is already included)." | ||
| echo "- After merging, run the \`forward-merge\` workflow manually with \`source_branch=production\` and \`tag_name=${TAG}\` to propagate the hotfix on to \`staging\`." | ||
| } > pr-body.md | ||
| gh pr create \ | ||
| --base production \ | ||
| --head "$SOURCE" \ | ||
| --title "chore: merge ${SOURCE} (${TAG}) into production" \ | ||
| --body-file pr-body.md | ||
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| { | ||
| ".": "1.1.0" | ||
| } |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| # Release Flow | ||
|
|
||
| How code moves from a feature branch to production, how versions are cut, and | ||
| how hotfixes work. The mechanics live in `.github/workflows/`; this doc is the | ||
| map. | ||
|
|
||
| ## Branch roles | ||
|
|
||
| | Branch | Role | Deploys to | Versioning | | ||
| |---|---|---|---| | ||
| | `jir*` | feature / ticket branches | `ocotillo-api-testing` (every push) | none | | ||
| | `staging` | integration branch (default) | `ocotillo-api-staging` (every push) | `vX.Y.Z-rc.N` prereleases via release-please | | ||
| | `production` | release branch | `ocotillo-api` (on release tag) | `vX.Y.Z` stable releases via release-please | | ||
| | `hotfix/vX.Y.Z` | emergency patch off a release tag | `ocotillo-api` (on release tag) | `vX.Y.Z` patch release via release-please | | ||
|
|
||
| ## The flow | ||
|
|
||
| ``` | ||
| feature (jir*) ──PR──▶ staging ──promotion PR──▶ production ──release PR──▶ tag vX.Y.Z ──▶ prod deploy | ||
| │ │ ▲ │ | ||
| ▼ ▼ │ auto forward-merge PR ▼ auto back-merge PR | ||
| testing svc staging svc deploy hotfix/vX.Y.(Z+1) production → staging | ||
| + RC release PR (off tag, via (syncs staging manifest) | ||
| → tag vX.Y.Z-rc.N hotfix-start.yml) | ||
| ``` | ||
|
|
||
| ### 1. Feature → staging (RC line) | ||
|
|
||
| 1. Branch `jir*` off `staging`; every push deploys the testing service | ||
| (`CD_testing.yml`). | ||
| 2. Merge the PR into `staging` (Conventional Commit title — `feat:`, `fix:`, | ||
| etc.; enforced by `pr-title-lint.yml`). | ||
| 3. Every push to `staging` deploys the staging service (`CD_staging.yml`) — | ||
| continuous, unversioned, date-stamped tag. | ||
| 4. release-please (staging config) maintains an **RC Release PR** | ||
| (`chore(staging): release X.Y.Z-rc.N`). Merging it tags `vX.Y.Z-rc.N` and | ||
| publishes a GitHub **prerelease**. This is a versioned checkpoint of what's | ||
| on staging — it never deploys production. | ||
| 5. Successive merges after an RC bump only the `rc.N` counter | ||
| (`versioning: prerelease`), so the target version is stable until promoted. | ||
|
|
||
| Cut an RC (merge the RC Release PR) when staging is in a state you intend to | ||
| promote — the RC tag is the thing you tested. | ||
|
|
||
| ### 2. Staging → production (stable release) | ||
|
|
||
| 1. Open a **promotion PR** `staging → production` (manual; this is the | ||
| "we want to ship what's on staging" decision). | ||
| 2. Merging it makes release-please (production config) open a **Release PR** | ||
| (`chore(production): release X.Y.Z`). | ||
| 3. Merging the Release PR tags `vX.Y.Z`, publishes the GitHub release, and the | ||
| same workflow run invokes `CD_production.yml` via `workflow_call` | ||
| (releases created with `GITHUB_TOKEN` don't emit events that trigger other | ||
| workflows, hence the inline call). | ||
| 4. `forward-merge.yml` then opens an automatic **back-merge PR | ||
| `production → staging`**, which also syncs | ||
| `.release-please-manifest.staging.json` to the released version so the next | ||
| RC computes from the new stable baseline. Merge it promptly. | ||
|
|
||
| ### 3. Hotfix | ||
|
|
||
| 1. Run the `hotfix-start` workflow (optionally pinning `base_tag`). It creates | ||
| `hotfix/vX.Y.(Z+1)` off the release tag. | ||
| 2. Open a fix PR targeting the hotfix branch (`fix:` title). | ||
| 3. release-please (production config, hotfix branch) opens a Release PR; | ||
| merging it tags `vX.Y.(Z+1)` and deploys production. | ||
| 4. `forward-merge.yml` automatically opens **`hotfix/vX.Y.(Z+1)` → | ||
| `production`**. Merge it. No new release is cut (the release commit is | ||
| already in the branch). | ||
| 5. Propagate to staging: run the `forward-merge` workflow manually with | ||
| `source_branch=production` and the hotfix tag (the hotfix merge doesn't cut | ||
| a release on production, so the automatic trigger doesn't fire). | ||
|
|
||
| ## Version-file ownership | ||
|
|
||
| | File | Written by | Lives meaningfully on | | ||
| |---|---|---| | ||
| | `.release-please-manifest.json` | release-please on `production` / `hotfix/v*` | production | | ||
| | `.release-please-manifest.staging.json` | release-please on `staging`; synced by back-merge PRs | staging | | ||
| | `pyproject.toml` version | release-please stable releases only (python release-type) | production | | ||
| | `CHANGELOG.md` | stable releases | production | | ||
| | `CHANGELOG-rc.md` | RC releases | staging | | ||
| | `version.txt` | RC releases (simple release-type bookkeeping) | staging | | ||
|
|
||
| RC releases deliberately do **not** touch `pyproject.toml`: `1.2.0-rc.1` is | ||
| not a valid PEP 440 version and would break `uv export` during staging | ||
| deploys, and skipping it avoids promotion-merge conflicts. | ||
|
|
||
| **Conflict rule:** if any of these files conflict during a merge, accept | ||
| either side and move on — release-please rewrites them on the next Release PR. | ||
| The manifests are the only state that matters, and the back-merge PR syncs the | ||
| staging one explicitly. | ||
|
|
||
| ## Caveats | ||
|
|
||
| - **CI on automated PRs:** PRs created with the default `GITHUB_TOKEN` do not | ||
| trigger `pull_request` workflows. Set the `FORWARD_MERGE_TOKEN` repo secret | ||
| (fine-grained PAT or GitHub App token, `contents: write` + | ||
| `pull-requests: write`) so back-merge/forward-merge PRs get CI. Without it, | ||
| close and reopen the PR to kick CI. | ||
| - **Workflow changes go live per branch:** release-please and the deploy | ||
| workflows resolve at the pushed branch's commit. A workflow fix merged to | ||
| `staging` does nothing for production releases until it reaches | ||
| `production`. | ||
| - **Tag visibility:** the staging release-please needs the last stable tag's | ||
| commit in `staging` history to bound its commit scan — another reason to | ||
| merge back-merge PRs promptly after each release. |
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| { | ||
| "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", | ||
| "release-type": "simple", | ||
| "include-v-in-tag": true, | ||
| "include-component-in-tag": false, | ||
| "bump-minor-pre-major": false, | ||
| "bump-patch-for-minor-pre-major": false, | ||
| "versioning": "prerelease", | ||
| "changelog-sections": [ | ||
| { "type": "feat", "section": "Features" }, | ||
| { "type": "fix", "section": "Bug Fixes" }, | ||
| { "type": "perf", "section": "Performance" }, | ||
| { "type": "deps", "section": "Dependencies" }, | ||
| { "type": "revert", "section": "Reverts" }, | ||
| { "type": "docs", "section": "Documentation", "hidden": true }, | ||
| { "type": "chore", "section": "Chores", "hidden": true }, | ||
| { "type": "refactor", "section": "Refactors", "hidden": true }, | ||
| { "type": "test", "section": "Tests", "hidden": true }, | ||
| { "type": "build", "section": "Build", "hidden": true }, | ||
| { "type": "ci", "section": "CI", "hidden": true }, | ||
| { "type": "style", "section": "Style", "hidden": true } | ||
| ], | ||
| "packages": { | ||
| ".": { | ||
| "package-name": "OcotilloAPI", | ||
| "prerelease": true, | ||
| "prerelease-type": "rc", | ||
| "changelog-path": "CHANGELOG-rc.md" | ||
| } | ||
| } | ||
| } |
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.