diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 4633d281..4db234c1 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -1,6 +1,6 @@ name: Bug Report description: Report a bug with apiops CLI -labels: ["bug"] +labels: ["type:bug"] body: - type: input id: command diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml index a994e57a..42a2a4f5 100644 --- a/.github/ISSUE_TEMPLATE/question.yml +++ b/.github/ISSUE_TEMPLATE/question.yml @@ -1,6 +1,6 @@ name: Question description: Ask a usage question or request clarification -labels: ["question"] +labels: ["type:question"] body: - type: textarea id: question diff --git a/.github/workflows/issue-labels-sync.yml b/.github/workflows/issue-labels-sync.yml index e7a7778b..335e493c 100644 --- a/.github/workflows/issue-labels-sync.yml +++ b/.github/workflows/issue-labels-sync.yml @@ -7,6 +7,8 @@ on: paths: - '.squad/team.md' - '.ai-team/team.md' + - '.github/ISSUE_TEMPLATE/**' + - '.github/workflows/issue-labels-sync.yml' workflow_dispatch: permissions: @@ -19,6 +21,19 @@ jobs: steps: - uses: actions/checkout@v6 + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '24' + cache: 'npm' + + # Install production dependencies so the github-script step can `require('js-yaml')`. + # github-script resolves bare module IDs from the workspace root (process.cwd()). + # --ignore-scripts blocks dependency lifecycle scripts (supply-chain hardening); + # our runtime deps are pure JS and need no install scripts. + - name: Install dependencies + run: npm ci --omit=dev --ignore-scripts + - name: Parse roster and sync labels uses: actions/github-script@v8 with: @@ -29,12 +44,12 @@ jobs: teamFile = '.ai-team/team.md'; } - if (!fs.existsSync(teamFile)) { - core.info('No .squad/team.md or .ai-team/team.md found — skipping label sync'); - return; + const rosterExists = fs.existsSync(teamFile); + if (!rosterExists) { + core.info('No .squad/team.md or .ai-team/team.md found — skipping squad member labels (static labels still sync and issue templates are still validated)'); } - const content = fs.readFileSync(teamFile, 'utf8'); + const content = rosterExists ? fs.readFileSync(teamFile, 'utf8') : ''; const lines = content.split('\n'); // Parse the Members table for agent names @@ -195,3 +210,61 @@ jobs: } core.info(`Label sync complete: ${labels.length} labels synced`); + + // Fail if any issue template references a label this workflow does not define. + const yaml = require('js-yaml'); + + const definedLabelNames = new Set(labels.map(l => l.name.toLowerCase())); + const templateDir = '.github/ISSUE_TEMPLATE'; + const violations = []; + let templateCount = 0; + + if (fs.existsSync(templateDir)) { + const templateFiles = fs.readdirSync(templateDir).filter(f => { + const lower = f.toLowerCase(); + return /\.ya?ml$/.test(lower) && lower !== 'config.yml' && lower !== 'config.yaml'; + }); + templateCount = templateFiles.length; + for (const file of templateFiles) { + const templateContent = fs.readFileSync(`${templateDir}/${file}`, 'utf8'); + + let parsed; + try { + parsed = yaml.load(templateContent); + } catch (err) { + violations.push({ file, error: `could not be parsed as YAML: ${err.message}` }); + continue; + } + + // GitHub issue forms accept `labels` as a YAML sequence or a + // comma-delimited string. + const rawLabels = parsed && parsed.labels; + let templateLabels = []; + if (Array.isArray(rawLabels)) { + templateLabels = rawLabels.map(l => String(l).trim()).filter(Boolean); + } else if (typeof rawLabels === 'string') { + templateLabels = rawLabels.split(',').map(l => l.trim()).filter(Boolean); + } + + for (const label of templateLabels) { + if (!definedLabelNames.has(label.toLowerCase())) { + violations.push({ file, label }); + } + } + } + } + + if (violations.length > 0) { + const details = violations + .map(v => v.error + ? ` - ${v.file}: ${v.error}` + : ` - ${v.file}: "${v.label}" is not a defined label`) + .join('\n'); + core.setFailed( + `Issue templates reference labels that are not defined by this workflow:\n${details}\n\n` + + `Fix by either adding the label to this workflow's definitions or updating the ` + + `template to use an existing label.` + ); + } else { + core.info(`Validated ${templateCount} issue template(s): all referenced labels are defined`); + } diff --git a/.squad/agents/githubexpert/charter.md b/.squad/agents/githubexpert/charter.md index 40333b91..a6eb381b 100644 --- a/.squad/agents/githubexpert/charter.md +++ b/.squad/agents/githubexpert/charter.md @@ -24,6 +24,8 @@ - **OIDC for Azure, no secrets.** Use `azure/login` action with OIDC — no client secrets to rotate. - **Reusable workflows for DRY.** If two repos do the same thing, extract to a reusable workflow. - **Environments for deployment gates.** Production deployments require environment protection rules. +- **Node.js 24 for all workflows (STANDING RULE).** Every GitHub Actions workflow this project authors must pin Node.js 24 — use `actions/setup-node` with `node-version: '24'`, and prefer Node 24 anywhere a workflow needs a Node runtime. See the Node.js Runtime Standard under Project-Specific Patterns. +- **Always `gh aw compile` agentic workflows (STANDING RULE).** After creating or editing ANY `.md` agentic workflow, always run `gh aw compile` to regenerate the `.lock.yml`, then commit BOTH files. An uncompiled `.md` edit does not change what runs. See GitHub Agentic Workflows (gh-aw) under Project-Specific Patterns. - I use `gh api` for anything not covered by dedicated `gh` commands — raw API access is essential. - I configure `gh auth login` with the right scopes upfront to avoid permission errors later. @@ -31,6 +33,48 @@ These patterns are specific to the apiops-cli project. +#### Node.js Runtime Standard (Standing Rule) + +- **Pin Node.js 24 in every workflow I author.** Any GitHub Actions workflow that needs a Node runtime must use `actions/setup-node` with `node-version: '24'`. Prefer Node 24 anywhere a workflow references a Node version (setup-node, container images, tool matrices). +- **Why it's compatible:** The repo's `package.json` `engines` requires Node `>=22`, so Node 24 satisfies the constraint. +- **Applies to future authoring.** New and regenerated workflows follow this by default; no need to re-open already-updated files unless touching them for other reasons. + +```yaml +- name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '24' +``` + +#### GitHub Agentic Workflows (gh-aw) + +GitHub Agentic Workflows (gh-aw) are authored as natural-language **markdown with YAML frontmatter** at `.github/workflows/.md`, and are **compiled** into runnable GitHub Actions YAML at `.github/workflows/.lock.yml`. The `.lock.yml` is what actually runs in Actions. + +- **⚠️ STANDING RULE — always compile.** After creating or editing ANY `.md` agentic workflow, **always run `gh aw compile`** to (re)generate the `.lock.yml`, then commit **BOTH** the `.md` and the `.lock.yml`. An uncompiled `.md` edit has no effect on what runs. +- **`.gitattributes` must contain:** `.github/workflows/*.lock.yml linguist-generated=true merge=ours` + +**Setup (one-time):** + +```bash +# Install the extension +curl -sL https://raw.githubusercontent.com/github/gh-aw/main/install-gh-aw.sh | bash +gh aw version # verify install +gh extension upgrade aw # upgrade later +``` + +**Key commands:** + +```bash +gh aw new # scaffold a new workflow +gh aw compile [workflow-name] # compile all workflows, or one by name (regenerates .lock.yml) +gh aw compile --validate # compile with validation +gh aw logs [workflow-name] # inspect run logs +gh aw audit # debug a specific run +gh aw fix --write # auto-fix/upgrade deprecated fields +``` + +**Full docs:** https://github.github.com/gh-aw/ · **Repo:** https://github.com/github/gh-aw + #### Repository Workflow Files | File | Purpose | |------|---------|