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: 2 additions & 2 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -197,8 +197,8 @@ jobs:
echo "projects=$AFFECTED_RELEASE_PROJECTS" >> "$GITHUB_OUTPUT"
fi

- name: 🔧 Configure Git for PREPARE
if: ${{ steps.release.outputs.mode == 'prepare' && steps.release.outputs.has_projects == 'true' }}
- name: 🔧 Configure Git for release
if: ${{ (steps.release.outputs.mode == 'prepare' || steps.release.outputs.mode == 'finalize') && steps.release.outputs.has_projects == 'true' }}
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
Expand Down
38 changes: 38 additions & 0 deletions scripts/release-policy-contract.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,23 @@
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$/
const betaGitIdentityCondition =
"${{ (steps.release.outputs.mode == 'prepare' || steps.release.outputs.mode == 'finalize') && steps.release.outputs.has_projects == 'true' }}"
const gitIdentityCommands = [
'git config user.name "github-actions[bot]"',
'git config user.email "github-actions[bot]@users.noreply.github.com"',
]
const betaGitIdentityRuns = (source, mode, hasProjects) => {
const step = extractSteps(source).find((candidate) =>
gitIdentityCommands.every((command) => candidate.commands.includes(command)),
)
if (!step || !step.condition.includes("steps.release.outputs.has_projects == 'true'")) return false

const configuredModes = [
...step.condition.matchAll(/steps\.release\.outputs\.mode == '(prepare|finalize|suppress)'/g),
].map(([, configuredMode]) => configuredMode)
return hasProjects && configuredModes.includes(mode)
}
const terminalGates = [
{
command: 'git commit -m "chore(release): prepare beta from $SOURCE_SHA [skip release]"',
Expand Down Expand Up @@ -255,10 +272,14 @@
const active = withoutComments(source)
const steps = extractSteps(source)
const resolve = steps.find((step) => step.commands.some((command) => /mode=prepare/.test(command)))
const gitIdentity = steps.find((step) => gitIdentityCommands.every((command) => step.commands.includes(command)))
const prepare = steps.find((step) => step.commands.some((command) => betaVersionCommand.test(command)))
const finalize = steps.find((step) => step.commands.some((command) => channelPublishCommand("beta").test(command)))

if (!/push:\s*\n\s*branches: \[master\]/.test(active)) violations.push("beta trigger")
if (!gitIdentity || gitIdentity.condition !== betaGitIdentityCondition) {
violations.push("beta PREPARE and FINALIZE Git identity")
}
if (!/expected_sha:\s*\n\s*description:[^\n]*\n\s*required: false/.test(active)) {
violations.push("beta expected SHA input")
}
Expand Down Expand Up @@ -520,10 +541,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 544 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 547 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 @@ -585,6 +606,18 @@
assert.deepEqual(betaViolations(workflows.beta), [])
})

test("beta Git identity covers project PREPARE and FINALIZE but skips suppression and empty selections", () => {
for (const [mode, hasProjects, expected] of [
["prepare", true, true],
["finalize", true, true],
["suppress", true, false],
["prepare", false, false],
["finalize", false, false],
]) {
assert.equal(betaGitIdentityRuns(workflows.beta, mode, hasProjects), expected, `${mode}/${hasProjects}`)
}
})

test("stable validates current master and selected projects before every release mutation", () => {
assert.deepEqual(stableViolations(workflows.stable), [])
})
Expand Down Expand Up @@ -779,6 +812,11 @@
const policy = { ...workflows, docs: readme }

for (const [name, before, after] of [
[
"skip Git identity in FINALIZE",
"(steps.release.outputs.mode == 'prepare' || steps.release.outputs.mode == 'finalize')",
"steps.release.outputs.mode == 'prepare'",
],
["accept short expected SHA", "^[0-9a-f]{40}$", "^[0-9a-f]{7,40}$"],
["weaken checkout equality", 'test "$HEAD_SHA" = "$EXPECTED_SHA"', 'test "$HEAD_SHA" != "$EXPECTED_SHA"'],
["weaken remote equality", 'test "$REMOTE_SHA" = "$EXPECTED_SHA"', 'test "$REMOTE_SHA" != "$EXPECTED_SHA"'],
Expand Down
Loading