Skip to content
Open
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
13 changes: 9 additions & 4 deletions .github/scripts/workflows/metrics-collection-orchestrator.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -255,9 +255,14 @@ async function main() {
process.exit(0);
}

main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});
// Only run as a CLI entry point — this file is also `require()`d directly
// by its own test suite, which must not trigger a real collection run or
// call process.exit() (that would kill the whole Jest process).
if (require.main === module) {
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});
}

module.exports = { MetricsCollectionOrchestrator };
6 changes: 5 additions & 1 deletion .jest.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,11 @@ module.exports = {
'<rootDir>/scripts/agents/includes/sync-version.js',
],
moduleNameMapper: {
'^(\.{1,2}/.*)\.js$': '$1',
// Double backslashes: this key is a JS string, and JS silently
// drops a backslash before an unrecognised escape like \. — a
// single-escaped '\.js$' compiles to the regex .js$ (wildcard
// dot), which also matches .cjs/.mjs, not just literal .js.
'^(\\.{1,2}/.*)\\.js$': '$1',
},
moduleFileExtensions: ['js', 'ts', 'jsx', 'tsx', 'json'],
coverageDirectory: process.env.JEST_COVERAGE_DIR || './coverage',
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- **Jest Module Resolution Wildcard Bug (#3340)** — `.jest.config.cjs`'s `moduleNameMapper` used a single-escaped `'\.js$'` regex key; JS string parsing silently dropped the backslash, compiling to a wildcard-dot regex that also matched `.cjs`/`.mjs` requires and stripped their real extension. Fixed by double-escaping (`'\\.js$'`) so only literal `.js` matches. Unblocked ~22 previously-failing test suites that couldn't load at all. ([Issue #3340](https://github.com/lightspeedwp/.github/issues/3340))
- **release-gates.test.js Indefinite Hang** — Two bugs compounded: (1) `git tag v1.0.0` with no `-m` opens `$EDITOR` and blocks forever under a global `tag.gpgsign=true` config (forces annotated tags); (2) several `describe` blocks reset `cwd` via `process.chdir("/")` instead of the real starting directory, corrupting the shared Jest worker's cwd for whatever test file runs next (surfaced as `EACCES` in `metrics-collection-orchestrator.test.js`'s relative-path `mkdir`). Fixed by forcing unsigned commits/tags in the test's throwaway repo and resetting to the captured original cwd. ([Issue #3340](https://github.com/lightspeedwp/.github/issues/3340))
- **Dependabot Scope Fix** — Added /website npm scanning and area:dependencies labels to dependabot.yml; added on develop in #1059 but never back-ported. ([PR #3316](https://github.com/lightspeedwp/.github/pull/3316))
- **Remaining Dependabot Alerts** — Added npm scanning for 3 more unscanned lockfiles; overrode transitive lodash-es to a patched version. ([PR #3335](https://github.com/lightspeedwp/.github/pull/3335))
- **Lint Debt (#3322)** — 268 of 271 lint errors were vendored skill assets ESLint was never told to ignore; excluded them and fixed a stray installed-file mutation. Real errors now 0. ([PR #3337](https://github.com/lightspeedwp/.github/pull/3337))
Expand Down
46 changes: 35 additions & 11 deletions agents/release/gates/__tests__/release-gates.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ const ReleaseGates = require("../release-gates.cjs");

// Test fixtures and utilities
const TMP_DIR = path.join(os.tmpdir(), "release-gates-test");
// Jest reuses one worker process across multiple test files. Resetting to
// "/" (filesystem root) instead of the real starting directory leaves the
// cwd there for whichever test file runs next in that worker, breaking any
// test that resolves a relative path (e.g. metrics-collection-orchestrator's
// default ".github/reports/metrics" storage dir, which then hits EACCES
// trying to mkdir under /).
const ORIGINAL_CWD = process.cwd();

function setupTestRepo() {
if (fs.existsSync(TMP_DIR)) {
Expand All @@ -33,6 +40,11 @@ function setupTestRepo() {
execSync("git init");
execSync('git config user.email "test@example.com"');
execSync('git config user.name "Test User"');
// Force plain, unsigned commits/tags for this throwaway repo, regardless
// of the developer's global git config (e.g. tag.gpgsign=true forces
// annotated tags, which then require a message and block on $EDITOR).
execSync("git config commit.gpgsign false");
execSync("git config tag.gpgsign false");

// Create initial VERSION file
fs.writeFileSync("VERSION", "1.0.0", "utf-8");
Expand Down Expand Up @@ -75,7 +87,7 @@ describe("GATE 1: Pre-flight Checks", () => {
});

afterEach(() => {
process.chdir("/");
process.chdir(ORIGINAL_CWD);
});

test("Should pass with valid pre-flight state", () => {
Expand Down Expand Up @@ -132,7 +144,7 @@ describe("GATE 2: Agentic Reasoning Score", () => {
});

afterEach(() => {
process.chdir("/");
process.chdir(ORIGINAL_CWD);
});

test("Should pass with valid changelog (score >= 0.80)", () => {
Expand Down Expand Up @@ -213,7 +225,7 @@ describe("GATE 3: Version Consistency", () => {
});

afterEach(() => {
process.chdir("/");
process.chdir(ORIGINAL_CWD);
});

test("Should pass with valid semver (X.Y.Z)", () => {
Expand Down Expand Up @@ -280,7 +292,7 @@ describe("GATE 4: Tag Uniqueness", () => {
});

afterEach(() => {
process.chdir("/");
process.chdir(ORIGINAL_CWD);
});

test("Should pass when tag does not exist", () => {
Expand All @@ -290,7 +302,7 @@ describe("GATE 4: Tag Uniqueness", () => {
});

test("Should fail when tag already exists", () => {
execSync("git tag v1.0.0");
execSync("git tag --no-sign v1.0.0");
const gates = new ReleaseGates();
gates.gate4TagUniqueness();
expect(gates.results.gate4_tag_unique.passed).toBe(false);
Expand All @@ -313,6 +325,10 @@ describe("GATE 5: Authorization", () => {
setupTestRepo();
});

afterEach(() => {
process.chdir(ORIGINAL_CWD);
});

test("Should pass for authorized actors", () => {
process.env.GITHUB_ACTOR = "ash";
const gates = new ReleaseGates();
Expand Down Expand Up @@ -354,7 +370,7 @@ describe("GATE 6: Integrity Filter", () => {
});

afterEach(() => {
process.chdir("/");
process.chdir(ORIGINAL_CWD);
});

test("Should pass when gitleaks not available", () => {
Expand All @@ -374,6 +390,10 @@ describe("GATE 7: Approval Enforcement", () => {
setupTestRepo();
});

afterEach(() => {
process.chdir(ORIGINAL_CWD);
});

test("Should auto-approve patch releases", () => {
process.env.INPUT_SCOPE = "patch";
const gates = new ReleaseGates();
Expand Down Expand Up @@ -414,7 +434,7 @@ describe("All Gates Integration", () => {
});

afterEach(() => {
process.chdir("/");
process.chdir(ORIGINAL_CWD);
});

test("Should pass all gates for valid patch release", () => {
Expand Down Expand Up @@ -507,23 +527,27 @@ describe("Security", () => {
// ============================================================================

describe("Error Handling", () => {
test("Should provide meaningful error messages", () => {
beforeEach(() => {
setupTestRepo();
});

afterEach(() => {
process.chdir(ORIGINAL_CWD);
});

test("Should provide meaningful error messages", () => {
execSync("git checkout -b main");
const gates = new ReleaseGates();
gates.gate1Preflight();
const details = gates.results.gate1_preflight.details.join("\n");
expect(details).toMatch(/not on develop/i);
process.chdir("/");
});

test("Should suggest fixes", () => {
setupTestRepo();
fs.unlinkSync("VERSION");
const gates = new ReleaseGates();
gates.runAllGates();
const log = gates.getResults();
expect(log.passed).toBe(false);
process.chdir("/");
});
});
Loading