diff --git a/.github/workflows/README.md b/.github/workflows/README.md new file mode 100644 index 00000000..deb57b67 --- /dev/null +++ b/.github/workflows/README.md @@ -0,0 +1,103 @@ +# GitHub Actions Workflows + +This directory contains GitHub Actions workflows for the do-knowledge-studio project. + +## Workflow Overview + +| Workflow | File | Trigger | Purpose | +|----------|------|---------|---------| +| **CI** | `ci-and-labels.yml` | Push to main/develop, PRs | Main CI pipeline with quality gates, tests, build, and coverage | +| **Security Scan** | `security-scan.yml` | Push to main, PRs, weekly schedule | Security scanning with ShellCheck and Trivy | +| **Dependabot Auto-Merge** | `dependabot-auto-merge.yml` | PRs from dependabot/jules | Auto-merge dependency updates with label requirement | +| **Commit Lint** | `commitlint.yml` | PRs | Validates commit messages follow conventional commits | +| **YAML Lint** | `yaml-lint.yml` | Push/PR | Validates YAML files | +| **Stale Issues** | `stale.yml` | Daily schedule | Manages stale issues and PRs | +| **Cleanup** | `cleanup.yml` | After PR merge | Cleans up branches and artifacts | +| **Labeler** | `labeler.yml` | PRs | Auto-labels PRs based on file paths | +| **Knowledge Cleanup** | `knowledge-cleanup.yml` | Schedule | Cleans up knowledge base | +| **Create Jules Issues** | `create-jules-issues.yml` | Issues | Creates issues for Jules integration | +| **Dedup Issues** | `dedup-issues.yml` | Issues | Detects and closes duplicate issues | +| **Sync Turso Skill** | `sync-turso-skill.yml` | Push | Syncs Turso skill documentation | + +## Key Workflows + +### CI Pipeline (`ci-and-labels.yml`) + +The main CI pipeline runs on every push and pull request: + +1. **Detect Changes** - Uses `dorny/paths-filter` to detect which files changed +2. **Quality Gate** - Runs linting, type checking, and tests on changed files +3. **Unit Tests** - Runs full test suite for regression testing +4. **E2E Tests** - Runs Playwright tests for frontend changes +5. **Build** - Verifies the project builds successfully +6. **Coverage** - Generates test coverage reports + +**Concurrency**: Uses `cancel-in-progress: true` to avoid redundant runs. + +### Security Scan (`security-scan.yml`) + +Runs security analysis on every push to main and PRs: + +- **ShellCheck** - Scans shell scripts for security vulnerabilities +- **Trivy** - Scans for secrets, misconfigurations, and vulnerabilities +- **SARIF Upload** - Results appear in GitHub Security tab + +### Dependabot Auto-Merge (`dependabot-auto-merge.yml`) + +Automatically approves and enables auto-merge for dependency updates: + +- **Trigger**: Only for `dependabot[bot]` or `google-labs-jules[bot]` +- **Label Requirement**: PRs must have the `automerge` label +- **Strategy**: Squash merge to keep history clean + +## Best Practices + +1. **Pin Actions**: All GitHub Actions are pinned to specific commit SHAs for security +2. **Timeouts**: All jobs have explicit timeouts to prevent hanging +3. **Concurrency**: Workflows use concurrency groups to avoid redundant runs +4. **Permissions**: Follow principle of least privilege +5. **Caching**: Use caching for dependencies and build artifacts + +## Testing Workflows + +Workflow validation tests are located in: +``` +src/lib/__tests__/workflows.test.ts +``` + +These tests validate: +- YAML syntax and structure +- Required fields and configurations +- Job dependencies and timeouts +- Permission settings + +Run tests with: +```bash +pnpm run test -- --testPathPattern=workflows.test.ts +``` + +## Adding New Workflows + +1. Create a new `.yml` file in this directory +2. Follow the existing naming conventions +3. Add appropriate timeouts and permissions +4. Pin all action versions to commit SHAs +5. Add validation tests in `src/lib/__tests__/workflows.test.ts` +6. Update this README with the new workflow + +## Troubleshooting + +### Workflow Fails to Trigger +- Check the `on:` configuration +- Verify branch names and event types +- Ensure the workflow file is on the default branch + +### Auto-Merge Not Working +- Verify the PR has the `automerge` label +- Check that the actor is `dependabot[bot]` or `google-labs-jules[bot]` +- Ensure the workflow has permission to approve and merge PRs + +### Security Scan False Positives +- Review the ShellCheck and Trivy output +- Suppress false positives with appropriate comments +- Update scanning configuration if needed \ No newline at end of file diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index cd9f3228..5be9d8bf 100644 --- a/.github/workflows/dependabot-auto-merge.yml +++ b/.github/workflows/dependabot-auto-merge.yml @@ -1,6 +1,10 @@ --- name: Dependabot Auto-Merge +# This workflow automatically approves and enables auto-merge for dependabot/jules PRs +# that have the 'automerge' label. This prevents unexpected auto-merges while allowing +# explicit opt-in for dependency updates. + # yamllint disable-line rule:truthy on: pull_request @@ -9,9 +13,12 @@ permissions: jobs: auto-merge: + # Only run for dependabot or jules bots with the automerge label if: >- - (github.actor == 'dependabot[bot]' || github.actor == 'google-labs-jules[bot]') - && contains(github.event.pull_request.labels.*.name, 'automerge') + (github.actor == 'dependabot[bot]' + || github.actor == 'google-labs-jules[bot]') + && contains( + github.event.pull_request.labels.*.name, 'automerge') runs-on: ubuntu-latest timeout-minutes: 5 permissions: @@ -20,7 +27,18 @@ jobs: steps: - name: Approve and enable auto-merge run: | - gh pr review --approve "$PR_URL" + # Check if PR is already approved to avoid duplicate approvals + if gh pr view "$PR_URL" --json reviews \ + --jq '.reviews[] | select(.author.login == "github-actions[bot]" + and .state == "APPROVED")' | grep -q .; then + echo "PR already approved, skipping approval step" + else + echo "Approving PR..." + gh pr review --approve "$PR_URL" + fi + + # Enable auto-merge with squash strategy + echo "Enabling auto-merge..." gh pr merge --auto --squash "$PR_URL" env: PR_URL: ${{ github.event.pull_request.html_url }} diff --git a/src/lib/__tests__/workflows.test.ts b/src/lib/__tests__/workflows.test.ts new file mode 100644 index 00000000..71a26deb --- /dev/null +++ b/src/lib/__tests__/workflows.test.ts @@ -0,0 +1,173 @@ +import { describe, it, expect, beforeAll } from 'vitest' +import { readFileSync } from 'fs' +import { join } from 'path' +import { parse } from 'yaml' + +type Workflow = ReturnType + +/** + * Load and parse a workflow YAML file from the .github/workflows directory. + * Shared by all workflow test suites to avoid duplicated loading logic. + */ +const loadWorkflow = (fileName: string): Workflow => { + const workflowPath = join(process.cwd(), '.github/workflows', fileName) + const workflowContent = readFileSync(workflowPath, 'utf-8') + return parse(workflowContent) +} + +describe('GitHub Actions Workflows', () => { + describe('Dependabot Auto-Merge Workflow', () => { + let workflow: Workflow + + beforeAll(() => { + workflow = loadWorkflow('dependabot-auto-merge.yml') + }) + + it('should be valid YAML', () => { + expect(workflow).toBeDefined() + expect(workflow.name).toBe('Dependabot Auto-Merge') + }) + + it('should trigger on pull_request events', () => { + expect(workflow.on).toBe('pull_request') + }) + + it('should have proper permissions', () => { + expect(workflow.permissions).toEqual({ contents: 'read' }) + }) + + it('should have auto-merge job with label requirement', () => { + const job = workflow.jobs['auto-merge'] + expect(job).toBeDefined() + expect(job.if).toContain('automerge') + expect(job.if).toContain('labels') + }) + + it('should only trigger for dependabot or jules bots', () => { + const job = workflow.jobs['auto-merge'] + expect(job.if).toContain('dependabot[bot]') + expect(job.if).toContain('google-labs-jules[bot]') + }) + + it('should run on ubuntu-latest with timeout', () => { + const job = workflow.jobs['auto-merge'] + expect(job['runs-on']).toBe('ubuntu-latest') + expect(job['timeout-minutes']).toBe(5) + }) + + it('should have proper permissions for PR operations', () => { + const job = workflow.jobs['auto-merge'] + expect(job.permissions).toEqual({ + 'pull-requests': 'write', + contents: 'write' + }) + }) + + it('should approve and enable auto-merge', () => { + const job = workflow.jobs['auto-merge'] + const step = job.steps[0] + expect(step.name).toBe('Approve and enable auto-merge') + expect(step.run).toContain('gh pr review --approve') + expect(step.run).toContain('gh pr merge --auto --squash') + }) + }) + + describe('CI Workflow', () => { + let workflow: Workflow + + beforeAll(() => { + workflow = loadWorkflow('ci-and-labels.yml') + }) + + it('should be valid YAML', () => { + expect(workflow).toBeDefined() + expect(workflow.name).toBe('CI') + }) + + it('should trigger on push and pull_request events', () => { + expect(workflow.on).toHaveProperty('push') + expect(workflow.on).toHaveProperty('pull_request') + }) + + it('should have concurrency settings', () => { + expect(workflow.concurrency).toBeDefined() + expect(workflow.concurrency.group).toContain('ci-') + expect(workflow.concurrency['cancel-in-progress']).toBe(true) + }) + + it('should have proper permissions', () => { + expect(workflow.permissions).toEqual({ contents: 'read' }) + }) + + it('should have required jobs', () => { + expect(workflow.jobs).toHaveProperty('changes') + expect(workflow.jobs).toHaveProperty('quality-gate') + expect(workflow.jobs).toHaveProperty('unit-tests') + expect(workflow.jobs).toHaveProperty('e2e-tests') + expect(workflow.jobs).toHaveProperty('build') + expect(workflow.jobs).toHaveProperty('coverage') + }) + + it('should have proper job dependencies', () => { + const qualityGate = workflow.jobs['quality-gate'] + expect(qualityGate.needs).toContain('changes') + + const unitTests = workflow.jobs['unit-tests'] + expect(unitTests.needs).toContain('changes') + + const e2eTests = workflow.jobs['e2e-tests'] + expect(e2eTests.needs).toContain('changes') + expect(e2eTests.needs).toContain('unit-tests') + }) + + it('should have proper timeouts', () => { + const jobs = workflow.jobs + expect(jobs['changes']['timeout-minutes']).toBe(10) + expect(jobs['quality-gate']['timeout-minutes']).toBe(15) + expect(jobs['unit-tests']['timeout-minutes']).toBe(15) + expect(jobs['e2e-tests']['timeout-minutes']).toBe(20) + expect(jobs['build']['timeout-minutes']).toBe(15) + expect(jobs['coverage']['timeout-minutes']).toBe(20) + }) + }) + + describe('Security Scan Workflow', () => { + let workflow: Workflow + + beforeAll(() => { + workflow = loadWorkflow('security-scan.yml') + }) + + it('should be valid YAML', () => { + expect(workflow).toBeDefined() + expect(workflow.name).toBe('Security Scan') + }) + + it('should trigger on push, pull_request, schedule, and workflow_dispatch', () => { + expect(workflow.on).toHaveProperty('push') + expect(workflow.on).toHaveProperty('pull_request') + expect(workflow.on).toHaveProperty('schedule') + expect(workflow.on).toHaveProperty('workflow_dispatch') + }) + + it('should have proper permissions', () => { + expect(workflow.permissions).toEqual({ + contents: 'read', + 'security-events': 'write' + }) + }) + + it('should have security scanning jobs', () => { + expect(workflow.jobs).toHaveProperty('shellcheck-security') + expect(workflow.jobs).toHaveProperty('trivy-fs') + }) + + it('should have proper job names', () => { + const shellcheck = workflow.jobs['shellcheck-security'] + expect(shellcheck.name).toBe('Shell Script Security Analysis') + + const trivy = workflow.jobs['trivy-fs'] + expect(trivy.name).toBe('Trivy Filesystem Security Scan') + }) + }) +}) \ No newline at end of file