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
43 changes: 42 additions & 1 deletion .github/actions/changeset-policy/check.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { execFileSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { existsSync, readdirSync, readFileSync } from "node:fs";
import process from "node:process";
import { pathToFileURL } from "node:url";

Expand Down Expand Up @@ -80,6 +80,45 @@ export const generatedExclusions = (generatedPaths, changesetPathspec) =>
(pathspec) => `:(top,exclude)${pathspec}`,
);

const FINALIZER_WORKFLOW = "/.github/workflows/npm-version-finalize.yml@";

export const validateChangelogOwnership = (file, text) => {
const workflowLines = text.split(/\r?\n/);
for (let index = 0; index < workflowLines.length; index += 1) {
const use = workflowLines[index].match(/^(\s*)uses:\s*(\S+)\s*(?:#.*)?$/);
if (!use?.[2].includes(FINALIZER_WORKFLOW)) continue;

const useIndent = use[1].length;
const jobLines = [workflowLines[index]];
for (let cursor = index + 1; cursor < workflowLines.length; cursor += 1) {
const line = workflowLines[cursor];
const trimmed = line.trim();
if (trimmed && !trimmed.startsWith("#")) {
const indent = line.length - line.trimStart().length;
if (indent < useIndent) break;
}
jobLines.push(line);
}

if (!jobLines.some((line) => /^\s+update-changelog:\s*false\s*(?:#.*)?$/.test(line))) {
fail(
`${file} delegates to npm-version-finalize.yml without ` +
"`update-changelog: false`. Changesets owns CHANGELOG.md; a post-release " +
"generator would create a second, conflicting writer.",
);
}
}
};

export const validateWorkflowChangelogOwnership = (directory = ".github/workflows") => {
if (!existsSync(directory)) return;
for (const entry of readdirSync(directory, { withFileTypes: true })) {
if (!entry.isFile() || !/\.ya?ml$/.test(entry.name)) continue;
const file = `${directory}/${entry.name}`;
validateChangelogOwnership(file, readFileSync(file, "utf8"));
}
};

const run = (command, args) =>
execFileSync(command, args, {
encoding: "utf8",
Expand Down Expand Up @@ -118,6 +157,8 @@ const main = (environment) => {
validateChangeset(file, readFileSync(file, "utf8"), packageNames);
}

validateWorkflowChangelogOwnership();

const runtimeChanged =
diff("ACMRD", lines(required(environment, "RELEASE_PATHS"))).length > 0;
if (!runtimeChanged) {
Expand Down
52 changes: 52 additions & 0 deletions .github/actions/changeset-policy/check.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { test } from "node:test";
import {
generatedExclusions,
isGeneratedVersionPullRequest,
validateChangelogOwnership,
validateChangeset,
} from "./check.mjs";

Expand Down Expand Up @@ -83,3 +84,54 @@ test("generated version exclusions stay rooted and include consumed changesets",
],
);
});

test("accepts a release finalizer when Changesets remains the changelog owner", () => {
validateChangelogOwnership(
".github/workflows/release.yml",
`jobs:
finalize:
uses: stella/.github/.github/workflows/npm-version-finalize.yml@immutable
with:
package-files: package.json
update-changelog: false
permissions:
contents: write
`,
);
});

test("rejects a release finalizer with the conflicting changelog writer enabled", () => {
assert.throws(
() =>
validateChangelogOwnership(
".github/workflows/release.yml",
`jobs:
finalize:
uses: stella/.github/.github/workflows/npm-version-finalize.yml@immutable
with:
package-files: package.json
`,
),
/second, conflicting writer/,
);
});

test("does not mistake another job's input for the finalizer setting", () => {
assert.throws(
() =>
validateChangelogOwnership(
".github/workflows/release.yml",
`jobs:
finalize:
uses: stella/.github/.github/workflows/npm-version-finalize.yml@immutable
with:
package-files: package.json
unrelated:
uses: owner/repository/.github/workflows/example.yml@immutable
with:
update-changelog: false
`,
),
/second, conflicting writer/,
);
});
Loading