Fast pages, required checks on the branch, self-hosted runners, honest incidents
- Speed: placement off, one round of calls per page, slow panels stream in behind skeletons, read replicas through D1 sessions, a cached shell, cached public pages, and Server-Timing everywhere. - Required checks: workflows report checks and branch protection picks the required ones; the same gate for people and agents; Add CI opens a starter workflow. - Self-hosted runners: g1t-runner, registration, groups, runs-on: self-hosted, agent work on your machines, and $0 runtime. - Runner images split into a rarely rebuilt base and a thin layer; sandboxes clone shallow; the Actions cache moves to R2 with larger entries; bigger instance sizes. - Workflow-only egress domains, and failure() that matches GitHub Actions for skipped needs. - Incidents: streaks reset on recovery, slow is not down, blips dismiss themselves, deploys open a quiet window, and times show in your zone. - Projects follow their repository's description; blob pages link and highlight lines; tabs get padding.
| 8 | 8 | # core, edge, front the units of each stage, in jobs that share a build; | |
| 9 | 9 | # a stage starts only when the one before it succeeded | |
| 10 | 10 | # | |
| 11 | − | # Needs the repository secret CLOUDFLARE_API_TOKEN (a Production row) and | |
| 12 | − | # the variable CLOUDFLARE_ACCOUNT_ID, and api.cloudflare.com among the | |
| 13 | − | # project's allowed domains (Settings, Guardrails). See docs/DEPLOYING.md. | |
| 11 | + | # Needs the repository secret CLOUDFLARE_API_TOKEN (a Production row), the | |
| 12 | + | # variable CLOUDFLARE_ACCOUNT_ID, and api.cloudflare.com among the project's | |
| 13 | + | # workflow-only domains for deploy.yml in production (Settings, Guardrails), | |
| 14 | + | # and registry.cloudflare.com there too, to find the runner's image. See | |
| 15 | + | # docs/DEPLOYING.md. | |
| 14 | 16 | name: Deploy | |
| 15 | 17 | ||
| 16 | 18 | on: | |
| 108 | 110 | core: | |
| 109 | 111 | name: core (${{ matrix.group }}) | |
| 110 | 112 | needs: [plan, migrate] | |
| 111 | − | if: ${{ !cancelled() && needs.plan.result == 'success' && (needs.migrate.result == 'success' || needs.migrate.result == 'skipped') && needs.plan.outputs.has_core == 'true' && inputs.dry_run != true }} | |
| 112 | − | runs-on: ubuntu-latest | |
| 113 | + | # Runs when nothing before it failed: a migrate job skipped for having | |
| 114 | + | # nothing to apply is not a failure. | |
| 115 | + | if: ${{ !failure() && !cancelled() && needs.plan.outputs.has_core == 'true' && inputs.dry_run != true }} | |
| 116 | + | # Rust builds get 4 vCPUs; everything else the standard machine. | |
| 117 | + | runs-on: ${{ matrix.rust && 'g1t-4core' || 'ubuntu-latest' }} | |
| 113 | 118 | environment: production | |
| 114 | 119 | timeout-minutes: 60 | |
| 115 | 120 | strategy: | |
| 146 | 151 | path: ~/.cargo/registry/cache | |
| 147 | 152 | key: cargo-crates-${{ runner.os }}-${{ hashFiles('Cargo.lock') }} | |
| 148 | 153 | restore-keys: cargo-crates-${{ runner.os }}- | |
| 154 | + | # The compiled dependencies of this job's units, for wasm32 and the | |
| 155 | + | # build scripts and proc macros they run. The workspace's own crates | |
| 156 | + | # are compiled again whatever is cached (a checkout's sources are | |
| 157 | + | # newer), so an entry is saved only when the dependencies change: a | |
| 158 | + | # new Cargo.lock, or a new base image (base.json names its Rust). | |
| 159 | + | # Otherwise the nearest earlier entry, of any group, is a start. | |
| 160 | + | - name: Cache the Cargo target | |
| 161 | + | if: ${{ matrix.rust }} | |
| 162 | + | uses: actions/cache@v4 | |
| 163 | + | with: | |
| 164 | + | path: | | |
| 165 | + | target/release | |
| 166 | + | target/wasm32-unknown-unknown/release | |
| 167 | + | !target/**/incremental | |
| 168 | + | !target/**/*.wasm | |
| 169 | + | key: cargo-target-${{ runner.os }}-${{ matrix.group }}-${{ hashFiles('Cargo.lock', 'services/runner/base.json') }} | |
| 170 | + | restore-keys: | | |
| 171 | + | cargo-target-${{ runner.os }}-${{ matrix.group }}- | |
| 172 | + | cargo-target-${{ runner.os }}- | |
| 149 | 173 | - name: Install | |
| 150 | 174 | run: node scripts/deploy.mjs install --only "${{ matrix.units }}" | |
| 151 | 175 | - name: Deploy ${{ matrix.units }} | |
| 156 | 180 | edge: | |
| 157 | 181 | name: edge (${{ matrix.group }}) | |
| 158 | 182 | needs: [plan, migrate, core] | |
| 159 | − | if: ${{ !cancelled() && needs.plan.result == 'success' && (needs.migrate.result == 'success' || needs.migrate.result == 'skipped') && (needs.core.result == 'success' || needs.core.result == 'skipped') && needs.plan.outputs.has_edge == 'true' && inputs.dry_run != true }} | |
| 160 | − | runs-on: ubuntu-latest | |
| 183 | + | if: ${{ !failure() && !cancelled() && needs.plan.outputs.has_edge == 'true' && inputs.dry_run != true }} | |
| 184 | + | runs-on: ${{ matrix.rust && 'g1t-4core' || 'ubuntu-latest' }} | |
| 161 | 185 | environment: production | |
| 162 | 186 | timeout-minutes: 60 | |
| 163 | 187 | strategy: | |
| 169 | 193 | front: | |
| 170 | 194 | name: front (${{ matrix.group }}) | |
| 171 | 195 | needs: [plan, migrate, core, edge] | |
| 172 | − | if: ${{ !cancelled() && needs.plan.result == 'success' && (needs.migrate.result == 'success' || needs.migrate.result == 'skipped') && (needs.core.result == 'success' || needs.core.result == 'skipped') && (needs.edge.result == 'success' || needs.edge.result == 'skipped') && needs.plan.outputs.has_front == 'true' && inputs.dry_run != true }} | |
| 173 | − | runs-on: ubuntu-latest | |
| 196 | + | if: ${{ !failure() && !cancelled() && needs.plan.outputs.has_front == 'true' && inputs.dry_run != true }} | |
| 197 | + | runs-on: ${{ matrix.rust && 'g1t-4core' || 'ubuntu-latest' }} | |
| 174 | 198 | environment: production | |
| 175 | 199 | timeout-minutes: 60 | |
| 176 | 200 | strategy: |
| 1 | + | # Rebuilds the base image of g1t's sandboxes (services/runner/base/Dockerfile: | |
| 2 | + | # the OS, toolchains and the Claude Code CLI), pushes it to Cloudflare's | |
| 3 | + | # registry, and opens a pull request that records it in | |
| 4 | + | # services/runner/base.json. Merging that pull request is what rolls it out: | |
| 5 | + | # the next deploy builds the runner's image on it, in seconds. | |
| 6 | + | # | |
| 7 | + | # Weekly, for security updates and new stable toolchains; when the base's | |
| 8 | + | # folder changes on main (a change that forgot to rebuild it); and by hand. | |
| 9 | + | # | |
| 10 | + | # It needs Docker, which g1t's own sandboxes do not have, so it runs on a | |
| 11 | + | # self-hosted runner with the `docker` label. Until one is registered, run | |
| 12 | + | # the same thing by hand on a machine with Docker: | |
| 13 | + | # | |
| 14 | + | # node scripts/deploy.mjs build-base | |
| 15 | + | # | |
| 16 | + | # and commit services/runner/base.json. docs/DEPLOYING.md explains both. | |
| 17 | + | name: Runner base image | |
| 18 | + | ||
| 19 | + | on: | |
| 20 | + | schedule: | |
| 21 | + | - cron: "17 6 * * 1" | |
| 22 | + | push: | |
| 23 | + | branches: [main] | |
| 24 | + | paths: | |
| 25 | + | - services/runner/base/** | |
| 26 | + | workflow_dispatch: | |
| 27 | + | inputs: | |
| 28 | + | no_cache: | |
| 29 | + | description: "Build every layer again, ignoring the previous base" | |
| 30 | + | type: boolean | |
| 31 | + | default: false | |
| 32 | + | ||
| 33 | + | concurrency: | |
| 34 | + | group: runner-base | |
| 35 | + | cancel-in-progress: false | |
| 36 | + | ||
| 37 | + | env: | |
| 38 | + | CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }} | |
| 39 | + | WRANGLER_SEND_METRICS: "false" | |
| 40 | + | ||
| 41 | + | jobs: | |
| 42 | + | build: | |
| 43 | + | name: Build and push the base | |
| 44 | + | runs-on: [self-hosted, docker] | |
| 45 | + | environment: production | |
| 46 | + | timeout-minutes: 60 | |
| 47 | + | steps: | |
| 48 | + | - uses: actions/checkout@v5 | |
| 49 | + | - name: Install Wrangler | |
| 50 | + | run: npm ci --workspaces=false --no-audit --no-fund | |
| 51 | + | - name: Build and push | |
| 52 | + | env: | |
| 53 | + | CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | |
| 54 | + | CI: "true" | |
| 55 | + | NO_CACHE: ${{ inputs.no_cache }} | |
| 56 | + | run: | | |
| 57 | + | args=() | |
| 58 | + | if [ "$NO_CACHE" = "true" ]; then args+=(--no-cache); fi | |
| 59 | + | node scripts/deploy.mjs build-base "${args[@]}" | |
| 60 | + | # The runner's own image on the new base, so the deploy after the | |
| 61 | + | # merge finds it in the registry instead of building it. | |
| 62 | + | - name: Build and push the runner's image on it | |
| 63 | + | env: | |
| 64 | + | CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | |
| 65 | + | CI: "true" | |
| 66 | + | run: node scripts/deploy.mjs image | |
| 67 | + | - name: Open a pull request with base.json | |
| 68 | + | env: | |
| 69 | + | G1T_TOKEN: ${{ github.token }} | |
| 70 | + | REPO: ${{ github.repository }} | |
| 71 | + | run: | | |
| 72 | + | if git diff --quiet -- services/runner/base.json; then | |
| 73 | + | echo "The base is unchanged." | |
| 74 | + | exit 0 | |
| 75 | + | fi | |
| 76 | + | branch="runner-base/$(date -u +%Y%m%d-%H%M)" | |
| 77 | + | git config user.name "g1t" | |
| 78 | + | git config user.email "actions@g1t.sh" | |
| 79 | + | git checkout -b "$branch" | |
| 80 | + | git add services/runner/base.json | |
| 81 | + | git commit -m "A new base image for g1t's sandboxes" | |
| 82 | + | git push origin "$branch" | |
| 83 | + | tag=$(node -e 'console.log(require("./services/runner/base.json").image.split(":").pop())') | |
| 84 | + | body=$(node -e 'const b=require("./services/runner/base.json"); console.log("Built and pushed by the Runner base image workflow.\n\n| | |\n| --- | --- |\n" + Object.entries(b.versions).map(([k,v]) => `| ${k} | ${v} |`).join("\n") + `\n| size | ${(b.size_bytes/1e9).toFixed(2)} GB unpacked |`)') | |
| 85 | + | curl -fsS -X POST "https://api.g1t.sh/repos/$REPO/pulls" \ | |
| 86 | + | -H "Authorization: Bearer $G1T_TOKEN" -H "Content-Type: application/json" \ | |
| 87 | + | -d "$(node -e 'console.log(JSON.stringify({ title: `Runner base image ${process.argv[1]}`, branch: process.argv[2], body: process.argv[3] }))' "$tag" "$branch" "$body")" |
| 1 | + | # Releases the self-hosted runner, g1t-runner (crates/runner): builds it for | |
| 2 | + | # Linux, macOS and Windows on x64 and arm64, signs the release, publishes it | |
| 3 | + | # to g1t.sh/downloads/runner/ (the g1t-downloads R2 bucket), and pushes its | |
| 4 | + | # container image. Runners already out there update themselves to it. | |
| 5 | + | # | |
| 6 | + | # A release is a tag `runner-v<version>`, where the version is the one in | |
| 7 | + | # crates/runner/Cargo.toml; or run it by hand. scripts/runner-release.mjs | |
| 8 | + | # does the work; docs/DEPLOYING.md, "The self-hosted runner", says how to | |
| 9 | + | # make the release key the first time. | |
| 10 | + | name: Runner release | |
| 11 | + | ||
| 12 | + | on: | |
| 13 | + | push: | |
| 14 | + | tags: ["runner-v*"] | |
| 15 | + | workflow_dispatch: | |
| 16 | + | ||
| 17 | + | concurrency: | |
| 18 | + | group: runner-release | |
| 19 | + | cancel-in-progress: false | |
| 20 | + | ||
| 21 | + | env: | |
| 22 | + | CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }} | |
| 23 | + | WRANGLER_SEND_METRICS: "false" | |
| 24 | + | ||
| 25 | + | jobs: | |
| 26 | + | binaries: | |
| 27 | + | name: Build, sign and publish | |
| 28 | + | runs-on: ubuntu-latest | |
| 29 | + | environment: production | |
| 30 | + | timeout-minutes: 60 | |
| 31 | + | steps: | |
| 32 | + | - uses: actions/checkout@v5 | |
| 33 | + | - name: The tag names this version | |
| 34 | + | if: startsWith(github.ref, 'refs/tags/runner-v') | |
| 35 | + | run: | | |
| 36 | + | version="$(sed -n 's/^version = "\(.*\)"/\1/p' crates/runner/Cargo.toml | head -1)" | |
| 37 | + | [ "runner-v$version" = "${GITHUB_REF_NAME}" ] || { echo "::error::The tag is ${GITHUB_REF_NAME}, but crates/runner is $version"; exit 1; } | |
| 38 | + | - name: Install zig and cargo-zigbuild | |
| 39 | + | run: | | |
| 40 | + | pip install --user ziglang==0.13.0 | |
| 41 | + | echo "$HOME/.local/bin" >> "$GITHUB_PATH" | |
| 42 | + | cargo install --locked cargo-zigbuild | |
| 43 | + | - uses: actions/cache@v4 | |
| 44 | + | with: | |
| 45 | + | path: | | |
| 46 | + | ~/.cargo/registry | |
| 47 | + | target | |
| 48 | + | key: runner-release-${{ hashFiles('Cargo.lock') }} | |
| 49 | + | - name: Build every platform | |
| 50 | + | env: | |
| 51 | + | G1T_RUNNER_RELEASE_KEY: ${{ vars.G1T_RUNNER_RELEASE_KEY }} | |
| 52 | + | RUNNER_AGENT_IMAGE: ${{ vars.RUNNER_AGENT_IMAGE }} | |
| 53 | + | run: node scripts/runner-release.mjs build | |
| 54 | + | - name: Sign | |
| 55 | + | env: | |
| 56 | + | RUNNER_RELEASE_KEY: ${{ secrets.RUNNER_RELEASE_KEY }} | |
| 57 | + | G1T_RUNNER_RELEASE_KEY: ${{ vars.G1T_RUNNER_RELEASE_KEY }} | |
| 58 | + | run: | | |
| 59 | + | node scripts/runner-release.mjs sign | |
| 60 | + | node scripts/runner-release.mjs verify | |
| 61 | + | - name: Install Wrangler | |
| 62 | + | run: npm ci --workspaces=false --no-audit --no-fund | |
| 63 | + | - name: Publish to g1t.sh/downloads/runner | |
| 64 | + | env: | |
| 65 | + | CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | |
| 66 | + | run: node scripts/runner-release.mjs publish | |
| 67 | + | - uses: actions/upload-artifact@v4 | |
| 68 | + | with: | |
| 69 | + | name: linux-binaries | |
| 70 | + | path: | | |
| 71 | + | target/runner-release/*/g1t-runner-linux-x64 | |
| 72 | + | target/runner-release/*/g1t-runner-linux-arm64 | |
| 73 | + | ||
| 74 | + | image: | |
| 75 | + | name: Container image | |
| 76 | + | needs: binaries | |
| 77 | + | # Needs Docker, which g1t's own sandboxes do not have. | |
| 78 | + | runs-on: [self-hosted, docker] | |
| 79 | + | environment: production | |
| 80 | + | timeout-minutes: 30 | |
| 81 | + | steps: | |
| 82 | + | - uses: actions/checkout@v5 | |
| 83 | + | - uses: actions/download-artifact@v4 | |
| 84 | + | with: | |
| 85 | + | name: linux-binaries | |
| 86 | + | path: release | |
| 87 | + | - name: Build and push for amd64 and arm64 | |
| 88 | + | env: | |
| 89 | + | REGISTRY_USER: ${{ vars.RUNNER_IMAGE_REGISTRY_USER }} | |
| 90 | + | REGISTRY_TOKEN: ${{ secrets.RUNNER_IMAGE_REGISTRY_TOKEN }} | |
| 91 | + | IMAGE: ${{ vars.RUNNER_IMAGE }} | |
| 92 | + | run: | | |
| 93 | + | version="$(sed -n 's/^version = "\(.*\)"/\1/p' crates/runner/Cargo.toml | head -1)" | |
| 94 | + | echo "$REGISTRY_TOKEN" | docker login --username "$REGISTRY_USER" --password-stdin | |
| 95 | + | for arch in amd64 arm64; do | |
| 96 | + | mkdir -p "context-$arch" | |
| 97 | + | cp deploy/runner/Dockerfile "context-$arch/" | |
| 98 | + | name="g1t-runner-linux-$([ "$arch" = amd64 ] && echo x64 || echo arm64)" | |
| 99 | + | cp release/*/"$name" "context-$arch/g1t-runner" | |
| 100 | + | docker buildx build --platform "linux/$arch" -t "$IMAGE:$version-$arch" --push "context-$arch" | |
| 101 | + | done | |
| 102 | + | docker buildx imagetools create -t "$IMAGE:$version" -t "$IMAGE:latest" "$IMAGE:$version-amd64" "$IMAGE:$version-arm64" |
| 9 | 9 | .dev.vars* | |
| 10 | 10 | ||
| 11 | 11 | .env* | |
| 12 | + | ||
| 13 | + | # Generated by scripts/deploy.mjs for a runner deploy (the image by reference) | |
| 14 | + | wrangler.deploy.json |
| 1 | 1 | # Contributing to g1t | |
| 2 | 2 | ||
| 3 | 3 | g1t is built the way it asks others to build: issues and pull requests on | |
| 4 | − | [g1t.sh/flagon-io/g1t](https://g1t.sh/flagon-io/g1t), checks that prove a change | |
| 5 | − | done, and a merge queue that keeps `main` passing. | |
| 4 | + | [g1t.sh/flagon-io/g1t](https://g1t.sh/flagon-io/g1t), required checks that prove | |
| 5 | + | a change works, and a merge queue that keeps `main` passing. | |
| 6 | 6 | ||
| 7 | 7 | ## A change ships with its docs | |
| 8 | 8 |
| 992 | 992 | "g1t-kit", | |
| 993 | 993 | "g1t-scan", | |
| 994 | 994 | "g1t-secrets", | |
| 995 | + | "miniz_oxide", | |
| 995 | 996 | "serde", | |
| 996 | 997 | "serde_json", | |
| 997 | 998 | "similar", | |
| 1004 | 1005 | dependencies = [ | |
| 1005 | 1006 | "anyhow", | |
| 1006 | 1007 | "base64 0.22.1", | |
| 1008 | + | "ed25519-dalek", | |
| 1007 | 1009 | "g1t-actions", | |
| 1008 | 1010 | "hex", | |
| 1009 | 1011 | "serde", |
| 55 | 55 | integrations, so nothing needs a shared service account. | |
| 56 | 56 | - Public and private repositories, and git over HTTPS, including creating a | |
| 57 | 57 | repository by pushing to it. | |
| 58 | − | - Issues with labels, acceptance checks and comments. | |
| 58 | + | - Issues with labels and comments; a description can say what done means, | |
| 59 | + | under a Definition of done. | |
| 59 | 60 | - Pull requests with a diff and a recorded agent session: in a | |
| 60 | 61 | copy-on-write fork, which is how agents work, or from a branch pushed to | |
| 61 | 62 | the repository. Several can be made for one issue. | |
| 62 | − | - Acceptance checks: an issue's commands are run against each pull request | |
| 63 | − | in a clean sandbox, by g1t and not by the agent being checked, and gate | |
| 64 | − | the merge. | |
| 63 | + | - Checks: the repository's workflows run on every pull request, a | |
| 64 | + | person's or an agent's, and report a check each. The default branch | |
| 65 | + | names the required checks a merge needs; an agent whose change fails a | |
| 66 | + | check is sent back with the failing jobs' logs. A repository with no | |
| 67 | + | workflows gets a starter CI workflow in one click. | |
| 65 | 68 | - Review: comments on lines of a change, and approve or request-changes | |
| 66 | 69 | verdicts, from people and from agents. | |
| 67 | 70 | - Overlap: each pull request shows which others in progress change the | |
| 128 | 131 | | `services/repos` | Repository registry, contents, forks, diffs, landing, git over HTTPS. Rust. | | |
| 129 | 132 | | `services/work` | Issues, pull requests, reviews, check runs and sessions. Rust. | | |
| 130 | 133 | | `services/events` | The event bus and its log. Rust. | | |
| 131 | − | | `services/runner` | Starts sandboxes: for g1t agents, and for acceptance checks. | | |
| 134 | + | | `services/runner` | Starts sandboxes: for g1t agents, workflow jobs and the merge queue. | | |
| 132 | 135 | | `services/og` | Social cards at `og.g1t.sh`: a PNG per page, showing only what anyone may see. | | |
| 133 | − | | `crates/runner` | The program inside a sandbox: runs an agent, or a set of checks, and reports back. Rust. | | |
| 136 | + | | `crates/runner` | The program inside a sandbox: runs an agent, a workflow job or a merge queue build, and reports back. Rust. | | |
| 134 | 137 | | `crates/contracts` | Types and service interfaces for the Rust services. | | |
| 135 | 138 | | `crates/kit` | Plumbing shared by Rust services on Workers. | | |
| 136 | 139 | | `crates/sshd` | Git over SSH, bridged to Artifacts. Not deployed yet. | |
| 1 | − | //! Artifacts and the cache of GitHub Actions jobs, kept in Workers KV in | |
| 2 | − | //! chunks, with KV's own expiry: artifacts for 14 days with their run, | |
| 3 | − | //! cache entries for 7 days with their repository. | |
| 1 | + | //! Artifacts and the cache of GitHub Actions jobs. | |
| 2 | + | //! | |
| 3 | + | //! Artifacts are kept in Workers KV in chunks, with KV's own expiry: 14 | |
| 4 | + | //! days with their run. Cache entries are kept in R2 (ACTIONS_CACHE), up | |
| 5 | + | //! to 2 GB each, uploaded in parts; the actions service lists them and | |
| 6 | + | //! decides what is found, what fits and what is evicted | |
| 7 | + | //! (services/actions/src/cache.rs). Entries saved in KV before the cache | |
| 8 | + | //! moved are still found there until they expire. | |
| 9 | + | //! | |
| 10 | + | //! A sandbox reaches these with its job's token: | |
| 11 | + | //! | |
| 12 | + | //! - `GET /actions/jobs/{job}/artifacts`, `PUT|GET .../artifacts/{name}` | |
| 13 | + | //! - `GET .../cache?key=&restore=`: the entry, streamed, its key in `x-g1t-key` | |
| 14 | + | //! - `POST .../cache/uploads?key=&size=`: `{ id, upload, part_bytes }` | |
| 15 | + | //! - `PUT .../cache/uploads/{id}/{part}?upload=`: one part, `{ part, etag }` | |
| 16 | + | //! - `POST .../cache/uploads/{id}/complete?upload=` with `{ size, parts }` | |
| 17 | + | //! - `DELETE .../cache/uploads/{id}?upload=`: gives the upload up | |
| 18 | + | //! - `PUT .../cache?key=`: a whole entry of at most 60 MB at once (older runners) | |
| 4 | 19 | //! | |
| 5 | − | //! A sandbox reaches these with its job's token, at | |
| 6 | − | //! `/actions/jobs/{job}/artifacts[/{name}]` and `/actions/jobs/{job}/cache`. | |
| 7 | 20 | //! People download an artifact at | |
| 8 | 21 | //! `/repos/{owner}/{repo}/actions/runs/{run}/artifacts/{name}`. | |
| 9 | 22 | ||
| 10 | 23 | use serde::{Deserialize, Serialize}; | |
| 11 | 24 | use serde_json::{Value, json}; | |
| 12 | 25 | use worker::kv::KvStore; | |
| 13 | − | use worker::{Env, Request, Response, Result}; | |
| 26 | + | use worker::{Bucket, Env, Request, Response, Result, UploadedPart}; | |
| 14 | 27 | ||
| 28 | + | use g1t_contracts::actions::{ | |
| 29 | + | CACHE_PART_BYTES, CacheAbortArgs, CacheCommitArgs, CacheCommitted, CacheHit, CacheLookupArgs, CacheReservation, CacheReserveArgs, | |
| 30 | + | }; | |
| 15 | 31 | use g1t_contracts::{FailureCode, Outcome}; | |
| 16 | 32 | ||
| 17 | 33 | use crate::operations::Services; | |
| 21 | 37 | /// The largest artifact or cache entry, kept within a Worker's memory. | |
| 22 | 38 | const MAX_BYTES: usize = 60 * 1024 * 1024; | |
| 23 | 39 | const ARTIFACT_TTL: u64 = 14 * 24 * 60 * 60; | |
| 24 | − | const CACHE_TTL: u64 = 7 * 24 * 60 * 60; | |
| 25 | 40 | ||
| 26 | 41 | #[derive(Serialize, Deserialize)] | |
| 27 | 42 | struct Meta { | |
| 182 | 197 | None => error(404, "No such artifact."), | |
| 183 | 198 | } | |
| 184 | 199 | } | |
| 185 | − | (_, "cache") => { | |
| 200 | + | (_, what) if what == "cache" || what.starts_with("cache/") => { | |
| 201 | + | let bucket = env.bucket("ACTIONS_CACHE")?; | |
| 202 | + | cache(request, &kv, &bucket, services, method, job, &token, &repo, what).await | |
| 203 | + | } | |
| 204 | + | _ => error(404, "No such endpoint."), | |
| 205 | + | } | |
| 206 | + | } | |
| 207 | + | ||
| 208 | + | /// A part's number and the etag R2 gave it. | |
| 209 | + | #[derive(Deserialize)] | |
| 210 | + | struct Part { | |
| 211 | + | part: u16, | |
| 212 | + | etag: String, | |
| 213 | + | } | |
| 214 | + | ||
| 215 | + | #[derive(Deserialize)] | |
| 216 | + | struct Complete { | |
| 217 | + | size: u64, | |
| 218 | + | parts: Vec<Part>, | |
| 219 | + | } | |
| 220 | + | ||
| 221 | + | /// An outcome of the actions service, or its failure as the reply it means. | |
| 222 | + | fn refused<T>(outcome: Outcome<T>) -> std::result::Result<T, Result<Response>> { | |
| 223 | + | match outcome { | |
| 224 | + | Outcome::Ok(value) => Ok(value), | |
| 225 | + | Outcome::Fail(failure) => { | |
| 226 | + | let status = match failure.code { | |
| 227 | + | FailureCode::Unauthenticated => 401, | |
| 228 | + | FailureCode::NotFound => 404, | |
| 229 | + | FailureCode::Conflict => 409, | |
| 230 | + | FailureCode::Forbidden => 403, | |
| 231 | + | _ => 400, | |
| 232 | + | }; | |
| 233 | + | Err(error(status, &failure.message)) | |
| 234 | + | } | |
| 235 | + | } | |
| 236 | + | } | |
| 237 | + | ||
| 238 | + | /// The cache: restoring, uploading in parts, and the older whole upload. | |
| 239 | + | #[allow(clippy::too_many_arguments)] | |
| 240 | + | async fn cache( | |
| 241 | + | mut request: Request, | |
| 242 | + | kv: &KvStore, | |
| 243 | + | bucket: &Bucket, | |
| 244 | + | services: &Services, | |
| 245 | + | method: &str, | |
| 246 | + | job: &str, | |
| 247 | + | token: &str, | |
| 248 | + | repo: &str, | |
| 249 | + | what: &str, | |
| 250 | + | ) -> Result<Response> { | |
| 251 | + | let parts: Vec<&str> = what.split('/').collect(); | |
| 252 | + | let upload_id = query(&request, "upload").unwrap_or_default(); | |
| 253 | + | match (method, parts.as_slice()) { | |
| 254 | + | ("GET", ["cache"]) => { | |
| 255 | + | let key = query(&request, "key").unwrap_or_default(); | |
| 256 | + | let restore: Vec<String> = | |
| 257 | + | query(&request, "restore").unwrap_or_default().lines().map(str::trim).filter(|p| !p.is_empty()).map(str::to_owned).collect(); | |
| 258 | + | let found: Outcome<Option<CacheHit>> = g1t_kit::call( | |
| 259 | + | &services.actions, | |
| 260 | + | "cache_lookup", | |
| 261 | + | &CacheLookupArgs { job: job.to_owned(), token: token.to_owned(), key: key.clone(), restore: restore.clone() }, | |
| 262 | + | ) | |
| 263 | + | .await?; | |
| 264 | + | let found = match refused(found) { | |
| 265 | + | Ok(found) => found, | |
| 266 | + | Err(reply) => return reply, | |
| 267 | + | }; | |
| 268 | + | if let Some(hit) = found | |
| 269 | + | && let Some(object) = bucket.get(&hit.object).execute().await? | |
| 270 | + | && let Some(body) = object.body() | |
| 271 | + | { | |
| 272 | + | let mut response = Response::from_body(body.response_body()?)?; | |
| 273 | + | let headers = response.headers_mut(); | |
| 274 | + | headers.set("x-g1t-key", &hit.key)?; | |
| 275 | + | headers.set("content-length", &object.size().to_string())?; | |
| 276 | + | headers.set("content-type", "application/octet-stream")?; | |
| 277 | + | return Ok(response); | |
| 278 | + | } | |
| 279 | + | // Entries saved in KV before the cache moved to R2. | |
| 280 | + | kv_lookup(kv, repo, &key, &restore).await | |
| 281 | + | } | |
| 282 | + | // Older runners send a whole entry of at most 60 MB at once. | |
| 283 | + | ("PUT", ["cache"]) => { | |
| 186 | 284 | let key = query(&request, "key").unwrap_or_default(); | |
| 187 | − | if key.is_empty() || key.len() > 400 { | |
| 188 | − | return error(400, "A cache key is 1 to 400 characters."); | |
| 285 | + | let bytes = request.bytes().await?; | |
| 286 | + | if bytes.len() > MAX_BYTES { | |
| 287 | + | return error(413, "An entry sent at once is at most 60 MB; newer runners upload it in parts."); | |
| 189 | 288 | } | |
| 190 | − | if method == "PUT" { | |
| 191 | − | let base = format!("c/{repo}/{key}"); | |
| 192 | − | // A key is written once, as on GitHub. | |
| 193 | − | if kv.get(&base).text().await?.is_some() { | |
| 194 | − | return crate::reply(&json!({ "saved": false, "reason": "That key is already cached." })); | |
| 195 | − | } | |
| 196 | − | let bytes = request.bytes().await?; | |
| 197 | − | if bytes.len() > MAX_BYTES { | |
| 198 | − | return error(413, "Cache entries are at most 60 MB."); | |
| 289 | + | let reserved: Outcome<CacheReservation> = g1t_kit::call( | |
| 290 | + | &services.actions, | |
| 291 | + | "cache_reserve", | |
| 292 | + | &CacheReserveArgs { job: job.to_owned(), token: token.to_owned(), key, size: bytes.len() as u64 }, | |
| 293 | + | ) | |
| 294 | + | .await?; | |
| 295 | + | let reserved = match reserved { | |
| 296 | + | Outcome::Fail(failure) if failure.code == FailureCode::Conflict => { | |
| 297 | + | return crate::reply(&json!({ "saved": false, "reason": failure.message })); | |
| 199 | 298 | } | |
| 200 | − | put(&kv, &base, &key, &bytes, CACHE_TTL).await?; | |
| 201 | − | return crate::reply(&json!({ "saved": true })); | |
| 299 | + | other => match refused(other) { | |
| 300 | + | Ok(reserved) => reserved, | |
| 301 | + | Err(reply) => return reply, | |
| 302 | + | }, | |
| 303 | + | }; | |
| 304 | + | let size = bytes.len() as u64; | |
| 305 | + | bucket.put(&reserved.object, bytes).execute().await?; | |
| 306 | + | commit(bucket, services, job, token, &reserved.id, size).await?; | |
| 307 | + | crate::reply(&json!({ "saved": true })) | |
| 308 | + | } | |
| 309 | + | ("POST", ["cache", "uploads"]) => { | |
| 310 | + | let key = query(&request, "key").unwrap_or_default(); | |
| 311 | + | let size = query(&request, "size").and_then(|s| s.parse::<u64>().ok()).unwrap_or(0); | |
| 312 | + | let reserved: Outcome<CacheReservation> = g1t_kit::call( | |
| 313 | + | &services.actions, | |
| 314 | + | "cache_reserve", | |
| 315 | + | &CacheReserveArgs { job: job.to_owned(), token: token.to_owned(), key, size }, | |
| 316 | + | ) | |
| 317 | + | .await?; | |
| 318 | + | let reserved = match refused(reserved) { | |
| 319 | + | Ok(reserved) => reserved, | |
| 320 | + | Err(reply) => return reply, | |
| 321 | + | }; | |
| 322 | + | let upload = bucket.create_multipart_upload(&reserved.object).execute().await?; | |
| 323 | + | crate::reply(&json!({ "id": reserved.id, "upload": upload.upload_id().await, "part_bytes": CACHE_PART_BYTES })) | |
| 324 | + | } | |
| 325 | + | ("PUT", ["cache", "uploads", id, part]) => { | |
| 326 | + | let part = part.parse::<u16>().unwrap_or(0); | |
| 327 | + | if part == 0 || upload_id.is_empty() { | |
| 328 | + | return error(400, "A part is numbered from 1, and names its upload."); | |
| 202 | 329 | } | |
| 203 | − | // The exact key, else the newest entry under each restore key. | |
| 204 | − | let exact = format!("c/{repo}/{key}"); | |
| 205 | − | if let Some(bytes) = get(&kv, &exact).await? { | |
| 206 | − | let mut response = Response::from_bytes(bytes)?; | |
| 207 | − | response.headers_mut().set("x-g1t-key", &key)?; | |
| 208 | − | return Ok(response); | |
| 330 | + | let length = request.headers().get("content-length")?.and_then(|l| l.parse::<u64>().ok()).unwrap_or(0); | |
| 331 | + | if length == 0 || length > CACHE_PART_BYTES { | |
| 332 | + | return error(413, &format!("A part is 1 to {} MB, with its length.", CACHE_PART_BYTES / 1_048_576)); | |
| 209 | 333 | } | |
| 210 | − | for prefix in query(&request, "restore").unwrap_or_default().lines().map(str::trim).filter(|p| !p.is_empty()) { | |
| 211 | − | if let Some((base, meta)) = list(&kv, &format!("c/{repo}/{prefix}")).await?.into_iter().next() | |
| 212 | − | && let Some(bytes) = get(&kv, &base).await? | |
| 213 | − | { | |
| 214 | − | let mut response = Response::from_bytes(bytes)?; | |
| 215 | − | response.headers_mut().set("x-g1t-key", &meta.name)?; | |
| 216 | − | return Ok(response); | |
| 217 | − | } | |
| 334 | + | // The body goes to R2 as it comes, never held whole here. | |
| 335 | + | let Some(body) = request.inner().body() else { return error(400, "The part is empty.") }; | |
| 336 | + | let upload = bucket.resume_multipart_upload(object_of(repo, id), &upload_id)?; | |
| 337 | + | let uploaded = upload.upload_part(part, body).await?; | |
| 338 | + | crate::reply(&json!({ "part": uploaded.part_number(), "etag": uploaded.etag() })) | |
| 339 | + | } | |
| 340 | + | ("POST", ["cache", "uploads", id, "complete"]) => { | |
| 341 | + | let done: Complete = match request.json().await { | |
| 342 | + | Ok(done) => done, | |
| 343 | + | Err(_) => return error(400, "Send { size, parts: [{ part, etag }] }."), | |
| 344 | + | }; | |
| 345 | + | let upload = bucket.resume_multipart_upload(object_of(repo, id), &upload_id)?; | |
| 346 | + | let mut parts = done.parts; | |
| 347 | + | parts.sort_by_key(|p| p.part); | |
| 348 | + | if let Err(problem) = upload.complete(parts.into_iter().map(|p| UploadedPart::new(p.part, p.etag))).await { | |
| 349 | + | let _ = abort(services, job, token, id).await; | |
| 350 | + | return error(400, &format!("The upload could not be completed: {problem}")); | |
| 351 | + | } | |
| 352 | + | commit(bucket, services, job, token, id, done.size).await?; | |
| 353 | + | crate::reply(&json!({ "saved": true })) | |
| 354 | + | } | |
| 355 | + | ("DELETE", ["cache", "uploads", id]) => { | |
| 356 | + | if let Ok(upload) = bucket.resume_multipart_upload(object_of(repo, id), &upload_id) { | |
| 357 | + | let _ = upload.abort().await; | |
| 218 | 358 | } | |
| 219 | − | error(404, "Nothing cached under those keys.") | |
| 359 | + | abort(services, job, token, id).await?; | |
| 360 | + | crate::reply(&json!({ "aborted": true })) | |
| 220 | 361 | } | |
| 221 | 362 | _ => error(404, "No such endpoint."), | |
| 222 | 363 | } | |
| 223 | 364 | } | |
| 224 | 365 | ||
| 366 | + | /// Where an entry is in R2: under its repository, by its id, as the | |
| 367 | + | /// actions service named it when it was reserved. | |
| 368 | + | fn object_of(repo: &str, id: &str) -> String { | |
| 369 | + | format!("c/{repo}/{id}") | |
| 370 | + | } | |
| 371 | + | ||
| 372 | + | /// Marks an uploaded entry ready, and deletes what that evicted. | |
| 373 | + | async fn commit(bucket: &Bucket, services: &Services, job: &str, token: &str, id: &str, size: u64) -> Result<()> { | |
| 374 | + | let committed: Outcome<CacheCommitted> = g1t_kit::call( | |
| 375 | + | &services.actions, | |
| 376 | + | "cache_commit", | |
| 377 | + | &CacheCommitArgs { job: job.to_owned(), token: token.to_owned(), id: id.to_owned(), size }, | |
| 378 | + | ) | |
| 379 | + | .await?; | |
| 380 | + | if let Outcome::Ok(committed) = committed | |
| 381 | + | && !committed.evicted.is_empty() | |
| 382 | + | { | |
| 383 | + | bucket.delete_multiple(committed.evicted.iter().map(String::as_str).collect()).await?; | |
| 384 | + | } | |
| 385 | + | Ok(()) | |
| 386 | + | } | |
| 387 | + | ||
| 388 | + | async fn abort(services: &Services, job: &str, token: &str, id: &str) -> Result<()> { | |
| 389 | + | let _: Outcome<bool> = g1t_kit::call( | |
| 390 | + | &services.actions, | |
| 391 | + | "cache_abort", | |
| 392 | + | &CacheAbortArgs { job: job.to_owned(), token: token.to_owned(), id: id.to_owned() }, | |
| 393 | + | ) | |
| 394 | + | .await?; | |
| 395 | + | Ok(()) | |
| 396 | + | } | |
| 397 | + | ||
| 398 | + | /// An entry saved in KV before the cache moved to R2, by key or restore key. | |
| 399 | + | async fn kv_lookup(kv: &KvStore, repo: &str, key: &str, restore: &[String]) -> Result<Response> { | |
| 400 | + | // The exact key, else the newest entry under each restore key. | |
| 401 | + | if let Some(bytes) = get(kv, &format!("c/{repo}/{key}")).await? { | |
| 402 | + | let mut response = Response::from_bytes(bytes)?; | |
| 403 | + | response.headers_mut().set("x-g1t-key", key)?; | |
| 404 | + | return Ok(response); | |
| 405 | + | } | |
| 406 | + | for prefix in restore { | |
| 407 | + | if let Some((base, meta)) = list(kv, &format!("c/{repo}/{prefix}")).await?.into_iter().next() | |
| 408 | + | && let Some(bytes) = get(kv, &base).await? | |
| 409 | + | { | |
| 410 | + | let mut response = Response::from_bytes(bytes)?; | |
| 411 | + | response.headers_mut().set("x-g1t-key", &meta.name)?; | |
| 412 | + | return Ok(response); | |
| 413 | + | } | |
| 414 | + | } | |
| 415 | + | error(404, "Nothing cached under those keys.") | |
| 416 | + | } | |
| 417 | + | ||
| 225 | 418 | /// Someone who can see the run downloading one of its artifacts. | |
| 226 | 419 | pub async fn download(env: &Env, services: &Services, viewer: &g1t_contracts::Viewer, owner: &str, repo: &str, run: &str, name: &str) -> Result<Response> { | |
| 227 | 420 | let seen: Outcome<Value> = g1t_kit::call( |
| 14 | 14 | #[cfg(test)] | |
| 15 | 15 | mod responses; | |
| 16 | 16 | mod rest; | |
| 17 | + | mod runners; | |
| 17 | 18 | mod tools; | |
| 18 | 19 | ||
| 19 | 20 | use g1t_contracts::billing::FinishRunArgs; | |
| 240 | 241 | }) | |
| 241 | 242 | } | |
| 242 | 243 | ||
| 243 | − | /// A sandbox reporting on its run of a pull request's acceptance checks. | |
| 244 | + | /// A sandbox reporting on a run of an issue's commands, from before a pull | |
| 245 | + | /// request's checks were the workflows run on it. | |
| 244 | 246 | /// The run's own token, in the body, is the credential: it was given to | |
| 245 | 247 | /// that sandbox and to nothing else. | |
| 246 | 248 | async fn report_checks( | |
| 469 | 471 | // g1t token either. | |
| 470 | 472 | if !on_mcp | |
| 471 | 473 | && let Some(rest) = path.strip_prefix("/actions/jobs/") | |
| 472 | − | && (rest.contains("/artifacts") || rest.ends_with("/cache")) | |
| 474 | + | && (rest.contains("/artifacts") || rest.ends_with("/cache") || rest.contains("/cache/uploads")) | |
| 473 | 475 | { | |
| 474 | 476 | let rest = rest.to_owned(); | |
| 475 | 477 | return blobs::for_job(request, env, &services, method, &rest).await; | |
| 476 | 478 | } | |
| 477 | 479 | ||
| 480 | + | // A self-hosted runner, with a registration token or its own | |
| 481 | + | // credential, neither of which is a g1t access token. | |
| 482 | + | if method == "POST" | |
| 483 | + | && !on_mcp | |
| 484 | + | && path.starts_with("/runners/") | |
| 485 | + | && let Some(response) = runners::handle(&mut request, &services, &path).await? | |
| 486 | + | { | |
| 487 | + | return Ok(response); | |
| 488 | + | } | |
| 489 | + | ||
| 478 | 490 | let viewer = match authenticate(&request, &services).await? { | |
| 479 | 491 | Ok(viewer) => viewer, | |
| 480 | 492 | Err(refused) => return Ok(refused), |
| 14 | 14 | const INSTRUCTIONS: &str = "g1t is a git forge where people and agents work through issues and pull requests. Repositories are named \"owner/name\"; issues and pull requests in one share a sequence of numbers. | |
| 15 | 15 | Tools are resources, each with an `action`: search, repository, issue, pull_request, agent, plan, memory, workflow, secret, webhook, access, workspace, account. The `action` field lists each action and the fields it needs. You see only what your token's scopes allow; a refusal names the scope it needs. | |
| 16 | 16 | Find a repository: account whoami lists your workspaces; repository list or search finds one. | |
| 17 | − | Work on an issue: issue get (read it and the pull requests already made for it), memory recall, then pull_request create with the issue's number: you get a draft with its own fork to clone and push to. Record your reasoning with pull_request record_session as you go, push, then pull_request ready with a summary. Watch `overlaps` and `behind` on pull_request get, and read the acceptance checks' output there; push a fix if they fail. | |
| 17 | + | Work on an issue: issue get (read it and the pull requests already made for it), memory recall, then pull_request create with the issue's number: you get a draft with its own fork to clone and push to. Record your reasoning with pull_request record_session as you go, push, then pull_request ready with a summary. Watch `overlaps` and `behind` on pull_request get, and its checks there: `statuses` from the repository's workflows and `required_checks`, which must pass before it merges. If one fails, read why with workflow get_run and job_logs, push a fix, and the checks run again. | |
| 18 | 18 | Hand work to g1t's agent: agent delegate opens an issue and starts it in one step; agent assign starts it on an existing issue. Each costs the workspace money. | |
| 19 | 19 | When you learn something the next agent needs, memory remember it (scope project or workspace). Never a secret."; | |
| 20 | 20 |
| 55 | 55 | Op::PurgeRepo, | |
| 56 | 56 | Op::GetRepoSettings, | |
| 57 | 57 | Op::UpdateRepoSettings, | |
| 58 | + | Op::ListCheckNames, | |
| 58 | 59 | Op::ListEvents, | |
| 59 | 60 | ], | |
| 60 | 61 | ), | |
| 162 | 163 | ], | |
| 163 | 164 | ), | |
| 164 | 165 | ( | |
| 166 | + | "Runners", | |
| 167 | + | "Self-hosted runners: your own machines, which run your workflow jobs (and, if you choose, your agents' work) for $0 of g1t compute. They register with a short-lived token and only ever connect out.", | |
| 168 | + | &[ | |
| 169 | + | Op::ListRunners, | |
| 170 | + | Op::CreateRunnerRegistrationToken, | |
| 171 | + | Op::RemoveRunner, | |
| 172 | + | Op::ListRunnerGroups, | |
| 173 | + | Op::CreateRunnerGroup, | |
| 174 | + | Op::UpdateRunnerGroup, | |
| 175 | + | Op::DeleteRunnerGroup, | |
| 176 | + | Op::GetRunnerSettings, | |
| 177 | + | Op::UpdateRunnerSettings, | |
| 178 | + | ], | |
| 179 | + | ), | |
| 180 | + | ( | |
| 165 | 181 | "Webhooks", | |
| 166 | 182 | "Signed HTTPS requests sent to your own address as things happen, for a repository or a whole workspace.", | |
| 167 | 183 | &[ | |
| 230 | 246 | Op::UpdateRepo => "Update a repository", | |
| 231 | 247 | Op::GetRepoSettings => "Get repository settings", | |
| 232 | 248 | Op::UpdateRepoSettings => "Update repository settings", | |
| 249 | + | Op::ListCheckNames => "List check names", | |
| 233 | 250 | Op::GetMergeQueue => "Get the merge queue", | |
| 234 | 251 | Op::MessageAgent => "Message an agent", | |
| 235 | 252 | Op::AnswerMessage => "Answer a message", | |
| 292 | 309 | Op::ListActionsVariables => "List variables", | |
| 293 | 310 | Op::SetActionsVariable => "Set a variable", | |
| 294 | 311 | Op::DeleteActionsVariable => "Delete a variable", | |
| 312 | + | Op::ListRunners => "List self-hosted runners", | |
| 313 | + | Op::ListRunnerGroups => "List runner groups", | |
| 314 | + | Op::GetRunnerSettings => "Get runner settings", | |
| 315 | + | Op::CreateRunnerRegistrationToken => "Create a runner registration token", | |
| 316 | + | Op::RemoveRunner => "Remove a self-hosted runner", | |
| 317 | + | Op::CreateRunnerGroup => "Create a runner group", | |
| 318 | + | Op::UpdateRunnerGroup => "Change a runner group", | |
| 319 | + | Op::DeleteRunnerGroup => "Delete a runner group", | |
| 320 | + | Op::UpdateRunnerSettings => "Change runner settings", | |
| 295 | 321 | Op::ListCollaborators => "List who has access", | |
| 296 | 322 | Op::AddCollaborator => "Add a collaborator", | |
| 297 | 323 | Op::UpdateCollaborator => "Change a collaborator's role", | |
| 828 | 854 | /// after the prefix, as `whsec_…` and `g1t_…` do. | |
| 829 | 855 | #[test] | |
| 830 | 856 | fn examples_hold_no_real_looking_secrets() { | |
| 831 | − | let prefixes = ["whsec_", "g1t_", "sk_live_", "sk_test_", "ghp_", "github_pat_", "xoxb-", "AKIA"]; | |
| 857 | + | let prefixes = ["whsec_", "g1t_", "g1tr_", "g1trt_", "sk_live_", "sk_test_", "ghp_", "github_pat_", "xoxb-", "AKIA"]; | |
| 832 | 858 | for (line, text) in REFERENCE.lines().enumerate() { | |
| 833 | 859 | for prefix in prefixes { | |
| 834 | 860 | let mut rest = text; |
| 92 | 92 | PurgeRepo, | |
| 93 | 93 | GetRepoSettings, | |
| 94 | 94 | UpdateRepoSettings, | |
| 95 | + | ListCheckNames, | |
| 95 | 96 | GetMergeQueue, | |
| 96 | 97 | MessageAgent, | |
| 97 | 98 | AnswerMessage, | |
| 154 | 155 | ListActionsVariables, | |
| 155 | 156 | SetActionsVariable, | |
| 156 | 157 | DeleteActionsVariable, | |
| 158 | + | ListRunners, | |
| 159 | + | ListRunnerGroups, | |
| 160 | + | GetRunnerSettings, | |
| 161 | + | CreateRunnerRegistrationToken, | |
| 162 | + | RemoveRunner, | |
| 163 | + | CreateRunnerGroup, | |
| 164 | + | UpdateRunnerGroup, | |
| 165 | + | DeleteRunnerGroup, | |
| 166 | + | UpdateRunnerSettings, | |
| 157 | 167 | ListCollaborators, | |
| 158 | 168 | AddCollaborator, | |
| 159 | 169 | UpdateCollaborator, | |
| 190 | 200 | call(service, method, args).await | |
| 191 | 201 | } | |
| 192 | 202 | ||
| 203 | + | /// Commands given the deprecated way, as `checks` or `acceptance_checks`. | |
| 204 | + | fn deprecated_checks(input: &Value) -> Vec<String> { | |
| 205 | + | let mut checks = strings(input, "checks").unwrap_or_default(); | |
| 206 | + | checks.extend(strings(input, "acceptance_checks").unwrap_or_default()); | |
| 207 | + | checks.retain(|check| !check.trim().is_empty()); | |
| 208 | + | checks | |
| 209 | + | } | |
| 210 | + | ||
| 211 | + | /// What the response says when `checks` was given: it still works, as | |
| 212 | + | /// words in the issue's body, and what replaced it. | |
| 213 | + | pub(crate) const CHECKS_DEPRECATION: &str = "checks is deprecated: commands are no longer run per issue. They were added to the issue's body under \"Definition of done\". What must pass before a pull request merges is the default branch's required checks: see update_repo_settings (required_checks)."; | |
| 214 | + | ||
| 215 | + | fn with_deprecation(outcome: Outcome<Value>, deprecated: bool) -> Outcome<Value> { | |
| 216 | + | match outcome { | |
| 217 | + | Outcome::Ok(mut value) if deprecated && value.is_object() => { | |
| 218 | + | value["deprecation"] = Value::String(CHECKS_DEPRECATION.to_owned()); | |
| 219 | + | Outcome::Ok(value) | |
| 220 | + | } | |
| 221 | + | other => other, | |
| 222 | + | } | |
| 223 | + | } | |
| 224 | + | ||
| 193 | 225 | fn text(input: &Value, key: &str) -> String { | |
| 194 | 226 | input[key].as_str().unwrap_or_default().to_owned() | |
| 195 | 227 | } | |
| 312 | 344 | properties | |
| 313 | 345 | } | |
| 314 | 346 | ||
| 347 | + | /// The inputs that say whose self-hosted runners: a repository's own, or a | |
| 348 | + | /// workspace's. | |
| 349 | + | fn runners_owner(properties: Value) -> Value { | |
| 350 | + | let mut properties = properties; | |
| 351 | + | properties["repo"] = json!({ | |
| 352 | + | "type": "string", | |
| 353 | + | "description": "Repository as \"owner/name\", for its own runners (and, when listing, the workspace's it may use).", | |
| 354 | + | }); | |
| 355 | + | properties["workspace"] = json!({ | |
| 356 | + | "type": "string", | |
| 357 | + | "description": "Instead of repo: the workspace, for the runners its repositories share.", | |
| 358 | + | }); | |
| 359 | + | properties | |
| 360 | + | } | |
| 361 | + | ||
| 315 | 362 | /// The inputs that say whose webhooks: a repository's, or a workspace's own. | |
| 316 | 363 | fn hook_owner(properties: Value) -> Value { | |
| 317 | 364 | let mut properties = properties; | |
| 351 | 398 | } | |
| 352 | 399 | ||
| 353 | 400 | impl Op { | |
| 354 | − | pub const ALL: [Op; 103] = [ | |
| 401 | + | pub const ALL: [Op; 113] = [ | |
| 355 | 402 | Op::Whoami, | |
| 356 | 403 | Op::CreateWorkspace, | |
| 357 | 404 | Op::DeleteWorkspace, | |
| 381 | 428 | Op::PurgeRepo, | |
| 382 | 429 | Op::GetRepoSettings, | |
| 383 | 430 | Op::UpdateRepoSettings, | |
| 431 | + | Op::ListCheckNames, | |
| 384 | 432 | Op::GetMergeQueue, | |
| 385 | 433 | Op::MessageAgent, | |
| 386 | 434 | Op::AnswerMessage, | |
| 443 | 491 | Op::ListActionsVariables, | |
| 444 | 492 | Op::SetActionsVariable, | |
| 445 | 493 | Op::DeleteActionsVariable, | |
| 494 | + | Op::ListRunners, | |
| 495 | + | Op::ListRunnerGroups, | |
| 496 | + | Op::GetRunnerSettings, | |
| 497 | + | Op::CreateRunnerRegistrationToken, | |
| 498 | + | Op::RemoveRunner, | |
| 499 | + | Op::CreateRunnerGroup, | |
| 500 | + | Op::UpdateRunnerGroup, | |
| 501 | + | Op::DeleteRunnerGroup, | |
| 502 | + | Op::UpdateRunnerSettings, | |
| 446 | 503 | Op::ListCollaborators, | |
| 447 | 504 | Op::AddCollaborator, | |
| 448 | 505 | Op::UpdateCollaborator, | |
| 492 | 549 | Op::RestoreRepo => "restore_repo", | |
| 493 | 550 | Op::PurgeRepo => "purge_repo", | |
| 494 | 551 | Op::GetRepoSettings => "get_repo_settings", | |
| 552 | + | Op::ListCheckNames => "list_check_names", | |
| 495 | 553 | Op::GetMergeQueue => "get_merge_queue", | |
| 496 | 554 | Op::MessageAgent => "message_agent", | |
| 497 | 555 | Op::AnswerMessage => "answer_message", | |
| 555 | 613 | Op::ListActionsVariables => "list_actions_variables", | |
| 556 | 614 | Op::SetActionsVariable => "set_actions_variable", | |
| 557 | 615 | Op::DeleteActionsVariable => "delete_actions_variable", | |
| 616 | + | Op::ListRunners => "list_runners", | |
| 617 | + | Op::ListRunnerGroups => "list_runner_groups", | |
| 618 | + | Op::GetRunnerSettings => "get_runner_settings", | |
| 619 | + | Op::CreateRunnerRegistrationToken => "create_runner_registration_token", | |
| 620 | + | Op::RemoveRunner => "remove_runner", | |
| 621 | + | Op::CreateRunnerGroup => "create_runner_group", | |
| 622 | + | Op::UpdateRunnerGroup => "update_runner_group", | |
| 623 | + | Op::DeleteRunnerGroup => "delete_runner_group", | |
| 624 | + | Op::UpdateRunnerSettings => "update_runner_settings", | |
| 558 | 625 | Op::ListCollaborators => "list_collaborators", | |
| 559 | 626 | Op::AddCollaborator => "add_collaborator", | |
| 560 | 627 | Op::UpdateCollaborator => "update_collaborator", | |
| 645 | 712 | "Move a repository to another workspace, keeping its name. You must own both workspaces, and the destination must not already have a repository of that name; a free destination takes a private repository only if its private storage has room. Everything moves with it: git data, issues, pull requests, comments, labels, workflow runs, deployments, its project, and its own secrets, variables and webhooks. Its old address keeps working: web pages, git remotes and API calls redirect to the new one until a repository is made at the old address. Usage from now on is charged to the new workspace." | |
| 646 | 713 | } | |
| 647 | 714 | Op::GetRepoSettings => { | |
| 648 | − | "How a repository handles pull requests: the approvals a merge needs, whether failed checks can be overridden, whether a pull request must be up to date, and how g1t's agents are reviewed, revised and merged." | |
| 715 | + | "How a repository handles pull requests, as its default branch's protection: the checks that must pass (required_checks), the approvals a merge needs, whether required checks can be bypassed, whether a pull request must be up to date, and how g1t's agents are reviewed, revised and merged. The same rules hold for a person's pull request and an agent's." | |
| 649 | 716 | } | |
| 650 | 717 | Op::UpdateRepoSettings => { | |
| 651 | − | "Change how a repository handles pull requests. Only the fields given are changed. Needs the Maintain role or higher." | |
| 718 | + | "Change how a repository handles pull requests. Only the fields given are changed; required_checks replaces the whole list. A required check is named as list_check_names gives it: a workflow's name, such as CI, or another status's context, such as g1t / deploy. Needs the Maintain role or higher." | |
| 652 | 719 | } | |
| 720 | + | Op::ListCheckNames => { | |
| 721 | + | "The check names reported on a repository's commits in the last 30 days, most recent first, with the events each was reported for: the names update_repo_settings takes in required_checks. A workflow's runs report a check named after the workflow; a check required on the default branch must be reported on a pull request's head (pull_request events) and, with the merge queue on, on its queued state (merge_group events)." | |
| 722 | + | } | |
| 653 | 723 | Op::MessageAgent => { | |
| 654 | 724 | "Send the agent working on a pull request a message: a correction, a hint, a change of plan. It receives it at its next step, and it is recorded in the pull request's session. The pull request's author, and anyone with the Write role or higher. An agent uses it to ask the agent on another pull request a question (kind: question) or hand it work that belongs there (kind: handoff), giving its own pull request as from_number; the answer comes back to it at its next step." | |
| 655 | 725 | } | |
| 684 | 754 | "Issues on a repository, newest first. An issue is something that should change: a bug, a feature, a question. Pull requests are made against it." | |
| 685 | 755 | } | |
| 686 | 756 | Op::GetIssue => { | |
| 687 | − | "An issue: its description, labels and acceptance checks, its comments, and every pull request made against it with its status. If the issue is closed, resolvedBy is the number of the pull request that was merged for it. Read this before opening a pull request, to see what others have already tried." | |
| 757 | + | "An issue: its description (which may say what done means, under \"Definition of done\"), labels, its comments, and every pull request made against it with its status. If the issue is closed, resolved_by is the number of the pull request that was merged for it. Read this before opening a pull request, to see what others have already tried." | |
| 758 | + | } | |
| 759 | + | Op::CreateIssue => { | |
| 760 | + | "Open an issue on a repository. Say what done means in the body if it helps, for instance under a \"Definition of done\" heading; what must pass before a pull request for it merges is the default branch's required checks, the same for every pull request." | |
| 688 | 761 | } | |
| 689 | − | Op::CreateIssue => "Open an issue on a repository.", | |
| 690 | 762 | Op::UpdateIssue => { | |
| 691 | 763 | "Change an issue's title, body, labels or the people it is assigned to. Only the fields given are changed; labels and assignees each replace the whole set. Its author may change their own issue; anyone else needs the Triage role or higher." | |
| 692 | 764 | } | |
| 695 | 767 | } | |
| 696 | 768 | Op::ReopenIssue => "Reopen a closed issue. Its author may reopen their own issue; anyone else needs the Triage role or higher.", | |
| 697 | 769 | Op::PlanWork => { | |
| 698 | − | "Turn an outcome into a plan. An agent reads the repository and proposes the issues that would get there: what each changes, the checks it must pass, the files it will touch, and which must merge before which. Returns the plan's id at once; the plan takes a minute or two to write, so read it with get_plan until its status is ready. Nothing is opened until apply_plan. Needs the Write role or higher." | |
| 770 | + | "Turn an outcome into a plan. An agent reads the repository and proposes the issues that would get there: what each changes, what done means for it (added to its body under \"Definition of done\"), the files it will touch, and which must merge before which. Returns the plan's id at once; the plan takes a minute or two to write, so read it with get_plan until its status is ready. Nothing is opened until apply_plan. Needs the Write role or higher." | |
| 699 | 771 | } | |
| 700 | 772 | Op::GetPlan => { | |
| 701 | 773 | "A plan: the outcome asked for, its status (planning, ready, failed or applied), and the issues it proposes with their dependencies." | |
| 704 | 776 | "Open a plan's issues, each blocked by the ones it depends on. With assign, g1t agents start at once on every issue that depends on nothing, working in parallel, and on the others as what they depend on merges. keep limits it to some of the proposed issues, by their positions counting from 1. A plan is applied once. Needs the Write role or higher." | |
| 705 | 777 | } | |
| 706 | 778 | Op::AssignIssue => { | |
| 707 | − | "Assign an issue to the g1t agent. It opens a pull request for the issue in a sandbox of its own and sees it through: the issue's acceptance checks, a review by a second agent, revision if either finds something, and catching up when main moves. Returns the pull request at once; follow its progress with get_pull_request. There is no model or agent count to choose. To put many agents to work, assign many issues. Needs the Write role or higher. In preview: only for accounts g1t agents are enabled for." | |
| 779 | + | "Assign an issue to the g1t agent. It opens a pull request for the issue in a sandbox of its own and sees it through: the repository's workflows run on it as its checks, a second agent reviews it, it revises when a check fails (reading the failing jobs' logs) or the review asks for changes, and it catches up when main moves. It is ready once the default branch's required checks pass and the review approves. Returns the pull request at once; follow its progress with get_pull_request. There is no model or agent count to choose. To put many agents to work, assign many issues. Needs the Write role or higher. In preview: only for accounts g1t agents are enabled for." | |
| 708 | 780 | } | |
| 709 | 781 | Op::Delegate => { | |
| 710 | − | "Put an agent on something in one step: open an issue and assign it to the g1t agent at once. Say what you want done in plain words; give checks, commands that must pass, when you know them. Needs the Write role or higher, and nothing is opened without it. The issue is opened whatever happens next: agent.status is started (pull is the draft pull request the agent opened; follow it with get_pull_request), queued (every agent slot of the workspace is busy; it starts by itself when one frees up) or not_started, with agent.code saying why (not_paid, trial_used, limit, paused, issue_cap, billing_unavailable or no_model), agent.message saying what to do, and agent.fix_url where. There is no model or agent count to choose." | |
| 782 | + | "Put an agent on something in one step: open an issue and assign it to the g1t agent at once. Say what you want done in plain words, with what done means if you know it. What must pass before its pull request merges is the default branch's required checks. Needs the Write role or higher, and nothing is opened without it. The issue is opened whatever happens next: agent.status is started (pull is the draft pull request the agent opened; follow it with get_pull_request), queued (every agent slot of the workspace is busy; it starts by itself when one frees up) or not_started, with agent.code saying why (not_paid, trial_used, limit, paused, issue_cap, billing_unavailable or no_model), agent.message saying what to do, and agent.fix_url where. There is no model or agent count to choose." | |
| 711 | 783 | } | |
| 712 | 784 | Op::ListLabels => "The labels available on a repository's issues.", | |
| 713 | 785 | Op::AddComment => { | |
| 720 | 792 | "Pull requests on a repository, newest first. State open covers drafts and those ready for review; closed covers merged and closed." | |
| 721 | 793 | } | |
| 722 | 794 | Op::GetPullRequest => { | |
| 723 | − | "A pull request's status, head commit, comments and reviews, the issue it is for, the latest run of that issue's acceptance checks with each command's output, whether it is behind the branch it would merge into, and overlaps: other pull requests in progress that change the same files. An overlap with a pull request for a different issue means the two will conflict; say so, or keep clear of those files." | |
| 795 | + | "A pull request's status, head commit, comments and reviews, the issue it is for, its checks (statuses: what each workflow run reported on its head, with a link to the run; get_workflow_run and get_job_logs say why one failed), required_checks (each check the default branch requires, as success, failure, pending or expected when nothing has reported it yet), whether it is behind the branch it would merge into, and overlaps: other pull requests in progress that change the same files. An overlap with a pull request for a different issue means the two will conflict; say so, or keep clear of those files." | |
| 724 | 796 | } | |
| 725 | 797 | Op::CreatePullRequest => { | |
| 726 | 798 | "Start a change. Opens a draft pull request with its own fork of the repository and returns the fork's git remote. Clone it, commit your work there, push, record your session as you go, then call mark_pull_request_ready. Give the issue it is for whenever there is one. If the change is already on a branch pushed to the repository, give that branch instead: no fork is made and the pull request is ready for review at once." | |
| 737 | 809 | "What a pull request changes: the files it touches and their line-by-line diff against the commit it started from. Use it to review a pull request or to compare several made for the same issue." | |
| 738 | 810 | } | |
| 739 | 811 | Op::MergePullRequest => { | |
| 740 | − | "Land a pull request on the repository's main branch. Merging needs the Write role or higher, and only once it is marked ready and its acceptance checks have passed. Merging resolves the issue it was made for: the issue closes recording this pull request, and the other pull requests still in progress for that issue close as superseded. Where the repository has a merge queue, it joins the queue instead of landing at once. If main has moved since the pull request was opened, it is brought up to date first and lands when that is done; a repository that requires pull requests to be up to date refuses instead, so pull main into its fork or branch, push, and merge again. Check status in the result to see whether it has landed." | |
| 812 | + | "Land a pull request on the repository's main branch. Merging needs the Write role or higher, and only once it is marked ready and every check the default branch requires has passed on its head (see required_checks on get_pull_request); with ignore_checks, someone who may merge can bypass them where the repository allows it. Merging resolves the issue it was made for: the issue closes recording this pull request, and the other pull requests still in progress for that issue close as superseded. Where the repository has a merge queue, it joins the queue instead of landing at once. If main has moved since the pull request was opened, it is brought up to date first and lands when that is done; a repository that requires pull requests to be up to date refuses instead, so pull main into its fork or branch, push, and merge again. Check status in the result to see whether it has landed." | |
| 741 | 813 | } | |
| 742 | 814 | Op::ListEvents => { | |
| 743 | 815 | "The timeline of a repository: pushes, issues, pull requests, comments and session activity, newest first." | |
| 810 | 882 | } | |
| 811 | 883 | Op::SetActionsVariable => "Add or change a variable's row, as for secrets.", | |
| 812 | 884 | Op::DeleteActionsVariable => "Remove a variable: one row by `id`, or every row of the key.", | |
| 885 | + | Op::ListRunners => { | |
| 886 | + | "A workspace's self-hosted runners, or a repository's: its own and the workspace's that its runner group lets it use. Each has its `labels` (always `self-hosted`, its OS and its architecture), `status` (`online`, `busy` or `offline`), the `work` it is doing, its `version` and when it was last seen. A workspace's are seen by its members; a repository's need the Admin role on it." | |
| 887 | + | } | |
| 888 | + | Op::ListRunnerGroups => { | |
| 889 | + | "A workspace's runner groups: which of its repositories may use the runners in each. The default group (every repository) is where runners go when no group is named. Members only." | |
| 890 | + | } | |
| 891 | + | Op::GetRunnerSettings => { | |
| 892 | + | "Where a workspace's (or a repository's) g1t agent work runs, and whether pull requests from forks may use its self-hosted runners. `agents_on_self_hosted` sends agent runs, checks, reviews and the merge queue to runners with `agent_labels` instead of g1t's sandboxes. A repository's are its workspace's unless it has its own (`inherited`)." | |
| 893 | + | } | |
| 894 | + | Op::CreateRunnerRegistrationToken => { | |
| 895 | + | "A registration token for `g1t-runner register`, shown once. It lasts an hour and registers any number of runners until then, into `group` (the default group if none) for a workspace, or as a repository's own runners. It can do nothing else. Owners of the workspace, or admins of the repository, signed in or with a person's token; workspace tokens, G1T_TOKEN included, are refused." | |
| 896 | + | } | |
| 897 | + | Op::RemoveRunner => { | |
| 898 | + | "Remove a self-hosted runner: its credential stops working at once and a job it is running fails. The machine's `g1t-runner` stops on its next poll. Owners of the workspace, or admins of the repository." | |
| 899 | + | } | |
| 900 | + | Op::CreateRunnerGroup => { | |
| 901 | + | "Create a runner group: the repositories (by name) that may use the runners in it; empty for every repository. Owners only." | |
| 902 | + | } | |
| 903 | + | Op::UpdateRunnerGroup => "Rename a runner group, or change which repositories may use it. Owners only.", | |
| 904 | + | Op::DeleteRunnerGroup => "Delete a runner group. Its runners join the default group, which cannot be deleted. Owners only.", | |
| 905 | + | Op::UpdateRunnerSettings => { | |
| 906 | + | "Change where g1t agent work runs and whether pull requests from forks may use self-hosted runners, for a workspace or one repository. Left out is unchanged; `inherit` drops a repository's own settings. Allowing forks lets anyone who can open a pull request run code on your machines. Owners of the workspace, or admins of the repository." | |
| 907 | + | } | |
| 813 | 908 | Op::ImportIssue => { | |
| 814 | 909 | "Open an issue from a ticket in Jira or Linear, or from a Sentry issue, by its key or address. The issue is linked to it: agents read the original, and when the work lands the ticket is told. Importing the same ticket again returns the issue already made. With assign, a g1t agent starts on it." | |
| 815 | 910 | } | |
| 1035 | 1130 | &["repo", "confirm"], | |
| 1036 | 1131 | ), | |
| 1037 | 1132 | Op::ListDeletedRepos => object(json!({ "workspace": workspace_schema() }), &["workspace"]), | |
| 1038 | − | Op::GetRepoSettings => object(json!({ "repo": repo_schema() }), &["repo"]), | |
| 1133 | + | Op::GetRepoSettings | Op::ListCheckNames => object(json!({ "repo": repo_schema() }), &["repo"]), | |
| 1039 | 1134 | Op::GetMergeQueue => object(json!({ "repo": repo_schema() }), &["repo"]), | |
| 1040 | 1135 | Op::MessageAgent => object( | |
| 1041 | 1136 | numbered(json!({ | |
| 1141 | 1236 | "type": "boolean", | |
| 1142 | 1237 | "description": "Land a g1t agent's pull request without a person once every rule is met.", | |
| 1143 | 1238 | }, | |
| 1239 | + | "required_checks": { | |
| 1240 | + | "type": "array", | |
| 1241 | + | "items": { "type": "string" }, | |
| 1242 | + | "description": "The checks that must pass on a pull request's head before it merges into the default branch, by name: a workflow's name (CI) or another status's context (g1t / deploy). list_check_names gives the names seen lately. Replaces the whole list; an empty list requires none.", | |
| 1243 | + | }, | |
| 1144 | 1244 | "require_up_to_date": { | |
| 1145 | 1245 | "type": "boolean", | |
| 1146 | 1246 | "description": "Refuse to merge a pull request that is behind the default branch. When false, merging brings it up to date first.", | |
| 1155 | 1255 | }, | |
| 1156 | 1256 | "allow_ignoring_checks": { | |
| 1157 | 1257 | "type": "boolean", | |
| 1158 | − | "description": "Whether a member may merge although the acceptance checks did not pass.", | |
| 1258 | + | "description": "Whether someone who may merge can bypass required checks that have not passed, with ignore_checks.", | |
| 1159 | 1259 | }, | |
| 1160 | 1260 | "agent_review": { | |
| 1161 | 1261 | "type": "boolean", | |
| 1221 | 1321 | "checks": { | |
| 1222 | 1322 | "type": "array", | |
| 1223 | 1323 | "items": { "type": "string" }, | |
| 1224 | − | "description": "Commands that must pass for a pull request to be accepted.", | |
| 1324 | + | "deprecated": true, | |
| 1325 | + | "description": "Deprecated. Commands are added to the body under \"Definition of done\", and the response says so in deprecation. What must pass before a pull request merges is the default branch's required checks.", | |
| 1225 | 1326 | }, | |
| 1226 | 1327 | }), | |
| 1227 | 1328 | &["repo", "title"], | |
| 1283 | 1384 | "checks": { | |
| 1284 | 1385 | "type": "array", | |
| 1285 | 1386 | "items": { "type": "string" }, | |
| 1286 | − | "description": "Commands that must pass for its pull request to be accepted, e.g. \"npm test\".", | |
| 1387 | + | "deprecated": true, | |
| 1388 | + | "description": "Deprecated, as on create_issue: commands are added to the body under \"Definition of done\".", | |
| 1287 | 1389 | }, | |
| 1288 | 1390 | "labels": { | |
| 1289 | 1391 | "type": "array", | |
| 1400 | 1502 | }, | |
| 1401 | 1503 | "ignore_checks": { | |
| 1402 | 1504 | "type": "boolean", | |
| 1403 | − | "description": "Merge although the acceptance checks have not passed.", | |
| 1505 | + | "description": "Merge although required checks have not passed, where the repository allows bypassing them (allow_ignoring_checks).", | |
| 1404 | 1506 | }, | |
| 1405 | 1507 | })), | |
| 1406 | 1508 | &["repo", "number"], | |
| 1518 | 1620 | })), | |
| 1519 | 1621 | &["setting"], | |
| 1520 | 1622 | ), | |
| 1623 | + | Op::ListRunners | Op::GetRunnerSettings => object(runners_owner(json!({})), &[]), | |
| 1624 | + | Op::CreateRunnerRegistrationToken => object( | |
| 1625 | + | runners_owner(json!({ | |
| 1626 | + | "group": { "type": "string", "description": "A workspace's runner group, by name or id, for the runners it registers. The default group if left out." }, | |
| 1627 | + | })), | |
| 1628 | + | &[], | |
| 1629 | + | ), | |
| 1630 | + | Op::RemoveRunner => object( | |
| 1631 | + | runners_owner(json!({ "id": { "type": "string", "description": "The runner's id, from a list." } })), | |
| 1632 | + | &["id"], | |
| 1633 | + | ), | |
| 1634 | + | Op::ListRunnerGroups => object(json!({ "workspace": workspace_schema() }), &["workspace"]), | |
| 1635 | + | Op::CreateRunnerGroup | Op::UpdateRunnerGroup => object( | |
| 1636 | + | json!({ | |
| 1637 | + | "workspace": workspace_schema(), | |
| 1638 | + | "id": { "type": "string", "description": "The group to change, from a list. Left out: a new group." }, | |
| 1639 | + | "name": { "type": "string", "description": "What to call it." }, | |
| 1640 | + | "repositories": { | |
| 1641 | + | "type": "array", | |
| 1642 | + | "items": { "type": "string" }, | |
| 1643 | + | "description": "Repository names that may use its runners. Empty is every repository in the workspace.", | |
| 1644 | + | }, | |
| 1645 | + | }), | |
| 1646 | + | if self == Op::UpdateRunnerGroup { &["workspace", "id"] } else { &["workspace", "name"] }, | |
| 1647 | + | ), | |
| 1648 | + | Op::DeleteRunnerGroup => object( | |
| 1649 | + | json!({ "workspace": workspace_schema(), "id": { "type": "string", "description": "The group's id." } }), | |
| 1650 | + | &["workspace", "id"], | |
| 1651 | + | ), | |
| 1652 | + | Op::UpdateRunnerSettings => object( | |
| 1653 | + | runners_owner(json!({ | |
| 1654 | + | "agents_on_self_hosted": { "type": "boolean", "description": "Run agent runs, checks, reviews and the merge queue on self-hosted runners." }, | |
| 1655 | + | "agent_labels": { | |
| 1656 | + | "type": "array", | |
| 1657 | + | "items": { "type": "string" }, | |
| 1658 | + | "description": "The labels a runner needs to take agent work. self-hosted is always one.", | |
| 1659 | + | }, | |
| 1660 | + | "fork_pull_requests": { "type": "boolean", "description": "Let jobs of pull requests from forks run on self-hosted runners." }, | |
| 1661 | + | "inherit": { "type": "boolean", "description": "For a repository: drop its own settings and follow its workspace's." }, | |
| 1662 | + | })), | |
| 1663 | + | &[], | |
| 1664 | + | ), | |
| 1521 | 1665 | Op::CreateWebhook => object( | |
| 1522 | 1666 | hook_owner(json!({ | |
| 1523 | 1667 | "url": { "type": "string", "description": "An HTTPS address on the public internet." }, | |
| 1659 | 1803 | | Op::GetPullRequestChanges | |
| 1660 | 1804 | | Op::ListEvents | |
| 1661 | 1805 | | Op::GetRepoSettings | |
| 1806 | + | | Op::ListCheckNames | |
| 1662 | 1807 | | Op::GetMergeQueue | |
| 1663 | 1808 | ) | |
| 1664 | 1809 | } | |
| 1710 | 1855 | | Op::ListActionsVariables | |
| 1711 | 1856 | | Op::SetActionsVariable | |
| 1712 | 1857 | | Op::DeleteActionsVariable | |
| 1858 | + | | Op::ListRunners | |
| 1859 | + | | Op::ListRunnerGroups | |
| 1860 | + | | Op::GetRunnerSettings | |
| 1861 | + | | Op::CreateRunnerRegistrationToken | |
| 1862 | + | | Op::RemoveRunner | |
| 1863 | + | | Op::CreateRunnerGroup | |
| 1864 | + | | Op::UpdateRunnerGroup | |
| 1865 | + | | Op::DeleteRunnerGroup | |
| 1866 | + | | Op::UpdateRunnerSettings | |
| 1713 | 1867 | | Op::ListMyRepoInvitations | |
| 1714 | 1868 | | Op::AcceptRepoInvitation | |
| 1715 | 1869 | | Op::DeclineRepoInvitation | |
| 2122 | 2276 | ) | |
| 2123 | 2277 | .await | |
| 2124 | 2278 | } | |
| 2279 | + | Op::ListCheckNames => { | |
| 2280 | + | pass( | |
| 2281 | + | work, | |
| 2282 | + | "seen_checks", | |
| 2283 | + | &json!({ "repo": repo, "viewer": viewer }), | |
| 2284 | + | ) | |
| 2285 | + | .await | |
| 2286 | + | } | |
| 2125 | 2287 | Op::GetMergeQueue => { | |
| 2126 | 2288 | pass(work, "queue", &json!({ "repo": repo, "viewer": viewer })).await | |
| 2127 | 2289 | } | |
| 2275 | 2437 | let flag = |key: &str, now: bool| input[key].as_bool().unwrap_or(now); | |
| 2276 | 2438 | let settings = RepoSettings { | |
| 2277 | 2439 | auto_merge: flag("auto_merge", current.auto_merge), | |
| 2440 | + | required_checks: strings(input, "required_checks").unwrap_or(current.required_checks.clone()), | |
| 2278 | 2441 | require_up_to_date: flag("require_up_to_date", current.require_up_to_date), | |
| 2279 | 2442 | required_approvals: integer(input, "required_approvals") | |
| 2280 | 2443 | .unwrap_or(current.required_approvals), | |
| 2342 | 2505 | } | |
| 2343 | 2506 | Op::GetIssue => pass(work, "get_issue", &view()).await, | |
| 2344 | 2507 | Op::CreateIssue => { | |
| 2345 | − | pass( | |
| 2508 | + | let checks = deprecated_checks(input); | |
| 2509 | + | let opened = pass( | |
| 2346 | 2510 | work, | |
| 2347 | 2511 | "open_issue", | |
| 2348 | 2512 | &OpenIssueArgs { | |
| 2351 | 2515 | title: text(input, "title"), | |
| 2352 | 2516 | body: text(input, "body"), | |
| 2353 | 2517 | labels: strings(input, "labels").unwrap_or_default(), | |
| 2354 | − | checks: strings(input, "checks").unwrap_or_default(), | |
| 2518 | + | checks: checks.clone(), | |
| 2355 | 2519 | }, | |
| 2356 | 2520 | ) | |
| 2357 | − | .await | |
| 2521 | + | .await?; | |
| 2522 | + | Ok(with_deprecation(opened, !checks.is_empty())) | |
| 2358 | 2523 | } | |
| 2359 | 2524 | Op::UpdateIssue => { | |
| 2360 | 2525 | pass( | |
| 2407 | 2572 | .await | |
| 2408 | 2573 | } | |
| 2409 | 2574 | Op::Delegate => { | |
| 2410 | − | pass( | |
| 2575 | + | let checks = deprecated_checks(input); | |
| 2576 | + | let delegated = pass( | |
| 2411 | 2577 | runner, | |
| 2412 | 2578 | "delegate", | |
| 2413 | 2579 | &json!({ | |
| 2416 | 2582 | "title": text(input, "title"), | |
| 2417 | 2583 | "body": text(input, "body"), | |
| 2418 | 2584 | "labels": strings(input, "labels").unwrap_or_default(), | |
| 2419 | − | "checks": strings(input, "checks").unwrap_or_default(), | |
| 2585 | + | "checks": checks, | |
| 2420 | 2586 | }), | |
| 2421 | 2587 | ) | |
| 2422 | − | .await | |
| 2588 | + | .await?; | |
| 2589 | + | Ok(with_deprecation(delegated, !checks.is_empty())) | |
| 2423 | 2590 | } | |
| 2424 | 2591 | Op::AssignIssue => { | |
| 2425 | 2592 | pass( | |
| 2755 | 2922 | Op::GetModelRoutes => { | |
| 2756 | 2923 | pass(integrations, "routes", &json!({ "workspace": workspace(), "viewer": viewer })).await | |
| 2757 | 2924 | } | |
| 2925 | + | Op::ListRunners | |
| 2926 | + | | Op::GetRunnerSettings | |
| 2927 | + | | Op::CreateRunnerRegistrationToken | |
| 2928 | + | | Op::RemoveRunner | |
| 2929 | + | | Op::UpdateRunnerSettings => { | |
| 2930 | + | // A repository's own runners, or with no repository named, | |
| 2931 | + | // the workspace's. | |
| 2932 | + | let mut args = match repo_path(input) { | |
| 2933 | + | Some(repo) => json!({ "repo": repo }), | |
| 2934 | + | None if !workspace().is_empty() => json!({ "workspace": workspace() }), | |
| 2935 | + | None => return failed(FailureCode::Invalid, "Name the repository as repo, or the workspace as workspace."), | |
| 2936 | + | }; | |
| 2937 | + | args["actor"] = json!(actor()); | |
| 2938 | + | let method = match self { | |
| 2939 | + | Op::ListRunners => "runners", | |
| 2940 | + | Op::GetRunnerSettings => "runner_settings", | |
| 2941 | + | Op::CreateRunnerRegistrationToken => "create_registration_token", | |
| 2942 | + | Op::RemoveRunner => "remove_runner", | |
| 2943 | + | _ => "set_runner_settings", | |
| 2944 | + | }; | |
| 2945 | + | if let Some(group) = optional_text(input, "group") { | |
| 2946 | + | args["group"] = json!(group); | |
| 2947 | + | } | |
| 2948 | + | if let Some(id) = optional_text(input, "id") { | |
| 2949 | + | args["id"] = json!(id); | |
| 2950 | + | } | |
| 2951 | + | for key in ["agents_on_self_hosted", "fork_pull_requests", "inherit"] { | |
| 2952 | + | if let Some(on) = input[key].as_bool() { | |
| 2953 | + | args[key] = json!(on); | |
| 2954 | + | } | |
| 2955 | + | } | |
| 2956 | + | if let Some(labels) = strings(input, "agent_labels") { | |
| 2957 | + | args["agent_labels"] = json!(labels); | |
| 2958 | + | } | |
| 2959 | + | pass(actions, method, &args).await | |
| 2960 | + | } | |
| 2961 | + | Op::ListRunnerGroups => { | |
| 2962 | + | pass(actions, "runner_groups", &json!({ "actor": actor(), "workspace": workspace() })).await | |
| 2963 | + | } | |
| 2964 | + | Op::CreateRunnerGroup | Op::UpdateRunnerGroup => { | |
| 2965 | + | let mut args = json!({ "actor": actor(), "workspace": workspace() }); | |
| 2966 | + | if self == Op::UpdateRunnerGroup { | |
| 2967 | + | args["id"] = json!(text(input, "id")); | |
| 2968 | + | } | |
| 2969 | + | if let Some(name) = optional_text(input, "name") { | |
| 2970 | + | args["name"] = json!(name); | |
| 2971 | + | } | |
| 2972 | + | if let Some(repositories) = strings(input, "repositories") { | |
| 2973 | + | args["repositories"] = json!(repositories); | |
| 2974 | + | } | |
| 2975 | + | pass(actions, "set_runner_group", &args).await | |
| 2976 | + | } | |
| 2977 | + | Op::DeleteRunnerGroup => { | |
| 2978 | + | pass(actions, "delete_runner_group", &json!({ "actor": actor(), "workspace": workspace(), "id": text(input, "id") })).await | |
| 2979 | + | } | |
| 2758 | 2980 | Op::SetModelRoutes => { | |
| 2759 | 2981 | let routes: Vec<Value> = input["routes"] | |
| 2760 | 2982 | .as_array() |
| 690 | 690 | "get_repo_settings": { | |
| 691 | 691 | "response": { | |
| 692 | 692 | "auto_merge": false, | |
| 693 | + | "required_checks": [ | |
| 694 | + | "CI" | |
| 695 | + | ], | |
| 693 | 696 | "require_up_to_date": false, | |
| 694 | 697 | "required_approvals": 0, | |
| 695 | 698 | "count_agent_approvals": true, | |
| 704 | 707 | }, | |
| 705 | 708 | "update_repo_settings": { | |
| 706 | 709 | "request": { | |
| 710 | + | "required_checks": [ | |
| 711 | + | "CI", | |
| 712 | + | "g1t / deploy" | |
| 713 | + | ], | |
| 707 | 714 | "required_approvals": 1, | |
| 708 | 715 | "count_agent_approvals": false, | |
| 709 | 716 | "merge_queue": true | |
| 710 | 717 | }, | |
| 711 | 718 | "response": { | |
| 712 | 719 | "auto_merge": false, | |
| 720 | + | "required_checks": [ | |
| 721 | + | "CI", | |
| 722 | + | "g1t / deploy" | |
| 723 | + | ], | |
| 713 | 724 | "require_up_to_date": false, | |
| 714 | 725 | "required_approvals": 1, | |
| 715 | 726 | "count_agent_approvals": false, | |
| 721 | 732 | "updated_by": "syntaqx", | |
| 722 | 733 | "updated_at": "2026-10-04T16:20:37.508Z" | |
| 723 | 734 | }, | |
| 724 | − | "notes": "`required_approvals` is at most 6 and `max_revisions` at most 5. See [what a repository can ask for](/guides/g1t-agents/#what-a-repository-can-ask-for). `hold_low_confidence` is on unless turned off: see [confidence](/guides/g1t-agents/#how-sure-the-agent-is)." | |
| 735 | + | "notes": "`required_checks` replaces the whole list: at most 20 names, each a workflow's name or another status's context, as [`list_check_names`](#list_check_names) gives them. A pull request merges only once each passes on its head; one nothing has reported holds it too. With the merge queue on, each must also pass on the queued state, so the workflows behind them need `merge_group` in their `on:`. `required_approvals` is at most 6 and `max_revisions` at most 5. See [what a repository can ask for](/guides/g1t-agents/#what-a-repository-can-ask-for). `hold_low_confidence` is on unless turned off: see [confidence](/guides/g1t-agents/#how-sure-the-agent-is)." | |
| 725 | 736 | }, | |
| 737 | + | "list_check_names": { | |
| 738 | + | "response": [ | |
| 739 | + | { | |
| 740 | + | "name": "CI", | |
| 741 | + | "events": [ | |
| 742 | + | "pull_request", | |
| 743 | + | "merge_group", | |
| 744 | + | "push" | |
| 745 | + | ], | |
| 746 | + | "last_seen": "2026-10-06T09:14:02.118Z" | |
| 747 | + | }, | |
| 748 | + | { | |
| 749 | + | "name": "g1t / deploy", | |
| 750 | + | "events": [], | |
| 751 | + | "last_seen": "2026-10-06T08:51:40.006Z" | |
| 752 | + | } | |
| 753 | + | ], | |
| 754 | + | "notes": "The names reported on the repository's commits in the last 30 days, most recent first. A workflow reports a check named after it (`CI`), for each event it ran for; another tool, such as a deployment, reports its own name (`g1t / deploy`). Empty until something has reported on a commit: add a workflow in `.g1t/workflows` first." | |
| 755 | + | }, | |
| 726 | 756 | "list_events": { | |
| 727 | 757 | "query": { | |
| 728 | 758 | "before": "evt_01m43t2a6c9e3g7j1m5q9t3x7b" | |
| 775 | 805 | "labels": [ | |
| 776 | 806 | "feature" | |
| 777 | 807 | ], | |
| 778 | − | "checks": [ | |
| 779 | − | "cargo test" | |
| 780 | − | ], | |
| 781 | 808 | "state": "open", | |
| 782 | 809 | "reason": null, | |
| 783 | 810 | "resolved_by": null, | |
| 807 | 834 | "body": "Take a name from the first argument; fall back to world.", | |
| 808 | 835 | "labels": [ | |
| 809 | 836 | "feature" | |
| 810 | − | ], | |
| 811 | − | "checks": [ | |
| 812 | − | "cargo test" | |
| 813 | 837 | ] | |
| 814 | 838 | }, | |
| 815 | 839 | "response": { | |
| 821 | 845 | "labels": [ | |
| 822 | 846 | "feature" | |
| 823 | 847 | ], | |
| 824 | − | "checks": [ | |
| 825 | − | "cargo test" | |
| 826 | − | ], | |
| 827 | 848 | "state": "open", | |
| 828 | 849 | "reason": null, | |
| 829 | 850 | "resolved_by": null, | |
| 856 | 877 | "body": "Take a name from the first argument; fall back to world.", | |
| 857 | 878 | "labels": [ | |
| 858 | 879 | "feature" | |
| 859 | − | ], | |
| 860 | − | "checks": [ | |
| 861 | − | "cargo test" | |
| 862 | 880 | ], | |
| 863 | 881 | "state": "closed", | |
| 864 | 882 | "reason": "completed", | |
| 1000 | 1018 | "labels": [ | |
| 1001 | 1019 | "feature", | |
| 1002 | 1020 | "good first issue" | |
| 1003 | − | ], | |
| 1004 | − | "checks": [ | |
| 1005 | − | "cargo test" | |
| 1006 | 1021 | ], | |
| 1007 | 1022 | "state": "open", | |
| 1008 | 1023 | "reason": null, | |
| 1039 | 1054 | "body": "Take a name from the first argument; fall back to world.", | |
| 1040 | 1055 | "labels": [ | |
| 1041 | 1056 | "feature" | |
| 1042 | − | ], | |
| 1043 | − | "checks": [ | |
| 1044 | − | "cargo test" | |
| 1045 | 1057 | ], | |
| 1046 | 1058 | "state": "closed", | |
| 1047 | 1059 | "reason": "not_planned", | |
| 1074 | 1086 | "labels": [ | |
| 1075 | 1087 | "feature" | |
| 1076 | 1088 | ], | |
| 1077 | − | "checks": [ | |
| 1078 | − | "cargo test" | |
| 1079 | − | ], | |
| 1080 | 1089 | "state": "open", | |
| 1081 | 1090 | "reason": null, | |
| 1082 | 1091 | "resolved_by": null, | |
| 1143 | 1152 | "delegate": { | |
| 1144 | 1153 | "request": { | |
| 1145 | 1154 | "title": "Retry webhooks with exponential backoff", | |
| 1146 | − | "body": "Deliveries that fail are dropped today. Retry them with exponential backoff, up to six times, then mark the delivery failed.", | |
| 1147 | − | "checks": [ | |
| 1148 | − | "npm test" | |
| 1149 | − | ] | |
| 1155 | + | "body": "Deliveries that fail are dropped today. Retry them with exponential backoff, up to six times, then mark the delivery failed.\n\n## Definition of done\n\n- A delivery that fails is retried after 1, 2, 4, 8, 16 and 32 seconds.\n- After the sixth failure it is marked failed and shows on the webhook's page." | |
| 1150 | 1156 | }, | |
| 1151 | 1157 | "response": { | |
| 1152 | 1158 | "issue": { | |
| 1156 | 1162 | "title": "Retry webhooks with exponential backoff", | |
| 1157 | 1163 | "body": "Deliveries that fail are dropped today. Retry them with exponential backoff, up to six times, then mark the delivery failed.", | |
| 1158 | 1164 | "labels": [], | |
| 1159 | − | "checks": [ | |
| 1160 | − | "npm test" | |
| 1161 | − | ], | |
| 1162 | 1165 | "state": "open", | |
| 1163 | 1166 | "reason": null, | |
| 1164 | 1167 | "resolved_by": null, | |
| 1286 | 1289 | "labels": [ | |
| 1287 | 1290 | "feature" | |
| 1288 | 1291 | ], | |
| 1289 | − | "checks": [ | |
| 1290 | − | "cargo test" | |
| 1292 | + | "done": [ | |
| 1293 | + | "`greet` prints \"Hello, ana!\" when given \"ana\", and \"Hello, world!\" with no argument." | |
| 1291 | 1294 | ], | |
| 1292 | 1295 | "files": [ | |
| 1293 | 1296 | "src/greet.rs" | |
| 1301 | 1304 | "labels": [ | |
| 1302 | 1305 | "feature" | |
| 1303 | 1306 | ], | |
| 1304 | − | "checks": [ | |
| 1305 | − | "cargo test" | |
| 1307 | + | "done": [ | |
| 1308 | + | "The page at / shows the name given in ?name=, escaped." | |
| 1306 | 1309 | ], | |
| 1307 | 1310 | "files": [ | |
| 1308 | 1311 | "src/web.rs" | |
| 1348 | 1351 | "labels": [ | |
| 1349 | 1352 | "feature" | |
| 1350 | 1353 | ], | |
| 1351 | − | "checks": [ | |
| 1352 | − | "cargo test" | |
| 1354 | + | "done": [ | |
| 1355 | + | "`greet` prints \"Hello, ana!\" when given \"ana\", and \"Hello, world!\" with no argument." | |
| 1353 | 1356 | ], | |
| 1354 | 1357 | "files": [ | |
| 1355 | 1358 | "src/greet.rs" | |
| 1363 | 1366 | "labels": [ | |
| 1364 | 1367 | "feature" | |
| 1365 | 1368 | ], | |
| 1366 | − | "checks": [ | |
| 1367 | − | "cargo test" | |
| 1369 | + | "done": [ | |
| 1370 | + | "The page at / shows the name given in ?name=, escaped." | |
| 1368 | 1371 | ], | |
| 1369 | 1372 | "files": [ | |
| 1370 | 1373 | "src/web.rs" | |
| 1544 | 1547 | "body": "Take a name from the first argument; fall back to world.", | |
| 1545 | 1548 | "labels": [ | |
| 1546 | 1549 | "feature" | |
| 1547 | − | ], | |
| 1548 | − | "checks": [ | |
| 1549 | − | "cargo test" | |
| 1550 | 1550 | ], | |
| 1551 | 1551 | "state": "open", | |
| 1552 | 1552 | "reason": null, | |
| 1586 | 1586 | "created_at": "2026-10-01T18:33:10.420Z" | |
| 1587 | 1587 | } | |
| 1588 | 1588 | ], | |
| 1589 | − | "checks": { | |
| 1590 | − | "id": "chk_01m43sw8e2g6j0m4q8t2x6a0c4", | |
| 1591 | − | "head_commit": "9f2c4e1a7b3d5f60812a4c6e8b0d2f4a6c8e0b13", | |
| 1592 | − | "status": "passed", | |
| 1593 | − | "results": [ | |
| 1594 | − | { | |
| 1595 | − | "command": "cargo test", | |
| 1596 | − | "passed": true, | |
| 1597 | − | "exit_code": 0, | |
| 1598 | − | "output": "test result: ok. 3 passed; 0 failed", | |
| 1599 | − | "duration_ms": 18234 | |
| 1600 | − | } | |
| 1601 | − | ], | |
| 1602 | − | "error": null, | |
| 1603 | − | "created_at": "2026-10-01T18:33:11.002Z", | |
| 1604 | − | "finished_at": "2026-10-01T18:35:44.902Z" | |
| 1605 | − | }, | |
| 1589 | + | "checks": null, | |
| 1606 | 1590 | "overlaps": [], | |
| 1607 | 1591 | "behind": false, | |
| 1608 | 1592 | "review_pending": false, | |
| 1623 | 1607 | "declined": false | |
| 1624 | 1608 | } | |
| 1625 | 1609 | ], | |
| 1626 | − | "statuses": [] | |
| 1610 | + | "statuses": [ | |
| 1611 | + | { | |
| 1612 | + | "context": "CI / pull_request", | |
| 1613 | + | "state": "success", | |
| 1614 | + | "description": "CI passed", | |
| 1615 | + | "target_url": "https://g1t.sh/syntaqx/hello/actions/runs/run_01m43sw8e2g6j0m4q8t2x6a0c4", | |
| 1616 | + | "updated_at": "2026-10-01T18:35:44.902Z" | |
| 1617 | + | } | |
| 1618 | + | ], | |
| 1619 | + | "required_checks": [ | |
| 1620 | + | { | |
| 1621 | + | "name": "CI", | |
| 1622 | + | "state": "success", | |
| 1623 | + | "description": "CI passed", | |
| 1624 | + | "target_url": "https://g1t.sh/syntaqx/hello/actions/runs/run_01m43sw8e2g6j0m4q8t2x6a0c4" | |
| 1625 | + | } | |
| 1626 | + | ] | |
| 1627 | 1627 | }, | |
| 1628 | − | "notes": "| Field | |\n| --- | --- |\n| `pull.files` | The files it changes, with lines added and removed. |\n| `checks` | The latest run of the acceptance checks against its head, with each command's output. |\n| `overlaps` | Other pull requests in progress that change the same files. |\n| `behind` | Whether the default branch has moved since it was made. |\n| `landing` | Whether it is being brought up to date to land. |\n| `lifecycle` | For a pull request the g1t agent is seeing through: its stage, such as `working`, `checking`, `reviewing` or `needs_you`. |\n| `comments` | Comments, reviews and events, with `path`, `line` and `verdict`. |\n| `messages` | Messages sent to the agent working on it. |\n| `statuses` | Commit statuses reported for its head, such as workflow runs. |\n| `mergeable` | Whether it merges cleanly into its target: `clean`, `conflicting`, `checking` (being worked out) or `unknown`. Worked out ahead of time whenever it or its target moves. |\n| `conflicts` | When `mergeable` is `conflicting`, the files that conflict. Resolve them by merging the target in, or have the g1t agent do it. |\n| `earlierChecks` | Earlier runs of the acceptance checks, newest first, without their output. |\n\nChecks are started by g1t, not through the API. They run when a pull request becomes ready for review and again when its head moves." | |
| 1628 | + | "notes": "| Field | |\n| --- | --- |\n| `pull.files` | The files it changes, with lines added and removed. |\n| `statuses` | Its checks: what each workflow run (or another tool, such as a deployment) reported on its head, as `pending`, `success`, `failure` or `error`, with a link to the run. |\n| `required_checks` | Each check the default branch requires (see [`update_repo_settings`](#update_repo_settings)), as it stands on the head: `success`, `failure`, `pending`, or `expected` when nothing has reported it yet. It merges only once all are `success`. |\n| `checks` | Set when the merge queue took it out, with why in `error`. Older pull requests may show a run of commands from their issue here, from before checks were workflows. |\n| `overlaps` | Other pull requests in progress that change the same files. |\n| `behind` | Whether the default branch has moved since it was made. |\n| `landing` | Whether it is being brought up to date to land. |\n| `lifecycle` | For a pull request the g1t agent is seeing through: its stage, such as `working`, `checking`, `reviewing` or `needs_you`. |\n| `comments` | Comments, reviews and events, with `path`, `line` and `verdict`. |\n| `messages` | Messages sent to the agent working on it. |\n\n| `mergeable` | Whether it merges cleanly into its target: `clean`, `conflicting`, `checking` (being worked out) or `unknown`. Worked out ahead of time whenever it or its target moves. |\n| `conflicts` | When `mergeable` is `conflicting`, the files that conflict. Resolve them by merging the target in, or have the g1t agent do it. |\n| `earlier_checks` | Earlier records like `checks`, newest first. |\n\nChecks are the repository's workflows: they run on `pull_request` events when it is opened, marked ready and pushed to. Read why one failed with [`get_workflow_run`](#get_workflow_run) and [`get_job_logs`](#get_job_logs)." | |
| 1629 | 1629 | }, | |
| 1630 | 1630 | "get_pull_request_changes": { | |
| 1631 | 1631 | "response": { | |
| 2683 | 2683 | }, | |
| 2684 | 2684 | "response": true | |
| 2685 | 2685 | }, | |
| 2686 | + | "list_runners": { | |
| 2687 | + | "response": [ | |
| 2688 | + | { | |
| 2689 | + | "id": "rnr_01kq2m8v4c7f0h3k6n9q2t5w8z", | |
| 2690 | + | "name": "build-01", | |
| 2691 | + | "workspace": "flagon-io", | |
| 2692 | + | "repo": null, | |
| 2693 | + | "group": "Default", | |
| 2694 | + | "labels": [ | |
| 2695 | + | "self-hosted", | |
| 2696 | + | "linux", | |
| 2697 | + | "x64", | |
| 2698 | + | "gpu" | |
| 2699 | + | ], | |
| 2700 | + | "os": "linux", | |
| 2701 | + | "arch": "x64", | |
| 2702 | + | "version": "0.2.0", | |
| 2703 | + | "ephemeral": false, | |
| 2704 | + | "status": "busy", | |
| 2705 | + | "work": { | |
| 2706 | + | "kind": "workflow", | |
| 2707 | + | "id": "job_01kq2m9b1d4g7j0m3p6s9v2y5b", | |
| 2708 | + | "name": "test (linux, 24)", | |
| 2709 | + | "repo": "flagon-io/hello", | |
| 2710 | + | "run_id": "run_01kq2m9a8c1f4h7k0n3q6t9w2z", | |
| 2711 | + | "started_at": "2026-10-06T14:02:11.000Z" | |
| 2712 | + | }, | |
| 2713 | + | "last_seen_at": "2026-10-06T14:05:40.512Z", | |
| 2714 | + | "created_at": "2026-10-02T09:30:00.000Z", | |
| 2715 | + | "created_by": null | |
| 2716 | + | }, | |
| 2717 | + | { | |
| 2718 | + | "id": "rnr_01kq2m8v4c7f0h3k6n9q2t5w9a", | |
| 2719 | + | "name": "mac-mini", | |
| 2720 | + | "workspace": "flagon-io", | |
| 2721 | + | "repo": null, | |
| 2722 | + | "group": "Default", | |
| 2723 | + | "labels": [ | |
| 2724 | + | "self-hosted", | |
| 2725 | + | "macos", | |
| 2726 | + | "arm64" | |
| 2727 | + | ], | |
| 2728 | + | "os": "macos", | |
| 2729 | + | "arch": "arm64", | |
| 2730 | + | "version": "0.2.0", | |
| 2731 | + | "ephemeral": false, | |
| 2732 | + | "status": "online", | |
| 2733 | + | "work": null, | |
| 2734 | + | "last_seen_at": "2026-10-06T14:05:40.512Z", | |
| 2735 | + | "created_at": "2026-10-02T09:30:00.000Z", | |
| 2736 | + | "created_by": null | |
| 2737 | + | } | |
| 2738 | + | ], | |
| 2739 | + | "notes": "A repository's list holds its own runners (`repo` set) and the workspace's that its runner group lets it use. A runner is `offline` when it has not polled for 90 seconds." | |
| 2740 | + | }, | |
| 2741 | + | "list_runners_for_workspace": { | |
| 2742 | + | "params": { | |
| 2743 | + | "workspace": "flagon-io" | |
| 2744 | + | }, | |
| 2745 | + | "response": [ | |
| 2746 | + | { | |
| 2747 | + | "id": "rnr_01kq2m8v4c7f0h3k6n9q2t5w8z", | |
| 2748 | + | "name": "build-01", | |
| 2749 | + | "workspace": "flagon-io", | |
| 2750 | + | "repo": null, | |
| 2751 | + | "group": "Default", | |
| 2752 | + | "labels": [ | |
| 2753 | + | "self-hosted", | |
| 2754 | + | "linux", | |
| 2755 | + | "x64", | |
| 2756 | + | "gpu" | |
| 2757 | + | ], | |
| 2758 | + | "os": "linux", | |
| 2759 | + | "arch": "x64", | |
| 2760 | + | "version": "0.2.0", | |
| 2761 | + | "ephemeral": false, | |
| 2762 | + | "status": "busy", | |
| 2763 | + | "work": { | |
| 2764 | + | "kind": "workflow", | |
| 2765 | + | "id": "job_01kq2m9b1d4g7j0m3p6s9v2y5b", | |
| 2766 | + | "name": "test (linux, 24)", | |
| 2767 | + | "repo": "flagon-io/hello", | |
| 2768 | + | "run_id": "run_01kq2m9a8c1f4h7k0n3q6t9w2z", | |
| 2769 | + | "started_at": "2026-10-06T14:02:11.000Z" | |
| 2770 | + | }, | |
| 2771 | + | "last_seen_at": "2026-10-06T14:05:40.512Z", | |
| 2772 | + | "created_at": "2026-10-02T09:30:00.000Z", | |
| 2773 | + | "created_by": null | |
| 2774 | + | }, | |
| 2775 | + | { | |
| 2776 | + | "id": "rnr_01kq2m8v4c7f0h3k6n9q2t5w9a", | |
| 2777 | + | "name": "mac-mini", | |
| 2778 | + | "workspace": "flagon-io", | |
| 2779 | + | "repo": null, | |
| 2780 | + | "group": "Default", | |
| 2781 | + | "labels": [ | |
| 2782 | + | "self-hosted", | |
| 2783 | + | "macos", | |
| 2784 | + | "arm64" | |
| 2785 | + | ], | |
| 2786 | + | "os": "macos", | |
| 2787 | + | "arch": "arm64", | |
| 2788 | + | "version": "0.2.0", | |
| 2789 | + | "ephemeral": false, | |
| 2790 | + | "status": "online", | |
| 2791 | + | "work": null, | |
| 2792 | + | "last_seen_at": "2026-10-06T14:05:40.512Z", | |
| 2793 | + | "created_at": "2026-10-02T09:30:00.000Z", | |
| 2794 | + | "created_by": null | |
| 2795 | + | } | |
| 2796 | + | ] | |
| 2797 | + | }, | |
| 2798 | + | "create_runner_registration_token": { | |
| 2799 | + | "response": { | |
| 2800 | + | "token": "g1trt_…", | |
| 2801 | + | "expires_at": "2026-10-06T15:04:00.000Z", | |
| 2802 | + | "workspace": "flagon-io", | |
| 2803 | + | "repo": "flagon-io/hello", | |
| 2804 | + | "group": null, | |
| 2805 | + | "url": "https://g1t.sh" | |
| 2806 | + | }, | |
| 2807 | + | "notes": "Pass it to `g1t-runner register --url https://g1t.sh --token …` within the hour. See [self-hosted runners](/guides/self-hosted-runners/)." | |
| 2808 | + | }, | |
| 2809 | + | "create_runner_registration_token_for_workspace": { | |
| 2810 | + | "params": { | |
| 2811 | + | "workspace": "flagon-io" | |
| 2812 | + | }, | |
| 2813 | + | "request": { | |
| 2814 | + | "group": "Default" | |
| 2815 | + | }, | |
| 2816 | + | "response": { | |
| 2817 | + | "token": "g1trt_…", | |
| 2818 | + | "expires_at": "2026-10-06T15:04:00.000Z", | |
| 2819 | + | "workspace": "flagon-io", | |
| 2820 | + | "repo": null, | |
| 2821 | + | "group": "Default", | |
| 2822 | + | "url": "https://g1t.sh" | |
| 2823 | + | } | |
| 2824 | + | }, | |
| 2825 | + | "remove_runner": { | |
| 2826 | + | "params": { | |
| 2827 | + | "id": "rnr_01kq2m8v4c7f0h3k6n9q2t5w8z" | |
| 2828 | + | }, | |
| 2829 | + | "response": true | |
| 2830 | + | }, | |
| 2831 | + | "remove_runner_for_workspace": { | |
| 2832 | + | "params": { | |
| 2833 | + | "workspace": "flagon-io", | |
| 2834 | + | "id": "rnr_01kq2m8v4c7f0h3k6n9q2t5w8z" | |
| 2835 | + | }, | |
| 2836 | + | "response": true | |
| 2837 | + | }, | |
| 2838 | + | "get_runner_settings": { | |
| 2839 | + | "response": { | |
| 2840 | + | "agents_on_self_hosted": false, | |
| 2841 | + | "agent_labels": [ | |
| 2842 | + | "self-hosted", | |
| 2843 | + | "linux" | |
| 2844 | + | ], | |
| 2845 | + | "fork_pull_requests": false, | |
| 2846 | + | "inherited": true | |
| 2847 | + | } | |
| 2848 | + | }, | |
| 2849 | + | "get_runner_settings_for_workspace": { | |
| 2850 | + | "params": { | |
| 2851 | + | "workspace": "flagon-io" | |
| 2852 | + | }, | |
| 2853 | + | "response": { | |
| 2854 | + | "agents_on_self_hosted": true, | |
| 2855 | + | "agent_labels": [ | |
| 2856 | + | "self-hosted", | |
| 2857 | + | "linux" | |
| 2858 | + | ], | |
| 2859 | + | "fork_pull_requests": false, | |
| 2860 | + | "inherited": false | |
| 2861 | + | } | |
| 2862 | + | }, | |
| 2863 | + | "update_runner_settings": { | |
| 2864 | + | "request": { | |
| 2865 | + | "inherit": true | |
| 2866 | + | }, | |
| 2867 | + | "response": { | |
| 2868 | + | "agents_on_self_hosted": false, | |
| 2869 | + | "agent_labels": [ | |
| 2870 | + | "self-hosted", | |
| 2871 | + | "linux" | |
| 2872 | + | ], | |
| 2873 | + | "fork_pull_requests": false, | |
| 2874 | + | "inherited": true | |
| 2875 | + | } | |
| 2876 | + | }, | |
| 2877 | + | "update_runner_settings_for_workspace": { | |
| 2878 | + | "params": { | |
| 2879 | + | "workspace": "flagon-io" | |
| 2880 | + | }, | |
| 2881 | + | "request": { | |
| 2882 | + | "agents_on_self_hosted": true, | |
| 2883 | + | "agent_labels": [ | |
| 2884 | + | "linux" | |
| 2885 | + | ] | |
| 2886 | + | }, | |
| 2887 | + | "response": { | |
| 2888 | + | "agents_on_self_hosted": true, | |
| 2889 | + | "agent_labels": [ | |
| 2890 | + | "self-hosted", | |
| 2891 | + | "linux" | |
| 2892 | + | ], | |
| 2893 | + | "fork_pull_requests": false, | |
| 2894 | + | "inherited": false | |
| 2895 | + | }, | |
| 2896 | + | "notes": "Agent work on your runners still uses g1t's model proxy, paid as before, unless the workspace has its own model provider." | |
| 2897 | + | }, | |
| 2898 | + | "list_runner_groups": { | |
| 2899 | + | "params": { | |
| 2900 | + | "workspace": "flagon-io" | |
| 2901 | + | }, | |
| 2902 | + | "response": [ | |
| 2903 | + | { | |
| 2904 | + | "id": "rng_01kq2m7r2a5d8g1j4m7p0s3v6y", | |
| 2905 | + | "name": "Default", | |
| 2906 | + | "default": true, | |
| 2907 | + | "repositories": [], | |
| 2908 | + | "runners": 2, | |
| 2909 | + | "updated_at": "2026-10-02T09:30:00.000Z" | |
| 2910 | + | }, | |
| 2911 | + | { | |
| 2912 | + | "id": "rng_01kq2m7r2a5d8g1j4m7p0s3v6z", | |
| 2913 | + | "name": "GPU", | |
| 2914 | + | "default": false, | |
| 2915 | + | "repositories": [ | |
| 2916 | + | "hello", | |
| 2917 | + | "models" | |
| 2918 | + | ], | |
| 2919 | + | "runners": 1, | |
| 2920 | + | "updated_at": "2026-10-05T11:20:00.000Z" | |
| 2921 | + | } | |
| 2922 | + | ] | |
| 2923 | + | }, | |
| 2924 | + | "create_runner_group": { | |
| 2925 | + | "params": { | |
| 2926 | + | "workspace": "flagon-io" | |
| 2927 | + | }, | |
| 2928 | + | "request": { | |
| 2929 | + | "name": "GPU", | |
| 2930 | + | "repositories": [ | |
| 2931 | + | "hello", | |
| 2932 | + | "models" | |
| 2933 | + | ] | |
| 2934 | + | }, | |
| 2935 | + | "response": { | |
| 2936 | + | "id": "rng_01kq2m7r2a5d8g1j4m7p0s3v6z", | |
| 2937 | + | "name": "GPU", | |
| 2938 | + | "default": false, | |
| 2939 | + | "repositories": [ | |
| 2940 | + | "hello", | |
| 2941 | + | "models" | |
| 2942 | + | ], | |
| 2943 | + | "runners": 1, | |
| 2944 | + | "updated_at": "2026-10-05T11:20:00.000Z" | |
| 2945 | + | } | |
| 2946 | + | }, | |
| 2947 | + | "update_runner_group": { | |
| 2948 | + | "params": { | |
| 2949 | + | "workspace": "flagon-io", | |
| 2950 | + | "id": "rng_01kq2m7r2a5d8g1j4m7p0s3v6z" | |
| 2951 | + | }, | |
| 2952 | + | "request": { | |
| 2953 | + | "repositories": [] | |
| 2954 | + | }, | |
| 2955 | + | "response": { | |
| 2956 | + | "id": "rng_01kq2m7r2a5d8g1j4m7p0s3v6z", | |
| 2957 | + | "name": "GPU", | |
| 2958 | + | "default": false, | |
| 2959 | + | "repositories": [], | |
| 2960 | + | "runners": 1, | |
| 2961 | + | "updated_at": "2026-10-05T11:20:00.000Z" | |
| 2962 | + | } | |
| 2963 | + | }, | |
| 2964 | + | "delete_runner_group": { | |
| 2965 | + | "params": { | |
| 2966 | + | "workspace": "flagon-io", | |
| 2967 | + | "id": "rng_01kq2m7r2a5d8g1j4m7p0s3v6z" | |
| 2968 | + | }, | |
| 2969 | + | "response": true | |
| 2970 | + | }, | |
| 2686 | 2971 | "list_webhooks": { | |
| 2687 | 2972 | "response": [ | |
| 2688 | 2973 | { |
| 99 | 99 | Op::DeleteRepo => through::<repos::DeletedRepo>(op, sent), | |
| 100 | 100 | Op::ListDeletedRepos => through::<Vec<repos::DeletedRepo>>(op, sent), | |
| 101 | 101 | Op::GetRepoSettings | Op::UpdateRepoSettings => through::<work::RepoSettings>(op, sent), | |
| 102 | + | Op::ListCheckNames => through::<Vec<work::SeenCheck>>(op, sent), | |
| 102 | 103 | Op::GetMergeQueue => through::<work::QueueView>(op, sent), | |
| 103 | 104 | Op::ListIssues => through::<Vec<work::Issue>>(op, sent), | |
| 104 | 105 | Op::CreateIssue | Op::UpdateIssue | Op::CloseIssue | Op::ReopenIssue => { | |
| 119 | 120 | through::<actions::WorkflowRun>(op, sent) | |
| 120 | 121 | } | |
| 121 | 122 | Op::ListActionsSecrets | Op::ListActionsVariables => through::<Vec<actions::Setting>>(op, sent), | |
| 123 | + | Op::ListRunners => through::<Vec<g1t_contracts::runners::Runner>>(op, sent), | |
| 124 | + | Op::ListRunnerGroups => through::<Vec<g1t_contracts::runners::RunnerGroup>>(op, sent), | |
| 125 | + | Op::CreateRunnerGroup | Op::UpdateRunnerGroup => through::<g1t_contracts::runners::RunnerGroup>(op, sent), | |
| 126 | + | Op::GetRunnerSettings | Op::UpdateRunnerSettings => through::<g1t_contracts::runners::RunnerSettings>(op, sent), | |
| 127 | + | Op::CreateRunnerRegistrationToken => through::<g1t_contracts::runners::RegistrationToken>(op, sent), | |
| 122 | 128 | Op::ListWebhooks => through::<Vec<webhooks::Hook>>(op, sent), | |
| 123 | 129 | Op::ListIntegrations => through::<Vec<integrations::Connection>>(op, sent), | |
| 124 | 130 | Op::GetModelRoutes | Op::SetModelRoutes => through::<Vec<integrations::ModelRoute>>(op, sent), |
| 112 | 112 | Op::UpdateRepoSettings, | |
| 113 | 113 | &[], | |
| 114 | 114 | ), | |
| 115 | + | route("GET", "/repos/:owner/:name/check-names", Op::ListCheckNames, &[]), | |
| 115 | 116 | route("GET", "/repos/:owner/:name/queue", Op::GetMergeQueue, &[]), | |
| 116 | 117 | route( | |
| 117 | 118 | "POST", | |
| 488 | 489 | Op::DeleteActionsVariable, | |
| 489 | 490 | &[], | |
| 490 | 491 | ), | |
| 492 | + | // Self-hosted runners: a repository's own, or a workspace's. | |
| 493 | + | route("GET", "/repos/:owner/:name/actions/runners", Op::ListRunners, &[]), | |
| 494 | + | route("POST", "/repos/:owner/:name/actions/runners/registration-token", Op::CreateRunnerRegistrationToken, &[]), | |
| 495 | + | route("DELETE", "/repos/:owner/:name/actions/runners/:id", Op::RemoveRunner, &[]), | |
| 496 | + | route("GET", "/repos/:owner/:name/actions/runner-settings", Op::GetRunnerSettings, &[]), | |
| 497 | + | route("PATCH", "/repos/:owner/:name/actions/runner-settings", Op::UpdateRunnerSettings, &[]), | |
| 498 | + | route("GET", "/workspaces/:workspace/actions/runners", Op::ListRunners, &[]), | |
| 499 | + | route("POST", "/workspaces/:workspace/actions/runners/registration-token", Op::CreateRunnerRegistrationToken, &[]), | |
| 500 | + | route("DELETE", "/workspaces/:workspace/actions/runners/:id", Op::RemoveRunner, &[]), | |
| 501 | + | route("GET", "/workspaces/:workspace/actions/runner-settings", Op::GetRunnerSettings, &[]), | |
| 502 | + | route("PATCH", "/workspaces/:workspace/actions/runner-settings", Op::UpdateRunnerSettings, &[]), | |
| 503 | + | route("GET", "/workspaces/:workspace/actions/runner-groups", Op::ListRunnerGroups, &[]), | |
| 504 | + | route("POST", "/workspaces/:workspace/actions/runner-groups", Op::CreateRunnerGroup, &[]), | |
| 505 | + | route("PATCH", "/workspaces/:workspace/actions/runner-groups/:id", Op::UpdateRunnerGroup, &[]), | |
| 506 | + | route("DELETE", "/workspaces/:workspace/actions/runner-groups/:id", Op::DeleteRunnerGroup, &[]), | |
| 491 | 507 | route("POST", "/repos/:owner/:name/plans", Op::PlanWork, &[]), | |
| 492 | 508 | route("GET", "/repos/:owner/:name/plans/:plan", Op::GetPlan, &[]), | |
| 493 | 509 | route( |
| 1 | + | //! What a self-hosted runner calls: registering with a registration token, | |
| 2 | + | //! then polling for work, saying how work ended and removing itself, with | |
| 3 | + | //! its own credential. Neither is a g1t access token, so these are served | |
| 4 | + | //! before anything reads one, and no other endpoint accepts either. | |
| 5 | + | //! | |
| 6 | + | //! | Route | Body | | |
| 7 | + | //! | --- | --- | | |
| 8 | + | //! | `POST /runners/register` | `token`, `name`, `labels`, `os`, `arch`, `version`, `ephemeral`, `group`, `replace` | | |
| 9 | + | //! | `POST /runners/{id}/poll` | `version`, `running`, `wait_ms` | | |
| 10 | + | //! | `POST /runners/{id}/finished` | `id`, `exit_code`, `reason` | | |
| 11 | + | //! | `POST /runners/{id}/remove` | nothing | | |
| 12 | + | //! | |
| 13 | + | //! The last three send `Authorization: Bearer g1tr_…`. See | |
| 14 | + | //! `g1t_contracts::runners` for what each answers. | |
| 15 | + | ||
| 16 | + | use g1t_contracts::Outcome; | |
| 17 | + | use g1t_kit::wire; | |
| 18 | + | use serde_json::{Value, json}; | |
| 19 | + | use worker::{Request, Response, Result}; | |
| 20 | + | ||
| 21 | + | use crate::operations::Services; | |
| 22 | + | use crate::{fail, failure, json_body}; | |
| 23 | + | ||
| 24 | + | /// What is passed through as given: an agent task's environment. | |
| 25 | + | const AS_GIVEN: &[&str] = &["env"]; | |
| 26 | + | ||
| 27 | + | fn credential(request: &Request) -> Result<String> { | |
| 28 | + | let header = request.headers().get("authorization")?.unwrap_or_default(); | |
| 29 | + | Ok(match header.split_once(' ') { | |
| 30 | + | Some((scheme, token)) if scheme.eq_ignore_ascii_case("bearer") => token.trim().to_owned(), | |
| 31 | + | _ => String::new(), | |
| 32 | + | }) | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | async fn answer(services: &Services, method: &str, args: &Value) -> Result<Response> { | |
| 36 | + | let answered: Outcome<Value> = g1t_kit::call(&services.actions, method, args).await?; | |
| 37 | + | match answered { | |
| 38 | + | Outcome::Ok(value) => Response::from_json(&wire::snake_case_keeping(value, AS_GIVEN)), | |
| 39 | + | Outcome::Fail(refused) => failure(&refused), | |
| 40 | + | } | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | /// Serves `path` if it is one of the runner's routes. | |
| 44 | + | pub async fn handle(request: &mut Request, services: &Services, path: &str) -> Result<Option<Response>> { | |
| 45 | + | let Some(rest) = path.strip_prefix("/runners/") else { return Ok(None) }; | |
| 46 | + | let body = json_body(request).await; | |
| 47 | + | let body = if body.is_object() { body } else { json!({}) }; | |
| 48 | + | if rest == "register" { | |
| 49 | + | let args = json!({ | |
| 50 | + | "token": body["token"].as_str().unwrap_or_default(), | |
| 51 | + | "name": body["name"].as_str().unwrap_or_default(), | |
| 52 | + | "labels": body["labels"].as_array().cloned().unwrap_or_default(), | |
| 53 | + | "os": body["os"].as_str().unwrap_or_default(), | |
| 54 | + | "arch": body["arch"].as_str().unwrap_or_default(), | |
| 55 | + | "version": body["version"].as_str().unwrap_or_default(), | |
| 56 | + | "ephemeral": body["ephemeral"].as_bool().unwrap_or(false), | |
| 57 | + | "group": body["group"].as_str(), | |
| 58 | + | "replace": body["replace"].as_bool().unwrap_or(false), | |
| 59 | + | }); | |
| 60 | + | return Ok(Some(answer(services, "runner_register", &args).await?)); | |
| 61 | + | } | |
| 62 | + | let Some((runner, action)) = rest.split_once('/') else { | |
| 63 | + | return Ok(Some(fail(g1t_contracts::FailureCode::NotFound, "No such endpoint.")?)); | |
| 64 | + | }; | |
| 65 | + | let auth = json!({ "runner": runner, "credential": credential(request)? }); | |
| 66 | + | let mut args = auth; | |
| 67 | + | let method = match action { | |
| 68 | + | "poll" => { | |
| 69 | + | args["version"] = json!(body["version"].as_str().unwrap_or_default()); | |
| 70 | + | args["running"] = json!(body["running"].as_array().cloned().unwrap_or_default()); | |
| 71 | + | args["wait_ms"] = json!(body["wait_ms"].as_u64().unwrap_or(0)); | |
| 72 | + | "runner_poll" | |
| 73 | + | } | |
| 74 | + | "finished" => { | |
| 75 | + | args["id"] = json!(body["id"].as_str().unwrap_or_default()); | |
| 76 | + | args["exit_code"] = json!(body["exit_code"].as_i64().unwrap_or(1)); | |
| 77 | + | args["reason"] = body["reason"].clone(); | |
| 78 | + | "runner_finished" | |
| 79 | + | } | |
| 80 | + | "remove" => "runner_remove_self", | |
| 81 | + | _ => return Ok(Some(fail(g1t_contracts::FailureCode::NotFound, "No such endpoint.")?)), | |
| 82 | + | }; | |
| 83 | + | Ok(Some(answer(services, method, &args).await?)) | |
| 84 | + | } |
| 64 | 64 | a("get", Op::GetRepo, "One repository"), | |
| 65 | 65 | a("create", Op::CreateRepo, "Create one, empty or copied from a public git URL"), | |
| 66 | 66 | a("update", Op::UpdateRepo, "Change description, website, topics, default branch, protection"), | |
| 67 | − | a("get_settings", Op::GetRepoSettings, "How pull requests merge"), | |
| 68 | − | a("update_settings", Op::UpdateRepoSettings, "Change how pull requests merge"), | |
| 67 | + | a("get_settings", Op::GetRepoSettings, "Branch protection: required checks, approvals, how pull requests merge"), | |
| 68 | + | a("update_settings", Op::UpdateRepoSettings, "Change branch protection and how pull requests merge"), | |
| 69 | + | a("check_names", Op::ListCheckNames, "Check names reported lately, to require on the default branch"), | |
| 69 | 70 | a("list_labels", Op::ListLabels, "Labels in use"), | |
| 70 | 71 | a("list_events", Op::ListEvents, "Timeline: pushes, issues, pull requests, comments"), | |
| 71 | 72 | a("rename_branch", Op::RenameBranch, "Rename a branch"), | |
| 87 | 88 | default_action: None, | |
| 88 | 89 | actions: &[ | |
| 89 | 90 | a("list", Op::ListIssues, "Issues on a repository, newest first"), | |
| 90 | − | a("get", Op::GetIssue, "One issue with comments, checks and its pull requests"), | |
| 91 | + | a("get", Op::GetIssue, "One issue with comments and its pull requests"), | |
| 91 | 92 | a("create", Op::CreateIssue, "Open an issue"), | |
| 92 | 93 | a("update", Op::UpdateIssue, "Change title, body, labels or assignees"), | |
| 93 | 94 | a("close", Op::CloseIssue, "Close it without a pull request"), | |
| 103 | 104 | default_action: None, | |
| 104 | 105 | actions: &[ | |
| 105 | 106 | a("list", Op::ListPullRequests, "Pull requests on a repository, newest first"), | |
| 106 | − | a("get", Op::GetPullRequest, "Status, checks, reviews, overlaps and whether it is behind"), | |
| 107 | + | a("get", Op::GetPullRequest, "Status, checks and required checks, reviews, overlaps, whether it is behind"), | |
| 107 | 108 | a("changes", Op::GetPullRequestChanges, "Files and line-by-line diff"), | |
| 108 | 109 | a("create", Op::CreatePullRequest, "Start a draft with its own fork to push to, or open one from a pushed branch"), | |
| 109 | 110 | a("record_session", Op::RecordSession, "Append prompt, reasoning and tool entries to its session"), | |
| 131 | 132 | Tool { | |
| 132 | 133 | name: "plan", | |
| 133 | 134 | title: "Plans", | |
| 134 | − | description: "Turn an outcome into issues: an agent proposes them with checks and dependencies; nothing opens until you apply the plan.", | |
| 135 | + | description: "Turn an outcome into issues: an agent proposes them with what done means and their dependencies; nothing opens until you apply the plan.", | |
| 135 | 136 | default_action: None, | |
| 136 | 137 | actions: &[ | |
| 137 | 138 | a("create", Op::PlanWork, "Ask an agent for a plan; read it with get until ready"), | |
| 152 | 153 | Tool { | |
| 153 | 154 | name: "workflow", | |
| 154 | 155 | title: "Workflows", | |
| 155 | − | description: "GitHub Actions workflows from .g1t/workflows: their runs, jobs and logs, and running, cancelling or rerunning them.", | |
| 156 | + | description: "GitHub Actions workflows from .g1t/workflows: their runs, jobs and logs, and running, cancelling or rerunning them. Also the self-hosted runners they run on: a workspace's (`workspace`) or a repository's own (`repo`), their groups, and where agent work runs.", | |
| 156 | 157 | default_action: None, | |
| 157 | 158 | actions: &[ | |
| 158 | 159 | a("list", Op::ListWorkflows, "Workflows on the default branch"), | |
| 163 | 164 | a("cancel", Op::CancelWorkflowRun, "Cancel a run"), | |
| 164 | 165 | a("rerun", Op::RerunWorkflowRun, "Run a finished run again"), | |
| 165 | 166 | a("update", Op::UpdateWorkflow, "Turn a workflow on or off"), | |
| 167 | + | a("list_runners", Op::ListRunners, "Self-hosted runners, with status, labels and what each is doing"), | |
| 168 | + | a("create_runner_token", Op::CreateRunnerRegistrationToken, "A one-hour token for g1t-runner register"), | |
| 169 | + | a("remove_runner", Op::RemoveRunner, "Remove a self-hosted runner"), | |
| 170 | + | a("list_runner_groups", Op::ListRunnerGroups, "A workspace's runner groups"), | |
| 171 | + | a("create_runner_group", Op::CreateRunnerGroup, "Make a group, for some repositories"), | |
| 172 | + | a("update_runner_group", Op::UpdateRunnerGroup, "Rename a group or change its repositories"), | |
| 173 | + | a("delete_runner_group", Op::DeleteRunnerGroup, "Delete a group; its runners join the default"), | |
| 174 | + | a("get_runner_settings", Op::GetRunnerSettings, "Where agent work runs; whether forks may use runners"), | |
| 175 | + | a("update_runner_settings", Op::UpdateRunnerSettings, "Change them"), | |
| 166 | 176 | ], | |
| 167 | 177 | }, | |
| 168 | 178 | Tool { | |
| 272 | 282 | | Op::SetModelRoutes | |
| 273 | 283 | | Op::SetBasePermission | |
| 274 | 284 | | Op::MergePullRequest | |
| 285 | + | | Op::RemoveRunner | |
| 286 | + | | Op::DeleteRunnerGroup | |
| 287 | + | | Op::UpdateRunnerSettings | |
| 275 | 288 | ) | |
| 276 | 289 | } | |
| 277 | 290 |
| 3 | 3 | "name": "g1t-api", | |
| 4 | 4 | "account_id": "1e6f2cffa3f445920836e8ebe446bb58", | |
| 5 | 5 | "compatibility_date": "2026-09-26", | |
| 6 | + | "placement": { "mode": "off" }, | |
| 6 | 7 | "main": "build/index.js", | |
| 7 | 8 | "build": { "command": "node ../../scripts/build-rust-worker.mjs" }, | |
| 8 | 9 | "workers_dev": false, | |
| 28 | 29 | { "binding": "CONTEXT", "service": "g1t-context" }, | |
| 29 | 30 | { "binding": "SEARCH", "service": "g1t-search" } | |
| 30 | 31 | ], | |
| 31 | − | // GitHub Actions artifacts and cache, in chunks, with KV's own expiry. | |
| 32 | − | // (Moves to R2 once R2 is enabled on the account.) | |
| 32 | + | // GitHub Actions artifacts, in chunks, with KV's own expiry (and cache | |
| 33 | + | // entries saved before the cache moved to R2, until they expire). | |
| 33 | 34 | "kv_namespaces": [{ "binding": "BLOBS", "id": "16a4232cb746418db53782aa068be693" }], | |
| 35 | + | // actions/cache entries, up to 2 GB each, uploaded in parts. The actions | |
| 36 | + | // service lists them and decides what is kept (services/actions/src/cache.rs). | |
| 37 | + | "r2_buckets": [{ "binding": "ACTIONS_CACHE", "bucket_name": "g1t-actions-cache" }], | |
| 34 | 38 | "observability": { "enabled": true } | |
| 35 | 39 | } |
| 92 | 92 | { label: 'Model providers', slug: 'guides/models' }, | |
| 93 | 93 | { label: 'Webhooks', slug: 'guides/webhooks' }, | |
| 94 | 94 | { label: 'GitHub Actions', slug: 'guides/actions' }, | |
| 95 | + | { label: 'Self-hosted runners', slug: 'guides/self-hosted-runners' }, | |
| 95 | 96 | ], | |
| 96 | 97 | }, | |
| 97 | 98 | { | |
| 123 | 124 | items: [ | |
| 124 | 125 | { label: 'API overview', slug: 'reference/api' }, | |
| 125 | 126 | { label: 'MCP tools', slug: 'reference/mcp' }, | |
| 127 | + | { label: "What g1t can't do yet", slug: 'reference/limitations' }, | |
| 126 | 128 | { label: 'Try it in the explorer', link: '/api/reference/', attrs: { target: '_self' } }, | |
| 127 | 129 | { label: 'OpenAPI document', link: 'https://api.g1t.sh/openapi.json' }, | |
| 128 | 130 | { label: 'llms.txt', link: 'https://g1t.sh/llms.txt' }, | |
| 129 | 131 | ], | |
| 130 | 132 | }, | |
| 133 | + | { | |
| 134 | + | label: 'About', | |
| 135 | + | items: [{ label: 'An open letter to Cloudflare', slug: 'about/open-letter-to-cloudflare' }], | |
| 136 | + | }, | |
| 131 | 137 | ...apiGroups, | |
| 132 | 138 | ], | |
| 133 | 139 | }), |
| 1 | + | --- | |
| 2 | + | title: An open letter to Cloudflare | |
| 3 | + | description: From the team at Flagon, Inc. building g1t, a git platform that runs entirely on Cloudflare. What works, where we hit walls, and what we'd ask for. | |
| 4 | + | --- | |
| 5 | + | ||
| 6 | + | Dear Cloudflare, | |
| 7 | + | ||
| 8 | + | We're the small team at Flagon, Inc. building g1t, a git platform where | |
| 9 | + | people and coding agents work in the same issues, pull requests and merge | |
| 10 | + | queue. Every part of it runs on you: about twenty Workers, a D1 database per | |
| 11 | + | service, Artifacts for every repository and every pull request, Containers | |
| 12 | + | for agents and CI, R2, KV, Queues, and Cloudflare for SaaS for our customers' | |
| 13 | + | domains. We have no servers. | |
| 14 | + | ||
| 15 | + | This is a thank-you, and a list of what would help us most next. | |
| 16 | + | ||
| 17 | + | ## Why we built on you | |
| 18 | + | ||
| 19 | + | A git platform is usually a fleet: storage nodes, a job system, a database | |
| 20 | + | cluster, a CDN in front, and people on call for all of it. We wanted to run | |
| 21 | + | one for thousands of teams with a handful of people. You offered a global | |
| 22 | + | platform where the unit of work is a request, the unit of storage is a | |
| 23 | + | Durable Object, and nothing costs money while nobody is using it. | |
| 24 | + | ||
| 25 | + | Artifacts is the reason g1t exists in this shape. One Durable Object per | |
| 26 | + | repository is the right isolation unit: a busy repository doesn't slow its | |
| 27 | + | neighbours, and there is nothing to shard by hand. It speaks real git, so | |
| 28 | + | stock clients cloned, fetched and pushed through our proxy from the first | |
| 29 | + | day. `fork()` is a single call, and per-pull-request isolation for agents | |
| 30 | + | fell out of it almost for free. Scoped tokens that expire on their own let | |
| 31 | + | us hand a sandbox a credential that dies with it. The read binding powers | |
| 32 | + | every page we render, blame, mergeability and search, without a git client | |
| 33 | + | anywhere. | |
| 34 | + | ||
| 35 | + | The rest of the platform held up too. Rust compiled to WebAssembly runs our | |
| 36 | + | services. D1's read replication is free and good. Containers gave us | |
| 37 | + | sandboxes in three sizes. With a cached credential and ref listing, a | |
| 38 | + | `git fetch` with nothing new answers in under half a second, and most of | |
| 39 | + | our pages answer in under 250 ms. We went from an empty repository to a | |
| 40 | + | launch on this stack, and most of it worked the first time. | |
| 41 | + | ||
| 42 | + | ## Where we hit walls | |
| 43 | + | ||
| 44 | + | Running a real platform for many teams found the edges. None of these | |
| 45 | + | stopped us. Each one costs us code, latency or certainty, and each one will | |
| 46 | + | cost the next team building on you the same. | |
| 47 | + | ||
| 48 | + | **What a billable operation is.** Artifacts pricing names "repo operations, | |
| 49 | + | such as create, push, pull, and clone", and the metrics list a different set | |
| 50 | + | of event names. Neither says whether binding reads, token mints or ref | |
| 51 | + | listings count. We price from cost, so this decides what our customers pay. | |
| 52 | + | Depending on the answer, our model for a few thousand workspaces lands | |
| 53 | + | anywhere between about $1.8k and $31k a month. Today we count every clone, | |
| 54 | + | fetch and push ourselves, and hope it matches. | |
| 55 | + | ||
| 56 | + | **What a fork stores.** Forks are the natural primitive for a pull request, | |
| 57 | + | and agents open pull requests by the thousand. We can't find whether a fork | |
| 58 | + | shares objects with its source or copies them. If it copies, an agent-heavy | |
| 59 | + | account reaches the 1 TB account limit in days, and at that point every push | |
| 60 | + | in the account fails, for every customer at once. We keep forks for now, | |
| 61 | + | and are measuring it ourselves. | |
| 62 | + | ||
| 63 | + | **A write path and a pre-receive hook.** The binding reads, but it can't | |
| 64 | + | list refs, move them, or write objects. So to land a pull request we speak | |
| 65 | + | git's wire protocol to our own storage from inside a Worker, buffering packs | |
| 66 | + | in an isolate with 128 MB to share. With no hook before refs move, branch | |
| 67 | + | protection and secret scanning only hold for pushes through our proxy, which | |
| 68 | + | parses every pack in WebAssembly before forwarding it. We wrote a second | |
| 69 | + | implementation of git's pack format to get there. | |
| 70 | + | ||
| 71 | + | **Ref-change events and the cost of a credential.** Push events need one | |
| 72 | + | subscription per repository, which doesn't scale to tens of thousands of | |
| 73 | + | repositories and forks. We record ref changes ourselves, and a test scans | |
| 74 | + | our own source to make sure every code path that moves a ref says so. Every | |
| 75 | + | git credential takes three binding calls and about 0.8 s to mint, so we | |
| 76 | + | cache sealed tokens across isolates in KV. | |
| 77 | + | ||
| 78 | + | **Placement that follows data.** Smart Placement once ran our site in | |
| 79 | + | Amsterdam for a visitor in Denver, while every D1 primary we have is in | |
| 80 | + | western North America. Every query crossed the Atlantic, and our Explore | |
| 81 | + | page took 0.85 s instead of 0.17 s. We turned placement off everywhere and | |
| 82 | + | measure each Worker by hand. | |
| 83 | + | ||
| 84 | + | **D1 sessions across service bindings.** Read replicas need a bookmark to | |
| 85 | + | give read-your-writes. Our site calls seven services, each with its own | |
| 86 | + | database, so we built a header protocol to carry bookmarks through service | |
| 87 | + | bindings into a cookie and back. | |
| 88 | + | ||
| 89 | + | **Containers that build images and keep disks.** We found no supported way | |
| 90 | + | to run Docker or BuildKit in a Container, so our own CI can't rebuild our | |
| 91 | + | sandbox image; that waits for a machine outside. Container disk is | |
| 92 | + | ephemeral, so the self-hostable git store we'd like as a warm fallback has | |
| 93 | + | to live off Cloudflare. | |
| 94 | + | ||
| 95 | + | **Inbound TCP for SSH.** Git users expect `git@host:owner/repo`. Workers | |
| 96 | + | take no inbound TCP, so g1t is HTTPS only. We've applied for the beta and | |
| 97 | + | are waiting. | |
| 98 | + | ||
| 99 | + | ## What we built in the meantime | |
| 100 | + | ||
| 101 | + | A per-workspace operation counter that is our best guess at your invoice. A | |
| 102 | + | fork sweep we can switch on once we know what forks cost. A smart HTTP | |
| 103 | + | client inside a Worker for landing, catch-up, mirrors and imports. Our own | |
| 104 | + | push policy in front of Artifacts. A versioned ref cache with a test that | |
| 105 | + | guards it. Two layers of credential caching. A bookmark protocol for D1. | |
| 106 | + | A probe that deploys throwaway Workers to measure placement, and a | |
| 107 | + | `Server-Timing` header on every response so we see the next regression. | |
| 108 | + | Image builds on a laptop. | |
| 109 | + | ||
| 110 | + | All of it works. Most of it is code we'd happily delete. | |
| 111 | + | ||
| 112 | + | ## What we're asking for | |
| 113 | + | ||
| 114 | + | 1. A published definition of a billable Artifacts operation, with | |
| 115 | + | per-repository metrics that use the same names as the invoice. | |
| 116 | + | 2. Documented fork storage, an expiry on `fork()`, a repository's stored | |
| 117 | + | bytes in `info()`, and a warning before the account storage limit, with | |
| 118 | + | failures per repository rather than account-wide. | |
| 119 | + | 3. Ref listing, atomic compare-and-swap ref updates and streaming pack | |
| 120 | + | writes in the binding. | |
| 121 | + | 4. A pre-receive hook that a Worker answers. | |
| 122 | + | 5. Account-level ref-change events to a Queue, a read-after-write guarantee | |
| 123 | + | for refs, and git forwarding authenticated by the binding, with no token | |
| 124 | + | to mint. | |
| 125 | + | 6. Placement that accounts for D1 primaries and service bindings, and D1 as | |
| 126 | + | a placement target. | |
| 127 | + | 7. D1 sessions that travel across service bindings. | |
| 128 | + | 8. Image builds and persistent volumes for Containers. | |
| 129 | + | 9. Inbound TCP, so git can run over SSH. | |
| 130 | + | 10. A support path during the beta, snapshot restore and export for | |
| 131 | + | repositories, and a date for general availability with an SLA. | |
| 132 | + | ||
| 133 | + | ## Let's work on it together | |
| 134 | + | ||
| 135 | + | We chose you on purpose, and we'd choose you again. A small team running a | |
| 136 | + | global git platform with no servers is the promise of what you've built, | |
| 137 | + | and g1t shows that it mostly holds. We'd like to help close the | |
| 138 | + | rest of the gap: traces, test repositories, early builds to try, or a call | |
| 139 | + | with the teams involved. Whatever is useful. | |
| 140 | + | ||
| 141 | + | You can reach us through [g1t.sh](https://g1t.sh/support). Our code lives | |
| 142 | + | at [g1t.sh/flagon-io/g1t](https://g1t.sh/flagon-io/g1t), on the platform | |
| 143 | + | it describes. | |
| 144 | + | ||
| 145 | + | With thanks, | |
| 146 | + | ||
| 147 | + | The team at Flagon, Inc. |
| 5 | 5 | ||
| 6 | 6 | g1t is where people and agents ship software together. You hand g1t an | |
| 7 | 7 | outcome, and agents converge it onto `main`: each change is made in a pull | |
| 8 | − | request of its own, checked in a clean sandbox, reviewed, revised and merged | |
| 8 | + | request of its own, checked by your workflows, reviewed, revised and merged | |
| 9 | 9 | under your repository's rules. People work alongside the agents in the same | |
| 10 | 10 | repositories, issues, pull requests and reviews, and every change can deploy | |
| 11 | 11 | to the edge. | |
| 22 | 22 | | **Pull request** | A proposed change, in its own fork or on a branch. Usually made for an issue. | | |
| 23 | 23 | | **Session** | The record of how a pull request was made: prompts, reasoning, tool calls. | | |
| 24 | 24 | ||
| 25 | − | The part that is different from other forges: one issue routinely has | |
| 25 | + | What g1t adds: one issue routinely has | |
| 26 | 26 | several pull requests, each from a different agent, and g1t keeps track of | |
| 27 | 27 | which one was merged. | |
| 28 | 28 | ||
| 35 | 35 | An issue has: | |
| 36 | 36 | ||
| 37 | 37 | - a **title** and a **description** in Markdown. An agent given the issue | |
| 38 | − | works from this text; | |
| 38 | + | works from this text. It can say what done means in plain words, | |
| 39 | + | under a `## Definition of done` heading if you like: context for the | |
| 40 | + | agent and its reviewers, not something a merge waits on; | |
| 39 | 41 | - **labels**, which say what kind of issue it is; | |
| 40 | − | - **acceptance checks**: commands a pull request should make pass, which | |
| 41 | − | g1t [runs itself](#acceptance-checks); | |
| 42 | 42 | - **comments**; | |
| 43 | 43 | - a **state**: open or closed. A closed issue records why: `completed` or | |
| 44 | 44 | `not_planned`. | |
| 119 | 119 | that the issue should stay open. The pull request merges, and the issue and | |
| 120 | 120 | the other pull requests are left as they are. | |
| 121 | 121 | ||
| 122 | − | ## Acceptance checks | |
| 122 | + | ## Checks | |
| 123 | 123 | ||
| 124 | − | An issue can list **acceptance checks**: commands, such as `cargo test`, | |
| 125 | − | that a pull request for it should make pass. | |
| 124 | + | A pull request's checks are what the repository's | |
| 125 | + | [workflows](/guides/actions/) report on its head commit. Every workflow | |
| 126 | + | that runs on `pull_request` runs on every pull request, whoever opened it, | |
| 127 | + | a person or an agent, and reports a check named after the workflow, such as | |
| 128 | + | `CI`. | |
| 126 | 129 | ||
| 127 | − | When a pull request for that issue is ready for review, g1t runs the checks | |
| 128 | − | itself. It starts a sandbox that holds nothing but the pull request's head | |
| 129 | − | commit, runs each command there, and records whether it passed and what it | |
| 130 | − | printed. Pushing to the pull request runs them again. | |
| 130 | + | The default branch decides which checks a merge needs: its | |
| 131 | + | [required status checks](/guides/pull-requests/#required-status-checks). | |
| 132 | + | A pull request merges only once each of them has passed on its head. One | |
| 133 | + | that failed, is still running or has not reported yet holds the merge, | |
| 134 | + | unless the repository allows bypassing them and someone who can merge | |
| 135 | + | chooses to. Checks that are not required are shown, and never hold a merge. | |
| 131 | 136 | ||
| 132 | − | - The sandbox is clean. No agent has worked in it, so a pass says something | |
| 133 | − | about the code and not about what was left lying around. | |
| 134 | − | - Only that sandbox can report the result. An agent cannot mark its own work | |
| 135 | − | as passing. | |
| 136 | − | - Each pull request for an issue is checked the same way, which makes | |
| 137 | − | several of them comparable at a glance. | |
| 138 | − | ||
| 139 | − | A pull request whose checks have not passed cannot be merged, unless | |
| 140 | − | someone who can merge chooses to merge anyway. | |
| 141 | − | ||
| 142 | − | Checks run in g1t's sandboxes, so they need a workspace on the | |
| 143 | − | [g1t plan](/guides/usage-and-billing/#the-g1t-plan), or one with | |
| 144 | − | [trial credit](/guides/usage-and-billing/#the-trial) left. On a public | |
| 145 | − | repository, [g1t's open-source pool](/guides/usage-and-billing/#the-open-source-pool) | |
| 146 | − | runs them too, after a card check. | |
| 137 | + | The same rules hold for people and agents. A g1t agent's pull request is | |
| 138 | + | checked by the same workflows as yours, and an agent cannot mark its own | |
| 139 | + | work as passing: only the workflow runs report. | |
| 147 | 140 | ||
| 148 | 141 | ## Review | |
| 149 | 142 | ||
| 186 | 179 | ||
| 187 | 180 | Someone with the [Write role](/guides/access-and-roles/) or higher on the | |
| 188 | 181 | repository merges a pull request once it is | |
| 189 | − | marked ready and its checks have passed. Merging moves `main` to the pull | |
| 182 | + | marked ready and its [required checks](/guides/pull-requests/#required-status-checks) | |
| 183 | + | have passed. Merging moves `main` to the pull | |
| 190 | 184 | request's head commit, or, in a repository that merges through | |
| 191 | 185 | [the merge queue](/guides/merge-queue/), adds it to the queue. | |
| 192 | 186 | ||
| 200 | 194 | into the pull request in a sandbox: if the merge is clean, it is pushed as it | |
| 201 | 195 | is; if it conflicts, the agent is given the conflicted files and what the | |
| 202 | 196 | pull request is for, resolves them, and pushes the result, and the session | |
| 203 | − | records what was done. Either way the checks run again on the result | |
| 197 | + | records what was done. Either way the workflows run again on the result | |
| 204 | 198 | ([how catching up works](/guides/pull-requests/#catching-up)). You can also do it by | |
| 205 | 199 | hand: pull `main` into the fork or the branch, resolve, and push. `main` never loses a commit this way, however many | |
| 206 | 200 | pull requests are in flight. | |
| 210 | 204 | Merging one pull request at a time keeps every merge clean as text, but two | |
| 211 | 205 | changes can merge without a conflict and still break each other. A | |
| 212 | 206 | repository that turns on **Merge through a queue** tests each pull request | |
| 213 | − | together with the ones ahead of it, along with the checks of every issue | |
| 214 | − | already completed, and `main` only moves to a state whose checks passed. | |
| 207 | + | together with the ones ahead of it, and `main` only moves to a state whose | |
| 208 | + | required checks passed. | |
| 215 | 209 | See [merge queue](/guides/merge-queue/). | |
| 216 | 210 | ||
| 217 | 211 | ## Sessions and why-blame |
| 44 | 44 | | `secrets.*`, `vars.*`, `secrets.GITHUB_TOKEN` | The same. `secrets.G1T_TOKEN` is the workspace's own token for the run; `GITHUB_TOKEN` is its alias. | | |
| 45 | 45 | | `environment:` on a job | The job reads each key's row for that environment, as GitHub's environment secrets work. | | |
| 46 | 46 | | `actions/upload-artifact`, `actions/download-artifact` | Kept with the run for 14 days, passed between its jobs, and downloadable from the run's page. Up to 60 MB each. | | |
| 47 | − | | `actions/cache`, `actions/cache/restore`, `actions/cache/save` | Kept per repository for 7 days, found by `key` or the newest under a `restore-keys` prefix. Up to 60 MB each. | | |
| 47 | + | | `actions/cache`, `actions/cache/restore`, `actions/cache/save` | Kept per repository, found by `key` or the newest under a `restore-keys` prefix. `path` takes globs and `!` exclusions. Up to 2 GB each; see [the cache](#the-cache). | | |
| 48 | 48 | ||
| 49 | 49 | The **Actions** page of a workflow says, under *How this runs on g1t*, | |
| 50 | 50 | anything in it that runs differently. | |
| 51 | 51 | ||
| 52 | 52 | ### Not yet | |
| 53 | 53 | ||
| 54 | − | - **Windows and macOS runners.** Jobs run on Linux; a job with | |
| 55 | − | `runs-on: windows-latest` or `macos-latest` fails, and says so. | |
| 54 | + | - **Windows and macOS on g1t's machines.** g1t's own runners are Linux; a | |
| 55 | + | job with `runs-on: windows-latest` or `macos-latest` fails, and says so. | |
| 56 | + | [Self-hosted runners](/guides/self-hosted-runners/) of any OS run them: | |
| 57 | + | `runs-on: [self-hosted, windows]`. | |
| 56 | 58 | - **Docker** container actions, `services:` containers and `container:`. | |
| 57 | 59 | - **Reusable workflows from other repositories** (`uses: owner/repo/.github/workflows/x.yml@v1`); ones in the same repository work. | |
| 58 | 60 | - **The toolkit's own cache.** Actions that cache through GitHub's service | |
| 73 | 75 | `actions/setup-node` and `actions/setup-python` install other versions as | |
| 74 | 76 | they do on GitHub. | |
| 75 | 77 | ||
| 78 | + | ### Machine sizes | |
| 79 | + | ||
| 80 | + | A job runs on the standard machine unless its `runs-on` names a larger | |
| 81 | + | one: | |
| 82 | + | ||
| 83 | + | | `runs-on` | vCPUs | Memory | Disk | | |
| 84 | + | | --- | --- | --- | --- | | |
| 85 | + | | `ubuntu-latest`, or any other Linux label | 0.5 | 4 GiB | 8 GB | | |
| 86 | + | | `g1t-2core` | 2 | 8 GiB | 16 GB | | |
| 87 | + | | `g1t-4core` | 4 | 12 GiB | 20 GB | | |
| 88 | + | ||
| 89 | + | ```yaml | |
| 90 | + | jobs: | |
| 91 | + | build: | |
| 92 | + | runs-on: g1t-4core | |
| 93 | + | ``` | |
| 94 | + | ||
| 95 | + | The label can come from the matrix or the run's inputs | |
| 96 | + | (`runs-on: ${{ matrix.big && 'g1t-4core' || 'ubuntu-latest' }}`). A | |
| 97 | + | larger machine costs what it costs g1t, plus the same margin as all | |
| 98 | + | sandbox time: see [usage and billing](/guides/usage-and-billing/#workflow-jobs-on-larger-machines). | |
| 99 | + | Builds that compile, such as Rust or a large TypeScript project, finish | |
| 100 | + | several times faster on one. | |
| 101 | + | ||
| 76 | 102 | A job runs for at most 60 minutes, whatever its `timeout-minutes`, and | |
| 77 | 103 | for less if the workspace's plan caps runs lower (a new workspace's first | |
| 78 | 104 | month, or the trial). A job stopped at its time cap fails saying so. | |
| 92 | 118 | allowed domains under **Settings → Guardrails**; a project whose guardrails | |
| 93 | 119 | turn the network restriction off runs its jobs with an open network. | |
| 94 | 120 | ||
| 121 | + | A host only workflows should reach, such as the API a deploy uploads to, | |
| 122 | + | goes in **Workflow-only domains** instead, limited to the workflows and | |
| 123 | + | environments that need it: `api.cloudflare.com | deploy.yml | production` | |
| 124 | + | lets only `deploy.yml`'s jobs with `environment: production` reach it. | |
| 125 | + | Agents never reach those hosts, and neither do runs of pull requests from | |
| 126 | + | forks. See [workflow-only domains](/guides/guardrails/#workflow-only-domains). | |
| 127 | + | ||
| 95 | 128 | g1t does not run cryptocurrency miners: a step that names one (`xmrig`, | |
| 96 | 129 | a `stratum+tcp://` pool, `--donate-level`) is not run, and a job that | |
| 97 | 130 | looks like it is mining is stopped. See | |
| 98 | 131 | [abuse and mining](/guides/guardrails/#abuse-and-mining). | |
| 99 | 132 | ||
| 133 | + | ## The cache | |
| 134 | + | ||
| 135 | + | `actions/cache` keeps what a job saves for the repository's later jobs: | |
| 136 | + | ||
| 137 | + | | | | | |
| 138 | + | | --- | --- | | |
| 139 | + | | One entry | Up to 2 GB, compressed. A larger one is not saved, and the job goes on. | | |
| 140 | + | | A repository's entries | Up to 10 GB together. Saving past it removes the entries restored longest ago. | | |
| 141 | + | | How long | Until it has not been restored for 7 days, and at most 28 days after it was saved. | | |
| 142 | + | | Keys | Written once: saving under a key that exists does nothing. A restore finds its `key` exactly, else the newest entry whose key starts with one of its `restore-keys`. | | |
| 143 | + | | `path` | Files and folders; globs, `**` included; `~/` is the home folder; a line starting with `!` leaves matching paths out. | | |
| 144 | + | | Compression | zstd. | | |
| 145 | + | ||
| 146 | + | ```yaml | |
| 147 | + | - uses: actions/cache@v4 | |
| 148 | + | with: | |
| 149 | + | path: | | |
| 150 | + | ~/.cargo/registry/cache | |
| 151 | + | target/release | |
| 152 | + | !target/**/incremental | |
| 153 | + | key: cargo-${{ runner.os }}-${{ hashFiles('Cargo.lock') }} | |
| 154 | + | restore-keys: cargo-${{ runner.os }}- | |
| 155 | + | ``` | |
| 156 | + | ||
| 157 | + | Each restore and save says on the job's log how large the entry was and | |
| 158 | + | how long it took. A workspace on the plan pays for what its caches hold | |
| 159 | + | (`Actions cache storage` on its statement), at R2's price plus the margin; | |
| 160 | + | see [usage and billing](/guides/usage-and-billing/#actions-cache). | |
| 161 | + | ||
| 100 | 162 | ## Runs and logs | |
| 101 | 163 | ||
| 102 | 164 | Open a repository's **Actions** page, in its sidebar. Pick a workflow to | |
| 115 | 177 | marks it ready, which on g1t is when it first has code. Each head runs | |
| 116 | 178 | each workflow once. | |
| 117 | 179 | ||
| 118 | − | A run on a pull request's latest commit is a check on it: | |
| 180 | + | ## Checks | |
| 119 | 181 | ||
| 120 | − | - While a workflow runs, the pull request waits for it before merging. | |
| 121 | − | - When one fails, merging is refused, as for failed acceptance checks. | |
| 122 | − | Where the repository allows ignoring checks, anyone who can merge can | |
| 123 | − | merge anyway. | |
| 124 | − | - In a repository that merges through the [merge queue](/guides/merge-queue/), | |
| 182 | + | A pull request's checks are its workflows. Each workflow that runs on | |
| 183 | + | `pull_request` runs on every pull request's head, whoever opened it, a | |
| 184 | + | person or an agent, and its runs report a check named after the workflow: | |
| 185 | + | a workflow with `name: CI` reports `CI`, with the status context | |
| 186 | + | `CI / pull_request` (the workflow's name and the event). | |
| 187 | + | ||
| 188 | + | - **Which checks a merge needs** is up to the default branch's | |
| 189 | + | [required status checks](/guides/pull-requests/#required-status-checks), | |
| 190 | + | under **Settings → Branches and merging**. A required check that failed, | |
| 191 | + | is still running or has not reported holds the merge. Checks that are not | |
| 192 | + | required are shown on the pull request and never hold it. | |
| 193 | + | - **In a repository that merges through the [merge queue](/guides/merge-queue/)**, | |
| 125 | 194 | workflows with `on: merge_group` run on each combined state the queue | |
| 126 | − | builds, as on GitHub, and the state lands only if they pass. | |
| 127 | − | - A pull request a **g1t agent** is working on goes back to the agent | |
| 128 | − | when a workflow fails. The agent reads the run and its logs with the | |
| 129 | − | same tools you have, fixes the cause, and pushes; the workflows run | |
| 130 | − | again. | |
| 195 | + | builds, on the branch `g1t-queue/<entry>`, and the state lands only if | |
| 196 | + | they and every required check pass on it. A workflow behind a required | |
| 197 | + | check needs `merge_group` in its `on:`. | |
| 198 | + | - **A pull request a g1t agent is working on** goes back to the agent when | |
| 199 | + | a check fails, with the end of each failed job's log. The agent reads the | |
| 200 | + | run and its logs with the same tools you have, fixes the cause, and | |
| 201 | + | pushes; the workflows run again. See | |
| 202 | + | [seeing it through](/guides/g1t-agents/#seeing-it-through). | |
| 203 | + | ||
| 204 | + | ```yaml | |
| 205 | + | name: CI | |
| 206 | + | ||
| 207 | + | on: | |
| 208 | + | pull_request: | |
| 209 | + | push: | |
| 210 | + | branches: [main] | |
| 211 | + | merge_group: | |
| 212 | + | ``` | |
| 213 | + | ||
| 214 | + | ### Add CI | |
| 215 | + | ||
| 216 | + | A repository with no workflows has nothing that proves a change works, for | |
| 217 | + | people or for agents. Its pull requests, its **Branches and merging** | |
| 218 | + | settings and its **Actions** page say **This repository has no checks**, | |
| 219 | + | with an **Add CI** button. Anyone who can push to the repository can use it: | |
| 220 | + | ||
| 221 | + | 1. Choose **Add CI**. g1t looks at the files at the repository's root and | |
| 222 | + | writes a starter workflow with a job for each stack it finds, up to | |
| 223 | + | three: Node (npm, pnpm, Yarn or Bun), Rust, Go, Python (pip or uv), Ruby, | |
| 224 | + | Java (Maven or Gradle), .NET, or Make. Each job installs, lints where | |
| 225 | + | the project says how, builds and tests. When it finds none, the job is a | |
| 226 | + | placeholder that fails until you replace its last step with your own | |
| 227 | + | commands. | |
| 228 | + | 2. The workflow is committed as `.g1t/workflows/ci.yml` on a new branch, | |
| 229 | + | `add-ci`, and opened as a pull request, by you. It is named `CI` and runs | |
| 230 | + | on `pull_request`, on `push` to the default branch, and on `merge_group`. | |
| 231 | + | 3. Change it on the pull request if the steps are not how your project | |
| 232 | + | builds, and merge it. | |
| 233 | + | 4. Once it has run, `CI` is offered under **Required status checks**. | |
| 234 | + | Require it, so that nothing merges into the default branch unless it | |
| 235 | + | passes. | |
| 131 | 236 | ||
| 132 | 237 | ## Secrets and variables | |
| 133 | 238 |
| 32 | 32 | | `update` | Merges in the branch its pull request will land on. | | |
| 33 | 33 | | `plan` | Turns an outcome into a plan of issues. | | |
| 34 | 34 | ||
| 35 | − | Sandboxes that run commands rather than a model show too, as `checks` and | |
| 36 | − | `queue`, so the list is everything g1t is running for the project. | |
| 35 | + | Sandboxes that run commands rather than a model show too, such as `queue` | |
| 36 | + | for the merge queue's builds, so the list is everything g1t is running for the project. | |
| 37 | 37 | ||
| 38 | 38 | Each run records: | |
| 39 | 39 |
| 323 | 323 | | Memory & search | `memory:read`, `memory:write` | | |
| 324 | 324 | | Account | `account:read`, `account:write` | | |
| 325 | 325 | | Workspace | `workspace:read`, `access:read`, `webhooks:read`, `secrets:read` | | |
| 326 | − | | Dangerous | `repo:admin`, `workspace:admin`, `access:admin`, `webhooks:admin`, `secrets:admin` | | |
| 326 | + | | Runners | `runners:read` | | |
| 327 | + | | Dangerous | `repo:admin`, `workspace:admin`, `access:admin`, `webhooks:admin`, `secrets:admin`, `runners:admin` | | |
| 327 | 328 | ||
| 328 | 329 | Ticking a higher level ticks the lower ones of its resource and greys | |
| 329 | 330 | them out: tick `issues:write` and `issues:read` is ticked too. Untick | |
| 355 | 356 | | `webhooks:admin` | Create, change and delete webhooks | | |
| 356 | 357 | | `secrets:read` | List secrets (never their values) and read variables | | |
| 357 | 358 | | `secrets:admin` | Set and delete secrets and variables | | |
| 359 | + | | `runners:read` | See [self-hosted runners](/guides/self-hosted-runners/), their groups and where agents run. Not in the Agent preset. | | |
| 360 | + | | `runners:admin` | Register and remove self-hosted runners, change their groups and settings | | |
| 358 | 361 | ||
| 359 | 362 | Every operation of the API and the MCP server needs exactly one of these, | |
| 360 | 363 | except `whoami` (`GET /user`), which any token may use. Each endpoint's page |
| 89 | 89 | ||
| 90 | 90 | ## How an agent works on an issue | |
| 91 | 91 | ||
| 92 | − | 1. `issue` with `get`, to read the description and acceptance checks, and | |
| 93 | − | to see which pull requests already exist for it. | |
| 92 | + | 1. `issue` with `get`, to read the description, including what done means | |
| 93 | + | if it says, and to see which pull requests already exist for it. | |
| 94 | 94 | 2. `memory` with `recall`, to read what the project remembers: how to | |
| 95 | 95 | build, conventions and traps. | |
| 96 | 96 | 3. `pull_request` with `create` and the issue's number. This opens a draft | |
| 107 | 107 | { "name": "pull_request", "arguments": { "action": "create", "repo": "flagon-io/hello", "issue": 42, "agent": "claude-code" } } | |
| 108 | 108 | ``` | |
| 109 | 109 | ||
| 110 | − | When the pull request is ready, g1t runs the issue's acceptance checks | |
| 111 | − | against it in a clean sandbox. `pull_request` with `get` returns each command's | |
| 112 | − | result and output, so an agent whose checks failed can read why, push a fix, | |
| 113 | − | and have them run again. | |
| 110 | + | Every push runs the repository's [workflows](/guides/actions/#checks) on | |
| 111 | + | the pull request, as for anyone's. `pull_request` with `get` returns its | |
| 112 | + | `statuses` and `required_checks`, the checks the default branch requires | |
| 113 | + | before it merges. When one fails, `workflow` with `get_run` and `job_logs` | |
| 114 | + | says why; push a fix and the workflows run again. Run the same tests and | |
| 115 | + | linters the workflows run before you push, and you will rarely need to. | |
| 114 | 116 | ||
| 115 | 117 | If merging reports that `main` has moved, pull `main` from the repository | |
| 116 | 118 | into the fork and push. The pull request can then be merged. |
| 164 | 164 | | **No open secret findings** | [Security](/guides/security/) has no open secret findings for it | | |
| 165 | 165 | ||
| 166 | 166 | A failing rule has **Fix with an agent**: g1t opens an issue saying what to | |
| 167 | − | do, with an acceptance check where one can be written (such as | |
| 168 | − | `test -f AGENTS.md`), and puts g1t's agent on it. | |
| 167 | + | do, with a definition of done where one can be written (such as | |
| 168 | + | "`test -f AGENTS.md` passes."), and puts g1t's agent on it. | |
| 169 | 169 | ||
| 170 | 170 | ## Agents start with context | |
| 171 | 171 |
| 25 | 25 | ||
| 26 | 26 | 1. On Mission control, choose **Put an agent on it**. | |
| 27 | 27 | 2. Pick the project, give a title, and say what you want done in plain | |
| 28 | − | words. Add acceptance checks, one command per line, if you know them. | |
| 28 | + | words, with what done means if you know it. | |
| 29 | 29 | 3. Choose **Put an agent on it**. | |
| 30 | 30 | ||
| 31 | 31 | You land on the new issue with the agent already at work on its pull | |
| 49 | 49 | curl -X POST https://api.g1t.sh/repos/<workspace>/<repo>/issues/delegate \ | |
| 50 | 50 | -H "Authorization: Bearer $G1T_TOKEN" \ | |
| 51 | 51 | -H "Content-Type: application/json" \ | |
| 52 | − | -d '{"title": "Retry webhooks with exponential backoff", "body": "Deliveries that fail are dropped today. Retry them up to six times.", "checks": ["npm test"]}' | |
| 52 | + | -d '{"title": "Retry webhooks with exponential backoff", "body": "Deliveries that fail are dropped today. Retry them up to six times."}' | |
| 53 | 53 | ``` | |
| 54 | 54 | ||
| 55 | 55 | The answer holds the `issue`, the `pull` request the agent opened (or | |
| 58 | 58 | server it is the `agent` tool's `delegate` action. See | |
| 59 | 59 | [put an agent on it](/reference/api/issues/delegate/). | |
| 60 | 60 | ||
| 61 | + | The `checks` field this call and `create_issue` used to take is | |
| 62 | + | deprecated. It is still accepted: its commands are added to the issue's | |
| 63 | + | body under `## Definition of done`, one line each (`` - `npm test` passes. ``), | |
| 64 | + | and the answer carries a `deprecation` string saying so. What has to pass | |
| 65 | + | before the pull request merges is the default branch's | |
| 66 | + | [required status checks](/guides/pull-requests/#required-status-checks). | |
| 67 | + | ||
| 61 | 68 | ## Assigning agents | |
| 62 | 69 | ||
| 63 | 70 | One issue: | |
| 166 | 173 | Making the change is the first step. g1t takes the rest itself, and you | |
| 167 | 174 | get the pull request back ready to merge: | |
| 168 | 175 | ||
| 169 | − | 1. **Checks.** The issue's | |
| 170 | − | [acceptance checks](/concepts/overview/#acceptance-checks) run against | |
| 171 | − | the change in a separate, clean sandbox. The agent has no say in the | |
| 172 | − | result. | |
| 176 | + | 1. **Checks.** Every push the agent makes runs the repository's | |
| 177 | + | [workflows](/guides/actions/) on the pull request, as for anyone's pull | |
| 178 | + | request. g1t waits for them to finish. Only the workflow runs report, so | |
| 179 | + | the agent has no say in the result. | |
| 173 | 180 | 2. **Review.** A different agent reads the change and posts comments on | |
| 174 | 181 | lines, a summary and a verdict. | |
| 175 | − | 3. **Revision.** If the checks fail or the review asks for changes, the | |
| 176 | − | author is sent back with exactly what was found, and steps 1 and 2 run | |
| 177 | − | again on the result. This happens at most twice. | |
| 178 | − | 4. **Ready to merge.** Checks passed and approved. Merging is yours, | |
| 179 | − | unless the repository says otherwise (below). | |
| 182 | + | 3. **Revision.** If a check fails or the review asks for changes, the | |
| 183 | + | author is sent back with exactly what was found. For a failed check, | |
| 184 | + | that is the end of the log of each failed job, up to three jobs and | |
| 185 | + | about 3,000 characters each; it can read more with `get_workflow_run` | |
| 186 | + | and `get_job_logs`. Steps 1 and 2 run again on the result. This happens | |
| 187 | + | at most twice, or as often as the repository's **Revisions before asking | |
| 188 | + | you** allows. | |
| 189 | + | 4. **Ready to merge.** The required checks passed and it is approved. | |
| 190 | + | Merging is yours, unless the repository says otherwise (below). | |
| 191 | + | ||
| 192 | + | Agents are told to run the same tests and linters the workflows run before | |
| 193 | + | they finish, so most failures are caught in the sandbox. What "done" means | |
| 194 | + | for the issue, if its description says so, is context for the agent and | |
| 195 | + | its reviewer; what decides the merge is the default branch's | |
| 196 | + | [required status checks](/guides/pull-requests/#required-status-checks). | |
| 197 | + | A repository with no workflows has nothing to prove a change works; **Add | |
| 198 | + | CI** gives it one ([add CI](/guides/actions/#add-ci)). | |
| 180 | 199 | ||
| 181 | 200 | If `main` has moved in the meantime, that does not hold the pull request | |
| 182 | 201 | up. Merging it brings it up to date first: g1t merges `main` in, an agent | |
| 192 | 211 | it: the agent is sent to merge `main` in and resolve the conflicts, told | |
| 193 | 212 | which files conflict, and the checks run again on the result. | |
| 194 | 213 | ||
| 195 | − | The pull request's page shows which step it is at. If g1t cannot finish, | |
| 196 | − | because the checks still fail after two revisions, a review could not be | |
| 197 | − | written, or a conflict could not be resolved while bringing it up to date, | |
| 198 | − | it stops and the page says | |
| 199 | − | **Needs you**, with the reason. Pushing to the pull request yourself starts | |
| 200 | − | it moving again. | |
| 214 | + | The pull request's page shows which step it is at. While a required check | |
| 215 | + | has not reported on its latest commit, it says so: "Waiting for the | |
| 216 | + | required check CI to report on its latest commit." If g1t cannot finish, | |
| 217 | + | because a required check still fails after the agent revised as often as | |
| 218 | + | the repository allows ("The required check CI still fails after the agent | |
| 219 | + | revised twice."), a review could not be written, or a conflict could not | |
| 220 | + | be resolved while bringing it up to date, it stops and the page says | |
| 221 | + | **Needs you**, with the reason. A check that is not required and still | |
| 222 | + | fails after the last revision does not hold it. Pushing to the pull | |
| 223 | + | request yourself starts it moving again. | |
| 201 | 224 | ||
| 202 | 225 | ### What a repository can ask for | |
| 203 | 226 | ||
| 204 | − | Under a project's **Settings → Repository**, someone with the Maintain | |
| 205 | − | [role](/guides/access-and-roles/) or higher sets the rules its pull requests follow: | |
| 227 | + | Under a project's **Settings → Branches and merging**, someone with the | |
| 228 | + | Maintain [role](/guides/access-and-roles/) or higher sets the rules its pull | |
| 229 | + | requests follow. **Branch protection** holds the rules for every pull | |
| 230 | + | request, a person's or an agent's; they are described in | |
| 231 | + | [required status checks](/guides/pull-requests/#required-status-checks): | |
| 206 | 232 | ||
| 207 | 233 | | Setting | Default | What it does | | |
| 208 | 234 | | --- | --- | --- | | |
| 209 | − | | Require a pull request to change `main` | Off | Refuses pushes to the default branch. | | |
| 235 | + | | Require a pull request to change the default branch | Off | Refuses pushes to the default branch. | | |
| 236 | + | | Required status checks | None | The checks that must pass on a pull request's head before it merges. | | |
| 210 | 237 | | Required approvals | None | How many reviewers must approve before a merge. A reviewer who asked for changes blocks it. | | |
| 211 | 238 | | A g1t agent's approval counts | On | Off means approvals have to come from people. | | |
| 212 | − | | Require acceptance checks to pass | Off | On means nobody can merge with failed checks. | | |
| 213 | − | | Require pull requests to be up to date | Off | On means catching up is a step of its own and the checks run again. | | |
| 239 | + | | Require branches to be up to date before merging | Off | On means catching up is a step of its own and the checks run again. | | |
| 240 | + | | Merge through a queue | Off | Merging tests a pull request together with those ahead of it; the default branch only moves to a combination that passed. See [merge queue](/guides/merge-queue/). | | |
| 241 | + | | Allow bypassing required checks | On | Lets someone who may merge merge without the required checks passing. Off means nobody can. | | |
| 242 | + | ||
| 243 | + | **g1t agents** holds what g1t does with its own agents' pull requests: | |
| 244 | + | ||
| 245 | + | | Setting | Default | What it does | | |
| 246 | + | | --- | --- | --- | | |
| 214 | 247 | | Review by a second agent | On | Off leaves review to people. | | |
| 215 | 248 | | Revisions before asking you | 2 | How often an agent is sent back before g1t stops. | | |
| 216 | 249 | | Merge automatically when ready | Off | Lands a g1t agent's pull request once every rule is met. | | |
| 217 | 250 | | Ask a person before merging low-confidence changes | On | A g1t agent's change [rated low](#how-sure-the-agent-is) waits for a person's approval instead of merging by itself or joining the queue. | | |
| 218 | − | | Merge through a queue | Off | Merging tests a pull request together with those ahead of it; `main` only moves to a combination that passed. See [merge queue](/guides/merge-queue/). | | |
| 219 | 251 | ||
| 220 | 252 | A g1t agent's pull request follows the same rules as anyone's. If the | |
| 221 | 253 | repository wants approvals from people, it waits for them, and shows | |
| 234 | 266 | ||
| 235 | 267 | A repository can land a g1t agent's pull request by itself once it is | |
| 236 | 268 | ready. Someone with the Maintain role or higher turns this on under the repository's | |
| 237 | − | **Settings → Repository**; it is off to begin with. The merge is recorded as made by | |
| 269 | + | **Settings → Branches and merging**; it is off to begin with. The merge is recorded as made by | |
| 238 | 270 | `g1t`, the issue closes naming the pull request, and nothing short of | |
| 239 | − | ready is ever merged this way. One that is behind `main` is brought up to | |
| 271 | + | ready is ever merged this way: every required check has passed on its | |
| 272 | + | head. One that is behind `main` is brought up to | |
| 240 | 273 | date as part of the merge. Pull requests from people and from other | |
| 241 | 274 | agents always wait for a person to merge them. | |
| 242 | 275 | ||
| 244 | 277 | **Session**, reviews in its conversation. | |
| 245 | 278 | ||
| 246 | 279 | This applies to pull requests made by g1t agents. One you or your own | |
| 247 | − | agent opened is yours to drive; the same checks run on it, and you can ask | |
| 280 | + | agent opened is yours to drive; the same workflows run on it, and you can ask | |
| 248 | 281 | for a review or a catch-up from its page. | |
| 249 | 282 | ||
| 250 | 283 | ### How sure the agent is | |
| 263 | 296 | ||
| 264 | 297 | | Signal | Effect | | |
| 265 | 298 | | --- | --- | | |
| 266 | − | | The acceptance checks fail, or could not run | Low | | |
| 299 | + | | Required checks fail | Low | | |
| 300 | + | | It failed in the [merge queue](/guides/merge-queue/) | Low | | |
| 267 | 301 | | The reviewer agent asks for changes | Low | | |
| 268 | 302 | | A run was stopped at its cost or time cap | Low | | |
| 269 | 303 | | Sent back to revise | 1 point per revision, at most 3 | | |
| 270 | − | | The checks have not finished, or passed only on a retry | 1 point | | |
| 271 | − | | The issue has no acceptance checks | 1 point | | |
| 304 | + | | Required checks have not finished, or have not run on its head | 1 point | | |
| 305 | + | | Checks passed only on a retry | 1 point | | |
| 306 | + | | The default branch has no required checks | 1 point | | |
| 272 | 307 | | No review yet, or the repository has no reviewer agent | 1 point | | |
| 273 | 308 | | The reviewer approved but left three or more comments on lines | 1 point | | |
| 274 | 309 | | Code changed and no test was added or changed | 1 point | | |
| 287 | 322 | When the agent's word is lower, the reasons start with "agent says low", | |
| 288 | 323 | and the pull request lists what it was unsure about. | |
| 289 | 324 | ||
| 290 | − | For high confidence, the reasons say what it rests on: checks pass, | |
| 325 | + | For high confidence, the reasons say what it rests on: required checks pass, | |
| 291 | 326 | approved on the first review, tests added, a small change. | |
| 292 | 327 | ||
| 293 | 328 | The pull request's `confidence` in the | |
| 445 | 480 | an agent can build and test most projects. If your project needs something | |
| 446 | 481 | else, the agent will say in its summary what it could not run. | |
| 447 | 482 | ||
| 483 | + | A workspace (or a project) can send its agents' work to | |
| 484 | + | [its own runners](/guides/self-hosted-runners/#agents-on-your-runners) | |
| 485 | + | instead, so agents build and test with what those machines have. The agent | |
| 486 | + | works the same way there, with the same short-lived credentials, and its | |
| 487 | + | model calls still go through g1t; the machine time is free. g1t's network | |
| 488 | + | guardrails cannot be enforced on your machines, and the run says so. | |
| 489 | + | ||
| 448 | 490 | ## Who can run agents | |
| 449 | 491 | ||
| 450 | 492 | Putting an agent to work (assigning it, mentioning it, asking it for a | |
| 461 | 503 | workspace has the whole forge, and two ways to try agents: | |
| 462 | 504 | ||
| 463 | 505 | - **The trial.** $5 of usage, once per workspace, after a card check. | |
| 464 | − | - **The open-source pool.** Checks, workflows and the merge queue on public | |
| 506 | + | - **The open-source pool.** Workflows and the merge queue on public | |
| 465 | 507 | repositories, after the same card check. It does not pay for agents. | |
| 466 | 508 | ||
| 467 | 509 | This holds whether the agent uses g1t's hosted models or | |
| 562 | 604 | | Catch up | Reads the repository; pushes to the pull request's fork or branch only | Records the session of its own pull request | | |
| 563 | 605 | | Review | Reads the change and the repository; pushes nothing | Reports its review through its own run | | |
| 564 | 606 | | Plan | Reads the repository; pushes nothing | Reports its plan through its own run, for a person to apply; it can create issues in its repository only | | |
| 565 | − | | Checks, merge check | Reads the change; pushes nothing | None | | |
| 607 | + | | Merge check | Reads the change; pushes nothing | None | | |
| 566 | 608 | | Merge queue | Reads each queued change; pushes the queue's own branch only | None | | |
| 567 | 609 | | Deploy | Reads the commit it builds; pushes nothing | None | | |
| 568 | 610 |
| 80 | 80 | ||
| 81 | 81 | ## Protected branches | |
| 82 | 82 | ||
| 83 | − | A repository can protect its default branch under **Settings → Repository**. Pushing to | |
| 84 | − | it is then refused for everyone, whatever their role, and for agents, and | |
| 83 | + | A repository can protect its default branch under **Settings → Branches and | |
| 84 | + | merging**. Pushing to it is then refused for everyone, whatever their role, and for agents, and | |
| 85 | 85 | git says why: | |
| 86 | 86 | ||
| 87 | 87 | ```text | |
| 91 | 91 | Changes reach a protected branch only by merging a pull request. The first | |
| 92 | 92 | push to an empty repository is still allowed. | |
| 93 | 93 | ||
| 94 | + | The same page sets what a merge needs: the | |
| 95 | + | [required status checks](/guides/pull-requests/#required-status-checks) | |
| 96 | + | and approvals. | |
| 97 | + | ||
| 94 | 98 | ## Branches | |
| 95 | 99 | ||
| 96 | 100 | Push any branch to a repository you can write to, and open a |
| 10 | 10 | ||
| 11 | 11 | They apply to every sandbox g1t starts for a project's agents (implement, | |
| 12 | 12 | revise, answer, catch up, review, plan, and replies to mentions). The | |
| 13 | − | sandboxes of its acceptance checks and merge queue get the network list and | |
| 14 | − | the time cap; they run the project's commands, not an agent, so command | |
| 15 | − | rules and the cost cap do not apply to them. GitHub Actions jobs and deploy | |
| 13 | + | sandboxes of its merge queue get the network list and the time cap; they | |
| 14 | + | build the queue's states, not an agent's work, so command rules and the | |
| 15 | + | cost cap do not apply to them. GitHub Actions jobs and deploy | |
| 16 | 16 | builds get the network list too, with what builds need added (see | |
| 17 | 17 | [builds](#builds)), and their own time limit. Merge checks are not covered; | |
| 18 | 18 | see [What is not covered](#what-is-not-covered). Every sandbox, whatever | |
| 29 | 29 | ||
| 30 | 30 | Every setting on a project's page starts as "As the workspace", which | |
| 31 | 31 | follows the workspace's default, whatever it is now. Choose a value to | |
| 32 | − | override it for that project only. Allowed domains and deny patterns add | |
| 33 | − | up: a project's are added to the workspace's, never instead of them. | |
| 32 | + | override it for that project only. Allowed domains, workflow-only domains | |
| 33 | + | and deny patterns add up: a project's are added to the workspace's, never | |
| 34 | + | instead of them. | |
| 34 | 35 | ||
| 35 | 36 | Changes apply to runs that start after you save. A run that is under way | |
| 36 | 37 | keeps the guardrails it started with. | |
| 108 | 109 | project whose guardrails set **Only allowed hosts** to Open runs its jobs | |
| 109 | 110 | and builds with an open network too. | |
| 110 | 111 | ||
| 112 | + | ### Workflow-only domains | |
| 113 | + | ||
| 114 | + | Some hosts only a workflow should reach: the API a deploy uploads to, a | |
| 115 | + | release server, a package registry you publish to. Listing them under | |
| 116 | + | allowed domains would open them to agents as well. List them under | |
| 117 | + | **Workflow-only domains** instead, one per line: | |
| 118 | + | ||
| 119 | + | ``` | |
| 120 | + | api.cloudflare.com | deploy.yml | production | |
| 121 | + | uploads.example.com | release.yml, nightly.yml | |
| 122 | + | *.internal.example.com | |
| 123 | + | ``` | |
| 124 | + | ||
| 125 | + | | Part | | | |
| 126 | + | | --- | --- | | |
| 127 | + | | The domain | As for allowed domains: `example.com`, or `*.example.com` for its subdomains. | | |
| 128 | + | | Workflows | Workflow files by name, comma-separated, as they are in `.g1t/workflows/`. Left out: any workflow. | | |
| 129 | + | | Environments | The environments a job must name with `environment:`, comma-separated. Left out: any job. | | |
| 130 | + | ||
| 131 | + | A domain is reached only by: | |
| 132 | + | ||
| 133 | + | - jobs of the workflows and environments its line names; | |
| 134 | + | - in a run that is not of a pull request from a fork, which runs code | |
| 135 | + | anyone could write. | |
| 136 | + | ||
| 137 | + | Agents, checks, the merge queue and deploy builds never reach these | |
| 138 | + | hosts, whatever the line says. A job that names its environment with an | |
| 139 | + | expression (`environment: ${{ inputs.target }}`) matches only lines with | |
| 140 | + | no environments. | |
| 141 | + | ||
| 142 | + | Workflow-only domains are set by the same people as the rest of the page: | |
| 143 | + | owners for the workspace's, Maintain or higher for a project's. Each change | |
| 144 | + | is recorded in the workspace's [audit log](/guides/audit-log/) as | |
| 145 | + | `update_guardrails`, saying which domains were added or removed and what | |
| 146 | + | they were limited to. | |
| 147 | + | ||
| 111 | 148 | ## Commands | |
| 112 | 149 | ||
| 113 | 150 | The agent's harness checks every tool call the agent makes before it runs. |
| 15 | 15 | | --- | --- | | |
| 16 | 16 | | **Name** | The repository's name, the second part of its address. | | |
| 17 | 17 | | **Details** | Its description, website and topics, shown on its page, in [search and Explore](/guides/search/). | | |
| 18 | − | | **Branches** | The default branch, renaming a branch, and a link to **Branches and merging**: [branch protection](/guides/git/#protected-branches), required approvals, the [merge queue](/guides/merge-queue/) and what agents do. What a sandbox may reach is under **Guardrails**; see [guardrails](/guides/guardrails/). | | |
| 18 | + | | **Branches** | The default branch, renaming a branch, and a link to **Branches and merging**: [branch protection](/guides/git/#protected-branches), [required status checks](/guides/pull-requests/#required-status-checks), required approvals, the [merge queue](/guides/merge-queue/) and what agents do. What a sandbox may reach is under **Guardrails**; see [guardrails](/guides/guardrails/). | | |
| 19 | 19 | | **Danger zone** | Change visibility, archive, [transfer](/guides/transferring-repositories/) and delete. Shown to people with the Admin role. | | |
| 20 | 20 | ||
| 21 | 21 | While a repository is [archived](#archive-a-repository), the settings that |
| 1 | 1 | --- | |
| 2 | 2 | title: Merge queue | |
| 3 | − | description: Test each pull request together with the ones ahead of it, so main only moves to a state whose checks passed. | |
| 3 | + | description: Test each pull request together with the ones ahead of it, so main only moves to a state whose required checks passed. | |
| 4 | 4 | --- | |
| 5 | 5 | ||
| 6 | 6 | Merging one pull request at a time, each caught up with `main`, keeps every | |
| 7 | 7 | merge clean as text. It does not prove the result works: two changes can | |
| 8 | 8 | merge without a conflict and still break each other. With the merge queue | |
| 9 | 9 | on, a pull request is tested together with everything ahead of it before it | |
| 10 | − | lands, and `main` only ever moves to a state whose checks passed. | |
| 10 | + | lands, and `main` only ever moves to a state whose required checks passed. | |
| 11 | 11 | ||
| 12 | 12 | The merge queue runs in g1t's sandboxes, which work in any workspace with | |
| 13 | 13 | [its own model provider](/guides/models/) and in those g1t's hosted models | |
| 16 | 16 | ||
| 17 | 17 | ## Turn it on | |
| 18 | 18 | ||
| 19 | − | 1. Open the project's **Settings → Repository**. You need the Maintain | |
| 19 | + | 1. Open the project's **Settings → Branches and merging**. You need the Maintain | |
| 20 | 20 | [role](/guides/access-and-roles/) or higher on its repository. | |
| 21 | 21 | 2. Turn on **Merge through a queue**. | |
| 22 | 22 | 3. Save. | |
| 23 | + | 4. Add `merge_group` to the `on:` of every workflow behind a | |
| 24 | + | [required status check](/guides/pull-requests/#required-status-checks), | |
| 25 | + | so that it runs on the queue's states too | |
| 26 | + | ([below](#what-each-state-is-held-to)). | |
| 23 | 27 | ||
| 24 | 28 | From the API, send `merge_queue` to `PATCH /repos/{owner}/{name}/settings` | |
| 25 | 29 | (or `update_repo_settings`): | |
| 37 | 41 | the `pull_request` tool's `merge` action, or with | |
| 38 | 42 | `POST /repos/{owner}/{name}/pulls/{number}/merge`, | |
| 39 | 43 | adds it to the queue instead of changing `main`. Everything a merge needs | |
| 40 | − | is still checked first: the pull request must be ready for review, its | |
| 41 | − | checks must have passed and it must have the approvals the repository asks | |
| 44 | + | is still checked first: the pull request must be ready for review, every | |
| 45 | + | [required check](/guides/pull-requests/#required-status-checks) must have | |
| 46 | + | passed on its head, and it must have the approvals the repository asks | |
| 42 | 47 | for. Only people with the Write [role](/guides/access-and-roles/) or higher can add to the | |
| 43 | 48 | queue. Merging a pull | |
| 44 | 49 | request that is already queued changes nothing. | |
| 66 | 71 | ||
| 67 | 72 | ### What each state is held to | |
| 68 | 73 | ||
| 69 | − | Each tested state runs: | |
| 74 | + | g1t pushes each state it built to a branch of its own, `g1t-queue/<entry>`, | |
| 75 | + | and runs the repository's [workflows](/guides/actions/) that run on | |
| 76 | + | `merge_group` on it. The entry waits for them, and passes only if: | |
| 70 | 77 | ||
| 71 | − | - the acceptance checks of every pull request in it; and | |
| 72 | − | - the **contract**: the checks of issues already completed on the | |
| 73 | − | repository, from the 30 most recently closed. Once an issue lands, its | |
| 74 | − | checks become part of what `main` promises, and every later change is | |
| 75 | − | held to them. | |
| 78 | + | - every `merge_group` workflow it started passed; and | |
| 79 | + | - every [required status check](/guides/pull-requests/#required-status-checks) | |
| 80 | + | of the default branch passed on that commit. | |
| 76 | 81 | ||
| 77 | − | So a change that breaks something that landed before it is caught here, | |
| 78 | − | even when it merges without a conflict and its own checks pass. | |
| 82 | + | So a change that breaks something another change ahead of it relies on is | |
| 83 | + | caught here, even when it merges without a conflict and its own checks | |
| 84 | + | passed. The branch is deleted once the entry lands or leaves the queue. | |
| 79 | 85 | ||
| 80 | − | Once those pass, the repository's [GitHub Actions](/guides/actions/) | |
| 81 | − | workflows that run `on: merge_group` run on the state too, with the same | |
| 82 | − | `merge_group` event GitHub sends, on the branch `g1t-queue/<entry>`. The | |
| 83 | − | entry waits for them, and lands only if they pass. The branch is deleted | |
| 84 | − | once the entry lands or leaves the queue. A workflow opts in like this: | |
| 86 | + | A workflow opts in like this: | |
| 85 | 87 | ||
| 86 | 88 | ```yaml | |
| 87 | 89 | on: | |
| 89 | 91 | merge_group: | |
| 90 | 92 | ``` | |
| 91 | 93 | ||
| 92 | − | A contract check that fails is run again on `main` alone. If it fails there | |
| 93 | − | too, it was broken already: it is marked as passing with a note, "already | |
| 94 | − | failing on the default branch; not held against this", and does not hold | |
| 95 | − | the change back. | |
| 94 | + | Required checks only report on a queued state if their workflows run on | |
| 95 | + | `merge_group`. When the branch requires checks and no workflow runs on | |
| 96 | + | `merge_group`, the entry fails with a message saying so: "the required | |
| 97 | + | check CI cannot report on it: no workflow runs on merge_group events. Add | |
| 98 | + | merge_group to the on: of the workflows the branch requires". A required | |
| 99 | + | check that a workflow did not report on the state fails it the same way. | |
| 100 | + | A repository that requires no checks and has no `merge_group` workflows | |
| 101 | + | only has each state built: an entry passes once it merges cleanly with | |
| 102 | + | what is ahead of it. | |
| 96 | 103 | ||
| 97 | 104 | ## How entries land | |
| 98 | 105 | ||
| 118 | 125 | ||
| 119 | 126 | ## When an entry fails | |
| 120 | 127 | ||
| 121 | − | An entry fails when its checks or its `merge_group` workflows fail in the | |
| 122 | − | combined state, when it does not merge cleanly with what is ahead of it, or | |
| 128 | + | An entry fails when its `merge_group` workflows or required checks fail | |
| 129 | + | on the combined state, when it does not merge cleanly with what is ahead of it, or | |
| 123 | 130 | when the state cannot be built. | |
| 124 | 131 | It leaves the queue, and: | |
| 125 | 132 | ||
| 126 | − | 1. Its pull request gets a failed check run. Each command is named with the | |
| 127 | − | state it ran in, such as `cargo test (merge queue, on the default branch | |
| 128 | − | with #41 merged in first)`, and the run says why it failed. A conflict | |
| 129 | − | names the pull request ahead it collided with, and the files. | |
| 133 | + | 1. Its pull request records the failure, saying why: which workflow failed | |
| 134 | + | on the state, which required check did not report, or, for a conflict, | |
| 135 | + | the pull request ahead it collided with and the files. | |
| 130 | 136 | 2. Its conversation records that it was taken out of the queue, and why: a | |
| 131 | 137 | conflict links the pull request it collided with and each conflicting | |
| 132 | 138 | file, which opens in the pull request's changes. | |
| 133 | 139 | 3. The entries that were tested on top of it are tested again without it. | |
| 134 | 140 | ||
| 135 | − | A g1t agent's pull request is then sent back to revise, like any failed | |
| 141 | + | A g1t agent's pull request is then sent back to revise, as for any failed | |
| 136 | 142 | check, starting from `main` as it is now. The revision counts towards | |
| 137 | 143 | **Revisions before asking you**. Once it is ready again, a repository with | |
| 138 | 144 | **Merge automatically when ready** on adds it to the queue again by itself; | |
| 152 | 158 | | --- | --- | | |
| 153 | 159 | | State | **Waiting**, **Testing** or **Passed**. | | |
| 154 | 160 | | Tested as | `main` and the pull requests merged into it, such as `main + #41 + #44`. | | |
| 155 | − | | Checks | How many of the checks passed. | | |
| 161 | + | | Checks | How its state's checks stand. | | |
| 156 | 162 | | Who | The agent or person who made the pull request, and who queued it. | | |
| 157 | 163 | | Commit | The tested state's commit. | | |
| 158 | 164 | ||
| 159 | 165 | **Recently** lists the last 20 that left the queue: **Landed**, **Failed** | |
| 160 | − | or **Removed**. A failed entry shows why, and the output of the checks that | |
| 161 | − | failed. | |
| 166 | + | or **Removed**. A failed entry shows why. | |
| 162 | 167 | ||
| 163 | 168 | ## From the API or an agent | |
| 164 | 169 | ||
| 199 | 204 | | `ahead` | The pull requests merged ahead of it in the state being tested. Empty when it was tested on `main` alone. | | |
| 200 | 205 | | `base_commit` | The commit of `main` the state was built on. | | |
| 201 | 206 | | `combined_commit` | The tested state. | | |
| 202 | − | | `results` | The checks run against it, each with `command`, `passed` and `output`. | | |
| 203 | − | | `error` | Why it failed: a conflict, or what could not be run. | | |
| 207 | + | | `results` | What building the state recorded, each with `command`, `passed` and `output`. The workflow runs on it are on its commit, `combined_commit`. | | |
| 208 | + | | `error` | Why it failed: a conflict, a workflow that failed on it, a required check that did not report, or what could not be built. | | |
| 204 | 209 | | `enqueued_by` | Who added it: a username, or `g1t` when it was merged automatically. | |
| 5 | 5 | ||
| 6 | 6 | You do not have to split work into issues yourself. Write the outcome you | |
| 7 | 7 | want on a repository's **Plan** page. An agent reads the repository and | |
| 8 | − | proposes the issues that would get there, with acceptance checks and the | |
| 9 | − | order they have to land in. You read the plan, keep what you want, and open | |
| 8 | + | proposes the issues that would get there, with what done means for each and | |
| 9 | + | the order they have to land in. You read the plan, keep what you want, and open | |
| 10 | 10 | it. g1t agents then work on the issues, as many at once as the dependencies | |
| 11 | 11 | allow, and the outcome page shows each one until it lands. | |
| 12 | 12 | ||
| 43 | 43 | | --- | --- | | |
| 44 | 44 | | Title and labels | What the issue is. | | |
| 45 | 45 | | **Starts at once**, or **After** | Whether it depends on nothing, or which earlier issues have to merge first. | | |
| 46 | − | | Acceptance checks | The commands a pull request for it must make pass, taken from how the repository is tested. | | |
| 46 | + | | Definition of done | What has to be true for it to be done, in plain words. It is added to the issue's description under **Definition of done**, for the agent and its reviewer. What a pull request for it must pass to merge is the default branch's [required status checks](/guides/pull-requests/#required-status-checks). | | |
| 47 | 47 | | Files | The files it will most likely change. | | |
| 48 | 48 | | **What the agent will be told** | The issue's description, in full. An agent given the issue works from this text. | | |
| 49 | 49 | ||
| 71 | 71 | it merged; and | |
| 72 | 72 | - the repository has room. At most six g1t agents make changes in one | |
| 73 | 73 | repository at once. The rest wait their turn, which also leaves sandboxes | |
| 74 | − | free for checks and reviews. | |
| 74 | + | free for reviews. | |
| 75 | 75 | ||
| 76 | 76 | Each queued issue says so in its conversation, for example "queued this for | |
| 77 | 77 | g1t-agent, to start once #41 has merged". From there each issue is | |
| 104 | 104 | | Waiting for an agent | Queued, and waiting for an agent to be free. | | |
| 105 | 105 | | Open | Nobody is working on it. | | |
| 106 | 106 | | Agent working | A g1t agent is making the change. | | |
| 107 | − | | Checking | The acceptance checks are running. | | |
| 107 | + | | Checking | Its workflows are running, or a required check has not reported yet. | | |
| 108 | 108 | | In review | A g1t agent is reviewing the change. | | |
| 109 | 109 | | Revising | The agent was sent back by the checks, a review or a person. | | |
| 110 | 110 | | Catching up | The agent is merging in the branch it will land on, which has moved. | | |
| 157 | 157 | ``` | |
| 158 | 158 | ||
| 159 | 159 | A plan's `status` is `planning`, `ready`, `failed` or `applied`. Each | |
| 160 | − | proposed issue has `title`, `body`, `labels`, `checks`, `files`, | |
| 160 | + | proposed issue has `title`, `body`, `labels`, `done` (what done means, in | |
| 161 | + | plain words, added to `body` under **Definition of done** when it is | |
| 162 | + | opened), `files`, | |
| 161 | 163 | `depends_on` (positions in the plan, counting from 1) and, once applied, | |
| 162 | 164 | `number`. `keep` takes positions counting from 1; leave it out to open | |
| 163 | 165 | every issue. |
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
This change is too large to show in full.