From c2105b65ac97f57d2f1432e0ccd3e0db2183b76e Mon Sep 17 00:00:00 2001 From: d-oit <6849456+d-oit@users.noreply.github.com> Date: Tue, 11 Aug 2026 19:44:27 +0200 Subject: [PATCH 1/4] feat(ci): add reusable workflow template and expand workflow tests --- .github/workflow-templates/ci.properties.json | 6 + .github/workflow-templates/ci.yml | 77 ++++++++++ src/lib/__tests__/workflows.test.ts | 133 ++++++++++++++++++ 3 files changed, 216 insertions(+) create mode 100644 .github/workflow-templates/ci.properties.json create mode 100644 .github/workflow-templates/ci.yml diff --git a/.github/workflow-templates/ci.properties.json b/.github/workflow-templates/ci.properties.json new file mode 100644 index 00000000..bef21efd --- /dev/null +++ b/.github/workflow-templates/ci.properties.json @@ -0,0 +1,6 @@ +{ + "name": "CI Pipeline", + "description": "Runs lint, typecheck, tests, and build on every push and pull request. Replace the pnpm setup block with your package manager.", + "iconName": "example-icon", + "categories": ["Continuous integration", "JavaScript", "TypeScript"] +} \ No newline at end of file diff --git a/.github/workflow-templates/ci.yml b/.github/workflow-templates/ci.yml new file mode 100644 index 00000000..4ae789c4 --- /dev/null +++ b/.github/workflow-templates/ci.yml @@ -0,0 +1,77 @@ +--- +name: CI + +# yamllint disable-line rule:truthy +on: + push: + branches: [main] + pull_request: + branches: [main] + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + quality-gate: + name: Quality Gate + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + # --- Replace with your package manager setup --- + # - name: Setup pnpm + # uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + # - name: Setup Node.js + # uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + # with: + # node-version: '22' + # cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Lint + run: pnpm run lint + + - name: Typecheck + run: pnpm run typecheck + + unit-tests: + name: Unit Tests + runs-on: ubuntu-latest + timeout-minutes: 15 + needs: quality-gate + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + # --- Replace with your package manager setup --- + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run tests + run: pnpm test + + build: + name: Build + runs-on: ubuntu-latest + timeout-minutes: 15 + needs: [quality-gate, unit-tests] + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + # --- Replace with your package manager setup --- + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build + run: pnpm run build \ No newline at end of file diff --git a/src/lib/__tests__/workflows.test.ts b/src/lib/__tests__/workflows.test.ts index 71a26deb..8e1ca119 100644 --- a/src/lib/__tests__/workflows.test.ts +++ b/src/lib/__tests__/workflows.test.ts @@ -170,4 +170,137 @@ describe('GitHub Actions Workflows', () => { expect(trivy.name).toBe('Trivy Filesystem Security Scan') }) }) + + describe('Cleanup Workflow', () => { + let workflow: Workflow + + beforeAll(() => { + workflow = loadWorkflow('cleanup.yml') + }) + + it('should be valid YAML', () => { + expect(workflow).toBeDefined() + expect(workflow.name).toBe('Automated Cleanup') + }) + + it('should run on schedule and be manually dispatchable', () => { + expect(workflow.on).toHaveProperty('schedule') + expect(workflow.on).toHaveProperty('workflow_dispatch') + }) + + it('should have proper permissions', () => { + expect(workflow.permissions).toEqual({ + contents: 'read', + 'pull-requests': 'write' + }) + }) + + it('should have a detect-unused job with timeout', () => { + const job = workflow.jobs['detect-unused'] + expect(job).toBeDefined() + expect(job['timeout-minutes']).toBe(15) + }) + + it('should have all jobs with explicit timeouts', () => { + const jobs = workflow.jobs + for (const [name, job] of Object.entries(jobs)) { + expect(job['timeout-minutes'], `job ${name}`).toBeDefined() + } + }) + }) + + describe('Stale Issues Workflow', () => { + let workflow: Workflow + + beforeAll(() => { + workflow = loadWorkflow('stale.yml') + }) + + it('should be valid YAML', () => { + expect(workflow).toBeDefined() + expect(workflow.name).toBe('Stale Issues and PRs') + }) + + it('should run on a daily schedule', () => { + expect(workflow.on).toHaveProperty('schedule') + expect(workflow.on.schedule[0].cron).toBe('0 0 * * *') + }) + + it('should have proper permissions', () => { + expect(workflow.permissions).toEqual({ + contents: 'write', + issues: 'write', + 'pull-requests': 'write' + }) + }) + + it('should use actions/stale with a timeout', () => { + const job = workflow.jobs.stale + expect(job).toBeDefined() + expect(job['timeout-minutes']).toBe(15) + const step = job.steps[0] + expect(step.uses).toContain('actions/stale') + }) + }) + + describe('Labeler Workflow', () => { + let workflow: Workflow + + beforeAll(() => { + workflow = loadWorkflow('labeler.yml') + }) + + it('should be valid YAML', () => { + expect(workflow).toBeDefined() + expect(workflow.name).toBe('Pull Request Labeler') + }) + + it('should trigger on pull_request_target events', () => { + expect(workflow.on).toHaveProperty('pull_request_target') + const types = workflow.on.pull_request_target.types + expect(types).toContain('opened') + expect(types).toContain('synchronize') + }) + + it('should have proper permissions', () => { + expect(workflow.permissions).toEqual({ + contents: 'read', + 'pull-requests': 'write' + }) + }) + + it('should use actions/labeler', () => { + const job = workflow.jobs.labeler + expect(job).toBeDefined() + const labelerStep = job.steps.find((step: { uses?: string }) => + step.uses?.includes('actions/labeler') + ) + expect(labelerStep).toBeDefined() + }) + }) + + describe('Workflow Template', () => { + it('should have a valid CI template with matching properties file', () => { + const templatePath = join( + process.cwd(), + '.github/workflow-templates/ci.yml' + ) + const template = parse(readFileSync(templatePath, 'utf-8')) + expect(template).toBeDefined() + expect(template.name).toBe('CI') + expect(template.jobs).toHaveProperty('quality-gate') + expect(template.jobs).toHaveProperty('unit-tests') + expect(template.jobs).toHaveProperty('build') + + const propertiesPath = join( + process.cwd(), + '.github/workflow-templates/ci.properties.json' + ) + const properties = JSON.parse( + readFileSync(propertiesPath, 'utf-8') + ) as { name: string; description: string } + expect(properties.name).toBe('CI Pipeline') + expect(properties.description.length).toBeGreaterThan(0) + }) + }) }) \ No newline at end of file From 8ff1b0b817e43106d8433009d1ded5e84af70109 Mon Sep 17 00:00:00 2001 From: d-oit <6849456+d-oit@users.noreply.github.com> Date: Tue, 11 Aug 2026 19:47:29 +0200 Subject: [PATCH 2/4] fix(ci): add trailing newlines to workflow template files --- .github/workflow-templates/ci.properties.json | 2 +- .github/workflow-templates/ci.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflow-templates/ci.properties.json b/.github/workflow-templates/ci.properties.json index bef21efd..9c37f183 100644 --- a/.github/workflow-templates/ci.properties.json +++ b/.github/workflow-templates/ci.properties.json @@ -3,4 +3,4 @@ "description": "Runs lint, typecheck, tests, and build on every push and pull request. Replace the pnpm setup block with your package manager.", "iconName": "example-icon", "categories": ["Continuous integration", "JavaScript", "TypeScript"] -} \ No newline at end of file +} diff --git a/.github/workflow-templates/ci.yml b/.github/workflow-templates/ci.yml index 4ae789c4..751cbbe0 100644 --- a/.github/workflow-templates/ci.yml +++ b/.github/workflow-templates/ci.yml @@ -74,4 +74,4 @@ jobs: run: pnpm install --frozen-lockfile - name: Build - run: pnpm run build \ No newline at end of file + run: pnpm run build From 776ea7ab0ec2eea2b80d07cb15286e537018c88d Mon Sep 17 00:00:00 2001 From: d-oit <6849456+d-oit@users.noreply.github.com> Date: Tue, 11 Aug 2026 20:00:26 +0200 Subject: [PATCH 3/4] refactor(test): extract permission helper and add file-existence guard --- src/lib/__tests__/workflows.test.ts | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/lib/__tests__/workflows.test.ts b/src/lib/__tests__/workflows.test.ts index 8e1ca119..0317d3eb 100644 --- a/src/lib/__tests__/workflows.test.ts +++ b/src/lib/__tests__/workflows.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, beforeAll } from 'vitest' -import { readFileSync } from 'fs' +import { readFileSync, existsSync } from 'fs' import { join } from 'path' import { parse } from 'yaml' @@ -15,6 +15,14 @@ const loadWorkflow = (fileName: string): Workflow => { return parse(workflowContent) } +/** + * Assert that a workflow declares exactly the expected top-level permissions. + * Shared by all workflow suites to avoid duplicated permission assertions. + */ +const expectWorkflowPermissions = (workflow: Workflow, expected: object): void => { + expect(workflow.permissions).toEqual(expected) +} + describe('GitHub Actions Workflows', () => { describe('Dependabot Auto-Merge Workflow', () => { let workflow: Workflow @@ -33,7 +41,7 @@ describe('GitHub Actions Workflows', () => { }) it('should have proper permissions', () => { - expect(workflow.permissions).toEqual({ contents: 'read' }) + expectWorkflowPermissions(workflow, { contents: 'read' }) }) it('should have auto-merge job with label requirement', () => { @@ -96,7 +104,7 @@ describe('GitHub Actions Workflows', () => { }) it('should have proper permissions', () => { - expect(workflow.permissions).toEqual({ contents: 'read' }) + expectWorkflowPermissions(workflow, { contents: 'read' }) }) it('should have required jobs', () => { @@ -151,7 +159,7 @@ describe('GitHub Actions Workflows', () => { }) it('should have proper permissions', () => { - expect(workflow.permissions).toEqual({ + expectWorkflowPermissions(workflow, { contents: 'read', 'security-events': 'write' }) @@ -189,7 +197,7 @@ describe('GitHub Actions Workflows', () => { }) it('should have proper permissions', () => { - expect(workflow.permissions).toEqual({ + expectWorkflowPermissions(workflow, { contents: 'read', 'pull-requests': 'write' }) @@ -227,7 +235,7 @@ describe('GitHub Actions Workflows', () => { }) it('should have proper permissions', () => { - expect(workflow.permissions).toEqual({ + expectWorkflowPermissions(workflow, { contents: 'write', issues: 'write', 'pull-requests': 'write' @@ -263,7 +271,7 @@ describe('GitHub Actions Workflows', () => { }) it('should have proper permissions', () => { - expect(workflow.permissions).toEqual({ + expectWorkflowPermissions(workflow, { contents: 'read', 'pull-requests': 'write' }) @@ -285,6 +293,7 @@ describe('GitHub Actions Workflows', () => { process.cwd(), '.github/workflow-templates/ci.yml' ) + expect(existsSync(templatePath)).toBe(true) const template = parse(readFileSync(templatePath, 'utf-8')) expect(template).toBeDefined() expect(template.name).toBe('CI') From adab84925aeeed5503b9cc1f4f8c48626b732b17 Mon Sep 17 00:00:00 2001 From: d-oit <6849456+d-oit@users.noreply.github.com> Date: Tue, 11 Aug 2026 20:04:48 +0200 Subject: [PATCH 4/4] docs(plans): record pre-existing gitleaks license failure (Plan 115) --- ...itleaks-license-pre-existing-2026-08-11.md | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 plans/115-gitleaks-license-pre-existing-2026-08-11.md diff --git a/plans/115-gitleaks-license-pre-existing-2026-08-11.md b/plans/115-gitleaks-license-pre-existing-2026-08-11.md new file mode 100644 index 00000000..03df54b3 --- /dev/null +++ b/plans/115-gitleaks-license-pre-existing-2026-08-11.md @@ -0,0 +1,37 @@ +# Plan 115 — Gitleaks License Failure (Pre-existing Infrastructure Issue) + +Date: 2026-08-11 + +## Issue + +`Security Scan` workflow's **Secret Detection with GitLeaks** step fails on every PR: + +``` +🛑 missing gitleaks license. Go grab one at gitleaks.io and store it as a +GitHub Secret named GITLEAKS_LICENSE. +``` + +Root cause chain (from CI logs, run 31520667156): + +1. `gitleaks/gitleaks-action@e0c47f4f8be36e29cdc102c57e68cb5cbf0e8d1e` (# v3.0.0) is pinned in `.github/workflows/security-scan.yml` (line 143). +2. The action tries to look up the repo owner (`Get user [d-oit]`) to validate license-free usage. +3. That API call fails with `self-signed certificate` — the runner cannot reach `api.github.com`. +4. gitleaks then **enforces** license validation → no `GITLEAKS_LICENSE` secret is configured → hard failure. + +## Verification + +- `security-scan.yml` on `main` is identical to the PR branch (not introduced by any PR). +- `gh secret list` shows **no** `GITLEAKS_LICENSE` secret → pre-existing. +- This check is **not** in the required status checks (only `Codacy Static Code Analysis` is required per branch rules). + +## Options + +| Option | Effort | Notes | +|--------|--------|-------| +| Configure `GITLEAKS_LICENSE` repo secret | Low (needs gitleaks.io account + license) | Cleanest fix; unblocks the step | +| Pin gitleaks-action to v2.x | Low | v2 does not require a license; loses v3 features | +| Add `continue-on-error: true` to the gitleaks step | Trivial | TruffleHog fallback already exists at line 149-155; keeps scan non-blocking | + +## Recommendation + +Option 1 (configure the secret) is the proper fix. As an interim, Option 3 (non-blocking with the existing TruffleHog fallback) prevents the red check without losing secret scanning coverage. Requires maintainer decision — no autonomous change made here.