Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
126 commits
Select commit Hold shift + click to select a range
680768c
refactor(aidd-context): action citations use markdown links, not @
blafourcade Jul 16, 2026
1a52770
docs(framework): reposition readme as agnostic token-optimized framew…
alexsoyes Jul 18, 2026
f2eb27e
chore: back-merge main into next (conflicts) (#454)
aidd-bot[bot] Jul 20, 2026
f17ea91
refactor(aidd-context): restructure 10-learn into a 5-action router (…
blafourcade Jul 20, 2026
b5f3c55
ci(deps): bump github/codeql-action/analyze from 4.37.0 to 4.37.1 (#456)
dependabot[bot] Jul 20, 2026
8d8742c
ci(deps): bump github/codeql-action/autobuild from 4.37.0 to 4.37.1 (…
dependabot[bot] Jul 20, 2026
10bdd60
fix(cli): migrate aidd-cli into framework as cli/, full history prese…
blafourcade Jul 22, 2026
0515741
fix(cli): repoint self-references from aidd-cli to framework
blafourcade Jul 22, 2026
14acb5f
fix(cli): add license and keywords for the npm package page
blafourcade Jul 22, 2026
459701c
ci(deps): bump github/codeql-action/autobuild from 4.37.1 to 4.37.3 (…
dependabot[bot] Jul 22, 2026
1407d13
chore(deps-dev): bump @biomejs/biome from 2.4.7 to 2.5.5 in /cli (#477)
dependabot[bot] Jul 22, 2026
7cce315
chore(deps-dev): bump fast-check from 4.7.0 to 4.9.0 in /cli (#481)
dependabot[bot] Jul 22, 2026
f4c4a27
chore(deps-dev): bump knip from 6.16.1 to 6.27.0 in /cli (#482)
dependabot[bot] Jul 22, 2026
3b0da1e
chore(deps-dev): bump jscpd from 5.0.7 to 5.0.12 in /cli (#483)
dependabot[bot] Jul 22, 2026
499bec5
ci(deps): bump github/codeql-action/analyze from 4.37.1 to 4.37.3 (#475)
dependabot[bot] Jul 22, 2026
4870dd6
chore: back-merge main into next (conflicts) (#499)
aidd-bot[bot] Jul 22, 2026
395e9f3
fix(cli): scope plugin restore to --tool, collapse double materializa…
blafourcade Jul 24, 2026
69b0537
fix(cli): document intentional force on internal build-cache rebuild …
blafourcade Jul 24, 2026
2ec851b
refactor(cli): inject shared StatusUseCase/RestoreUseCase/UpdateOneTo…
blafourcade Jul 24, 2026
fd33892
fix(cli): resolve buildClaudeStyleMarketplaceEntry and PluginTranslat…
blafourcade Jul 24, 2026
4acabcb
refactor(aidd-refine): 01-brainstorm moves from fixed probing loop to…
blafourcade Jul 24, 2026
bfe9253
chore: back-merge main into next (conflicts) (#503)
aidd-bot[bot] Jul 24, 2026
6a8df2c
ci(deps): bump github/codeql-action/init from 4.37.0 to 4.37.3 (#476)
dependabot[bot] Jul 24, 2026
79b875a
ci(deps): bump actions/checkout from 7.0.0 to 7.0.1 (#458)
dependabot[bot] Jul 24, 2026
28dce18
ci(deps): bump actions/setup-node from 6.4.0 to 7.0.0 (#459)
dependabot[bot] Jul 24, 2026
29c8281
chore(deps-dev): bump @types/node from 24.10.15 to 26.1.1 in /cli (#480)
dependabot[bot] Jul 24, 2026
c72ea0d
chore(deps-dev): bump @commitlint/cli from 19.8.1 to 21.2.1 in /cli (…
dependabot[bot] Jul 24, 2026
c378c40
chore(deps): bump commander from 12.1.0 to 15.0.0 in /cli (#479)
dependabot[bot] Jul 24, 2026
ff802a8
chore(deps-dev): bump typescript from 5.9.3 to 7.0.2 in /cli (#484)
dependabot[bot] Jul 24, 2026
9762006
fix(cli): derive framework build's SUPPORTED_TARGETS from the build r…
blafourcade Jul 24, 2026
26e9241
test(cli): add AiTool registry conformance suite (#516)
blafourcade Jul 27, 2026
7ff9edb
refactor(cli): inject PostInstallPipelineUseCase and GitignoreUseCase…
blafourcade Jul 27, 2026
f1ece43
fix(cli): surface menu errors instead of swallowing them into an infi…
blafourcade Jul 27, 2026
5463aa3
fix(ci): promote.yml recognizes squash-merged back-merges as sync bou…
blafourcade Jul 27, 2026
57f4a52
fix(cli): drop the auth gate from self-update (#525)
blafourcade Jul 27, 2026
0ad4a48
test(cli): drop ticket and point-in-time references from test comment…
blafourcade Jul 27, 2026
eb08a27
fix(cli): move marketplace refresh --force cache clear into its use-c…
blafourcade Jul 27, 2026
07368d9
fix(cli): ide uninstall removes the settings keys it merged in (#529)
blafourcade Jul 27, 2026
dd163f1
fix(cli): report each status/doctor scope once, under an accurate lab…
blafourcade Jul 27, 2026
4ff167e
test(cli): pin the report-and-continue policy in UpdateOneToolUseCase…
blafourcade Jul 27, 2026
c502275
chore(deps-dev): bump js-yaml from 5.2.1 to 5.2.2 (#533)
dependabot[bot] Jul 27, 2026
cc2ef9e
chore(deps-dev): bump @types/node from 26.1.1 to 26.1.2 in /cli (#534)
dependabot[bot] Jul 27, 2026
48b66e1
chore(deps-dev): bump knip from 6.27.0 to 6.29.0 in /cli (#536)
dependabot[bot] Jul 27, 2026
9cf172f
chore(deps-dev): bump jscpd from 5.0.12 to 5.0.14 in /cli (#537)
dependabot[bot] Jul 27, 2026
026452e
chore(deps): bump smol-toml from 1.6.1 to 1.7.1 in /cli (#538)
dependabot[bot] Jul 27, 2026
079dd47
refactor(cli): detect plugin drift through one implementation (#532)
blafourcade Jul 28, 2026
bd05a0d
fix(cli): repair the broken pnpm lockfile (#542)
blafourcade Jul 28, 2026
6a9e84f
feat(aidd-pm): add evidence-bounded spike skill (#543)
blafourcade Jul 28, 2026
830f358
fix: framework headline (#520)
alexsoyes Jul 28, 2026
3de6ed2
refactor(cli): resolve the plugin base dir in one place (#544)
blafourcade Jul 28, 2026
db2243a
test(cli): cover the built-tree update path before refactoring it (#541)
blafourcade Jul 28, 2026
a9abe8b
refactor(cli): share the plugin translator pipeline (#546)
blafourcade Jul 28, 2026
c6dd536
refactor(cli): resolve plugin catalogs through one path (#547)
blafourcade Jul 29, 2026
afd1fa1
refactor(cli): resolve marketplaces through one path (#549)
blafourcade Jul 29, 2026
5c78f4a
test(cli): cover install and restore before generalizing them (#550)
blafourcade Jul 29, 2026
fbc3ff3
feat(aidd-pm): add Product Brief discovery skill (#551)
blafourcade Jul 29, 2026
08613d9
refactor(cli): install content sections through one engine (#552)
blafourcade Jul 29, 2026
ab8a071
refactor(cli): update ai and ide tools through one implementation (#553)
blafourcade Jul 29, 2026
f7eb469
refactor(cli): decide restores in one place (#554)
blafourcade Jul 29, 2026
b703ae3
fix(cli): report restore outcomes it previously hid (#555)
blafourcade Jul 29, 2026
22a450e
fix(cli): stop update and restore materializing marketplace plugins (…
blafourcade Jul 29, 2026
90b602c
fix(cli): remove the build force option that never did anything (#557)
blafourcade Jul 29, 2026
7e0b70a
docs: fix marketplace install steps and note host-wide duplicate comm…
davidmohamedfr Jul 30, 2026
b059dd0
docs(framework): simplify contribution to one issue-first path (#562)
blafourcade Jul 31, 2026
1731b0f
chore(deps-dev): bump lefthook from 1.13.6 to 2.1.10 in /cli (#535)
dependabot[bot] Jul 31, 2026
9168255
chore(deps-dev): bump @commitlint/config-conventional from 19.8.1 to …
dependabot[bot] Jul 31, 2026
2a92a52
docs(framework): fix ROADMAP.md link, dedupe RELEASE.md, drop UPGRADE…
blafourcade Jul 31, 2026
8a8ffe6
chore(deps): bump @inquirer/prompts from 7.10.1 to 8.5.2 in /cli (#540)
dependabot[bot] Jul 31, 2026
c2c2c94
chore: back-merge main into next (conflicts) (#568)
aidd-bot[bot] Jul 31, 2026
07416a8
refactor(aidd-context): citations are links at their point of use
blafourcade Jul 31, 2026
5ee9da9
refactor(plugins): action citations use markdown links, not @
blafourcade Jul 31, 2026
9b1313e
feat(framework): add QA and communication evaluations (#512)
alexsoyes Aug 2, 2026
d9df978
feat(kanban): mount the kanban viewer on aidd as a hidden command (#573)
blafourcade Aug 3, 2026
94c22ef
fix(ci): unbreak the framework-local checks after the kanban move (#574)
blafourcade Aug 3, 2026
95555e2
ci(deps): bump github/codeql-action/autobuild from 4.37.3 to 4.37.4 (…
dependabot[bot] Aug 3, 2026
a045cab
chore(deps): bump react and @types/react in /cli (#576)
dependabot[bot] Aug 3, 2026
f9a0839
chore(deps-dev): bump @biomejs/biome from 2.4.7 to 2.5.6 in /cli (#577)
dependabot[bot] Aug 3, 2026
ea8f03b
chore(deps-dev): bump knip from 6.16.1 to 6.31.0 in /cli (#578)
dependabot[bot] Aug 3, 2026
129e3bf
refactor(orchestrator): replace SDLC actions with protocols (#517)
alexsoyes Aug 6, 2026
fc38ecd
feat(aidd-pm): keep a typed product backlog coherent whoever writes t…
blafourcade Aug 6, 2026
21c718c
fix(ci): promote.yml uses the most recent sync commit as its replay b…
blafourcade Aug 6, 2026
5a3b934
chore: back-merge main into next (conflicts)
blafourcade Aug 7, 2026
f3c75b2
ci(deps): bump github/codeql-action/init from 4.37.4 to 4.37.6 (#580)
dependabot[bot] Aug 7, 2026
c9afde7
ci(deps): bump github/codeql-action/analyze from 4.37.4 to 4.37.6 (#579)
dependabot[bot] Aug 7, 2026
03c2e30
ci: align codeql autobuild pin to v4.37.6 and group actions bumps (#600)
blafourcade Aug 7, 2026
eb24ef6
refactor(framework): one home for the skill contract (#601)
blafourcade Aug 7, 2026
8b8e4d6
fix(orchestrator): backlog decide waits, apply forwards next step (#602)
blafourcade Aug 7, 2026
56bec12
fix(framework): leave a fenced block alone when normalising Process s…
blafourcade Aug 7, 2026
2cba898
chore(deps-dev): bump js-yaml from 5.2.2 to 5.2.3 (#605)
dependabot[bot] Aug 7, 2026
ed2a4c4
chore(deps-dev): bump jscpd from 5.0.7 to 5.0.14 in /cli (#606)
dependabot[bot] Aug 7, 2026
ea9c5c5
chore(deps-dev): bump fast-check from 4.7.0 to 4.9.0 in /cli (#607)
dependabot[bot] Aug 7, 2026
a923d56
chore: back-merge main into next (conflicts) (#610)
aidd-bot[bot] Aug 10, 2026
e599e97
chore(deps-dev): bump knip from 6.31.0 to 6.32.0 in /cli (#613)
dependabot[bot] Aug 10, 2026
72e6a83
chore(deps): bump smol-toml from 1.6.1 to 1.7.1 in /cli (#614)
dependabot[bot] Aug 10, 2026
5ee45c1
chore(deps-dev): bump @types/node from 26.1.2 to 26.2.0 in /cli (#615)
dependabot[bot] Aug 10, 2026
3852a6d
chore(deps-dev): bump @biomejs/biome from 2.5.6 to 2.5.7 in /cli (#616)
dependabot[bot] Aug 10, 2026
e73d872
fix(aidd-context): make 10-learn's recommendation step consistent (#622)
blafourcade Aug 12, 2026
088df33
feat(refine): remove condense skill and its hook (#624)
blafourcade Aug 13, 2026
12b89d6
refactor(aidd-pm): migrate ticket-info, prd, and spec to router contr…
blafourcade Aug 14, 2026
cc71d63
fix(ci): render the star history chart from the repo's own token (#627)
blafourcade Aug 14, 2026
7ec7bbc
fix(cli): transform codex marketplace skills (#571)
davidmohamedfr Aug 14, 2026
4c39e5c
refactor(aidd-context): project memory shows what it would change bef…
blafourcade Aug 14, 2026
af098d9
fix(ci): promote widens its window until it reproduces next (#634)
blafourcade Aug 14, 2026
7d2d390
chore: back-merge main into next (conflicts) (#641)
aidd-bot[bot] Aug 14, 2026
702d01a
chore: back-merge main into next (conflicts) (#644)
aidd-bot[bot] Aug 14, 2026
68b8aa5
chore(deps-dev): bump @commitlint/config-conventional from 21.2.0 to …
dependabot[bot] Aug 17, 2026
7bf99e0
chore(deps-dev): bump js-yaml from 5.2.3 to 5.3.0 (#668)
dependabot[bot] Aug 17, 2026
ac0a5da
chore(deps-dev): bump @biomejs/biome from 2.5.7 to 2.5.8 in /cli (#669)
dependabot[bot] Aug 17, 2026
f2b52c1
chore(deps-dev): bump @commitlint/config-conventional from 21.2.0 to …
dependabot[bot] Aug 17, 2026
82f7573
chore(deps-dev): bump jscpd from 5.0.14 to 5.0.15 in /cli (#671)
dependabot[bot] Aug 17, 2026
574edb4
chore(deps-dev): bump knip from 6.32.0 to 6.32.2 in /cli (#673)
dependabot[bot] Aug 17, 2026
8432a2a
chore(deps): bump smol-toml from 1.7.1 to 1.8.0 in /cli (#674)
dependabot[bot] Aug 17, 2026
e4456de
ci(deps): bump the github-actions group with 3 updates (#675)
dependabot[bot] Aug 17, 2026
a36fcf9
chore(deps-dev): bump @commitlint/cli from 21.2.1 to 21.2.2 (#667)
dependabot[bot] Aug 17, 2026
16b8041
chore(deps-dev): bump @commitlint/cli from 21.2.1 to 21.2.2 in /cli (…
dependabot[bot] Aug 17, 2026
971c980
feat(aidd-context): add retrospective lens to learn (#712)
alexsoyes Aug 24, 2026
7c3e03f
docs(framework): position README for enterprise teams (#664)
alexsoyes Aug 24, 2026
df4195d
chore(deps): bump @inquirer/prompts from 8.5.2 to 8.6.0 in /cli (#713)
dependabot[bot] Aug 24, 2026
f6e7bad
chore(deps-dev): bump jscpd from 5.0.15 to 5.0.16 in /cli (#714)
dependabot[bot] Aug 24, 2026
5144c47
chore(deps-dev): bump @biomejs/biome from 2.5.8 to 2.5.10 in /cli (#715)
dependabot[bot] Aug 24, 2026
a600806
ci(deps): bump the github-actions group with 3 updates (#716)
dependabot[bot] Aug 24, 2026
765d6a2
docs(readme): clarify the feature delivery flow (#717)
alexsoyes Aug 27, 2026
d9dfed9
fix(aidd-context): load the project memory block in Claude Code (#721)
blafourcade Aug 28, 2026
25ea44c
ci: back-merge main into next
blafourcade Aug 28, 2026
158843d
ci: back-merge main into next (#725)
blafourcade Aug 29, 2026
7bf1ece
ci: promote and back-merge through merge commits (#726)
blafourcade Aug 29, 2026
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
58 changes: 31 additions & 27 deletions .github/workflows/back-merge.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
name: Back-merge

# After a release on main, sync main back into next so its changelog, manifest,
# and version bumps do not drift. When next holds no unreleased work, realign it
# onto main's hashes (force-push, the App is an "always" bypass actor on next)
# so the rebase-promote hash drift cannot accumulate. When next has real work,
# keep the merge. Conflict -> open a PR for a human. Any other failure -> open a
# tracking issue so the drift is never silent.
# After a release on main, fold main's changelog, manifest and version bumps
# back into next so the two branches do not drift.
#
# This stays a plain merge because promote.yml lands as a merge commit: main and
# next share a real merge base, so the only thing left to reconcile here is the
# release commit itself. Conflict -> open a PR for a human. Any other failure ->
# open a tracking issue so the drift is never silent.
#
# A release can publish several tags at once (umbrella, plugins, cli), firing
# this workflow once per tag. The first run folds main in; the rest find main
# already merged and exit, instead of each opening its own duplicate PR.

on:
release:
Expand Down Expand Up @@ -37,35 +42,34 @@ jobs:
- name: Merge main into next
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
REPO: ${{ github.repository }}
RUN_ID: ${{ github.run_id }}
run: |
set -euo pipefail

git config user.name "aidd-bot[bot]"
git config user.email "aidd-bot[bot]@users.noreply.github.com"
git fetch origin main
if git merge --no-edit origin/main; then
# Promotes to main are rebase-merged, so main carries new commit
# hashes and next drifts even though the content matches. When the
# merge result is content-identical to main (next held no unreleased
# work), realign next onto main's clean hashes so the drift cannot
# accumulate into a giant conflicting promote later. Otherwise next
# has real unreleased work, so keep the merge and push normally.
if git diff --quiet origin/main HEAD; then
git push --force origin "origin/main:refs/heads/next"
else
git push origin next
fi

if git merge-base --is-ancestor origin/main HEAD; then
echo "next already contains main; nothing to back-merge."
exit 0
fi

# `ci:` on purpose. The type is absent from release-please's
# changelog-sections, so this plumbing commit never surfaces in a
# released changelog, while commitlint still accepts it as the tip of
# a later promote PR.
if git merge --no-ff -m "ci: back-merge main into next" origin/main; then
git push origin next
else
git merge --abort
BRANCH="back-merge/main-to-next-${{ github.run_id }}"
BRANCH="back-merge/main-to-next-${RUN_ID}"
git checkout -b "$BRANCH" origin/main
git push origin "$BRANCH"
# promote.yml matches this exact title prefix to recognize a
# squash-merged conflict resolution as a sync boundary (this repo
# disallows merge-commit PRs, so this can't land as a real merge
# commit). Keep both in sync if you change it.
gh pr create --base next --head "$BRANCH" \
--title "chore: back-merge main into next (conflicts)" \
--body "Automated back-merge hit conflicts (CHANGELOG / manifest / version files). Resolve manually, then merge into next." \
--repo "${{ github.repository }}"
gh pr create --repo "$REPO" --base next --head "$BRANCH" \
--title "ci: back-merge main into next (conflicts)" \
--body "Automated back-merge hit conflicts. Resolve manually, then **merge with a merge commit, not a squash**: the second parent is what keeps a shared merge base between \`main\` and \`next\`, and a squash puts the next back-merge back into conflict."
fi

# Never let a back-merge fail silently: a rejected push or any other error
Expand Down
165 changes: 51 additions & 114 deletions .github/workflows/promote.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
name: Promote next to main

# One intuitive button to ship `next` to `main` the RIGHT way: a rebase-merge
# that preserves every conventional commit, so commitlint passes per commit and
# release-please reads the scopes to bump each plugin. A squash here collapses
# the commits into one (often non-conventional) message and breaks both, which
# is exactly the failure this workflow prevents.
# One intuitive button to ship `next` to `main` the RIGHT way: a merge commit.
#
# Run it from the Actions tab (Run workflow). It creates a linear promotion
# branch from main, opens a PR to main, and enables rebase auto-merge. CI gates
# it, then it merges itself.
# The merge commit does two jobs. It carries every conventional commit onto
# `main` untouched, so commitlint passes and release-please reads each scope to
# bump the right plugin. And its two parents give `main` and `next` a real merge
# base, which is the only thing that keeps the back-merge after each release
# conflict-free.
#
# Neither alternative does the second job. A squash collapses the batch into one
# subject and hides the scopes. A rebase recopies the commits under new hashes,
# so git never learns the branches were reconciled: the recorded merge base goes
# stale, and every later back-merge conflicts on the release metadata that
# release-please rewrites each time. That is the failure this workflow prevents.
#
# Run it from the Actions tab (Run workflow). It snapshots `next`, opens a PR to
# main, and enables merge auto-merge. CI gates it, then it merges itself.

on:
workflow_dispatch:
Expand All @@ -21,7 +28,7 @@ permissions: {}

jobs:
promote:
name: Open and rebase-merge next into main
name: Open and merge next into main
runs-on: ubuntu-latest
steps:
- uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
Expand All @@ -35,124 +42,54 @@ jobs:
fetch-depth: 0
token: ${{ steps.app-token.outputs.token }}

- name: Open or reuse the promote PR, enable rebase auto-merge
- name: Open the promote PR, enable merge auto-merge
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
REPO: ${{ github.repository }}
RUN_ID: ${{ github.run_id }}
run: |
set -euo pipefail

git config user.name "aidd-bot[bot]"
git config user.email "aidd-bot[bot]@users.noreply.github.com"
git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${REPO}.git"
BRANCH="promote/next-to-main-linear"
git fetch origin main next --prune
git fetch origin "+refs/heads/${BRANCH}:refs/remotes/origin/${BRANCH}" 2>/dev/null || true

if [ "$(git rev-parse origin/main^{tree})" = "$(git rev-parse origin/next^{tree})" ]; then
echo "No commits to promote."
# Compare trees, not ancestry. After a back-merge, next's tip is a
# merge commit main does not carry, so next is never an ancestor of
# main even when the two hold identical content. Going by ancestry
# would open an empty promote PR, and merging it is a push to main,
# which runs release-please for nothing.
if [ "$(git rev-parse origin/next^{tree})" = "$(git rev-parse origin/main^{tree})" ]; then
echo "next and main already hold the same tree; nothing to promote."
exit 0
fi

# A "sync commit" marks a point where main's history was folded into
# next by back-merge.yml, so the sync itself carries main's own
# content and must NOT be replayed onto main. back-merge.yml lands a
# sync two ways:
# - no conflicts: a direct-pushed real merge commit (2 parents)
# - conflicts: a PR merged by squash (this repo disallows
# merge-commit PRs), whose subject is stamped verbatim from
# back-merge.yml's fixed PR title
# Both must be recognised, or a squash sync gets mistaken for genuine
# next-only work and cherry-picked back onto main, colliding with
# content main already has. Keep the title prefix below in sync with
# back-merge.yml's `gh pr create --title`.
is_sync_commit() {
git rev-parse --verify -q "$1^2" >/dev/null 2>&1 && return 0
local subject
subject="$(git log -1 --format=%s "$1")"
[[ "$subject" == "chore: back-merge main into next"* ]]
}

mapfile -t CANDIDATES < <(git rev-list --first-parent --reverse origin/main..origin/next)
SYNCS=()
for c in "${CANDIDATES[@]}"; do
is_sync_commit "$c" && SYNCS+=("$c")
done

# Commits main already carries. A promote is a rebase-merge, so main's
# copy of a promoted commit keeps the same patch under a new hash, and
# `git cherry` marks it "-". Skipping those is what stops an
# already-promoted commit from being replayed as an empty cherry-pick
# (which would halt the script, since it doesn't pass --allow-empty)
# or as a spurious conflict with a later bump to the same lockfile.
declare -A ALREADY=()
while read -r sign sha; do
[ "$sign" = "-" ] && ALREADY["$sha"]=1
done < <(git cherry origin/main origin/next)

# Start the window at the most recent sync and widen to older syncs
# until the replayed branch reproduces origin/next exactly. The
# narrowest window is right only when every commit before the last
# back-merge was already promoted. It is wrong when work landed on
# next after the previous promote but before that back-merge: a
# back-merge only folds main into next, it never carries next's own
# commits to main, so those commits are still owed and a sync-anchored
# window silently drops them forever. Widening recovers them, and the
# tree comparison against origin/next is the acceptance test.
PICKED=()
MATCHED=""
for ((i = ${#SYNCS[@]} - 1; i >= -1; i--)); do
if [ "$i" -ge 0 ]; then
START="${SYNCS[$i]}"
else
START="origin/main"
fi

COMMITS=()
while IFS= read -r c; do
is_sync_commit "$c" && continue
[ -n "${ALREADY[$c]:-}" ] && continue
COMMITS+=("$c")
done < <(git rev-list --first-parent --reverse "${START}..origin/next")

if [ "${#COMMITS[@]}" -eq 0 ]; then
continue
fi

git cherry-pick --abort >/dev/null 2>&1 || true
git switch -C "$BRANCH" origin/main
if ! git cherry-pick "${COMMITS[@]}"; then
git cherry-pick --abort >/dev/null 2>&1 || true
echo "Window from ${START} conflicts on replay; widening."
continue
fi
if [ "$(git rev-parse HEAD^{tree})" = "$(git rev-parse origin/next^{tree})" ]; then
PICKED=("${COMMITS[@]}")
MATCHED=1
break
fi
echo "Window from ${START} does not reproduce origin/next; widening."
git diff --stat HEAD origin/next
done
# Promote a snapshot of next, never next itself. A PR headed by the
# live branch silently widens as work merges into next during review,
# so what ships stops matching what was approved. The snapshot points
# at next's exact tip, so merging it still makes that commit an
# ancestor of main and still refreshes the merge base.
BRANCH="promote/next-to-main-${RUN_ID}"
git push origin "origin/next:refs/heads/${BRANCH}"

PR=$(gh pr create --repo "$REPO" --base main --head "$BRANCH" \
--title "ci: promote next to main" \
--body "Automated promotion of \`next\` to \`main\`, from a snapshot of next taken at run ${RUN_ID}. Merged as a **merge commit** so \`main\` keeps every conventional commit for release-please, and so both branches keep a shared merge base for the back-merge. Do not squash, do not rebase." \
| grep -oE '[0-9]+$')
echo "Promote PR: #$PR"

if [ -z "$MATCHED" ]; then
echo "No window of next-only commits reproduces origin/next."
echo "main and next have diverged beyond a linear replay; back-merge main into next first."
# `ci:` on purpose. The type is absent from release-please's
# changelog-sections, so this plumbing commit never surfaces in a
# released changelog, while commitlint still accepts it.
gh pr merge "$PR" --repo "$REPO" --merge --auto --delete-branch \
--subject "ci: promote next to main (#${PR})"

# Without a recorded subject GitHub generates "Merge pull request #N
# from ...", which is not conventional. Commitlint lints main's tip on
# push, so fail here rather than on main.
gh pr view "$PR" --repo "$REPO" --json autoMergeRequest \
--jq '.autoMergeRequest.commitHeadline // empty' | grep -q . || {
echo "Auto-merge did not record the commit subject; refusing to leave main's tip to chance."
exit 1
fi
echo "Promoting ${#PICKED[@]} commits."

git push --force-with-lease origin "HEAD:refs/heads/${BRANCH}"

# Reuse an open linear promote PR if one exists, else open one with a
# conventional, squash-safe title.
PR=$(gh pr list --repo "$REPO" --base main --head "$BRANCH" --state open --json number --jq '.[0].number' || true)
if [ -z "$PR" ]; then
PR=$(gh pr create --repo "$REPO" --base main --head "$BRANCH" \
--title "chore: promote next to main" \
--body "Automated linear promotion of \`next\` to \`main\`. Merged by **rebase** to preserve conventional commits for release-please. Do not squash." \
| grep -oE '[0-9]+$')
fi
echo "Promote PR: #$PR"
# Rebase auto-merge: GitHub merges it once CI is green. Never squash.
gh pr merge "$PR" --repo "$REPO" --rebase --auto
}
11 changes: 6 additions & 5 deletions docs/MAINTAINERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,12 @@ Versions live in `.release-please-manifest.json`. Forcing a version / pre-releas

## 🔄 Promotion & recovery

The weekly `next` → `main` promotion **must rebase, never squash**:
The weekly `next` → `main` promotion **must be a merge commit, never a squash and never a rebase**:

- A squash collapses the batch's conventional commits into one subject from the PR title. If that title isn't a valid conventional type, `Commitlint` fails on `main` and **release-please is skipped** — no release.
- release-please also reads each commit's type/scope to bump the right package, which a squash hides.
- Use the **Promote next to main** workflow (it rebase-merges); merging by hand, pick **Rebase and merge**.
- A squash collapses the batch's conventional commits into one subject from the PR title. If that title isn't a valid conventional type, `Commitlint` fails on `main` and **release-please is skipped** — no release. release-please also reads each commit's type/scope to bump the right package, which a squash hides.
- A rebase keeps the commits but recopies them under new hashes, so git never records that the branches were reconciled. The merge base between `main` and `next` then goes stale, and every later back-merge conflicts on the release metadata release-please rewrites each time — a conflict with no real content behind it.
- A merge commit does both jobs: the commits land verbatim, and its second parent keeps a shared merge base so the back-merge stays clean.
- Use the **Promote next to main** workflow (it merges); merging by hand, pick **Create a merge commit** and give it a conventional subject.
- The Release PR release-please opens is its own single commit and is fine to squash.

**Recovery** — a bad squashed promote turns `main` red on `Commitlint` and skips **Release Please**. An admin:
Expand All @@ -105,7 +106,7 @@ The App: ID in secret `AIDD_BOT_APP_ID`, key in `AIDD_BOT_PRIVATE_KEY`. If the A

Head branches are **not** auto-deleted on merge (`delete_branch_on_merge: false`):

- The promote PR merges `next` into `main` without deleting `next`, so the back-merge that realigns `next` never hits a missing branch. **Do not re-enable** the setting.
- The promote PR is headed by a disposable `promote/next-to-main-<run_id>` snapshot of `next`, which the workflow deletes on merge. `next` itself is never a head branch, so the back-merge never hits a missing branch.
- The back-merge runs unattended (bot App `always` bypass on the `next` ruleset). If it can't push, it opens a tracking issue — resync with a `main` → `next` PR.
- If `next` is ever missing, recreate it: `git push origin main:next`.

Expand Down
Loading