From 9ccc2577034a881cb4f3648848929f66fdaef4b2 Mon Sep 17 00:00:00 2001 From: d-oit <6849456+d-oit@users.noreply.github.com> Date: Tue, 11 Aug 2026 17:00:27 +0200 Subject: [PATCH 1/5] ci: enhance dependabot auto-merge workflow with better docs and tests --- .github/workflows/dependabot-auto-merge.yml | 18 +++++- src/lib/__tests__/workflows.test.ts | 63 +++++++++++++++++++++ 2 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 src/lib/__tests__/workflows.test.ts diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index cd9f3228..a2c04429 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,6 +13,7 @@ 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') @@ -20,8 +25,17 @@ 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 }} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/src/lib/__tests__/workflows.test.ts b/src/lib/__tests__/workflows.test.ts new file mode 100644 index 00000000..2fcd0bdd --- /dev/null +++ b/src/lib/__tests__/workflows.test.ts @@ -0,0 +1,63 @@ +import { describe, it, expect } from 'vitest' +import { readFileSync } from 'fs' +import { join } from 'path' +import { parse } from 'yaml' + +describe('GitHub Actions Workflows', () => { + describe('Dependabot Auto-Merge Workflow', () => { + const workflowPath = join(process.cwd(), '.github/workflows/dependabot-auto-merge.yml') + let workflow: ReturnType + + beforeAll(() => { + const workflowContent = readFileSync(workflowPath, 'utf-8') + workflow = parse(workflowContent) + }) + + 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('contains(github.event.pull_request.labels.*.name, \'automerge\')') + }) + + it('should only trigger for dependabot or jules bots', () => { + const job = workflow.jobs['auto-merge'] + expect(job.if).toContain('github.actor == \'dependabot[bot]\'') + expect(job.if).toContain('github.actor == \'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') + }) + }) +}) \ No newline at end of file From abc60abd7e1d04216c8a99ec8c644582ae7c7389 Mon Sep 17 00:00:00 2001 From: d-oit <6849456+d-oit@users.noreply.github.com> Date: Tue, 11 Aug 2026 17:21:36 +0200 Subject: [PATCH 2/5] docs(ci): add workflow README and expand workflow tests --- .github/workflows/README.md | 103 ++++++++++++++++++++++++++++ src/lib/__tests__/workflows.test.ts | 103 ++++++++++++++++++++++++++++ 2 files changed, 206 insertions(+) create mode 100644 .github/workflows/README.md 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/src/lib/__tests__/workflows.test.ts b/src/lib/__tests__/workflows.test.ts index 2fcd0bdd..612c2557 100644 --- a/src/lib/__tests__/workflows.test.ts +++ b/src/lib/__tests__/workflows.test.ts @@ -60,4 +60,107 @@ describe('GitHub Actions Workflows', () => { expect(step.run).toContain('gh pr merge --auto --squash') }) }) + + describe('CI Workflow', () => { + const workflowPath = join(process.cwd(), '.github/workflows/ci-and-labels.yml') + let workflow: ReturnType + + beforeAll(() => { + const workflowContent = readFileSync(workflowPath, 'utf-8') + workflow = parse(workflowContent) + }) + + 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', () => { + const workflowPath = join(process.cwd(), '.github/workflows/security-scan.yml') + let workflow: ReturnType + + beforeAll(() => { + const workflowContent = readFileSync(workflowPath, 'utf-8') + workflow = parse(workflowContent) + }) + + 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 From d5be9705949e636bba213d9a5c0b1e5a3aa2d1d5 Mon Sep 17 00:00:00 2001 From: d-oit <6849456+d-oit@users.noreply.github.com> Date: Tue, 11 Aug 2026 18:19:37 +0200 Subject: [PATCH 3/5] fix(ci): fix yamllint line-length + trailing newline in dependabot-auto-merge.yml --- .github/workflows/dependabot-auto-merge.yml | 8 +++++--- src/lib/__tests__/workflows.test.ts | 7 ++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index a2c04429..f9e62326 100644 --- a/.github/workflows/dependabot-auto-merge.yml +++ b/.github/workflows/dependabot-auto-merge.yml @@ -15,8 +15,10 @@ 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: @@ -38,4 +40,4 @@ jobs: gh pr merge --auto --squash "$PR_URL" env: PR_URL: ${{ github.event.pull_request.html_url }} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/src/lib/__tests__/workflows.test.ts b/src/lib/__tests__/workflows.test.ts index 612c2557..47b7a499 100644 --- a/src/lib/__tests__/workflows.test.ts +++ b/src/lib/__tests__/workflows.test.ts @@ -29,13 +29,14 @@ describe('GitHub Actions Workflows', () => { it('should have auto-merge job with label requirement', () => { const job = workflow.jobs['auto-merge'] expect(job).toBeDefined() - expect(job.if).toContain('contains(github.event.pull_request.labels.*.name, \'automerge\')') + 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('github.actor == \'dependabot[bot]\'') - expect(job.if).toContain('github.actor == \'google-labs-jules[bot]\'') + expect(job.if).toContain('dependabot[bot]') + expect(job.if).toContain('google-labs-jules[bot]') }) it('should run on ubuntu-latest with timeout', () => { From fcba344f55aea6c5f99ffc25c5b8b110de5ec770 Mon Sep 17 00:00:00 2001 From: d-oit <6849456+d-oit@users.noreply.github.com> Date: Tue, 11 Aug 2026 19:20:41 +0200 Subject: [PATCH 4/5] fix(ci): break long line in dependabot-auto-merge run block --- .github/workflows/dependabot-auto-merge.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index f9e62326..5be9d8bf 100644 --- a/.github/workflows/dependabot-auto-merge.yml +++ b/.github/workflows/dependabot-auto-merge.yml @@ -28,7 +28,9 @@ jobs: - name: Approve and enable auto-merge run: | # 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 + 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..." From 744bf62718c2578b82190467bc5a91ac6b125791 Mon Sep 17 00:00:00 2001 From: d-oit <6849456+d-oit@users.noreply.github.com> Date: Tue, 11 Aug 2026 19:33:26 +0200 Subject: [PATCH 5/5] refactor(test): extract shared loadWorkflow helper in workflow tests --- src/lib/__tests__/workflows.test.ts | 38 +++++++++++++++++------------ 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/src/lib/__tests__/workflows.test.ts b/src/lib/__tests__/workflows.test.ts index 47b7a499..71a26deb 100644 --- a/src/lib/__tests__/workflows.test.ts +++ b/src/lib/__tests__/workflows.test.ts @@ -1,16 +1,26 @@ -import { describe, it, expect } from 'vitest' +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', () => { - const workflowPath = join(process.cwd(), '.github/workflows/dependabot-auto-merge.yml') - let workflow: ReturnType + let workflow: Workflow beforeAll(() => { - const workflowContent = readFileSync(workflowPath, 'utf-8') - workflow = parse(workflowContent) + workflow = loadWorkflow('dependabot-auto-merge.yml') }) it('should be valid YAML', () => { @@ -63,12 +73,10 @@ describe('GitHub Actions Workflows', () => { }) describe('CI Workflow', () => { - const workflowPath = join(process.cwd(), '.github/workflows/ci-and-labels.yml') - let workflow: ReturnType + let workflow: Workflow beforeAll(() => { - const workflowContent = readFileSync(workflowPath, 'utf-8') - workflow = parse(workflowContent) + workflow = loadWorkflow('ci-and-labels.yml') }) it('should be valid YAML', () => { @@ -103,10 +111,10 @@ describe('GitHub Actions Workflows', () => { 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') @@ -124,12 +132,10 @@ describe('GitHub Actions Workflows', () => { }) describe('Security Scan Workflow', () => { - const workflowPath = join(process.cwd(), '.github/workflows/security-scan.yml') - let workflow: ReturnType + let workflow: Workflow beforeAll(() => { - const workflowContent = readFileSync(workflowPath, 'utf-8') - workflow = parse(workflowContent) + workflow = loadWorkflow('security-scan.yml') }) it('should be valid YAML', () => { @@ -159,7 +165,7 @@ describe('GitHub Actions Workflows', () => { 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') })