Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
# want: a known advisory beats an unaudited release, which is the same
# exception `scripts/dependency-age-exceptions.json` records for a human.
#
# Both ecosystems target `develop`, not `main`. A dependency bump is ordinary
# work, and ordinary work enters through `develop` -- see Branching in
# CONTRIBUTING.md. Opening these against `main` put them on the release branch
# and left the back-merge to carry them the wrong way round.
#
# See .agents/skills/check-dependencies/SKILL.md for the reasoning, and
# AGENTS.md for how a dependency change reaches a release.

Expand All @@ -18,6 +23,7 @@ updates:
- package-ecosystem: npm
# The workspace root: one pnpm-lock.yaml covers all four manifests.
directory: /
target-branch: develop
schedule:
interval: weekly
day: monday
Expand Down Expand Up @@ -46,6 +52,7 @@ updates:

- package-ecosystem: github-actions
directory: /
target-branch: develop
schedule:
interval: weekly
day: monday
Expand Down
69 changes: 45 additions & 24 deletions .github/workflows/backmerge.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
# After anything lands on main, keep develop in sync by opening (or refreshing)
# a pull request that merges main back into develop.
# After anything lands on main, bring develop up to it.
#
# A PR rather than a direct push, so the merge is visible and CI runs on it.
# Conflicts are resolved on the PR.
# A pull request that merges ITSELF. `develop` is protected -- pushes to it are
# refused with "Changes must be made through a pull request" -- so the PR is not
# optional. What is optional is the person, and the person was the problem: the
# first back-merge PR was merged with "squash", which copied main's content into
# develop as one ordinary commit and dropped its history. Git then had no way to
# know develop already held those changes, so every later back-merge tried to
# re-apply 236 commits whose content was already there: 67 conflicting files on
# a merge that should have been a formality.
#
# So the workflow opens the PR and turns on auto-merge with the MERGE method.
# GitHub merges it once the required checks pass, nobody chooses a method, and
# a squash cannot happen by accident. Auto-merge has to be enabled on the
# repository for this (`allow_auto_merge`); if it is off, the run says so and
# leaves the PR for a person, who must merge it with a merge commit.
#
# While the project is pre-release the flow is inverted from the eventual one:
# work happens on main, and develop follows it. Once releases start, features
Expand All @@ -16,6 +27,8 @@ on:
branches: [main]

permissions:
# Read is enough: nothing here pushes. The merge is GitHub'''s, done on the
# pull request once its checks pass.
contents: read
pull-requests: write

Expand All @@ -24,60 +37,68 @@ concurrency:
cancel-in-progress: false

jobs:
open-backmerge-pr:
back-merge:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0

- name: Skip if develop already contains main
- name: Decide what is needed
id: check
run: |
set -uo pipefail
# Said plainly rather than dying on `fatal: Not a valid object name`,
# which is what the first run did before develop existed.
if ! git rev-parse --verify --quiet origin/develop >/dev/null; then
echo "needs_pr=false" >> "$GITHUB_OUTPUT"
echo "action=none" >> "$GITHUB_OUTPUT"
echo "::notice::there is no develop branch, so there is nothing to back-merge into."
exit 0
fi
if git merge-base --is-ancestor origin/main origin/develop; then
echo "needs_pr=false" >> "$GITHUB_OUTPUT"
echo "action=none" >> "$GITHUB_OUTPUT"
echo "develop already contains main; nothing to do."
else
echo "needs_pr=true" >> "$GITHUB_OUTPUT"
echo "action=pr" >> "$GITHUB_OUTPUT"
fi

- name: Open or update back-merge PR
if: steps.check.outputs.needs_pr == 'true'
- name: Open the back-merge PR and let it merge itself
if: steps.check.outputs.action == 'pr'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
WORKFLOW_NAME: ${{ github.workflow }}
RUN_ID: ${{ github.run_id }}
run: |
set -uo pipefail
MAIN_SHA="$(git rev-parse --short origin/main)"
MAIN_SUBJECT="$(git log -1 --format='%s' origin/main)"

EXISTING="$(gh pr list --head main --base develop --state open --json number --jq '.[0].number')"
if [ -n "$EXISTING" ]; then
echo "PR #$EXISTING already open for main -> develop; it will auto-update with the new push."
exit 0
fi

NUMBER="$(gh pr list --head main --base develop --state open --json number --jq '.[0].number')"
BODY_FILE="$(mktemp)"
cat > "$BODY_FILE" <<EOF
Automated back-merge of \`main\` into \`develop\`.
Automated back-merge of \`main\` into \`develop\`. Auto-merge is on, so this merges itself once the required checks pass.

Latest commit on main: \`${MAIN_SHA}\` - ${MAIN_SUBJECT}

Triggered by push to \`main\` in workflow \`${WORKFLOW_NAME}\` run \`${RUN_ID}\`.

Merge this PR (or enable auto-merge on it) to keep \`develop\` in sync with the latest release commit on \`main\`. Resolve any conflicts manually before merging.
**Merge this with a MERGE COMMIT, not a squash.** A squash copies main's content without its history, so git stops knowing that develop already contains it and every later back-merge conflicts on files nobody touched. That is what happened once already; see the comment at the top of \`.github/workflows/backmerge.yml\`.
EOF

gh pr create \
--base develop \
--head main \
--title "chore: back-merge main into develop" \
--body-file "$BODY_FILE"
if [ -z "$NUMBER" ]; then
gh pr create \
--base develop \
--head main \
--title "chore: back-merge main into develop" \
--body-file "$BODY_FILE"
NUMBER="$(gh pr list --head main --base develop --state open --json number --jq '.[0].number')"
else
echo "PR #$NUMBER is already open; it tracks main and needs no new one."
fi

# --merge, never --squash: a squashed back-merge is what broke this
# once. If auto-merge is disabled on the repository this fails, and
# saying so is better than a PR that sits open looking healthy.
if ! gh pr merge "$NUMBER" --auto --merge; then
echo "::warning::could not enable auto-merge on PR #$NUMBER. Enable allow_auto_merge on the repository, or merge it by hand WITH A MERGE COMMIT."
fi
Loading