diff --git a/.github/workflows/bugbot-gate.yml b/.github/workflows/bugbot-gate.yml index 3e19099..a30ff48 100644 --- a/.github/workflows/bugbot-gate.yml +++ b/.github/workflows/bugbot-gate.yml @@ -6,6 +6,9 @@ on: sha: required: true type: string + pull_number: + required: true + type: number bugbot_check_name: required: false type: string @@ -14,6 +17,14 @@ on: required: false type: number default: 15 + qualifying_reviewer_login_regex: + required: false + type: string + default: "" + qualifying_comment_body_regexes: + required: false + type: string + default: "" permissions: contents: read @@ -27,70 +38,209 @@ jobs: timeout-minutes: ${{ inputs.timeout_minutes + 2 }} steps: - - name: Wait for Bugbot check + - name: Wait for Bugbot and verify no unresolved threads uses: actions/github-script@v7 env: BUGBOT_GATE_SHA: ${{ inputs.sha }} + BUGBOT_GATE_PULL_NUMBER: ${{ inputs.pull_number }} BUGBOT_GATE_CHECK_NAME: ${{ inputs.bugbot_check_name }} BUGBOT_GATE_TIMEOUT_MINUTES: ${{ inputs.timeout_minutes }} + BUGBOT_GATE_REVIEWER_REGEX: ${{ inputs.qualifying_reviewer_login_regex }} + BUGBOT_GATE_BODY_REGEXES: ${{ inputs.qualifying_comment_body_regexes }} with: script: | const owner = context.repo.owner; const repo = context.repo.repo; const sha = process.env.BUGBOT_GATE_SHA; + const pullNumber = Number(process.env.BUGBOT_GATE_PULL_NUMBER); const target = process.env.BUGBOT_GATE_CHECK_NAME; const timeoutMinutes = Number(process.env.BUGBOT_GATE_TIMEOUT_MINUTES); + const reviewerRegexRaw = process.env.BUGBOT_GATE_REVIEWER_REGEX || ''; + const bodyRegexesRaw = process.env.BUGBOT_GATE_BODY_REGEXES || ''; - if (!sha) { - core.setFailed("Missing required input: sha"); - return; - } - - if (!target) { - core.setFailed("Missing required input: bugbot_check_name"); - return; - } - - if (!Number.isFinite(timeoutMinutes) || timeoutMinutes <= 0) { - core.setFailed("Invalid input: timeout_minutes must be a positive number"); - return; - } + if (!sha) { core.setFailed('Missing required input: sha'); return; } + if (!Number.isFinite(pullNumber) || pullNumber <= 0) { core.setFailed('Missing or invalid input: pull_number'); return; } + if (!target) { core.setFailed('Missing required input: bugbot_check_name'); return; } + if (!Number.isFinite(timeoutMinutes) || timeoutMinutes <= 0) { core.setFailed('Invalid input: timeout_minutes must be a positive number'); return; } const timeoutMs = timeoutMinutes * 60 * 1000; const start = Date.now(); + // ── Step 1: Wait for the Bugbot check run to complete ───────────────── + + core.info(`Waiting for '${target}' check run on ${sha}…`); + while (true) { const { data } = await github.rest.checks.listForRef({ - owner, - repo, - ref: sha, - per_page: 100 + owner, repo, ref: sha, per_page: 100, }); - const matching = data.check_runs.filter(run => run.name === target); + const matching = data.check_runs.filter(r => r.name === target); const latest = matching.sort((a, b) => b.id - a.id)[0]; if (latest) { - core.info( - `Latest ${target} status is ${latest.status} (conclusion: ${latest.conclusion ?? "n/a"})` - ); + core.info(`${target}: status=${latest.status} conclusion=${latest.conclusion ?? 'n/a'}`); } - if (latest && latest.status === "completed") { - core.info(`Found completed ${target} with conclusion: ${latest.conclusion}`); - - if (latest.conclusion !== "success") { - core.setFailed(`${target} conclusion was ${latest.conclusion}`); - } - - return; + if (latest?.status === 'completed') { + core.info(`${target} completed with conclusion: ${latest.conclusion}`); + // Give Bugbot a moment to finish posting review comments. + await new Promise(r => setTimeout(r, 5000)); + break; } if (Date.now() - start > timeoutMs) { - core.setFailed(`Timed out waiting for ${target}`); + core.setFailed(`Timed out after ${timeoutMinutes}m waiting for '${target}' on ${sha}`); return; } - core.info(`Still waiting for ${target} on ${sha}...`); - await new Promise(resolve => setTimeout(resolve, 15000)); + await new Promise(r => setTimeout(r, 15000)); + core.info(`Still waiting for '${target}'…`); + } + + // ── Step 2: Guard against a concurrent push changing the PR head ────── + + const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: pullNumber }); + if (pr.head.sha !== sha) { + core.setFailed(`Race: PR head changed from ${sha} to ${pr.head.sha} during evaluation`); + return; + } + + // ── Step 3: Determine the cycle boundary (timestamp of the latest push) ─ + + const timelineEvents = await github.paginate( + github.rest.issues.listEventsForTimeline, + { owner, repo, issue_number: pullNumber, per_page: 100 }, + ); + + const parseMs = (event, type) => { + // For 'committed' events prefer created_at (GitHub push timestamp) over + // author/committer dates, which are user-controllable and can be + // arbitrarily old (cherry-picks, offline work, pre-created commits). + // Using an old author date as the cycle boundary would cause prior-cycle + // Bugbot comments to satisfy `ts > cycleBoundaryMs` and block the gate. + const candidates = type === 'committed' + ? [event?.created_at, event?.committer?.date, event?.author?.date] + : [event?.created_at]; + for (const raw of candidates) { + const ms = Date.parse(String(raw || '')); + if (!Number.isNaN(ms)) return ms; + } + return Number.NaN; + }; + + const latestPushMs = timelineEvents.reduce((best, event) => { + const type = String(event?.event || ''); + if (type === 'head_ref_force_pushed') { + const afterSha = String(event?.after_commit_id || ''); + if (afterSha && afterSha !== sha) return best; + const ms = parseMs(event, type); + return Number.isNaN(ms) ? best : Math.max(best, ms); + } + if (type === 'committed') { + if (event?.sha !== sha) return best; + const ms = parseMs(event, type); + return Number.isNaN(ms) ? best : Math.max(best, ms); + } + return best; + }, Number.NEGATIVE_INFINITY); + + const fallbackMs = Date.parse(String(pr.created_at || '')); + const cycleBoundaryMs = Number.isFinite(latestPushMs) ? latestPushMs : fallbackMs; + if (Number.isNaN(cycleBoundaryMs)) { + core.setFailed(`Unable to determine cycle boundary for PR #${pullNumber}`); + return; + } + core.info(`Cycle boundary (latest push): ${new Date(cycleBoundaryMs).toISOString()}`); + + // ── Step 4: Build comment qualification matchers ─────────────────────── + + const splitTerms = raw => + String(raw || '').split(/\r?\n|,|;|\|\|/).map(s => s.trim()).filter(Boolean); + + const safeRegex = pattern => { + try { return new RegExp(pattern, 'i'); } + catch (e) { core.setFailed(`Invalid regex '${pattern}': ${e.message}`); return null; } + }; + + const reviewerRegex = reviewerRegexRaw.trim() ? safeRegex(reviewerRegexRaw.trim()) : null; + const bodyRegexes = splitTerms(bodyRegexesRaw).map(safeRegex).filter(Boolean); + + const defaultBodyMatchers = [ + //i, + //i, + //i, + //i, + /https:\/\/cursor\.com\/fix-in-cursor/i, + ]; + + const isQualifying = comment => { + const login = String(comment?.author?.login || '').toLowerCase(); + const body = String(comment?.body || ''); + const reviewerMatch = reviewerRegex + ? reviewerRegex.test(login) + : (login === 'cursor' || login.includes('cursor')); + const bodyMatch = bodyRegexes.length > 0 + ? bodyRegexes.some(r => r.test(body)) + : defaultBodyMatchers.some(r => r.test(body)); + return reviewerMatch && bodyMatch; + }; + + // ── Step 5: Fetch review threads and evaluate ───────────────────────── + + const query = ` + query($owner: String!, $repo: String!, $pullNumber: Int!, $after: String) { + repository(owner: $owner, name: $repo) { + pullRequest(number: $pullNumber) { + reviewThreads(first: 100, after: $after) { + pageInfo { hasNextPage endCursor } + nodes { + isResolved + comments(first: 50) { + nodes { body path line createdAt author { login } } + } + } + } + } + } + } + `; + + let allThreads = []; + let after = null; + while (true) { + const data = await github.graphql(query, { owner, repo, pullNumber, after }); + const rt = data.repository.pullRequest.reviewThreads; + allThreads = allThreads.concat(rt.nodes ?? []); + if (!rt.pageInfo.hasNextPage) break; + after = rt.pageInfo.endCursor; + } + + const qualifying = allThreads.filter(t => + (t?.comments?.nodes ?? []).some(c => isQualifying(c)) + ); + + const currentCycle = qualifying.filter(t => + (t?.comments?.nodes ?? []).some(c => { + if (!isQualifying(c)) return false; + const ts = Date.parse(String(c?.createdAt || '')); + return !Number.isNaN(ts) && ts > cycleBoundaryMs; + }) + ); + + const unresolved = currentCycle.filter(t => !t.isResolved); + + core.info(`Qualifying threads — all cycles: ${qualifying.length}, current cycle: ${currentCycle.length}, unresolved: ${unresolved.length}`); + + if (currentCycle.length === 0) { + core.info('No qualifying Bugbot threads in the current cycle — gate passes.'); + return; + } + + if (unresolved.length > 0) { + core.setFailed( + `Found ${unresolved.length} unresolved Bugbot thread(s) in the current cycle. Resolve them before merging.` + ); + } else { + core.info(`All ${currentCycle.length} Bugbot thread(s) in the current cycle are resolved — gate passes.`); } diff --git a/.github/workflows/node-pnpm-build.yml b/.github/workflows/node-pnpm-build.yml new file mode 100644 index 0000000..273816d --- /dev/null +++ b/.github/workflows/node-pnpm-build.yml @@ -0,0 +1,68 @@ +name: Node pnpm Build + +# Reusable workflow: install and build. +# Intended to be called from a CI After Gate workflow triggered by workflow_run. +# +# The job name "Build" is stable — branch protection rulesets reference it. + +on: + workflow_call: + inputs: + ref: + description: Commit SHA or ref to check out. + required: true + type: string + node_version: + required: false + type: string + default: "lts/*" + pnpm_version: + description: pnpm version to install. Leave empty to read from packageManager in package.json. + required: false + type: string + default: "" + install_command: + required: false + type: string + default: "pnpm install --frozen-lockfile" + build_command: + required: false + type: string + default: "pnpm build" + working_directory: + description: Working directory for all commands. + required: false + type: string + default: "." + +permissions: + contents: read + +jobs: + build: + name: Build + runs-on: ubuntu-latest + timeout-minutes: 15 + defaults: + run: + working-directory: ${{ inputs.working_directory }} + + steps: + - uses: actions/checkout@v5 + with: + ref: ${{ inputs.ref }} + + - uses: pnpm/action-setup@v4 + with: + version: ${{ inputs.pnpm_version }} + + - uses: actions/setup-node@v5 + with: + node-version: ${{ inputs.node_version }} + cache: pnpm + + - name: Install dependencies + run: ${{ inputs.install_command }} + + - name: Build + run: ${{ inputs.build_command }} diff --git a/.github/workflows/node-pnpm-playwright.yml b/.github/workflows/node-pnpm-playwright.yml new file mode 100644 index 0000000..8b977ff --- /dev/null +++ b/.github/workflows/node-pnpm-playwright.yml @@ -0,0 +1,82 @@ +name: Node pnpm Playwright + +# Reusable workflow: install, build, run Playwright tests. +# Intended to be called from a CI After Gate workflow triggered by workflow_run. +# +# The job name "Playwright" is stable — branch protection rulesets reference it. + +on: + workflow_call: + inputs: + ref: + description: Commit SHA or ref to check out. + required: true + type: string + node_version: + required: false + type: string + default: "lts/*" + pnpm_version: + description: pnpm version to install. Leave empty to read from packageManager in package.json. + required: false + type: string + default: "" + install_command: + required: false + type: string + default: "pnpm install --frozen-lockfile" + install_browsers_command: + required: false + type: string + default: "pnpm exec playwright install --with-deps chromium" + build_command: + required: false + type: string + default: "pnpm build" + test_command: + required: false + type: string + default: "pnpm exec playwright test" + working_directory: + description: Working directory for all commands. + required: false + type: string + default: "." + +permissions: + contents: read + +jobs: + playwright: + name: Playwright + runs-on: ubuntu-latest + timeout-minutes: 25 + defaults: + run: + working-directory: ${{ inputs.working_directory }} + + steps: + - uses: actions/checkout@v5 + with: + ref: ${{ inputs.ref }} + + - uses: pnpm/action-setup@v4 + with: + version: ${{ inputs.pnpm_version }} + + - uses: actions/setup-node@v5 + with: + node-version: ${{ inputs.node_version }} + cache: pnpm + + - name: Install dependencies + run: ${{ inputs.install_command }} + + - name: Install Playwright browsers + run: ${{ inputs.install_browsers_command }} + + - name: Build + run: ${{ inputs.build_command }} + + - name: Run Playwright tests + run: ${{ inputs.test_command }} diff --git a/.github/workflows/node-pnpm-quality.yml b/.github/workflows/node-pnpm-quality.yml new file mode 100644 index 0000000..db7345b --- /dev/null +++ b/.github/workflows/node-pnpm-quality.yml @@ -0,0 +1,84 @@ +name: Node pnpm Quality + +# Reusable workflow: lint, typecheck, optional format check. +# Intended to be called from a CI After Gate workflow triggered by workflow_run. +# +# The job name "Quality" is stable — branch protection rulesets reference it. + +on: + workflow_call: + inputs: + ref: + description: Commit SHA or ref to check out. + required: true + type: string + node_version: + required: false + type: string + default: "lts/*" + pnpm_version: + description: pnpm version to install. Leave empty to read from packageManager in package.json. + required: false + type: string + default: "" + install_command: + required: false + type: string + default: "pnpm install --frozen-lockfile" + lint_command: + required: false + type: string + default: "pnpm lint" + typecheck_command: + required: false + type: string + default: "pnpm typecheck" + format_command: + description: Format check command. Leave empty to skip. + required: false + type: string + default: "" + working_directory: + description: Working directory for all commands. + required: false + type: string + default: "." + +permissions: + contents: read + +jobs: + quality: + name: Quality + runs-on: ubuntu-latest + timeout-minutes: 15 + defaults: + run: + working-directory: ${{ inputs.working_directory }} + + steps: + - uses: actions/checkout@v5 + with: + ref: ${{ inputs.ref }} + + - uses: pnpm/action-setup@v4 + with: + version: ${{ inputs.pnpm_version }} + + - uses: actions/setup-node@v5 + with: + node-version: ${{ inputs.node_version }} + cache: pnpm + + - name: Install dependencies + run: ${{ inputs.install_command }} + + - name: Lint + run: ${{ inputs.lint_command }} + + - name: Typecheck + run: ${{ inputs.typecheck_command }} + + - name: Format check + if: ${{ inputs.format_command != '' }} + run: ${{ inputs.format_command }} diff --git a/.github/workflows/pr-metadata-check.yml b/.github/workflows/pr-metadata-check.yml new file mode 100644 index 0000000..cdf6b96 --- /dev/null +++ b/.github/workflows/pr-metadata-check.yml @@ -0,0 +1,202 @@ +name: PR Metadata Check + +# Reusable workflow that enforces PR description quality. +# +# What it checks (blocking unless noted): +# - PR links a tracked issue (Fixes/Closes #N) or a work doc (opt-in) +# - ContractImpact field matches whether contract files changed (opt-in) +# - Bot PRs are skipped automatically +# +# Non-blocking warnings: +# - Template sections that appear unfilled +# - Code changes without changelog update (when code_change_regex is set) +# - Missing or empty Release: field + +on: + workflow_call: + inputs: + require_issue_ref: + description: Require Fixes/Closes #N in the PR description. + required: false + type: boolean + default: true + allow_work_doc: + description: "Also accept WorkDoc: docs/work/.md as an alternative to an issue ref." + required: false + type: boolean + default: false + work_doc_path_prefix: + description: Path prefix for WorkDoc links (used when allow_work_doc is true). + required: false + type: string + default: "docs/work/" + require_contract_impact: + description: Require ContractImpact:none|updated and validate it against changed files. + required: false + type: boolean + default: false + contract_dir: + description: Directory path for contract files (used when require_contract_impact is true). + required: false + type: string + default: "docs/contracts" + changelog_path: + description: Path to the changelog file (used for the non-blocking changelog warning). + required: false + type: string + default: "CHANGELOG.md" + code_change_regex: + description: Regex pattern to identify code files. When set, warns if code changed without changelog update. + required: false + type: string + default: "" + skip_bot_prs: + description: Skip all checks for PRs from bot users or bot/ branches. + required: false + type: boolean + default: true + +permissions: + contents: read + pull-requests: read + issues: read + +jobs: + check: + name: PR Metadata Check + runs-on: ubuntu-latest + timeout-minutes: 5 + + steps: + - name: Check PR metadata + uses: actions/github-script@v7 + env: + REQUIRE_ISSUE_REF: ${{ inputs.require_issue_ref }} + ALLOW_WORK_DOC: ${{ inputs.allow_work_doc }} + WORK_DOC_PATH_PREFIX: ${{ inputs.work_doc_path_prefix }} + REQUIRE_CONTRACT_IMPACT: ${{ inputs.require_contract_impact }} + CONTRACT_DIR: ${{ inputs.contract_dir }} + CHANGELOG_PATH: ${{ inputs.changelog_path }} + CODE_CHANGE_REGEX: ${{ inputs.code_change_regex }} + SKIP_BOT_PRS: ${{ inputs.skip_bot_prs }} + with: + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + const prNumber = context.payload.pull_request.number; + + const requireIssueRef = process.env.REQUIRE_ISSUE_REF !== 'false'; + const allowWorkDoc = process.env.ALLOW_WORK_DOC === 'true'; + const workDocPrefix = process.env.WORK_DOC_PATH_PREFIX || 'docs/work/'; + const requireContractImpact = process.env.REQUIRE_CONTRACT_IMPACT === 'true'; + const contractDir = process.env.CONTRACT_DIR || 'docs/contracts'; + const changelogPath = process.env.CHANGELOG_PATH || 'CHANGELOG.md'; + const codeChangeRegex = process.env.CODE_CHANGE_REGEX || ''; + const skipBotPrs = process.env.SKIP_BOT_PRS !== 'false'; + + const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber }); + + if (skipBotPrs) { + const isBotUser = pr.user?.type === 'Bot'; + const isBotBranch = (pr.head?.ref || '').startsWith('bot/'); + if (isBotUser || isBotBranch) { + core.info('Skipping metadata checks for bot PR.'); + return; + } + } + + const prBody = pr.body || ''; + const errors = []; + + function warn(msg) { + core.warning(msg); + } + + function escapeRegExp(s) { + return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + } + + function extractSection(body, heading) { + const re = new RegExp(`^##\\s+${escapeRegExp(heading)}\\s*$([\\s\\S]*?)(?=^##\\s+|(?![\\s\\S]))`, 'gmi'); + const m = re.exec(body); + return m ? (m[1] || '') : ''; + } + + function sectionLooksUnfilled(text) { + const lines = (text || '').split(/\r?\n/).map(l => l.trim()); + return lines.filter(l => l && !['- [ ]', '- [x]', '-', '- '].includes(l) && /[A-Za-z0-9]/.test(l)).length === 0; + } + + // Fetch changed files for ContractImpact and changelog checks. + const files = await github.paginate(github.rest.pulls.listFiles, { + owner, repo, pull_number: prNumber, per_page: 100, + }); + const filenames = files.map(f => f.filename); + + // ── 1. Issue / work doc linkage (blocking) ──────────────────────────── + if (requireIssueRef || allowWorkDoc) { + const hasFixesCloses = /(?:Fixes|Closes)\s+#\d+/i.test(prBody); + const workDocPattern = new RegExp( + `WorkDoc:\\s*(?:\`|\\[)?${escapeRegExp(workDocPrefix)}[^\\s\\]\`]+\\.md`, 'i' + ); + const hasWorkDoc = allowWorkDoc && workDocPattern.test(prBody); + + if (requireIssueRef && !hasFixesCloses && !hasWorkDoc) { + const msg = allowWorkDoc + ? 'PR description must include "Fixes #" / "Closes #" or "WorkDoc: ' + workDocPrefix + '.md"' + : 'PR description must include "Fixes #" or "Closes #"'; + errors.push(msg); + } + + // Warn about bare issue references without closing keywords. + const bodyForScan = prBody.replace(/```[\s\S]*?```/g, '').replace(/`[^`]*`/g, '').replace(/https?:\/\/\S+/gi, ''); + if (/(^|[\s(])#\d+\b/m.test(bodyForScan) && !hasFixesCloses) { + warn('PR body references issue(s) without closing keywords. Use "Fixes #" or "Closes #" to auto-close on merge.'); + } + } + + // ── 2. ContractImpact (blocking when enabled) ───────────────────────── + if (requireContractImpact) { + const match = prBody.match(/ContractImpact:\s*(none|updated)/i); + if (!match) { + errors.push('PR description must include "ContractImpact: none" or "ContractImpact: updated"'); + } else { + const declared = match[1].toLowerCase(); + const contractChanged = filenames.some(f => f.startsWith(contractDir.replace(/\/$/, '') + '/')); + if (declared === 'updated' && !contractChanged) { + errors.push(`PR claims "ContractImpact: updated" but no files under ${contractDir}/ changed`); + } + if (declared === 'none' && contractChanged) { + errors.push(`PR claims "ContractImpact: none" but files under ${contractDir}/ were changed`); + } + } + } + + // ── 3. Changelog warning (non-blocking) ─────────────────────────────── + if (codeChangeRegex) { + try { + const codeRe = new RegExp(codeChangeRegex); + const hasCodeChange = filenames.some(f => codeRe.test(f)); + const hasChangelogUpdate = filenames.some(f => f === changelogPath || f.endsWith('/' + changelogPath)); + if (hasCodeChange && !hasChangelogUpdate) { + warn(`Code changes detected but ${changelogPath} was not updated. Add an entry if the change is user-visible.`); + } + } catch (e) { + core.warning(`code_change_regex is invalid: ${e.message}`); + } + } + + // ── 4. Template quality warnings (non-blocking) ─────────────────────── + for (const heading of ['What changed', 'Why it changed', 'How to verify', 'Summary', 'Changes']) { + const section = extractSection(prBody, heading); + if (section && sectionLooksUnfilled(section)) { + warn(`PR section "${heading}" appears to contain unfilled template content. Add concrete details.`); + } + } + + // ── 5. Fail on blocking errors ──────────────────────────────────────── + if (errors.length > 0) { + core.setFailed('PR metadata validation failed:\n' + errors.map(e => ` - ${e}`).join('\n')); + } else { + core.info('PR metadata validation passed.'); + } diff --git a/.github/workflows/python-sdk-tests.yml b/.github/workflows/python-sdk-tests.yml new file mode 100644 index 0000000..da70056 --- /dev/null +++ b/.github/workflows/python-sdk-tests.yml @@ -0,0 +1,86 @@ +name: Python SDK Tests + +# Reusable workflow for Python SDK test suites. +# Designed for PR fast-lane (skip network tests) and nightly/manual broad runs. +# Intended to be called from a CI After Gate workflow triggered by workflow_run. +# +# The job name "SDK Tests" is stable — branch protection rulesets reference it. + +on: + workflow_call: + inputs: + ref: + description: Commit SHA or ref to check out. + required: true + type: string + python_version: + required: false + type: string + default: "3.11" + working_directory: + description: Directory to run test commands from. + required: false + type: string + default: "." + install_command: + description: Dependency install command. Leave empty to skip. + required: false + type: string + default: "" + test_command: + description: Main test command. + required: true + type: string + skip_network_tests: + description: Set SKIP_NETWORK_TESTS=1 env var during the test run. + required: false + type: boolean + default: true + upload_artifacts: + description: Upload .test-output/ artifacts after the run. + required: false + type: boolean + default: false + artifact_name: + description: Name for the uploaded artifact (used when upload_artifacts is true). + required: false + type: string + default: "sdk-test-output" + +permissions: + contents: read + +jobs: + sdk-tests: + name: SDK Tests + runs-on: ubuntu-latest + timeout-minutes: 20 + defaults: + run: + working-directory: ${{ inputs.working_directory }} + + steps: + - uses: actions/checkout@v5 + with: + ref: ${{ inputs.ref }} + + - uses: actions/setup-python@v5 + with: + python-version: ${{ inputs.python_version }} + + - name: Install dependencies + if: ${{ inputs.install_command != '' }} + run: ${{ inputs.install_command }} + + - name: Run tests + env: + SKIP_NETWORK_TESTS: ${{ inputs.skip_network_tests && '1' || '0' }} + run: ${{ inputs.test_command }} + + - name: Upload test output + if: ${{ inputs.upload_artifacts && always() }} + uses: actions/upload-artifact@v4 + with: + name: ${{ inputs.artifact_name }} + path: ${{ inputs.working_directory }}/.test-output/ + if-no-files-found: ignore diff --git a/.github/workflows/raycast-ci.yml b/.github/workflows/raycast-ci.yml new file mode 100644 index 0000000..1ba87b9 --- /dev/null +++ b/.github/workflows/raycast-ci.yml @@ -0,0 +1,128 @@ +name: Raycast CI + +# Reusable workflow for Raycast extension repositories. +# Validates Raycast Store metadata, applies migration drift check, builds, lints, and runs optional tests. +# Intended to be called from a CI After Gate workflow triggered by workflow_run. +# +# Requires macOS because the Raycast CLI only runs on macOS. +# The job name "Raycast CI" is stable — branch protection rulesets reference it. + +on: + workflow_call: + inputs: + ref: + description: Commit SHA or ref to check out. + required: true + type: string + node_version: + description: Node.js version for the extension. + required: false + type: string + default: "22" + raycast_migration_version: + description: "@raycast/migration version to use for the drift check. Pin to your @raycast/api minor." + required: false + type: string + default: "1.103.0" + build_command: + description: Build command (Raycast Store requires a build export). + required: false + type: string + default: "npx ray build -e dist" + lint_command: + description: Lint command (Raycast Store requirement). + required: false + type: string + default: "npx ray lint" + typecheck_command: + description: Type check command. Leave empty to skip. + required: false + type: string + default: "npm run type-check" + test_command: + description: Unit test command. Leave empty to skip. + required: false + type: string + default: "" + +permissions: + contents: read + +jobs: + verify: + name: Raycast CI + runs-on: macos-latest + timeout-minutes: 20 + env: + NPM_CONFIG_CACHE: .npm-cache + + steps: + - uses: actions/checkout@v5 + with: + ref: ${{ inputs.ref }} + fetch-depth: 0 + + - uses: actions/setup-node@v5 + with: + node-version: ${{ inputs.node_version }} + + - name: Restore npm cache + uses: actions/cache@v5 + with: + path: ${{ env.NPM_CONFIG_CACHE }} + key: ${{ runner.os }}-npm-cache-node${{ inputs.node_version }}-${{ hashFiles('package-lock.json') }} + restore-keys: | + ${{ runner.os }}-npm-cache-node${{ inputs.node_version }}- + + - name: Verify Raycast Store metadata + run: | + node <<'NODE' + const fs = require("fs"); + const { execSync } = require("child_process"); + + function fail(msg) { console.error(msg); process.exit(1); } + + const pkg = JSON.parse(fs.readFileSync("package.json", "utf8")); + + if (pkg.license !== "MIT") fail(`Expected package.json license to be MIT, got: ${pkg.license}`); + if (typeof pkg.author !== "string" || !pkg.author.trim()) fail("Expected package.json author to be a non-empty string (your Raycast username)."); + if (!fs.existsSync("package-lock.json")) fail("Expected package-lock.json to exist (Raycast requires npm + lockfile)."); + + try { + execSync("git ls-files --error-unmatch package-lock.json", { stdio: "ignore" }); + } catch { + fail("Expected package-lock.json to be committed (tracked by git)."); + } + + console.log("Store metadata OK"); + NODE + + - name: Ensure Raycast migrations are applied + env: + RAYCAST_MIGRATION_VERSION: ${{ inputs.raycast_migration_version }} + run: | + set -euo pipefail + npx -y "@raycast/migration@${RAYCAST_MIGRATION_VERSION}" "$GITHUB_WORKSPACE" + if [ -n "$(git status --porcelain)" ]; then + echo "Raycast migrations produced uncommitted changes. Run 'npx -y @raycast/migration@${RAYCAST_MIGRATION_VERSION} .' locally and commit the result." + git status --porcelain + git diff + exit 1 + fi + + - name: Install dependencies + run: npm ci + + - name: Build + run: ${{ inputs.build_command }} + + - name: Lint + run: ${{ inputs.lint_command }} + + - name: Typecheck + if: ${{ inputs.typecheck_command != '' }} + run: ${{ inputs.typecheck_command }} + + - name: Unit tests + if: ${{ inputs.test_command != '' }} + run: ${{ inputs.test_command }} diff --git a/docs/bugbot-gate-rollout.md b/docs/bugbot-gate-rollout.md index cbc1bb4..1311df2 100644 --- a/docs/bugbot-gate-rollout.md +++ b/docs/bugbot-gate-rollout.md @@ -1,14 +1,23 @@ # Bugbot Gate Rollout Handoff -This org-level `.github` repo now contains the shared reusable gate workflow at: +The shared org-level gate lives in this repo at: -- `.github/workflows/bugbot-gate.yml` +- `.github/workflows/bugbot-gate.yml` — reusable gate (wait for Cursor Bugbot + unresolved thread check) +- `.github/workflows/pr-metadata-check.yml` — reusable PR metadata/description check +- `.github/workflows/node-pnpm-quality.yml` — reusable lint + typecheck +- `.github/workflows/node-pnpm-build.yml` — reusable build +- `.github/workflows/node-pnpm-playwright.yml` — reusable Playwright +- `.github/workflows/raycast-ci.yml` — reusable Raycast extension CI +- `.github/workflows/python-sdk-tests.yml` — reusable Python SDK tests -Use the following files in each active application repository. +Each active application repo needs two workflow files and nothing else for the baseline. -## 1) Thin per-repo caller workflow +--- -Create `.github/workflows/bugbot-gate.yml` in each active repo: +## 1) Thin Bugbot Gate caller (all repos) + +Create `.github/workflows/bugbot-gate.yml` in each active repo. +Replace `kyndig` with the actual organisation name if it ever changes. ```yaml name: Bugbot Gate @@ -26,18 +35,24 @@ permissions: jobs: gate: name: Bugbot Gate - uses: ORG/.github/.github/workflows/bugbot-gate.yml@main + uses: kyndig/.github/.github/workflows/bugbot-gate.yml@main with: sha: ${{ github.event.pull_request.head.sha }} + pull_number: ${{ github.event.pull_request.number }} bugbot_check_name: Cursor Bugbot timeout_minutes: 15 ``` -Replace `ORG` with the actual organization name. +--- + +## 2) CI After Gate — per repo category + +The expensive CI workflow uses `workflow_run` so it only starts after `Bugbot Gate` succeeds. +Use `github.event.workflow_run.head_sha` for checkout so tests always run on the exact commit that passed the gate. -## 2) Downstream expensive CI workflow +### Web app (pnpm) — kynd-web, kynd-web-new, spork-web -Create `.github/workflows/ci-after-gate.yml` in each active repo: +Create `.github/workflows/ci-after-gate.yml`: ```yaml name: CI After Gate @@ -52,96 +67,284 @@ permissions: jobs: quality: - name: Quality if: ${{ github.event.workflow_run.conclusion == 'success' }} - runs-on: ubuntu-latest + uses: kyndig/.github/.github/workflows/node-pnpm-quality.yml@main + with: + ref: ${{ github.event.workflow_run.head_sha }} + lint_command: pnpm lint + typecheck_command: pnpm typecheck + # format_command: pnpm format # uncomment if the repo enforces formatting + + build: + if: ${{ github.event.workflow_run.conclusion == 'success' }} + uses: kyndig/.github/.github/workflows/node-pnpm-build.yml@main + with: + ref: ${{ github.event.workflow_run.head_sha }} + + playwright: + if: ${{ github.event.workflow_run.conclusion == 'success' }} + uses: kyndig/.github/.github/workflows/node-pnpm-playwright.yml@main + with: + ref: ${{ github.event.workflow_run.head_sha }} + test_command: pnpm exec playwright test + # Remove or adjust this job if the repo has no Playwright tests. +``` + +**kynd-web** uses `pnpm check` instead of separate lint/typecheck — override accordingly: + +```yaml + quality: + if: ${{ github.event.workflow_run.conclusion == 'success' }} + uses: kyndig/.github/.github/workflows/node-pnpm-quality.yml@main + with: + ref: ${{ github.event.workflow_run.head_sha }} + lint_command: pnpm check + typecheck_command: "true" # check already includes typecheck; skip explicit step +``` + +**spork-web** also runs Python API tests — add a bespoke step or a separate job for `pnpm test:api` in a local `ci-after-gate.yml` that extends the shared quality/build jobs. + +--- + +### Node/pnpm monorepo (Turborepo) — kynd-bid-system + +```yaml +name: CI After Gate + +on: + workflow_run: + workflows: ["Bugbot Gate"] + types: [completed] + +permissions: + contents: read + +jobs: + quality: + if: ${{ github.event.workflow_run.conclusion == 'success' }} + uses: kyndig/.github/.github/workflows/node-pnpm-quality.yml@main + with: + ref: ${{ github.event.workflow_run.head_sha }} + lint_command: pnpm turbo lint + typecheck_command: pnpm turbo typecheck + + test: + if: ${{ github.event.workflow_run.conclusion == 'success' }} + uses: kyndig/.github/.github/workflows/node-pnpm-build.yml@main + with: + ref: ${{ github.event.workflow_run.head_sha }} + build_command: pnpm turbo test +``` + +--- + +### Raycast extension — yr-wfc, brreg-search + +```yaml +name: CI After Gate + +on: + workflow_run: + workflows: ["Bugbot Gate"] + types: [completed] + +permissions: + contents: read + +jobs: + raycast: + if: ${{ github.event.workflow_run.conclusion == 'success' }} + uses: kyndig/.github/.github/workflows/raycast-ci.yml@main + with: + ref: ${{ github.event.workflow_run.head_sha }} + node_version: "22" + raycast_migration_version: "1.103.0" + # yr-wfc has unit tests; brreg-search currently does not: + # test_command: npm run test:unit +``` + +--- + +### Python SDK — varde + +```yaml +name: CI After Gate + +on: + workflow_run: + workflows: ["Bugbot Gate"] + types: [completed] + +permissions: + contents: read + +jobs: + sdk-tests: + if: ${{ github.event.workflow_run.conclusion == 'success' }} + uses: kyndig/.github/.github/workflows/python-sdk-tests.yml@main + with: + ref: ${{ github.event.workflow_run.head_sha }} + python_version: "3.11" + working_directory: sdk/adapters/terminal + test_command: python -m unittest discover -s tests + skip_network_tests: true + upload_artifacts: true + artifact_name: varde-test-output +``` + +--- + +### Swift/macOS — ritz + +Ritz uses SwiftLint on a macOS runner. There is no shared reusable workflow for Swift yet. Create a local `ci-after-gate.yml` that calls SwiftLint directly after the gate: + +```yaml +name: CI After Gate + +on: + workflow_run: + workflows: ["Bugbot Gate"] + types: [completed] + +permissions: + contents: read + +jobs: + swiftlint: + name: SwiftLint + if: ${{ github.event.workflow_run.conclusion == 'success' }} + runs-on: macos-15 + timeout-minutes: 15 steps: - uses: actions/checkout@v5 with: ref: ${{ github.event.workflow_run.head_sha }} - - uses: pnpm/action-setup@v4 - with: - run_install: false - - uses: actions/setup-node@v5 - with: - node-version: lts/* - cache: pnpm - - run: pnpm install --frozen-lockfile - - run: pnpm lint - - run: pnpm typecheck + - name: Install SwiftLint + run: brew install swiftlint + - name: Run SwiftLint + run: swiftlint lint --strict --reporter github-actions-logging +``` - build: - name: Build +--- + +### CLI tool — spork + +Spork uses bats and shellcheck on macOS. Create a local `ci-after-gate.yml`: + +```yaml +name: CI After Gate + +on: + workflow_run: + workflows: ["Bugbot Gate"] + types: [completed] + +permissions: + contents: read + +jobs: + ci: + name: CI if: ${{ github.event.workflow_run.conclusion == 'success' }} - runs-on: ubuntu-latest + runs-on: macos-latest + timeout-minutes: 15 steps: - uses: actions/checkout@v5 with: ref: ${{ github.event.workflow_run.head_sha }} - - uses: pnpm/action-setup@v4 - with: - run_install: false - - uses: actions/setup-node@v5 - with: - node-version: lts/* - cache: pnpm - - run: pnpm install --frozen-lockfile - - run: pnpm build + - name: Install dependencies + run: brew install bats-core shellcheck + - name: Run CI + run: make ci ``` -Important: +--- + +## 3) Optional PR metadata check + +If the repo enforces PR description requirements, also create `.github/workflows/pr-metadata.yml`: + +```yaml +name: PR Validation + +on: + pull_request: + branches: [main] + types: [opened, synchronize, reopened, edited] -- Keep expensive CI out of direct `pull_request` triggers if the goal is to save Actions minutes. -- Use `github.event.workflow_run.head_sha` for checkout so downstream CI tests the same PR commit that passed the gate. -- In `actions/github-script`, pass workflow inputs via `env` and read from `process.env` (never interpolate inputs into script literals). +permissions: + contents: read + pull-requests: read + issues: read -## 3) Org ruleset setup +jobs: + metadata: + name: PR Metadata Check + uses: kyndig/.github/.github/workflows/pr-metadata-check.yml@main + with: + require_issue_ref: true + allow_work_doc: false # set true for repos using WorkDoc: docs/work/.md + require_contract_impact: false # set true for repos with a docs/contracts/ directory +``` + +--- -1. Add a pilot repo with the two workflows above. -2. Open a real test PR and wait for checks to run. -3. In GitHub UI, verify the exact emitted check names. -4. Add required status checks in org rulesets using exact names. +## 4) Org ruleset setup -Minimum required check: +1. Add the two per-repo workflow files above to a pilot repo. +2. Open a real test PR and wait for both workflows to run. +3. In GitHub UI → repo → Actions, confirm the exact emitted check names. +4. Add required status checks in org or repo rulesets using the exact names. + +Minimum required check for every repo: - `Bugbot Gate` -Optional additional required checks (only after verified names exist): +Downstream checks to add once check names are verified (example names below — confirm from a real PR before locking): + +- Web pnpm: `CI After Gate / Quality`, `CI After Gate / Build`, `CI After Gate / Playwright` +- Raycast: `CI After Gate / Raycast CI` +- Python SDK: `CI After Gate / SDK Tests` -- `Quality` -- `Build` -- `Playwright` +Do not guess check names and lock them into policy without observing them first. -## 4) Rollout checklist (active repos first) +--- -1. Keep shared gate logic centralized in `ORG/.github`. -2. Onboard one pilot active repo. -3. Confirm expensive CI does not start before gate success. -4. Confirm required-check names from real runs before locking rulesets. -5. Onboard the next 3-5 active repos. -6. Expand further only where there is active development. +## 5) Rollout checklist -## 5) v1 behavior and scope guardrails +1. Enable base repo settings where missing: branch deletion after merge, `main` protection, require PRs. +2. Implement shared workflows in this `.github` repo (done). +3. Onboard one pilot repo (recommended: `kynd-web-new` — already has clean job categories). +4. Confirm `Bugbot Gate` appears as a required check from a real PR. +5. Confirm expensive CI does **not** start before `Bugbot Gate` succeeds. +6. Lock required checks via ruleset. +7. Remove or disable local Bugbot-comment check in the onboarded repo. +8. Onboard next 3–5 active repos (see `docs/pr-checks-audit.md` for priority order). +9. Do not force rollout to inactive repos. -`Bugbot Gate` v1 semantics: +--- -- accept PR head SHA -- wait for `Cursor Bugbot` check (or configured check name) -- pass only when conclusion is `success` -- fail on non-success conclusion or timeout +## 6) Gate semantics + +`Bugbot Gate` passes only when both conditions are true: + +- The `Cursor Bugbot` check run has completed (any conclusion — completion is the trigger). +- No unresolved Bugbot review threads exist for the **current push cycle** (threads posted after the most recent commit or force-push to the PR). + +Current-cycle scoping means resolving old threads from a previous commit does not re-open the gate; only threads from the latest push count. Out of scope in v1: -- comment-resolution checks -- severity parsing -- stale-thread handling -- manual acknowledgment exceptions +- Severity-level filtering (Low / Medium / High / Critical treated equally) +- Manual acknowledgment exceptions +- Stale-thread dismissal + +--- -## 6) Fork PR expectation (v1) +## 7) Fork PR expectation -Fork handling is explicit in v1: support fork PRs only for non-secret downstream CI. +Fork PRs are supported as long as: -- `CI After Gate` runs from a `workflow_run` event, so use the `workflow_run` payload fields directly and do not assume `pull_request`-event semantics. -- `actions/checkout` with `github.event.workflow_run.head_sha` executes contributor-controlled code from the fork; treat that code as untrusted. -- Keep `CI After Gate` permissions minimal and avoid secrets-dependent steps unless you intentionally design and review a fork-safe model. -- Confirm `Cursor Bugbot` emits the named check for fork PRs in your org settings (including first-time contributor approval flows); if it does not, `Bugbot Gate` fails by design. +- `CI After Gate` steps do not depend on repo secrets. +- `actions/checkout` uses `github.event.workflow_run.head_sha` (contributor-controlled code — treat as untrusted for secrets-sensitive steps). +- `Cursor Bugbot` emits the named check for fork PRs in your org settings. If first-time contributor approval is required by GitHub, the gate will not find the check and will time out by design. diff --git a/docs/org-baseline.md b/docs/org-baseline.md new file mode 100644 index 0000000..6e6bea4 --- /dev/null +++ b/docs/org-baseline.md @@ -0,0 +1,140 @@ +# Org Repository Baseline + +Required settings and branch protection configuration for all active kyndig repositories. +Apply these before or alongside the shared workflow rollout. + +--- + +## Repository Settings (apply to every active repo) + +### Automatic branch deletion after merge + +Enable `delete_branch_on_merge` on every repo. This prevents stale branches accumulating after PRs are merged. + +**Currently missing on:** `switchto`, `varde-web`, `faen-ta` + +Apply via GitHub UI (Settings → General → Pull Requests → Automatically delete head branches) +or via the API / Terraform if you manage settings programmatically. + +### Default branch + +All active repos already use `main` as the default branch. + +--- + +## Main Branch Protection (apply to every active repo) + +Minimum ruleset or branch protection configuration for `main`: + +| Setting | Value | Reason | +|---------|-------|--------| +| Require a pull request before merging | ✅ | Core policy: no direct pushes to main | +| Require approvals | 0 (or 1 if preferred) | Gate is the quality signal; reviews are optional | +| Dismiss stale reviews when new commits are pushed | ✅ recommended | Prevents stale approvals from carrying through | +| Require status checks to pass before merging | ✅ | See required checks below | +| Require branches to be up to date before merging | ✅ | Prevents merging stale branches that bypass gate | +| Do not allow bypassing the above settings | ✅ | Enforce admins too | + +**Currently unprotected:** `ritz`, `switchto`, `varde-web`, `yr-wfc`, `brreg-search`, `faen-ta`, `kynd-bid-system`, `spork`, `spork-web` + +**Currently protected but needing normalisation:** `kynd-web` (requires `Check` — stale name), `kynd-web-new` (no `Bugbot Gate`), `varde` (no `Bugbot Gate`) + +--- + +## Required Status Checks + +Add required checks in the following sequence to avoid blocking PRs before workflows exist. + +### Step 1 — Available on all onboarded repos immediately + +After adding the thin `bugbot-gate.yml` caller and the `CI After Gate` workflow to a repo, require: + +- `Bugbot Gate` + +This is the minimum gate. Do not add any other required check until you have observed its exact emitted name in a real PR. + +### Step 2 — Add downstream checks after verification + +Once you have confirmed exact emitted check names from a real PR run, add the relevant downstream checks. Example names by category (verify before locking): + +| Category | Expected check names | +|----------|---------------------| +| Web pnpm | `CI After Gate / Quality`, `CI After Gate / Build` | +| Web pnpm + Playwright | `CI After Gate / Quality`, `CI After Gate / Build`, `CI After Gate / Playwright` | +| Raycast extension | `CI After Gate / Raycast CI` | +| Python SDK | `CI After Gate / SDK Tests` | +| Swift/macOS | `CI After Gate / SwiftLint` | +| CLI tool | `CI After Gate / CI` | + +Do not guess check names. The check name in the GitHub UI is what to use. + +### Current protected repo gaps + +| Repo | Current required checks | Action needed | +|------|------------------------|---------------| +| kynd-web | `Check` | Replace with `Bugbot Gate` + category checks after onboarding | +| kynd-web-new | `Quality`, `Build`, `Playwright` | Add `Bugbot Gate`; migrate expensive CI to `CI After Gate` | +| varde | `PR Metadata Check`, `SDK tests (PR, fast)`, `Check Bugbot Comments` | Add `Bugbot Gate`; migrate to shared gate + `CI After Gate` | + +--- + +## Org-Level Ruleset (recommended) + +Rather than configuring each repo independently, consider a single org-level ruleset targeting all active repos. This avoids per-repo drift. + +Recommended org ruleset: + +``` +Target: all repositories (or specific repos by name pattern) +Target branch: main + +Rules: + - Require pull request + - Required status checks: + - Bugbot Gate + - Require branches to be up to date + - Delete head branch on merge (enforced at ruleset level) +``` + +Per-repo downstream checks (`Quality`, `Build`, etc.) cannot be required org-wide because they differ per category. Add them as repo-level required checks after verifying names. + +--- + +## Rollout Sequence + +Apply in this order to avoid breaking existing PRs: + +1. **Enable branch deletion** on `switchto`, `varde-web`, `faen-ta` (no workflow dependency, safe to do immediately). +2. **Add main protection** to all unprotected repos with PR-required-only (no required status checks yet). +3. **Onboard pilot repo** with shared gate workflows (recommended: `kynd-web-new`). +4. **Verify check names** from a real pilot PR. +5. **Add `Bugbot Gate` as required check** on pilot repo. +6. **Expand to next 3–5 active repos** following the same pattern. +7. **Migrate existing protected repos** (`kynd-web`, `kynd-web-new`, `varde`) to shared gate after their local implementations are removed. +8. **Do not force rollout** to inactive repos (`switchto`, `varde-web`, `faen-ta`) until they have active development. + +--- + +## Normalising Currently Protected Repos + +### kynd-web + +1. Add `bugbot-gate.yml` caller and `ci-after-gate.yml` (pnpm web pattern). +2. Remove direct `pull_request` trigger from the existing `ci.yml`. +3. Verify `Bugbot Gate`, `CI After Gate / Quality`, `CI After Gate / Build` appear in checks. +4. Update branch protection: replace `Check` with `Bugbot Gate` + verified downstream names. + +### kynd-web-new + +1. Add `bugbot-gate.yml` caller. +2. Replace the direct `pull_request` trigger in `ci.yml` with a `ci-after-gate.yml` using `workflow_run`. +3. Verify check names (they may change from `Quality` to `CI After Gate / Quality`). +4. Update branch protection: add `Bugbot Gate`, update downstream check names. + +### varde + +1. Add `bugbot-gate.yml` caller. +2. Replace local `bugbot-check.yml` with the shared gate (remove local after migration). +3. Move SDK tests to `ci-after-gate.yml` triggered by `workflow_run`. +4. Keep `pr-validation.yml` on `pull_request` (it is cheap and not blocked by the gate). +5. Update branch protection: replace `Check Bugbot Comments` with `Bugbot Gate`; update test check name if it changes. diff --git a/docs/pr-checks-audit.md b/docs/pr-checks-audit.md new file mode 100644 index 0000000..33577bf --- /dev/null +++ b/docs/pr-checks-audit.md @@ -0,0 +1,204 @@ +# PR Checks Audit + +Current state of PR workflows, branch protection, and repository settings across active kyndig repositories. + +Last updated: 2026-05-02 + +--- + +## Repository Settings Overview + +| Repo | Default Branch | Delete Branch on Merge | main Protected | Required Status Checks | +|------|---------------|----------------------|----------------|------------------------| +| ritz | main | ✅ | ❌ | — | +| kynd-web | main | ✅ | ✅ | `Check` | +| kynd-web-new | main | ✅ | ✅ | `Quality`, `Build`, `Playwright` | +| switchto | main | ❌ | ❌ | — | +| varde | main | ✅ | ✅ | `PR Metadata Check`, `SDK tests (PR, fast)`, `Check Bugbot Comments` | +| varde-web | main | ❌ | ❌ | — | +| yr-wfc | main | ✅ | ❌ | — | +| brreg-search | main | ✅ | ❌ | — | +| faen-ta | main | ❌ | ❌ | — | +| kynd-bid-system | main | ✅ | ❌ | — | +| spork | main | ✅ | ❌ | — | +| spork-web | main | ✅ | ❌ | — | + +### Gaps to close + +**Branch deletion after merge** — must be enabled on: `switchto`, `varde-web`, `faen-ta` + +**Main branch protection** — must be added on: `ritz`, `switchto`, `varde-web`, `yr-wfc`, `brreg-search`, `faen-ta`, `kynd-bid-system`, `spork`, `spork-web` + +**Protected repos to normalise** — `kynd-web` uses `Check` (stale name from an old local CI job), `kynd-web-new` and `varde` use custom local check names. All three need to converge to the shared `Bugbot Gate` baseline once onboarded. + +--- + +## PR Workflows Per Repo + +### ritz (Swift/macOS) + +| Workflow | Trigger | Notes | +|----------|---------|-------| +| `Bugbot Review Check` | pull_request, review events | Waits for Cursor Bugbot check completion; GraphQL unresolved-thread check (all cycles, no cycle-boundary logic) | +| `PR Validation` | pull_request | SwiftLint strict (macOS runner), PR metadata (issue link, ContractImpact, changelog warning), failure comment on PR | +| `Integration Tests` | schedule (weekly) | Not on PR | +| `Nightly Tests` | schedule | Not on PR | +| `Plan Sync`, `Planning JSON`, `Project Status Sync` | Not PR-specific | | + +**Bugbot first**: No — SwiftLint runs in parallel with Bugbot. +**Expensive CI on `pull_request`**: Yes (SwiftLint on macOS runner). +**Shared gate**: Not yet onboarded. + +--- + +### kynd-web (pnpm web app) + +| Workflow | Trigger | Notes | +|----------|---------|-------| +| `CI` → job `Check` | pull_request, push (non-main) | pnpm install, `pnpm check`, `pnpm build` | +| `Issue Hygiene` | Not PR-specific | | + +**Bugbot first**: No. +**Expensive CI on `pull_request`**: Yes. +**Shared gate**: Not yet onboarded. + +--- + +### kynd-web-new (pnpm web app) + +| Workflow | Trigger | Notes | +|----------|---------|-------| +| `CI` → jobs `Quality`, `Build`, `Playwright` | pull_request (main), push (main) | All three jobs run directly on pull_request | +| `Lighthouse Budget` | schedule (Monday), manual | Not on PR | + +**Bugbot first**: No — all three expensive jobs run in parallel. +**Expensive CI on `pull_request`**: Yes. +**Shared gate**: Not yet onboarded. + +Note: `kynd-web-new` already has the canonical `Quality`, `Build`, `Playwright` job name split; it is the best pilot for moving expensive CI behind the gate. + +--- + +### switchto + +No workflow files found. No protection. Branch deletion disabled. + +--- + +### varde (Python SDK / workflow kit) + +| Workflow | Trigger | Notes | +|----------|---------|-------| +| `Bugbot Review Check (Kit)` | pull_request, review events | Configurable reviewer/body matchers; current-cycle GraphQL thread check (most advanced local implementation) | +| `PR Validation (Kit)` | pull_request | Issue link / WorkDoc, ContractImpact, release field, SDK docs drift, work registry validation | +| `Tests (Kit)` → `SDK tests (PR, fast)` | pull_request (path-filtered) | Fast SDK tests, no network; nightly lane with network smoke | +| Various project sync, release, staleness, epic workflows | Not PR-specific | | + +**Bugbot first**: No — SDK tests and metadata check run in parallel with Bugbot. +**Expensive CI on `pull_request`**: SDK tests are relatively cheap and path-filtered. +**Shared gate**: Not yet onboarded. The local Kit implementations are the reference for the shared Bugbot gate upgrade. + +--- + +### varde-web + +No workflow files found. No protection. Branch deletion disabled. + +--- + +### yr-wfc (Raycast extension) + +| Workflow | Trigger | Notes | +|----------|---------|-------| +| `Raycast CI` | pull_request, push (main) | Metadata validation, migration drift, npm ci, build, typecheck, lint, unit tests; macOS runner | + +**Bugbot first**: No. +**Expensive CI on `pull_request`**: Yes (macOS runner). +**Shared gate**: Not yet onboarded. + +--- + +### brreg-search (Raycast extension) + +| Workflow | Trigger | Notes | +|----------|---------|-------| +| `Raycast CI` | pull_request, push (main) | Metadata validation, migration drift, npm ci, lint, build; macOS runner | + +**Bugbot first**: No. +**Expensive CI on `pull_request`**: Yes (macOS runner). +**Shared gate**: Not yet onboarded. +**Divergence from yr-wfc**: Different Node version, action versions, no type check or unit tests. + +--- + +### faen-ta + +| Workflow | Trigger | Notes | +|----------|---------|-------| +| `substack-rebuild` | Not PR-specific | Deploy/content workflow | + +No PR checks. No protection. Branch deletion disabled. + +--- + +### kynd-bid-system (pnpm monorepo / Turborepo) + +| Workflow | Trigger | Notes | +|----------|---------|-------| +| `CI` → `validate` + `bugbot-gate` | pull_request, push (main) | `validate`: turbo lint, typecheck, test; `bugbot-gate`: inline GraphQL thread check only (no check-run wait, no cycle boundary) | + +**Bugbot first**: No — `validate` and `bugbot-gate` run in parallel. +**Expensive CI on `pull_request`**: Yes. +**Shared gate**: Partial inline implementation, no check-run wait, no cycle-boundary semantics. + +--- + +### spork (CLI tool, shell/macOS) + +| Workflow | Trigger | Notes | +|----------|---------|-------| +| `CI` | pull_request, push (main/master) | `make ci` (bats + shellcheck); macOS runner | + +**Bugbot first**: No. +**Expensive CI on `pull_request`**: Yes (macOS runner). +**Shared gate**: Not yet onboarded. + +--- + +### spork-web (pnpm + Python) + +| Workflow | Trigger | Notes | +|----------|---------|-------| +| `CI` | pull_request, push (main) | pnpm + Python setup, contract generation/check, lint, typecheck, test, test:api, build | + +**Bugbot first**: No. +**Expensive CI on `pull_request`**: Yes. +**Shared gate**: Not yet onboarded. + +--- + +## Existing Bugbot Implementations + +Three diverging local Bugbot-comment check implementations exist. All should be replaced by the shared reusable `Bugbot Gate`. + +| Repo | Waits for check run? | Cycle-boundary logic? | Configurable matchers? | +|------|---------------------|----------------------|----------------------| +| ritz | ✅ (5 min timeout, any completion) | ❌ | ❌ | +| varde | ❌ (no check-run wait) | ✅ | ✅ | +| kynd-bid-system | ❌ | ❌ | ❌ | + +The shared gate combines check-run wait (from `ritz`) with cycle-boundary logic and configurable matchers (from `varde`). + +--- + +## Repo Categories + +| Category | Repos | +|----------|-------| +| Web app, pnpm | `kynd-web`, `kynd-web-new`, `spork-web` | +| Node/pnpm monorepo | `kynd-bid-system` | +| Raycast extension | `yr-wfc`, `brreg-search` | +| Swift/macOS app | `ritz` | +| Python SDK / kit | `varde` | +| CLI tool (shell/macOS) | `spork` | +| No known CI yet | `switchto`, `varde-web`, `faen-ta` |