mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-08-23 12:15:28 -07:00
test(release): match workflow commands at command positions only
An unanchored pattern matched the command text anywhere on an executable line, so wrapping it in echo or printf satisfied the ordering assertion while the workflow ran no validation at all. Split each line on shell separators, strip control-flow prefixes, and match anchored patterns against those command positions. Cover the helper directly with cases for echo, printf, comments, and quoted mentions.
This commit is contained in:
@@ -137,13 +137,18 @@ test('prerelease workflow rejects committed notes generated for a different beta
|
||||
'bun run scripts/build-changelog.ts check-prerelease-notes',
|
||||
);
|
||||
|
||||
// Matched against executable lines only, so commenting the check out fails the
|
||||
// test rather than silently satisfying it.
|
||||
// Matched at command positions only, so commenting the check out or quoting it
|
||||
// inside an echo fails the test rather than silently satisfying it.
|
||||
const steps = jobSteps(parsedPrereleaseWorkflow, 'release');
|
||||
const checkIndex = steps.findIndex((step) =>
|
||||
stepRunsCommand(step, /changelog:check-prerelease-notes --version "\$RELEASE_VERSION"/),
|
||||
stepRunsCommand(
|
||||
step,
|
||||
/^bun run changelog:check-prerelease-notes --version "\$RELEASE_VERSION"/,
|
||||
),
|
||||
);
|
||||
const publishIndex = steps.findIndex((step) =>
|
||||
stepRunsCommand(step, /^gh release (create|edit)\b/),
|
||||
);
|
||||
const publishIndex = steps.findIndex((step) => stepRunsCommand(step, /gh release (create|edit)/));
|
||||
|
||||
assert.notEqual(checkIndex, -1);
|
||||
assert.notEqual(publishIndex, -1);
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
commandPositions,
|
||||
executableRunLines,
|
||||
stepRunsCommand,
|
||||
stepsMissingEnvDeclaration,
|
||||
templateExpressionsInRunBodies,
|
||||
} from './workflow-test-helpers';
|
||||
|
||||
const runs = (run: string): boolean => stepRunsCommand({ run }, /^bun run verify --flag "\$VALUE"/);
|
||||
|
||||
test('stepRunsCommand matches a command that actually executes', () => {
|
||||
assert.equal(runs('bun run verify --flag "$VALUE"'), true);
|
||||
assert.equal(runs('if ! bun run verify --flag "$VALUE"; then\nexit 1\nfi'), true);
|
||||
assert.equal(runs('set -e && bun run verify --flag "$VALUE"'), true);
|
||||
assert.equal(runs(' bun run verify --flag "$VALUE" || exit 1'), true);
|
||||
});
|
||||
|
||||
test('stepRunsCommand rejects commands that are only mentioned, not run', () => {
|
||||
assert.equal(runs('# bun run verify --flag "$VALUE"'), false);
|
||||
assert.equal(runs('echo \'bun run verify --flag "$VALUE"\''), false);
|
||||
assert.equal(runs("printf '%s\\n' 'bun run verify --flag \"$VALUE\"'"), false);
|
||||
assert.equal(runs('echo "run: bun run verify --flag \\"$VALUE\\"" >> notes.txt'), false);
|
||||
// A different argument list is a different command.
|
||||
assert.equal(runs('bun run verify'), false);
|
||||
});
|
||||
|
||||
test('commandPositions splits on separators and strips control-flow prefixes', () => {
|
||||
assert.deepEqual(
|
||||
commandPositions({ run: 'if gh release view "$V"; then\ngh release edit "$V"\nfi' }),
|
||||
['gh release view "$V"', 'then', 'gh release edit "$V"', 'fi'],
|
||||
);
|
||||
});
|
||||
|
||||
test('executableRunLines drops blank and comment-only lines', () => {
|
||||
assert.deepEqual(executableRunLines({ run: '\n# a comment\n \nreal command\n' }), [
|
||||
'real command',
|
||||
]);
|
||||
});
|
||||
|
||||
test('templateExpressionsInRunBodies reports every expression spelling in a run body', () => {
|
||||
const workflow = {
|
||||
jobs: {
|
||||
release: {
|
||||
steps: [
|
||||
{ name: 'Safe', env: { V: '${{ steps.version.outputs.VERSION }}' }, run: 'echo "$V"' },
|
||||
{ name: 'Dotted', run: 'echo "${{ steps.version.outputs.VERSION }}"' },
|
||||
{ name: 'Bracketed', run: 'echo "${{ steps.version.outputs[\'VERSION\'] }}"' },
|
||||
{ name: 'Github', run: 'echo "${{ github[\'ref_name\'] }}"' },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
assert.deepEqual(templateExpressionsInRunBodies(workflow), [
|
||||
'release/Dotted: ${{ steps.version.outputs.VERSION }}',
|
||||
"release/Bracketed: ${{ steps.version.outputs['VERSION'] }}",
|
||||
"release/Github: ${{ github['ref_name'] }}",
|
||||
]);
|
||||
});
|
||||
|
||||
test('stepsMissingEnvDeclaration finds shell reads with no matching env entry', () => {
|
||||
const workflow = {
|
||||
jobs: {
|
||||
release: {
|
||||
steps: [
|
||||
{ name: 'Declared', env: { TAG: 'x' }, run: 'echo "$TAG"' },
|
||||
{ name: 'Undeclared', run: 'echo "${TAG}"' },
|
||||
{ name: 'Unrelated', run: 'echo "$TAGGED"' },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
assert.deepEqual(stepsMissingEnvDeclaration(workflow, 'TAG'), ['release/Undeclared']);
|
||||
});
|
||||
@@ -50,9 +50,34 @@ export function executableRunLines(step: WorkflowStep): string[] {
|
||||
.filter((line) => line.length > 0 && !line.startsWith('#'));
|
||||
}
|
||||
|
||||
// Whether a step actually executes a command matching the pattern.
|
||||
// Leading shell keywords and operators that can precede a real command.
|
||||
const COMMAND_PREFIX = /^(?:if|elif|while|until|then|else|do|!|&&|\|\||\(|\{)\s+/;
|
||||
|
||||
// Command positions within a step's shell body: each line split on separators,
|
||||
// with control-flow prefixes stripped. A pattern anchored with ^ therefore
|
||||
// matches only where a command actually starts, so text quoted inside an
|
||||
// `echo`/`printf` argument is not mistaken for the command running.
|
||||
export function commandPositions(step: WorkflowStep): string[] {
|
||||
return executableRunLines(step).flatMap((line) =>
|
||||
line
|
||||
.split(/;|&&|\|\||(?<![0-9<>])\|(?!\|)/)
|
||||
.map((segment) => {
|
||||
let candidate = segment.trim();
|
||||
let stripped = candidate.replace(COMMAND_PREFIX, '');
|
||||
while (stripped !== candidate) {
|
||||
candidate = stripped;
|
||||
stripped = candidate.replace(COMMAND_PREFIX, '');
|
||||
}
|
||||
return candidate;
|
||||
})
|
||||
.filter(Boolean),
|
||||
);
|
||||
}
|
||||
|
||||
// Whether a step actually executes a command matching the pattern. Anchor the
|
||||
// pattern with ^ so it has to match at a command position.
|
||||
export function stepRunsCommand(step: WorkflowStep, pattern: RegExp): boolean {
|
||||
return executableRunLines(step).some((line) => pattern.test(line));
|
||||
return commandPositions(step).some((position) => pattern.test(position));
|
||||
}
|
||||
|
||||
// GitHub substitutes ${{ }} into a run script before the shell parses it, so any
|
||||
|
||||
Reference in New Issue
Block a user