Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions .github/workflows/bugbot-gate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
name: Bugbot Gate

on:
workflow_call:
inputs:
sha:
required: true
type: string
bugbot_check_name:
required: false
type: string
default: Cursor Bugbot
timeout_minutes:
required: false
type: number
default: 15

permissions:
contents: read
checks: read
pull-requests: read

jobs:
gate:
name: Bugbot Gate
runs-on: ubuntu-latest
timeout-minutes: ${{ inputs.timeout_minutes + 2 }}

steps:
- name: Wait for Bugbot check
uses: actions/github-script@v7
env:
BUGBOT_GATE_SHA: ${{ inputs.sha }}
BUGBOT_GATE_CHECK_NAME: ${{ inputs.bugbot_check_name }}
BUGBOT_GATE_TIMEOUT_MINUTES: ${{ inputs.timeout_minutes }}
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const sha = process.env.BUGBOT_GATE_SHA;
const target = process.env.BUGBOT_GATE_CHECK_NAME;
const timeoutMinutes = Number(process.env.BUGBOT_GATE_TIMEOUT_MINUTES);

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;
}

const timeoutMs = timeoutMinutes * 60 * 1000;
const start = Date.now();

while (true) {
const { data } = await github.rest.checks.listForRef({
owner,
repo,
ref: sha,
per_page: 100
});

const matching = data.check_runs.filter(run => run.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"})`
);
}

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 (Date.now() - start > timeoutMs) {
core.setFailed(`Timed out waiting for ${target}`);
return;
}

core.info(`Still waiting for ${target} on ${sha}...`);
await new Promise(resolve => setTimeout(resolve, 15000));
}
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
# kynd

Made with 🫶 by [kynd](https://kynd.no)
147 changes: 147 additions & 0 deletions docs/bugbot-gate-rollout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# Bugbot Gate Rollout Handoff

This org-level `.github` repo now contains the shared reusable gate workflow at:

- `.github/workflows/bugbot-gate.yml`

Use the following files in each active application repository.

## 1) Thin per-repo caller workflow

Create `.github/workflows/bugbot-gate.yml` in each active repo:

```yaml
name: Bugbot Gate

on:
pull_request:
branches: [main]
types: [opened, synchronize, reopened]

permissions:
contents: read
checks: read
pull-requests: read

jobs:
gate:
name: Bugbot Gate
uses: ORG/.github/.github/workflows/bugbot-gate.yml@main
with:
sha: ${{ github.event.pull_request.head.sha }}
bugbot_check_name: Cursor Bugbot
timeout_minutes: 15
```

Replace `ORG` with the actual organization name.

## 2) Downstream expensive CI workflow

Create `.github/workflows/ci-after-gate.yml` in each active repo:

```yaml
name: CI After Gate

on:
workflow_run:
workflows: ["Bugbot Gate"]
types: [completed]

permissions:
contents: read

jobs:
quality:
name: Quality
if: ${{ github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
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

build:
name: Build
if: ${{ github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
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
```

Important:

- 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).

## 3) Org ruleset setup

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.

Minimum required check:

- `Bugbot Gate`

Optional additional required checks (only after verified names exist):

- `Quality`
- `Build`
- `Playwright`

## 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) v1 behavior and scope guardrails

`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

Out of scope in v1:

- comment-resolution checks
- severity parsing
- stale-thread handling
- manual acknowledgment exceptions

## 6) Fork PR expectation (v1)

Fork handling is explicit in v1: support fork PRs only for non-secret downstream CI.

- `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.
Loading