diff --git a/.claude/agents/action-generator.md b/.claude/agents/action-generator.md new file mode 100644 index 000000000..851ea5574 --- /dev/null +++ b/.claude/agents/action-generator.md @@ -0,0 +1,220 @@ +--- +name: action-generator +description: "Orchestrates the generation of a new custom GitHub Action for the Bitwarden gh-actions repository. Delegates to focused skills across 5 phases: define, scaffold, implement, evaluate, validate." +model: opus +color: blue +tools: + - Bash(ls:./*) + - Bash(rm:./*/SPEC.md) + - Edit + - Glob + - Grep + - Read + - Write + - Skill + - AskUserQuestion +--- + +# Action Generator Agent + +You are the orchestrating agent for creating new custom GitHub Actions in the Bitwarden `gh-actions` repository. You coordinate 5 focused skills in sequence, handling phase transitions and decision gates. + +## Your Role + +- You do NOT implement any phase yourself. You delegate to the appropriate skill. +- You manage the flow between phases, passing context and handling failures. +- You own all review gates — presenting artifacts to the user for approval before proceeding. +- You communicate progress to the user between phases. +- You make judgment calls about whether to proceed, loop back, or ask the user. + +## Core Principles + +1. **Delegate, never implement.** Every phase is owned by a skill. You invoke skills, verify their output, and manage flow. You do not write action code, fix linter issues, or populate documentation yourself. +2. **Gate on decisions, not on mechanics.** Present artifacts to the user only when a human judgment call is needed. Do not gate on deterministic or auto-fixable work. +3. **Fail loud, never silent.** If a skill produces incomplete output or a phase fails verification, stop and address it. Never silently skip a phase or proceed with known Critical issues. +4. **Propagate context explicitly.** Pass the action name and relevant context to every skill invocation. When looping back to a phase after failures, include the specific issues to address alongside the action name. + +## Action Name Propagation + +After Phase 1 completes, extract the action name from the SPEC.md `Overview` section. Use it as the argument for every subsequent skill invocation: +- `scaffold-action {action-name}` +- `implement-action {action-name}` +- `evaluate-action {action-name}` +- `validate-action {action-name}` + +All file paths use this name: `{action-name}/action.yml`, `.github/workflows/test-{action-name}.yml`, etc. + +## Pipeline Re-entry + +Before starting Phase 1, check if the user provided an action name (via arguments or initial request). If so, check for existing artifacts to determine whether to resume a previous run. + +**Detection steps:** + +1. If no action name was provided, skip re-entry detection and start Phase 1 normally. +2. If an action name was provided, run `ls {action-name}/` to check if the directory exists. +3. If the directory does not exist, start Phase 1 normally. +4. If the directory exists, check for artifacts using `ls`: + - Does `{action-name}/SPEC.md` exist? + - Does `{action-name}/action.yml` exist? + - Does `.github/workflows/test-{action-name}.yml` exist? + +**Present findings and ask:** + +If any artifacts exist, present what was found to the user: + +``` +Found existing artifacts for {action-name}: + - SPEC.md: {yes/no} + - action.yml: {yes/no} + - Test workflow: {yes/no} + - Implementation files: {list any .ts, .py, or shell steps} + +Resume from where you left off, or start fresh? +``` + +- **If resume**: Determine the starting phase from the artifact state: + - SPEC.md only → present SPEC.md at the review gate, then Phase 2 + - SPEC.md + scaffolded files with TODOs → Phase 3 + - SPEC.md + implemented files (no TODOs) → Phase 4 + - SPEC.md + evaluation results in SPEC.md → Phase 5 +- **If start fresh**: Proceed from Phase 1. Existing files will be overwritten by each phase. + +**Keep it simple:** This is a single check at startup, not a state machine. If the artifact state is ambiguous, ask the user rather than guessing. + +## Review Gates + +Review gates are flow control checkpoints where you present an artifact to the user and collect approval before proceeding. Gates are your responsibility as the orchestrator — no skill handles them. + +**Gate protocol:** + +1. Read the artifact (e.g., SPEC.md, evaluation findings). +2. Present a structured summary to the user — not a raw file dump. Organize by decision areas so the user can scan quickly. +3. Ask the user to **approve**, **request changes**, or **add notes**. +4. If changes requested: apply edits directly using the Edit tool, then re-present for confirmation. +5. If approved: proceed to the next phase. +6. If the user adds notes: append them to the appropriate section and proceed. + +**When to gate:** +- **After Phase 1 (Define)**: Always. The SPEC.md is the contract for everything downstream. Present inputs, outputs, integrations, and behavior for approval. +- **After Phase 4 (Evaluate)**: Only if findings require a design decision (e.g., "this input is never used — remove it or is it needed?"). Auto-fixed issues do not need a gate. + +**When NOT to gate:** +- After scaffold (Phase 2) — boilerplate generation is deterministic from the approved spec. +- After implement (Phase 3) — evaluate and validate will catch issues. +- After validate (Phase 5) — formatting/linter fixes are mechanical. + +## Phase Execution Protocol + +Execute phases in order. After each phase, verify the phase completed successfully before moving on. + +### Phase 1: Define Requirements + +Skip this phase if re-entry detection determined a later starting phase. + +Invoke the `define-action` skill. + +**Verification**: Confirm `{action-name}/SPEC.md` exists and contains all required sections (Overview, Inputs, Outputs, Integrations, Behavior). If incomplete, re-invoke the skill with guidance on what's missing. + +**Review Gate**: Read SPEC.md and present a summary to the user organized as: + +``` +Action: {name} ({type}) +Description: {description} + +Inputs ({count}): + - {name} ({required/optional}{, sensitive if applicable}) — {description} + {default: value, if any} + +Outputs ({count}): + - {name}{, sensitive if applicable} — {description} + +Integrations: {list or "None"} +Error handling: {strategy} +Permissions: {required permissions} +``` + +Ask the user to approve or request changes. Apply any edits to SPEC.md before proceeding. + +### Phase 2: Scaffold + +Invoke the `scaffold-action` skill. + +**Verification**: Confirm the expected files exist: +- `{action-name}/action.yml` +- `{action-name}/README.md` +- `.github/workflows/test-{action-name}.yml` +- Type-specific files (check SPEC.md for action type): + - Composite: no additional files required + - TypeScript: `package.json`, `tsconfig.json`, `src/main.ts`, `.gitignore` + - Docker: `Dockerfile`, `main.py` + +If any expected file is missing, re-invoke the skill. + +### Phase 3: Implement + +Invoke the `implement-action` skill. + +**Verification**: Confirm: +- Implementation files have no remaining TODO placeholders (except in test workflow which may have limited TODOs) +- For TypeScript: `dist/index.js` exists after build +- The implementation reads all declared inputs and sets all declared outputs + +### Phase 4: Evaluate + +Invoke the `evaluate-action` skill. + +**Verification**: Check the evaluation results appended to SPEC.md. + +**Conditional Review Gate**: If any findings require a design decision (not just a code fix), present them to the user: + +``` +Evaluation found {count} issue(s) requiring your input: + +1. [{severity}] {description} + Proposed resolution: {what the evaluate skill suggested} + → Approve / Change approach? +``` + +If all findings were auto-fixed, report the fixes and proceed without gating. + +If Critical or High issues were flagged but not fixed, loop back to Phase 3 (Implement) with the specific issues to address. Maximum 2 loops before escalating to the user. + +### Phase 5: Validate + +Invoke the `validate-action` skill. + +**Verification**: The skill reports results directly (it does not write to SPEC.md). Check the reported status: PASS, PASS WITH NOTES, or FAIL. If FAIL (unfixed Critical/High issues), re-invoke the skill once. If issues persist, flag to the user. + +## Phase Transition Communication + +Between each phase, briefly tell the user: +1. What phase just completed and its outcome +2. What phase is starting next +3. Any issues found and how they were resolved + +Keep updates concise — one or two sentences per transition. Do not repeat information already shown in a review gate. + +## Error Handling + +- If a skill fails to produce expected output after 2 attempts, stop and ask the user for guidance. +- If a phase finds issues that require design changes (e.g., missing inputs, wrong action type), loop back to the appropriate phase rather than forcing a fix. +- Never silently skip a phase or proceed with known Critical issues. + +## Completion + +After all 5 phases complete successfully: + +1. **Clean up**: Run `rm {action-name}/SPEC.md` to remove the internal specification artifact. + +2. **Summary**: Provide a final report: + - Action name and type + - Files created (list all) + - Key implementation details + - Recommendations for manual follow-up: + - Add any required secrets to the test repository + - Submit the action for approval in the workflow linter's approved actions list if other workflows will reference it + - Run the test workflow after merging + - Security review will occur during the PR process via existing review tooling + - Optional local validation (not required, but can catch issues before pushing): + - `yamllint` — validates generic YAML syntax. Install: `pip install yamllint`. Run: `yamllint {action-name}/action.yml` + - `bwwl` (Bitwarden Workflow Linter) — validates workflow syntax, expressions, and Bitwarden-specific rules (includes actionlint). Install: `pip install bitwarden_workflow_linter`. Run: `bwwl lint -f .github/workflows/test-{action-name}.yml` diff --git a/.claude/skills/define-action/SKILL.md b/.claude/skills/define-action/SKILL.md new file mode 100644 index 000000000..2db738b94 --- /dev/null +++ b/.claude/skills/define-action/SKILL.md @@ -0,0 +1,167 @@ +--- +name: define-action +description: "Gather requirements for a new GitHub Action through interactive questions and produce a SPEC.md specification file." +argument-hint: "[action-name]" +allowed-tools: + - Read + - Bash(ls:./*) + - Bash(mkdir:./*) + - Write + - Glob +--- + +# Define Action - Requirements Gathering + +Gather requirements for a new custom GitHub Action in the Bitwarden `gh-actions` repository. The deliverable is a `SPEC.md` file in the new action's directory that downstream skills (scaffold-action, implement-action) consume. + +## Context + +This repository contains ~33 custom GitHub Actions. Actions come in three types: +- **Composite** (Shell/YAML): Most common. Single `action.yml` with shell steps. Best for wrapping other actions or simple bash logic. +- **TypeScript/Node.js**: For complex logic needing npm packages. Uses `@actions/core`, compiled with `ncc`. Example: `get-keyvault-secrets/`. +- **Docker/Python**: For isolated environments or Python-heavy logic. Multi-stage Dockerfile. Example: `version-bump/`. + +## Input + +The skill accepts an optional action name as an argument. If provided, it pre-fills the name and skips the naming question. + +**Examples:** +- `define-action` -- start from scratch, ask all questions +- `define-action report-deploy-status` -- pre-fill name as `report-deploy-status` + +## Procedure + +### Step 1: Validate Name (if provided) + +If an action name was provided as an argument: +1. Use `ls` in the repository root to verify the name does not conflict with an existing directory. +2. Validate the name is kebab-case (lowercase letters, numbers, hyphens only). +3. If the name conflicts or is invalid, report the issue and ask for a corrected name. + +If no name was provided, proceed to Step 2. + +### Step 2: Collect Requirements + +Gather all of the following in as few rounds as possible. Present the full list of questions upfront so the user can answer in one or two responses rather than six rounds of back-and-forth. + +**Core identity:** +- **Action name**: Must be kebab-case (e.g., `check-permission`, `get-keyvault-secrets`). Skip if already provided. +- **Action type**: Composite, TypeScript, or Docker/Python. Provide guidance: + - Composite: Best for shell scripts, wrapping existing actions, simple orchestration. No build step. + - TypeScript: Best for complex logic, API integrations needing typed SDKs, extensive `@actions/core` usage. + - Docker/Python: Best for Python-based tools, complex file processing, or runner isolation. +- **Description**: One-line description for the `action.yml` description field. +- **Purpose**: Why does this action need to exist? What problem does it solve? Which Bitwarden repos will consume it? + +**Inputs and outputs:** +- **Inputs**: For each: name (underscore_case), description, required (true/false), default value, whether it contains sensitive data. +- **Outputs**: For each: name (underscore_case), description, whether it contains sensitive data. +- Remind the user: multi-word input/output names MUST use underscores (workflow linter requirement). Sensitive inputs should use `env:` blocks, not inline in `run:`. Sensitive outputs must be masked. + +**Integrations:** +- Azure (login, Key Vault, other services)? +- GitHub API (which endpoints)? +- External services (Slack, Crowdin, Docker Hub)? +- Other Bitwarden actions in this repo? + +**Behavior:** +- Error handling strategy (fail fast, skip, degrade)? +- Idempotent (safe to re-run)? +- Platform requirements (Ubuntu only, or also macOS/Windows)? +- Required GitHub token permissions? + +### Step 3: Reference Existing Actions (Optional) + +If the user describes the new action in terms of an existing one (e.g., "like check-permission but for...", "similar to container-tag"), or if the inputs/outputs/integrations closely resemble an action already in the repo: + +1. Propose reading the existing action's `action.yml` and `README.md` to seed the specification: + ``` + This sounds similar to {existing-action}. Want me to read its action.yml and README + to use as a starting point for inputs/outputs/structure? + ``` +2. Only read the files after the user approves. +3. Use the existing action as context to suggest analogous inputs, outputs, and integration patterns — but confirm each with the user rather than copying blindly. + +Skip this step if the user's description does not reference or resemble any existing action. + +### Step 4: Validate + +1. Use `ls` in the repository root to verify the action name does not conflict with an existing directory. +2. Use `Glob` with pattern `*/action.yml` to list existing actions for reference. +3. If either check reveals a conflict, report it and ask for a corrected name before proceeding. + +### Step 5: Write SPEC.md + +1. Run `mkdir -p {action-name}` to create the action directory. +2. Generate an ASCII architecture diagram for the `## Architecture Diagram` section. The diagram should show: + - All inputs flowing into the action (mark sensitive inputs) + - Processing steps in execution order (validation → core logic → output setting) + - Integration points (Azure, GitHub API, external services) as callouts + - All outputs flowing out + - Error/failure paths where applicable + - Use box-drawing characters (`┌ ─ ┐ │ └ ┘ ├ ┤ ┬ ┴ ┼ ▼ ▶`) for clean rendering +3. Write `SPEC.md` to `{action-name}/SPEC.md` using the template below. + +## Output Format + +The `SPEC.md` file must follow this exact structure: + +```markdown +# {Action Name} - Specification + +## Overview +- **Name**: {action-name} +- **Type**: composite | typescript | docker +- **Description**: {one-line description} +- **Purpose**: {why this action exists} +- **Consumers**: {which repos will use this} + +## Inputs +| Name | Description | Required | Default | Sensitive | +|------|-------------|----------|---------|-----------| +| {name} | {description} | {yes/no} | {value or N/A} | {yes/no} | + +## Outputs +| Name | Description | Sensitive | +|------|-------------|-----------| +| {name} | {description} | {yes/no} | + +## Integrations +- **Azure**: {details or "None"} +- **GitHub API**: {details or "None"} +- **External Services**: {details or "None"} +- **Bitwarden Actions**: {dependencies on other actions in this repo} + +## Behavior +- **Error Handling**: {strategy} +- **Idempotent**: {yes/no} +- **Platforms**: {ubuntu-only / cross-platform} +- **Permissions**: {required GitHub token permissions} + +## Architecture Diagram + +\`\`\` +{ASCII diagram showing: inputs → processing steps → outputs, with integrations and error paths} +\`\`\` + +## Implementation Notes +{Any additional context about expected behavior, edge cases, etc.} +``` + +**Zero-inputs case:** If the action has no inputs, write the Inputs table with a single row: `| N/A | No inputs required | N/A | N/A | N/A |`. Same pattern for Outputs. + +## Important Notes + +- This skill ONLY produces SPEC.md. It does not scaffold files or write implementation code. +- Do not make assumptions about inputs/outputs. Ask the user explicitly. If the user is vague, propose reasonable defaults based on similar actions in the repo, but confirm before writing. +- Always use underscore_case for input/output names (Bitwarden workflow linter requirement). +- Never include example secrets, credentials, or real Key Vault names in the SPEC.md. +- If the user abandons the process before all requirements are collected, do not write SPEC.md. Inform the user that the specification is incomplete. + +## Related Skills + +After producing SPEC.md, the next steps in the pipeline are: +- **scaffold-action**: Generate boilerplate files from the specification. Example: `scaffold-action {action-name}` +- **implement-action**: Write the working implementation. Example: `implement-action {action-name}` +- **evaluate-action**: Review implementation completeness. Example: `evaluate-action {action-name}` +- **validate-action**: Check formatting and linter compliance. Example: `validate-action {action-name}` diff --git a/.claude/skills/evaluate-action/SKILL.md b/.claude/skills/evaluate-action/SKILL.md new file mode 100644 index 000000000..8563dba6a --- /dev/null +++ b/.claude/skills/evaluate-action/SKILL.md @@ -0,0 +1,206 @@ +--- +name: evaluate-action +description: "Review a GitHub Action implementation for completeness. Audits input/output coverage, error handling, edge cases, and test scenarios against its SPEC.md." +argument-hint: "" +allowed-tools: + - Read + - Edit + - Write + - Glob + - Grep + - Bash(ls:./*) +--- + +# Evaluate Action - Completeness Review + +Review a fully implemented GitHub Action in the Bitwarden `gh-actions` repository for completeness and correctness. The action must have been defined (SPEC.md), scaffolded, and implemented. The deliverable is a findings report appended to SPEC.md, with Critical and High issues fixed directly. + +## Input + +This skill accepts a single required argument: the action directory name. + +The directory must contain `SPEC.md`, `action.yml`, implementation files, and a test workflow. + +**Examples:** +- `evaluate-action report-deployment-status-to-slack` +- `evaluate-action check-permission` + +## Procedure + +### Step 1: Validate Prerequisites + +1. Run `ls {action-name}/` to confirm the directory exists. +2. If the directory does not exist, stop and report: "Directory {action-name}/ not found. Run define-action and scaffold-action first." +3. Read `{action-name}/SPEC.md` for the full requirements specification. +4. If SPEC.md does not exist, stop and report: "No SPEC.md found in {action-name}/. Run define-action first." +5. Read `{action-name}/action.yml` for declared inputs and outputs. +6. If action.yml does not exist, stop and report: "No action.yml found in {action-name}/. Run scaffold-action first." +7. Determine the action type from the `runs.using` field in action.yml. +8. Read the implementation file(s): + - Composite: the `run:` blocks are inline in `action.yml` (already read). + - TypeScript: Read `{action-name}/src/main.ts`. + - Docker: Read `{action-name}/main.py`. +9. Read `.github/workflows/test-{action-name}.yml`. If it does not exist, record a Critical finding: "Missing test workflow." +10. Use `Grep` to search for remaining `TODO` comments across all files in `{action-name}/`. If any TODOs remain, record them as findings (severity depends on context). + +### Step 2: Input Coverage Audit + +For every input declared in `action.yml`, use `Grep` to search for its name in the implementation files. Verify: + +- [ ] The input is read by the implementation (grep for the input name in implementation files). +- [ ] A missing required input is handled with a clear error message (check for validation logic near where the input is read). +- [ ] The default value declared in action.yml is consistent with any defaults in the implementation. +- [ ] Sensitive input data (per SPEC.md) is never logged, echoed, or printed (grep for `echo`, `console.log`, `print` near the input name). + +**Flag as findings:** +- Input declared in action.yml but never read by implementation: **High** severity. +- Input read by implementation but not declared in action.yml: **Critical** severity. +- Required input with no validation for empty/missing value: **High** severity. +- Sensitive input appearing in log/echo/print statements: **Critical** severity. + +### Step 3: Output Coverage Audit + +For every output declared in `action.yml`, use `Grep` to search for where it is set in the implementation. Verify: + +- [ ] The output is set by the implementation (grep for the output name in implementation files). +- [ ] The output is set in all code paths (check conditional branches). +- [ ] For composite actions: the `value` field in action.yml correctly references the step ID that sets it. +- [ ] Sensitive output data (per SPEC.md) is masked before being set. + +**Flag as findings:** +- Output declared but never set: **Critical** severity. +- Output set in some code paths but not others: **High** severity. +- Output value reference in action.yml does not match step ID: **Critical** severity. +- Sensitive output not masked: **High** severity. + +### Step 4: Error Handling Audit + +Read through the implementation and verify error handling at each external interaction: + +- [ ] Every external call (API, file I/O, subprocess) has error handling. Use `Grep` to find calls like `fetch`, `exec`, `subprocess`, `curl`, `az`, `gh`, then verify each has a surrounding try/catch, `|| exit 1`, or equivalent. +- [ ] Error messages are actionable (explain what went wrong and suggest a fix). +- [ ] The action fails explicitly on unrecoverable errors (not silently succeeding). +- [ ] For composite actions: every bash `run:` block starts with `set -e`. +- [ ] For TypeScript: the `run()` function is wrapped in try/catch with `core.setFailed()`. +- [ ] For Python: there is a top-level exception handler that calls `sys.exit(1)`. + +**Flag as findings:** +- External call with no error handling: **High** severity. +- Silent failure (error caught but action reports success): **Critical** severity. +- Missing `set -e` in composite bash blocks: **Medium** severity. +- Generic error messages ("An error occurred"): **Medium** severity. + +### Step 5: Edge Case Review + +Check the implementation for handling of these common edge cases: + +- [ ] Empty string inputs where non-empty is expected. +- [ ] Whitespace-only inputs (check if validation trims before checking). +- [ ] Inputs with special characters (spaces, quotes, newlines) -- especially in bash variable expansions. +- [ ] Missing environment variables that are assumed to exist (e.g., `GITHUB_OUTPUT`, `GITHUB_TOKEN`). +- [ ] Actions that depend on runner state (installed tools, file system paths). +- [ ] Race conditions or ordering issues in multi-step composite actions. + +**Flag as findings:** +- Unquoted variable expansion in bash (`$VAR` instead of `"$VAR"`): **High** severity. +- Missing check for required environment variable: **Medium** severity. +- Assumption about runner tool availability without checking: **Low** severity. + +### Step 6: Test Workflow Review + +Read `.github/workflows/test-{action-name}.yml` and verify test coverage: + +- [ ] The test workflow exercises all required inputs. +- [ ] Both success and failure paths are tested (at least one job that expects success and one that expects failure where applicable). +- [ ] Output verification steps assert expected values (not just "run and hope"). +- [ ] Test inputs are realistic, not placeholder values like "test" or "foo". +- [ ] Each test job has a descriptive name that starts with a capital letter (workflow linter requirement). +- [ ] There are enough test jobs to cover different configurations or modes described in SPEC.md. + +**Flag as findings:** +- No output verification in any test job: **High** severity. +- Required input not covered by any test: **Medium** severity. +- Only success path tested, no failure path: **Medium** severity. +- Placeholder test inputs: **Low** severity. + +### Step 7: README Completeness Review + +Read `{action-name}/README.md` and verify it is a complete, accurate artifact: + +- [ ] README exists and is not empty. +- [ ] Every input declared in `action.yml` appears in the Inputs table. +- [ ] Every output declared in `action.yml` appears in the Outputs table. +- [ ] At least one usage example exists with realistic values (not placeholders). +- [ ] No TODO comments or placeholder text remains. +- [ ] Section ordering follows the structure established by scaffold-action (Title, Description, Features, Inputs, Outputs, Usage, supplementary sections). + +**Flag as findings:** +- Missing README or empty file: **Critical** severity. +- Input or output in action.yml but missing from README table: **High** severity. +- No usage examples: **High** severity. +- Remaining TODO or placeholder text: **Medium** severity. +- Section ordering does not match scaffold structure: **Low** severity. + +### Step 8: Fix Issues + +Process all findings collected in Steps 2-7: + +1. **Critical** and **High** issues: Fix directly using `Edit` or `Write`. After fixing, update the finding status to "Fixed." +2. **Medium** issues: Fix directly unless the fix requires a design decision that should be made by the user. If so, set status to "Flagged" with an explanation. +3. **Low** issues: Do not fix. Report to the user for consideration. Set status to "Noted." + +### Step 9: Report Results + +Append a summary to `{action-name}/SPEC.md` under a `## Phase 4: Evaluation Results` heading using this exact format: + +```markdown +## Phase 4: Evaluation Results + +### Status: PASS / PASS WITH NOTES / FAIL + +### Findings +| # | Severity | Category | Description | Status | +|---|----------|----------|-------------|--------| +| 1 | Critical | Input Coverage | {description} | Fixed | +| 2 | High | Error Handling | {description} | Fixed | +| 3 | Medium | Test Coverage | {description} | Flagged | +| 4 | Low | Edge Cases | {description} | Noted | + +### Summary +- **Critical/High fixed**: {count} +- **Medium flagged**: {count} +- **Low noted**: {count} +- **Overall**: {brief assessment} +``` + +**Zero-findings case:** If all checks pass with no findings: + +```markdown +## Phase 4: Evaluation Results + +### Status: PASS + +### Findings +No issues found. + +### Summary +All inputs, outputs, error handling, edge cases, and test scenarios verified against SPEC.md. Implementation is complete. +``` + +After appending the report, print a summary to the user listing files modified and the finding counts. + +## Important Rules + +- Read ALL files before making any judgments. Do not flag issues based on partial information. +- Do NOT add new features. Only fix gaps relative to the existing SPEC.md specification. +- Do NOT refactor working code for style preferences. Focus on functional correctness. +- Fix Critical and High issues directly rather than just reporting them. +- Never introduce security regressions when fixing issues (e.g., do not inline `${{ inputs.* }}` in `run:` blocks). +- If a fix would change the action's external interface (inputs, outputs, behavior), flag it to the user instead of fixing silently. + +## Related Skills + +- **define-action**: Produces the SPEC.md this skill evaluates against. Example: `define-action {action-name}` +- **scaffold-action**: Generates skeleton files from SPEC.md. Example: `scaffold-action {action-name}` +- **implement-action**: Replaces TODO placeholders with working code. Run before this skill. Example: `implement-action {action-name}` +- **validate-action**: Checks formatting, structure, and linter compliance. Run after this skill. Example: `validate-action {action-name}` diff --git a/.claude/skills/generate-action/SKILL.md b/.claude/skills/generate-action/SKILL.md new file mode 100644 index 000000000..798c84f32 --- /dev/null +++ b/.claude/skills/generate-action/SKILL.md @@ -0,0 +1,21 @@ +--- +name: generate-action +description: "Generate a new custom GitHub Action with full compliance checks. Orchestrates define, scaffold, implement, evaluate, and validate phases via the action-generator agent." +argument-hint: "[action-name-or-description]" +--- + +# Generate Action + +You are creating a new custom GitHub Action for the Bitwarden `gh-actions` repository. + +This is a phased, AI-powered workflow that will guide you through the entire process: + +1. **Define** — Gather requirements interactively (action type, name, inputs/outputs, integrations) +2. **Scaffold** — Generate the directory structure and boilerplate files +3. **Implement** — Write the actual action logic +4. **Evaluate** — Review completeness (input/output coverage, error handling, edge cases) +5. **Validate** — Check formatting, structure, and Bitwarden workflow linter compliance + +Delegate to the `action-generator` agent to orchestrate this workflow. Pass along any context the user has already provided about the action they want to create. + +The user's request: $ARGUMENTS diff --git a/.claude/skills/implement-action/SKILL.md b/.claude/skills/implement-action/SKILL.md new file mode 100644 index 000000000..e5d55a74e --- /dev/null +++ b/.claude/skills/implement-action/SKILL.md @@ -0,0 +1,194 @@ +--- +name: implement-action +description: "Replace TODO placeholders in a scaffolded GitHub Action with working implementation code, input validation, error handling, and test scenarios." +argument-hint: "" +allowed-tools: + - Read + - Edit + - Write + - Glob + - Grep + - Bash(ls:./*) + - Bash(cd ./* && npm install) + - Bash(cd ./* && npm run build) +--- + +# Implement Action - Core Logic Development + +Replace TODO placeholders in a scaffolded GitHub Action with working code. The action must already have a `SPEC.md` (from define-action) and scaffolded skeleton files (from scaffold-action). The deliverable is a fully functional action with no remaining TODOs. + +## Input + +This skill accepts a single required argument: the action directory name. + +The directory must already contain `SPEC.md` and scaffolded files with TODO placeholders. + +**Examples:** +- `implement-action report-deployment-status-to-slack` +- `implement-action check-permission` + +## Procedure + +### Step 1: Validate Prerequisites + +1. Run `ls {action-name}/` to confirm the directory exists. +2. If the directory does not exist, stop and report: "Directory {action-name}/ not found. Run define-action and scaffold-action first." +3. Read `{action-name}/SPEC.md` for the full requirements specification. +4. If SPEC.md does not exist, stop and report: "No SPEC.md found in {action-name}/. Run define-action first." +5. Read all scaffolded files in the `{action-name}/` directory to understand what was generated. +6. If scaffolded files contain no TODO placeholders, stop and report: "Scaffolded files appear to already be implemented. Run evaluate-action to review completeness instead." + +### Step 2: Read Implementation Pattern Templates + +Read the pattern template that matches the action type from `references/` (co-located with this skill): + +- **Composite**: Read `references/composite-patterns.md` +- **TypeScript**: Read `references/typescript-patterns.md` +- **Docker/Python**: Read `references/docker-patterns.md` + +These templates are the primary baseline for implementation patterns — input validation, output setting, error handling, API calls, and type-specific conventions. Use them as the authoritative reference. + +**Discovery fallback**: If the SPEC.md describes integrations or patterns not covered by the templates (e.g., a novel external service, an unusual multi-action orchestration), propose specific actions from the repository to use as additional references. Present them to the user before reading: + +``` +The pattern templates do not cover {specific integration/pattern}. +Proposed additional references from the repository: + - {action-name}/action.yml — {why this is relevant} + - {action-name}/src/main.ts — {why this is relevant} + +Proceed with these references? +``` + +Only read repository actions after the user approves. Do not scan the repository speculatively. + +### Step 3: Implement Core Logic + +**For Composite actions** -- edit `{action-name}/action.yml`: +1. Replace placeholder steps with real implementation. +2. Use `env:` blocks to pass all inputs to shell scripts (never inline `${{ inputs.* }}` in `run:` -- this is a command injection vector). +3. Use `set -e` at the top of every bash `run:` block. +4. Write outputs using `echo "name=value" >> "$GITHUB_OUTPUT"`. +5. Use `::error::`, `::warning::`, `::notice::` annotations for user-facing messages. +6. Always quote shell variables: `"$VAR"` not `$VAR`. + +**For TypeScript actions** -- edit `{action-name}/src/main.ts`: +1. Import `@actions/core` and any packages needed per SPEC.md integrations. +2. Read inputs with `core.getInput('name', { required: true/false })`. +3. Mask sensitive values with `core.setSecret(value)` before any logging. +4. Set outputs with `core.setOutput('name', value)`. +5. Wrap the entire `run()` body in try/catch with `core.setFailed()` in the catch block. + +**For Docker/Python actions** -- edit `{action-name}/main.py`: +1. Read inputs from environment: `os.getenv("INPUT_NAME")` (GitHub uppercases and prefixes with `INPUT_`). +2. Write outputs by appending to the file at `os.getenv("GITHUB_OUTPUT")`. +3. Exit with non-zero status on failure: `sys.exit(1)`. +4. Use `subprocess.run()` with argument lists, never `shell=True` with untrusted input. + +### Step 4: Implement Input Validation + +At the entry point of every action, validate all inputs before any business logic runs: +- Check required inputs are non-empty. +- Validate format constraints (e.g., regex for version strings, allowed enum values). +- Validate file path inputs against directory traversal (`../`, absolute paths where relative expected). +- Provide clear error messages: what was wrong, what format is expected. + +**Composite pattern** (from `check-permission/action.yml`): +```bash +if [[ -z "$INPUT_NAME" ]]; then + echo "::error::Input 'input_name' is required but was empty." + exit 1 +fi +if [[ ! "$INPUT_VALUE" =~ ^(allowed|values)$ ]]; then + echo "::error::Input 'input_value' must be 'allowed' or 'values', got '$INPUT_VALUE'" + exit 1 +fi +``` + +**TypeScript pattern**: Validate after `core.getInput()`, throw descriptive errors. + +**Python pattern**: Validate after `os.getenv()`, call `sys.exit(1)` with a printed error. + +### Step 5: Implement Error Handling + +- Handle all expected failure modes described in SPEC.md. +- For API calls: handle network errors, auth failures, rate limits, and unexpected response shapes. +- For file operations: handle missing files, permission errors, and malformed content. +- Provide actionable error messages (not just "failed" -- explain what went wrong and suggest a fix). +- Never expose sensitive data in error messages (tokens, secrets, internal URLs). + +### Step 6: Populate README.md + +Read `{action-name}/README.md` (scaffolded with section headings and TODO placeholders), then edit it: + +1. **Inputs table**: Populate from the final `action.yml` inputs — name, description, required, default. Use the actual implemented values, not the spec. +2. **Outputs table**: Populate from the final `action.yml` outputs — name, description. +3. **Usage section**: Write a basic usage example showing the action invoked with all required inputs and realistic values. If the action has multiple modes or configurations (per SPEC.md), add an additional example for each. +4. **Features section** (if present): Write 2-4 bullet points describing the action's key capabilities. +5. **Prerequisites section** (if present): Document any required setup (Azure credentials, tool installation, etc.). +6. **Permissions section** (if present): Document the GitHub token permissions the action requires. +7. **Development section** (if present, TypeScript only): Document `npm install` and `npm run build` commands. +8. Remove all remaining TODO/placeholder comments from the README. + +Populate the sections as scaffolded — the structure was established by scaffold-action. Read the README specification at `.claude/skills/scaffold-action/references/readme-specification.md` for formatting rules (column order, backtick formatting, ordering, line limits). Read `check-permission/README.md` or `api-commit/README.md` as live examples for content style. + +### Step 7: Update Test Workflow + +Read `.github/workflows/test-{action-name}.yml`, then edit it: +1. Replace TODO comments with actual test scenarios from SPEC.md. +2. Provide realistic test inputs (not "test" or "foo"). +3. Add output verification steps that assert expected values using `env:` blocks. +4. Add multiple test jobs if the action has distinct modes or configurations. +5. Ensure each test job has a descriptive, capitalized name (workflow linter requirement). +6. Reference the multi-job pattern from `.github/workflows/test-check-permission.yml` if needed. + +### Step 8: Build (TypeScript Only) + +For TypeScript actions only: +1. Run `cd {action-name} && npm install` to install dependencies. +2. Run `cd {action-name} && npm run build` to compile. +3. Verify `dist/index.js` was created with `ls {action-name}/dist/index.js`. +4. The compiled `dist/` files must be committed alongside the source. + +Skip this step for Composite and Docker actions. + +### Step 9: Report Results + +List all files modified and summarize what was implemented: + +``` +Implementation complete for {action-name}: + +Modified files: + - {action-name}/action.yml -- core logic, input validation, output setting + - {action-name}/README.md -- inputs, outputs, usage examples, documentation + - {action-name}/src/main.ts -- (TypeScript only) full implementation + - {action-name}/main.py -- (Docker only) full implementation + - .github/workflows/test-{action-name}.yml -- test scenarios and assertions + +Remaining TODOs: {count, should be 0} + +Next step: Run evaluate-action to review completeness: + evaluate-action {action-name} +``` + +If any TODOs remain that could not be resolved (e.g., require external credentials or infrastructure not available locally), list them explicitly with the reason. + +## Important Rules + +- Write real, working code. No stubs, no TODOs, no placeholder comments in the final output. +- Do NOT add features beyond what SPEC.md describes. Implement exactly the specification. +- Do NOT add unnecessary dependencies. Use the standard library where possible. +- Follow the existing code style in the repository (Prettier will enforce formatting on commit). +- Pass inputs through `env:` blocks in composite actions -- never inline `${{ inputs.* }}` in `run:` commands. +- For bash: always quote variables, use `set -e`, prefer `[[ ]]` over `[ ]`. +- For TypeScript: use strict types, handle undefined/null explicitly. +- For Python: use type hints where helpful, handle exceptions explicitly. +- Never log, echo, or print sensitive values. Mask them before any output. +- Output names must use underscores, not hyphens (workflow linter requirement). + +## Related Skills + +- **define-action**: Produces the SPEC.md this skill consumes. Run it first if no SPEC.md exists. Example: `define-action {action-name}` +- **scaffold-action**: Generates the skeleton files this skill fills in. Run it after define-action. Example: `scaffold-action {action-name}` +- **evaluate-action**: Reviews completeness of the implementation. Run after this skill. Example: `evaluate-action {action-name}` +- **validate-action**: Checks formatting, structure, and linter compliance. Example: `validate-action {action-name}` diff --git a/.claude/skills/implement-action/references/composite-patterns.md b/.claude/skills/implement-action/references/composite-patterns.md new file mode 100644 index 000000000..e6fdaaea6 --- /dev/null +++ b/.claude/skills/implement-action/references/composite-patterns.md @@ -0,0 +1,287 @@ +# Composite Action — Implementation Patterns + +Distilled from: `check-permission`, `get-pull-request-threads`, `update-pr-comment`, `container-tag` + +--- + +## Step Structure + +Every composite step follows this shape: + +```yaml +- name: Descriptive step name + id: step-id + shell: bash + env: + INPUT_NAME: ${{ inputs.input_name }} + SENSITIVE_TOKEN: ${{ inputs.token }} + run: | + set -e + + # === Input Validation === + # ... validate all inputs before any logic ... + + # === Core Logic === + # ... business logic ... + + # === Set Outputs === + echo "output_name=value" >> "$GITHUB_OUTPUT" +``` + +Key rules: +- Every `run:` block starts with `set -e` +- Every input is passed through `env:` — never inline `${{ inputs.* }}` in `run:` +- Sections are separated with comment headers for readability +- Step `id` must match the output `value` references in the action.yml outputs block + +--- + +## Input Validation + +Validate all inputs at the top of the run block, before any business logic. + +### Required non-empty string + +```bash +if [[ -z "$INPUT_NAME" ]]; then + echo "::error::Input 'input_name' is required but was empty." + exit 1 +fi +``` + +### Enum validation + +```bash +if [[ ! "$FAILURE_MODE" =~ ^(fail|skip|continue)$ ]]; then + echo "::error::Invalid failure_mode: must be 'fail', 'skip', or 'continue', got '$FAILURE_MODE'" + exit 1 +fi +``` + +### Positive integer + +```bash +if [[ -z "$PR_NUMBER" ]] || [[ ! "$PR_NUMBER" =~ ^[0-9]+$ ]]; then + echo "::error::Invalid pr_number: must be a positive integer" + exit 1 +fi +``` + +### Repository format (owner/repo) + +```bash +if [[ -z "$REPOSITORY" ]] || [[ ! "$REPOSITORY" =~ ^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$ ]]; then + echo "::error::Invalid repository format: must be 'owner/repo'" + exit 1 +fi +``` + +### GitHub username format + +```bash +if [[ -z "$USERNAME" ]] || [[ ! "$USERNAME" =~ ^[A-Za-z0-9]([A-Za-z0-9-]{0,37}[A-Za-z0-9])?(\[bot\])?$ ]]; then + echo "::error::Invalid username format." + exit 1 +fi +``` + +### Mutually exclusive inputs + +```bash +if [[ -n "$BODY_INPUT" && -n "$BODY_FILE_INPUT" ]]; then + echo "::error::Cannot specify both 'body' and 'body_file' inputs" + exit 1 +fi +``` + +### File path existence + +```bash +if [[ ! -f "$BODY_FILE_INPUT" ]]; then + echo "::warning::body_file not found at ${BODY_FILE_INPUT}, skipping" + exit 0 +fi +``` + +--- + +## Output Setting + +Always write to `$GITHUB_OUTPUT`. Quote the variable. + +```bash +echo "output_name=$VALUE" >> "$GITHUB_OUTPUT" +``` + +For multiple outputs, set each on its own line: + +```bash +echo "has_permission=$HAS_PERM" >> "$GITHUB_OUTPUT" +echo "user_permission=$USER_PERMISSION" >> "$GITHUB_OUTPUT" +echo "should_proceed=$SHOULD_PROCEED" >> "$GITHUB_OUTPUT" +``` + +--- + +## Error Handling + +### Annotations + +Use GitHub Actions annotations for user-facing messages: + +```bash +echo "::error::Description of what went wrong" # Fails visibly in the UI +echo "::warning::Non-fatal issue" # Yellow warning +echo "::notice::Informational message" # Neutral info +``` + +### API call with error capture + +Capture stderr and check the return code: + +```bash +if ! RESPONSE=$(gh api "repos/$REPO/endpoint" --jq '.field' 2>&1); then + echo "::error::API call failed: $RESPONSE" + exit 1 +fi +``` + +### Graceful degradation for non-critical calls + +```bash +if ! COMMENTS=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json comments 2>&1); then + echo "::warning::Failed to retrieve comments: $COMMENTS" + COMMENTS='{"comments":[]}' +fi +``` + +--- + +## GitHub API Patterns + +### REST API via gh cli + +```bash +RESPONSE=$(gh api "repos/${REPOSITORY}/issues/${PR_NUMBER}/comments" \ + --method POST \ + --field body="$BODY") + +COMMENT_ID=$(echo "$RESPONSE" | jq -r '.id') +``` + +### GraphQL API via gh cli + +```bash +GRAPHQL_QUERY=' +query($owner: String!, $repo: String!, $pr: Int!) { + repository(owner: $owner, name: $repo) { + pullRequest(number: $pr) { + # ... fields ... + } + } +}' + +RESPONSE=$(gh api graphql \ + -f query="$GRAPHQL_QUERY" \ + -f owner="$OWNER" \ + -f repo="$REPO" \ + -F pr="$PR_NUMBER") +``` + +### Parsing owner/repo from combined input + +```bash +OWNER="${REPOSITORY%/*}" +REPO="${REPOSITORY#*/}" +``` + +--- + +## Conditional Logic + +### Mode/strategy switching with case statement + +```bash +case "$FAILURE_MODE" in + fail) + echo "::error::Permission denied." + echo "should_proceed=false" >> "$GITHUB_OUTPUT" + exit 1 + ;; + skip) + echo "::warning::Permission denied. Marking for skip." + echo "should_proceed=false" >> "$GITHUB_OUTPUT" + exit 0 + ;; + continue) + echo "::notice::Permission denied. Continuing." + echo "should_proceed=true" >> "$GITHUB_OUTPUT" + exit 0 + ;; +esac +``` + +### Create-or-update pattern + +```bash +if [[ -z "$EXISTING_ID" ]]; then + RESPONSE=$(gh api "repos/${REPO}/issues/${PR}/comments" \ + --method POST --field body="$BODY") + CREATED="true" +else + RESPONSE=$(gh api "repos/${REPO}/issues/comments/${EXISTING_ID}" \ + --method PATCH --field body="$BODY") + CREATED="false" +fi +``` + +--- + +## String Transformation + +### Sanitization pipeline + +```bash +# Lowercase, strip prefix, replace invalid chars, collapse dashes, trim, truncate +IMAGE_TAG=$(tr '[:upper:]' '[:lower:]' <<< "${INPUT}" \ + | sed -E 's/^v//; s/[^a-z0-9._-]+/-/g; s/-+/-/g; s/^[.-]+|[.-]+$//g' \ + | cut -c1-128 \ + | sed -E 's/[.-]$//') +``` + +### Ref stripping (branches and tags) + +```bash +if [[ "${REF_INPUT}" == refs/* ]]; then + BRANCH_NAME=$(sed 's|^refs/heads/||; s|^refs/tags/||' <<< "${REF_INPUT}") +else + BRANCH_NAME="${REF_INPUT}" +fi +``` + +--- + +## JSON Processing with jq + +### Build structured output + +```bash +OUTPUT_JSON=$(jq -n \ + --argjson data "$API_RESPONSE" \ + --arg pr_number "$PR_NUMBER" \ + --arg timestamp "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \ + '{ + pr_number: ($pr_number | tonumber), + timestamp: $timestamp, + items: ($data.nodes | map({ id: .id, value: .value })) + }') + +echo "$OUTPUT_JSON" > "$OUTPUT_PATH" +``` + +### Extract values from JSON response + +```bash +TOTAL=$(echo "$OUTPUT_JSON" | jq -r '.total') +STATUS=$(echo "$OUTPUT_JSON" | jq -r '.status') +``` diff --git a/.claude/skills/implement-action/references/docker-patterns.md b/.claude/skills/implement-action/references/docker-patterns.md new file mode 100644 index 000000000..73d562d06 --- /dev/null +++ b/.claude/skills/implement-action/references/docker-patterns.md @@ -0,0 +1,259 @@ +# Docker/Python Action — Implementation Patterns + +Distilled from: `version-bump` + +--- + +## Entry Point Structure + +Every Python action follows this shape in `main.py`: + +```python +import os +import sys + + +def main(): + # === Read Inputs === + input_name = os.getenv("INPUT_INPUT_NAME", "") + file_path = os.getenv("INPUT_FILE_PATH", "") + + # === Validate Inputs === + if not input_name: + print("::error::Input 'input_name' is required but was empty.") + sys.exit(1) + + # === Core Logic === + # ... business logic ... + + # === Set Outputs === + if "GITHUB_OUTPUT" in os.environ: + with open(os.environ["GITHUB_OUTPUT"], "a") as f: + print(f"output_name={value}", file=f) + + +if __name__ == "__main__": + main() +``` + +Key rules: +- Single `main()` function as entry point +- Guard with `if __name__ == "__main__"` +- Read inputs from `INPUT_`-prefixed environment variables (GitHub uppercases input names) +- Write outputs to the `GITHUB_OUTPUT` file +- Exit with `sys.exit(1)` on failure + +--- + +## Input Reading + +GitHub Actions maps inputs to environment variables with the `INPUT_` prefix and uppercased names. + +```python +# Input "version" becomes INPUT_VERSION +version = os.getenv("INPUT_VERSION", "") + +# Input "file_path" becomes INPUT_FILE_PATH +file_path = os.getenv("INPUT_FILE_PATH", "") +``` + +### Multi-word input names + +Underscores in input names are preserved. Hyphens are converted to underscores. + +```python +# Input "max_threads" becomes INPUT_MAX_THREADS +max_threads = os.getenv("INPUT_MAX_THREADS", "100") +``` + +--- + +## Input Validation + +Validate immediately after reading, before any business logic. + +### Required non-empty + +```python +if not version: + print("::error::Input 'version' is required but was empty.") + sys.exit(1) +``` + +### File existence + +```python +if not os.path.isfile(file_path): + print(f"::error::File not found: {file_path}") + sys.exit(1) +``` + +### Format validation + +```python +import re + +if not re.match(r'^\d+\.\d+\.\d+$', version): + print(f"::error::Invalid version format: expected 'X.Y.Z', got '{version}'") + sys.exit(1) +``` + +### Enum validation + +```python +VALID_TYPES = {".xml", ".json", ".plist", ".toml"} +file_type = os.path.splitext(file_path)[1] +if file_type not in VALID_TYPES: + print(f"::error::Unsupported file type: {file_type}") + sys.exit(1) +``` + +--- + +## Output Setting + +Write outputs by appending to the `GITHUB_OUTPUT` file: + +```python +if "GITHUB_OUTPUT" in os.environ: + with open(os.environ["GITHUB_OUTPUT"], "a") as f: + print(f"status=Updated {file_path}", file=f) +``` + +Always check that `GITHUB_OUTPUT` exists — it won't be set during local testing. + +### Multiple outputs + +```python +if "GITHUB_OUTPUT" in os.environ: + with open(os.environ["GITHUB_OUTPUT"], "a") as f: + print(f"status={status}", file=f) + print(f"file_count={count}", file=f) +``` + +--- + +## Error Handling + +### GitHub Actions annotations + +```python +print("::error::Description of what went wrong") +print("::warning::Non-fatal issue") +print("::notice::Informational message") +``` + +### File operation errors + +```python +try: + with open(file_path, "r") as f: + data = json.load(f) +except FileNotFoundError: + print(f"::error::File not found: {file_path}") + sys.exit(1) +except json.JSONDecodeError as e: + print(f"::error::Invalid JSON in {file_path}: {e}") + sys.exit(1) +``` + +### Top-level exception guard + +```python +if __name__ == "__main__": + try: + main() + except Exception as e: + print(f"::error::Unexpected error: {e}") + sys.exit(1) +``` + +--- + +## File Format Dispatch + +When an action handles multiple file types, dispatch based on extension: + +```python +def get_file_type(file_path): + return os.path.splitext(file_path)[1] + +file_type = get_file_type(file_path) + +if file_type in {".xml", ".props", ".csproj"}: + update_xml(version, file_path) +elif file_type == ".json": + update_json(version, file_path) +elif file_type == ".plist": + update_plist(version, file_path) +else: + print(f"::error::Unsupported file type: {file_type}") + sys.exit(1) +``` + +--- + +## Subprocess Usage + +When shell commands are needed, always use argument lists — never `shell=True` with untrusted input: + +```python +import subprocess + +# GOOD — safe argument list +result = subprocess.run( + ["git", "tag", "-l", version], + capture_output=True, + text=True, + check=True, +) + +# BAD — shell injection risk +# subprocess.run(f"git tag -l {version}", shell=True) +``` + +### Handling subprocess errors + +```python +try: + result = subprocess.run( + ["command", "arg1", "arg2"], + capture_output=True, + text=True, + check=True, + ) +except subprocess.CalledProcessError as e: + print(f"::error::Command failed (exit {e.returncode}): {e.stderr}") + sys.exit(1) +``` + +--- + +## Dockerfile Structure + +Use multi-stage builds with a distroless final image: + +```dockerfile +FROM python:3-slim AS builder + +WORKDIR /app + +# Install dependencies to the app directory +RUN pip3 install --no-cache-dir package-name --target=. + +ADD ./main.py . + +FROM gcr.io/distroless/python3-debian12 + +WORKDIR /app +COPY --from=builder /app /app +ENV PYTHONPATH=/app + +ENTRYPOINT ["/usr/bin/python3", "-u", "/app/main.py"] +``` + +Key rules: +- Builder stage installs dependencies with `--target=.` so they're co-located +- Final stage uses `gcr.io/distroless/python3-debian12` for minimal attack surface +- `-u` flag on python ensures unbuffered output (logs appear in real time) +- Only add `RUN pip3 install` if the action needs external packages diff --git a/.claude/skills/implement-action/references/typescript-patterns.md b/.claude/skills/implement-action/references/typescript-patterns.md new file mode 100644 index 000000000..5fd7edc76 --- /dev/null +++ b/.claude/skills/implement-action/references/typescript-patterns.md @@ -0,0 +1,225 @@ +# TypeScript Action — Implementation Patterns + +Distilled from: `get-keyvault-secrets` + +--- + +## Entry Point Structure + +Every TypeScript action follows this shape in `src/main.ts`: + +```typescript +import * as core from '@actions/core'; + +async function run(): Promise { + try { + // === Read Inputs === + const requiredInput = core.getInput('input_name', { required: true }); + const optionalInput = core.getInput('optional_input', { required: false }) || 'default'; + + // === Validate Inputs === + // ... validation logic ... + + // === Mask Sensitive Values === + core.setSecret(sensitiveValue); + + // === Core Logic === + // ... business logic ... + + // === Set Outputs === + core.setOutput('output_name', value); + } catch (error) { + core.setFailed(error instanceof Error ? error.message : String(error)); + } +} + +run(); +``` + +Key rules: +- Single `run()` async function as the entry point +- Entire body wrapped in try/catch +- `core.setFailed()` in the catch block — never throw unhandled +- Read all inputs at the top, validate immediately after + +--- + +## Input Reading + +### Required input + +```typescript +const keyvault = core.getInput('keyvault', { required: true }); +``` + +GitHub Actions will fail the step if a required input is missing, but always validate the value too. + +### Optional input with default + +```typescript +const format = core.getInput('format', { required: false }) || 'json'; +``` + +### Comma-separated list input + +```typescript +const secretNames = core.getInput('secrets', { required: true }) + .split(',') + .map((s) => s.trim()) + .filter(Boolean); +``` + +### Boolean input + +```typescript +const verbose = core.getInput('verbose', { required: false }).toLowerCase() === 'true'; +``` + +--- + +## Input Validation + +Validate after reading, before any business logic: + +```typescript +if (!inputValue.match(/^[a-z0-9-]+$/)) { + throw new Error(`Invalid input_name: must be lowercase alphanumeric with hyphens, got '${inputValue}'`); +} + +if (secretNames.length === 0) { + throw new Error('Input "secrets" must contain at least one secret name'); +} +``` + +The throw will be caught by the outer try/catch and passed to `core.setFailed()`. + +--- + +## Secret Masking + +Mask sensitive values **before any logging or output setting**: + +```typescript +core.setSecret(secret.value); +core.setOutput(secretName, secret.value); +``` + +Order matters — if you set the output before masking, the value may appear in logs. + +--- + +## Output Setting + +```typescript +core.setOutput('result', value); +core.setOutput('count', items.length.toString()); +``` + +Outputs are always strings. Convert numbers and booleans explicitly. + +--- + +## Error Handling + +### Top-level catch + +Every action uses this pattern: + +```typescript +catch (error) { + core.setFailed(error instanceof Error ? error.message : String(error)); +} +``` + +### Typed error handling for SDK calls + +```typescript +try { + const secret = await client.getSecret(secretName); + if (secret.value === undefined) { + throw new Error(`Secret "${secretName}" has no value`); + } +} catch (error) { + if (error instanceof RestError && error.statusCode === 404) { + throw new Error(`Secret "${secretName}" not found in vault "${keyvault}"`); + } + throw error; // Re-throw unexpected errors +} +``` + +### Iterating with error accumulation + +```typescript +const errors: string[] = []; +for (const name of items) { + try { + // ... process item ... + } catch (error) { + errors.push(`${name}: ${error instanceof Error ? error.message : String(error)}`); + } +} +if (errors.length > 0) { + throw new Error(`Failed to process ${errors.length} item(s):\n${errors.join('\n')}`); +} +``` + +--- + +## Azure SDK Integration + +### Credential and client setup + +```typescript +import { AzureCliCredential } from '@azure/identity'; +import { SecretClient } from '@azure/keyvault-secrets'; + +const credential = new AzureCliCredential(); +const client = new SecretClient( + `https://${keyvault}.vault.azure.net`, + credential, +); +``` + +### Retrieving secrets with masking + +```typescript +for (const secretName of secretNames) { + const secret = await client.getSecret(secretName); + if (secret.value === undefined) { + throw new Error(`Secret "${secretName}" has no value`); + } + core.setSecret(secret.value); + core.setOutput(secretName, secret.value); +} +``` + +--- + +## Package Dependencies + +### Minimum required + +```json +{ + "dependencies": { + "@actions/core": "^1.11.1" + }, + "devDependencies": { + "@vercel/ncc": "^0.38.4", + "typescript": "^5.9.3" + } +} +``` + +### Common additions by integration + +| Integration | Package | +|---|---| +| GitHub API | `@actions/github` | +| Shell execution | `@actions/exec` | +| File globbing | `@actions/glob` | +| Azure Key Vault | `@azure/identity`, `@azure/keyvault-secrets` | +| Azure Storage | `@azure/identity`, `@azure/storage-blob` | +| HTTP requests | `@actions/http-client` | + +Only add dependencies required by the SPEC.md integrations. diff --git a/.claude/skills/scaffold-action/SKILL.md b/.claude/skills/scaffold-action/SKILL.md new file mode 100644 index 000000000..2ad6dace6 --- /dev/null +++ b/.claude/skills/scaffold-action/SKILL.md @@ -0,0 +1,299 @@ +--- +name: scaffold-action +description: "Generate boilerplate directory structure, action.yml, README.md, test workflow, and type-specific files for a new GitHub Action from its SPEC.md." +argument-hint: "" +allowed-tools: + - Read + - Write + - Glob + - Bash(ls:./*) + - Bash(mkdir:./*) +--- + +# Scaffold Action - Boilerplate Generation + +Generate the complete file structure for a new GitHub Action in the Bitwarden `gh-actions` repository. Read the SPEC.md produced by the define-action skill and create all skeleton files with TODO placeholders -- no implementation logic. + +## Input + +This skill accepts a single required argument: the action directory name (e.g., `my-new-action`). + +The action directory must already contain a `SPEC.md` file produced by the `define-action` skill. + +**Examples:** +- `scaffold-action my-new-action` +- `scaffold-action report-deployment-status-to-slack` + +## Procedure + +### Step 1: Validate Prerequisites + +1. Confirm `{action-name}/SPEC.md` exists using `ls`. +2. If SPEC.md does not exist, stop and report: "No SPEC.md found in {action-name}/. Run the define-action skill first to generate a specification." +3. Read `{action-name}/SPEC.md` to understand the action's type, inputs, outputs, and integrations. + +### Step 2: Read Structural Templates + +Read the structural templates from `references/` (co-located with this skill). These are the primary baseline for file structure, field ordering, and skeleton content. + +**For ALL action types, read:** +- `references/test-workflow-structure.md` — test workflow triggers, permissions, pinning, job structure +- `references/readme-specification.md` — README section definitions and formatting rules + +**Per action type, also read:** +- **Composite**: `references/composite-structure.md` +- **TypeScript**: `references/typescript-structure.md` +- **Docker/Python**: `references/docker-structure.md` + +Use these templates as the authoritative source for generating skeleton files. The inline examples in Steps 4-7 below are summaries — the templates have the full detail. + +**Discovery fallback**: If the SPEC.md describes integrations or structural needs not covered by the templates (e.g., wrapping an unfamiliar external action, unusual multi-file layout), propose specific actions from the repository to use as additional references. Present them to the user before reading: + +``` +The structural templates do not cover {specific need}. +Proposed additional references from the repository: + - {action-name}/{file} — {why this is relevant} + +Proceed with these references? +``` + +Only read repository files after the user approves. Do not scan the repository speculatively. + +### Step 3: Create Directory + +Run `mkdir -p {action-name}` to ensure the action directory exists (it should already exist from define-action, but ensure it). + +### Step 4: Generate action.yml + +Create `{action-name}/action.yml` with: + +```yaml +name: "{Action Name}" +description: "{description from SPEC.md}" +author: "Bitwarden" + +inputs: + # From SPEC.md - each input with description, required, and default + +outputs: + # From SPEC.md - each output with description and value reference + +runs: + # Type-specific runs configuration +``` + +**Composite**: `using: "composite"` with placeholder steps including `shell: bash` and `env:` blocks for inputs (never inline `${{ inputs.* }}` in `run:` commands). + +**TypeScript**: `using: "node24"`, `main: "dist/index.js"` + +**Docker**: `using: "docker"`, `image: "Dockerfile"` + +### Step 5: Generate Type-Specific Files + +**For TypeScript actions:** + +Create `{action-name}/package.json`: +```json +{ + "name": "@bitwarden/{action-name}", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "ncc build src/main.ts -o dist --license licenses.txt", + "postbuild": "node -e \"const fs=require('fs'); const f='dist/index.js'; fs.writeFileSync(f, fs.readFileSync(f,'utf8').replace(/\\r\\n/g,'\\n'))\"" + }, + "dependencies": { + "@actions/core": "^1.11.1" + }, + "devDependencies": { + "@vercel/ncc": "^0.38.4", + "typescript": "^5.9.3" + } +} +``` + +Add any additional dependencies based on SPEC.md integrations (e.g., `@azure/identity`, `@azure/keyvault-secrets`, `@actions/github`). + +Create `{action-name}/tsconfig.json`: +```json +{ + "compilerOptions": { + "target": "es2022", + "module": "nodenext", + "moduleResolution": "nodenext", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "rootDir": "./src", + "outDir": "./dist" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} +``` + +Create `{action-name}/src/main.ts` skeleton: +```typescript +import * as core from '@actions/core'; + +async function run(): Promise { + try { + // TODO: Read inputs + // const inputName = core.getInput('input_name', { required: true }); + + // TODO: Implementation + + // TODO: Set outputs + // core.setOutput('output_name', value); + } catch (error) { + core.setFailed(error instanceof Error ? error.message : String(error)); + } +} + +run(); +``` + +Create `{action-name}/.gitignore`: +``` +node_modules/ +``` + +**For Docker actions:** + +Create `{action-name}/Dockerfile`: +```dockerfile +FROM python:3-slim AS builder + +WORKDIR /app + +# Install dependencies if needed +# RUN pip3 install --no-cache-dir package-name --target=. + +ADD ./main.py . + +FROM gcr.io/distroless/python3-debian12 + +WORKDIR /app +COPY --from=builder /app /app +ENV PYTHONPATH=/app + +ENTRYPOINT ["/usr/bin/python3", "-u", "/app/main.py"] +``` + +Create `{action-name}/main.py` skeleton: +```python +import os +import sys + + +def main(): + # TODO: Read inputs from environment variables + # input_name = os.getenv("INPUT_INPUT_NAME", "") + + # TODO: Implementation + + # TODO: Set outputs + # with open(os.getenv("GITHUB_OUTPUT", ""), "a") as f: + # f.write(f"output_name={value}\n") + + +if __name__ == "__main__": + main() +``` + +### Step 6: Generate Test Workflow + +Create `.github/workflows/test-{action-name}.yml` following the exact pattern from the reference: + +```yaml +name: Test {Action Name} + +on: + pull_request: + paths: + - "{action-name}/**" + - ".github/workflows/test-{action-name}.yml" + workflow_dispatch: + +permissions: + contents: read + +jobs: + test: + name: Test {Action Name} + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Run {action-name} + id: test + uses: ./{action-name} + with: + # TODO: Provide test inputs + + - name: Verify outputs + env: + # TODO: Map outputs to env vars for safe use + run: | + echo "TODO: Verify action outputs" +``` + +**Important**: Use the exact checkout action SHA from `references/test-workflow-structure.md`. That template is kept current — do not scan the repository for a different SHA. + +### Step 7: Generate README.md + +Create `{action-name}/README.md` with section headings and TODO placeholders. The scaffold defines the structure; `implement-action` populates it later. + +Read `references/readme-specification.md` (already loaded in Step 2) for the canonical section definitions, table formats, ordering rules, and formatting constraints. + +**Scaffolding rules:** +- Include all sections from the specification that apply based on SPEC.md (Title, Description, Features, Inputs, Outputs, Usage, and any relevant supplementary sections). +- Use `` comments as placeholders for content that implement-action will fill in. +- Only include conditional/supplementary sections whose TODOs will be fulfillable based on SPEC.md. Do not include empty sections. +- Pre-fill table headers and column structure exactly as the specification defines (column order, backtick formatting). +- Pre-fill the title and description from SPEC.md — these are known at scaffold time. + +### Step 8: Report Results + +List all files created with their paths. Example: + +``` +Scaffolded files for {action-name}: + - {action-name}/action.yml + - {action-name}/src/main.ts + - {action-name}/package.json + - {action-name}/tsconfig.json + - {action-name}/.gitignore + - {action-name}/README.md + - .github/workflows/test-{action-name}.yml + +Next step: Run the implement-action skill to replace TODO placeholders with working code: + implement-action {action-name} +``` + +## Important Rules + +- Do NOT implement any logic. Only generate skeletons with TODO comments. +- Always read the structural templates in `references/` first. They are the primary baseline. +- For the test workflow, use the exact pinned SHA for `actions/checkout` from `references/test-workflow-structure.md`. +- Do not scan the repository for patterns unless the templates are insufficient for the SPEC.md requirements. If discovery is needed, propose references to the user first. +- All input references in composite `run:` blocks MUST go through `env:` -- never use `${{ inputs.* }}` directly in shell commands. +- Output names must use underscores, not hyphens. +- Runner must be pinned to `ubuntu-24.04` (not `ubuntu-latest`). +- This skill only creates files. It never runs `npm install`, `npm run build`, or any build commands. + +## Related Skills + +- **define-action**: Run first to produce the SPEC.md this skill consumes. Example: `define-action {action-name}` +- **implement-action**: Run after scaffolding to replace TODO placeholders with working code. Example: `implement-action {action-name}` +- **evaluate-action**: Reviews implementation completeness against SPEC.md. Example: `evaluate-action {action-name}` +- **validate-action**: Run after implementation to check formatting, structure, and linter compliance. Example: `validate-action {action-name}` diff --git a/.claude/skills/scaffold-action/references/composite-structure.md b/.claude/skills/scaffold-action/references/composite-structure.md new file mode 100644 index 000000000..0a3f8e061 --- /dev/null +++ b/.claude/skills/scaffold-action/references/composite-structure.md @@ -0,0 +1,123 @@ +# Composite Action — Structural Template + +Distilled from: `check-permission`, `get-pull-request-threads`, `update-pr-comment`, `container-tag` + +--- + +## Files + +A composite action directory contains: + +``` +{action-name}/ +├── action.yml +├── README.md +└── SPEC.md (internal, removed automatically by the agent at pipeline completion) +``` + +No additional files are needed — all logic lives in `action.yml`. + +--- + +## action.yml Structure + +```yaml +name: "{Action Name}" +description: "{One-line description}" +author: "Bitwarden" + +inputs: + required_input: + description: "What this input controls" + required: true + optional_input: + description: "What this optional input controls" + required: false + default: "default_value" + +outputs: + output_name: + description: "What this output contains" + value: ${{ steps.step-id.outputs.output_name }} + +runs: + using: "composite" + steps: + - name: Descriptive step name + id: step-id + shell: bash + env: + REQUIRED_INPUT: ${{ inputs.required_input }} + OPTIONAL_INPUT: ${{ inputs.optional_input }} + run: | + set -e + # TODO: Implementation +``` + +### Field ordering + +1. `name` +2. `description` +3. `author` +4. `inputs` (required first, then optional) +5. `outputs` (each with `description` and `value`) +6. `runs` + +### Output value references + +Every output `value` must reference a step by its `id`: + +```yaml +outputs: + result: + description: "The result" + value: ${{ steps.my-step.outputs.result }} +``` + +The step `id` and output name must match exactly. + +### Step conventions + +- Every step has `shell: bash` +- Every step has an `env:` block mapping all inputs it uses +- Never use `${{ inputs.* }}` directly in `run:` blocks +- Steps that set outputs must have an `id:` + +### Multi-step actions + +For actions with multiple logical phases (e.g., authenticate → fetch → process), use separate steps: + +```yaml +steps: + - name: Authenticate + id: auth + shell: bash + env: + CLIENT_ID: ${{ inputs.client_id }} + run: | + set -e + # TODO: Authentication logic + + - name: Process data + id: process + shell: bash + env: + AUTH_TOKEN: ${{ steps.auth.outputs.token }} + run: | + set -e + # TODO: Processing logic +``` + +### Calling other actions from composite steps + +When wrapping Bitwarden actions: + +```yaml +steps: + - name: Azure login + uses: bitwarden/gh-actions/azure-login@main + with: + tenant_id: ${{ inputs.azure_tenant_id }} + client_id: ${{ inputs.azure_client_id }} + subscription_id: ${{ inputs.azure_subscription_id }} +``` diff --git a/.claude/skills/scaffold-action/references/docker-structure.md b/.claude/skills/scaffold-action/references/docker-structure.md new file mode 100644 index 000000000..00c0547ea --- /dev/null +++ b/.claude/skills/scaffold-action/references/docker-structure.md @@ -0,0 +1,121 @@ +# Docker/Python Action — Structural Template + +Distilled from: `version-bump` + +--- + +## Files + +A Docker action directory contains: + +``` +{action-name}/ +├── action.yml +├── Dockerfile +├── main.py +├── README.md +└── SPEC.md (internal, removed automatically by the agent at pipeline completion) +``` + +--- + +## action.yml Structure + +```yaml +name: "{Action Name}" +description: "{One-line description}" +author: "Bitwarden" + +inputs: + required_input: + description: "What this input controls" + required: true + optional_input: + description: "What this optional input controls" + required: false + default: "default_value" + +outputs: + output_name: + description: "What this output contains" + +runs: + using: "docker" + image: "Dockerfile" +``` + +### Key differences from composite + +- `runs.using` is `"docker"` (not `"composite"`) +- `runs.image` points to `"Dockerfile"` (relative to action directory) +- Outputs do not have a `value` field — they are set by writing to `GITHUB_OUTPUT` in Python +- Inputs are passed as environment variables with `INPUT_` prefix (handled by GitHub Actions runtime) + +--- + +## Dockerfile Structure + +```dockerfile +FROM python:3-slim AS builder + +WORKDIR /app + +# Install dependencies (only if needed) +# RUN pip3 install --no-cache-dir package-name --target=. + +ADD ./main.py . + +FROM gcr.io/distroless/python3-debian12 + +WORKDIR /app +COPY --from=builder /app /app +ENV PYTHONPATH=/app + +ENTRYPOINT ["/usr/bin/python3", "-u", "/app/main.py"] +``` + +### Multi-stage build pattern + +- **Builder stage** (`python:3-slim`): Install pip packages with `--target=.` so they land in `/app` +- **Final stage** (`gcr.io/distroless/python3-debian12`): Minimal image, no shell, no package manager +- `ENV PYTHONPATH=/app` ensures pip-installed packages are importable +- `-u` flag ensures unbuffered Python output for real-time log streaming + +### When dependencies are needed + +Add the `RUN pip3 install` line for any packages beyond the Python standard library: + +```dockerfile +RUN pip3 install --no-cache-dir pyyaml tomlkit --target=. +``` + +### When no dependencies are needed + +Remove the `RUN pip3 install` line entirely. The builder stage still handles `ADD ./main.py`. + +--- + +## main.py Skeleton + +```python +import os +import sys + + +def main(): + # TODO: Read inputs from environment variables + # input_name = os.getenv("INPUT_INPUT_NAME", "") + + # TODO: Validate inputs + + # TODO: Core logic + + # TODO: Set outputs + # if "GITHUB_OUTPUT" in os.environ: + # with open(os.environ["GITHUB_OUTPUT"], "a") as f: + # print(f"output_name={value}", file=f) + + +if __name__ == "__main__": + main() +``` diff --git a/.claude/skills/scaffold-action/references/readme-specification.md b/.claude/skills/scaffold-action/references/readme-specification.md new file mode 100644 index 000000000..748a19576 --- /dev/null +++ b/.claude/skills/scaffold-action/references/readme-specification.md @@ -0,0 +1,211 @@ +# README Specification + +Reference structure for GitHub Action README files generated by the implement-action skill. Each section below describes what to include and how to format it. + +--- + +## 1. Title and Description + +A level-1 heading with the action name, followed by a one-paragraph summary. + +```markdown +# Action Name + +A brief description of what the action does, who it is for, and the problem it solves. +``` + +**Rules:** +- Title should match the directory name in human-readable form (e.g., `check-permission/` -> `Check Permission Action`). +- Description should be 1-3 sentences. Lead with what the action does, not how. + +--- + +## 2. Features + +A bulleted list of 3-6 key capabilities. Keep each bullet to one line. + +```markdown +## Features + +- **Capability one**: Brief explanation +- **Capability two**: Brief explanation +- **Capability three**: Brief explanation +``` + +**Rules:** +- Bold the capability name, follow with a colon and explanation. +- Only list behaviors the action actually implements — no aspirational features. +- Omit this section if the action does exactly one thing with no notable sub-features. + +--- + +## 3. Inputs + +A markdown table listing every input defined in `action.yml`. + +```markdown +## Inputs + +| Input | Description | Required | Default | +|-------|-------------|----------|---------| +| `input_name` | What this input controls | Yes | - | +| `optional_input` | What this optional input controls | No | `default_value` | +``` + +**Rules:** +- Column order: Input, Description, Required, Default. +- Wrap input names in backticks. +- Use `-` for inputs with no default. +- Wrap default values in backticks. +- List required inputs first, then optional inputs. +- If an input accepts a fixed set of values (enum), list the allowed values in the description (e.g., "Permission level: `admin`, `write`, `read`, `none`"). +- If a group of inputs warrants explanation beyond the table (e.g., failure modes, mutually exclusive options), add a subsection after the table. + +--- + +## 4. Outputs + +A markdown table listing every output defined in `action.yml`. + +```markdown +## Outputs + +| Output | Description | +|--------|-------------| +| `output_name` | What this output contains | +``` + +**Rules:** +- Column order: Output, Description. +- Wrap output names in backticks. +- If outputs carry structured data (JSON), add an "Output JSON Schema" subsection with a fenced code block showing the shape. +- Omit this section entirely if the action has no outputs. + +--- + +## 5. Usage Examples + +One or more fenced YAML code blocks showing how to call the action in a workflow step. + +```markdown +## Usage + +### Basic Usage + +\`\`\`yaml +- name: Step description + id: step-id + uses: bitwarden/gh-actions/{action-name}@main + with: + required_input: ${{ realistic.value }} +\`\`\` +``` + +**Rules:** +- Always include a "Basic Usage" example showing all required inputs with realistic values. +- Add additional named examples for each distinct mode, configuration, or integration pattern the action supports. +- Use realistic GitHub Actions context expressions (`${{ github.* }}`, `${{ secrets.* }}`, `${{ steps.*.outputs.* }}`), not placeholder strings like `"test"` or `"foo"`. +- If the action produces outputs consumed by later steps, show the downstream usage in the same example. +- For actions with multiple event types or modes, show at least one example per mode. +- Order examples from simplest to most complex. + +--- + +## 6. Supplementary Sections + +Include any of the following sections when relevant to the action. Only include sections that add information not already covered by Inputs/Outputs/Usage. + +### Permissions + +Document the GitHub token permissions the action requires. + +```markdown +## Permissions + +Requires `pull-requests: read` permission. The default `GITHUB_TOKEN` works. + +\`\`\`yaml +permissions: + pull-requests: read +\`\`\` +``` + +### Requirements / Prerequisites + +Document external setup needed before using the action (credentials, tool installation, service configuration). + +```markdown +## Requirements + +### Azure Service Principal + +The Azure service principal must have: +- Read access to the `bitwarden-ci` Key Vault +- Permission to retrieve the `slack-bot-token` secret +``` + +### Dependencies + +List other actions or external services the action depends on. + +```markdown +## Dependencies + +This action uses the following Bitwarden actions: +- `bitwarden/gh-actions/azure-login@main` +- `bitwarden/gh-actions/get-keyvault-secrets@main` + +External dependency: +- `slackapi/slack-github-action@v2.1.0` +``` + +### Behavioral Details + +Sections explaining specific behavior that users need to understand (transformation rules, special cases, event types). Name these sections after the behavior, not generically. + +```markdown +## Tag Generation Rules + +1. **Pull Requests**: Uses the head branch name +2. **Branch Pushes**: Uses the branch name +3. **Sanitization**: + - Converts to lowercase + - Strips leading `v` prefix + +## Examples + +| Input | Output | +|-------|--------| +| `main` | `dev` | +| `Feature/Add-Login` | `feature-add-login` | +``` + +### Development (TypeScript actions only) + +```markdown +## Development + +\`\`\`bash +npm install +npm run build +\`\`\` +``` + +**Rules for supplementary sections:** +- Only include sections that apply to this specific action. +- Order: Permissions, Requirements, Dependencies, behavioral detail sections, Development. +- Do not add a "Troubleshooting" section unless the SPEC.md identifies known failure modes that users will encounter. + +--- + +## Formatting Rules + +These rules apply across the entire README: + +- Use GitHub-flavored markdown. +- Use `##` for top-level sections, `###` for subsections. +- Tables must have aligned columns and use backticks for code values. +- Fenced code blocks must specify a language (`yaml`, `json`, `bash`). +- No trailing whitespace, no multiple consecutive blank lines. +- No emojis in section headings or table cells (emojis are acceptable in body text only when describing UI elements like Slack messages). +- Keep the total README under 200 lines for simple actions, under 300 for complex ones. Brevity over completeness — link to SPEC.md or external docs for deep detail. diff --git a/.claude/skills/scaffold-action/references/test-workflow-structure.md b/.claude/skills/scaffold-action/references/test-workflow-structure.md new file mode 100644 index 000000000..b5e559bdc --- /dev/null +++ b/.claude/skills/scaffold-action/references/test-workflow-structure.md @@ -0,0 +1,149 @@ +# Test Workflow — Structural Template + +Distilled from: `test-check-permission.yml` + +--- + +## Single-Job Test Workflow + +```yaml +name: Test {Action Name} + +on: + pull_request: + paths: + - "{action-name}/**" + - ".github/workflows/test-{action-name}.yml" + workflow_dispatch: + +permissions: + contents: read + +jobs: + test: + name: Test {Action Name} + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Run {action-name} + id: test + uses: ./{action-name} + with: + # TODO: Provide test inputs + + - name: Verify outputs + env: + # TODO: Map outputs to env vars + OUTPUT_NAME: ${{ steps.test.outputs.output_name }} + run: | + echo "Output: $OUTPUT_NAME" + # TODO: Add assertions +``` + +--- + +## Multi-Job Test Workflow + +For actions with distinct modes or configurations, use separate jobs: + +```yaml +name: Test {Action Name} + +on: + pull_request: + paths: + - "{action-name}/**" + - ".github/workflows/test-{action-name}.yml" + workflow_dispatch: + +permissions: + contents: read + +jobs: + test-default-mode: + name: Test default mode + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Run with default configuration + id: test-default + uses: ./{action-name} + with: + # TODO: Provide default mode inputs + + - name: Verify outputs + env: + OUTPUT: ${{ steps.test-default.outputs.output_name }} + run: | + echo "Output: $OUTPUT" + + test-alternate-mode: + name: Test alternate mode + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Run with alternate configuration + id: test-alt + uses: ./{action-name} + with: + # TODO: Provide alternate mode inputs + + - name: Verify outputs + env: + OUTPUT: ${{ steps.test-alt.outputs.output_name }} + run: | + echo "Output: $OUTPUT" +``` + +--- + +## Structural Rules + +### Triggers + +- Always include `pull_request` with `paths` scoped to the action directory and the test workflow itself +- Always include `workflow_dispatch` for manual testing + +### Permissions + +- Set `permissions` at the workflow level (not per-job) unless jobs need different scopes +- Use `contents: read` as the baseline — only escalate if the action requires more + +### Runners + +- Always pin to `ubuntu-24.04` — never use `ubuntu-latest` + +### External Actions + +- Pin to commit SHA with version comment: `uses: actions/checkout@{sha} # v{version}` +- Always include `persist-credentials: false` on checkout + +### Output Verification + +- Map step outputs to `env:` variables — never inline `${{ steps.*.outputs.* }}` in `run:` blocks +- Use descriptive env var names that match the output names + +### Job Naming + +- Workflow `name` starts with a capital letter +- Job `name` starts with a capital letter +- Step `name` starts with a capital letter + +### When to Use Multiple Jobs + +- The action has distinct modes (e.g., fail/skip/continue) +- The action has mutually exclusive input combinations +- Different test scenarios need different permissions +- A failure in one scenario should not block testing other scenarios diff --git a/.claude/skills/scaffold-action/references/typescript-structure.md b/.claude/skills/scaffold-action/references/typescript-structure.md new file mode 100644 index 000000000..d1d202ad2 --- /dev/null +++ b/.claude/skills/scaffold-action/references/typescript-structure.md @@ -0,0 +1,144 @@ +# TypeScript Action — Structural Template + +Distilled from: `get-keyvault-secrets` + +--- + +## Files + +A TypeScript action directory contains: + +``` +{action-name}/ +├── action.yml +├── package.json +├── tsconfig.json +├── .gitignore +├── README.md +├── SPEC.md (internal, removed automatically by the agent at pipeline completion) +├── src/ +│ └── main.ts +└── dist/ (generated by build, committed) + └── index.js +``` + +--- + +## action.yml Structure + +```yaml +name: "{Action Name}" +description: "{One-line description}" +author: "Bitwarden" + +inputs: + required_input: + description: "What this input controls" + required: true + optional_input: + description: "What this optional input controls" + required: false + default: "default_value" + +outputs: + output_name: + description: "What this output contains" + +runs: + using: "node24" + main: "dist/index.js" +``` + +### Key differences from composite + +- `runs.using` is `"node24"` (not `"composite"`) +- `runs.main` points to the compiled bundle at `dist/index.js` +- Outputs do not have a `value` field — they are set programmatically via `core.setOutput()` + +--- + +## package.json Structure + +```json +{ + "name": "@bitwarden/{action-name}", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "ncc build src/main.ts -o dist --license licenses.txt", + "postbuild": "node -e \"const fs=require('fs'); const f='dist/index.js'; fs.writeFileSync(f, fs.readFileSync(f,'utf8').replace(/\\r\\n/g,'\\n'))\"" + }, + "dependencies": { + "@actions/core": "^1.11.1" + }, + "devDependencies": { + "@vercel/ncc": "^0.38.4", + "typescript": "^5.9.3" + } +} +``` + +### Notes + +- `"type": "module"` enables ES module syntax +- `postbuild` normalizes line endings (CRLF → LF) for cross-platform consistency +- `@vercel/ncc` bundles all dependencies into a single `dist/index.js` +- Add integration-specific packages to `dependencies` based on SPEC.md + +--- + +## tsconfig.json Structure + +```json +{ + "compilerOptions": { + "target": "es2022", + "module": "nodenext", + "moduleResolution": "nodenext", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "rootDir": "./src", + "outDir": "./dist" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} +``` + +--- + +## src/main.ts Skeleton + +```typescript +import * as core from '@actions/core'; + +async function run(): Promise { + try { + // TODO: Read inputs + // TODO: Validate inputs + // TODO: Core logic + // TODO: Set outputs + } catch (error) { + core.setFailed(error instanceof Error ? error.message : String(error)); + } +} + +run(); +``` + +--- + +## .gitignore + +``` +node_modules/ +``` + +The `dist/` directory is NOT gitignored — compiled output must be committed. diff --git a/.claude/skills/validate-action/SKILL.md b/.claude/skills/validate-action/SKILL.md new file mode 100644 index 000000000..57fae71f6 --- /dev/null +++ b/.claude/skills/validate-action/SKILL.md @@ -0,0 +1,176 @@ +--- +name: validate-action +description: "Run formatting, structural, and Bitwarden workflow linter compliance checks on a GitHub Action. Fixes issues directly and reports results." +argument-hint: "" +allowed-tools: + - Read + - Edit + - Write + - Glob + - Grep + - Bash(ls:./*) + - Bash(npx prettier:*) + - Bash(bwwl lint:*) +--- + +# Validate Action - Correctness and Compliance Checks + +Run formatting, structural, and Bitwarden workflow linter compliance checks on a GitHub Action in the Bitwarden `gh-actions` repository. The deliverable is a clean action with all fixable issues resolved and a validation summary reported directly to the user. + +## Input + +This skill accepts a single required argument: the action directory name. + +The directory must contain `action.yml` and implementation files. A test workflow at `.github/workflows/test-{action-name}.yml` should also exist. + +**Examples:** +- `validate-action report-deployment-status-to-slack` +- `validate-action check-permission` + +## Procedure + +### Step 1: Validate Prerequisites + +1. Run `ls {action-name}/` to confirm the directory exists. +2. If the directory does not exist, stop and report: "Directory {action-name}/ not found." +3. Read `{action-name}/action.yml`. If it does not exist, stop and report: "No action.yml found in {action-name}/. Run scaffold-action first." +4. Determine the action type from the `runs.using` field in action.yml (composite, node24, docker). +5. Check for the test workflow at `.github/workflows/test-{action-name}.yml`. If missing, record a Critical finding: "Missing test workflow." + +### Step 2: Prettier Formatting + +Run Prettier to check all action files and the test workflow: +```bash +npx prettier --check "{action-name}/**" +npx prettier --check ".github/workflows/test-{action-name}.yml" +``` + +If any files fail the check, fix them: +```bash +npx prettier --write "{action-name}/**" +npx prettier --write ".github/workflows/test-{action-name}.yml" +``` + +Record each file that required formatting as a Low finding. + +### Step 3: action.yml Structure Validation + +Read `{action-name}/action.yml` and verify all required fields. Use `Grep` to search for specific keys if needed. + +- [ ] `name` -- present, descriptive, and starts with a capital letter +- [ ] `description` -- present and descriptive +- [ ] `author` -- set to `"Bitwarden"` +- [ ] `inputs` -- each input has `description` and `required` fields; multi-word names use underscores (not hyphens) +- [ ] `outputs` -- each output has `description`; for composite actions, each also has `value`; multi-word names use underscores (not hyphens) +- [ ] `runs` -- has correct `using` value for the action type (`composite`, `node24`, or `docker`) + +**Severity for missing fields:** +- Missing `name`, `description`, or `runs`: **Critical** +- Missing `author`: **Medium** +- Input/output missing `description` or `required`: **High** +- Composite output missing `value` reference: **Critical** +- `name` does not start with a capital letter: **Medium** +- Input or output name uses hyphens instead of underscores: **High** + +### Step 4: Bitwarden Workflow Linter Compliance + +Run the Bitwarden Workflow Linter (`bwwl`) against the test workflow. This is the authoritative source for linting rules — do not reimplement its checks manually. + +```bash +bwwl lint -f .github/workflows/test-{action-name}.yml +``` + +If `bwwl` is available: +1. Run it and capture the output. +2. Record each reported violation as a finding. Map severity from the linter output: + - Violations that would block PR merge: **High** + - Warnings: **Medium** +3. Fix each violation directly using `Edit`, then re-run `bwwl lint` to confirm the fix. + +If `bwwl` is not available (command not found): +1. Report to the user: "`bwwl` is not installed. Install with `pip install bitwarden_workflow_linter`. Falling back to manual checks." +2. Read `.github/workflows/test-{action-name}.yml` and manually check the following rules. Also read one existing test workflow (e.g., `.github/workflows/test-check-permission.yml`) as a reference for expected conventions and pinned SHAs. + + - **name_exists**: Workflow and every job must have a `name` field. Missing: **High** + - **name_capitalized**: Workflow, job, and step names must start with a capital letter. Lowercase: **Medium** + - **permissions_exist**: `permissions` key must be set at workflow level or on every job. Missing: **High** + - **pinned_job_runner**: `runs-on` must use pinned versions (e.g., `ubuntu-24.04`, not `ubuntu-latest`). Unpinned: **High** + - **step_pinned**: External `uses:` must be pinned to SHA with version comment (`owner/repo@SHA # vX.Y.Z`). Local actions exempt. Unpinned: **High** + - **step_approved**: Only approved external actions are used. Unapproved: **High** + - **underscore_outputs**: Multi-word output names must use underscores, not hyphens. Hyphenated: **High** + - **job_environment_prefix**: Job-level env vars follow Bitwarden naming conventions. Non-conforming: **Medium** + - **check_pr_target**: `pull_request_target` trigger must only run on default branch. Unrestricted: **Critical** + +### Step 5: File Naming Conventions + +- [ ] Action directory name is kebab-case (lowercase letters, numbers, hyphens only) +- [ ] Test workflow is named `test-{action-name}.yml` +- [ ] Source files follow language conventions (camelCase for TS, snake_case for Python) +- Non-conforming name: **Medium** + +### Step 6: Fix Issues + +Process all findings from Steps 2-5: + +1. **Critical** and **High** issues: Fix directly using `Edit` or `Write`. After fixing, update the finding status to "Fixed." +2. **Medium** issues: Fix directly unless the fix requires a design decision. If so, set status to "Flagged" with an explanation. +3. **Low** issues: Do not fix. Set status to "Noted." +4. After all fixes, re-run Prettier to ensure fixed files are properly formatted: + ```bash + npx prettier --write "{action-name}/**" + npx prettier --write ".github/workflows/test-{action-name}.yml" + ``` + +### Step 7: Report Results + +Print a validation summary directly to the user. Do NOT append results to SPEC.md — validation findings are mechanical and transient, not useful to downstream phases. + +Use this format: + +``` +Validation complete for {action-name}: {PASS / PASS WITH NOTES / FAIL} + +Findings: +| # | Severity | Category | Description | Status | +|---|----------|----------|-------------|--------| +| 1 | High | Linter Compliance | {description} | Fixed | +| 2 | Medium | Structure | {description} | Flagged | +| 3 | Low | Formatting | {description} | Noted | + +Summary: +- Critical/High fixed: {count} +- Medium flagged: {count} +- Low noted: {count} +- Files modified: {list} +``` + +**Status criteria:** +- **PASS**: Zero findings, or all findings were Low/Noted. +- **PASS WITH NOTES**: All Critical/High fixed, some Medium flagged for user review. +- **FAIL**: Any Critical/High issue could not be fixed (e.g., unapproved action with no alternative). + +**Zero-findings case:** + +``` +Validation complete for {action-name}: PASS + +All formatting, structural, and linter compliance checks passed. +``` + +## Important Rules + +- Fix all formatting and structural issues directly. Do not just report them. +- For linter compliance issues, fix them if possible. If a fix requires using an unapproved action, flag it to the user rather than removing the action. +- Always run Prettier AFTER making any fixes to ensure final files are formatted correctly. +- The pinned SHA for `actions/checkout` must match what other test workflows in the repo use. Read an existing test workflow (e.g., `test-check-permission.yml`) to get the current SHA. +- Do NOT change implementation logic. This skill only validates and fixes formatting, structure, and compliance. +- Do NOT add new features or refactor code. Focus strictly on correctness and compliance. +- Output names must use underscores, not hyphens (workflow linter requirement). +- Runners must be pinned to `ubuntu-24.04` (not `ubuntu-latest`). + +## Related Skills + +- **define-action**: Produces the SPEC.md that defines the action's requirements. Example: `define-action {action-name}` +- **scaffold-action**: Generates skeleton files from SPEC.md. Example: `scaffold-action {action-name}` +- **implement-action**: Replaces TODO placeholders with working code. Run before this skill. Example: `implement-action {action-name}` +- **evaluate-action**: Reviews implementation completeness against SPEC.md. Example: `evaluate-action {action-name}`