Skip to content
Merged
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
6 changes: 6 additions & 0 deletions .github/workflow-templates/ci.properties.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

77 changes: 77 additions & 0 deletions .github/workflow-templates/ci.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 37 additions & 0 deletions plans/115-gitleaks-license-pre-existing-2026-08-11.md
Original file line number Diff line number Diff line change
@@ -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.
150 changes: 146 additions & 4 deletions src/lib/__tests__/workflows.test.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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
Expand All @@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -151,7 +159,7 @@ describe('GitHub Actions Workflows', () => {
})

it('should have proper permissions', () => {
expect(workflow.permissions).toEqual({
expectWorkflowPermissions(workflow, {
contents: 'read',
'security-events': 'write'
})
Expand All @@ -170,4 +178,138 @@ describe('GitHub Actions Workflows', () => {
expect(trivy.name).toBe('Trivy Filesystem Security Scan')
})
})

describe('Cleanup Workflow', () => {
let workflow: Workflow

beforeAll(() => {
workflow = loadWorkflow('cleanup.yml')
Comment thread
d-oit marked this conversation as resolved.
})

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', () => {
expectWorkflowPermissions(workflow, {
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', () => {
expectWorkflowPermissions(workflow, {
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', () => {
expectWorkflowPermissions(workflow, {
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'
)
expect(existsSync(templatePath)).toBe(true)
const template = parse(readFileSync(templatePath, 'utf-8'))
expect(template).toBeDefined()
expect(template.name).toBe('CI')
Comment thread
d-oit marked this conversation as resolved.
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)
})
})
})
Loading