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/training-agent-storyboards.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Training Agent Storyboards

on:
pull_request:
branches: [main, '3.1.x', '3.0.x']
branches: [main, '3.2.x', '3.1.x', '3.0.x']
paths:
- 'server/src/training-agent/**'
- 'server/src/services/hosted-compliance-version.ts'
Expand All @@ -22,7 +22,7 @@ on:
- 'dist/schemas/**'
- '.github/workflows/training-agent-storyboards.yml'
push:
branches: [main, '3.1.x', '3.0.x']
branches: [main, '3.2.x', '3.1.x', '3.0.x']
paths:
- 'server/src/training-agent/**'
- 'server/src/services/hosted-compliance-version.ts'
Expand Down
36 changes: 29 additions & 7 deletions scripts/check-changeset-protocol-scope.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,27 @@ function isProtocolScopedPath(filePath) {
return PROTOCOL_SCOPED_PATHS.some(pattern => pattern.test(normalized));
}

function hasProtocolScopedChanges(changes) {
return changes.some(change => (change.paths || []).some(isProtocolScopedPath));
function isStoryboardBranchRegistrationOnly(headContent, baseContent) {
const oldBranches = "branches: [main, '3.1.x', '3.0.x']";
const newBranches = "branches: [main, '3.2.x', '3.1.x', '3.0.x']";
return baseContent.split(oldBranches).length === 3
&& headContent === baseContent.replaceAll(oldBranches, newBranches);
}

function isProtocolScopedChange(change, readFileAtHead, readFileAtBase) {
return (change.paths || []).some(filePath => {
if (!isProtocolScopedPath(filePath)) return false;
if (normalizePath(filePath) === '.github/workflows/training-agent-storyboards.yml'
&& change.status === 'M'
&& isStoryboardBranchRegistrationOnly(
readFileAtHead(filePath), readFileAtBase(filePath)
)) return false;
return true;
});
}

function hasProtocolScopedChanges(changes, readFileAtHead = () => '', readFileAtBase = () => '') {
return changes.some(change => isProtocolScopedChange(change, readFileAtHead, readFileAtBase));
}

function isChangesetMaintenancePath(filePath) {
Expand Down Expand Up @@ -221,10 +240,8 @@ function findChangesetProtocolScopeViolations(changes, readFileAtHead, readFileA
const protocolScopedFiles = [];

for (const change of changes) {
for (const filePath of change.paths || []) {
if (isProtocolScopedPath(filePath)) {
protocolScopedFiles.push(filePath);
}
if (isProtocolScopedChange(change, readFileAtHead, readFileAtBase)) {
protocolScopedFiles.push(...change.paths.filter(isProtocolScopedPath));
}

const headPath = changedPathForHead(change);
Expand Down Expand Up @@ -313,7 +330,11 @@ function run(argv = process.argv.slice(2)) {
}

if (argv.includes('--has-protocol-scoped-changes')) {
const hasProtocol = hasProtocolScopedChanges(changes);
const hasProtocol = hasProtocolScopedChanges(
changes,
readFileAtHead,
filePath => readFileAtRef(baseRef, filePath)
);
if (hasProtocol) {
console.log('Protocol-scoped changes detected.');
return 0;
Expand Down Expand Up @@ -347,6 +368,7 @@ module.exports = {
findChangesetProtocolScopeViolations,
formatViolationMessage,
hasProtocolScopedChanges,
isStoryboardBranchRegistrationOnly,
isChangesetBumpDowngradeOrRemoval,
isChangesetBumpEscalation,
isChangesetClassificationMaintenance,
Expand Down
51 changes: 51 additions & 0 deletions tests/changeset-protocol-scope.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const {
isChangesetEditOnlyMaintenance,
isChangesetStatusExemptMaintenance,
isProtocolScopedPath,
isStoryboardBranchRegistrationOnly,
parseNameStatus,
} = require('../scripts/check-changeset-protocol-scope.cjs');

Expand Down Expand Up @@ -78,6 +79,56 @@ assert.strictEqual(isProtocolScopedPath('.changeset/billing-fix.md'), false);
assert.strictEqual(isProtocolScopedPath('.github/workflows/release.yml'), false);
assert.strictEqual(isProtocolScopedPath('.github/workflows/training-agent-storyboards.yml'), true);
assert.strictEqual(isProtocolScopedPath('scripts/build-protocol-tarball.cjs'), true);
const storyboardWorkflowPath = '.github/workflows/training-agent-storyboards.yml';
const storyboardWorkflowBase = `pull_request:\n branches: [main, '3.1.x', '3.0.x']\npush:\n branches: [main, '3.1.x', '3.0.x']\njob: run\n`;
const storyboardWorkflowRegistered = storyboardWorkflowBase.replaceAll(
"branches: [main, '3.1.x', '3.0.x']",
"branches: [main, '3.2.x', '3.1.x', '3.0.x']"
);
const storyboardChange = [{ status: 'M', paths: [storyboardWorkflowPath] }];
assert.strictEqual(
isStoryboardBranchRegistrationOnly(storyboardWorkflowRegistered, storyboardWorkflowBase),
true
);
assert.strictEqual(
hasProtocolScopedChanges(
storyboardChange,
readFiles({ [storyboardWorkflowPath]: storyboardWorkflowRegistered }),
readFiles({ [storyboardWorkflowPath]: storyboardWorkflowBase })
),
false,
'Registering the exact two 3.2.x CI triggers does not release the protocol'
);
assert.strictEqual(
hasProtocolScopedChanges(
storyboardChange,
readFiles({ [storyboardWorkflowPath]: `${storyboardWorkflowRegistered}another job\n` }),
readFiles({ [storyboardWorkflowPath]: storyboardWorkflowBase })
),
true,
'Changes to storyboard jobs remain protocol scoped'
);
assert.strictEqual(
hasProtocolScopedChanges(
storyboardChange,
readFiles({ [storyboardWorkflowPath]: storyboardWorkflowRegistered.replace('push:\n branches: [main, \'3.2.x\',', 'push:\n branches: [main,') }),
readFiles({ [storyboardWorkflowPath]: storyboardWorkflowBase })
),
true,
'Both pull request and push trigger lists must be registered together'
);
assert.strictEqual(
findChangesetProtocolScopeViolations(
[...storyboardChange, { status: 'A', paths: ['.changeset/empty-workflow.md'] }],
readFiles({
[storyboardWorkflowPath]: storyboardWorkflowRegistered,
'.changeset/empty-workflow.md': emptyChangeset,
}),
readFiles({ [storyboardWorkflowPath]: storyboardWorkflowBase })
).length,
1,
'Branch registration cannot justify an artificial protocol changeset'
);
assert.strictEqual(
hasProtocolScopedChanges([
{ status: 'M', paths: ['.github/workflows/release.yml'] },
Expand Down
9 changes: 8 additions & 1 deletion tests/docs-nav-validation.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,14 @@ test('OpenAPI navigation uses release-pinned public sources', () => {
function stableDocsRef() {
if (process.env.STABLE_DOCS_REF) return process.env.STABLE_DOCS_REF;
const line = /^(\d+\.\d+)$/.exec(defaultVersion)?.[1];
return line ? `origin/${line}.x` : null;
if (!line) return null;
const branch = `${line}.x`;
// On the stable maintenance branch itself, or a pull request into it, the
// branch is its own release surface. Comparing against its pre-change tip
// would reject every navigation change there, including the GA flip that
// makes it the stable surface.
if ((process.env.GITHUB_BASE_REF || process.env.GITHUB_REF_NAME) === branch) return 'HEAD';
return `origin/${branch}`;
}

test('default navigation matches the stable release branch surface', () => {
Expand Down
Loading