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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Removed

- **Duplicate `.github/agents/` folder (Phase 1 restructuring compliance)** — Deleted entire `.github/agents/` folder (55 files) consolidating all agent implementations to root `agents/` folder per Phase 1 restructuring rules. The `.github/agents/` folder violated the portable assets rule by containing multi-file agent implementations (Claude/Copilot/OpenAI) when it should only contain "simple YAML/JSON definitions" (GitHub-native only). All agent implementations now properly organized at root as portable reusable assets. ([PR #1533](https://github.com/lightspeedwp/.github/pull/1533), [#1510](https://github.com/lightspeedwp/.github/issues/1510), [#1507](https://github.com/lightspeedwp/.github/issues/1507))

- **Legacy README workflows (Phase 2.4 consolidation)** — Removed three legacy README management workflows (`readme-audit.yml`, `readme-regen.yml`, `readme-update.yml`) consolidated into unified `documentation.yml` workflow. Eliminates 449 lines of code duplication (~44% reduction for README workflows), saves ~3-4 min/month GitHub Actions execution time, and establishes single source of truth for README validation logic. Push trigger re-enabled in `documentation.yml` following consolidation. ([PR #1317](https://github.com/lightspeedwp/.github/pull/1317), [Epic #1227](https://github.com/lightspeedwp/.github/issues/1227), [#1310](https://github.com/lightspeedwp/.github/issues/1310))

### Deprecated

(none identified)

### Fixed

- **Agent file_type frontmatter validation (Phase 1 restructuring)** — Added missing `file_type` frontmatter to all root agent configuration files: 48 provider-specific agent.md files (claude/, copilot/, openai/) with `file_type: 'agent'`, and 16 shared core-prompt.md files with `file_type: 'prompt'`. Fixes 200+ frontmatter validation errors and ensures all agent files comply with documentation schema requirements. ([PR #1533](https://github.com/lightspeedwp/.github/pull/1533), [#1510](https://github.com/lightspeedwp/.github/issues/1510), [#1507](https://github.com/lightspeedwp/.github/issues/1507))

### Added

- **Gitleaks secret scanning** — Added `gitleaks-reusable.yml`, an organisation-wide reusable workflow other repositories call via `workflow_call`, plus a `gitleaks.yml` caller running on pull requests into `develop`/`main`. Runs the open-source Gitleaks CLI directly (the `gitleaks-action` wrapper requires a paid licence for organisation repositories). Per-PR runs scan the working tree; `workflow_dispatch` accepts a `full-history` input for on-demand full-history rescans. A baseline full-history scan of this repository returned 50 hits, all verified as placeholder values in documentation and tests, allowlisted in `.gitleaks.toml`. ([PR #1444](https://github.com/lightspeedwp/.github/pull/1444))
Expand Down
251 changes: 251 additions & 0 deletions scripts/validation/__tests__/validate-labels-before-creation.test.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
/**
* Unit Tests: validate-labels-before-creation.cjs
*
* Test suite for pre-creation label validation script.
* Validates:
* 1. Canonical label existence
* 2. Family prefix requirements
* 3. One-hot per family constraint
* 4. Required type: label
* 5. Error and warning messages
*/

const { execSync } = require('child_process');
const path = require('path');

const SCRIPT_PATH = path.join(__dirname, '../validate-labels-before-creation.cjs');
const LABELS_FILE = path.join(__dirname, '../../../.github/labels.yml');

/**
* Execute validation script and parse output
* @param {string[]} labels - Labels to validate
* @returns {object} Parsed result
*/
function validateLabels(labels) {
const labelStr = labels.join(',');
try {
execSync(
`node ${SCRIPT_PATH} --labels "${labelStr}" --canonical-file ${LABELS_FILE}`,
{ stdio: 'pipe' }
);
return { valid: true, errors: [], warnings: [] };
} catch (error) {
// Extract JSON from stderr
const stderr = error.stderr.toString();
const jsonMatch = stderr.match(/\{[\s\S]*\}/);
if (jsonMatch) {
return JSON.parse(jsonMatch[0]);
}
return { valid: false, errors: [error.message], warnings: [] };
}
}

// ============================================================================
// Test Suite
// ============================================================================

describe('Label Validation', () => {
describe('Valid Labels', () => {
test('accepts canonical type:bug label', () => {
const result = validateLabels(['type:bug']);
expect(result.valid).toBe(true);
expect(result.errors.length).toBe(0);
});

test('accepts full canonical label set', () => {
const result = validateLabels([
'type:bug',
'status:needs-triage',
'priority:critical',
'area:ci'
]);
expect(result.valid).toBe(true);
expect(result.errors.length).toBe(0);
});

test('accepts all type:* variants', () => {
const types = [
'type:bug',
'type:feature',
'type:task',
'type:documentation',
'type:design',
'type:refactor',
'type:chore'
];

for (const type of types) {
const result = validateLabels([type]);
expect(result.valid).toBe(true);
}
});

test('accepts multiple meta: labels (allowed exception)', () => {
const result = validateLabels([
'type:bug',
'meta:needs-changelog',
'meta:has-pr'
]);
expect(result.valid).toBe(true);
});
});

describe('Bare Labels (Invalid)', () => {
test('rejects bare "bug" label', () => {
const result = validateLabels(['bug']);
expect(result.valid).toBe(false);
expect(result.errors.some(e => e.includes('bug'))).toBe(true);
});

test('rejects bare "feature" label', () => {
const result = validateLabels(['feature']);
expect(result.valid).toBe(false);
expect(result.errors.some(e => e.includes('feature'))).toBe(true);
});

test('rejects all common bare labels', () => {
const bareLabels = [
'bug',
'feature',
'task',
'documentation',
'urgent',
'critical',
'ci',
'docs',
'release',
'automation'
];

for (const bare of bareLabels) {
const result = validateLabels([bare]);
expect(result.valid).toBe(false);
}
});

test('detects bare labels in mixed set', () => {
const result = validateLabels(['type:bug', 'feature', 'status:needs-triage']);
expect(result.valid).toBe(false);
expect(result.errors.some(e => e.includes('feature'))).toBe(true);
});
});

describe('Non-Existent Labels', () => {
test('rejects unknown label', () => {
const result = validateLabels(['type:unknown']);
expect(result.valid).toBe(false);
expect(result.errors.some(e => e.includes('not found'))).toBe(true);
});

test('rejects completely made-up label', () => {
const result = validateLabels(['invalid:label']);
expect(result.valid).toBe(false);
});
});

describe('One-Hot Constraint (One per Family)', () => {
test('rejects multiple type: labels', () => {
const result = validateLabels(['type:bug', 'type:feature']);
expect(result.valid).toBe(false);
expect(result.errors.some(e => e.includes('Multiple labels'))).toBe(true);
});

test('rejects multiple status: labels', () => {
const result = validateLabels([
'type:bug',
'status:needs-triage',
'status:in-progress'
]);
expect(result.valid).toBe(false);
expect(result.errors.some(e => e.includes('Multiple labels'))).toBe(true);
});

test('rejects multiple priority: labels', () => {
const result = validateLabels([
'type:bug',
'priority:critical',
'priority:important'
]);
expect(result.valid).toBe(false);
});

test('allows multiple meta: labels (exception)', () => {
const result = validateLabels([
'type:bug',
'meta:needs-changelog',
'meta:has-pr',
'meta:duplicate'
]);
expect(result.valid).toBe(true);
});

test('allows multiple comp: labels (exception)', () => {
const result = validateLabels([
'type:feature',
'comp:block-editor',
'comp:theme-json'
]);
expect(result.valid).toBe(true);
});
});

describe('Required type: Label', () => {
test('requires at least one type: label', () => {
const result = validateLabels(['status:needs-triage', 'priority:critical']);
expect(result.valid).toBe(false);
expect(result.errors.some(e => e.includes("Missing required 'type:*'"))).toBe(true);
});

test('passes with any type: variant', () => {
const types = [
'type:bug',
'type:feature',
'type:task',
'type:documentation'
];

for (const type of types) {
const result = validateLabels([type]);
expect(result.valid).toBe(true);
}
});
});

describe('Warnings', () => {
test('warns about bare label "bug"', () => {
const result = validateLabels(['bug']);
expect(result.warnings.some(w => w.includes('Bare label'))).toBe(true);
});

test('suggests corrections for bare labels', () => {
const result = validateLabels(['bug']);
expect(result.warnings.some(w => w.includes('type:bug'))).toBe(true);
});
});

describe('Edge Cases', () => {
test('handles empty label list', () => {
const result = validateLabels([]);
expect(result.valid).toBe(false);
expect(result.errors.some(e => e.includes("Missing required 'type:*'"))).toBe(true);
});

test('ignores whitespace in labels', () => {
const result = validateLabels(['type:bug ', ' status:needs-triage']);
// Script should handle this gracefully
expect(result).toHaveProperty('valid');
});

test('handles very long label list', () => {
const labels = [
'type:feature',
'status:ready',
'priority:normal',
'area:ci',
'meta:needs-changelog'
];
const result = validateLabels(labels);
expect(result.valid).toBe(true);
});
});
});
Loading
Loading