diff --git a/.github/agents/implementation-planner.agent.md b/.github/agents/implementation-planner.agent.md new file mode 100644 index 00000000..e24b7c19 --- /dev/null +++ b/.github/agents/implementation-planner.agent.md @@ -0,0 +1,43 @@ +--- +name: implementation-planner +description: + Creates detailed implementation plans and technical specifications for the Terraform Module Releaser project. Analyzes + requirements and breaks them into actionable tasks. +tools: ["read", "search"] +--- + +You are a technical planning specialist for the Terraform Module Releaser project — a TypeScript GitHub Action that +automates versioning, releases, and wiki documentation for Terraform modules in monorepos. + +Before creating plans, read the relevant documentation: + +- `docs/architecture.md` — Execution flow, module relationships, design decisions +- `docs/testing.md` — Test infrastructure and mock patterns +- `docs/development.md` — CI/CD pipeline, tooling, release process + +## Project Context + +- **Stack**: TypeScript 5.9+ strict, Vitest, Biome, @actions/core, @octokit +- **Key modules**: `src/main.ts` (orchestrator), `src/terraform-module.ts` (domain model), `src/parser.ts` (discovery) +- **Patterns**: Proxy singletons, effective change detection, idempotency via PR markers, tag normalization +- **Tests**: 3-tier mock system with helpers in `__tests__/helpers/` + +## Your Responsibilities + +- Analyze requirements and break them into actionable, well-scoped tasks +- Create detailed technical specifications considering the existing architecture +- Document which files need to change, what tests need updating, and integration points +- Identify risks: backwards compatibility, config changes, wiki generation impacts +- Structure plans with clear headings, task breakdowns, and acceptance criteria +- Consider the CI validation pipeline (lint, typecheck, test, build) in your plans + +## Plan Structure + +Always include: + +1. **Goal**: What the change achieves +2. **Affected files**: Source, test, config, and documentation files +3. **Implementation steps**: Ordered, specific, referencing existing patterns +4. **Testing strategy**: Which tests to add/modify, mock setup needed +5. **Validation checklist**: Commands to run, edge cases to verify +6. **Risks**: Breaking changes, backwards compatibility, performance implications diff --git a/.github/agents/pr-writer.agent.md b/.github/agents/pr-writer.agent.md new file mode 100644 index 00000000..d0c83525 --- /dev/null +++ b/.github/agents/pr-writer.agent.md @@ -0,0 +1,112 @@ +--- +name: pr-writer +description: + Generates a clean, markdown-formatted pull request title and description from branch commits. Enforces conventional + commit style for PR titles. +tools: ["read", "search", "runCommands"] +--- + +You are a pull request writing specialist for the Terraform Module Releaser project. + +Your output must always be valid Markdown. + +For easy copy/paste, section headings must be outside code fences. + +- Include `## Suggested PR Title` as plain Markdown text (not fenced) +- Include `## Suggested PR Description` as plain Markdown text (not fenced) +- Put only the title value in the first fenced block +- Put only the description body in the second fenced block +- Do not place section headings inside any fence + +## Execution Mode (Task-First) + +Treat this as an autonomous task, not a prompt-driven creative request. + +- Always analyze the currently checked-out Git branch +- Do not require user-provided input details to start +- Treat user text primarily as a trigger to run the task +- Ignore optional style requests that conflict with this spec + +Branch rules: + +1. Determine current branch name from Git +2. If branch is `main` (or `master`), do not generate PR content +3. In that case, return a short Markdown response explaining generation only runs on non-default branches +4. If branch is not default, generate PR content from commits unique to current branch vs default branch + +## Goal + +Produce a high-quality PR title and PR description from the current branch changes. + +## Required Output Contract + +When branch is non-default, always return exactly these sections in this order: + +1. `## Suggested PR Title` +2. `## Suggested PR Description` + +Fence placement: + +- Output `## Suggested PR Title` as a heading outside the fence +- Then emit one fenced block containing only the title string +- Output `## Suggested PR Description` as a heading outside the fence +- Then emit one fenced block containing only the description Markdown body + +The PR title must: + +- Follow Conventional Commits format: `(): ` or `: ` +- Use one of: `feat`, `fix`, `chore`, `docs`, `refactor`, `test`, `build`, `ci` +- Be concise, specific, and action-oriented + +The PR description must be easy to read and include: + +- `### Summary` +- `### What Changed` +- `### Validation` +- `### Risks / Notes` + +When branch is default (`main`/`master`), return exactly: + +1. `## PR Generation Unavailable` +2. One short explanation that PR generation is restricted to non-default branches +3. One short next-step bullet telling the user to checkout/create a feature branch + +Fence placement for default-branch case: + +- Return `## PR Generation Unavailable` as a heading outside the fence +- Then emit one fenced block containing only the explanatory body + +## Analysis Process + +1. Detect default branch (`origin/HEAD` target when available, otherwise `main`) +2. Inspect commits unique to the current branch compared to the default branch +3. Group changes by intent (feature, maintenance, docs, CI, tests) +4. Infer the dominant change type for the PR title +5. Summarize major files and behavioral impact +6. Include validation commands relevant to touched areas + +## Formatting Rules + +- Output Markdown only +- Use fenced blocks tagged `markdown` +- For non-default branches, emit exactly two fenced blocks (title first, description second) +- Never combine title and description in the same fence +- Keep `Suggested PR` section headings outside fences for easy copy/paste +- Prefer short bullets over long paragraphs +- Keep wording clear for reviewers scanning quickly +- Avoid exaggerated language and avoid inventing changes not present in commits +- If uncertainty exists, call it out briefly in `### Risks / Notes` + +## Project-Specific Context + +- This repository expects Conventional Commits for commits and PR titles +- CI and release behavior are sensitive to workflow and action-version changes +- Documentation-only branches should typically use `docs:` titles unless CI/build behavior also changes + +## Repetitive Workflow + +When asked to regenerate content after additional commits: + +- Recompute from the latest commit range +- Keep the same section structure +- Prefer stable phrasing so revisions are easy to diff diff --git a/.github/agents/test-specialist.agent.md b/.github/agents/test-specialist.agent.md new file mode 100644 index 00000000..a849f261 --- /dev/null +++ b/.github/agents/test-specialist.agent.md @@ -0,0 +1,50 @@ +--- +name: test-specialist +description: + Focuses on test coverage, quality, and testing best practices for the Terraform Module Releaser codebase. Understands + the 3-tier mock architecture and Vitest patterns. +--- + +You are a testing specialist for the Terraform Module Releaser project — a TypeScript GitHub Action using Vitest with V8 +coverage. + +Before writing or modifying tests, read `docs/testing.md` for the full mock architecture and testing patterns. + +## Test Infrastructure + +- **Framework**: Vitest with V8 coverage on `src/` only +- **Setup**: `__tests__/_setup.ts` auto-mocks `@actions/core`, `@/config`, `@/context` +- **Path aliases**: `@/` → `src/`, `@/tests/` → `__tests__/`, `@/mocks/` → `__mocks__/` + +## Mock Architecture (3-Tier) + +1. `__mocks__/@actions/core.ts` — Global replacement. Logging silenced. `getInput`/`getBooleanInput` use real + implementations (read `INPUT_*` env vars) +2. `__mocks__/config.ts` — Proxy mock: `.set({...})` to override, `.resetDefaults()` to restore +3. `__mocks__/context.ts` — Proxy mock: `.set({...})`, `.reset()`, `.useRealOctokit()`, `.useMockOctokit()` + +## Test Helpers + +- `__tests__/helpers/octokit.ts`: `stubOctokitReturnData()`, `stubOctokitImplementation()`, `createRealOctokit()` +- `__tests__/helpers/terraform-module.ts`: `createMockTerraformModule()`, `createMockTag()`, `createMockTags()` +- `__tests__/helpers/inputs.ts`: `setupTestInputs()` for `INPUT_*` env var management + +## Your Responsibilities + +- Analyze existing tests and identify coverage gaps +- Write unit and integration tests following existing patterns +- Use `describe`/`it` blocks with descriptive names explaining expected behavior +- Reset config/context in `beforeEach` using `.resetDefaults()` and `.reset()` +- Use factory functions from helpers (never manually construct complex test objects) +- Gate integration tests behind `GITHUB_TOKEN` availability checks +- Ensure tests are isolated, deterministic, and well-documented +- Focus on test files only — avoid modifying production code in `src/` unless specifically requested + +## Validation + +After writing tests, verify: + +```bash +npm run test # All tests pass with coverage +npm run typecheck # Type checking passes +``` diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 037c7037..11b878b5 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,101 +1,98 @@ # Terraform Module Releaser -A GitHub Action written in TypeScript that automates versioning, releases, and documentation for Terraform modules in -monorepos. Creates module-specific Git tags, GitHub releases, PR comments, and comprehensive wiki documentation. +A GitHub Action (TypeScript) that automates versioning, releases, and documentation for Terraform modules in monorepos. +Creates module-specific Git tags, GitHub releases, PR comments, and comprehensive wiki documentation. + +For detailed architecture, testing patterns, and design context, see the `docs/` folder. ## Tech Stack -- **TypeScript 5.9+** with strict mode -- **Node.js 24+** for local development (`.node-version`); compiles to Node.js 20+ compatible output for GitHub Actions - runtime (action.yml uses `node20`) +- **TypeScript 5.9+** strict mode, ES modules (`"type": "module"` in package.json) +- **Node.js 24+** local dev (`.node-version`); compiles to Node.js 20+ for GitHub Actions runtime (`action.yml` → + `node20`) - **Vitest** for testing with V8 coverage -- **Biome** for linting/formatting (not ESLint/Prettier) -- **@actions/core** and **@octokit** for GitHub integration +- **Biome** for linting/formatting — NOT ESLint/Prettier (except Prettier for Markdown/YAML only) +- **@actions/core** + **@octokit** for GitHub integration +- **minimatch** for glob pattern matching, **p-limit** for concurrency control -## Essential Commands +## Commands — Run Before Every Commit ```bash -# Format and lint (run before every commit) -npm run check:fix -npm run textlint:fix - -# Type checking -npm run typecheck - -# Testing -npm run test # Full test suite (requires GITHUB_TOKEN) -npm run test:watch # Watch mode for development +npm run check:fix # Biome lint/format + Prettier for md/yml +npm run textlint:fix # Prose linting for markdown +npm run typecheck # TypeScript strict compilation check +npm run test # Full Vitest suite with coverage (requires GITHUB_TOKEN) ``` -## GITHUB_TOKEN Setup +Additional commands: `npm run test:watch` (dev mode), `npm run package` (build dist/). -Integration tests require a valid GitHub token. Set it in your environment: +## GITHUB_TOKEN -```bash -# For current session -export GITHUB_TOKEN="ghp_your_token_here" - -# Or create .env file (add to .gitignore) -echo "GITHUB_TOKEN=ghp_your_token_here" > .env -``` - -## Project Structure +Integration tests require a valid GitHub token. Tests without it are automatically skipped. ```bash -src/ # TypeScript source -├── index.ts # Entry point -├── ... # Core logic and utilities -└── types/ # Type definitions -__tests__/ # Tests (mirror src/) -tf-modules/ # Example Terraform modules for testing -dist/ # Compiled output (auto-generated) +export GITHUB_TOKEN="ghp_your_token_here" ``` -## Code Standards - -**Naming:** - -- Functions/variables: `camelCase` (`parseModules`, `tagName`) -- Types/interfaces: `PascalCase` (`TerraformModule`, `WikiConfig`) -- Constants: `UPPER_SNAKE_CASE` (`WIKI_HOME_FILENAME`) +The devcontainer forwards `GITHUB_TOKEN` from the host automatically. -**Style:** Biome enforces all formatting automatically via `npm run check:fix` +## Project Layout -## Development Workflow +``` +src/ # TypeScript source (ES modules) +├── index.ts # Entry point → calls run() from main.ts +├── main.ts # Orchestrator: init → parse → release/comment +├── config.ts # Singleton config (reads action inputs via Proxy) +├── context.ts # Singleton context (repo, PR, Octokit via Proxy) +├── parser.ts # Discovers Terraform modules, maps commits → modules +├── terraform-module.ts # Central domain model (TerraformModule class) +├── tags.ts # Git tag CRUD operations +├── releases.ts # GitHub release creation, tag pushing +├── pull-request.ts # PR comment management, commit fetching +├── changelog.ts # Changelog generation (per-module and aggregated) +├── wiki.ts # Wiki generation lifecycle (clone, generate, push) +├── terraform-docs.ts # terraform-docs binary install and execution +├── types/ # TypeScript type definitions +└── utils/ # Constants, file ops, GitHub helpers, string utils +__tests__/ # Tests mirror src/ structure +├── _setup.ts # Global test setup (mocks config/context/@actions/core) +├── helpers/ # Test utilities (mock factories, Octokit stubs) +├── fixtures/ # Wiki fixture files (use Unicode slug chars) +└── utils/ # Utility function tests +__mocks__/ # Vitest module mocks +├── config.ts # Proxy-based mock config with .set()/.resetDefaults() +├── context.ts # Proxy-based mock context with .set()/.reset() +└── @actions/core.ts # Silenced logging, real getInput/getBooleanInput +tf-modules/ # Example Terraform modules for integration tests +dist/ # Compiled output (auto-generated, never edit manually) +docs/ # Detailed documentation for humans and AI agents +``` -1. Make changes in `src/` -2. Run `npm run check:fix && npm run textlint:fix` (autofix formatting) -3. Run `npm run typecheck` (verify compilation) -4. Run `npm run test` (ensure tests pass) -5. Commit using [Conventional Commits](https://www.conventionalcommits.org/) format (e.g., `feat:`, `fix:`, `chore:`) +## Architecture — Key Patterns -**Commit Format:** We follow Conventional Commits with semantic versioning. Examples: `feat: add new feature`, -`fix: resolve bug`, `chore: update dependencies` +- **Proxy-based singletons**: `config` and `context` use `Proxy` for lazy initialization; import at module scope without + triggering init until first property access. Both have `clearForTesting()` methods. +- **Config before Context**: Config must initialize first — Context reads `config.githubToken` for Octokit auth. +- **Idempotency**: A hidden HTML comment marker in post-release PR comments prevents duplicate releases on re-runs. +- **Effective change detection**: Commits that modify then revert a file within the same PR are excluded. +- **Tag normalization**: All separator chars (`-`, `_`, `/`, `.`) are normalized before tag-to-module matching. +- **Wiki Unicode slugs**: `/` and `-` in wiki page names are replaced with Unicode lookalikes (`∕` U+2215, `‒` U+2012) + because GitHub Wiki breaks with those characters. Test fixtures use these chars in filenames. +- **Path aliases**: `@/` → `src/`, `@/tests/` → `__tests__/`, `@/mocks/` → `__mocks__/` (configured in tsconfig.json and + vitest.config.ts). -## Testing Notes +## Code Standards -- Path aliases: `@/` → `src/`, `@/tests/` → `__tests__/` -- Some tests download terraform-docs binary (requires internet) -- Tests without GITHUB_TOKEN are automatically skipped -- Test modules in `tf-modules/` directory +- **Functions/variables**: `camelCase` — **Types/interfaces**: `PascalCase` — **Constants**: `UPPER_SNAKE_CASE` +- Biome enforces all TS/JS formatting. Prettier handles Markdown/YAML only. +- Use Conventional Commits: `feat:`, `fix:`, `chore:`, `docs:`, `refactor:`, `test:` ## Boundaries -✅ **Always do:** - -- Run `npm run check:fix` before committing -- Add/update tests for code changes -- Follow TypeScript strict mode -- Use existing patterns in codebase - -⚠️ **Ask first:** - -- Adding new dependencies -- Changing build configuration -- Modifying GitHub Actions workflows +**Always**: Run `npm run check:fix` before committing. Add/update tests for all code changes. Follow TypeScript strict +mode. Use existing patterns. -🚫 **Never do:** +**Ask first**: Adding new dependencies. Changing build config. Modifying GitHub Actions workflows. -- Commit without running lint/tests -- Modify `dist/` manually (auto-generated) -- Bypass TypeScript strict checks +**Never**: Commit without running lint/tests. Modify `dist/` manually. Bypass TypeScript strict checks. Check in bundle +artifacts (handled by release automation). diff --git a/.github/copilot-setup-steps.yml b/.github/copilot-setup-steps.yml new file mode 100644 index 00000000..7323f6fb --- /dev/null +++ b/.github/copilot-setup-steps.yml @@ -0,0 +1,19 @@ +name: "Copilot Setup Steps" + +on: workflow_dispatch + +jobs: + copilot-setup-steps: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Node.js + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + with: + node-version-file: .node-version + cache: npm + + - name: Install dependencies + run: npm ci --no-fund diff --git a/.github/instructions/tests.instructions.md b/.github/instructions/tests.instructions.md new file mode 100644 index 00000000..42e28aee --- /dev/null +++ b/.github/instructions/tests.instructions.md @@ -0,0 +1,43 @@ +--- +applyTo: "__tests__/**/*.ts,__mocks__/**/*.ts" +--- + +## Test and Mock Guidelines + +Tests use **Vitest** with V8 coverage. Test files mirror `src/` structure in `__tests__/`. + +### Test Setup + +- Global setup in `__tests__/_setup.ts` — auto-mocks `@actions/core`, `@/config`, `@/context` +- Path aliases: `@/tests/` → `__tests__/`, `@/mocks/` → `__mocks__/`, `@/` → `src/` +- Some tests require `GITHUB_TOKEN` env var — tests skip gracefully without it + +### Mock Architecture (3-tier system) + +1. **`__mocks__/@actions/core.ts`** — Replaces `@actions/core` globally. Logging silenced, `getInput`/`getBooleanInput` + use real implementations (read `INPUT_*` env vars) +2. **`__mocks__/config.ts`** — Proxy mock with `.set({...})` to override config values and `.resetDefaults()` to + restore. Loads real defaults from `action.yml` +3. **`__mocks__/context.ts`** — Proxy mock with `.set({...})`, `.reset()`, `.useRealOctokit()`, `.useMockOctokit()`. + Defaults to mock Octokit + +### Test Helpers (`__tests__/helpers/`) + +- `action-defaults.ts` — Reads `action.yml` to extract input defaults +- `inputs.ts` — `setupTestInputs()` sets `INPUT_*` env vars. Exports categorized input arrays +- `octokit.ts` — Mock Octokit: `stubOctokitReturnData()`, `stubOctokitImplementation()`, `createRealOctokit()` +- `terraform-module.ts` — `createMockTerraformModule()`, `createMockTag()`, `createMockTags()` factories + +### Writing Tests + +- Always use `describe`/`it` blocks with descriptive names explaining expected behavior +- Reset mocks in `beforeEach` — use `config.resetDefaults()` and `context.reset()` when needed +- Use `stubOctokitReturnData()` for simple mock return values +- Use `stubOctokitImplementation()` for complex mock behavior +- For integration tests needing real GitHub API, use `context.useRealOctokit()` (requires `GITHUB_TOKEN`) +- Wiki test fixtures in `__tests__/fixtures/` use Unicode chars in filenames (U+2215, U+2012) — handle carefully + +### Coverage + +- V8 coverage on `src/` only (excludes tests, mocks, types) +- Run `npm run test` for full suite with coverage report diff --git a/.github/instructions/typescript.instructions.md b/.github/instructions/typescript.instructions.md new file mode 100644 index 00000000..04994bc5 --- /dev/null +++ b/.github/instructions/typescript.instructions.md @@ -0,0 +1,35 @@ +--- +applyTo: "src/**/*.ts" +--- + +## TypeScript Source Guidelines + +This project uses TypeScript 5.9+ with strict mode and ES modules (`"type": "module"`). + +### Module Patterns + +- Use ES module imports/exports exclusively (`import`/`export`, never `require`) +- Path aliases: `@/` maps to `src/` — use `@/config`, `@/context`, etc. for internal imports +- Reexport types through `src/types/index.ts` + +### Config and Context Singletons + +- `config` and `context` are Proxy-based singletons with lazy initialization +- Import them at module scope: `import { config } from '@/config'` — safe because Proxy defers init +- Config must initialize before Context (Context reads `config.githubToken`) +- Both expose `clearForTesting()` for test cleanup + +### Coding Standards + +- Functions/variables: `camelCase` — Types/interfaces: `PascalCase` — Constants: `UPPER_SNAKE_CASE` +- All constants live in `src/utils/constants.ts` +- Use `@actions/core` for logging (`core.info()`, `core.debug()`, `core.warning()`) +- Use `@actions/core` for action outputs (`core.setOutput()`) and failure (`core.setFailed()`) +- Prefer async/await over raw promises +- Error handling: catch and wrap with `core.setFailed()` at top level only + +### Formatting + +- **Biome** enforces all TS/JS formatting — run `npm run check:fix` to autoformat +- 120-char line width, 2-space indent, LF line endings +- Single quotes, trailing commas, semicolons always diff --git a/.github/linters/.codespellrc b/.github/linters/.codespellrc new file mode 100644 index 00000000..9c166a3c --- /dev/null +++ b/.github/linters/.codespellrc @@ -0,0 +1,2 @@ +[codespell] +ignore-words-list = afterall diff --git a/.github/workflows/check-dist.yml b/.github/workflows/check-dist.yml index 50a3d2ae..35510382 100644 --- a/.github/workflows/check-dist.yml +++ b/.github/workflows/check-dist.yml @@ -22,14 +22,14 @@ jobs: steps: - name: Checkout id: checkout - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 persist-credentials: false - name: Setup Node.js id: setup-node - uses: actions/setup-node@v6 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version-file: .node-version cache: npm @@ -63,7 +63,7 @@ jobs: - if: ${{ failure() && steps.diff.outcome == 'failure' }} name: Upload Artifact id: upload - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: dist path: dist/ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e773d520..257c5dc1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,13 +21,13 @@ jobs: steps: - name: Checkout id: checkout - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Setup Node.js id: setup-node - uses: actions/setup-node@v6 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version-file: .node-version cache: npm diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 738a60f2..09a7027a 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -30,21 +30,21 @@ jobs: steps: - name: Checkout id: checkout - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Initialize CodeQL id: initialize - uses: github/codeql-action/init@v4 + uses: github/codeql-action/init@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3 with: languages: ${{ matrix.language }} source-root: src - name: Autobuild id: autobuild - uses: github/codeql-action/autobuild@v4 + uses: github/codeql-action/autobuild@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3 - name: Perform CodeQL Analysis id: analyze - uses: github/codeql-action/analyze@v4 + uses: github/codeql-action/analyze@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index e0ac9f54..df3ffb35 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -21,14 +21,14 @@ jobs: steps: - name: Checkout id: checkout - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 persist-credentials: false - name: Setup Node.js id: setup-node - uses: actions/setup-node@v6 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version-file: .node-version cache: npm @@ -48,18 +48,18 @@ jobs: steps: - name: Checkout id: checkout - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 persist-credentials: false - name: Lint Codebase with Super-Linter id: super-linter - uses: super-linter/super-linter@d5b0a2ab116623730dd094f15ddc1b6b25bf7b99 # v8.3.2 + uses: super-linter/super-linter@61abc07d755095a68f4987d1c2c3d1d64408f1f9 # v8.5.0 env: DEFAULT_BRANCH: main GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - FILTER_REGEX_EXCLUDE: (dist/**/*)|(__tests__/fixtures/**/*) + FILTER_REGEX_EXCLUDE: (dist/**/*)|(__tests__/fixtures/**/*)|package-lock\.json FIX_TYPESCRIPT_PRETTIER: false # Using biome FIX_JAVASCRIPT_PRETTIER: false # Using biome VALIDATE_ALL_CODEBASE: true diff --git a/.github/workflows/release-start.yml b/.github/workflows/release-start.yml index df3aef2b..e21e9bf9 100644 --- a/.github/workflows/release-start.yml +++ b/.github/workflows/release-start.yml @@ -24,14 +24,14 @@ jobs: permissions: contents: read # Required to read repo contents. Note: We leverage release-preview app for PR + commit generation steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 # Get all history which is required for parsing commits persist-credentials: false - name: Setup Node.js id: setup-node - uses: actions/setup-node@v6 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version-file: .node-version cache: npm @@ -70,7 +70,7 @@ jobs: run: npm run check:fix - name: Generate Changelog - uses: actions/github-script@v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 id: changelog env: GITHUB_TOKEN: ${{ secrets.GH_TOKEN_READ_AND_MODELS }} @@ -95,7 +95,7 @@ jobs: # # https://github.com/peter-evans/create-pull-request/blob/main/docs/concepts-guidelines.md#triggering-further-workflow-runs - - uses: actions/create-github-app-token@v2 + - uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1 id: app-token with: app-id: ${{ secrets.RELEASE_PREVIEW_APP_ID }} @@ -117,7 +117,7 @@ jobs: # Note: We can't change the head branch once a PR is opened. Thus we need to delete any branches # that exist from any existing open pull requests. (App Perm = Pull Request: Read + Write) - name: Close existing release pull requests - uses: actions/github-script@v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: github-token: ${{ steps.app-token.outputs.token }} script: | @@ -160,7 +160,7 @@ jobs: } - name: Create Branch and Pull Request - uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0 + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 with: token: ${{ steps.app-token.outputs.token }} base: main diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 23af62b3..ecf0101f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: github.event.pull_request.merged == true steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 # Get all history fetch-tags: true @@ -26,7 +26,7 @@ jobs: - name: Extract version from PR body id: extract-version - uses: actions/github-script@v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: result-encoding: string script: | @@ -39,7 +39,7 @@ jobs: - name: Extract release notes from PR body id: extract-release-notes - uses: actions/github-script@v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: result-encoding: string script: | @@ -63,7 +63,7 @@ jobs: git push origin v${{ steps.extract-version.outputs.result }} - name: Create GitHub Release via API - uses: actions/github-script@v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | const owner = context.repo.owner; diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5eb11237..0bb8eda4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,14 +16,14 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: # Disabling shallow clone is recommended for improving relevancy of reporting fetch-depth: 0 persist-credentials: false - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version-file: .node-version cache: npm @@ -38,6 +38,6 @@ jobs: # Note: SonarQube requires the results from tests to get the coverage report - name: SonarQube Scan - uses: SonarSource/sonarqube-scan-action@a31c9398be7ace6bbfaf30c0bd5d415f843d45e9 # v5 + uses: SonarSource/sonarqube-scan-action@2f77a1ec69fb1d595b06f35ab27e97605bdef703 # v5.3.2 env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..d9acfc7f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,84 @@ +# Agent Instructions — Terraform Module Releaser + +A GitHub Action (TypeScript, strict mode) that automates versioning, releases, and wiki documentation for Terraform +modules in monorepos. + +## Quick Reference + +```bash +npm run check:fix # Biome lint/format + Prettier (md/yml) — run before every commit +npm run textlint:fix # Prose linting for markdown files +npm run typecheck # TypeScript strict compilation check +npm run test # Full Vitest test suite with V8 coverage (requires GITHUB_TOKEN) +npm run test:watch # Watch mode for development +npm run package # Build dist/ via ncc (auto-generated, never edit manually) +``` + +## Environment + +- **Node.js 24+** locally (`.node-version`); compiled output targets Node.js 20+ (`action.yml` → `node20`) +- **GITHUB_TOKEN** required for integration tests — tests skip gracefully without it +- Path aliases: `@/` → `src/`, `@/tests/` → `__tests__/`, `@/mocks/` → `__mocks__/` + +## Project Structure + +- `src/` — TypeScript source (ES modules). Entry: `index.ts` → `main.ts` orchestrator +- `src/types/` — Type definitions (`Config`, `Context`, `TerraformModule`, etc.) +- `src/utils/` — Constants, file ops, GitHub helpers, string utilities +- `__tests__/` — Tests mirror `src/` structure. Setup: `_setup.ts` (mocks config/context/core) +- `__tests__/helpers/` — Mock factories, Octokit stubs, test input helpers +- `__mocks__/` — Module-level Vitest mocks (config/context use Proxy pattern with `.set()`/`.resetDefaults()`) +- `tf-modules/` — Example Terraform modules for integration tests +- `docs/` — Detailed architecture, testing, and development documentation +- `dist/` — Auto-generated build output (never edit) + +## Architecture Essentials + +The action runs on `pull_request` events with two flows: + +1. **PR open/sync** → Parse modules → Post release plan comment → Check wiki status +2. **PR merged** → Create tagged releases → Post release comment → Clean up orphaned tags → Generate wiki + +Key patterns: + +- **Proxy singletons**: `config` and `context` use `Proxy` for lazy init — import at module scope safely +- **Config before Context**: Config must initialize first (Context needs `config.githubToken`) +- **Idempotency**: Hidden HTML marker in PR comments prevents duplicate releases on re-runs +- **Wiki Unicode slugs**: `/` → `∕` (U+2215), `-` → `‒` (U+2012) in wiki page names +- **Tag normalization**: All separators (`-`, `_`, `/`, `.`) normalized for tag-to-module matching + +## Code Conventions + +- Naming: `camelCase` (functions/vars), `PascalCase` (types), `UPPER_SNAKE_CASE` (constants) +- Formatting: Biome for TS/JS/JSON; Prettier for Markdown/YAML only +- Commits: Conventional Commits (`feat:`, `fix:`, `chore:`, `docs:`, `refactor:`, `test:`) + +## Rules + +- Always run `npm run check:fix` and `npm run textlint:fix` before committing +- Always add/update tests for code changes +- Never modify `dist/` manually or check in bundle artifacts +- Never bypass TypeScript strict checks +- Ask before adding new dependencies or changing build configuration + +## Detailed Documentation + +Before making significant changes, read the relevant docs: + +- `docs/architecture.md` — Execution flow, module relationships, design decisions +- `docs/testing.md` — Test patterns, mock strategy, writing new tests +- `docs/development.md` — Development workflow, CI/CD, release process + +## Built-in Chat Agents + +- `.github/agents/implementation-planner.agent.md` — Creates implementation plans and task breakdowns +- `.github/agents/test-specialist.agent.md` — Focuses on test design, coverage, and mock usage +- `.github/agents/pr-writer.agent.md` — Generates Markdown PR title/description from branch commits + +### PR Writer Agent Usage + +Use the PR writer agent when preparing pull requests, especially for branches with many commits. + +- Input: Ask for a PR title and description from current branch commits +- Output: Markdown-only result with a Conventional Commit PR title and reviewer-friendly summary sections +- Regeneration: Re-run after new commits are pushed to refresh content diff --git a/__tests__/config.test.ts b/__tests__/config.test.ts index a0e800ae..ceb20c3b 100644 --- a/__tests__/config.test.ts +++ b/__tests__/config.test.ts @@ -22,7 +22,7 @@ describe('config', () => { }); beforeEach(() => { - // The config is cached. To ensure each test starts with a clean slate, we implicity clear it. + // The config is cached. To ensure each test starts with a clean slate, we implicitly clear it. // We don't do this globally in setup as it's not necessary for all tests. clearConfigForTesting(); }); diff --git a/__tests__/helpers/inputs.ts b/__tests__/helpers/inputs.ts index 0801dbdd..65ad634b 100644 --- a/__tests__/helpers/inputs.ts +++ b/__tests__/helpers/inputs.ts @@ -80,7 +80,7 @@ export function setupTestInputs(overrides: Record = {}) { * Clears a specific action input environment variable. * * Useful for testing scenarios where you need to remove a specific input. Wrapper around - * vi.stubEnv which has an unsual syntax for clearing environment variables/ + * vi.stubEnv which has an unusual syntax for clearing environment variables. * * @param inputName The input name to clear (e.g., 'github_token', 'module-path-ignore') */ diff --git a/__tests__/terraform-docs.test.ts b/__tests__/terraform-docs.test.ts index 214114d6..8d1f1697 100644 --- a/__tests__/terraform-docs.test.ts +++ b/__tests__/terraform-docs.test.ts @@ -219,7 +219,7 @@ describe('terraform-docs', async () => { 'terraform-docs', 'terraform-docs.tar.gz', 'terraform-docs.zip', - // It appears that removing the actual installed binary causes isssues with other async tests + // It appears that removing the actual installed binary causes issues with other async tests // resulting in errors. Thus, we leave the actual installed binaries on the system. // join('/usr/local/bin', 'terraform-docs'), // join('C:\\Windows\\System32', 'terraform-docs.exe'), diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 00000000..63ddb264 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,215 @@ +# Architecture + +This document describes the architecture of the Terraform Module Releaser GitHub Action. + +## Execution Flow + +The action is triggered by `pull_request` events and has two distinct flows: + +### Entry Point + +`src/index.ts` → calls `run()` from `src/main.ts` + +### Initialization Phase + +1. **`getConfig()`** — Reads all GitHub Action inputs via `@actions/core.getInput()`, validates them, and caches in a + singleton. Uses a `Proxy` wrapper so imports at module scope don't trigger initialization until first property + access. +2. **`getContext()`** — Reads GitHub environment variables (`GITHUB_REPOSITORY`, `GITHUB_EVENT_NAME`, etc.), parses the + PR event payload, and creates an authenticated Octokit client using `config.githubToken`. + +> **Critical**: Config must initialize before Context because Context reads `config.githubToken` for Octokit auth. + +### Data Gathering Phase + +Runs in parallel after initialization: + +- **`getPullRequestCommits()`** — Fetches PR commits via Octokit, then cross-references with PR changed files to + implement "effective change detection" (commits that modify then revert a file are excluded) +- **`getAllTags()`** — Paginated fetch of all repository tags +- **`getAllReleases()`** — Paginated fetch of all repository releases + +### Module Parsing Phase (`parseTerraformModules()`) + +Three-phase discovery in `src/parser.ts`: + +1. **Discover** — Recursively find all directories containing `.tf` files in the workspace, filtering out paths matching + `module-path-ignore` patterns +2. **Instantiate** — Create a `TerraformModule` instance per discovered directory, associating matching tags and + releases +3. **Map commits** — Analyze each commit's changed files to determine which modules are affected + +### Event Handling + +After parsing, the flow branches on event type: + +#### PR Open/Synchronize + +`handlePullRequestEvent()`: + +1. Check wiki status (clone wiki repository, test connectivity) +2. Post release plan comment — Rich Markdown table showing: + - Modules with changes and their next version + - Modules without changes + - Tags/releases to be cleaned up (orphaned from deleted modules) + - Wiki status indicator + +#### PR Merged + +`handlePullRequestMergedEvent()`: + +1. **Idempotency check** — `hasReleaseComment()` looks for a hidden HTML comment marker in existing PR comments. If + found, exits early (prevents duplicate releases on workflow re-runs) +2. **Create tagged releases** — For each module needing release: + - Copy module files to temp directory (excluding configured patterns) + - Copy `.git` directory + - Create new commit + tag in temp directory + - Push tag to remote + - Create GitHub release via API with changelog body +3. **Post release comment** — Summary of created releases with marker for idempotency +4. **Delete legacy tags/releases** — Remove orphaned tags from deleted modules (if `delete-legacy-tags` is enabled) +5. **Generate wiki** — Clone wiki repository, generate/update all module pages, push changes + +### Action Outputs + +Six outputs are set via `core.setOutput()` before the merge/release operation: + +| Output | Type | Description | +| ---------------------- | ----------- | ------------------------------------------------------------------- | +| `changed-module-names` | JSON array | Module names changed in the PR | +| `changed-module-paths` | JSON array | File system paths to changed modules | +| `changed-modules-map` | JSON object | Module names → change details (current tag, next tag, release type) | +| `all-module-names` | JSON array | All discovered module names | +| `all-module-paths` | JSON array | All discovered module paths | +| `all-modules-map` | JSON object | All module names → details (path, latest tag, version) | + +> **Note**: Outputs are set before `clearCommits()` is called during release, since `needsRelease()` checks commit +> presence. + +## Core Modules + +### `TerraformModule` (Domain Model) + +File: `src/terraform-module.ts` + +The central data structure, combining state and behavior: + +- **State**: directory, name, commits, tags, releases +- **Computed**: `needsRelease()`, `getReleaseType()` (keyword scanning), `getReleaseTag()`, `getReleaseTagVersion()` +- **Static utilities**: Tag/release association, orphan detection, module name normalization + +Key behaviors: + +- `getReleaseType()` scans commit messages against major/minor/patch keywords from config +- `getReleaseTag()` constructs the next tag using the configured separator and version prefix +- Tag association normalizes all separators (`-`, `_`, `/`, `.`) to a common character before comparison +- Tags and releases are stored sorted by SemVer (not lexicographically) + +### Config Singleton (`src/config.ts`) + +- Reads GitHub Action inputs via `@actions/core.getInput()` and `getBooleanInput()` +- Validates: tag separator (must be `/`, `-`, `_`, or `.`), SemVer level, module ref mode +- Exposes `getConfig()` for direct access and `config` (Proxy) for ergonomic module-scope imports +- `clearForTesting()` resets the cached instance + +### Context Singleton (`src/context.ts`) + +- Reads: `GITHUB_REPOSITORY`, `GITHUB_EVENT_NAME`, `GITHUB_EVENT_PATH`, `GITHUB_WORKSPACE`, `GITHUB_SERVER_URL` +- Parses PR event payload from `GITHUB_EVENT_PATH` JSON file +- Creates authenticated Octokit client with paginate + REST plugins +- Exposes `getContext()` and `context` (Proxy) +- `clearForTesting()` resets + +### Parser (`src/parser.ts`) + +- `parseTerraformModules(commits, tags, releases)` — Main entry point +- Uses `findTerraformModuleDirectories()` for recursive `.tf` file discovery +- Applies `module-path-ignore` patterns via minimatch +- Deduplicates commits to modules (a commit may touch files in multiple modules) + +### Release Creation (`src/releases.ts`) + +- `createTaggedReleases()` — Per module: + 1. Copy module contents to temp dir (respecting `module-asset-exclude-patterns`) + 2. Copy `.git` directory + 3. Configure Git authentication (HTTP extraheader with base64-encoded token) + 4. Commit + tag + push + 5. Create GitHub release via `octokit.rest.repos.createRelease()` +- Tags point to commits containing only the module's files (clean release artifacts) + +### Wiki Generation (`src/wiki.ts`) + +- Full lifecycle: checkout → generate → commit → push +- Module pages include: Usage (templated), Attributes (terraform-docs output), Changelog +- Sidebar: Grouped by module with recent changelog entries +- Uses `p-limit` for concurrency control during parallel page generation +- Unicode slug substitution for GitHub Wiki compatibility + +## Key Design Patterns + +### Proxy-Based Lazy Singletons + +Both `config` and `context` use this pattern: + +```typescript +let instance: Config | undefined; +export const config = new Proxy({} as Config, { + get: (_, prop) => { + if (!instance) instance = getConfig(); + return instance[prop]; + }, +}); +``` + +Benefits: Import at module scope without triggering initialization. Test-friendly via `clearForTesting()`. + +### Effective Change Detection + +The action filters commits to exclude "phantom" changes — when a file is modified and then reverted within the same PR. +This prevents unnecessary version bumps from commits that have no net effect on a module. + +### Idempotency via PR Comments + +A hidden HTML comment (``) is embedded in post-release +comments. On re-runs, `hasReleaseComment()` checks for this marker and exits early if found. + +### Tag Normalization + +`TerraformModule.isModuleAssociatedWithTag()` normalizes all valid separators to a common character before comparison. +This handles repositories that may have changed their tagging scheme over time (e.g., from `/` to `-`). + +### Wiki Unicode Slugs + +GitHub Wiki can't handle `/` or `-` in page filenames without breaking navigation. The action replaces these with +Unicode lookalikes: + +- `/` → `∕` (U+2215, DIVISION SLASH) +- `-` → `‒` (U+2012, FIGURE DASH) + +These substitutions are defined in `WIKI_TITLE_REPLACEMENTS` in `src/utils/constants.ts`. + +## Module Dependency Graph + +``` +index.ts + └── main.ts + ├── config.ts (singleton, reads action inputs) + ├── context.ts (singleton, creates Octokit) + ├── parser.ts (discovers modules) + │ ├── terraform-module.ts (domain model) + │ └── utils/file.ts (filesystem discovery) + ├── tags.ts (CRUD operations) + ├── releases.ts (create releases, push tags) + │ └── utils/github.ts (git auth, bot email) + ├── pull-request.ts (PR comments, commit fetching) + ├── changelog.ts (changelog generation) + ├── wiki.ts (wiki lifecycle) + │ ├── terraform-docs.ts (binary install + exec) + │ └── utils/string.ts (template rendering) + └── utils/constants.ts (shared constants) +``` + +## Configuration + +All action inputs are defined in `action.yml` and mapped in `src/utils/metadata.ts`. The `ACTION_INPUTS` constant +provides type-safe metadata for each input, used by `createConfigFromInputs()` to dynamically build the config object. diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 00000000..2e154895 --- /dev/null +++ b/docs/development.md @@ -0,0 +1,144 @@ +# Development Guide + +This document covers the development environment, workflow, CI/CD pipeline, and release process for the Terraform Module +Releaser project. + +## Development Environment + +### DevContainer (Recommended) + +The repository includes a pre-configured devcontainer with: + +- **Base image**: `mcr.microsoft.com/devcontainers/typescript-node:24` (Node.js 24) +- **Named volume**: `node_modules` volume persists across container rebuilds +- **Post-create script**: Sets Git safe directory, fixes node_modules ownership, runs `npm install` +- **Visual Studio Code extensions**: Biome, Prettier, GitHub Actions, Markdown tools, GitHub PR extension +- **Formatting config**: Biome as default formatter for TS/JS/JSON; Prettier for markdown/YAML +- **Environment**: `GITHUB_TOKEN` forwarded from host automatically + +### Manual Setup + +1. Install Node.js 24+ (see `.node-version`) +2. Run `npm ci --no-fund` +3. Export `GITHUB_TOKEN` for integration tests + +## Development Workflow + +### Making Changes + +1. Create a feature branch from `main` +2. Make changes in `src/` +3. Add or update tests in `__tests__/` +4. Run validation: + +```bash +npm run check:fix # Biome lint/format + Prettier (md/yml) +npm run textlint:fix # Prose linting for markdown +npm run typecheck # TypeScript strict compilation check +npm run test # Full test suite with coverage +``` + +5. Commit using Conventional Commits format + +### Conventional Commits + +All commits must follow the [Conventional Commits](https://www.conventionalcommits.org/) specification: + +| Prefix | Purpose | Example | +| ----------- | ------------------ | -------------------------------------- | +| `feat:` | New feature | `feat: add SSH source format option` | +| `fix:` | Bugfix | `fix: handle empty module directory` | +| `chore:` | Maintenance | `chore: update dependencies` | +| `docs:` | Documentation | `docs: improve wiki generation guide` | +| `refactor:` | Code restructuring | `refactor: simplify tag normalization` | +| `test:` | Test changes | `test: add coverage for edge cases` | + +## Tooling + +### Biome (Linting & Formatting) + +- Handles all TS/JS/JSON formatting and linting +- Config: `biome.json` +- NOT ESLint or Prettier for TypeScript/JavaScript +- 120-char line width, 2-space indent, LF endings, single quotes, trailing commas, semicolons + +### Prettier (Markdown & YAML only) + +- Only used for `.md` and `.yml` files +- Config: `prettier` key in `package.json` +- 120-char print width for Markdown, prose wrap enabled + +### Textlint (Prose Linting) + +- Lints Markdown prose for terminology and style +- Config: `.github/linters/.textlintrc` +- Run: `npm run textlint:fix` + +### TypeScript + +- Strict mode with all strict checks enabled +- Target: ECMAScript 2022, Module: ECMAScript 2022, ModuleResolution: bundler +- Path aliases configured in `tsconfig.json` and `vitest.config.ts` +- Type-check only: `npm run typecheck` (uses `--noEmit`) + +## CI/CD Pipeline + +### Pull Request Workflows + +When a PR is opened or updated against `main`, these workflows run: + +| Workflow | File | Purpose | +| ---------- | --------------------- | ---------------------------------------------------------------------------------- | +| **CI** | `ci.yml` | Builds the action (`npm run package`), runs it against the repository (`uses: ./`) | +| **Test** | `test.yml` | Runs Vitest suite (`npm run test`), then SonarQube coverage analysis | +| **Lint** | `lint.yml` | Biome check (`npm run check`) + GitHub Super-Linter | +| **CodeQL** | `codeql-analysis.yml` | Security analysis for TypeScript | + +### Release Workflows + +| Workflow | File | Trigger | Purpose | +| ----------------- | ------------------- | ----------------- | ----------------------------------------------------------------------------------------- | +| **Release Start** | `release-start.yml` | Manual dispatch | Validates version, bumps package.json, builds, generates AI changelog, creates release PR | +| **Check Dist** | `check-dist.yml` | Release PR | Verifies `dist/` matches `npm run package` output | +| **Release** | `release.yml` | Release PR merged | Creates Git tag + GitHub release with notes | + +### Release Process + +1. Maintainer manually triggers **Release Start** workflow with a version number +2. Workflow: validates version → bumps package.json → runs build + tests → generates changelog (via OpenAI in + `scripts/changelog.js`) → creates PR titled `chore(release): vX.Y.Z` +3. The release PR triggers all standard CI workflows +4. After review and merge, **Release** workflow creates the Git tag and GitHub release + +> **Important**: Contributors should never manually create releases, modify `dist/`, or check in bundle artifacts. + +## Build + +### Package for Distribution + +```bash +npm run package # Build dist/ via @vercel/ncc +``` + +This bundles `src/index.ts` and all dependencies into `dist/index.js` (single file) with source maps. The `dist/` +directory is only committed during the automated release process. + +### Action Runtime + +- `action.yml` specifies `node20` as the runtime +- The action runs on GitHub Actions runners (Ubuntu) with Node.js 20+ +- Entry point: `dist/index.js` + +## Key Scripts + +| Script | Command | Purpose | +| -------------- | --------------------------------------------------- | ----------------------------- | +| `check` | `biome check . && prettier -c ...` | Lint check (no changes) | +| `check:fix` | `biome check --write --unsafe . && prettier -w ...` | Autofix linting issues | +| `textlint` | `textlint -c ... **/*.md` | Check Markdown prose | +| `textlint:fix` | `textlint --fix ...` | Fix Markdown prose | +| `typecheck` | `tsc --noEmit` | TypeScript type checking | +| `test` | `vitest run --coverage` | Full test suite with coverage | +| `test:watch` | `vitest` | Watch mode for development | +| `package` | `ncc build src/index.ts -o dist` | Build distribution bundle | +| `coverage` | `make-coverage-badge --output-path ...` | Generate coverage badge SVG | diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 00000000..a786b30f --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,190 @@ +# Testing Guide + +This document covers the test infrastructure, mock architecture, and patterns for writing tests in the Terraform Module +Releaser project. + +## Overview + +- **Framework**: Vitest with V8 coverage +- **Test location**: `__tests__/` mirrors `src/` structure +- **Mock location**: `__mocks__/` for module-level mocks +- **Coverage scope**: `src/` only (excludes tests, mocks, `src/types/`) + +## Running Tests + +```bash +npm run test # Full suite with coverage report +npm run test:watch # Watch mode for development (re-runs on file changes) +``` + +Some tests require the `GITHUB_TOKEN` environment variable for real API calls. Tests without it skip gracefully. + +## Test Setup (`__tests__/_setup.ts`) + +The global setup file runs before every test file: + +1. **Auto-mocks**: `vi.mock('@actions/core')`, `vi.mock('@/config')`, `vi.mock('@/context')` +2. **Environment variables**: Sets required GitHub env vars (`GITHUB_EVENT_NAME`, `GITHUB_REPOSITORY`, etc.) +3. **Input setup**: Calls `setupTestInputs()` in `beforeEach` to set `INPUT_*` env vars from action.yml defaults +4. **Console silencing**: Stubs `console.time`/`console.timeEnd` to reduce noise + +## Mock Architecture + +The project uses a sophisticated 3-tier mock system: + +### Tier 1: `__mocks__/@actions/core.ts` + +Replaces the `@actions/core` package globally. All functions are `vi.fn()` spies. + +- **Silenced**: `info`, `debug`, `warning`, `error`, `notice`, `startGroup`, `endGroup`, `group` +- **Real implementations**: `getInput` and `getBooleanInput` delegate to actual implementations (read `INPUT_*` env + vars), enabling tests to control action inputs via environment variables +- **`setFailed`**: Throws an error in tests for easier assertion + +### Tier 2: `__mocks__/config.ts` + +Proxy-based mock config singleton with two helper methods: + +- **`.set({...})`** — Override specific config values for a test +- **`.resetDefaults()`** — Restore all values to action.yml defaults + +The mock loads real defaults by calling `createConfigFromInputs()` from `src/utils/metadata.ts`, which reads `INPUT_*` +env vars set by `setupTestInputs()`. + +```typescript +// In a test: +import { config } from "@/config"; +config.set({ deleteLegacyTags: false, disableWiki: true }); +// ... run test ... +config.resetDefaults(); // restore in beforeEach/afterEach +``` + +### Tier 3: `__mocks__/context.ts` + +Proxy-based mock context with helper methods: + +- **`.set({...})`** — Override specific context values +- **`.reset()`** — Restore defaults +- **`.useRealOctokit()`** — Switch to real authenticated Octokit client (requires `GITHUB_TOKEN`) +- **`.useMockOctokit()`** — Switch back to mock Octokit (default) + +Default context provides: mock repository info (`techpivot/terraform-module-releaser`), PR number 1, workspace +directory, and a mock Octokit instance. + +## Test Helpers (`__tests__/helpers/`) + +### `action-defaults.ts` + +Reads `action.yml` at test time to extract input defaults. Ensures tests always match production defaults even when +action.yml changes. + +### `inputs.ts` + +- **`setupTestInputs(overrides?)`** — Sets `INPUT_*` env vars from action.yml defaults, with optional overrides +- Exports categorized arrays: `requiredInputs`, `optionalInputs`, `booleanInputs`, `arrayInputs`, `stringInputs`, + `numberInputs` +- Used in `_setup.ts` beforeEach to ensure clean input state + +### `octokit.ts` + +Sophisticated Octokit mock system: + +- **`MockStore`** — Internal store for mock return values and implementations +- **`stubOctokitReturnData(path, method, data)`** — Set mock return value for a specific API call +- **`stubOctokitImplementation(path, method, fn)`** — Set mock implementation for complex scenarios +- **`createRealOctokit()`** — Creates real authenticated Octokit (for integration tests) + +```typescript +// Mock a specific API call: +stubOctokitReturnData('repos', 'listTags', [{ name: 'module/v1.0.0', ... }]); + +// Mock with custom logic: +stubOctokitImplementation('repos', 'listTags', async (params) => { + return { data: [...], status: 200 }; +}); +``` + +### `terraform-module.ts` + +Factory functions for test fixtures: + +- **`createMockTerraformModule(overrides?)`** — Creates a `TerraformModule` instance with sensible defaults +- **`createMockTag(overrides?)`** — Creates a mock `GitHubTag` +- **`createMockTags(count, overrides?)`** — Creates an array of mock tags + +## Writing New Tests + +### Structure + +```typescript +import { describe, it, expect, beforeEach, vi } from "vitest"; +import { config } from "@/config"; +import { context } from "@/context"; + +describe("featureName", () => { + beforeEach(() => { + config.resetDefaults(); + context.reset(); + }); + + it("should describe expected behavior clearly", async () => { + // Arrange + config.set({ someOption: "value" }); + stubOctokitReturnData("repos", "listTags", mockTags); + + // Act + const result = await functionUnderTest(); + + // Assert + expect(result).toBe(expectedValue); + }); +}); +``` + +### Best Practices + +1. **Descriptive test names** — Use `it('should ...')` format that explains expected behavior +2. **Reset state** — Always reset config/context in `beforeEach` +3. **Minimal mocking** — Only mock what's necessary; let real code run where possible +4. **Isolated tests** — Each test should be independent; no shared mutable state between tests +5. **Use factories** — Prefer `createMockTerraformModule()` over manual object construction +6. **Integration tests** — Gate behind `GITHUB_TOKEN` check: + +```typescript +const describeWithToken = process.env.GITHUB_TOKEN ? describe : describe.skip; +describeWithToken("integration tests", () => { + beforeEach(() => { + context.useRealOctokit(); + }); + // ... +}); +``` + +### Wiki Test Fixtures + +Wiki fixture files in `__tests__/fixtures/` use Unicode characters in filenames to match the wiki slug behavior: + +- `∕` (U+2215) replaces `/` in paths +- `‒` (U+2012) replaces `-` in module names + +Handle these carefully when creating or modifying fixtures. + +## Path Aliases + +Configured in both `tsconfig.json` and `vitest.config.ts`: + +| Alias | Maps to | +| ---------- | ------------ | +| `@/` | `src/` | +| `@/tests/` | `__tests__/` | +| `@/mocks/` | `__mocks__/` | + +## Coverage + +V8 coverage is collected on `src/` with these exclusions: + +- `__tests__/` — Test files +- `__mocks__/` — Mock files +- `src/types/` — Type-only files (no runtime code) + +Coverage reporters: `json-summary`, `text` (console), `lcov` (HTML + SonarQube). diff --git a/src/pull-request.ts b/src/pull-request.ts index ad2f9a8a..19081627 100644 --- a/src/pull-request.ts +++ b/src/pull-request.ts @@ -234,7 +234,7 @@ export async function addReleasePlanComment( '|--|--|--|--|--|', ); for (const module of terraformModulesToRelese) { - // Prevent module name from wrapping on hyphens in table cells (Doesn't work reliabily) + // Prevent module name from wrapping on hyphens in table cells (Doesn't work reliably) const name = `${module.name}`; const type = module.getReleaseType(); const latestVersion = module.getLatestTagVersion() ?? ''; @@ -404,7 +404,7 @@ export async function addPostReleaseComment(releasedTerraformModules: TerraformM issueNumber: issue_number, } = context; - // Contruct the comment body as an array of strings + // Construct the comment body as an array of strings const commentBody: string[] = [ PR_RELEASE_MARKER, '\n## :rocket: Terraform Module Releases\n',