Files
msd-core/tests/adr-218-release-version-validation.test.cjs
Tom Boucher 5fa4dcd78c fix: recover silently-excluded test dirs + test-architecture audit hardening (#1195)
* fix: recurse test discovery so subdir test suites actually run

scripts/run-tests.cjs discovered tests with a flat readdirSync(testDir),
silently excluding tests/observability/ (4 files), tests/dispatch/ (1) and
tests/installer-migrations/ (1) — 94 passing tests — from `npm test` and all
CI lanes. Walk the tree recursively (relative subpaths preserved), classify
suites by basename, and add a fail-on-zero-executed guard for suite/default
runs (escape hatch GSD_ALLOW_EMPTY_SUITE=1) while preserving the empty
--files/--files-from path the CI inert lane relies on.

Unit suite 735 -> 741 files; surfaces ADR-227's observability/dispatch seam.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test: retire 5 verified-worthless tests

Adversarial verification confirmed these 5 prove nothing — their coverage is
provided more strictly elsewhere:
- enh-2790 'has a name: field' spot-checks (command-contract enforces /^gsd[:-]/)
- command-routing-hub duplicate construct + duplicate ERROR_KINDS assertions
- no-cjs-sdk-handsync-tooling (guarded files that never existed on main; bug-190
  covers the real retired SDK artifacts)
- runtime-artifact-layout cline edge case (subsumed by the explicit-global test
  and bug-782-cline-skills-emission)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test: add ADR-218 release version-validation coverage

ADR-218 (reject leading-zero versions like 1.01.0; npm duplicate pre-check) had
zero tests — the logic lived only in release.yml bash. Add a test that extracts
the actual rejection regexes from the workflow and exercises them against a
boundary table (leading-zero/malformed rejected, valid accepted) plus structural
wiring assertions. Goes red if the regex is reverted to [0-9]+.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test: redesign weak tests into behavioral, deterministic assertions

Per the ADR test audit, rewrite 27 weak test files (test-only, no source
changes) so each can go red for the defect it guards:
- kill pass-always assert.ok(true) placeholders (research-cli, worktree-baseref,
  bug-260 security guard, eslint-rules x24, clusters '|| true')
- replace source-text grep with behavioral calls (install Kilo, sh-hook-paths,
  plan-review-convergence) and add a repo-layout governance test
- de-flake real-clock/Math.random coupling (phase last_updated, bug-3707 mtime,
  context-utilization property, feat-3594)
- fix independence/shared-state violations (bug-492 singleton, issue-844 tmpRoot,
  core reapStaleTempFiles, active-workstream TTY, feat-488 GSD_HOME)
- strengthen property/shape-only tests (research-provider/store classification +
  collision) and unconditional plugin.json schema validation (issue-766)

Verified: all 28 files run together 1220 pass / 0 fail / 1 skip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore: add no-tautological-assert lint rule, error in test suite

New custom ESLint rule (eslint-rules/no-tautological-assert.cjs) bans asserts
that can never fail: assert(true)/assert.ok(<always-truthy literal>),
'cond || true' inside an assert, and equality asserts comparing two identical
literals. Wired as error on tests/**; full sweep confirmed zero existing
violations so the suite stays green. Prevents the placeholder-assert regressions
the audit redesigns just removed. RuleTester coverage added (6 valid, 8 invalid).

Note: no-only-tests was already enforced via eslint-plugin-no-only-tests, so no
duplicate rule was added.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore: gate new allow-test-rule exemptions to require an issue ref

ADR-456 requires any allow-test-rule exemption added after the ADR to carry a
tracking issue number, but nothing enforced it. New ratchet gate
(scripts/lint-allow-test-rule-refs.cjs, wired into lint:ci) fails when a NEW
allow-test-rule comment lacks a #NNN/URL reference; the 323 existing untracked
exemptions are grandfathered in an allowlist that ratchets down as they gain
refs. Red-green verified (novel untracked offender fails; compliant passes).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: add ADR test-audit evidence report (#1192)

Full risk-first qa-test-architect audit of the ADR portfolio (37 ADRs + 4
platform lenses, adversarial verification of retire verdicts) that drove the
P0 discovery fix, ADR-218 coverage, 5 retires, 27 redesigns, and the two new
lint gates. Filed as point-in-time evidence under docs/issueevidence/, named
for tracking issue #1192.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test: replace pre-existing raw NUL byte with escape in feat-3594 fixture

feat-3594's null-byte parser fixture contained a literal NUL byte (pre-existing
on next at b10e5681 — confirmed: base blob has 1 NUL, this fix has 0), which
made git treat the file as binary and would break grep/editors. Switch to the
\x00 escape; the runtime string value (a real NUL in the parser input) is
unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test: address adversarial-review findings

Codex adversarial pass over the branch:
- capability-registry drift test no longer mutates the committed generated
  capability-registry.cjs in place (concurrency hazard) — uses in-memory
  checkPipeline comparison instead.
- allow-test-rule ratchet now detects exemptions in ALL comment forms (block
  /* */ too, matching no-source-grep) so a block comment can't bypass it;
  one newly-surfaced pre-existing offender grandfathered (323->324).
- install.test Kilo case asserts on what install(false,'kilo') actually writes
  rather than manually calling configureKiloPermissions (masked the call site).
- issue-766 drops the undeclared transitive ajv dep for explicit structural
  assertions from the schema fixture.
- adr-218 test notes the hotfix leading-zero gap is tracked in #1186.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: address code-review findings (subdir discovery, rule + test gaps)

xhigh code review surfaced 15 confirmed issues, all fixed:
- run-tests.cjs --files now resolves subdir tests by bare basename + handles
  Windows backslash paths (ambiguous basenames error clearly).
- affected-tests-lib.cjs listTestFiles made recursive — the targeted CI lane was
  silently dropping changed subdir tests (same false-green class the audit fixed).
- no-tautological-assert now catches 'true || cond' and empty []/{}  equality.
- verify-test-quality: restore provenance-classification coverage, tighten the
  writeFile circular-detection check, guard the module-level file read.
- sh-hook-paths: cover the global-install .sh delegation branch (#2045 guard).
- active-workstream null-guard runs deterministically (no longer skipped on TTY).
- adr-218 structural guards tightened (major/minor leading-zero; needs: membership).
- repo-layout AGENTS.md guard no longer false-alarms on equivalent refactors.
- cross-ai ordering guard fails red when the step is missing.
- issue-766 parses required fields from the schema fixture (auto-enforced).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test: stub USERPROFILE alongside HOME in feat-488 (Windows parity)

The feat-488 redesign stubbed process.env.HOME but not USERPROFILE; os.homedir()
resolves from USERPROFILE on Windows, so the home stub was not hermetic there —
caught by windows-test-parity-guard (stubsHomeNoUserProfile). Save/set/restore
USERPROFILE symmetrically with HOME (delete-if-originally-undefined).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore: reconcile allow-test-rule allowlist after rebase onto next

Rebasing onto current next pulled in merged PR #1170, which added
inventory-headings-countfree.test.cjs (a baseline allow-test-rule exemption) and
deleted inventory-counts.test.cjs. Grandfather the former and prune the latter so
the ratchet matches the merged tree. No new debt from this PR.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 23:35:08 -04:00

468 lines
18 KiB
JavaScript

'use strict';
/**
* ADR-218 regression guard: release-workflow version validation.
*
* (A) Behavioral regex coverage — extracts the actual leading-zero-rejection
* regex strings from .github/workflows/release.yml at test time, compiles
* them as RegExp, and asserts boundary behavior. If someone weakens the
* regex (e.g. back to [0-9]+), these assertions go RED.
*
* (B) Structural wiring assertions — confirms the validate-version job exists,
* the npm duplicate-version pre-check step is present, and that all
* downstream publish/create jobs declare `needs: validate-version`.
*/
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const WORKFLOW_PATH = process.env.ADR218_WORKFLOW_PATH
|| path.join(__dirname, '..', '.github', 'workflows', 'release.yml');
function loadWorkflow() {
assert.ok(
fs.existsSync(WORKFLOW_PATH),
`release.yml not found at ${WORKFLOW_PATH} — file moved or deleted?`
);
return fs.readFileSync(WORKFLOW_PATH, 'utf8');
}
/**
* Extract all grep -qE '...' or grep -qE "..." patterns from a bash block.
* Returns an array of raw regex strings (the content inside the quotes).
*/
function extractGrepPatterns(text) {
// Match: grep -qE '...' or grep -qE "..."
const re = /grep\s+-qE\s+(?:'([^']+)'|"([^"]+)")/g;
const found = [];
let m;
while ((m = re.exec(text)) !== null) {
found.push(m[1] !== undefined ? m[1] : m[2]);
}
return found;
}
/**
* Parse the `needs:` list of a GitHub Actions job.
*
* @param {string} src - Full YAML source text.
* @param {number} jobIdx - Index of the '\n <job-name>:\n' match in src.
* @returns {string[] | null} Array of job-name strings listed under `needs:`,
* or null if no `needs:` key was found for the job.
*
* Strategy: slice from jobIdx to the next top-level (two-space-indented) job
* header, then scan that segment for the `needs:` key. The key accepts two
* YAML forms:
* needs: validate-version (scalar)
* needs: [validate-version, install-smoke] (flow sequence)
* needs: (block sequence)
* - validate-version
* - install-smoke
*
* This deliberately only inspects the `needs:` line and its immediate list
* items — it will NOT match "validate-version" appearing later in a step
* expression such as `${{ needs.validate-version.outputs.branch }}`.
*/
function parseJobNeeds(src, jobIdx) {
// Isolate the job's YAML block: from the job header to the next top-level
// job (same two-space indentation) or end of file.
const rest = src.slice(jobIdx + 1); // skip the leading newline of the match
// Next top-level job starts with "\n <word>:\n" at column 2
const nextJobMatch = rest.match(/\n {2}[a-z][a-z0-9_-]*:\n/);
const segment = nextJobMatch
? rest.slice(0, rest.indexOf(nextJobMatch[0]) + 1)
: rest;
// Match the `needs:` line within the segment
const needsLineMatch = segment.match(/^ {4}needs:\s*(.*)$/m);
if (!needsLineMatch) return null;
const inline = needsLineMatch[1].trim();
if (inline === '') {
// Block sequence form: lines following `needs:` at deeper indent
// needs:
// - job-one
// - job-two
const blockItems = [];
const blockRe = /^ {6}- ([a-z][a-z0-9_-]*)$/gm;
// Only scan text after the `needs:` line
const afterNeeds = segment.slice(needsLineMatch.index + needsLineMatch[0].length);
let bm;
while ((bm = blockRe.exec(afterNeeds)) !== null) {
blockItems.push(bm[1]);
}
return blockItems.length > 0 ? blockItems : null;
}
if (inline.startsWith('[')) {
// Flow sequence form: needs: [job-one, job-two]
const inner = inline.replace(/^\[|\]$/g, '');
return inner.split(',').map(s => s.trim()).filter(Boolean);
}
// Scalar form: needs: single-job
return [inline];
}
// ---------------------------------------------------------------------------
// (A) Behavioral regex tests
// ---------------------------------------------------------------------------
describe('ADR-218 — leading-zero rejection regex (behavioral)', () => {
test('release.yml contains at least two leading-zero-aware grep patterns', () => {
const src = loadWorkflow();
const patterns = extractGrepPatterns(src);
assert.ok(
patterns.length >= 2,
`Expected at least 2 grep -qE patterns in release.yml, found ${patterns.length}: ${JSON.stringify(patterns)}`
);
});
test('minor/major pattern (X.Y.0) rejects leading zeros and accepts valid versions', () => {
const src = loadWorkflow();
const patterns = extractGrepPatterns(src);
// The minor/major pattern must match X.Y.0 (not hotfix) and must be the
// one that guards the release branch decision. We identify it as the first
// pattern that matches `1.0.0` AND `0.1.0` AND `10.20.0`.
const minorMajorPatterns = patterns.filter(p => {
const re = new RegExp(p);
return re.test('1.0.0') && re.test('0.1.0') && re.test('10.20.0');
});
assert.ok(
minorMajorPatterns.length >= 1,
`Could not locate the minor/major (X.Y.0) grep pattern in release.yml.\n` +
`Extracted patterns: ${JSON.stringify(patterns)}\n` +
`If the pattern was relocated or renamed, update this test to match.`
);
const re = new RegExp(minorMajorPatterns[0]);
// Boundary table: REJECTED (leading zeros or malformed)
const shouldReject = [
'1.01.0', // leading zero in minor
'01.0.0', // leading zero in major
'1.1.01', // leading zero in patch (also not X.Y.0 form)
'00.0.0', // double leading zero in major
'1.00.0', // double leading zero in minor
];
for (const v of shouldReject) {
assert.equal(
re.test(v), false,
`Version "${v}" should be REJECTED by the minor/major pattern but was accepted.\n` +
`Pattern: ${minorMajorPatterns[0]}\n` +
`This is the ADR-218 leading-zero regression. Restore (0|[1-9][0-9]*) grouping.`
);
}
// Boundary table: ACCEPTED (valid semver, no leading zeros)
const shouldAccept = [
'1.0.0',
'0.1.0',
'10.20.0',
'1.2.0',
'0.0.0',
];
for (const v of shouldAccept) {
assert.equal(
re.test(v), true,
`Version "${v}" should be ACCEPTED by the minor/major pattern but was rejected.\n` +
`Pattern: ${minorMajorPatterns[0]}`
);
}
});
test('major-only sub-check (X.0.0) correctly classifies major releases', () => {
const src = loadWorkflow();
const patterns = extractGrepPatterns(src);
// The major-only pattern matches X.0.0 exactly (not X.Y.0 with Y>0).
// We identify it as the pattern that matches `1.0.0` but NOT `1.1.0`.
const majorOnlyPatterns = patterns.filter(p => {
const re = new RegExp(p);
return re.test('1.0.0') && !re.test('1.1.0');
});
assert.ok(
majorOnlyPatterns.length >= 1,
`Could not locate the major-only (X.0.0) grep pattern in release.yml.\n` +
`Extracted patterns: ${JSON.stringify(patterns)}\n` +
`ADR-218 requires IS_MAJOR detection to also forbid leading zeros.`
);
const re = new RegExp(majorOnlyPatterns[0]);
// REJECTED: leading zeros in the major segment
const shouldReject = [
'01.0.0', // leading zero in major
'00.0.0', // double leading zero
];
for (const v of shouldReject) {
assert.equal(
re.test(v), false,
`Version "${v}" should be REJECTED by the major-only pattern but was accepted.\n` +
`Pattern: ${majorOnlyPatterns[0]}\n` +
`ADR-218: IS_MAJOR check must also use (0|[1-9][0-9]*) grouping.`
);
}
// ACCEPTED: valid major versions
const shouldAccept = [
'1.0.0',
'10.0.0',
'0.0.0',
];
for (const v of shouldAccept) {
assert.equal(
re.test(v), true,
`Version "${v}" should be ACCEPTED by the major-only pattern but was rejected.\n` +
`Pattern: ${majorOnlyPatterns[0]}`
);
}
});
test('hotfix pattern (X.Y.Z, Z>0) is present and uses digit anchors', () => {
const src = loadWorkflow();
const patterns = extractGrepPatterns(src);
// The hotfix pattern matches X.Y.Z where Z > 0, e.g. `1.2.3`.
// Identify it as the pattern matching `1.2.3` but NOT `1.2.0`.
const hotfixPatterns = patterns.filter(p => {
const re = new RegExp(p);
return re.test('1.2.3') && !re.test('1.2.0');
});
assert.ok(
hotfixPatterns.length >= 1,
`Could not locate the hotfix (X.Y.Z, Z>0) grep pattern in release.yml.\n` +
`Extracted patterns: ${JSON.stringify(patterns)}\n` +
`Expected a pattern matching 1.2.3 but not 1.2.0.`
);
const re = new RegExp(hotfixPatterns[0]);
// Sanity: valid hotfix versions accepted
assert.equal(re.test('1.2.3'), true, 'Hotfix pattern must accept 1.2.3');
assert.equal(re.test('1.0.1'), true, 'Hotfix pattern must accept 1.0.1');
assert.equal(re.test('10.20.30'), true, 'Hotfix pattern must accept 10.20.30');
// Patch = 0 must be rejected (that is the minor/major form)
assert.equal(re.test('1.2.0'), false, 'Hotfix pattern must not match X.Y.0 (Z must be >0)');
// NOTE: The hotfix regex in release.yml currently permits leading zeros on
// the major and minor segments (e.g. `1.01.3` and `01.2.3` both pass).
// This is a known gap tracked in issue #1186. The assertions below are
// intentionally absent for those cases: this test documents CURRENT
// behavior, not ideal behavior. Fix #1186 will harden the pattern and
// add leading-zero rejection assertions here.
});
test('extracted patterns use strict leading-zero guard, not the old [0-9]+ form', () => {
const src = loadWorkflow();
const patterns = extractGrepPatterns(src);
// ADR-218 Decision #1: the X.Y.0 (minor/major) pattern and the X.0.0
// (major-only) pattern MUST guard their major and minor segments with
// (0|[1-9][0-9]*), not the old bare [0-9]+ form.
//
// The hotfix pattern contains "[1-9][0-9]*" for its PATCH segment, so
// testing .some(p => p.includes('[1-9][0-9]*')) would still pass even if
// the major/minor patterns were weakened back to [0-9]+. We therefore
// assert specifically against the patterns that guard major/minor segments.
// Identify the minor/major pattern: matches X.Y.0 (both 1.0.0 and 1.1.0)
const minorMajorPatterns = patterns.filter(p => {
const re = new RegExp(p);
return re.test('1.0.0') && re.test('1.1.0') && re.test('10.20.0');
});
assert.ok(
minorMajorPatterns.length >= 1,
`Could not locate the minor/major (X.Y.0) grep pattern in release.yml.\n` +
`Extracted patterns: ${JSON.stringify(patterns)}`
);
// The minor/major pattern must contain (0|[1-9][0-9]*) to guard BOTH the
// major AND minor segments. A pattern that uses [0-9]+ on those segments
// would allow "01.0.0" or "1.01.0" — the ADR-218 regression.
assert.ok(
minorMajorPatterns[0].includes('(0|[1-9][0-9]*)'),
`The minor/major (X.Y.0) grep pattern does NOT contain the strict ` +
`"(0|[1-9][0-9]*)" guard required by ADR-218 Decision #1.\n` +
`Pattern found: ${minorMajorPatterns[0]}\n` +
`This is the leading-zero regression. Restore (0|[1-9][0-9]*) grouping ` +
`on every major/minor segment.`
);
// Identify the major-only pattern: matches X.0.0 but NOT X.Y.0 with Y>0
const majorOnlyPatterns = patterns.filter(p => {
const re = new RegExp(p);
return re.test('1.0.0') && !re.test('1.1.0');
});
assert.ok(
majorOnlyPatterns.length >= 1,
`Could not locate the major-only (X.0.0) grep pattern in release.yml.\n` +
`Extracted patterns: ${JSON.stringify(patterns)}`
);
// The major-only pattern must also contain (0|[1-9][0-9]*) to guard the
// major segment.
assert.ok(
majorOnlyPatterns[0].includes('(0|[1-9][0-9]*)'),
`The major-only (X.0.0) grep pattern does NOT contain the strict ` +
`"(0|[1-9][0-9]*)" guard required by ADR-218 Decision #1.\n` +
`Pattern found: ${majorOnlyPatterns[0]}\n` +
`ADR-218: IS_MAJOR check must also use (0|[1-9][0-9]*) grouping.`
);
});
});
// ---------------------------------------------------------------------------
// (B) Structural / wiring assertions
// ---------------------------------------------------------------------------
describe('ADR-218 — structural wiring of release.yml', () => {
test('validate-version job exists in release.yml', () => {
const src = loadWorkflow();
assert.ok(
src.includes('validate-version:'),
'release.yml must define a `validate-version:` job (ADR-218 requires it as the gate)'
);
});
test('npm duplicate-version pre-check step is present', () => {
const src = loadWorkflow();
// Assert both the "Reject already-published versions" step name and
// the npm view command exist in the file.
assert.ok(
src.includes('Reject already-published versions'),
'release.yml must contain a step named "Reject already-published versions" (ADR-218 Decision #2)'
);
assert.ok(
src.includes('npm view'),
'release.yml must contain an `npm view` call for duplicate-version pre-check (ADR-218 Decision #2)'
);
});
test('npm duplicate-check step appears AFTER format validation step within validate-version job', () => {
const src = loadWorkflow();
const formatIdx = src.indexOf('Validate version format');
const dupCheckIdx = src.indexOf('Reject already-published versions');
assert.ok(
formatIdx !== -1,
'Could not find "Validate version format" step in release.yml'
);
assert.ok(
dupCheckIdx !== -1,
'Could not find "Reject already-published versions" step in release.yml'
);
assert.ok(
dupCheckIdx > formatIdx,
`"Reject already-published versions" (offset ${dupCheckIdx}) must appear AFTER ` +
`"Validate version format" (offset ${formatIdx}) in release.yml.\n` +
`Format validation must gate before the npm pre-check.`
);
});
test('create job declares needs: validate-version', () => {
const src = loadWorkflow();
// Check that between "create:" and the next top-level job, "needs: validate-version" appears.
const createJobIdx = src.indexOf('\n create:\n');
assert.ok(createJobIdx !== -1, 'release.yml must have a `create:` job');
// Find the segment from create: to the next job header
const afterCreate = src.slice(createJobIdx);
const nextJobMatch = afterCreate.match(/\n {2}[a-z][a-z-]+:\n/g);
const createSegment = nextJobMatch && nextJobMatch.length > 1
? afterCreate.slice(0, afterCreate.indexOf(nextJobMatch[1]))
: afterCreate;
assert.ok(
createSegment.includes('needs: validate-version') || createSegment.includes('needs: [validate-version'),
'The `create` job must declare `needs: validate-version` to ensure validation runs first (ADR-218)'
);
});
test('rc job declares needs including validate-version', () => {
const src = loadWorkflow();
const rcJobIdx = src.indexOf('\n rc:\n');
assert.ok(rcJobIdx !== -1, 'release.yml must have an `rc:` job');
// Parse the `needs:` list from the rc job header region. We look for the
// `needs:` key in the lines immediately following the job header, stopping
// at the first non-indented (top-level) keyword. This avoids false passes
// where "validate-version" only appears inside a step expression such as
// `${{ needs.validate-version.outputs.branch }}` but is absent from the
// actual `needs:` dependency declaration.
const needsList = parseJobNeeds(src, rcJobIdx);
assert.ok(
needsList !== null,
'Could not locate a `needs:` declaration in the `rc` job of release.yml'
);
assert.ok(
needsList.includes('validate-version'),
`The \`rc\` job must list validate-version as a member of its \`needs:\` ` +
`(ADR-218 gate must run before rc).\n` +
`Parsed needs list: ${JSON.stringify(needsList)}`
);
});
test('finalize job declares needs including validate-version', () => {
const src = loadWorkflow();
const finalizeIdx = src.indexOf('\n finalize:\n');
assert.ok(finalizeIdx !== -1, 'release.yml must have a `finalize:` job');
// Same targeted parse as the rc test above.
const needsList = parseJobNeeds(src, finalizeIdx);
assert.ok(
needsList !== null,
'Could not locate a `needs:` declaration in the `finalize` job of release.yml'
);
assert.ok(
needsList.includes('validate-version'),
`The \`finalize\` job must list validate-version as a member of its \`needs:\` ` +
`(ADR-218 gate must run before finalize).\n` +
`Parsed needs list: ${JSON.stringify(needsList)}`
);
});
test('validate-version job appears before create/rc/finalize jobs in file', () => {
const src = loadWorkflow();
const validateIdx = src.indexOf('\n validate-version:\n');
const createIdx = src.indexOf('\n create:\n');
const rcIdx = src.indexOf('\n rc:\n');
const finalizeIdx = src.indexOf('\n finalize:\n');
assert.ok(validateIdx !== -1, 'validate-version job must be defined');
if (createIdx !== -1) {
assert.ok(
validateIdx < createIdx,
'validate-version must be declared before the create job in release.yml'
);
}
if (rcIdx !== -1) {
assert.ok(
validateIdx < rcIdx,
'validate-version must be declared before the rc job in release.yml'
);
}
if (finalizeIdx !== -1) {
assert.ok(
validateIdx < finalizeIdx,
'validate-version must be declared before the finalize job in release.yml'
);
}
});
});