From 63a13ed5d5e522e1ded172095a702285ab70f842 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 20 Feb 2026 13:56:43 -0500 Subject: [PATCH 1/2] fix: resolve sonar code smells and correct stale agent config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Merge redundant switch cases in onPermissionRequest (all returned same value; SonarCloud code smell) - Apply ES6 shorthand for title/body properties in createPullRequest (SonarCloud code smell) - Set sonar.tests=__tests__ so SonarCloud classifies test files correctly - Raise process.setMaxListeners(50) in test setup to suppress MaxListenersExceededWarning from pino instances created per vi.resetModules() - Rewrite AGENTS.md as concise AI-only strict instructions; fix stale references to Jest (โ†’ vitest), 70% coverage threshold (โ†’ 80%), and nonexistent test:coverage script Co-Authored-By: Claude Sonnet 4.6 --- AGENTS.md | 471 +++------------------------------------ __tests__/mocks.js | 4 + sonar-project.properties | 2 +- src/index.js | 6 +- 4 files changed, 33 insertions(+), 450 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 280af81..84d74cc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,454 +1,35 @@ -# ๐Ÿค– AI Agent Configuration +# AGENTS.md -## Repository Context + -**Repository Type**: GitHub Action -**Primary Language**: JavaScript (CommonJS, Node.js 22+) -**Build Tool**: @vercel/ncc -**Testing**: Jest -**Linting**: ESLint 9 (flat config) -**Formatting**: Prettier -**CI/CD**: GitHub Actions -**Quality**: SonarCloud, CodeQL, Codecov -**Pre-commit**: Lefthook +## Hard Rules ---- +1. **Never** modify `.github/workflows/` without explicit user request. +2. **Always** run `npm run format && npm run lint && npm run build && npm test` before marking work complete. +3. **Always** rebuild `dist/` via `npm run build` after any `src/` change โ€” the compiled bundle is committed. +4. **Always** update `__tests__/index.test.js` when changing `src/index.js`. +5. **Always** export every function in `src/index.js` via named ESM exports (required for testability). +6. **Maintain** 80%+ coverage thresholds (branches, functions, lines, statements) โ€” enforced by `vitest.config.js`. +7. **Never** log secrets or tokens; use pino structured logging. +8. **Commits** require Conventional Commits format with RAI attribution footer (enforced by commitlint via lefthook). -## ๐ŸŽฏ Action Purpose +## Corrections to Common AI Assumptions -GitHub Action that delegates code changes to GitHub Copilot CLI, creates feature branches, commits changes, opens PRs, and assigns them to the workflow actor. No auto-merge. Human review required. +- **Test framework is vitest**, not Jest. Use `vitest` APIs (`vi`, `describe`, `test`, `expect`, `beforeEach`, `afterEach`). +- **Coverage threshold is 80%**, not 70%. Check `vitest.config.js` as source of truth. +- **No `test:coverage` script** exists. Coverage runs via `npm test` (`vitest run --coverage`). +- **`src/copilot-loader.js` is excluded** from coverage in `vitest.config.js` โ€” do not add coverage tests for it. +- **Module system is ESM** (`"type": "module"` in package.json). Do not use `require()`. -**Key Workflow**: +## Architecture Constraints -1. Validate optional file input -2. Execute Copilot CLI with instructions -3. Create timestamped branch -4. Commit changes with conventional commit messages -5. Run Copilot review pass -6. Create PR with description -7. Assign PR to workflow actor +- Entry point: `src/index.js` (ESM) +- Bundle: `dist/index.js` โ€” generated by `@vercel/ncc`, must be committed +- Test mocks: `__tests__/mocks.js` โ€” uses `vi.hoisted()` for mock setup before module imports +- SonarCloud: `sonar.tests=__tests__`, `sonar.sources=src`, coverage via `coverage/lcov.info` ---- +## Security Invariants โ€” Do Not Remove or Weaken -## ๐Ÿ—๏ธ Architecture - -### Entry Point - -- `src/index.js`: Main action logic -- `dist/index.js`: Compiled bundle (committed to repo, required by GitHub Actions) - -### Core Functions - -- `validateFilename()`: Sanitize and validate filename input (path traversal protection) -- `validateFile()`: Check file existence, size limits (1MB max), and type -- `runCopilot()`: Execute @github/copilot npm package with token and instructions -- `createBranch()`: Create timestamped feature branch -- `commitAndPush()`: Configure git, commit changes, push to remote -- `createPullRequest()`: Use Octokit to create PR via GitHub API -- `assignPR()`: Assign PR to workflow actor - -### Security Measures - -- Input sanitization via `sanitize-filename` and `validator` -- Path traversal prevention (rejects absolute paths and `..`) -- File size limits (1MB) to prevent memory exhaustion -- No shell execution of user input -- Structured logging via `pino` - ---- - -## ๐Ÿ“‹ Inputs - -| Name | Required | Default | Validation | -| --------------- | -------- | ------- | ------------------------------------------- | -| `PRIVATE_TOKEN` | Yes | - | Must be valid GitHub PAT | -| `filename` | No | `''` | Sanitized, no path traversal, max 255 chars | -| `branch` | No | `main` | Base branch for PR | - ---- - -## ๐Ÿ“ค Outputs - -| Name | Description | -| ----------- | ------------------- | -| `pr_number` | Created PR number | -| `branch` | Feature branch name | - ---- - -## ๐Ÿ”„ CI/CD Agents - -### Setup Agent - -**Job**: `setup` -**Purpose**: Extract Node.js version from Volta configuration in package.json -**Outputs**: `node-version` (default: 22 if not found) - -### Format Agent - -**Job**: `format` -**Command**: `npm run format:check` -**Purpose**: Validate Prettier formatting compliance -**Blocking**: Yes - -### Lint Agent - -**Job**: `lint` -**Command**: `npm run lint` -**Purpose**: ESLint validation, Responsible AI commit footer enforcement -**Blocking**: Yes - -### Build Agent - -**Job**: `build` -**Command**: `npm run build` -**Purpose**: Compile action with @vercel/ncc into dist/ -**Artifacts**: Uploads dist/ for 7 days -**Blocking**: Yes - -### Test Agent - -**Job**: `test` -**Command**: `npm test` -**Purpose**: Execute Jest test suite -**Coverage Target**: 70% (branches, functions, lines, statements) -**Blocking**: Yes - -### Code Coverage Agent - -**Job**: `codecov` -**Command**: `npm run test:coverage` -**Purpose**: Upload coverage to Codecov -**Runs**: Only on push to main (not PRs) -**Blocking**: No (fail_ci_if_error: false) - -### Quality Gate Agent - -**Job**: `gate` -**Purpose**: Final validation that all quality agents passed -**Dependencies**: format, lint, build, test -**Blocking**: Yes - -### CodeQL Security Agent - -**Workflow**: `.github/workflows/codeql.yml` -**Language**: JavaScript -**Query Suite**: security-and-quality -**Schedule**: Weekly (Mondays 00:00 UTC) -**Runs**: Push to main, PRs -**Blocking**: No (reports to Security tab) - -### Secret Scanning Agent - -**Workflow**: `.github/workflows/secret-scanning.yml` -**Tool**: Gitleaks v2 -**Scope**: Full git history -**Runs**: Push to main, PRs -**Blocking**: Yes (prevents secret leaks) - -### SonarCloud Agent - -**Workflow**: `.github/workflows/sonarcloud.yml` -**Version**: v3.1.0 (pinned) -**Runs**: Push to main, PRs -**Metrics**: Code smells, bugs, vulnerabilities, tech debt -**Dashboard**: https://sonarcloud.io/project/overview?id=ChecKMarKDevTools_delegate-action - -### Release Please Agent - -**Workflow**: `.github/workflows/release-please.yml` -**Version**: v4 -**Trigger**: Push to main -**Purpose**: Parse conventional commits, generate changelog, create release PRs, bump versions - ---- - -## ๐Ÿงช Testing Strategy - -### Unit Tests - -**Location**: `__tests__/index.test.js` -**Framework**: Jest -**Mocking**: @actions/core, @actions/exec, @actions/github, fs, pino -**Coverage Target**: 70% minimum - -### Test Cases - -- Filename validation (empty, too long, absolute paths, traversal attempts, valid) -- File validation (existence, size limits, file vs directory) -- Copilot execution (version check, token passing, error handling) -- Branch creation (new branch, existing branch fallback) -- Commit/push (git config, change detection, no-change skip, error handling) -- PR creation (API success, API errors) -- PR assignment (API success, API errors) -- Input validation (required token, default branch) - -### Coverage Thresholds - -```javascript -{ - global: { - branches: 70, - functions: 70, - lines: 70, - statements: 70 - } -} -``` - ---- - -## ๐Ÿ” Security Guidelines - -**Input Validation**: - -- All filenames sanitized with `sanitize-filename` -- Validator.js for additional validation -- Reject absolute paths -- Reject path traversal (`..`) -- Max filename length: 255 characters -- Max file size: 1MB - -**Secret Management**: - -- Never log secrets -- Use GitHub secrets for tokens -- Gitleaks scans every commit -- CodeQL weekly SAST - -**Permissions**: - -- CI: `contents: read`, `pull-requests: write`, `checks: write` -- CodeQL: `actions: read`, `contents: read`, `security-events: write` -- Secret Scanning: `contents: read`, `security-events: write` -- Release Please: `contents: write`, `pull-requests: write` - ---- - -## ๐Ÿ“ Code Style Rules - -**ESLint**: - -- ECMAScript 2024 -- CommonJS modules -- Prettier integration -- No unused vars (except prefixed with `_`) -- Console allowed (structured logging via pino) - -**Prettier**: - -- Auto-format via lefthook pre-commit hook -- Targets: `*.{js,json,md,yml,yaml}` - -**Commitlint**: - -- Conventional Commits enforced -- Responsible AI attribution required via `@checkmarkdevtools/commitlint-plugin-rai` -- Allowed types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert - ---- - -## ๐Ÿ› ๏ธ Development Workflow - -**Setup**: - -```bash -npm install # Installs deps + lefthook hooks -``` - -**Build**: - -```bash -npm run build # Compiles to dist/ -``` - -**Lint**: - -```bash -npm run lint # ESLint check -``` - -**Format**: - -```bash -npm run format # Auto-fix -npm run format:check # Check only -``` - -**Test**: - -```bash -npm test # Run tests -npm run test:coverage # With coverage -``` - -**Pre-commit Hooks** (Lefthook): - -1. Format (auto-fixes) -2. Lint (must pass) -3. Test (must pass) - ---- - -## ๐ŸŽฎ Agent Coordination - -``` -setup -โ”œโ”€โ”€ format โ”€โ”€โ”€โ”€โ” -โ”œโ”€โ”€ lint โ”€โ”€โ”€โ”€โ”€โ”€โ”ค -โ”œโ”€โ”€ build โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€> gate -โ””โ”€โ”€ test โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - โ””โ”€โ”€ codecov (main only) -``` - -**Parallel Execution**: format, lint, build, test all run in parallel after setup -**Quality Gate**: Blocks merge if any quality agent fails -**Coverage Upload**: Only after test succeeds on main branch - ---- - -## ๐Ÿ“Š Quality Metrics - -**Required for Merge**: - -- โœ… Format check passes -- โœ… Lint check passes -- โœ… Build succeeds -- โœ… Tests pass (70% coverage minimum) -- โœ… No secrets detected -- โœ… Quality gate passes - -**Advisory** (non-blocking): - -- CodeQL security alerts -- SonarCloud quality metrics -- Codecov coverage trends - ---- - -## ๐Ÿšจ Error Handling Patterns - -**Graceful Degradation**: - -- Copilot execution errors โ†’ log warning, continue -- Commit errors (no changes) โ†’ log info, skip -- PR assignment errors โ†’ log warning, continue - -**Hard Failures**: - -- Missing PRIVATE_TOKEN โ†’ setFailed -- Invalid filename โ†’ setFailed -- File too large โ†’ setFailed -- PR creation fails โ†’ return null, log error - -**Logging**: - -- Use `pino` for structured JSON logs -- Include context (filename, branch, error messages) -- No secrets in logs - ---- - -## ๐Ÿ”ง Build Artifacts - -**Compiled Output**: - -- `dist/index.js`: Single bundled file created by @vercel/ncc -- **Must be committed to repo** (GitHub Actions requirement) -- Generated via `npm run build` - -**Excluded from Bundle**: - -- node_modules (bundled into dist) -- Tests -- Dev dependencies - ---- - -## ๐Ÿ“ฆ Dependencies - -**Production**: - -- `@actions/core@^2.0.2`: GitHub Actions core utilities -- `@actions/exec@^2.0.0`: Execute shell commands -- `@actions/github@^7.0.0`: Octokit GitHub API client -- `pino@^10.2.0`: Structured logging -- `pino-pretty@^13.1.3`: Log formatting -- `sanitize-filename@^1.6.3`: Filename sanitization -- `validator@^13.15.26`: Input validation - -**Development**: - -- `jest@^29.7.0`: Testing framework -- `eslint@^9.39.2`: Linting -- `prettier@^3.3.3`: Code formatting -- `@vercel/ncc@^0.38.3`: Bundler -- `@commitlint/*@^20.3.1`: Commit message validation -- `lefthook@^2.0.15`: Git hooks - ---- - -## ๐ŸŽฏ AI Agent Instructions - -When working in this repository: - -1. **Never** modify CI configuration without explicit request -2. **Always** export functions in `src/index.js` for testability -3. **Always** update tests when changing functionality -4. **Always** run validation loop before completion: - - `npm run format` - - `npm run lint` - - `npm run build` - - `npm test` -5. **Always** maintain 70%+ test coverage -6. **Never** commit secrets or sensitive data -7. **Always** use conventional commits with RAI attribution -8. **Always** update dist/ after src/ changes -9. **Never** introduce breaking changes without major version bump -10. **Always** validate inputs for security (path traversal, size limits) - ---- - -## ๐Ÿ” Common Tasks - -**Add new feature**: - -1. Update `src/index.js` -2. Export new functions in module.exports -3. Add tests in `__tests__/index.test.js` -4. Run `npm run build` to update dist/ -5. Verify coverage: `npm run test:coverage` -6. Commit with conventional commit + RAI footer - -**Fix bug**: - -1. Write failing test -2. Fix implementation -3. Verify test passes -4. Run full validation loop -5. Update dist/ - -**Update dependencies**: - -1. Run `npm outdated` -2. Update package.json versions -3. Run `npm install` -4. Test thoroughly -5. Update dist/ -6. Commit with `build:` prefix - ---- - -## ๐Ÿ“š Further Reading - -- [GitHub Actions Toolkit](https://github.com/actions/toolkit) -- [Conventional Commits](https://www.conventionalcommits.org/) -- [Jest Documentation](https://jestjs.io/) -- [ESLint Flat Config](https://eslint.org/docs/latest/use/configure/configuration-files) -- [Lefthook Documentation](https://github.com/evilmartians/lefthook) - ---- - -**Last Updated**: 2026-01-17 -**Node Version**: 22.13.1 (Volta) -**Action Version**: 1.0.0 +- `PROMPT_INJECTION_PATTERNS` in `src/index.js` โ€” guards `runCopilot()` input +- `validateFilename()` โ€” rejects absolute paths, `..`, lengths outside 1โ€“255 chars +- `validateFile()` โ€” enforces 1MB size limit (`MAX_FILE_SIZE`) diff --git a/__tests__/mocks.js b/__tests__/mocks.js index 6e52219..bb2960b 100644 --- a/__tests__/mocks.js +++ b/__tests__/mocks.js @@ -1,5 +1,9 @@ import { vi } from 'vitest'; +// Each vi.resetModules() + reimport of src/index.js creates a new pino instance +// that adds process event listeners. Raise the limit to avoid false-positive warnings. +process.setMaxListeners(50); + const mocks = vi.hoisted(() => { const mockCopilotClient = class { started = false; diff --git a/sonar-project.properties b/sonar-project.properties index 1781c0b..3faea61 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -2,7 +2,7 @@ sonar.projectKey=ChecKMarKDevTools_delegate-action sonar.organization=checkmarkdevtools sonar.sources=src -sonar.tests= +sonar.tests=__tests__ sonar.exclusions=node_modules/**,dist/**,coverage/** sonar.javascript.lcov.reportPaths=coverage/lcov.info diff --git a/src/index.js b/src/index.js index 3c9931b..aa3b9a3 100644 --- a/src/index.js +++ b/src/index.js @@ -162,9 +162,7 @@ async function runCopilot(token, instructions, instructionFile = null) { switch (request.kind) { case 'read': - return { kind: 'approved' }; case 'write': - return { kind: 'approved' }; case 'shell': return { kind: 'approved' }; default: @@ -305,8 +303,8 @@ async function createPullRequest(token, branch, baseBranch, title, body) { const { data: pr } = await octokit.rest.pulls.create({ owner: context.repo.owner, repo: context.repo.repo, - title: title, - body: body, + title, + body, head: branch, base: baseBranch, }); From 44aae287d64df5bb2c3e063996fc9e9ca8edf5fe Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 20 Feb 2026 14:03:07 -0500 Subject: [PATCH 2/2] fix: resolve all sonarcloud alerts src/index.js: - Use node:fs and node:path prefixes (S4616) - replaceAll() over replace() with global regex (S5852) - Extract nested template literal to promptFileSection variable (S4624) - Use top-level await for run() entry guard (S4123) __tests__/index.test.js: - Remove duplicate mocks.js import; use node:fs prefix (S4616) - Replace always-true ternary with mockResolvedValue(0) (S3923) - Remove async keyword from intentionally empty stub methods (S4790) .github/workflows/ci.yml: - Move all permissions from workflow level to job level (S6275) - setup/format/lint/build: contents:read only - test: contents:read + checks:write + pull-requests:write Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/ci.yml | 17 +++++++++++++---- __tests__/index.test.js | 21 +++++++++------------ src/index.js | 28 ++++++++++++++++------------ 3 files changed, 38 insertions(+), 28 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 10142b6..8cdab42 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,10 +8,7 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true -permissions: - contents: read - pull-requests: write - checks: write +permissions: {} jobs: setup: @@ -19,6 +16,8 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 if: github.event.pull_request.draft == false + permissions: + contents: read outputs: node-version: ${{ steps.volta.outputs.node-version }} @@ -38,6 +37,8 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 needs: [setup] + permissions: + contents: read steps: - name: Checkout code @@ -60,6 +61,8 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 needs: [setup] + permissions: + contents: read steps: - name: Checkout code @@ -82,6 +85,8 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 needs: [setup] + permissions: + contents: read steps: - name: Checkout code @@ -111,6 +116,10 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 needs: [setup] + permissions: + contents: read + checks: write + pull-requests: write steps: - name: Checkout code diff --git a/__tests__/index.test.js b/__tests__/index.test.js index 7ee680c..31e35aa 100644 --- a/__tests__/index.test.js +++ b/__tests__/index.test.js @@ -1,6 +1,5 @@ -import './mocks.js'; import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest'; -import * as fs from 'fs'; +import * as fs from 'node:fs'; import { mockCore, mockExec, mockGitHub, mockCopilotLoader } from './mocks.js'; describe('Delegate Action', () => { @@ -156,7 +155,7 @@ describe('Delegate Action', () => { async start() { throw new Error('Start failed'); } - async forceStop() {} + forceStop() {} } ); const { runCopilot } = await import('../src/index.js'); @@ -167,7 +166,7 @@ describe('Delegate Action', () => { let permissionHandler; mockCopilotLoader.getCopilotClient.mockResolvedValueOnce( class { - async start() {} + start() {} async createSession(options) { permissionHandler = options.onPermissionRequest; return { @@ -177,8 +176,8 @@ describe('Delegate Action', () => { destroy: vi.fn(), }; } - async stop() {} - async forceStop() {} + stop() {} + forceStop() {} } ); const { runCopilot } = await import('../src/index.js'); @@ -194,7 +193,7 @@ describe('Delegate Action', () => { let eventHandler; mockCopilotLoader.getCopilotClient.mockResolvedValueOnce( class { - async start() {} + start() {} async createSession() { return { sessionId: 'test', @@ -205,8 +204,8 @@ describe('Delegate Action', () => { destroy: vi.fn(), }; } - async stop() {} - async forceStop() {} + stop() {} + forceStop() {} } ); const { runCopilot } = await import('../src/index.js'); @@ -263,9 +262,7 @@ describe('Delegate Action', () => { }); test('skips when no changes', async () => { - mockExec.exec.mockImplementation((cmd, args) => - args?.includes('diff-index') ? Promise.resolve(0) : Promise.resolve(0) - ); + mockExec.exec.mockResolvedValue(0); const { commitAndPush } = await import('../src/index.js'); await commitAndPush('msg', 'branch'); expect(mockExec.exec).not.toHaveBeenCalledWith('git', expect.arrayContaining(['commit'])); diff --git a/src/index.js b/src/index.js index aa3b9a3..c0739a4 100644 --- a/src/index.js +++ b/src/index.js @@ -1,8 +1,8 @@ import * as core from '@actions/core'; import * as exec from '@actions/exec'; import * as github from '@actions/github'; -import fs from 'fs'; -import path from 'path'; +import fs from 'node:fs'; +import path from 'node:path'; import sanitizeFilename from 'sanitize-filename'; import validator from 'validator'; import pino from 'pino'; @@ -355,7 +355,7 @@ async function run() { const baseBranch = core.getInput('branch', { required: false }) || 'main'; const { context } = github; - const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const timestamp = new Date().toISOString().replaceAll(/[:.]/g, '-'); const newBranch = `copilot/delegate-${timestamp}`; logger.info( @@ -401,19 +401,23 @@ async function run() { newBranch ); + const promptFileSection = filename ? `**Prompt file:** \`${filename}\`\n\n` : ''; + const prBody = + `## Automated changes by Delegate Action\n\n` + + `This PR was automatically created by the delegate-action.\n\n` + + promptFileSection + + `**Base branch:** \`${baseBranch}\`\n` + + `**Created by:** @${context.actor}\n\n` + + `Please review the changes carefully before merging.\n\n` + + `---\n\n` + + `_Generated with GitHub Copilot as directed by @${context.actor}_`; + const prNumber = await createPullRequest( privateToken, newBranch, baseBranch, `Delegate: ${filename || 'Repository changes'}`, - `## Automated changes by Delegate Action\n\n` + - `This PR was automatically created by the delegate-action.\n\n` + - `${filename ? `**Prompt file:** \`${filename}\`\n\n` : ''}` + - `**Base branch:** \`${baseBranch}\`\n` + - `**Created by:** @${context.actor}\n\n` + - `Please review the changes carefully before merging.\n\n` + - `---\n\n` + - `_Generated with GitHub Copilot as directed by @${context.actor}_` + prBody ); if (prNumber) { @@ -429,7 +433,7 @@ async function run() { } if (import.meta.url === `file://${process.argv[1]}`) { - run(); + await run(); } export {