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
8 changes: 5 additions & 3 deletions .github/SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ Manual publish-only recovery requires an explicit comma-separated `projects` inp

#### Beta quick path

1. Let an eligible `master` push run PREPARE. For this incident only, manual PREPARE must select all seven projects listed above.
The published `@effectify/solid-query@0.5.12` collision cannot be repaired by moving `latest` back to the stale tarball. Recovery is strictly ordered: merge the implementation PR; run corrective beta PREPARE/PR/FINALIZE for only `@effectify/solid-query@0.5.13-beta.0`; then run the seven-project stable PREPARE/PR/FINALIZE ending at `@effectify/solid-query@0.5.13`.

1. Let an eligible `master` push run PREPARE. The existing incident PREPARE still requires all seven projects. The one corrective exception selects only `@effectify/solid-query`; the workflow fixes positional `prepatch`, `--preid=beta`, and all disabled git/staging effects, and accepts only root `CHANGELOG.md` plus `packages/solid/query/package.json` at `0.5.13-beta.0`.
2. Verify the summary's source SHA, release branch, changed paths, and versions. PREPARE changes only root `CHANGELOG.md` and the selected manifests.
3. Create or reuse the required approved issue. Manually open one linked PR from the reported release branch to `master`; its sole `type:*` label is `type:chore`.
4. Use ordinary required checks, human review, and protected merge. Confirm the merge-triggered beta run reports `suppress` and publishes nothing.
Expand All @@ -86,7 +88,7 @@ FINALIZE freshly checks that checkout `HEAD`, `origin/master`, and `expected_sha
| `@effectify/prisma` | `1.1.13-beta.0` |
| `@effectify/hatchet` | `0.1.0-beta.0` |

Do **not** dispatch stable during beta recovery. Stop on a newer `master`, an unexpected generated path or version, an ambiguous remote read, a lightweight or wrong-target tag, a conflicting Release, or inconsistent npm state.
Do **not** dispatch stable during either beta recovery. Complete corrective beta PREPARE, its protected PR, and exact-SHA FINALIZE before starting the seven-project stable PREPARE. Stop on a newer `master`, an unexpected generated path or version, an ambiguous remote read, a lightweight or wrong-target tag, a conflicting Release, or inconsistent npm state.

Before publication, rollback is limited to deleting the unprotected prepared branch or closing/reverting the release PR through normal policy. Do not delete published tags, Releases, or npm artifacts as rollback; rerun the exact FINALIZE request or obtain authorization for a fix-forward release.

Expand All @@ -102,7 +104,7 @@ Stable is a protected **PREPARE → manual authorization → FINALIZE** promotio
| `@effectify/react-query` | `1.0.0-beta.1` | `1.0.0` |
| `@effectify/react-router` | `0.6.0-beta.0` | `0.6.0` |
| `@effectify/react-router-better-auth` | `0.5.12-beta.0` | `0.5.12` |
| `@effectify/solid-query` | `0.5.12-beta.0` | `0.5.12` |
| `@effectify/solid-query` | `0.5.13-beta.0` | `0.5.13` |

#### Protected stable quick path

Expand Down
46 changes: 33 additions & 13 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,14 @@ jobs:
}
echo "mode=finalize" >> "$GITHUB_OUTPUT"
else
cmp -s <(printf '%s\n' "$ALL_PROJECTS") <(printf '%s\n' "$SELECTED_PROJECTS") || {
echo "manual PREPARE requires all seven release projects" >&2
if cmp -s <(printf '%s\n' "$ALL_PROJECTS") <(printf '%s\n' "$SELECTED_PROJECTS"); then
echo "version_specifier=" >> "$GITHUB_OUTPUT"
elif [ "$SELECTED_PROJECTS" = "@effectify/solid-query" ]; then
echo "version_specifier=prepatch" >> "$GITHUB_OUTPUT"
else
echo "manual PREPARE requires all seven release projects or the corrective solid-query singleton" >&2
exit 1
}
fi
echo "mode=prepare" >> "$GITHUB_OUTPUT"
fi
echo "has_projects=true" >> "$GITHUB_OUTPUT"
Expand Down Expand Up @@ -151,8 +155,19 @@ jobs:
'@effectify/react-query=1.0.0-beta.1=1.0.0|packages/react/query/package.json' \
'@effectify/react-router=0.6.0-beta.0=0.6.0|packages/react/router/package.json' \
'@effectify/react-router-better-auth=0.5.12-beta.0=0.5.12|packages/react/router-better-auth/package.json' \
'@effectify/solid-query=0.5.12-beta.0=0.5.12|packages/solid/query/package.json' > "$STABLE_TRANSITIONS"
'@effectify/solid-query=0.5.13-beta.0=0.5.13|packages/solid/query/package.json' > "$STABLE_TRANSITIONS"
CHANGED=$(mktemp); git diff --name-only --no-renames "$BASE" "$HEAD" | sort -u > "$CHANGED"
CORRECTIVE_PATHS=$(mktemp)
printf '%s\n' CHANGELOG.md packages/solid/query/package.json | sort > "$CORRECTIVE_PATHS"
CORRECTIVE_TRANSITION='@effectify/solid-query=0.5.12-beta.0=0.5.13-beta.0|packages/solid/query/package.json'
if cmp -s "$CORRECTIVE_PATHS" "$CHANGED"; then
OLD_NAME=$(git show "$BASE:packages/solid/query/package.json" | jq -er .name) || exit 1
OLD_VERSION=$(git show "$BASE:packages/solid/query/package.json" | jq -er .version) || exit 1
NEW_NAME=$(jq -er .name packages/solid/query/package.json) || exit 1
NEW_VERSION=$(jq -er .version packages/solid/query/package.json) || exit 1
[ "$OLD_NAME" = "@effectify/solid-query" ] && [ "$NEW_NAME" = "@effectify/solid-query" ] && [ "$OLD_VERSION" = "0.5.12-beta.0" ] && [ "$NEW_VERSION" = "0.5.13-beta.0" ] || { echo "corrective beta shape is malformed" >&2; exit 1; }
echo "mode=suppress" >> "$GITHUB_OUTPUT"; echo "has_projects=false" >> "$GITHUB_OUTPUT"; echo "projects=" >> "$GITHUB_OUTPUT"; exit 0
fi
STABLE_SHAPE=false
if cmp -s "$EXPECTED_PATHS" "$CHANGED"; then
STABLE_SHAPE=true
Expand Down Expand Up @@ -218,6 +233,7 @@ jobs:
if: ${{ steps.release.outputs.mode == 'prepare' && steps.release.outputs.has_projects == 'true' }}
env:
PROJECTS: ${{ steps.release.outputs.projects }}
VERSION_SPECIFIER: ${{ steps.release.outputs.version_specifier }}
MANUAL_PREPARE: ${{ github.event_name == 'workflow_dispatch' }}
run: |
set -euo pipefail
Expand All @@ -242,7 +258,7 @@ jobs:
printf '%s\n' "${RELEASE_PATHS[@]}" | sort -u > /tmp/expected-release-paths
test "$(wc -l < /tmp/expected-release-paths | tr -d ' ')" = "$(( ${#SELECTED[@]} + 1 ))"

pnpm nx release version "--projects=$PROJECTS" --preid=beta --git-commit=false --git-tag=false --git-push=false --stage-changes=false
pnpm nx release version $VERSION_SPECIFIER "--projects=$PROJECTS" --preid=beta --git-commit=false --git-tag=false --git-push=false --stage-changes=false

verify_prepared_tree() {
ACTUAL_PATHS=$(mktemp)
Expand All @@ -265,14 +281,18 @@ jobs:

if [ "$MANUAL_PREPARE" = "true" ]; then
EXPECTED_MATRIX=$(mktemp)
printf '%s\n' \
'@effectify/hatchet=0.1.0-beta.0' \
'@effectify/node-better-auth=0.5.12-beta.0' \
'@effectify/prisma=1.1.13-beta.0' \
'@effectify/react-query=1.0.0-beta.1' \
'@effectify/react-router=0.6.0-beta.0' \
'@effectify/react-router-better-auth=0.5.12-beta.0' \
'@effectify/solid-query=0.5.12-beta.0' | sort > "$EXPECTED_MATRIX"
if [ "$PROJECTS" = "@effectify/solid-query" ]; then
printf '%s\n' '@effectify/solid-query=0.5.13-beta.0' > "$EXPECTED_MATRIX"
else
printf '%s\n' \
'@effectify/hatchet=0.1.0-beta.0' \
'@effectify/node-better-auth=0.5.12-beta.0' \
'@effectify/prisma=1.1.13-beta.0' \
'@effectify/react-query=1.0.0-beta.1' \
'@effectify/react-router=0.6.0-beta.0' \
'@effectify/react-router-better-auth=0.5.12-beta.0' \
'@effectify/solid-query=0.5.12-beta.0' | sort > "$EXPECTED_MATRIX"
fi
ACTUAL_MATRIX=$(mktemp)
while IFS=$'\t' read -r project name manifest; do
printf '%s=%s\n' "$name" "$(jq -er '.version' "$manifest")"
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release-stable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ jobs:
SOURCE_SHA=$(git rev-parse HEAD); SHA_PREFIX=${SOURCE_SHA:0:12}; BRANCH="release/stable-$SHA_PREFIX"
git config user.name 'github-actions[bot]'; git config user.email 'github-actions[bot]@users.noreply.github.com'
EXPECTED_PATHS=$(mktemp); printf '%s\n' CHANGELOG.md packages/hatchet/package.json packages/node/better-auth/package.json packages/prisma/package.json packages/react/query/package.json packages/react/router/package.json packages/react/router-better-auth/package.json packages/solid/query/package.json | sort > "$EXPECTED_PATHS"
RECORDS=$(mktemp); printf '%s\n' '@effectify/hatchet|packages/hatchet/package.json|0.1.0-beta.0|0.1.0' '@effectify/node-better-auth|packages/node/better-auth/package.json|0.5.12-beta.0|0.5.12' '@effectify/prisma|packages/prisma/package.json|1.1.13-beta.0|1.1.13' '@effectify/react-query|packages/react/query/package.json|1.0.0-beta.1|1.0.0' '@effectify/react-router|packages/react/router/package.json|0.6.0-beta.0|0.6.0' '@effectify/react-router-better-auth|packages/react/router-better-auth/package.json|0.5.12-beta.0|0.5.12' '@effectify/solid-query|packages/solid/query/package.json|0.5.12-beta.0|0.5.12' > "$RECORDS"
RECORDS=$(mktemp); printf '%s\n' '@effectify/hatchet|packages/hatchet/package.json|0.1.0-beta.0|0.1.0' '@effectify/node-better-auth|packages/node/better-auth/package.json|0.5.12-beta.0|0.5.12' '@effectify/prisma|packages/prisma/package.json|1.1.13-beta.0|1.1.13' '@effectify/react-query|packages/react/query/package.json|1.0.0-beta.1|1.0.0' '@effectify/react-router|packages/react/router/package.json|0.6.0-beta.0|0.6.0' '@effectify/react-router-better-auth|packages/react/router-better-auth/package.json|0.5.12-beta.0|0.5.12' '@effectify/solid-query|packages/solid/query/package.json|0.5.13-beta.0|0.5.13' > "$RECORDS"
while IFS='|' read -r NAME PATH OLD NEW; do test "$(jq -er .name "$PATH")" = "$NAME"; test "$(jq -er .version "$PATH")" = "$OLD" || { echo "::error::unauthorized source $NAME"; exit 1; }; done < "$RECORDS"
test -z "$(git status --porcelain)" || { echo '::error::PREPARE requires clean tree'; exit 1; }
REFS_BEFORE=$(git for-each-ref --format='%(refname) %(objectname)' refs/heads refs/tags | sort)
Expand Down Expand Up @@ -140,7 +140,7 @@ jobs:
set -euo pipefail
git fetch origin master:refs/remotes/origin/master --no-tags
test "$(git rev-parse HEAD)" = "$EXPECTED_SHA"; test "$(git rev-parse origin/master)" = "$EXPECTED_SHA"
RECORDS=$(mktemp); printf '%s\n' '@effectify/hatchet|packages/hatchet/package.json|0.1.0' '@effectify/node-better-auth|packages/node/better-auth/package.json|0.5.12' '@effectify/prisma|packages/prisma/package.json|1.1.13' '@effectify/react-query|packages/react/query/package.json|1.0.0' '@effectify/react-router|packages/react/router/package.json|0.6.0' '@effectify/react-router-better-auth|packages/react/router-better-auth/package.json|0.5.12' '@effectify/solid-query|packages/solid/query/package.json|0.5.12' > "$RECORDS"
RECORDS=$(mktemp); printf '%s\n' '@effectify/hatchet|packages/hatchet/package.json|0.1.0' '@effectify/node-better-auth|packages/node/better-auth/package.json|0.5.12' '@effectify/prisma|packages/prisma/package.json|1.1.13' '@effectify/react-query|packages/react/query/package.json|1.0.0' '@effectify/react-router|packages/react/router/package.json|0.6.0' '@effectify/react-router-better-auth|packages/react/router-better-auth/package.json|0.5.12' '@effectify/solid-query|packages/solid/query/package.json|0.5.13' > "$RECORDS"
: > /tmp/missing-projects; : > /tmp/missing-tags; : > /tmp/missing-releases
while IFS='|' read -r NAME PATH VERSION; do
test "$(jq -er .name "$PATH")" = "$NAME"; test "$(jq -er .version "$PATH")" = "$VERSION" || { echo '::error::merged stable matrix mismatch'; exit 1; }
Expand Down
22 changes: 21 additions & 1 deletion openspec/specs/protected-stable-promotion/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,26 @@ Define a protected, auditable promotion of the complete authorized beta matrix t

## Requirements

### Requirement: Collision correction precedes stable promotion

The system MUST correct the stale `@effectify/solid-query@0.5.12` collision by authorizing exactly one manual beta PREPARE exception: the singleton project `@effectify/solid-query`, positional `prepatch`, `--preid=beta`, disabled commit/tag/push/staging effects, exact output `0.5.13-beta.0`, and exactly `CHANGELOG.md` plus `packages/solid/query/package.json`. No caller-selectable version specifier or other subset is authorized. Its protected merge MUST be suppressed only for the exact `0.5.12-beta.0` to `0.5.13-beta.0` transition and those two paths.

The required order is implementation PR, corrective beta PREPARE/PR/FINALIZE, then seven-project stable PREPARE/PR/FINALIZE. Recovery MUST NOT move npm `latest` to the stale `0.5.12` tarball.

#### Scenario: Corrective beta is exact

- GIVEN the implementation PR has merged
- WHEN manual beta PREPARE selects only `@effectify/solid-query`
- THEN Nx uses positional `prepatch` and `--preid=beta` with all git and staging effects disabled
- AND the only generated version is `@effectify/solid-query@0.5.13-beta.0`
- AND the only generated paths are the root changelog and Solid manifest

#### Scenario: Corrective suppression fails closed

- GIVEN a merge has partial, mixed, message-only, wrong-version, or additional-path changes
- WHEN beta classifies the merge
- THEN it MUST NOT suppress the run as the corrective beta shape

### Requirement: Exact authorized promotion matrix

The system MUST accept the promotion only when the requested project set is complete, duplicate-free, and exactly matches this source-to-target matrix:
Expand All @@ -18,7 +38,7 @@ The system MUST accept the promotion only when the requested project set is comp
| `@effectify/react-query` | `1.0.0-beta.1` | `1.0.0` |
| `@effectify/react-router` | `0.6.0-beta.0` | `0.6.0` |
| `@effectify/react-router-better-auth` | `0.5.12-beta.0` | `0.5.12` |
| `@effectify/solid-query` | `0.5.12-beta.0` | `0.5.12` |
| `@effectify/solid-query` | `0.5.13-beta.0` | `0.5.13` |

Every stable target MUST equal the semver core of its authorized beta source. The system MUST NOT recalculate, increment, substitute, or partially promote this matrix.

Expand Down
32 changes: 29 additions & 3 deletions scripts/release-policy-contract.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@
const channelPublishCommand = (channel) =>
new RegExp(`^pnpm nx release publish "--projects=\\$PROJECTS" --tag=${channel}$`)
const betaVersionCommand =
/^pnpm nx release version "--projects=\$PROJECTS" --preid=beta --git-commit=false --git-tag=false --git-push=false --stage-changes=false$/
/^pnpm nx release version(?: \$VERSION_SPECIFIER)? "--projects=\$PROJECTS" --preid=beta --git-commit=false --git-tag=false --git-push=false --stage-changes=false$/
const betaGitIdentityCondition =
"${{ (steps.release.outputs.mode == 'prepare' || steps.release.outputs.mode == 'finalize') && steps.release.outputs.has_projects == 'true' }}"
const gitIdentityCommands = [
Expand Down Expand Up @@ -476,7 +476,7 @@
"@effectify/react-query=1.0.0-beta.1=1.0.0|packages/react/query/package.json",
"@effectify/react-router=0.6.0-beta.0=0.6.0|packages/react/router/package.json",
"@effectify/react-router-better-auth=0.5.12-beta.0=0.5.12|packages/react/router-better-auth/package.json",
"@effectify/solid-query=0.5.12-beta.0=0.5.12|packages/solid/query/package.json",
"@effectify/solid-query=0.5.13-beta.0=0.5.13|packages/solid/query/package.json",
])
if (!active.includes(transition)) violations.push(`beta stable transition ${transition}`)
for (const pattern of [
Expand Down Expand Up @@ -600,10 +600,10 @@

const releasePolicyBootstrapViolations = (source) => {
const steps = extractSteps(extractJob(source, "release-policy"))
const setupNodeIndex = steps.findIndex((step) => /^actions\/setup-node@/.test(step.uses))

Check warning on line 603 in scripts/release-policy-contract.test.mjs

View workflow job for this annotation

GitHub Actions / 🔍 Lint & Format

unicorn(prefer-string-starts-ends-with)

Prefer String#startsWith over a regex with a caret.
if (setupNodeIndex === -1) return ["release-policy setup-node"]

const pnpmIndex = steps.findIndex((step) => /^pnpm\/action-setup@/.test(step.uses))

Check warning on line 606 in scripts/release-policy-contract.test.mjs

View workflow job for this annotation

GitHub Actions / 🔍 Lint & Format

unicorn(prefer-string-starts-ends-with)

Prefer String#startsWith over a regex with a caret.
const cacheDisabled = steps[setupNodeIndex].packageManagerCache === "false"
return pnpmIndex !== -1 && pnpmIndex < setupNodeIndex ? [] : cacheDisabled ? [] : ["release-policy setup-node cache"]
}
Expand Down Expand Up @@ -968,12 +968,38 @@
"@effectify/react-query=1.0.0-beta.1=1.0.0",
"@effectify/react-router=0.6.0-beta.0=0.6.0",
"@effectify/react-router-better-auth=0.5.12-beta.0=0.5.12",
"@effectify/solid-query=0.5.12-beta.0=0.5.12",
"@effectify/solid-query=0.5.13-beta.0=0.5.13",
])
assert.match(active, new RegExp(transition.replaceAll("/", "\\/")))
assert.match(active, /stable promotion shape is partial, mixed, or malformed/)
})

test("corrective solid-query beta and updated stable matrix are exact", () => {
const beta = withoutComments(workflows.beta)
const stable = withoutComments(workflows.stable)
assert.match(beta, /manual PREPARE requires all seven release projects or the corrective solid-query singleton/)
assert.match(beta, /echo "version_specifier=prepatch" >> "\$GITHUB_OUTPUT"/)
assert.match(beta, /pnpm nx release version \$VERSION_SPECIFIER "--projects=\$PROJECTS" --preid=beta --git-commit=false --git-tag=false --git-push=false --stage-changes=false/)
assert.match(beta, /0\.5\.12-beta\.0=0\.5\.13-beta\.0\|packages\/solid\/query\/package\.json/)
assert.match(beta, /CHANGELOG\.md packages\/solid\/query\/package\.json \| sort > "\$CORRECTIVE_PATHS"/)
assert.match(stable, /@effectify\/solid-query\|packages\/solid\/query\/package\.json\|0\.5\.13-beta\.0\|0\.5\.13/)
assert.doesNotMatch(stable, /@effectify\/solid-query\|packages\/solid\/query\/package\.json\|0\.5\.12-beta\.0\|0\.5\.12/)

for (const [name, before, after, required] of [
["arbitrary singleton", 'elif [ "$SELECTED_PROJECTS" = "@effectify/solid-query" ]', 'elif [ "$SELECTED_PROJECTS" = "@effectify/react-query" ]', /SELECTED_PROJECTS" = "@effectify\/solid-query/],
["prerelease specifier", "version_specifier=prepatch", "version_specifier=prerelease", /version_specifier=prepatch/],
["wrong target", "CORRECTIVE_TRANSITION='@effectify/solid-query=0.5.12-beta.0=0.5.13-beta.0", "CORRECTIVE_TRANSITION='@effectify/solid-query=0.5.12-beta.0=0.5.14-beta.0", /CORRECTIVE_TRANSITION='@effectify\/solid-query=0\.5\.12-beta\.0=0\.5\.13-beta\.0/],
["wrong counter", "CORRECTIVE_TRANSITION='@effectify/solid-query=0.5.12-beta.0=0.5.13-beta.0", "CORRECTIVE_TRANSITION='@effectify/solid-query=0.5.12-beta.0=0.5.13-beta.1", /CORRECTIVE_TRANSITION='@effectify\/solid-query=0\.5\.12-beta\.0=0\.5\.13-beta\.0/],
["broad paths", "CHANGELOG.md packages/solid/query/package.json | sort", "CHANGELOG.md README.md packages/solid/query/package.json | sort", /CHANGELOG\.md packages\/solid\/query\/package\.json \| sort/],
["message-only", 'if cmp -s "$CORRECTIVE_PATHS" "$CHANGED"; then', 'if [[ "$HEAD_MESSAGE" == *"[skip release]"* ]]; then', /cmp -s "\$CORRECTIVE_PATHS" "\$CHANGED"/],
]) {
const mutated = mutate(beta, before, after)
assert.doesNotMatch(mutated, required, name)
}
const oldStable = mutate(stable, "0.5.13-beta.0|0.5.13", "0.5.12-beta.0|0.5.12")
assert.doesNotMatch(oldStable, /0\.5\.13-beta\.0\|0\.5\.13/)
})

test("alpha and beta exact-range and membership mutations fail closed", () => {
const policy = { ...workflows, docs: readme }

Expand Down
Loading