-
Notifications
You must be signed in to change notification settings - Fork 120
feat: Add merge-forward workflow #684
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
base: ubuntu-jammy
Are you sure you want to change the base?
Changes from all commits
c687b43
ea241df
f189a86
5d1d41f
58a2e05
9e02fc3
e9d1fbe
42a208e
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,171 @@ | ||
| name: Merge Forward | ||
|
|
||
| on: | ||
| pull_request: | ||
| types: [closed] | ||
| branches: | ||
| - ubuntu-jammy | ||
| - ubuntu-noble | ||
|
|
||
| # Serialize runs per source branch to prevent push races on re-runs | ||
| concurrency: | ||
| group: merge-forward-${{ github.base_ref }}-${{ github.event.pull_request.number }} | ||
| cancel-in-progress: false | ||
|
|
||
| # Default: no permissions; write access is granted only to the merge-forward job | ||
| permissions: {} | ||
|
|
||
| jobs: | ||
| merge-forward: | ||
| if: github.event.pull_request.merged == true | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| contents: write | ||
| pull-requests: write | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| with: | ||
| fetch-depth: 0 | ||
| token: ${{ secrets.GITHUB_TOKEN }} | ||
|
|
||
| - name: Configure git identity | ||
| run: | | ||
| git config user.name "github-actions[bot]" | ||
| git config user.email "github-actions[bot]@users.noreply.github.com" | ||
|
|
||
| - name: Determine forward target | ||
| id: target | ||
| env: | ||
| BASE_REF: ${{ github.base_ref }} | ||
| run: | | ||
| case "$BASE_REF" in | ||
| ubuntu-jammy) echo "target=ubuntu-noble" >> "$GITHUB_OUTPUT" ;; | ||
| ubuntu-noble) echo "target=ubuntu-resolute" >> "$GITHUB_OUTPUT" ;; | ||
| *) | ||
| echo "No forward target for $BASE_REF — skipping." | ||
| echo "target=" >> "$GITHUB_OUTPUT" | ||
| ;; | ||
| esac | ||
|
|
||
| - name: Check if merge-forward branch or PR already exists | ||
| if: steps.target.outputs.target != '' | ||
| id: check | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| PR_NUMBER: ${{ github.event.pull_request.number }} | ||
| SOURCE: ${{ github.base_ref }} | ||
| TARGET: ${{ steps.target.outputs.target }} | ||
| run: | | ||
| branch="merge-forward/${SOURCE}-to-${TARGET}-pr${PR_NUMBER}" | ||
| echo "branch=$branch" >> "$GITHUB_OUTPUT" | ||
|
|
||
| # Query PR history first, regardless of whether the branch still exists. | ||
| # This prevents re-opening a PR whose branch was deleted after it was closed. | ||
| pr_state=$(gh pr list --head "$branch" --state all --json state --jq '.[0].state // "NONE"') | ||
| if [ "$pr_state" != "NONE" ]; then | ||
| echo "exists=true" >> "$GITHUB_OUTPUT" | ||
| elif git ls-remote --exit-code origin "refs/heads/$branch" > /dev/null 2>&1; then | ||
| # Branch exists but no PR was ever created (previous run failed after push). | ||
| echo "exists=branch_only" >> "$GITHUB_OUTPUT" | ||
| else | ||
| echo "exists=false" >> "$GITHUB_OUTPUT" | ||
| fi | ||
|
|
||
| - name: Merge source into target branch | ||
| if: > | ||
| steps.target.outputs.target != '' && | ||
| (steps.check.outputs.exists == 'false' || steps.check.outputs.exists == 'branch_only') | ||
| id: merge | ||
| env: | ||
| SOURCE: ${{ github.base_ref }} | ||
| TARGET: ${{ steps.target.outputs.target }} | ||
| BRANCH: ${{ steps.check.outputs.branch }} | ||
| EXISTS: ${{ steps.check.outputs.exists }} | ||
| run: | | ||
| # Recovery path: branch exists but PR creation failed on a previous run. | ||
| # The merge result is already on the branch — skip straight to opening the PR. | ||
| if [ "$EXISTS" = "branch_only" ]; then | ||
| git fetch origin "$BRANCH" "$SOURCE" "$TARGET" | ||
| # No-op check: if source is already an ancestor of target the branch | ||
| # has nothing new to forward — skip PR creation entirely. | ||
| if git merge-base --is-ancestor "origin/$SOURCE" "origin/$TARGET"; then | ||
| echo "noop=true" >> "$GITHUB_OUTPUT" | ||
| exit 0 | ||
| fi | ||
| last_msg=$(git log -1 --pretty=%B "origin/$BRANCH") | ||
| if printf '%s' "$last_msg" | grep -q '^merge-forward: conflict'; then | ||
| echo "conflict=true" >> "$GITHUB_OUTPUT" | ||
| else | ||
| echo "conflict=false" >> "$GITHUB_OUTPUT" | ||
| fi | ||
| exit 0 | ||
| fi | ||
|
|
||
| git fetch origin "$SOURCE" "$TARGET" | ||
|
|
||
| # No-op check: source already fully contained in target — nothing to forward. | ||
| if git merge-base --is-ancestor "origin/$SOURCE" "origin/$TARGET"; then | ||
|
Comment on lines
+104
to
+107
Author
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. Disagreeing with this one. This is a merge-forward workflow, not a cherry-pick workflow — the intent is to keep the target branch fully current with the source, so using the live |
||
| echo "No new commits from $SOURCE not already in $TARGET — skipping." | ||
| echo "noop=true" >> "$GITHUB_OUTPUT" | ||
| exit 0 | ||
| fi | ||
|
|
||
| git checkout -b "$BRANCH" "origin/$TARGET" | ||
|
|
||
| if git merge --no-edit "origin/$SOURCE"; then | ||
| echo "conflict=false" >> "$GITHUB_OUTPUT" | ||
| else | ||
| # Record the conflict state and push an incomplete merge branch so the | ||
| # draft PR gives reviewers a base to resolve from. | ||
| git merge --abort | ||
| # Create a placeholder commit to mark the conflict — the reviewer will | ||
| # need to check out the branch and perform the merge manually. | ||
| git commit --allow-empty -m "merge-forward: conflict merging $SOURCE into $TARGET — resolve manually" | ||
| echo "conflict=true" >> "$GITHUB_OUTPUT" | ||
| fi | ||
|
|
||
| git push origin "$BRANCH" | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| - name: Open merge-forward PR | ||
| if: > | ||
| steps.target.outputs.target != '' && | ||
| (steps.check.outputs.exists == 'false' || steps.check.outputs.exists == 'branch_only') && | ||
| steps.merge.outputs.noop != 'true' | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| PR_NUMBER: ${{ github.event.pull_request.number }} | ||
| PR_TITLE: ${{ github.event.pull_request.title }} | ||
| PR_URL: ${{ github.event.pull_request.html_url }} | ||
| SOURCE: ${{ github.base_ref }} | ||
| TARGET: ${{ steps.target.outputs.target }} | ||
| BRANCH: ${{ steps.check.outputs.branch }} | ||
| CONFLICT: ${{ steps.merge.outputs.conflict }} | ||
| run: | | ||
| body_file=$(mktemp) | ||
| printf 'Automated merge-forward of `%s` into `%s`, triggered by #%s.\n\nSource PR: %s\n' \ | ||
| "$SOURCE" "$TARGET" "$PR_NUMBER" "$PR_URL" > "$body_file" | ||
|
|
||
| if [ "$CONFLICT" = "true" ]; then | ||
| printf '\n> [!WARNING]\n> **This merge-forward has conflicts.** The merge of `%s` into `%s` could not be applied cleanly.\n> Check out branch `%s`, perform the merge manually, resolve the conflicts, and mark this PR as ready for review.\n' \ | ||
| "$SOURCE" "$TARGET" "$BRANCH" >> "$body_file" | ||
| fi | ||
|
|
||
| draft_flag="" | ||
| [ "$CONFLICT" = "true" ] && draft_flag="--draft" | ||
|
|
||
| # shellcheck disable=SC2086 | ||
| gh pr create \ | ||
| --title "[Merge Forward $SOURCE→$TARGET] $PR_TITLE" \ | ||
| --body-file "$body_file" \ | ||
| --base "$TARGET" \ | ||
| --head "$BRANCH" \ | ||
| $draft_flag | ||
|
|
||
| rm -f "$body_file" | ||
|
|
||
| - name: Skip (merge-forward PR already exists) | ||
| if: steps.target.outputs.target != '' && steps.check.outputs.exists == 'true' | ||
| env: | ||
| BRANCH: ${{ steps.check.outputs.branch }} | ||
| run: | | ||
| echo "Merge-forward branch ${BRANCH} already exists with a PR — skipping." | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,14 +1,47 @@ | ||
| # Contributing to BOSH Linux Stemcell Builder | ||
|
|
||
| **NOTE:** Please ensure that changes are made to the earliest supported branch | ||
| to which the change should apply. The change should then be merged forward to | ||
| all other supported branches. | ||
|
|
||
| Branches are names for the Ubuntu release on which they are based. For example | ||
| an "Ubuntu SHORT_NAME" based stemcell will be on the branch: | ||
| Branches are named for the Ubuntu release on which they are based: | ||
| - `ubuntu-<short_name>` | ||
|
|
||
| As of `2026-06-09` the following stemcell lines / branches are supported: | ||
| - Ubuntu Jammy / `ubuntu-jammy` | ||
| - Ubuntu Noble / `ubuntu-noble` | ||
| - Ubuntu Resolute / `ubuntu-resolute` | ||
|
|
||
| ## Merge-Forward Strategy | ||
|
|
||
| This repository uses a **merge-forward** strategy to keep stemcell branches in sync. | ||
|
|
||
| **Branch order (oldest → newest):** | ||
|
|
||
| ```text | ||
| ubuntu-jammy → ubuntu-noble → ubuntu-resolute | ||
| ``` | ||
|
|
||
| ### How it works | ||
|
|
||
| 1. Open your PR against the **oldest applicable branch** (e.g. `ubuntu-jammy` if the change applies to all lines). | ||
| 2. Merge it. | ||
| 3. The merge-forward workflow automatically opens a PR merging that branch into the next one in the chain. | ||
| 4. Merge the forwarded PR, which then triggers another forward merge to the next branch, and so on. | ||
|
|
||
| No labels are needed — every PR merged into `ubuntu-jammy` or `ubuntu-noble` triggers an automatic forward merge to the next branch. | ||
|
|
||
| ### Clean vs. conflict merge-forwards | ||
|
|
||
| - **Clean merge** — the merge-forward PR is opened ready for review. | ||
| - **Conflict** — the merge-forward PR is opened as a draft. Check out the branch, | ||
| perform the merge manually, resolve the conflicts, and mark the PR ready for review. | ||
|
|
||
| ### Re-runs | ||
|
|
||
| The workflow is idempotent: if the merge-forward branch already exists but has no | ||
| associated PR (i.e. the branch was pushed but PR creation failed on a previous run), | ||
| re-running retries opening the PR without re-doing the merge. If a PR already exists | ||
| (open or closed), the run skips silently. | ||
|
|
||
| ### CI on merge-forward PRs | ||
|
|
||
| GitHub suppresses workflow runs triggered by a `GITHUB_TOKEN`-pushed branch. This | ||
| means the auto-opened merge-forward PR will not have CI results initially. A | ||
| maintainer can start CI by pushing a trivial commit to the forward branch. |
Uh oh!
There was an error while loading. Please reload this page.