diff --git a/next-branch-files.tar.gz b/next-branch-files.tar.gz deleted file mode 100644 index 6d4a90728..000000000 Binary files a/next-branch-files.tar.gz and /dev/null differ diff --git a/rollout-next-phase1.sh b/rollout-next-phase1.sh deleted file mode 100755 index 338ca4585..000000000 --- a/rollout-next-phase1.sh +++ /dev/null @@ -1,546 +0,0 @@ -#!/usr/bin/env zsh -# rollout-next-phase1.sh — native zsh, runs under macOS Terminal's default shell. -# DO NOT prefix with `bash`. Run as: ./rollout-next-phase1.sh -# -# Phase 1 of the `next` integration-branch rollout. -# -# What it does (in order, on YOUR Mac, in your repo dir): -# 1. Stashes any uncommitted work on your current branch (codex/...) with a -# named stash you can recover with `git stash list` + `git stash pop`. -# 2. Switches to main, pulls --ff-only to be sure you're current. -# 3. Creates branch: chore/introduce-next-integration-branch -# 4. Unpacks the 5 new files from the tarball. -# 5. Applies the 2 edits (branch-naming.yml, CONTRIBUTING.md) programmatically -# against the clean origin/main versions — no chance of pulling in -# unrelated commits from your current codex/ branch. -# 6. Sanity-checks YAML + bash syntax. -# 7. Commits with a conventional-commit message. -# 8. Pushes the branch to origin. -# 9. Files a `type: chore` issue using gh. -# 10. Renames the ADR file XXXX-...md → -...md and replaces XXXX in -# the ADR body with the real issue number. -# 11. Amends the commit, force-pushes-with-lease. -# 12. Opens the PR against main with `Closes #` in the body. -# -# Idempotent: re-running after a failure converges. Each step checks if it's -# already done. -# -# Usage: -# cd /Volumes/Mini\ Me/Users/trekkie/projects/gsd-core -# bash /path/to/rollout-next-phase1.sh -# -# Env overrides: -# TARBALL=/path/to/next-branch-files.tar.gz (default: ./next-branch-files.tar.gz) -# REPO=open-gsd/gsd-core (default: that) -# DRY_RUN=1 (skip push, issue, PR) - -set -euo pipefail - -# ─────────────────────────────────────────────────────────── -# Config -# ─────────────────────────────────────────────────────────── -REPO="${REPO:-open-gsd/gsd-core}" -TARBALL="${TARBALL:-./next-branch-files.tar.gz}" -BRANCH="chore/introduce-next-integration-branch" -DRY_RUN="${DRY_RUN:-0}" - -# Colors for readability (no-op if not a tty) -if [ -t 1 ]; then - C_BOLD=$'\033[1m'; C_GRN=$'\033[32m'; C_YEL=$'\033[33m'; C_RED=$'\033[31m'; C_DIM=$'\033[2m'; C_RST=$'\033[0m' -else - C_BOLD=''; C_GRN=''; C_YEL=''; C_RED=''; C_DIM=''; C_RST='' -fi - -step() { echo; echo "${C_BOLD}▸ $*${C_RST}"; } -ok() { echo "${C_GRN} ✓${C_RST} $*"; } -warn() { echo "${C_YEL} ⚠${C_RST} $*"; } -die() { echo "${C_RED} ✗ $*${C_RST}" >&2; exit 1; } -note() { echo "${C_DIM} $*${C_RST}"; } - -# ─────────────────────────────────────────────────────────── -# Step 0: Sanity checks -# ─────────────────────────────────────────────────────────── -step "Sanity checks" - -[ -d .git ] || die "Not in a git repo. cd to your gsd-core checkout first." -command -v gh >/dev/null || die "gh CLI not found. Install from https://cli.github.com/" -command -v jq >/dev/null || die "jq not found. Install: brew install jq" -gh auth status >/dev/null 2>&1 || die "gh not authenticated. Run: gh auth login" -[ -f "$TARBALL" ] || die "Tarball not found at: $TARBALL (override with TARBALL=/path/to/next-branch-files.tar.gz)" - -# Verify we're pointed at the right remote. -REMOTE_URL=$(git remote get-url origin 2>/dev/null || true) -case "$REMOTE_URL" in - *"$REPO"*) ok "Remote: $REMOTE_URL" ;; - *) die "Remote 'origin' is $REMOTE_URL — expected to contain $REPO. Wrong checkout?" ;; -esac - -# Verify tarball contents look right (fail loudly if user grabbed the wrong tar). -EXPECTED_PATHS=( - "docs/branching.md" - "docs/adr/XXXX-introduce-next-integration-branch.md" - ".github/workflows/auto-backmerge.yml" - ".github/workflows/pr-target-validator.yml" - "scripts/setup-branch-protection.sh" -) -TARLIST=$(tar tzf "$TARBALL") -for p in "${EXPECTED_PATHS[@]}"; do - echo "$TARLIST" | grep -qx "$p" || die "Tarball missing expected file: $p" -done -ok "Tarball contains all 5 expected files" - -# ─────────────────────────────────────────────────────────── -# Step 1: Stash any uncommitted work (protecting rollout files from the sweep) -# ─────────────────────────────────────────────────────────── -step "Stash any uncommitted work on current branch" - -CURRENT_BR=$(git rev-parse --abbrev-ref HEAD) -note "Currently on: $CURRENT_BR" - -# git stash --include-untracked would otherwise grab the rollout script and -# tarball (they're untracked). Move them aside, stash, move them back. -# trap EXIT guarantees restore even on script failure. -ROLLOUT_STAGE=$(mktemp -d) -ROLLOUT_FILES=(rollout-next-phase1.sh rollout-next-phase2.sh next-branch-files.tar.gz) -for f in "${ROLLOUT_FILES[@]}"; do - [ -f "./$f" ] && mv "./$f" "$ROLLOUT_STAGE/" -done -restore_rollout_files() { - # (N) is zsh's nullglob qualifier — empty match expands to nothing - # instead of erroring with "no matches found". - for f in "$ROLLOUT_STAGE"/*(N); do - cp "$f" ./ 2>/dev/null || true - done - rm -rf "$ROLLOUT_STAGE" 2>/dev/null || true -} -trap restore_rollout_files EXIT - -if [ -n "$(git status --porcelain)" ]; then - STASH_MSG="pre-next-rollout-$(date +%Y%m%d-%H%M%S) (from $CURRENT_BR)" - git stash push --include-untracked --message "$STASH_MSG" >/dev/null - ok "Stashed as: $STASH_MSG" - note "Recover later with: git stash list then git stash pop " -else - ok "Working tree clean — nothing to stash" -fi - -# Bring rollout files back into the working tree NOW so subsequent retries can find them. -restore_rollout_files - -# ─────────────────────────────────────────────────────────── -# Step 2: Switch to main, pull -# ─────────────────────────────────────────────────────────── -step "Switch to main and pull" - -git fetch origin --quiet -git checkout main >/dev/null 2>&1 || die "Could not checkout main" -git pull --ff-only origin main >/dev/null -ok "main is current at $(git log -1 --format='%h %s' | head -c 80)" - -# ─────────────────────────────────────────────────────────── -# Step 3: Create or switch to rollout branch -# ─────────────────────────────────────────────────────────── -step "Create branch $BRANCH" - -# Prune any stale worktree registrations first (e.g. from a prior aborted -# attempt that left .git/worktrees/ pointing at a deleted directory). -git worktree prune 2>/dev/null || true - -if git show-ref --verify --quiet "refs/heads/$BRANCH"; then - # Detect if a worktree still claims this branch. - # NB: no `exit` in awk — that would SIGPIPE git and trip pipefail/set -e - # silently. `head -1 || true` neutralizes the SIGPIPE we inflict ourselves. - CLAIMING_WT=$( { git worktree list --porcelain 2>/dev/null || true; } | awk -v br="refs/heads/$BRANCH" ' - /^worktree / { wt=$2 } - $0 == "branch " br { print wt } - ' | head -1 || true) - if [ -n "$CLAIMING_WT" ] && [ "$CLAIMING_WT" != "$(pwd)" ]; then - die "Branch $BRANCH is held by worktree $CLAIMING_WT. Run: git worktree remove --force '$CLAIMING_WT' (or) git worktree prune then re-run." - fi - warn "Branch $BRANCH already exists locally. Switching to it." - git checkout "$BRANCH" >/dev/null - if git rev-parse --verify "origin/$BRANCH" >/dev/null 2>&1; then - warn "origin/$BRANCH already exists. Resetting to main would lose remote commits — ABORT." - die "Delete the remote branch first if you want a clean restart: gh api -X DELETE /repos/$REPO/git/refs/heads/$BRANCH" - fi - git reset --hard main >/dev/null - ok "Reset local $BRANCH to current main" -else - git checkout -b "$BRANCH" >/dev/null - ok "Created and switched to $BRANCH" -fi - -# ─────────────────────────────────────────────────────────── -# Step 4: Unpack the 5 new files -# ─────────────────────────────────────────────────────────── -step "Unpack new files from tarball" - -tar xzf "$TARBALL" -chmod +x scripts/setup-branch-protection.sh -ok "Unpacked: $(tar tzf "$TARBALL" | wc -l | tr -d ' ') files" - -# ─────────────────────────────────────────────────────────── -# Step 5: Apply the 2 edits programmatically -# ─────────────────────────────────────────────────────────── -step "Apply edit 1/2 — add 'next' to branch-naming.yml alwaysValid" - -NAMING=".github/workflows/branch-naming.yml" -if grep -q "alwaysValid = \['main', 'next', 'develop'\]" "$NAMING"; then - ok "branch-naming.yml already has 'next' in alwaysValid (idempotent skip)" -else - if ! grep -q "alwaysValid = \['main', 'develop'\]" "$NAMING"; then - die "Could not find the expected anchor in $NAMING. Maybe upstream changed it. Open the file and add 'next' manually." - fi - # BSD sed (macOS default) needs -i ''; GNU sed needs -i. Use -i.bak then remove. - sed -i.rollout-bak "s/alwaysValid = \['main', 'develop'\]/alwaysValid = ['main', 'next', 'develop']/" "$NAMING" - rm -f "$NAMING.rollout-bak" - grep -q "alwaysValid = \['main', 'next', 'develop'\]" "$NAMING" || die "sed didn't take. Aborting." - ok "branch-naming.yml updated" -fi - -step "Apply edit 2/2 — insert 'Where Do I Open My PR?' section into CONTRIBUTING.md" - -CONTRIB="CONTRIBUTING.md" -if grep -q "^## Where Do I Open My PR?" "$CONTRIB"; then - ok "CONTRIBUTING.md already has the section (idempotent skip)" -else - # Anchor must exist exactly once. - ANCHOR_COUNT=$(grep -c "^## Pull Request Guidelines\$" "$CONTRIB" || true) - [ "$ANCHOR_COUNT" -eq 1 ] || die "Expected exactly 1 '## Pull Request Guidelines' anchor in $CONTRIB, found $ANCHOR_COUNT. Insert the section manually." - - # Write section to a temp file. BSD awk (macOS default) rejects newlines in - # -v variable values, so we pass a filename instead and let awk read it. - SECTION_FILE=$(mktemp -t gsd-rollout-section.XXXXXX) - cat > "$SECTION_FILE" <<'EOF' -## Where Do I Open My PR? (Branching Model) - -GSD uses two long-lived branches: `main` (production, what's on npm `@latest`) -and `next` (integration for the upcoming release). **Almost every PR targets -`next`.** Full guide: [`docs/branching.md`](docs/branching.md). - -| Your branch | PR target | Notes | -|---|---|---| -| `feat/NNN-slug` | `next` | Default for all new features | -| `fix/NNN-slug` | `next` | Default for all bug fixes; ships in next minor or via hotfix cherry-pick | -| `chore/`, `docs/`, `refactor/`, `test/`, `perf/`, `ci/`, `revert/` | `next` | All routine work | -| `fix/critical-NNN-slug` | `main` | Production-down emergencies only; auto-back-merges to `next` | -| `release/X.Y.0` | `main` | Created by `release.yml` — don't make these by hand | -| `hotfix/X.Y.Z` | `main` | Created by `hotfix.yml` — don't make these by hand | -| Stabilization PR for an in-flight release | `release/X.Y.0` | Fix a regression found during the RC cycle | - -**Day-to-day commands:** - -```bash -git fetch origin -git checkout next -git pull --ff-only origin next -git checkout -b fix/3187-config-corruption -# ... commit, push -gh pr create --base next --repo open-gsd/gsd-core -``` - -If you target the wrong branch by accident, the `PR Target Validator` -workflow will post a comment with the one-line fix (click "Edit" by the PR -title and change the base branch — no need to recreate the PR). - -**Why this matters:** Under the old single-branch model, every PR required -rebasing onto `main` because branch protection required "up-to-date before -merging" and `main` moved on every merge. With `next` as the integration -branch and that flag disabled on `next`, concurrent PRs can merge in any -order as long as they don't conflict on the same lines. The rebase -treadmill is gone for the 95% case. - ---- -EOF - # Trailing blank line so awk emits a blank between our closing rule and - # the next H2 (CommonMark requires blank before any header). - echo "" >> "$SECTION_FILE" - - awk -v sectionfile="$SECTION_FILE" ' - /^## Pull Request Guidelines$/ && !inserted { - while ((getline line < sectionfile) > 0) print line - close(sectionfile) - inserted = 1 - } - { print } - ' "$CONTRIB" > "$CONTRIB.tmp" - mv "$CONTRIB.tmp" "$CONTRIB" - rm -f "$SECTION_FILE" - grep -q "^## Where Do I Open My PR?" "$CONTRIB" || die "awk insertion failed." - ok "CONTRIBUTING.md section inserted" -fi - -# ─────────────────────────────────────────────────────────── -# Step 6: Sanity-check YAML + bash -# ─────────────────────────────────────────────────────────── -step "Validate YAML + bash" - -for f in .github/workflows/auto-backmerge.yml \ - .github/workflows/pr-target-validator.yml \ - .github/workflows/branch-naming.yml; do - python3 -c "import yaml,sys; yaml.safe_load(open('$f'))" \ - || die "YAML invalid: $f" -done -ok "All 3 workflow YAMLs parse" - -bash -n scripts/setup-branch-protection.sh || die "Bash syntax error: setup-branch-protection.sh" -ok "setup-branch-protection.sh syntax OK" - -# ─────────────────────────────────────────────────────────── -# Step 7: Commit -# ─────────────────────────────────────────────────────────── -step "Commit" - -git add docs/branching.md \ - docs/adr/XXXX-introduce-next-integration-branch.md \ - .github/workflows/auto-backmerge.yml \ - .github/workflows/pr-target-validator.yml \ - .github/workflows/branch-naming.yml \ - CONTRIBUTING.md \ - scripts/setup-branch-protection.sh - -if git diff --cached --quiet; then - warn "Nothing to commit (already committed from a previous run?)" -else - git commit -m "chore: introduce \`next\` integration branch (Phase 1 — additive) - -Adds: - - docs/branching.md — beginner contributor guide - - docs/adr/XXXX-...md — ADR (will be renamed with issue#) - - .github/workflows/auto-backmerge.yml — disabled in Phase 1 - - .github/workflows/pr-target-validator.yml — warn-only in Phase 1 - - scripts/setup-branch-protection.sh — idempotent gh api script - -Modifies: - - .github/workflows/branch-naming.yml — recognize 'next' - - CONTRIBUTING.md — 'Where Do I Open My PR?' section - -Phase 1 is additive: nothing operational changes until Phase 2 flips -auto-backmerge.yml's if:false→true, flips pr-target-validator.yml's -WARN_ONLY→false, creates the next branch, and switches the default -branch. See the ADR for the migration plan." - ok "Committed" -fi - -# ─────────────────────────────────────────────────────────── -# Step 8: Push -# ─────────────────────────────────────────────────────────── -step "Push branch" - -if [ "$DRY_RUN" = "1" ]; then - warn "DRY_RUN=1 — skipping push" -else - git push -u origin "$BRANCH" - ok "Pushed $BRANCH" -fi - -# ─────────────────────────────────────────────────────────── -# Step 9: File the issue (or reuse if one already exists) -# ─────────────────────────────────────────────────────────── -step "File or reuse the chore issue" - -ISSUE_TITLE="chore: introduce \`next\` integration branch to end rebase treadmill" -ISSUE_BODY=$(cat <<'EOF' -## Problem - -Every contributor branch is cut from `main` and PR'd back to `main`. Combined -with branch protection's "Require branches to be up to date before merging", -this produces a rebase treadmill: every time another PR merges, every -in-flight PR demands a rebase. With ~315 unreleased changesets queued and -multiple PRs at any time, this is constant. - -## Proposed change - -Introduce `next` as a long-lived integration branch. Routine PRs target -`next`; `main` only changes on release / hotfix / emergency fix. An -auto-back-merge workflow keeps `next` aligned with `main`. - -Full design: see the ADR in this PR (`docs/adr/XXXX-introduce-next-integration-branch.md`). -Contributor-facing guide: `docs/branching.md`. - -## Scope - -This issue covers Phase 1 of the rollout (additive infrastructure — no -operational change until the workflow flags are flipped in Phase 2). - -- New: `docs/branching.md`, ADR, `auto-backmerge.yml` (disabled), - `pr-target-validator.yml` (warn-only), `setup-branch-protection.sh`. -- Modified: `branch-naming.yml` (recognize `next`), `CONTRIBUTING.md` - ("Where Do I Open My PR?" section). -- Not yet touched: `release.yml`, `hotfix.yml`, `auto-branch.yml` — these - are Phase 3, with patches inlined in the ADR. - -## Acceptance criteria - -- All 7 files land via this PR. -- CI is green. -- `docs/branching.md` renders correctly on GitHub. -- ADR filename is renamed from `XXXX-` to `-`. - -## Phase 2 follow-up (separate PR) - -After Phase 1 merges, a small follow-up PR will: -- Create the `next` branch from `main` HEAD. -- Apply branch protection via the new script. -- Switch the default branch to `next`. -- Flip `if: false` → `if: true` in `auto-backmerge.yml`. -- Flip `WARN_ONLY: 'true'` → `'false'` in `pr-target-validator.yml`. -EOF -) - -# Look for an existing open issue with this exact title to support idempotent re-runs. -EXISTING_ISSUE=$(gh issue list --repo "$REPO" --search "in:title \"$ISSUE_TITLE\"" --state open --json number --jq '.[0].number' 2>/dev/null || echo "") - -if [ -n "$EXISTING_ISSUE" ]; then - ISSUE_NUM="$EXISTING_ISSUE" - ok "Reusing existing open issue #$ISSUE_NUM" -elif [ "$DRY_RUN" = "1" ]; then - ISSUE_NUM="DRYRUN" - warn "DRY_RUN=1 — skipping issue creation; ISSUE_NUM=$ISSUE_NUM" -else - ISSUE_URL=$(echo "$ISSUE_BODY" | gh issue create \ - --repo "$REPO" \ - --title "$ISSUE_TITLE" \ - --label "type: chore" \ - --body-file -) - ISSUE_NUM=$(echo "$ISSUE_URL" | sed 's|.*/||') - ok "Filed issue #$ISSUE_NUM — $ISSUE_URL" -fi - -# ─────────────────────────────────────────────────────────── -# Step 10: Rename ADR with issue number, replace XXXX in body -# ─────────────────────────────────────────────────────────── -step "Rename ADR and substitute issue number" - -OLD_ADR="docs/adr/XXXX-introduce-next-integration-branch.md" -NEW_ADR="docs/adr/${ISSUE_NUM}-introduce-next-integration-branch.md" - -if [ -f "$OLD_ADR" ]; then - git mv "$OLD_ADR" "$NEW_ADR" - ok "Renamed: $OLD_ADR → $NEW_ADR" -elif [ -f "$NEW_ADR" ]; then - ok "ADR already renamed (idempotent skip)" -else - die "Neither $OLD_ADR nor $NEW_ADR exists. Something is off." -fi - -# Replace XXXX inside ADR with real issue number (only in the placeholder context). -if [ "$ISSUE_NUM" != "DRYRUN" ]; then - sed -i.rollout-bak "s/XXXX-introduce-next-integration-branch/${ISSUE_NUM}-introduce-next-integration-branch/g" "$NEW_ADR" - sed -i.rollout-bak "s/placeholder \`XXXX\` prefix/placeholder (now resolved to \`${ISSUE_NUM}\`)/g" "$NEW_ADR" - rm -f "$NEW_ADR.rollout-bak" - ok "Substituted XXXX → ${ISSUE_NUM} in ADR body" -fi - -# Also patch the references in the new workflow files which mention XXXX-introduce-next-integration-branch. -for f in .github/workflows/auto-backmerge.yml .github/workflows/pr-target-validator.yml docs/branching.md CONTRIBUTING.md; do - if [ -f "$f" ] && grep -q "XXXX-introduce-next-integration-branch" "$f"; then - sed -i.rollout-bak "s/XXXX-introduce-next-integration-branch/${ISSUE_NUM}-introduce-next-integration-branch/g" "$f" - rm -f "$f.rollout-bak" - ok "Updated cross-reference in $f" - fi -done - -# ─────────────────────────────────────────────────────────── -# Step 11: Amend commit + force-push -# ─────────────────────────────────────────────────────────── -step "Amend commit and force-push" - -git add -A -if git diff --cached --quiet; then - ok "No changes to amend (idempotent skip)" -else - git commit --amend --no-edit - ok "Amended commit" -fi - -if [ "$DRY_RUN" = "1" ]; then - warn "DRY_RUN=1 — skipping force-push" -else - git push --force-with-lease origin "$BRANCH" - ok "Force-pushed (with lease)" -fi - -# ─────────────────────────────────────────────────────────── -# Step 12: Open the PR (or reuse if one already exists) -# ─────────────────────────────────────────────────────────── -step "Open PR against main" - -PR_TITLE="chore: introduce \`next\` integration branch (Phase 1 — additive)" -PR_BODY=$(cat </dev/null || echo "") - -if [ -n "$EXISTING_PR" ]; then - ok "Reusing existing PR #$EXISTING_PR" - if [ "$DRY_RUN" != "1" ]; then - echo "$PR_BODY" | gh pr edit "$EXISTING_PR" --repo "$REPO" --title "$PR_TITLE" --body-file - - ok "Updated PR #$EXISTING_PR title and body" - fi -elif [ "$DRY_RUN" = "1" ]; then - warn "DRY_RUN=1 — skipping PR creation" -else - PR_URL=$(echo "$PR_BODY" | gh pr create \ - --repo "$REPO" \ - --base main \ - --head "$BRANCH" \ - --title "$PR_TITLE" \ - --body-file -) - ok "Opened PR: $PR_URL" -fi - -# ─────────────────────────────────────────────────────────── -# Done. -# ─────────────────────────────────────────────────────────── -echo -echo "${C_BOLD}${C_GRN}━━━ Phase 1 complete ━━━${C_RST}" -echo -echo "Issue: #${ISSUE_NUM}" -echo "Branch: $BRANCH" -echo "PR: $(gh pr list --repo "$REPO" --head "$BRANCH" --state open --json url --jq '.[0].url' 2>/dev/null || echo "(check gh pr list)")" -echo -echo "Next steps:" -echo " 1. Review CI on the PR. The Changeset Required check may ask for a" -echo " changeset fragment — drop one if needed:" -echo " npm run changeset -- --type Changed --pr \\" -echo " --body \"Introduce \\\`next\\\` integration branch (Phase 1 — additive infrastructure).\"" -echo " 2. Get an approval, merge." -echo " 3. Run Phase 2: bash /path/to/rollout-next-phase2.sh" -echo -echo "Recovering your stashed work on $CURRENT_BR (if you had any):" -echo " git checkout $CURRENT_BR" -echo " git stash list # find the 'pre-next-rollout-...' entry" -echo " git stash pop stash@{N}" diff --git a/rollout-next-phase2.sh b/rollout-next-phase2.sh deleted file mode 100755 index fb45bc52c..000000000 --- a/rollout-next-phase2.sh +++ /dev/null @@ -1,300 +0,0 @@ -#!/usr/bin/env zsh -# rollout-next-phase2.sh — native zsh, runs under macOS Terminal's default shell. -# DO NOT prefix with `bash`. Run as: ./rollout-next-phase2.sh -# -# Phase 2 of the `next` integration-branch rollout. -# RUN THIS ONLY AFTER THE PHASE-1 PR HAS BEEN MERGED TO main. -# -# What it does: -# 1. Pulls main (which now contains Phase 1). -# 2. Creates the `next` branch from main HEAD, pushes it. -# 3. Applies branch protection rules via the script committed in Phase 1. -# 4. Switches the repo's default branch to `next` via gh api. -# 5. Flips `if: false` → `if: true` in auto-backmerge.yml. -# 6. Flips `WARN_ONLY: 'true'` → `'false'` in pr-target-validator.yml. -# 7. Commits the flips on a small follow-up branch, opens a PR to next. -# -# Idempotent: re-running converges. Each step checks if it's already done. -# -# Usage: -# cd /Volumes/Mini\ Me/Users/trekkie/projects/gsd-core -# bash /path/to/rollout-next-phase2.sh -# -# Env overrides: -# REPO=open-gsd/gsd-core (default) -# SKIP_PROTECTION=1 (skip running setup-branch-protection.sh) -# SKIP_DEFAULT_FLIP=1 (skip switching default branch) -# DRY_RUN=1 (skip all pushes, PRs, and api writes) - -set -euo pipefail - -REPO="${REPO:-open-gsd/gsd-core}" -FLIP_BRANCH="chore/flip-next-rollout-flags" -DRY_RUN="${DRY_RUN:-0}" - -if [ -t 1 ]; then - C_BOLD=$'\033[1m'; C_GRN=$'\033[32m'; C_YEL=$'\033[33m'; C_RED=$'\033[31m'; C_DIM=$'\033[2m'; C_RST=$'\033[0m' -else - C_BOLD=''; C_GRN=''; C_YEL=''; C_RED=''; C_DIM=''; C_RST='' -fi - -step() { echo; echo "${C_BOLD}▸ $*${C_RST}"; } -ok() { echo "${C_GRN} ✓${C_RST} $*"; } -warn() { echo "${C_YEL} ⚠${C_RST} $*"; } -die() { echo "${C_RED} ✗ $*${C_RST}" >&2; exit 1; } -note() { echo "${C_DIM} $*${C_RST}"; } - -# ─────────────────────────────────────────────────────────── -# Step 0: Sanity -# ─────────────────────────────────────────────────────────── -step "Sanity checks" - -[ -d .git ] || die "Not in a git repo. cd to your gsd-core checkout." -command -v gh >/dev/null || die "gh not found. https://cli.github.com/" -gh auth status >/dev/null 2>&1 || die "gh not authenticated." - -REMOTE_URL=$(git remote get-url origin 2>/dev/null || true) -case "$REMOTE_URL" in - *"$REPO"*) ok "Remote: $REMOTE_URL" ;; - *) die "Remote 'origin' is $REMOTE_URL — expected to contain $REPO." ;; -esac - -# Protect rollout files from the stash sweep. -ROLLOUT_STAGE=$(mktemp -d) -ROLLOUT_FILES=(rollout-next-phase1.sh rollout-next-phase2.sh next-branch-files.tar.gz) -for f in "${ROLLOUT_FILES[@]}"; do - [ -f "./$f" ] && mv "./$f" "$ROLLOUT_STAGE/" -done -restore_rollout_files() { - for f in "$ROLLOUT_STAGE"/*; do - [ -e "$f" ] || continue - cp "$f" ./ 2>/dev/null || true - done - rm -rf "$ROLLOUT_STAGE" 2>/dev/null || true -} -trap restore_rollout_files EXIT - -# Stash any in-progress work before switching branches. -CURRENT_BR=$(git rev-parse --abbrev-ref HEAD) -note "Currently on: $CURRENT_BR" -if [ -n "$(git status --porcelain)" ]; then - STASH_MSG="pre-next-phase2-$(date +%Y%m%d-%H%M%S) (from $CURRENT_BR)" - git stash push --include-untracked --message "$STASH_MSG" >/dev/null - ok "Stashed: $STASH_MSG" - STASHED=1 -else - STASHED=0 -fi - -restore_rollout_files - -# ─────────────────────────────────────────────────────────── -# Step 1: Refresh main, ensure Phase 1 is present -# ─────────────────────────────────────────────────────────── -step "Switch to main and pull" - -git fetch origin --quiet -git checkout main >/dev/null -git pull --ff-only origin main >/dev/null -ok "main is current at $(git log -1 --format='%h %s' | head -c 80)" - -# Phase-1 sentinel files must be on main now. -PHASE1_FILES=( - "docs/branching.md" - ".github/workflows/auto-backmerge.yml" - ".github/workflows/pr-target-validator.yml" - "scripts/setup-branch-protection.sh" -) -for f in "${PHASE1_FILES[@]}"; do - [ -f "$f" ] || die "Phase 1 file missing on main: $f. Has the Phase 1 PR merged?" -done -ok "Phase 1 files present on main" - -# ─────────────────────────────────────────────────────────── -# Step 2: Create next branch (idempotent) -# ─────────────────────────────────────────────────────────── -step "Create or verify the next branch" - -if git ls-remote --exit-code origin next >/dev/null 2>&1; then - ok "origin/next already exists — leaving as is" -else - if [ "$DRY_RUN" = "1" ]; then - warn "DRY_RUN=1 — would create and push next from main HEAD" - else - # Create next from current main HEAD locally and push. - git checkout -b next main 2>/dev/null || git checkout next - git push -u origin next - ok "Created and pushed origin/next at $(git log -1 --format='%h')" - # Switch back to main so subsequent steps don't accidentally edit next. - git checkout main >/dev/null - fi -fi - -# ─────────────────────────────────────────────────────────── -# Step 3: Apply branch protection -# ─────────────────────────────────────────────────────────── -step "Apply branch protection to main and next" - -if [ "${SKIP_PROTECTION:-0}" = "1" ]; then - warn "SKIP_PROTECTION=1 — skipping. You will need to run this manually:" - note " bash scripts/setup-branch-protection.sh" -elif [ "$DRY_RUN" = "1" ]; then - warn "DRY_RUN=1 — would run scripts/setup-branch-protection.sh" - REPO="$REPO" DRY_RUN=1 bash scripts/setup-branch-protection.sh | head -40 || true -else - REPO="$REPO" bash scripts/setup-branch-protection.sh - ok "Branch protection applied" -fi - -# ─────────────────────────────────────────────────────────── -# Step 4: Flip default branch to next -# ─────────────────────────────────────────────────────────── -step "Switch default branch to next" - -CURRENT_DEFAULT=$(gh api "/repos/$REPO" --jq '.default_branch') -note "Current default: $CURRENT_DEFAULT" - -if [ "$CURRENT_DEFAULT" = "next" ]; then - ok "Default is already next — skip" -elif [ "${SKIP_DEFAULT_FLIP:-0}" = "1" ]; then - warn "SKIP_DEFAULT_FLIP=1 — leaving default as $CURRENT_DEFAULT" - note "To flip later: Settings → Branches → Default branch → next" -elif [ "$DRY_RUN" = "1" ]; then - warn "DRY_RUN=1 — would PATCH /repos/$REPO default_branch=next" -else - gh api -X PATCH "/repos/$REPO" -f default_branch=next >/dev/null - ok "Default branch switched to next" -fi - -# ─────────────────────────────────────────────────────────── -# Step 5 + 6: Flip the two phase-gate flags on a follow-up branch -# ─────────────────────────────────────────────────────────── -step "Flip phase-gate flags (auto-backmerge.yml + pr-target-validator.yml)" - -# Both edits need to land on a feature branch off `next`, then PR'd back. -# (We can't push directly to main anymore — branch protection forbids it.) - -# Create or switch to the flip branch off next. -git fetch origin next --quiet 2>/dev/null || true -if git show-ref --verify --quiet "refs/heads/$FLIP_BRANCH"; then - git checkout "$FLIP_BRANCH" >/dev/null - if git rev-parse --verify "origin/$FLIP_BRANCH" >/dev/null 2>&1; then - warn "origin/$FLIP_BRANCH already exists — pulling to be sure" - git pull --ff-only origin "$FLIP_BRANCH" >/dev/null || true - else - git reset --hard origin/next >/dev/null - fi -else - git checkout -b "$FLIP_BRANCH" origin/next >/dev/null -fi - -AUTO_BM=".github/workflows/auto-backmerge.yml" -VALID=".github/workflows/pr-target-validator.yml" - -# Flip auto-backmerge: `if: false` (the phase-1 gate line) → `if: true` -# The pattern is anchored by the exact comment + indentation so we don't -# accidentally hit a different `if:` line. -if grep -q " if: false" "$AUTO_BM"; then - # macOS-compatible in-place edit - sed -i.rollout-bak "s/^ if: false$/ if: true/" "$AUTO_BM" - rm -f "$AUTO_BM.rollout-bak" - grep -q " if: true" "$AUTO_BM" || die "auto-backmerge.yml flip didn't take" - ok "auto-backmerge.yml: if: false → if: true" -elif grep -q " if: true" "$AUTO_BM"; then - ok "auto-backmerge.yml already flipped (idempotent skip)" -else - die "Could not find phase-gate 'if:' line in $AUTO_BM" -fi - -# Flip validator: WARN_ONLY: 'true' → 'false' -if grep -q "WARN_ONLY: 'true'" "$VALID"; then - sed -i.rollout-bak "s/WARN_ONLY: 'true'/WARN_ONLY: 'false'/" "$VALID" - rm -f "$VALID.rollout-bak" - grep -q "WARN_ONLY: 'false'" "$VALID" || die "validator flip didn't take" - ok "pr-target-validator.yml: WARN_ONLY 'true' → 'false'" -elif grep -q "WARN_ONLY: 'false'" "$VALID"; then - ok "pr-target-validator.yml already flipped (idempotent skip)" -else - die "Could not find WARN_ONLY in $VALID" -fi - -# YAML validation -for f in "$AUTO_BM" "$VALID"; do - python3 -c "import yaml; yaml.safe_load(open('$f'))" || die "YAML invalid after flip: $f" -done -ok "YAML still valid" - -# Commit -git add "$AUTO_BM" "$VALID" -if git diff --cached --quiet; then - warn "Nothing to commit (already committed)" -else - git commit -m "chore: enable next-branch automation (Phase 2 flips) - -- auto-backmerge.yml: enable the job (was if: false in Phase 1) -- pr-target-validator.yml: enforce instead of warn-only - -These flips are the operational gate for the next-branch model. The -next branch and branch protection were created/applied before this PR." - ok "Committed flips" -fi - -# ─────────────────────────────────────────────────────────── -# Step 7: Push + open follow-up PR against next -# ─────────────────────────────────────────────────────────── -step "Push and open follow-up PR (base = next)" - -if [ "$DRY_RUN" = "1" ]; then - warn "DRY_RUN=1 — skipping push and PR" -else - git push -u origin "$FLIP_BRANCH" - ok "Pushed $FLIP_BRANCH" - - EXISTING_PR=$(gh pr list --repo "$REPO" --head "$FLIP_BRANCH" --state open --json number --jq '.[0].number' 2>/dev/null || echo "") - PR_TITLE="chore: enable next-branch automation (Phase 2 flips)" - PR_BODY="Phase 2 follow-up to the \`next\` integration-branch rollout. - -This PR flips the two phase-gate flags that shipped inert in Phase 1: - -- \`auto-backmerge.yml\`: \`if: false\` → \`if: true\` (the back-merge job now runs on every push to main) -- \`pr-target-validator.yml\`: \`WARN_ONLY: 'true'\` → \`'false'\` (the validator now fails the check instead of just commenting) - -The \`next\` branch and branch-protection rules were created/applied out-of-band by \`scripts/rollout-next-phase2.sh\` before this PR. - -After this merges, the model is fully live. New PRs should target \`next\` (it's the default now); the validator will catch mistakes and tell contributors how to retarget." - - if [ -n "$EXISTING_PR" ]; then - echo "$PR_BODY" | gh pr edit "$EXISTING_PR" --repo "$REPO" --title "$PR_TITLE" --body-file - - ok "Updated existing PR #$EXISTING_PR" - else - PR_URL=$(echo "$PR_BODY" | gh pr create \ - --repo "$REPO" \ - --base next \ - --head "$FLIP_BRANCH" \ - --title "$PR_TITLE" \ - --body-file -) - ok "Opened PR: $PR_URL" - fi -fi - -# ─────────────────────────────────────────────────────────── -# Done -# ─────────────────────────────────────────────────────────── -echo -echo "${C_BOLD}${C_GRN}━━━ Phase 2 complete ━━━${C_RST}" -echo -echo "What's live now:" -echo " • next branch exists, branch protection applied to main + next" -echo " • Default branch: next (new PRs default to it)" -echo " • auto-backmerge.yml: enabled — will open main→next PR after each push to main" -echo " • pr-target-validator.yml: enforcing — fails the check on wrong target" -echo -echo "Follow-up PR to merge: $(gh pr list --repo "$REPO" --head "$FLIP_BRANCH" --state open --json url --jq '.[0].url' 2>/dev/null || echo "(check gh pr list)")" -echo -echo "Phase 3 (when ready, weeks-to-months out): apply the release.yml /" -echo "hotfix.yml / auto-branch.yml patches inlined in the ADR." -if [ "$STASHED" = "1" ]; then - echo - echo "Your earlier work is stashed. Recover it with:" - echo " git checkout $CURRENT_BR && git stash list && git stash pop stash@{0}" -fi diff --git a/scripts/diff-touches-shipped-paths.cjs b/scripts/diff-touches-shipped-paths.cjs index da0943d60..ae27dc64c 100644 --- a/scripts/diff-touches-shipped-paths.cjs +++ b/scripts/diff-touches-shipped-paths.cjs @@ -12,11 +12,10 @@ * - package.json (always included by `npm pack`, regardless of `files`) * - every entry in package.json `files`, treated as either an exact * file match or a directory prefix (matching `npm pack` semantics). - * - CI-gating test paths: `tests/` plus - * `sdk/src//.test.` and `.spec.` variants - * — these don't ship in the tarball, but they gate the hotfix-branch - * test job. A test fixture update that aligns with a cherry-picked - * production fix MUST be pickable or CI fails on the hotfix run. + * - CI-gating test paths: `tests/` — these don't ship in the + * tarball, but they gate the hotfix-branch test job. A test fixture + * update that aligns with a cherry-picked production fix MUST be + * pickable or CI fails on the hotfix run. * #3621 — root cause of the v1.42.3 hotfix red CI. * * `package-lock.json` is intentionally NOT considered shipped — `npm pack` @@ -65,12 +64,7 @@ function loadShipPrefixes(pkgPath) { // in hotfix.yml, this lets `test(####):` fixture-alignment commits be // cherry-picked alongside their production counterparts. function isCiGating(diffPath) { - if (diffPath.startsWith('tests/')) return true; - // SDK vitest specs live next to source. Production source ships via - // sdk/dist/ (already in package.json `files`); the test files are what's - // missing from that surface. - if (diffPath.startsWith('sdk/src/') && /\.(test|spec)\.(ts|cjs|mjs|js)$/.test(diffPath)) return true; - return false; + return diffPath.startsWith('tests/'); } function isShipped(diffPath, shipPrefixes) { diff --git a/vitest.config.ts b/vitest.config.ts deleted file mode 100644 index b0200d123..000000000 --- a/vitest.config.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { defineConfig } from 'vitest/config'; - -export default defineConfig({ - test: { - projects: [ - { - test: { - name: 'unit', - root: './sdk', - include: ['src/**/*.test.ts'], - exclude: ['src/**/*.integration.test.ts'], - }, - }, - { - test: { - name: 'integration', - root: './sdk', - include: ['src/**/*.integration.test.ts'], - testTimeout: 120_000, - }, - }, - ], - }, -});