diff --git a/.cursor/rules/devops/github-actions.mdc b/.cursor/rules/devops/github-actions.mdc index c7f2853c51..9f4fe35e76 100644 --- a/.cursor/rules/devops/github-actions.mdc +++ b/.cursor/rules/devops/github-actions.mdc @@ -21,13 +21,15 @@ Pod scope and high-level principles live in `.cursor/rules/devops/main.mdc`. Thi ## Permissions -- **Top-level `permissions:` is mandatory.** Default to least privilege: - ```yaml - permissions: - contents: read - ``` -- **Widen per-job, not per-workflow,** when a job needs more (e.g., `pull-requests: write` for label automation). -- **`id-token: write` is required for OIDC.** Grant only on jobs that authenticate to a cloud provider. +- **Every workflow declares `permissions:` explicitly** — top-level OR per-job. Relying on repository-default permissions is forbidden. + - **Top-level** is preferred when all jobs share the same scope; default to least privilege: + ```yaml + permissions: + contents: read + ``` + - **Per-job** is preferred when scopes differ between jobs (e.g., one job needs `id-token: write` for OIDC, another only needs `contents: read`). Per-job blocks REPLACE the top-level — they do not merge. +- **Widen per-job, not per-workflow,** when a single job needs more than the rest (e.g., `pull-requests: write` for label automation). +- **`id-token: write` is required for OIDC.** Grant only on the specific job that authenticates to a cloud provider. - **Never use `permissions: write-all`.** If you think you need it, you don't. ## Triggers and untrusted input @@ -129,6 +131,7 @@ Pod scope and high-level principles live in `.cursor/rules/devops/main.mdc`. Thi - `release-.yml` / `create-github-release-.yml` — release pipelines - `prebuilds-.yml` — prebuilt artifact generation - `pr-test-*.yml` / `pr-validation-*.yml` / `pr-checks-*.yml` — PR test/validation pipelines + - `integration--.yml` — cross-package integration / e2e suites (e.g., mobile device-farm runs) - `reusable-*.yml` — reusable workflow building block (consumed via `workflow_call`) - `trigger-reusable-*.yml` — entrypoint workflow whose only job is to call a `reusable-*.yml` - **One responsibility per workflow file.** Split when triggers diverge. diff --git a/.cursor/skills/devops-daily-update/SKILL.md b/.cursor/skills/devops-daily-update/SKILL.md new file mode 100644 index 0000000000..05b308d9da --- /dev/null +++ b/.cursor/skills/devops-daily-update/SKILL.md @@ -0,0 +1,242 @@ +--- +name: devops-daily-update +description: Compose a daily standup in the team's Slack format (🔨 Done today / 📅 Planned for tomorrow / 🚧 Blockers / risks), aggregating recent PRs, reviews, and CI from tetherto/qvac. Use when asked for a daily update, standup, EOD, or invoking /devops-daily-update. +disable-model-invocation: true +--- + +# DevOps Daily Update + +Composes a standup / EOD update in the team's standard Slack format and writes it to a temp file ready to paste. Sourced from the user's GitHub activity in `tetherto/qvac` plus optional Asana context. + +The skill is read-only with respect to GitHub state and the local working tree. It NEVER posts the message — the user copies it manually. The canonical Slack form is documented in [Step 8](#8-assemble-the-output). + +## When to use this skill + +**Use when:** + +- User asks for a "daily update", "standup", "EOD", or "what did I do yesterday?" +- User invokes `/devops-daily-update` +- User asks to draft a status post for the team channel + +## Prerequisites + +- `gh` CLI installed and authenticated (`gh auth status`) +- User has access to `tetherto/qvac` +- Optional: Asana MCP available — surfaces today's assigned tasks (degrades gracefully if not) + +## Inputs + +- **Optional**: `--since ` — defaults to yesterday 00:00 in the user's local timezone. The default 24-hour lookback works for both EOD posts (late evening) and morning standups; extend it for Monday-after-weekend (`--since 3d`) or post-holiday (`--since 1w`). +- **Optional**: `--format slack | markdown` — defaults to `slack`. The Slack form is what gets pasted; Markdown is the chat preview form (`**bold**`, `[text](URL)`) and is also accepted by Asana rich-text comments. +- **Optional**: `--no-asana` — skip the Asana lookup even if the MCP is available. + +If the user did not specify, default to yesterday 00:00 / `slack`. + +## Safety rules + +This skill is read-only. It does NOT: + +- Modify the user's working tree, branch, or any file under `~/.cache/` +- Post to Slack, Asana, or GitHub +- Write secrets to any output (the assembler runs a regex scrub before file write; see step 7) + +The skill MAY write its assembled output to `/tmp/devops-daily-update-.txt` so the user can `pbcopy < `. The extension is `.txt` (not `.md`) because the canonical form is Slack mrkdwn, not GitHub-flavored Markdown. + +## Efficiency rules + +Total shell calls per run: **≤ 6** (one per data source + one for the timestamp + one to write the temp file). Cache `gh api user` and reuse via Read for the rest of the session. If a data source errors (e.g., Asana MCP not configured), continue with that section's items missing from the aggregate rather than failing the whole skill — the canonical form does not have an "Asana" section, so missing Asana data only thins out the bullet pools, not the layout. + +## Workflow + +### 1. Resolve the lookback window + +```bash +SINCE="$(date -u -v-1d -j -f "%Y-%m-%d" "$(date -u +%Y-%m-%d)" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null \ + || date -u -d 'yesterday 00:00' +%Y-%m-%dT%H:%M:%SZ)" +echo "$SINCE" +``` + +Parse `--since` if provided (`Nd` → N days, `Nw` → N weeks, ISO date → that date 00:00 UTC). + +### 2. Resolve the current user + +```bash +gh api user --jq '.login' > /tmp/devops-daily-update-user.txt +``` + +Reuse via `Read` for the rest of the run. + +### 3. Pull recently-merged PRs (mine) + +```bash +gh search prs \ + --repo tetherto/qvac \ + --author "@me" \ + --merged-at ">=$SINCE" \ + --json number,title,url,closedAt \ + --limit 30 \ + > /tmp/devops-daily-update-merged.json +``` + +These feed `🔨 Done today`. `gh search prs --json` does not expose `mergedAt`, `additions`, or `deletions` — only `closedAt` is available, and the `--merged-at ">=$SINCE"` filter already guarantees the result set is merged-in-window. If size signal is needed for the bullet wording, fetch it per-PR via `gh pr view --json additions,deletions` (one extra call per PR — only do this when the user asks for it). + +### 4. Pull my open PRs and reviews owed + +```bash +node .cursor/skills/_lib/pr-skills/pr-status.mjs --mode my \ + > /tmp/devops-daily-update-my.txt 2> /tmp/devops-daily-update-my.stderr +gh search prs \ + --repo tetherto/qvac \ + --review-requested "@me" \ + --state open \ + --json number,title,url,author,updatedAt \ + --limit 30 \ + > /tmp/devops-daily-update-reviews-owed.json +``` + +If `pr-status.mjs` stderr contains `SLACK_VALIDATION_REQUIRED`, follow the validation gate in [`pr-mine`'s workflow](../pr-mine/SKILL.md) (step 2). Do not present the daily update until the gate clears. + +Output routing: + +- Open PRs I authored that received commits since `$SINCE` (i.e., I pushed work on them today) → `🔨 Done today` with the action `addressed comments on the PR` (when the recent commits follow a review event) or `pushed updates on ` (otherwise). +- Open PRs I authored without recent commits → `📅 Planned for tomorrow` with the action `continue / wrap up `. +- Reviews owed (`--review-requested "@me"`) → `📅 Planned for tomorrow` as `review # by <author>`. **Cap surfaced reviews at 5** (sorted by `updatedAt` desc — most recent first); if the queue is longer, append a single line `(+N more review requests in queue — run /devops-pr-status for the full list)`. A standup with 30 review-bullets is unreadable. +- Open PRs I authored with `mergeable: CONFLICTING` → `🚧 Blockers / risks` as `conflicts on #<num> — needs rebase`. +- Open PRs I authored with stale review requests (no review activity in >3 days) → `🚧 Blockers / risks` as `stale review on #<num> — pinged <reviewer> on <date>`. + +### 5. Pull recent CI runs (filter to mine) + +`gh run list` does not have an author filter. Approximate the user's runs by scoping to recent PR head branches: + +```bash +gh run list \ + --repo tetherto/qvac \ + --created ">=$SINCE" \ + --limit 50 \ + --json conclusion,event,headBranch,name,url,workflowName,headSha,displayTitle \ + > /tmp/devops-daily-update-runs.json +``` + +Filter client-side: keep runs where `headBranch` matches one of the user's PRs from steps 3 or 4. Failed runs feed `🚧 Blockers / risks` as `CI failing on #<num> — <workflowName>`. In-progress / queued runs are NOT surfaced (too noisy for a daily update). + +### 6. (Optional) Pull today's Asana tasks + +If the Asana MCP is available and `--no-asana` was not passed, call the appropriate tool (read the descriptor first per the agentic-automation rule) to fetch the user's tasks. Filter to: + +- Status = in-progress or due today/tomorrow → feed `📅 Planned for tomorrow` as `<TICKET>: <task title>` +- Status = blocked → feed `🚧 Blockers / risks` as `<TICKET>: blocked — <reason from notes>` + +Asana tickets in the QVAC project follow the `QVAC-\d+` format and slot directly into the bullet shape. + +If Asana is unavailable or `--no-asana` is set, skip this step. The output will rely on GitHub-derived items only. + +### 7. Run a secret-pattern scrub on every assembled string + +Before writing `/tmp/devops-daily-update-<YYYY-MM-DD>.txt`, run a regex check on every PR title, branch name, run name, Asana task title, and any user-provided extras: + +``` +(sk_live_|AIza[0-9A-Za-z\-_]{35}|AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9]{36}|gho_|github_pat_|xoxb-|-----BEGIN [A-Z ]+ KEY-----) +``` + +If any string matches, redact the matching span (`[REDACTED]`) and add a chat-only note: "Daily update redacted N suspicious tokens — review the source PRs/runs manually." Never include the raw matched string anywhere in the output, the chat preview, or the temp file. + +### 8. Assemble the output + +Build the message in two forms: + +- **Slack form** (the canonical, matches the team template), saved to `/tmp/devops-daily-update-<YYYY-MM-DD>.txt`. +- **Markdown form** (for chat preview only), printed in chat. + +Each item from steps 3–6 must be normalized to a `TICKET: action` bullet. Ticket extraction rules: + +1. Extract `QVAC-\d+` (or `[A-Z]+-\d+`) from the PR title; that's the ticket. +2. If the PR title has no ticket, extract from the head branch name (e.g., `feat/QVAC-12345-thing`). +3. If still no ticket, fall back to `#<pr-number>` as the leading label. +4. The action is the PR-title subject (the part after `prefix[tags]:`), past tense for `Done today`, action-verb-leading for `Planned for tomorrow`. Drop the `prefix[tags]:` from the rendered action. +5. Sub-bullets are added when (a) the user provided extra context for that item, or (b) more than one PR shares the same ticket — the parent line is the ticket, sub-bullets are each PR. + +#### Slack form (canonical) + +``` +🔨 *Done today* +- <TICKET-or-#num>: <past-tense action> +- ... + - <optional sub-bullet> + +📅 *Planned for tomorrow* +- <TICKET-or-#num>: <forward-looking action> +- ... + +🚧 *Blockers / risks* +- N/A +``` + +Empty section → single bullet `- N/A` (literal). Three sections always rendered, in this order, separated by a single blank line. Bare ticket bullets (`- QVAC-13860` with no `:` and no action) are allowed when the work is self-evident from the ticket title. + +#### Markdown form (chat preview only) + +``` +**🔨 Done today** +- <TICKET-or-#num>: <past-tense action> +- ... + +**📅 Planned for tomorrow** +- <TICKET-or-#num>: <forward-looking action> +- ... + +**🚧 Blockers / risks** +- N/A +``` + +(Same content, GitHub-flavored Markdown rendering for the chat preview only.) + +#### Format-conversion cheatsheet + +If the user requested `--format markdown`, save the Markdown form to the temp file too. Conversion rules between the two forms: + +| Markdown | Slack | +|---|---| +| `**X**` | `*X*` | +| `*X*` (italic) | `_X_` | +| `[text](URL)` | `<URL\|text>` | +| `# H1` / `## H2` | not used (use Slack-bold instead) | +| Plain `QVAC-\d+` | Plain `QVAC-\d+` (the workspace's Slack/Asana integration auto-links) | +| 4-space-indented `- sub` | 4-space-indented `- sub` (Slack respects 4-space indent for sub-bullets) | + +Do NOT pre-link ticket numbers via `<URL|TICKET>` — the workspace's Asana app handles auto-linking. Pre-linking conflicts with that and renders awkwardly. + +### 9. Print the result + +1. Print the Markdown form in a fenced code block in chat for the user to scan. +2. Print the path to the Slack-form temp file with copy commands: + + ```bash + pbcopy < /tmp/devops-daily-update-<YYYY-MM-DD>.txt # macOS + xclip -selection clipboard < /tmp/devops-daily-update-<YYYY-MM-DD>.txt # Linux + ``` + +3. Offer: "Edit any line before posting? Tell me which ticket and the new action wording, and I'll regenerate." + +## Quality Checklist + +Before printing the output, verify: + +- [ ] Three sections rendered, in order: 🔨 Done today / 📅 Planned for tomorrow / 🚧 Blockers / risks +- [ ] Section headings use the exact emoji and the exact section names from the canonical template +- [ ] Empty sections render as a single `- N/A` bullet (never `_(none)_`, never empty, never removed) +- [ ] Every bullet leads with `TICKET:` (or `#<pr-num>:` only when no ticket could be extracted) +- [ ] No bullet prefixes, no severity tags, no PR-state tags (`[needs-review]`, `[ready]`, etc.) — that meta is folded into prose actions +- [ ] Each `Done today` item is genuinely activity since `$SINCE` (merged PR, pushed commits, etc. — not stale) +- [ ] Each `Planned for tomorrow → review` item is open and the user is in `requestedReviewers` +- [ ] Each `Blockers / risks → CI failing` item is on a PR the user authored or a branch they own +- [ ] No raw secret-shaped strings made it through the scrub +- [ ] Slack form has no Markdown headings, no `**bold**` (uses `*bold*`), no GitHub-style links +- [ ] Temp-file path matches the day's local-tz ISO date + +## References + +- DevOps main rule: [.cursor/rules/devops/main.mdc](.cursor/rules/devops/main.mdc) +- Agentic automation rule: [.cursor/rules/devops/agentic-automation.mdc](.cursor/rules/devops/agentic-automation.mdc) (read-only default; bounded shell calls; idempotency) +- Cross-pod my-PRs skill: [.cursor/skills/pr-mine/SKILL.md](.cursor/skills/pr-mine/SKILL.md) +- DevOps PR status skill: [.cursor/skills/devops-pr-status/SKILL.md](.cursor/skills/devops-pr-status/SKILL.md) +- Pod metadata: [.github/teams/devops.json](.github/teams/devops.json) diff --git a/.cursor/skills/devops-pr-create/SKILL.md b/.cursor/skills/devops-pr-create/SKILL.md new file mode 100644 index 0000000000..5dc25807c4 --- /dev/null +++ b/.cursor/skills/devops-pr-create/SKILL.md @@ -0,0 +1,188 @@ +--- +name: devops-pr-create +description: Generate PR titles and descriptions for DevOps surfaces (CI/CD, composite actions, automation scripts, IaC) following the devops.md PR template and commit/PR format rule. Use when creating a DevOps PR or invoking /devops-pr-create. +disable-model-invocation: true +--- + +# DevOps PR Creation + +Generate PR titles and descriptions for DevOps changes (CI/CD workflows, composite actions, repo-wide automation scripts, IaC), following the DevOps pod's format rule and PR template. + +## When to use this skill + +**Applies to PRs whose changes are dominated by paths in [`.github/teams/devops.json`'s `ownedPaths`](.github/teams/devops.json):** `.github/workflows/`, `.github/actions/`, `.github/scripts/`, `scripts/`. Also applies to repo-level configuration changes (`.github/CODEOWNERS`, `.github/dependabot.yml`, top-level Dockerfiles, IaC under top-level `terraform/`, `ansible/`, `k8s/`). + +**Use when:** + +- Creating a PR for any DevOps change +- User asks to generate a DevOps PR description +- User invokes `/devops-pr-create` + +If the touched paths are dominated by a non-DevOps pod (e.g., `packages/sdk/**`), use that pod's `*-pr-create` skill instead. If a PR mixes DevOps + package changes, prefer the package's pod skill and call out the cross-pod touches in the PR body. + +## Prerequisites + +- `gh` CLI installed and authenticated (`gh auth status`) +- Branch is pushed to a fork (or the upstream when the user has write access to `tetherto/qvac`) +- Local branch tracks the remote being PR'd from + +## Workflow + +1. Confirm base and current branch — note whether the base is `main` (default) or a `release-*` branch (uncommon for DevOps; treat as user-provided). +2. Collect commits/diff from `<base>...origin/<branch>`. +3. Infer ticket, prefix, and tag from changes (see Inference Strategy). +4. Detect trigger sections (action pinning / permissions / plan-dry-run / breaking) and only ask the user for input when inference confidence is low. +5. Generate title: `TICKET prefix[tag]?: subject`. +6. Fill template sections based on changes and detected triggers. +7. Validate tag requirements (`[bc]`). +8. **Save the assembled PR body to `/tmp/pr-body.md`** so subsequent steps and any `gh pr create` invocation can read it back without re-rendering. Print the body in chat for inspection AND keep the file canonical (the gh-CLI step below reads from it). +9. Print the paste-ready commands alongside the in-chat preview — both for direct paste into the GitHub PR-create form and for clipboard tools: + + ```bash + pbcopy < /tmp/pr-body.md # macOS + xclip -selection clipboard < /tmp/pr-body.md # Linux + wl-copy < /tmp/pr-body.md # Wayland + ``` + +10. Optionally create the PR via `gh` (see "gh CLI Integration" below). + +## Inference Strategy + +Infer first, ask only if uncertain. + +**Ticket number:** + +- Extract from branch name pattern: `QVAC-\d+`, `SDK-\d+`, etc. +- Extract from commit messages if referenced. +- ASK only if no ticket found (offer `[notask]` as the alternative). + +**Prefix (`infra` / `feat` / `fix` / `chore` / `doc` / `test`):** + +- Extract from branch name prefix when present (e.g. `infra/`, `fix/`, `chore/`). +- Use majority prefix from existing commit messages. +- If no conventional commits, infer from diff: + - `.github/workflows/**`, `.github/actions/**`, `.github/scripts/**`, `scripts/**`, IaC files, Dockerfiles, runner config → `infra` + - New skill / new workflow type / new composite action → `feat` + - Bug-related changes (commit messages mention "fix", "broken", "regression"; reverts) → `fix` + - Only `.md` / docs files → `doc` + - Only test files (`.github/actions/*-test/**`, `*.test.*`, `tests/**`) → `test` + - Pure dependency bumps / deprecations / lockfile churn → `chore` +- ASK only on mixed signals. + +**Tag (`[bc]` / `[notask]` / `[skiplog]` — not combinable):** + +- `[bc]` (breaking change) when the diff: + - Renames a job whose name is referenced by a branch protection ruleset (status check rename) + - Changes a reusable workflow's `inputs:` / `outputs:` shape (workflow_call signature) + - Changes a composite action's `inputs:` / `outputs:` shape (`.github/actions/<name>/action.yml`) + - Changes a `workflow_call` `secrets:` or `permissions:` contract + - Removes a slash command / public surface from a skill that other teams consume +- `[notask]` (PRs only): when no ticket — minor automation hygiene. Ask the user before applying. +- `[skiplog]`: only when the user explicitly asks (this repo uses `[skiplog]` to opt out of changelog generation; not the default for DevOps). + +ASK if `[bc]` is ambiguous. + +## Trigger detection (drives required template sections) + +Walk the diff before assembling the body. For each trigger detected, the corresponding section is REQUIRED in the PR body. + +| Trigger | Detection signal | Required section | +|---|---|---| +| Third-party action added / bumped / repinned | `git diff` shows a `uses: <vendor>/<action>@<sha>` change in any `.github/workflows/**` or `.github/actions/**/action.yml` | "🔐 Action pinning" with before/after SHA + version | +| Top-level or per-job `permissions:` block added / modified / removed | `git diff` shows `^[+-].*permissions:` or `^[+-]\s+(contents|pull-requests|id-token|...)\s*:` | "🛡️ Permissions changes" with scope, before/after, justification | +| State-changing op | Any of: `terraform/**` `*.tf`, `ansible/**` `*.yml`, `k8s/**` `*.yaml`, `gh ruleset edit` invocations, branch-protection patches | "📋 Plan / dry-run output" — paste the plan/diff | +| Breaking change | `[bc]` tag set per Inference Strategy | "💥 Breaking changes" with BEFORE/AFTER YAML blocks | + +Sections that are NOT triggered MUST be deleted (per the template's "Delete this section if not applicable" markers). + +## Format References + +- **PR title format**: see the Validation regex below — `^([A-Z]+-\d+ )?(infra|feat|fix|chore|doc|test)(\[(bc|notask|skiplog)\])?: \S.+$` +- **PR body template**: [.github/PULL_REQUEST_TEMPLATE/devops.md](.github/PULL_REQUEST_TEMPLATE/devops.md) +- **Pod conventions**: [.cursor/rules/devops/main.mdc](.cursor/rules/devops/main.mdc) +- **GHA conventions** (drives "How was it tested?" content): [.cursor/rules/devops/github-actions.mdc](.cursor/rules/devops/github-actions.mdc) + +Fill the template based on the diff analysis. Delete sections that don't apply. + +## Output Format + +ALWAYS output the PR in this copy-ready format, even when making corrections: + +~~~ +## PR Title +``` +TICKET prefix[tag]?: subject +``` + +## PR Body +```markdown +## 🎯 What problem does this PR solve? +... +``` +~~~ + +## Validation + +No `pr-validation-devops.yml` workflow exists yet (the SDK-pod validator is paths-scoped to `packages/<pkg>/`). This skill MUST validate the title client-side before pushing or invoking `gh pr create`. Refuse and ask for correction if any of these fail: + +- Title regex: `^([A-Z]+-\d+ )?(infra|feat|fix|chore|doc|test)(\[(bc|notask|skiplog)\])?: \S.+$` +- Lowercase prefix and lowercase subject (sentence case allowed; the first word may be capitalized for proper nouns) +- A ticket prefix (`QVAC-\d+`) is present unless `[notask]` is used +- For `[bc]`: body contains a "💥 Breaking changes" section with BEFORE/AFTER fenced blocks +- For an action-pinning trigger: body contains a "🔐 Action pinning" section +- For a permissions trigger: body contains a "🛡️ Permissions changes" section +- For an IaC/state-changing trigger: body contains a "📋 Plan / dry-run output" section + +## gh CLI Integration + +After generating the PR description, check for `gh` and offer to create the PR: + +1. `which gh` — confirm CLI is installed +2. `git remote -v` — identify whether `origin` is the upstream (`tetherto/qvac`) or a fork +3. Ask the user: "Create PR now with gh CLI?" [Yes / No / Preview first] +4. If yes, ensure changes are committed and pushed first +5. Create PR with explicit base/head; for fork workflows, pass `--repo`, `--base`, `--head`: + +```bash +gh pr create \ + --repo tetherto/qvac \ + --base main \ + --head <fork-owner>:<branch> \ + --title "TICKET infra: subject" \ + --body "$(cat /tmp/pr-body.md)" +``` + +For direct-push workflows (the user has write to `tetherto/qvac`): + +```bash +gh pr create \ + --base main \ + --title "TICKET infra: subject" \ + --body "$(cat /tmp/pr-body.md)" +``` + +6. Print the resulting PR URL as a clickable hyperlink. + +**Never run `gh pr create --web`** for this skill — `--web` does not actually create the PR; it only opens the browser. The body we generated would be lost. + +## Quality Checklist + +Before outputting, verify: + +- [ ] Title matches the regex above +- [ ] "What problem" describes operator/user impact, not implementation +- [ ] "How it solves" is high-level approach, not line-by-line +- [ ] "How was it tested?" lists concrete validation steps (`actionlint` clean, `workflow_dispatch` test run, `terraform plan` output, `kubectl diff` output, etc.) +- [ ] Untriggered template sections are deleted +- [ ] `[bc]` body has BEFORE/AFTER YAML blocks +- [ ] Action-pinning trigger → action-pinning section present +- [ ] Permissions trigger → permissions section present +- [ ] State-changing trigger → plan/dry-run section present + +## References + +- DevOps pod main rule: [.cursor/rules/devops/main.mdc](.cursor/rules/devops/main.mdc) +- GitHub Actions conventions: [.cursor/rules/devops/github-actions.mdc](.cursor/rules/devops/github-actions.mdc) +- Secrets handling: [.cursor/rules/devops/secrets-and-credentials.mdc](.cursor/rules/devops/secrets-and-credentials.mdc) +- PR template: [.github/PULL_REQUEST_TEMPLATE/devops.md](.github/PULL_REQUEST_TEMPLATE/devops.md) +- Pod metadata: [.github/teams/devops.json](.github/teams/devops.json) diff --git a/.cursor/skills/devops-pr-review/SKILL.md b/.cursor/skills/devops-pr-review/SKILL.md new file mode 100644 index 0000000000..e7864ecce7 --- /dev/null +++ b/.cursor/skills/devops-pr-review/SKILL.md @@ -0,0 +1,140 @@ +--- +name: devops-pr-review +description: PR review for DevOps changes — runs the generic /pr-review flow then layers a structured GitHub Actions security audit (action pinning, permissions, OIDC, hardened runner, secrets handling). Use when reviewing a PR that touches DevOps paths or invoking /devops-pr-review. +disable-model-invocation: true +--- + +# DevOps PR Review + +A DevOps-flavored PR review on top of the generic [`/pr-review`](../pr-review/SKILL.md) flow. Adds a deterministic GitHub Actions security audit pass driven by [`.cursor/rules/devops/github-actions.mdc`](../../rules/devops/github-actions.mdc) and [`.cursor/rules/devops/secrets-and-credentials.mdc`](../../rules/devops/secrets-and-credentials.mdc), so DevOps reviewers always see the same checklist of high-risk patterns regardless of the prose style of the diff. + +The skill is **a wrapper, not a fork** — the bulk of the review (gitflow, CI, title/body, generic correctness, security) is done by `/pr-review`. This skill layers DevOps-specific findings into the same pending review payload. + +## When to use this skill + +**Use when:** + +- User asks to review a PR whose changes are dominated by DevOps-owned paths (`.github/workflows/`, `.github/actions/`, `.github/scripts/`, `scripts/`, IaC under `terraform/`, `ansible/`, `k8s/`, `Dockerfile*`, `.github/CODEOWNERS`, `.github/dependabot.yml`) +- User invokes `/devops-pr-review` with a PR URL +- Triggered as a follow-up from `/devops-pr-status` after the user picks a PR + +If the PR's touched paths are dominated by a different pod, fall back to `/pr-review`. The generic flow already auto-loads the relevant pod's rules. + +## Inputs + +- **Required**: PR URL (`https://github.com/tetherto/qvac/pull/<num>`) +- **Optional**: user-provided focus notes + +If the URL is missing, ask for it. Nothing else to ask up-front. + +## Safety rules + +Inherits all safety rules from [`/pr-review`](../pr-review/SKILL.md#safety-rules--do-not-touch-the-users-local-repo) verbatim: + +- **Read-only with respect to the user's local working tree.** No `git switch`, `git checkout`, `git reset`, `git restore`, `git stash`, `git pull`, `git merge`, `git rebase`, `git cherry-pick`, `git clean`, `gh pr checkout`, or any write inside the user's working tree. +- All file inspection happens in the worktree cache at `~/.cache/qvac-pr-review/pr-<num>/` (managed by `worktree-prepare.mjs`) or via `gh api .../contents/<path>?ref=<sha>` to `/tmp/`. + +## Efficiency rules + +Inherits the `~5–8 shell-call` budget from `/pr-review`. The DevOps audit pass adds at most **3 additional reads** (Read/Grep/Glob, not shell) per touched workflow/action file. Cache: reuse `/tmp/pr-<num>.json`, `/tmp/pr-<num>.patch`, and the worktree path from the underlying `/pr-review` run. + +## Workflow + +### 1. Run the generic /pr-review flow up to step 7a (overview) + +Read [`.cursor/skills/pr-review/SKILL.md`](../pr-review/SKILL.md) and follow steps 0a → 7a inclusive: + +- 0a. Prepare worktree (`worktree-prepare.mjs`) +- 1. Parse PR URL +- 2. Fetch PR metadata +- 3. Gitflow validation +- 4. Read applicable cursor rules — the touched DevOps paths will auto-load `.cursor/rules/devops/main.mdc`, `.cursor/rules/devops/github-actions.mdc`, and `.cursor/rules/devops/secrets-and-credentials.mdc`. (The PR-format spec is intentionally NOT a rule — it lives in the [`devops-pr-create`](../devops-pr-create/SKILL.md) skill, invoked explicitly.) +- 5. Validate PR title + body against the format spec inlined below (DevOps allowed prefixes: `infra`/`feat`/`fix`/`chore`/`doc`/`test`; tags: `[bc]`/`[notask]`/`[skiplog]`; title regex: `^([A-Z]+-\d+ )?(infra|feat|fix|chore|doc|test)(\[(bc|notask|skiplog)\])?: \S.+$`) +- 6. Review dimensions +- 7a. Print risk overview in chat — but **do not yet run step 7b** (the inline-comment selection prompt). The DevOps audit pass in step 2 below contributes additional findings. + +### 2. Run the DevOps GitHub Actions security audit + +For every `.github/workflows/*.yml` and `.github/actions/**/action.yml` in the patch, perform the checks below using Read/Grep/Glob against the worktree path. Each check that fires becomes a finding with a tier per the table. + +| # | Check | Source rule | Finding tier | +|---|---|---|---| +| A1 | Every `uses: <vendor>/<action>@<ref>` is pinned to a 40-char SHA (regex `@[0-9a-f]{40}\b`) with a trailing `# v<ver>` comment. Tag pins (`@v3`, `@main`, branch refs) fail. First-party `./.github/actions/<name>` references are exempt. | github-actions.mdc § Action references | High | +| A2 | Permissions are declared explicitly — either a top-level `permissions:` block, OR every job has its own `permissions:` block. Relying on repo-default permissions fails. | github-actions.mdc § Permissions | High | +| A3 | No occurrence of `permissions: write-all` anywhere in the workflow. | github-actions.mdc § Permissions | High | +| A4 | If the workflow uses `pull_request_target`, none of its jobs check out PR HEAD via `actions/checkout@... ref: ${{ github.event.pull_request.head.sha }}` or `${{ github.head_ref }}`. | github-actions.mdc § Triggers and untrusted input | High | +| A5 | No direct interpolation of `${{ github.event.pull_request.title }}`, `body`, `head_ref`, `commits[*].message`, or any `github.event.*` user-controlled field inside `run:` blocks. They MUST be piped via `env:`. | github-actions.mdc § Triggers and untrusted input | High | +| A6 | If the workflow has `id-token: write`, OIDC is consumed by an auth action (`google-github-actions/auth`, `aws-actions/configure-aws-credentials`, `azure/login`) — not used for token forging that reaches a long-lived credential. | github-actions.mdc § OIDC | Medium | +| A7 | Sensitive workflows (touch secrets, publish artifacts, deploy, or `id-token: write`) run `step-security/harden-runner` as the first step. Missing → finding. | github-actions.mdc § Hardened runners | Medium | +| A8 | No `${{ secrets.X }}` interpolated directly inside a `run:` block. Pass via `env:` or action `with:` instead. | secrets-and-credentials.mdc § Access in workflows | High | +| A9 | No `set -x` / `bash -x` in steps that touch secrets; no `printenv`, `env`, `cat`, or `echo` of a secret value, even for "debugging". | secrets-and-credentials.mdc § Access in workflows | High | +| A10 | Concurrency block is declared. Release/state-mutating workflows have `cancel-in-progress: false`. | github-actions.mdc § Concurrency | Medium | +| A11 | Every job has `timeout-minutes`. Default budget 30; >30 needs a justifying comment. | github-actions.mdc § Failure handling | Low | +| A12 | `continue-on-error: true` is NOT set on Tier-1 checks (lint, format, type-check, security scans, tests). | github-actions.mdc § Failure handling | Medium | +| A13 | Outputs use `$GITHUB_OUTPUT`, never the deprecated `::set-output::`. | github-actions.mdc § Outputs and step IDs | Low | +| A14 | When `actions/cache` is used, cache writes are gated on non-fork triggers (`push` / `workflow_dispatch` / `merge_group`) — not blanket on `pull_request`. | github-actions.mdc § Caching | Medium | +| A15 | Workflow filename matches the existing repo conventions (`on-pr-*.yml`, `on-merge-*.yml`, `on-pr-close-*.yml`, `release-*.yml`, `create-github-release-*.yml`, `prebuilds-*.yml`, `pr-test-*.yml`, `pr-validation-*.yml`, `pr-checks-*.yml`, `integration-*-*.yml`, `reusable-*.yml`, `trigger-reusable-*.yml`). New file with a divergent name → finding. Pre-existing files keep their name unless the PR renames them. | github-actions.mdc § File layout and naming | Low | + +For each finding, capture: file, line, the offending excerpt (3-8 lines), the tier, and a one-line "why" pulled from the rule. Write findings into the same chat overview structure that `/pr-review` step 7a uses, under a new sub-heading `### GHA security audit`. + +### 3. Re-print the consolidated chat overview + +After both passes, re-print the full overview (gitflow / CI / generic-high / generic-medium / GHA-audit / lows / verified) so the user sees one ranked list. Findings retain the deep-link + excerpt rules from `/pr-review` step 7a. + +### 4. Run /pr-review steps 7b → 12 + +Continue with the generic flow: + +- 7b. Selection prompt — the multi-select includes BOTH generic findings and GHA-audit findings. Defaults: every High pre-selected (including all GHA-audit Highs); Medium pre-selected; Low opt-in. +- 8. Assemble inline comments from the user-confirmed set +- 9. Pre-flight check +- 10. Show the `gh api ... pulls/<num>/reviews` command, wait for confirmation +- 11. Post on confirmation +- 12. Output the link to the pending review + +### 5. Audit summary in chat (after posting) + +After the pending review URL is printed, append a single-line audit summary: + +``` +GHA audit: <H high> / <M medium> / <L low> findings on N workflow files. <K> filed inline; <skipped> skipped per user. +``` + +This makes audit coverage observable without reading the inline payload. + +## Inline comment style for GHA-audit findings + +Inherit the comment style from `/pr-review`. Add one DevOps-specific convention: every GHA-audit finding's body MUST cite the rule it traces back to, e.g.: + +```markdown +Untrusted input piped directly into shell. Per `.cursor/rules/devops/github-actions.mdc § Triggers and untrusted input`, +this MUST go through an `env:` block — `${{ github.event.pull_request.title }}` lands in the rendered shell verbatim and is exploitable. + +```yaml +env: + PR_TITLE: ${{ github.event.pull_request.title }} +run: | + echo "$PR_TITLE" +``` +``` + +The cite makes the audit finding auditable: the reviewer can verify the rule still says what the comment claims. + +## Quality Checklist + +Before posting the pending review, verify: + +- [ ] Underlying `/pr-review` workflow ran through step 7a successfully (worktree prepared, gitflow checked, CI checked, rules loaded, title/body validated) +- [ ] GHA-audit pass ran on every changed `.github/workflows/*.yml` and `.github/actions/**/action.yml` +- [ ] Each GHA-audit finding has: file path, post-change line, 3-8 line excerpt at PR head SHA, deep link, tier, rule cite +- [ ] User-confirmed selection from step 7b matches the assembled payload exactly (count + IDs) +- [ ] Audit summary line is printed after step 12 + +## References + +- Generic PR review skill (parent flow): [.cursor/skills/pr-review/SKILL.md](../pr-review/SKILL.md) +- DevOps GHA conventions (audit source): [.cursor/rules/devops/github-actions.mdc](../../rules/devops/github-actions.mdc) +- DevOps secrets handling (audit source): [.cursor/rules/devops/secrets-and-credentials.mdc](../../rules/devops/secrets-and-credentials.mdc) +- DevOps PR-create skill (canonical home for commit / PR-title format): [`devops-pr-create`](../devops-pr-create/SKILL.md) +- Pod metadata: [.github/teams/devops.json](../../../.github/teams/devops.json) +- Worktree manager: [.cursor/skills/_lib/pr-skills/worktree-prepare.mjs](../_lib/pr-skills/worktree-prepare.mjs) diff --git a/.cursor/skills/devops-pr-status/SKILL.md b/.cursor/skills/devops-pr-status/SKILL.md new file mode 100644 index 0000000000..c9349dd5f7 --- /dev/null +++ b/.cursor/skills/devops-pr-status/SKILL.md @@ -0,0 +1,57 @@ +--- +name: devops-pr-status +description: Team-wide PR dashboard for the DevOps pod. Shows open PRs touching DevOps-owned paths, grouped into needs-your-re-review / stale (>3d) / needs-review, with merge-conflict warnings. Use when checking DevOps pod PR status, asking about stale PRs, or invoking /devops-pr-status. +disable-model-invocation: true +--- + +# DevOps Pod PR Status + +Thin wrapper over the shared pr-skills library, pinned to the DevOps pod. + +## When to use this skill + +**Use when:** + +- User asks about open DevOps pod PRs, review status, or what needs attention +- User asks specifically about stale PRs touching DevOps paths +- User wants to know which DevOps pod PRs to review next +- User invokes `/devops-pr-status` + +## Prerequisites + +- `gh` CLI installed and authenticated (`gh auth status`) +- User must have access to `tetherto/qvac` repository +- Team roster maintained at [.github/teams/devops.json](.github/teams/devops.json) + +## Usage + +```bash +DATE="$(date -u +%Y-%m-%d)" +node .cursor/skills/_lib/pr-skills/pr-status.mjs --pod devops --mode team \ + 2> /tmp/devops-pr-status-${DATE}.stderr \ + | tee "/tmp/devops-pr-status-${DATE}.txt" +``` + +For the personal review queue scoped to DevOps PRs, use `--mode review`. The script and its output format are documented in [.cursor/skills/_lib/pr-skills/README.md](.cursor/skills/_lib/pr-skills/README.md). + +## Workflow + +1. Run the script with `--pod devops --mode team`, **teeing stdout to `/tmp/devops-pr-status-<YYYY-MM-DD>.txt`** so the dashboard is available for paste afterwards. Redirect stderr to a sibling `.stderr` file (it contains progress / `SLACK_VALIDATION_REQUIRED` notices, not dashboard content). +2. Present the grouped output to the user. +3. Surface the summary header counts (need your re-review / stale / merge conflicts) prominently. +4. **Print the paste-ready copy commands.** The dashboard is plain text with two-space indent — when pasted into a Slack thread, Slack auto-renders the indented lines as nested bullets and turns `#<num>` into PR auto-links (with the em-dash separator). No re-formatting is needed. + + ```bash + pbcopy < /tmp/devops-pr-status-${DATE}.txt # macOS + xclip -selection clipboard < /tmp/devops-pr-status-${DATE}.txt # Linux + wl-copy < /tmp/devops-pr-status-${DATE}.txt # Wayland + ``` + +5. After showing results, offer: "Want me to review any of these? Provide the PR URL and I'll run `/devops-pr-review` (or `/pr-review` for the generic flow)." + +## References + +- Pod metadata: [.github/teams/devops.json](.github/teams/devops.json) +- Shared library README: [.cursor/skills/_lib/pr-skills/README.md](.cursor/skills/_lib/pr-skills/README.md) +- Generic PR review skill: [.cursor/skills/pr-review/SKILL.md](.cursor/skills/pr-review/SKILL.md) +- DevOps-flavored PR review skill: [.cursor/skills/devops-pr-review/SKILL.md](.cursor/skills/devops-pr-review/SKILL.md) diff --git a/.github/PULL_REQUEST_TEMPLATE/devops.md b/.github/PULL_REQUEST_TEMPLATE/devops.md index 99db54b047..0355514d73 100644 --- a/.github/PULL_REQUEST_TEMPLATE/devops.md +++ b/.github/PULL_REQUEST_TEMPLATE/devops.md @@ -1,5 +1,3 @@ -**Note**: be concise and prefer bullet points. - ## 🎯 What problem does this PR solve? -