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
4 changes: 3 additions & 1 deletion .github/workflows/release-stable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,9 @@ jobs:
while IFS='|' read -r NAME MANIFEST_PATH OLD NEW; do if DETAIL=$(node -e 'const fs=require("node:fs");const [path,name,version]=process.argv.slice(1);let value;try{value=JSON.parse(fs.readFileSync(path,"utf8"))}catch{process.exit(2)}if(!value||typeof value!=="object"||Array.isArray(value)||typeof value.name!=="string"||typeof value.version!=="string"||value.name!==name||value.version!==version){const actual={name:typeof value?.name==="string"?value.name:null,version:typeof value?.version==="string"?value.version:null};process.stdout.write(`actual=${JSON.stringify(actual)} expected=${JSON.stringify({name,version})}`);process.exit(1)}' "$MANIFEST_PATH" "$NAME" "$OLD"); then :; else STATUS=$?; if [ "$STATUS" = 1 ]; then echo "::error::source manifest identity mismatch for $NAME: $DETAIL"; else echo "::error::source manifest execution or parse failed for $NAME"; fi; exit 1; fi; 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)
pnpm nx release version patch "--projects=$PROJECTS" --git-commit=false --git-tag=false --git-push=false --stage-changes=false
while IFS='|' read -r NAME MANIFEST_PATH OLD NEW; do
pnpm nx release version "$NEW" "--projects=$NAME" --git-commit=false --git-tag=false --git-push=false --stage-changes=false
done < "$RECORDS"
test "$REFS_BEFORE" = "$(git for-each-ref --format='%(refname) %(objectname)' refs/heads refs/tags | sort)" || { echo '::error::Nx moved refs'; exit 1; }
test -z "$(git diff --cached --name-only)" || { echo '::error::Nx staged files'; exit 1; }
ACTUAL=$(mktemp); { git diff --name-only --no-renames HEAD; git ls-files --others --exclude-standard; } | sort -u > "$ACTUAL"
Expand Down
20 changes: 18 additions & 2 deletions scripts/release-policy-contract.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -543,7 +543,7 @@
["ref snapshot", /REFS_BEFORE=\$\(git for-each-ref/],
[
"Nx flags",
/^pnpm nx release version patch "--projects=\$PROJECTS" --git-commit=false --git-tag=false --git-push=false --stage-changes=false$/m,
/^pnpm nx release version "\$NEW" "--projects=\$NAME" --git-commit=false --git-tag=false --git-push=false --stage-changes=false$/m,
],
["refs unchanged", /test "\$REFS_BEFORE" = "\$\(git for-each-ref/],
["no Nx staging", /test -z "\$\(git diff --cached --name-only\)"/],
Expand Down Expand Up @@ -672,6 +672,22 @@
violations.push(`stable ${phase} manifest expected identity detail`)
}
}
if (prepare) {
const versionCommands = prepare.commands.filter((command) => /pnpm nx release version/.test(command))
if (versionCommands.length !== 1 || /\bpatch\b|--projects=\$PROJECTS/.test(versionCommands[0])) violations.push("stable exact per-record version action")
if (/writeFileSync|fs\.writeFile|jq[^\n]*\.version|sed -i|npm pkg set/.test(prepare.source)) violations.push("stable direct manifest edit")
const records = [...prepare.source.matchAll(/'(@effectify\/[^|']+\|[^|']+\|[^|']+\|[^']+)'/g)].map(([, record]) => record)
const expectedRecords = [
"@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",
]
if (JSON.stringify(records) !== JSON.stringify(expectedRecords)) violations.push("stable exact PREPARE records")
}
if (!prepare || !/mode == 'prepare'/.test(prepare.condition)) violations.push("stable PREPARE isolation")
if (
prepare &&
Expand All @@ -696,10 +712,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 715 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 718 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 @@ -1101,7 +1117,7 @@
assert.match(active, /MODE=finalize/)
assert.match(
active,
/pnpm nx release version patch "--projects=\$PROJECTS" --git-commit=false --git-tag=false --git-push=false --stage-changes=false/,
/pnpm nx release version "\$NEW" "--projects=\$NAME" --git-commit=false --git-tag=false --git-push=false --stage-changes=false/,
)
assert.match(active, /HEAD:refs\/heads\/release\/stable-\$SHA_PREFIX/)
assert.match(active, /git push --atomic origin "\$\{TAG_REFS\[@\]\}"/)
Expand Down
71 changes: 71 additions & 0 deletions tools/release-version-actions.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,77 @@ const releasedPackages = [
{ name: "@effectify/prisma", version: "1.0.0" },
]

const stableMatrix = [
["@effectify/hatchet", "0.1.0", []],
["@effectify/node-better-auth", "0.5.12", []],
["@effectify/prisma", "1.1.13", []],
["@effectify/react-query", "1.0.0", []],
["@effectify/react-router", "0.6.0", ["0.5.10"]],
["@effectify/react-router-better-auth", "0.5.12", []],
["@effectify/solid-query", "0.5.13", ["0.5.12"]],
]

test("no-network stable actions accept the exact heterogeneous matrix and reject an exact target collision", async () => {
const delegated = []
const delegatedUpdates = []
class BaseVersionActions {
constructor(releaseGroup, projectGraphNode, finalConfigForProject) {
this.releaseGroup = releaseGroup
this.projectGraphNode = projectGraphNode
this.finalConfigForProject = finalConfigForProject
}
async init() {}
async calculateNewVersion() {
delegated.push(this.packageJson?.name ?? this.projectGraphNode.data.root)
return { newVersion: this.finalConfigForProject.candidate, logText: "Nx selected exact target" }
}
async updateProjectVersion(_tree, version) {
delegatedUpdates.push(version)
}
}

const run = async ([name, candidate, publishedVersions]) => {
const VersionActions = createCollisionAwareVersionActions({
BaseVersionActions,
resolveRegistry: async () => "https://registry.example.test/",
getPublishedVersions: async () => publishedVersions,
})
const action = new VersionActions({}, { data: { root: name.slice("@effectify/".length) } }, {
candidate,
versionActionsOptions: {},
})
await action.init({ root: "/repo", read: () => Buffer.from(JSON.stringify({ name, version: "0.0.0" })) })
return action.calculateNewVersion("0.0.0", candidate, "exact stable", {}, "")
}

const accepted = []
for (const record of stableMatrix) accepted.push(await run(record))
assert.deepEqual(accepted.map(({ newVersion }) => newVersion), stableMatrix.map(([, version]) => version))
assert.equal(delegated.length, 7)

const beforeCollision = delegated.length
await assert.rejects(
() => run(["@effectify/solid-query", "0.5.13", ["0.5.12", "0.5.13"]]),
/stable candidate 0\.5\.13 is already published/,
)
assert.equal(delegated.length, beforeCollision + 1)
assert.deepEqual(delegatedUpdates, [])
})

test("cumulative seven-package root changelog is idempotent and dry-run updates never write", () => {
const packages = stableMatrix.map(([name, version]) => ({ name, version }))
const date = new Date("2026-07-12T00:00:00Z")
const changelog = mergeRootChangelog(undefined, packages, date)
assert.equal((changelog.match(/^## @effectify\//gm) ?? []).length, 7)
assert.equal(mergeRootChangelog(changelog, packages, date), changelog)
assert.deepEqual(getRootChangelogUpdate("# stale\n", changelog, true), {
changed: true,
content: changelog,
shouldWrite: false,
shouldReportChangedFile: false,
})
})

test("creates the root release snapshot with UTC version dates and package names", () => {
assert.equal(
createRootChangelog(releasedPackages, new Date("2026-07-11T23:59:59-07:00")),
Expand Down
Loading