From ef618b766cb689ea7a1686dd0a8dbad1bcf0f944 Mon Sep 17 00:00:00 2001 From: Oluwaseun Ismaila Date: Wed, 6 May 2026 16:40:54 +0100 Subject: [PATCH 1/4] QVAC-18394 infra: add devops pod conventions, team file, and PR template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Baseline DevOps pod metadata and conventions to unblock the QVAC-18394 skill subtasks (Stale-Prs, Create-pr, Daily-update, Pr-review). Documentation and config only; no behavior change. Files: - .github/teams/devops.json — pod metadata (leads, members, ownedPaths) - .cursor/rules/devops/main.mdc — pod entry point + operating principles - .cursor/rules/devops/github-actions.mdc — workflow/action conventions - .cursor/rules/devops/secrets-and-credentials.mdc — secrets handling + leak-response playbook - .cursor/rules/devops/agentic-automation.mdc — read-only-default, plan-then-apply, validation-before-success for AI-driven work - .cursor/rules/devops/commit-and-pr-format.mdc — commit/PR title format scoped to .github/** and scripts/** (sdk pod's rule is package-scoped) - .github/PULL_REQUEST_TEMPLATE/devops.md — PR body template mirroring sdk-pod.md / addon.md discipline (flat sections only) Validated: - All .mdc frontmatter parses cleanly (description, globs, alwaysApply) - devops.json parses cleanly - No linter errors, no secret patterns matched - PR template structure mirrors existing templates (no H3 nesting, no tables, no HTML) Co-authored-by: Cursor --- .cursor/rules/devops/agentic-automation.mdc | 110 ++++++++++++++ .cursor/rules/devops/commit-and-pr-format.mdc | 118 +++++++++++++++ .cursor/rules/devops/github-actions.mdc | 134 ++++++++++++++++++ .cursor/rules/devops/main.mdc | 95 +++++++++++++ .../rules/devops/secrets-and-credentials.mdc | 113 +++++++++++++++ .github/PULL_REQUEST_TEMPLATE/devops.md | 62 ++++++++ .github/teams/devops.json | 13 ++ 7 files changed, 645 insertions(+) create mode 100644 .cursor/rules/devops/agentic-automation.mdc create mode 100644 .cursor/rules/devops/commit-and-pr-format.mdc create mode 100644 .cursor/rules/devops/github-actions.mdc create mode 100644 .cursor/rules/devops/main.mdc create mode 100644 .cursor/rules/devops/secrets-and-credentials.mdc create mode 100644 .github/PULL_REQUEST_TEMPLATE/devops.md create mode 100644 .github/teams/devops.json diff --git a/.cursor/rules/devops/agentic-automation.mdc b/.cursor/rules/devops/agentic-automation.mdc new file mode 100644 index 0000000000..c6f551c165 --- /dev/null +++ b/.cursor/rules/devops/agentic-automation.mdc @@ -0,0 +1,110 @@ +--- +description: Agentic automation rules — plan-then-apply for state changes, read-only by default, validation-before-success, provenance, idempotency, fail-stop on ambiguity. Applies to skills, hooks, and any AI-driven DevOps workflow. +globs: + - .cursor/skills/** + - .cursor/rules/** + - .cursor/hooks/** + - .github/workflows/** + - .github/scripts/** + - scripts/** +alwaysApply: false +--- + +# Agentic Automation + +Rules governing agent-driven automation in DevOps surfaces — Cursor skills, hooks, MCPs, AI-invoked scripts, and any workflow where an agent takes action on the user's or repo's behalf. + +## Core stance + +- **AI-first, human-gated.** Agents propose; humans approve state changes. The agent should always be the one drafting the diff, the command, the PR body — never the one quietly running `apply`. + +## Read-only default + +- **Skills default to read-only.** Mutations to the local repo, the remote (GitHub state, releases, branch protection), or external systems (cloud, k8s, Slack, Asana) require explicit user opt-in per call. +- **The `pr-review` pattern is the reference**: print the exact command, wait for user confirmation, then execute. Never auto-execute. +- **No silent file mutations** to the user's working tree. If a skill writes outside `/tmp/` or its own `_lib/` cache, it announces what and why first. +- **No silent git mutations**: forbidden without explicit user instruction — `git switch`, `git checkout` (any ref/file), `git reset`, `git restore`, `git stash`, `git pull`, `git merge`, `git rebase`, `git cherry-pick`, `git clean`, `gh pr checkout`. (See `.cursor/skills/pr-review/SKILL.md` "Safety rules" section for the canonical wording.) + +## Plan-then-apply for state changes + +- Any state-changing operation MUST present a plan/diff and require human confirmation before execution. State-changing means: anything that, if undone, requires effort. +- Examples that REQUIRE plan-then-apply: + - `terraform apply` — show `terraform plan` output first + - `kubectl apply` — show `kubectl diff -f ` (or `kubectl apply --dry-run=server -f `) first + - `helm install` / `helm upgrade` — show `helm diff upgrade` (via the `helm-diff` plugin) or `--dry-run` output first + - `gh ruleset edit` / branch protection updates — show before/after + - Secret rotation — show the rotation steps and rollback before executing + - Release tag creation, release publication + - `gh pr merge` (any merge method) + - Any `gcloud … set-iam-policy` / `aws … put-policy` / equivalent + - Repository setting changes (`gh repo edit`) +- **Production-impacting ops always require human-in-the-loop**, even if the user has previously approved similar ops. There is no "trusted auto-apply" mode. +- **Confirmation is per-operation, not per-session.** A blanket "yes, do everything" is not accepted. + +## Idempotency + +- **Skills must be idempotent.** Re-running with identical inputs produces the same effect, or reports "already applied" without making changes. +- **Generated artifacts (NOTICE files, changelog entries, generated docs) sort deterministically** so re-runs produce byte-identical output and clean diffs (the `notice-generate` skill is the reference). +- **Network-dependent skills** cache responses where reasonable (`/tmp/-.json`) and document staleness. + +## Validation before success + +- Skills MUST validate their output before reporting "done." + - File mutations → re-read, parse, lint + - Git operations → `git status` shows expected state + - PR operations → `gh pr view` confirms state + - Workflow file edits → `actionlint` clean +- **No "done" without evidence.** The user should never have to ask "did it work?" +- **Print the verification step** in the chat output, not just "✅ done". + +## Fail-stop on ambiguity + +- **Stop when state is unexpected.** Examples that trigger fail-stop: + - Uncommitted local changes when the skill expects a clean tree + - Branch is not what the skill expected + - A required env var, secret, or file is missing + - Two conflicting cursor rules apply + - A required tool is not on PATH + - PR data fetched but the SHA does not match the expected one +- **Failure mode is "stop and ask," never "guess and proceed."** +- **Partial success is reported as partial success.** Don't summarize "everything worked" when one of three steps failed. + +## Provenance and traceability + +- **Agent-authored commits/PRs are labeled.** PR body includes a footer indicating which agent and which skill produced the work, so reviewers know what to audit harder. +- **MCP tool descriptors are read before invocation.** Never call an MCP tool blind. (Reinforced in the system prompt; rule-level for emphasis.) +- **Sensitive operations are logged.** Skills that touch secrets, deploy, or modify protected branches append a redacted summary to a session log under `/tmp/agent-session-.log`. The log path is mentioned in the chat output. +- **Skills declare which secrets they read** in their `SKILL.md` frontmatter description or "Prerequisites" section. No silent secret access. + +## Bounded resource use + +- **Bound shell-call count per skill.** State an explicit budget in the skill's "Efficiency rules" section (the `pr-review` skill caps at ~5–8 calls; treat that as the ceiling unless justified). +- **Cache fetched data once per session** (`/tmp/-.json`). Never re-fetch the same PR/run/file in one session. +- **Use dedicated tools, not shell.** `Read` instead of `cat`/`head`/`tail`, `Grep` instead of `grep`/`rg`, `Glob` instead of `find`, `Write`/`StrReplace` instead of `echo >` / heredoc. +- **Prefer scripts in `_lib//`** over inline shell pipelines. Scripts are testable, deterministic, and share-able across skills. + +## Skill authoring conventions + +- **One skill = one capability.** Don't compose unrelated workflows in a single skill. +- **Naming**: kebab-case, prefix-grouped (`devops-*`, `sdk-*`, `addon-*`, `pr-*`). +- **`SKILL.md` body ≤ 500 lines.** Push detail to `references/*.md` and link one level deep. +- **Slash-command discoverability**: include `/` in the description. +- **Auto-invoke vs explicit**: `disable-model-invocation: true` for any skill that mutates state, posts publicly, or runs slow / expensive tooling. Auto-invoke only for read-only, fast, contextually obvious helpers. +- **Description includes WHAT and WHEN** in third person. +- **Co-locate shared logic** under `.cursor/skills/_lib//` when two or more skills want the same primitive. + +## Hooks + +- **Hooks run on every applicable agent event.** Keep them fast (<2s) and side-effect-free unless the side effect is the whole point. +- **Hooks must not silently modify the user's working tree** (see read-only default). +- **Hooks that block the agent** must say so loudly and explain how to unblock. + +## Secrets and external systems + +- **Secrets handling defers to `.cursor/rules/devops/secrets-and-credentials.mdc`** — every rule there applies. +- **External writes (Slack, Asana, Linear, GitHub) require explicit user confirmation** for the specific message/issue/PR being created. +- **Drafts over publishes**: where the platform supports drafts (PRs, releases, Slack scheduled), prefer creating a draft and surfacing the link to the user. + +## When the rails block a useful flow + +- If a rail is blocking a flow that's clearly safe and clearly useful, the answer is to relax the rail in this rule (with a PR), not to override it case-by-case in a skill. Code that quietly bypasses these rules is a bug. diff --git a/.cursor/rules/devops/commit-and-pr-format.mdc b/.cursor/rules/devops/commit-and-pr-format.mdc new file mode 100644 index 0000000000..94af63843a --- /dev/null +++ b/.cursor/rules/devops/commit-and-pr-format.mdc @@ -0,0 +1,118 @@ +--- +description: Commit message and PR title format for DevOps surfaces — same shape as SDK pod, scoped to .github/** and scripts/** so the rule auto-loads on devops paths +globs: + - .github/workflows/** + - .github/actions/** + - .github/scripts/** + - .github/PULL_REQUEST_TEMPLATE/** + - .github/CODEOWNERS + - .github/dependabot.yml + - scripts/** +alwaysApply: false +--- + +# Commit and PR Format (DevOps) + +Same shape as `.cursor/rules/sdk/commit-and-pr-format.mdc`. Replicated here with devops-flavored examples and the devops PR template so the rule auto-loads on `.github/**` and `scripts/**` paths. + +## Commit messages + +``` +prefix[tags]?: subject +``` + +**Allowed prefixes:** + +- `infra` — CI/CD, tooling, automation, runners, repo configuration +- `feat` — new automation surface (a new skill, a new workflow type, a new action) +- `fix` — bug fixes +- `chore` — maintenance, dependency bumps, deprecations +- `doc` — documentation changes (rules, runbooks, READMEs) +- `test` — test additions/modifications + +**Allowed tags (optional, cannot be combined):** + +- `[bc]` — breaking changes (workflow output contract change, status check rename, exposed action interface change) +- `[notask]` — (PRs only) allows omitting the ticket +- `[skiplog]` — skip changelog generation for the PR + +(`[api]` and `[mod]` are SDK-pod / addon tags and do not apply to devops surfaces. They are intentionally absent from the allowed list above.) + +**Examples:** + +- `infra: pin actions/checkout to v4.1.1 SHA` +- `infra: add concurrency block to release-sdk workflow` +- `fix: correct workflow_dispatch caller event for release guard` +- `chore: bump trufflehog action to v3.95.0` +- `doc: add devops pod conventions rule` +- `infra[bc]: rename ci-lint job to lint (breaks branch protection)` + +## PR titles + +``` +TICKET prefix[tags]?: subject +``` + +TICKET follows the pattern `PROJECT-123` (e.g., `QVAC-18394`). + +Use `[notask]` to omit the ticket: `prefix[notask]: subject`. + +**Examples:** + +- `QVAC-18394 infra: add devops team file and pod conventions rule` +- `QVAC-12345 fix: handle approval-worker fork pull_request_review` +- `chore[notask]: bump dependabot grouping for npm-major` + +## PR body template + +Use `.github/PULL_REQUEST_TEMPLATE/devops.md`. Sections (top-level only, no nesting): + +- 🎯 What problem does this PR solve? +- 📝 How does it solve it? +- 🧪 How was it tested? +- 🔐 Action pinning +- 🛡️ Permissions changes +- 📋 Plan / dry-run output +- 💥 Breaking changes + +Delete any section that doesn't apply — a focused PR body beats a half-filled template. + +**Required content by trigger:** + +| Trigger | Required section | +|---|---| +| `[bc]` tag | "Breaking changes" with BEFORE/AFTER YAML blocks | +| Bumping any third-party action | "Action pinning" with before/after SHA + version | +| Modifying `permissions:` block | "Permissions changes" with scope, before/after, justification | +| State-changing op (IaC apply, deploy, ruleset edit, branch protection update) | "Plan / dry-run output" | + +Out-of-cycle operations like secret rotation and hardened-runner audits are not tracked in the PR template. Document them in the relevant runbook (e.g., `docs/runbooks/secrets-rotation.md`) and reference the runbook entry from the PR description if it's the reason for the change. + +## Validation + +- **No PR title validation workflow exists for devops paths today.** `pr-validation-sdk-pod.yml` is paths-scoped to SDK packages only. A `pr-validation-devops.yml` (or expanded reusable validator) is a follow-up — model it on the SDK validator and the existing `scripts/sdk/validator.cjs`, scoped to `.github/**` and `scripts/**`. +- **No commit-msg hook applies to devops paths** today (husky hooks live under `packages//.husky/` and are scoped to those packages). +- Until the workflow lands, **skills that create devops PRs** (e.g., a future `devops-pr-create`) MUST validate inputs against this format client-side and reject malformed titles before pushing. + +## Quality guidelines + +Same as SDK's rule, applied to devops surfaces: + +- **"What problem"**: describe user / operator impact, not implementation. The diff shows what. +- **"How"**: high-level approach, not line-by-line. +- **"Tested"**: actual scenarios. For DevOps: + - `actionlint` clean + - ran the workflow on a test branch / via `workflow_dispatch` + - `terraform plan` applied successfully on staging + - dry-run output attached +- **Delete unused sections.** A nearly-empty template is worse than a focused one. +- **No vague claims.** "Improved performance" is not a description. + +## Important notes + +- **NEVER** suggest commit messages that don't follow this format. +- **ALWAYS** include the colon (`:`) after the prefix and tags. +- **ALWAYS** use lowercase prefix and lowercase subject (sentence case). +- Tags must be in square brackets and cannot be combined. +- PR titles MUST start with a ticket number unless `[notask]` is used. +- When creating PRs that touch workflows, actions, or pinned deps, ALWAYS remind the user about the required Security & Compliance subsections. diff --git a/.cursor/rules/devops/github-actions.mdc b/.cursor/rules/devops/github-actions.mdc new file mode 100644 index 0000000000..c7f2853c51 --- /dev/null +++ b/.cursor/rules/devops/github-actions.mdc @@ -0,0 +1,134 @@ +--- +description: GitHub Actions conventions — pinned actions, least-privilege permissions, OIDC, hardened runners, caching, concurrency, environments, reusable workflows +globs: + - .github/workflows/** + - .github/actions/** +alwaysApply: false +--- + +# GitHub Actions + +Pod scope and high-level principles live in `.cursor/rules/devops/main.mdc`. This file is the deep-dive for workflow and composite-action authoring. + +## Action references + +- **Third-party actions MUST be pinned to a 40-character commit SHA** with a trailing `# v` comment. Tags are silently mutable. + - ✅ `uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 6.0.2` (this repo's current pin) + - ❌ `uses: actions/checkout@v4` +- **Pin transitively.** If a composite action under `.github/actions//` uses a third-party action, that nested reference is also pinned to a SHA. +- **First-party actions** in this repo (`.github/actions//`) may be referenced by relative path or by SHA. Prefer relative path for in-repo usage; SHA when the action is consumed by a workflow that may run on a different ref. +- **Action upgrades land in their own PR**, never bundled with feature changes. The PR body links the action's release notes and includes the SHA diff. + +## 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. +- **Never use `permissions: write-all`.** If you think you need it, you don't. + +## Triggers and untrusted input + +- **`pull_request_target` is dangerous.** Use it only when the workflow does not check out PR HEAD code. Common safe uses: labeling, commenting, gating on metadata. +- **Never reference user-controlled `${{ github.event.* }}`, `github.head_ref`, `github.event.pull_request.title`, `body`, or `commits[*].message` directly inside `run:` blocks.** Pipe via `env:` and quote. + - ✅ + ```yaml + env: + PR_TITLE: ${{ github.event.pull_request.title }} + run: | + echo "$PR_TITLE" + ``` + - ❌ `run: echo "${{ github.event.pull_request.title }}"` +- **Token scope on fork PRs**: `pull_request` from forks gets a read-only `GITHUB_TOKEN` and no secrets. Do not work around this — design fork-safe workflows or split into a `pull_request_target` follow-up that does not check out PR code. + +## OIDC over long-lived credentials + +- **Cloud authentication uses OIDC.** No long-lived access keys / service-account JSON in secrets where OIDC is supported. + - GCP: `google-github-actions/auth` with `workload_identity_provider` + - AWS: `aws-actions/configure-aws-credentials` with `role-to-assume` + - Azure: `azure/login` with `client-id` + `tenant-id` + `subscription-id` +- **OIDC role/policy must be scoped per-workflow or per-environment**, never repo-wide read-write. +- **Document the trust relationship** in a comment near the auth step (which IdP, which role, which environment). + +## Hardened runners + +- **Sensitive workflows run `step-security/harden-runner`** as the first step, in `audit` mode initially and `block` mode once egress is characterized. + - Examples of sensitive: any workflow handling secrets, publishing artifacts, deploying to prod, or with `id-token: write`. +- **Document allowed egress** in the `harden-runner` config; do not silently expand it. + +## Caching + +- `actions/cache` keys are deterministic and prefixed: + ```yaml + key: ${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('**/lockfile') }} + restore-keys: | + ${{ runner.os }}-${{ matrix.target }}- + ``` +- **Never write to a shared cache from PR-triggered jobs running on fork code.** Cache poisoning is a real attack. Restrict cache writes to `push` / `workflow_dispatch` / `merge_group` triggers. +- Caches scoped to a branch by default; that's fine for build artifacts. Cross-branch caches require justification. + +## Concurrency + +- **Every workflow declares a `concurrency` block** unless concurrent runs are explicitly required. + ```yaml + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + ``` +- **Release workflows do NOT cancel in progress.** Use `cancel-in-progress: false` on `release-*.yml` and any workflow that mutates external state. + +## Matrix builds + +- `fail-fast: true` is the default. Set `fail-fast: false` only for diagnostic / cross-platform matrices where seeing all failures is more useful than stopping early. +- Matrix dimension names are concrete and stable: `os`, `arch`, `node-version`, `target` — not `config`, `variant`, `flavor`. +- `include` and `exclude` are documented in a comment when used; they obscure the matrix. + +## Environments + +- **Production deployments target a GitHub Environment** with required reviewers configured in the GitHub UI, not in YAML. +- **Environment names are stable**: `production`, `staging`, `preview-`. Do not invent per-PR environment names that won't be reused. +- **Environment secrets** are scoped to the environment, not the repo. Promote from preview → staging → production. + +## Reusable workflows and composite actions + +- **Reusable workflow** (`workflow_call`): for multi-step orchestration with its own triggers, jobs, and concurrency. +- **Composite action** (`.github/actions//action.yml`): for a sequence of steps that runs inside a job. One responsibility per action. +- **Extract when duplicated** across two or more workflows. +- **Reusable workflow inputs and outputs are documented** in the workflow header. No magic strings. + +## Status checks + +- **Job names are stable.** Branch protection rulesets bind to job names; renaming a job breaks protection silently. +- **Names are descriptive and lowercase-hyphenated** (e.g., `lint`, `unit-tests`, `cpp-tests`). Choose once and treat as part of the public contract. +- **Required checks are declared in the GitHub ruleset**, not in YAML. The ruleset is the source of truth. +- **Renaming a required check requires a coordinated change**: update the ruleset and the workflow in the same change window, or add the new name as required while leaving the old one until merges drain. + +## Outputs and step IDs + +- **Use `$GITHUB_OUTPUT`**, never `::set-output::` (deprecated). + ```bash + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + ``` +- **Every step that produces an output has an `id:`.** Reference as `${{ steps..outputs. }}`. + +## Failure handling + +- **`continue-on-error: true` is forbidden on Tier-1 checks** (lint, format, type-check, security scans, tests). Allowed on diagnostic-only steps. +- **`if: always()`** on cleanup steps so artifacts and logs are uploaded even on failure. +- **Timeouts**: every job declares `timeout-minutes`. Default budget is 30 minutes; longer requires a comment explaining why. + +## File layout and naming + +- Workflow filenames describe the trigger and target. Existing repo conventions: + - `on-pr-.yml` — triggered by PR events on a package surface + - `on-merge-.yml` — triggered when a PR merges to main + - `on-pr-close-.yml` — triggered when a PR closes + - `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 + - `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/rules/devops/main.mdc b/.cursor/rules/devops/main.mdc new file mode 100644 index 0000000000..f7d80c6d55 --- /dev/null +++ b/.cursor/rules/devops/main.mdc @@ -0,0 +1,95 @@ +--- +description: DevOps pod conventions — security-first, AI-first, validate-before-merge. Index pointing to topic-specific rules under .cursor/rules/devops/ +globs: + - .github/workflows/** + - .github/actions/** + - .github/scripts/** + - scripts/** +alwaysApply: false +--- + +# DevOps Pod + +## Scope + +Owns CI/CD pipelines (GitHub Actions workflows + composite actions), repo-wide automation scripts under `.github/scripts/` and `scripts/`, and any infra-as-code (Terraform, Ansible, Docker, K8s) that lands in this repo. Per-package scripts under `packages//scripts/` are owned by the corresponding package's pod, not DevOps. + +## Operating Principles + +- **AI-first**: Prefer Cursor / agentic paths — MCPs, skills, hooks, rules — over manual operations. Fall back to manual only when the AI path is infeasible or unsafe, and document why. +- **Security-first**: Treat every change as security- and compliance-sensitive. Never log, echo, or interpolate secrets, tokens, or credentials. Default to least-privilege scope for tokens, runners, and roles. +- **Validate before commit, validate before PR**: Run linters, syntax checks, and dry-runs locally. For state-changing operations and IaC, surface the plan/diff in the PR body before any apply is suggested. +- **Speed with rigor**: Move fast; never trade correctness or security for speed. + +## Related rules (read when relevant) + +| Rule | Scope | When it applies | +|---|---|---| +| `.cursor/rules/devops/github-actions.mdc` | `.github/workflows/**`, `.github/actions/**` | Authoring or modifying any workflow or composite action | +| `.cursor/rules/devops/secrets-and-credentials.mdc` | `.github/**`, `scripts/**` | Anything that can read, derive, or transmit a secret | +| `.cursor/rules/devops/agentic-automation.mdc` | `.cursor/skills/**`, `.cursor/rules/**`, `.cursor/hooks/**`, `.github/workflows/**`, `.github/scripts/**`, `scripts/**` | Authoring skills, hooks, MCP integrations, or any AI-driven DevOps workflow | +| `.cursor/rules/devops/commit-and-pr-format.mdc` | `.github/**`, `scripts/**` | Commit message and PR title format on devops surfaces (auto-loads where `sdk/commit-and-pr-format.mdc` does not) | + +## Quick rules — surface-level reminders + +These are the bar; the deep-dive lives in the topic files above. + +### GitHub Actions +- Pin third-party actions to a 40-character commit SHA with `# v` annotation; never `@v3`. +- Top-level `permissions:` is mandatory and defaults to `contents: read`. +- `pull_request_target` never checks out untrusted PR HEAD ("pwn request" pattern). +- Use OIDC for cloud auth; long-lived credentials are the fallback, not the default. + +### Secrets +- Never inline a secret value. Always reference via `${{ secrets.X }}`. +- Never `echo`, `cat`, `printenv`, or `set -x` near a secret. +- A leaked secret is rotated FIRST, scrubbed second. History rewriting alone does not protect. +- Personal credentials are forbidden in CI; use machine accounts, OIDC, or GitHub Apps. + +### Scripts +- Bash: `set -euo pipefail` at the top; quote every variable expansion. +- Node: target Node 20+, prefer ESM (`.mjs`). +- Idempotent: re-runs must not corrupt state. +- Never `curl ... | bash`. + +### Runners +- Self-hosted runners must not execute jobs from forked PRs by default. +- Runner labels must match actual hardware/OS requirements. + +### Validation expectations +- `actionlint` clean for `.github/workflows/**` and `.github/actions/**` before merge. +- YAML formatted with `yamlfmt` (existing repo convention; see `.github/actions/yamlfmt/`). +- Terraform: `fmt -check` + `plan` summary in the PR body. +- Ansible: `--syntax-check` + `--check --diff` summary in the PR body. +- Docker: `hadolint` clean; pinned base-image digests (`@sha256:...`), never floating tags. + +### Agentic automation +- Read-only by default. Mutations require explicit user opt-in per call. +- Plan-then-apply for any state-changing operation. +- Validation before "success." No "done" without evidence. +- Fail-stop on ambiguity — never guess. + +## Useful commands + +- `gh workflow list` — list workflows +- `gh workflow view ` — inspect a workflow +- `gh run list --workflow=` — recent runs +- `gh run view --log-failed --job ` — fetch failing job logs +- `gh api repos/tetherto/qvac/actions/permissions` — repo-wide GHA settings +- `actionlint .github/workflows/*.yml` — lint workflows locally +- `gitleaks detect --source . --no-banner` — secret scan the working tree + +## Commit messages and PR titles + +See `.cursor/rules/devops/commit-and-pr-format.mdc` for the canonical devops format. Same shape as the SDK pod's: + +- Commits: `prefix[tags]?: subject` +- PRs: `TICKET prefix[tags]?: subject` + +The dominant prefix for DevOps changes is `infra` (CI/CD, tooling, automation). Use `chore` for maintenance, `fix` for bug fixes, `feat` for new automation surfaces. + +Examples: + +- `infra: pin actions/checkout to v4.1.1 SHA` +- `QVAC-18394 infra: add devops team file and pod conventions rule` +- `fix[notask]: correct workflow_dispatch caller event for release guard` diff --git a/.cursor/rules/devops/secrets-and-credentials.mdc b/.cursor/rules/devops/secrets-and-credentials.mdc new file mode 100644 index 0000000000..7c38477239 --- /dev/null +++ b/.cursor/rules/devops/secrets-and-credentials.mdc @@ -0,0 +1,113 @@ +--- +description: Secrets and credentials handling — storage, access, masking, OIDC over PATs, fork-PR isolation, leak response playbook +globs: + - .github/workflows/** + - .github/actions/** + - .github/scripts/** + - scripts/** +alwaysApply: false +--- + +# Secrets and Credentials + +This rule applies whenever a workflow, action, or script can read, derive, or transmit a secret value. + +## Storage — sources of truth + +Secrets live in exactly one of these locations, ordered by preference: + +1. **Cloud IdP via OIDC** — no secret stored at all (preferred, see GitHub Actions rule). +2. **GitHub Environment secrets** — scoped to a deployment environment with required reviewers (`production`, `staging`). +3. **GitHub Repository secrets** — scoped to this repo only. +4. **GitHub Organization secrets** — shared across multiple repos. Justified for shared registries / cloud roles only. + +Secrets MUST NOT live in: + +- Workflow YAML files (inline string values) +- Repository `.env*` files committed to git +- Code comments, doc files, or markdown +- Issues, PR descriptions, commit messages, branch names +- Build artifacts uploaded via `actions/upload-artifact` +- Caches (`actions/cache`) +- Logs (workflow run logs, container logs, application logs) +- Slack / Asana / Linear comments +- Screenshots or recordings shared in tickets + +## Access in workflows + +- **Reference syntax**: `${{ secrets. }}`. Never reconstruct a secret from parts. +- **Never interpolate `${{ secrets.X }}` directly into a `run:` shell command.** The expansion lands in the rendered shell script verbatim and can leak via `set -x`, error traces, and reflected echoes. Pass through `env:` instead: + ```yaml + env: + GH_TOKEN: ${{ secrets.GH_TOKEN }} + run: | + gh api ... + ``` +- **Action `with:` inputs are the appropriate channel** when an action explicitly accepts credentials (e.g., `aws-actions/configure-aws-credentials`'s `aws-access-key-id`). Pass `${{ secrets.X }}` to `with:` is fine; passing it to `run:` is not. +- **Mask derived values**: if a step computes a value from a secret (token exchange, signed URL), mask it before it leaves the step: + ```bash + echo "::add-mask::$DERIVED" + ``` +- **No `set -x` or `bash -x`** in steps that touch secrets. No `printenv` / `env` dump. +- **No `cat`, `echo`, or `printf`** of a secret value, even for "debugging". Use `wc -c` or `sha256sum` if proving non-emptiness. + +## OIDC over long-lived credentials + +- **Cloud authentication uses OIDC** wherever the provider supports it. PAT/SSH-key/service-account-JSON storage is the fallback, not the default. +- **Fallback credentials must be rotated quarterly.** GitHub Actions secrets do not have a description field — track rotation metadata (last-rotated date, owner, runbook link) in a dedicated runbook (e.g., `docs/runbooks/secrets-rotation.md`) keyed by secret name. +- **Personal credentials are forbidden in CI.** Use machine accounts, deploy tokens, GitHub Apps, or OIDC. +- **GitHub App tokens** are preferred over PATs for cross-repo automation. Generate per-job via `actions/create-github-app-token` (or equivalent), scoped to the minimum repos and permissions. + +## Fork-PR isolation + +- **`pull_request` from a fork**: read-only `GITHUB_TOKEN`, no secrets, no environment access. This is enforced by GitHub and must not be circumvented. +- **`pull_request_target` from a fork**: full repo context with secrets. **MUST NOT check out PR HEAD.** This is the "pwn request" pattern. +- **Conditional secret pass-through to forks is forbidden.** A PR from a fork either gets no secrets (default) or the workflow is reviewed line-by-line by a lead. +- **Self-hosted runners**: configured to refuse fork-PR jobs by default at the runner level. + +## Egress hardening + +- **Workflows that handle secrets run `step-security/harden-runner`** as the first step (see GitHub Actions rule). Start in `audit` mode, promote to `block` once egress endpoints are characterized. +- **Allowed egress is documented inline.** No silent expansion. + +## Local development + +- `.env` files at the repo root are git-ignored. Use them locally; never commit. +- `direnv` (`.envrc`) is acceptable when added to `.gitignore`. +- `gh auth login` and `gcloud auth login` use device-code or OIDC where possible. +- Personal credentials stay in the OS keychain (macOS Keychain, Linux Secret Service, Windows Credential Manager). Never in plaintext config. +- Pre-commit secret scanning: `gitleaks detect --source . --no-banner` before pushing significant changes. + +## Detection in CI + +- **Required workflow**: a secret-scanning job runs on every PR. Failing matches block the merge. +- **Allowlists are explicit and documented.** Any test fixture that intentionally contains a secret-like string is annotated with a `# gitleaks:allow` (or equivalent) comment and cited in the PR. + +## Leak response playbook + +When a real secret is detected to have leaked (in git history, in a log, in any external surface): + +1. **Rotate immediately.** Generate a new secret in the source-of-truth system, update GitHub Actions secret, redeploy any consumers. Do this BEFORE anything else. +2. **Revoke the old secret** in the source system. +3. **Audit usage.** Pull access logs from the source system covering from "first time the secret could have been exposed" to now. Look for unexpected callers. +4. **Scrub the leaked surface.** Force-push history rewrite (`git filter-repo`), purge build artifacts, clear caches, delete leaked log lines where possible. +5. **Open an incident ticket** in Asana with a written timeline. +6. **Postmortem** within 5 working days. Include: what leaked, blast radius, root cause, what would have prevented it. +7. **Update tooling** to prevent recurrence (rule, scanner config, hook, ruleset). + +History rewriting alone does NOT protect a secret that was ever pushed. Step 1 is non-optional. + +## Allowed-vs-forbidden quick reference + +| Action | Allowed? | +|---|---| +| `${{ secrets.X }}` referenced in `env:` block | ✅ | +| `${{ secrets.X }}` interpolated into `run:` shell command | ❌ — pipe via `env:` instead | +| `echo $TOKEN` in a step | ❌ | +| `wc -c <<< "$TOKEN"` to assert non-empty | ✅ | +| Storing a secret in a workflow `env:` literal value | ❌ | +| Storing a secret in `actions/cache` | ❌ | +| Uploading a file containing a secret as an artifact | ❌ | +| Pasting a secret into an Asana / Slack message for "quick debugging" | ❌ | +| Rotating a quarterly token without updating the runbook date | ❌ | +| Adding a fixture token to test data with a `# gitleaks:allow` annotation | ✅ | diff --git a/.github/PULL_REQUEST_TEMPLATE/devops.md b/.github/PULL_REQUEST_TEMPLATE/devops.md new file mode 100644 index 0000000000..99db54b047 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/devops.md @@ -0,0 +1,62 @@ +**Note**: be concise and prefer bullet points. + +## 🎯 What problem does this PR solve? + +- + +- + +## 📝 How does it solve it? + +- + +- + +## 🧪 How was it tested? + +**Delete this section if not applicable.** + +- + +- + +## 🔐 Action pinning + +**Required when third-party actions are added, bumped, or repinned. Delete this section if not applicable.** + +- `actions/checkout`: ` # v6.0.1` → ` # v6.0.2` +- + +## 🛡️ Permissions changes + +**Required when a top-level or per-job `permissions:` block is added, modified, or removed. Delete this section if not applicable.** + +- Scope: top-level / job `` +- Before: `` +- After: `` +- Justification: + +## 📋 Plan / dry-run output + +**Required for state-changing operations: IaC apply, ruleset edits, deploys, branch protection updates. Delete this section if not applicable.** + +``` + +``` + +## 💥 Breaking changes + +**Required for PRs with `[bc]` tag. Delete this section if not applicable.** + +**BEFORE:** + +```yaml +# old workflow / output / contract +``` + +**AFTER:** + +```yaml +# new workflow / output / contract +``` + diff --git a/.github/teams/devops.json b/.github/teams/devops.json new file mode 100644 index 0000000000..dc0f324bfa --- /dev/null +++ b/.github/teams/devops.json @@ -0,0 +1,13 @@ +{ + "name": "DevOps Pod", + "leads": ["Proletter"], + "members": [ + "Proletter" + ], + "ownedPaths": [ + ".github/workflows/", + ".github/actions/", + ".github/scripts/", + "scripts/" + ] +} From 50accd76a3f704b015a516b04e62362570386606 Mon Sep 17 00:00:00 2001 From: Oluwaseun Ismaila Date: Thu, 7 May 2026 09:46:06 +0100 Subject: [PATCH 2/4] QVAC-18394 chore: expand devops pod roster with 5 team members MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the rest of the active DevOps engineers to .github/teams/devops.json so /devops-pr-status correctly partitions reviewers between "Reviews:" (team) and "Other:" (outside) buckets. Without this, every team-member review currently lands in "Other:" and the dashboard reports approvals as still-needed. Members (alphabetical, case-insensitive): - darkynt (Matt Cavanagh) - GiacomoSorbiWork (Giacomo) - sidj-thr - tamer-hassan-tether - yauhenipankratovich-web Removes Proletter from members per the cross-pod convention (lead is listed in `leads` only — see .github/teams/sdk.json). Validation: - JSON parses; pr-status.mjs --pod devops --mode team loads the new roster without error. - No code/path changes, data-only update. Co-authored-by: Cursor --- .github/teams/devops.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/teams/devops.json b/.github/teams/devops.json index dc0f324bfa..a4e8779cd2 100644 --- a/.github/teams/devops.json +++ b/.github/teams/devops.json @@ -2,7 +2,11 @@ "name": "DevOps Pod", "leads": ["Proletter"], "members": [ - "Proletter" + "darkynt", + "GiacomoSorbiWork", + "sidj-thr", + "tamer-hassan-tether", + "yauhenipankratovich-web" ], "ownedPaths": [ ".github/workflows/", From 1ea53acca632391e3fb28fb50e6938673c2cd578 Mon Sep 17 00:00:00 2001 From: Oluwaseun Ismaila Date: Thu, 7 May 2026 13:32:06 +0100 Subject: [PATCH 3/4] QVAC-18394 chore: drop commit-and-pr-format rule (skill-only) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per review feedback: rules auto-attach via globs and pollute the context window on every devops surface. The format spec is already encoded in devops-pr-create (regex validation, allowed prefixes/tags, trigger detection) and devops-pr-review (title validation against the same regex) — both invoked explicitly, never autoloaded. - Delete .cursor/rules/devops/commit-and-pr-format.mdc (5 KB). - main.mdc: drop the rule from the related-rules table and replace the "Commit messages and PR titles" section with a one-line pointer to the devops-pr-create skill. Skill-side cross-references to the deleted rule are cleaned up on PR #1929 (next in the stack) since that's where the skills live. Co-authored-by: Cursor --- .cursor/rules/devops/commit-and-pr-format.mdc | 118 ------------------ .cursor/rules/devops/main.mdc | 16 +-- 2 files changed, 3 insertions(+), 131 deletions(-) delete mode 100644 .cursor/rules/devops/commit-and-pr-format.mdc diff --git a/.cursor/rules/devops/commit-and-pr-format.mdc b/.cursor/rules/devops/commit-and-pr-format.mdc deleted file mode 100644 index 94af63843a..0000000000 --- a/.cursor/rules/devops/commit-and-pr-format.mdc +++ /dev/null @@ -1,118 +0,0 @@ ---- -description: Commit message and PR title format for DevOps surfaces — same shape as SDK pod, scoped to .github/** and scripts/** so the rule auto-loads on devops paths -globs: - - .github/workflows/** - - .github/actions/** - - .github/scripts/** - - .github/PULL_REQUEST_TEMPLATE/** - - .github/CODEOWNERS - - .github/dependabot.yml - - scripts/** -alwaysApply: false ---- - -# Commit and PR Format (DevOps) - -Same shape as `.cursor/rules/sdk/commit-and-pr-format.mdc`. Replicated here with devops-flavored examples and the devops PR template so the rule auto-loads on `.github/**` and `scripts/**` paths. - -## Commit messages - -``` -prefix[tags]?: subject -``` - -**Allowed prefixes:** - -- `infra` — CI/CD, tooling, automation, runners, repo configuration -- `feat` — new automation surface (a new skill, a new workflow type, a new action) -- `fix` — bug fixes -- `chore` — maintenance, dependency bumps, deprecations -- `doc` — documentation changes (rules, runbooks, READMEs) -- `test` — test additions/modifications - -**Allowed tags (optional, cannot be combined):** - -- `[bc]` — breaking changes (workflow output contract change, status check rename, exposed action interface change) -- `[notask]` — (PRs only) allows omitting the ticket -- `[skiplog]` — skip changelog generation for the PR - -(`[api]` and `[mod]` are SDK-pod / addon tags and do not apply to devops surfaces. They are intentionally absent from the allowed list above.) - -**Examples:** - -- `infra: pin actions/checkout to v4.1.1 SHA` -- `infra: add concurrency block to release-sdk workflow` -- `fix: correct workflow_dispatch caller event for release guard` -- `chore: bump trufflehog action to v3.95.0` -- `doc: add devops pod conventions rule` -- `infra[bc]: rename ci-lint job to lint (breaks branch protection)` - -## PR titles - -``` -TICKET prefix[tags]?: subject -``` - -TICKET follows the pattern `PROJECT-123` (e.g., `QVAC-18394`). - -Use `[notask]` to omit the ticket: `prefix[notask]: subject`. - -**Examples:** - -- `QVAC-18394 infra: add devops team file and pod conventions rule` -- `QVAC-12345 fix: handle approval-worker fork pull_request_review` -- `chore[notask]: bump dependabot grouping for npm-major` - -## PR body template - -Use `.github/PULL_REQUEST_TEMPLATE/devops.md`. Sections (top-level only, no nesting): - -- 🎯 What problem does this PR solve? -- 📝 How does it solve it? -- 🧪 How was it tested? -- 🔐 Action pinning -- 🛡️ Permissions changes -- 📋 Plan / dry-run output -- 💥 Breaking changes - -Delete any section that doesn't apply — a focused PR body beats a half-filled template. - -**Required content by trigger:** - -| Trigger | Required section | -|---|---| -| `[bc]` tag | "Breaking changes" with BEFORE/AFTER YAML blocks | -| Bumping any third-party action | "Action pinning" with before/after SHA + version | -| Modifying `permissions:` block | "Permissions changes" with scope, before/after, justification | -| State-changing op (IaC apply, deploy, ruleset edit, branch protection update) | "Plan / dry-run output" | - -Out-of-cycle operations like secret rotation and hardened-runner audits are not tracked in the PR template. Document them in the relevant runbook (e.g., `docs/runbooks/secrets-rotation.md`) and reference the runbook entry from the PR description if it's the reason for the change. - -## Validation - -- **No PR title validation workflow exists for devops paths today.** `pr-validation-sdk-pod.yml` is paths-scoped to SDK packages only. A `pr-validation-devops.yml` (or expanded reusable validator) is a follow-up — model it on the SDK validator and the existing `scripts/sdk/validator.cjs`, scoped to `.github/**` and `scripts/**`. -- **No commit-msg hook applies to devops paths** today (husky hooks live under `packages//.husky/` and are scoped to those packages). -- Until the workflow lands, **skills that create devops PRs** (e.g., a future `devops-pr-create`) MUST validate inputs against this format client-side and reject malformed titles before pushing. - -## Quality guidelines - -Same as SDK's rule, applied to devops surfaces: - -- **"What problem"**: describe user / operator impact, not implementation. The diff shows what. -- **"How"**: high-level approach, not line-by-line. -- **"Tested"**: actual scenarios. For DevOps: - - `actionlint` clean - - ran the workflow on a test branch / via `workflow_dispatch` - - `terraform plan` applied successfully on staging - - dry-run output attached -- **Delete unused sections.** A nearly-empty template is worse than a focused one. -- **No vague claims.** "Improved performance" is not a description. - -## Important notes - -- **NEVER** suggest commit messages that don't follow this format. -- **ALWAYS** include the colon (`:`) after the prefix and tags. -- **ALWAYS** use lowercase prefix and lowercase subject (sentence case). -- Tags must be in square brackets and cannot be combined. -- PR titles MUST start with a ticket number unless `[notask]` is used. -- When creating PRs that touch workflows, actions, or pinned deps, ALWAYS remind the user about the required Security & Compliance subsections. diff --git a/.cursor/rules/devops/main.mdc b/.cursor/rules/devops/main.mdc index f7d80c6d55..33b7ef8991 100644 --- a/.cursor/rules/devops/main.mdc +++ b/.cursor/rules/devops/main.mdc @@ -28,7 +28,8 @@ Owns CI/CD pipelines (GitHub Actions workflows + composite actions), repo-wide a | `.cursor/rules/devops/github-actions.mdc` | `.github/workflows/**`, `.github/actions/**` | Authoring or modifying any workflow or composite action | | `.cursor/rules/devops/secrets-and-credentials.mdc` | `.github/**`, `scripts/**` | Anything that can read, derive, or transmit a secret | | `.cursor/rules/devops/agentic-automation.mdc` | `.cursor/skills/**`, `.cursor/rules/**`, `.cursor/hooks/**`, `.github/workflows/**`, `.github/scripts/**`, `scripts/**` | Authoring skills, hooks, MCP integrations, or any AI-driven DevOps workflow | -| `.cursor/rules/devops/commit-and-pr-format.mdc` | `.github/**`, `scripts/**` | Commit message and PR title format on devops surfaces (auto-loads where `sdk/commit-and-pr-format.mdc` does not) | + +Commit message and PR title format guidance lives in the [`devops-pr-create`](../../skills/devops-pr-create/SKILL.md) skill — invoke it explicitly rather than autoloading a rule for it (skill-only on purpose, to keep the format spec out of the always-on context window). ## Quick rules — surface-level reminders @@ -81,15 +82,4 @@ These are the bar; the deep-dive lives in the topic files above. ## Commit messages and PR titles -See `.cursor/rules/devops/commit-and-pr-format.mdc` for the canonical devops format. Same shape as the SDK pod's: - -- Commits: `prefix[tags]?: subject` -- PRs: `TICKET prefix[tags]?: subject` - -The dominant prefix for DevOps changes is `infra` (CI/CD, tooling, automation). Use `chore` for maintenance, `fix` for bug fixes, `feat` for new automation surfaces. - -Examples: - -- `infra: pin actions/checkout to v4.1.1 SHA` -- `QVAC-18394 infra: add devops team file and pod conventions rule` -- `fix[notask]: correct workflow_dispatch caller event for release guard` +The full format spec, allowed prefixes, allowed tags, validation regex, and quality guidelines live in the [`devops-pr-create`](../../skills/devops-pr-create/SKILL.md) skill. Invoke `/devops-pr-create` rather than relying on this rule — kept out of the auto-load context window on purpose. From 5a020bde4b32fbcf7ca14f97d1f84d0f21213d07 Mon Sep 17 00:00:00 2001 From: Proletter <40578159+Proletter@users.noreply.github.com> Date: Mon, 11 May 2026 10:05:35 +0100 Subject: [PATCH 4/4] QVAC-18394 feat: add devops pod skills (pr-status, pr-create, daily-update, pr-review) (#1929) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * QVAC-18394 feat: add devops pod skills (pr-status, pr-create, daily-update, pr-review) Resolves the four QVAC-18394 subtasks by adding the DevOps pod's user-facing Cursor skills on top of the conventions and team file landed in the prereq branch. The skills lean on the existing _lib/pr-skills/ shared library for pod discovery, PR enumeration, Slack-handle mapping, and worktree management, so no new shared infra is added — only thin SKILL.md surfaces and DevOps-specific workflows. Files: - .cursor/skills/devops-pr-status/SKILL.md — Stale-Prs subtask. Thin wrapper invoking pr-status.mjs --pod devops --mode team. The shared script already segregates PRs into needs-your-re-review / stale (>3d) / needs-review and flags merge conflicts; no separate stale-only mode is needed. - .cursor/skills/devops-pr-create/SKILL.md — Create-pr subtask. Generates TICKET prefix[tag]?: subject titles + devops.md PR body, with trigger detection (action-pinning / permissions / IaC plan / [bc]) driving which template sections are required. Client-side title validation since no pr-validation-devops.yml exists yet. - .cursor/skills/devops-daily-update/SKILL.md — Daily-update subtask. Aggregates yesterday's merged PRs, today's open PRs, reviews owed, and recent CI runs into a Slack/Asana-ready message. Bounded to <=6 shell calls. Read-only; never posts. Includes a secret-pattern scrub before writing the temp file. - .cursor/skills/devops-pr-review/SKILL.md — Pr-review subtask, absorbs gha-audit. Wraps /pr-review (does NOT fork it) and layers a deterministic GitHub Actions security audit (15 checks A1-A15) sourced verbatim from .cursor/rules/devops/github-actions.mdc and secrets-and-credentials.mdc. Findings flow into the same pending-review payload the user confirms. All four skills: - disable-model-invocation: true (state-changing or PR-posting flows) - Reference rules and team file landed by the prereq PR - Inherit safety + efficiency rules from .cursor/rules/devops/agentic-automation.mdc (read-only by default, plan-then-apply for state changes, bounded shell calls) Validated: - All four SKILL.md frontmatter parses (name matches directory; non-trivial description) - All 12 cross-file references resolve (rules, team file, PR template, shared lib, parent skills) - gh search prs / gh run list flags + JSON fields verified against gh CLI 2.x help output - ReadLints clean - No formatter mangling Co-authored-by: Cursor * QVAC-18394 fix: align devops-daily-update output to team's slack template The first draft used a generic Markdown layout (`## Yesterday`, `## Today`, `## Blockers`, `_(none)_` for empty sections, GitHub-flavored links). The team's actual daily-update format on Slack is different: 🔨 *Done today* - QVAC-XXXXX: - 📅 *Planned for tomorrow* - QVAC-XXXXX: - QVAC-YYYYY 🚧 *Blockers / risks* - N/A Changes: - Replaced the section names and added the canonical 🔨 / 📅 / 🚧 emoji - Switched from Markdown headings to Slack-bold (`*Section*`) so the output renders correctly when pasted into Slack (Slack does not render `##`) - Empty sections now render `- N/A` (literal), not `_(none)_` - Bullets lead with `TICKET:` (auto-linked by the workspace's Asana app), not `#` — falls back to `#` only when no ticket can be extracted from PR title or branch name - Sub-bullets at 4-space indent for ticket-level context - Default `--format` is now `slack` (not `markdown`) — Slack is the primary destination; chat preview keeps the Markdown form - Temp file extension changed `.md` → `.txt` to reflect Slack mrkdwn (not GitHub-flavored Markdown) as the canonical form - Added ticket-extraction rules (PR title → branch name → `#`) - Added a per-section routing table (merged-today / pushed-today / open-no-recent-commits / reviews-owed / conflicting / stale-review / CI-failing) so the agent knows which bucket each item lands in Lookback default unchanged at "yesterday 00:00 local" — covers both an EOD post late evening and a morning standup at 7am without manual `--since`. Quality gates updated to enforce the new layout (correct emoji + section names; `- N/A` for empty; no Markdown headings in Slack form; no GitHub- style links). The skill is still read-only and never posts. The user copies from the temp file and pastes into Slack manually. Co-authored-by: Cursor * QVAC-18394 chore: align devops skills to sdk-pod conventions Self-audit pass against `.cursor/rules/sdk/skill-authoring-guidelines.mdc` and the SDK pod's reference skills (sdk-pr-status, sdk-pr-create, sdk-changelog, sdk-backmerge). Documentation-only. Description tightening: - devops-pr-status: 341 → 275 chars - devops-pr-create: 269 → 231 chars - devops-daily-update: 398 → 255 chars - devops-pr-review: 386 → 271 chars Reference: sdk-pr-status's description is 256 chars. All four are now in the same 230–280 range, vs the prior 270–400 range. WHAT/WHEN preserved on each. Heading consistency: - "## Quality gates" → "## Quality Checklist" in devops-daily-update, devops-pr-review (sdk-changelog / sdk-backmerge / sdk-pr-create all use "Quality Checklist") - "## Validation gate (CLIENT-SIDE)" → "## Validation" in devops-pr-create (no SDK skill uses uppercase parenthetical scope qualifiers in headings) Editorial cleanup: - devops-pr-status: dropped the "Resolves the Stale-Prs subtask of QVAC-18394 …" paragraph (skill bodies should not reference their own PR/ticket; SDK skills never do) - devops-daily-update: dropped the upfront "## Canonical template" section (~25 lines). Step 8's "#### Slack form (canonical)" is the single source of truth for the format. Folded the one unique line — bare-ticket bullets allowed when self-evident — into Step 8. Reduced devops-daily-update from 269 → 242 lines. Other line-counts stable (46, 183, 140). No behaviour changes. Cross-file references still resolve. Frontmatter parses; name matches dir; disable-model-invocation: true preserved on all four. ReadLints clean. Co-authored-by: Cursor * QVAC-18394 fix: devops skill issues found during test pass - github-actions.mdc § Permissions: accept top-level OR per-job permissions blocks as equivalent (per-job is the more secure narrower-scope pattern). - github-actions.mdc § File layout: add integration--.yml to the canonical filename list (existing repo convention). - devops-pr-review SKILL.md: tighten A2 + A15 check descriptions to mirror the loosened rule (audit becomes more permissive — no consumers break). - devops-daily-update SKILL.md: trim merged-PRs gh-search --json field set to what the API actually exposes (closedAt, not mergedAt/ additions/deletions); add cap of 5 most-recently-updated reviews to the standup output with overflow line. - devops-pr-create SKILL.md + devops.md PR template: drop the redundant "be concise" Note line from the template head. All issues uncovered by the end-to-end test session of the four new devops skills on this branch. Co-authored-by: Cursor * QVAC-18394 fix: emit paste-ready output files in devops pr-status + pr-create - devops-pr-status: tee dashboard stdout to /tmp/devops-pr-status-.txt and redirect stderr to a sibling .stderr file. Print pbcopy/xclip/wl-copy commands so the operator can paste the dashboard straight into a Slack thread (Slack auto-renders the indented plain text as nested bullets and turns # into PR auto-links). - devops-pr-create: add an explicit step 8 to write the assembled PR body to /tmp/pr-body.md (the gh CLI Integration section already cat's that path). Add the pbcopy/xclip/wl-copy commands as step 9 for direct paste into the GitHub PR-create form. Discovered during the test pass — the dashboard output was useful but the operator had to manually copy from the terminal. Now there's a single pbcopy command to grab paste-ready content. Co-authored-by: Cursor * QVAC-18394 chore: drop commit-and-pr-format rule cross-references in skills Mirror the rule deletion on PR #1926 — remove dead links from devops-pr-create and devops-pr-review SKILL.md, and inline the title regex / allowed prefixes / allowed tags so the skills stay self-contained without auto-loading anything via globs. - devops-pr-create: Format References now points at the inline Validation regex; the "see rule" parenthetical in Validation is replaced with a one-line note that no pr-validation-devops.yml exists yet; the References bullet for the deleted rule is removed. - devops-pr-review: drop commit-and-pr-format from the auto-load list in step 4 (it's deleted, no longer auto-loads); inline the format spec in step 5 (regex + prefixes + tags); replace the rule bullet in References with a pointer to devops-pr-create as the canonical home for the format spec. No behavior changes — same regex, same prefix/tag list, same validation logic. Co-authored-by: Cursor --------- Co-authored-by: Cursor --- .cursor/rules/devops/github-actions.mdc | 17 +- .cursor/skills/devops-daily-update/SKILL.md | 242 ++++++++++++++++++++ .cursor/skills/devops-pr-create/SKILL.md | 188 +++++++++++++++ .cursor/skills/devops-pr-review/SKILL.md | 140 +++++++++++ .cursor/skills/devops-pr-status/SKILL.md | 57 +++++ .github/PULL_REQUEST_TEMPLATE/devops.md | 2 - 6 files changed, 637 insertions(+), 9 deletions(-) create mode 100644 .cursor/skills/devops-daily-update/SKILL.md create mode 100644 .cursor/skills/devops-pr-create/SKILL.md create mode 100644 .cursor/skills/devops-pr-review/SKILL.md create mode 100644 .cursor/skills/devops-pr-status/SKILL.md 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? -