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
10 changes: 5 additions & 5 deletions .github/workflows/release-stable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,15 +112,15 @@ jobs:
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.13-beta.0|0.5.13' > "$RECORDS"
while IFS='|' read -r NAME PATH OLD NEW; do node -e 'const fs=require("node:fs");const [path,name,version]=process.argv.slice(1);const value=JSON.parse(fs.readFileSync(path,"utf8"));if(!value||typeof value!=="object"||Array.isArray(value)||typeof value.name!=="string"||typeof value.version!=="string"||value.name!==name||value.version!==version)process.exit(1)' "$PATH" "$NAME" "$OLD" || { echo "::error::unauthorized source $NAME"; exit 1; }; done < "$RECORDS"
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
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"
cmp -s "$EXPECTED_PATHS" "$ACTUAL" || { echo '::error::unexpected PREPARE paths'; diff -u "$EXPECTED_PATHS" "$ACTUAL" || true; exit 1; }
while IFS='|' read -r NAME PATH OLD NEW; do node -e 'const fs=require("node:fs");const [path,name,version]=process.argv.slice(1);const value=JSON.parse(fs.readFileSync(path,"utf8"));if(!value||typeof value!=="object"||Array.isArray(value)||typeof value.name!=="string"||typeof value.version!=="string"||value.name!==name||value.version!==version)process.exit(1)' "$PATH" "$NAME" "$NEW" || { echo "::error::wrong target $NAME"; exit 1; }; done < "$RECORDS"
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" "$NEW"); then :; else STATUS=$?; if [ "$STATUS" = 1 ]; then echo "::error::target manifest identity mismatch for $NAME: $DETAIL"; else echo "::error::target manifest execution or parse failed for $NAME"; fi; exit 1; fi; done < "$RECORDS"
git add --pathspec-from-file="$EXPECTED_PATHS"
git diff --cached --name-only --no-renames | sort > /tmp/stable-staged
cmp -s "$EXPECTED_PATHS" /tmp/stable-staged || { echo '::error::staged path contamination'; exit 1; }
Expand All @@ -147,8 +147,8 @@ jobs:
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.13' > "$RECORDS"
: > /tmp/missing-projects; : > /tmp/missing-tags; : > /tmp/missing-releases
while IFS='|' read -r NAME PATH VERSION; do
node -e 'const fs=require("node:fs");const [path,name,version]=process.argv.slice(1);const value=JSON.parse(fs.readFileSync(path,"utf8"));if(!value||typeof value!=="object"||Array.isArray(value)||typeof value.name!=="string"||typeof value.version!=="string"||value.name!==name||value.version!==version)process.exit(1)' "$PATH" "$NAME" "$VERSION" || { echo '::error::merged stable matrix mismatch'; exit 1; }
while IFS='|' read -r NAME MANIFEST_PATH VERSION; 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" "$VERSION"); then :; else STATUS=$?; if [ "$STATUS" = 1 ]; then echo "::error::merged manifest identity mismatch for $NAME: $DETAIL"; else echo "::error::merged manifest execution or parse failed for $NAME"; fi; exit 1; fi
TAG="$NAME@$VERSION"; VERSIONS=$(npm view "$NAME" versions --json); printf '%s' "$VERSIONS" | node -e 'const fs=require("node:fs");const value=JSON.parse(fs.readFileSync(0,"utf8"));if(!(typeof value==="string"||Array.isArray(value)&&value.every(item=>typeof item==="string")))process.exit(1)'
LATEST_JSON=$(npm view "$NAME" dist-tags.latest --json); LATEST=$(printf '%s' "$LATEST_JSON" | node -e 'const fs=require("node:fs");const value=JSON.parse(fs.readFileSync(0,"utf8"));if(typeof value!=="string")process.exit(1);process.stdout.write(value)')
if printf '%s' "$VERSIONS" | node -e 'const fs=require("node:fs");const version=process.argv[1];const value=JSON.parse(fs.readFileSync(0,"utf8"));if(!(typeof value==="string"||Array.isArray(value)&&value.every(item=>typeof item==="string")))process.exit(1);process.exit((Array.isArray(value)?value.includes(version):value===version)?0:1)' "$VERSION"; then test "$LATEST" = "$VERSION" || { echo '::error::existing stable has divergent latest'; exit 1; }; else printf '%s\n' "${NAME#@effectify/}" >> /tmp/missing-projects; fi
Expand All @@ -162,7 +162,7 @@ jobs:
while IFS= read -r TAG; do [ -n "$TAG" ] && gh release create "$TAG" --verify-tag --generate-notes; done < /tmp/missing-releases
MISSING=$(paste -sd, /tmp/missing-projects); if [ -n "$MISSING" ]; then PROJECTS="$MISSING"; pnpm nx release publish "--projects=$PROJECTS"; fi
MAX_NPM_READS=6; for ATTEMPT in $(seq 1 "$MAX_NPM_READS"); do
REMAINING=0; while IFS='|' read -r NAME PATH VERSION; do V=$(npm view "$NAME" versions --json) || { REMAINING=$((REMAINING+1)); continue; }; L_JSON=$(npm view "$NAME" dist-tags.latest --json) || { REMAINING=$((REMAINING+1)); continue; }; L=$(printf '%s' "$L_JSON" | node -e 'const fs=require("node:fs");const value=JSON.parse(fs.readFileSync(0,"utf8"));if(typeof value!=="string")process.exit(1);process.stdout.write(value)') || { REMAINING=$((REMAINING+1)); continue; }; printf '%s' "$V" | node -e 'const fs=require("node:fs");const version=process.argv[1];const value=JSON.parse(fs.readFileSync(0,"utf8"));if(!(typeof value==="string"||Array.isArray(value)&&value.every(item=>typeof item==="string")))process.exit(1);process.exit((Array.isArray(value)?value.includes(version):value===version)?0:1)' "$VERSION" && [ "$L" = "$VERSION" ] || REMAINING=$((REMAINING+1)); done < "$RECORDS"
REMAINING=0; while IFS='|' read -r NAME MANIFEST_PATH VERSION; do V=$(npm view "$NAME" versions --json) || { REMAINING=$((REMAINING+1)); continue; }; L_JSON=$(npm view "$NAME" dist-tags.latest --json) || { REMAINING=$((REMAINING+1)); continue; }; L=$(printf '%s' "$L_JSON" | node -e 'const fs=require("node:fs");const value=JSON.parse(fs.readFileSync(0,"utf8"));if(typeof value!=="string")process.exit(1);process.stdout.write(value)') || { REMAINING=$((REMAINING+1)); continue; }; printf '%s' "$V" | node -e 'const fs=require("node:fs");const version=process.argv[1];const value=JSON.parse(fs.readFileSync(0,"utf8"));if(!(typeof value==="string"||Array.isArray(value)&&value.every(item=>typeof item==="string")))process.exit(1);process.exit((Array.isArray(value)?value.includes(version):value===version)?0:1)' "$VERSION" && [ "$L" = "$VERSION" ] || REMAINING=$((REMAINING+1)); done < "$RECORDS"
[ "$REMAINING" = 0 ] && break; [ "$ATTEMPT" = "$MAX_NPM_READS" ] && { echo "::error::npm did not converge: $REMAINING"; exit 1; }; sleep 10
done
- name: 📊 Stable summary
Expand Down
36 changes: 36 additions & 0 deletions scripts/release-policy-contract.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,7 @@
"manifest exact identity",
].includes(name),
)
const manifestCommand = /node -e '[^\n]*fs\.readFileSync\(path,"utf8"\)[^\n]*' "\$MANIFEST_PATH" "\$NAME"/
const releaseValidationCommands = commandEntries(finalizeBody)
.map(({ command }) => command)
.filter((command) => /printf '%s' "\$RELEASE" \| node -e /.test(command))
Expand All @@ -653,6 +654,23 @@
pattern.lastIndex = 0
if (!pattern.test(body)) violations.push(`stable ${phase} ${name}`)
}
if (/\bread\s+-r\s+[^\n;]*\bPATH\b/.test(body)) violations.push(`stable ${phase} reserved PATH shadowing`)
const manifestCommands = commandEntries(body)
.map(({ command }) => command)
.filter((command) => /fs\.readFileSync\(path,"utf8"\)/.test(command))
if (manifestCommands.length === 0 || manifestCommands.some((command) => !manifestCommand.test(command))) {
violations.push(`stable ${phase} MANIFEST_PATH manifest command`)
}
if (!/process\.exit\(2\)/.test(body) || !/manifest execution or parse failed/.test(body)) {
violations.push(`stable ${phase} manifest execution diagnostic`)
}
if (!/manifest identity mismatch/.test(body)) violations.push(`stable ${phase} manifest identity diagnostic`)
if (!/actual=\$\{JSON\.stringify\(actual\)\}/.test(body)) {
violations.push(`stable ${phase} manifest actual identity detail`)
}
if (!/expected=\$\{JSON\.stringify\(\{name,version\}\)\}/.test(body)) {
violations.push(`stable ${phase} manifest expected identity detail`)
}
}
if (!prepare || !/mode == 'prepare'/.test(prepare.condition)) violations.push("stable PREPARE isolation")
if (
Expand All @@ -678,10 +696,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 699 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 702 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 @@ -924,6 +942,24 @@
]) {
const changed = mutateStep(policy.stable, stepName, /JSON\.parse/g, "JSON.parseSafe")
assert.ok(stableViolations(changed).includes(`stable ${phase} Node JSON type validation`))
const withoutActual = mutateStep(policy.stable, stepName, /actual=\$\{JSON\.stringify\(actual\)\} /g, "")
assert.ok(stableViolations(withoutActual).includes(`stable ${phase} manifest actual identity detail`))
const withoutExpected = mutateStep(
policy.stable,
stepName,
/expected=\$\{JSON\.stringify\(\{name,version\}\)\}/g,
"",
)
assert.ok(stableViolations(withoutExpected).includes(`stable ${phase} manifest expected identity detail`))
}
for (const [phase, stepName] of [
["PREPARE", "PREPARE protected stable"],
["FINALIZE", "FINALIZE exact stable artifacts"],
]) {
const shadowed = mutateStep(policy.stable, stepName, /read -r NAME MANIFEST_PATH/, "read -r NAME PATH")
assert.ok(stableViolations(shadowed).includes(`stable ${phase} reserved PATH shadowing`))
const wrongArgument = mutateStep(policy.stable, stepName, /"\$MANIFEST_PATH" "\$NAME"/, '"$PATH" "$NAME"')
assert.ok(stableViolations(wrongArgument).includes(`stable ${phase} MANIFEST_PATH manifest command`))
}
const literalRelease = mutateStep(
policy.stable,
Expand Down
Loading