--- name: craci description: Move a repository's existing GitHub Actions jobs onto CRACI runners — change routing, keep build logic untouched. Use when asked to migrate CI to CRACI, switch runners to CRACI, run builds on CRACI, or set up CRACI in a repo that already has workflows. --- # Migrate existing workflows to CRACI runners CRACI runs GitHub Actions jobs on isolated Linux VMs and generates an SBOM for every build from the packages it sees a job download. All egress goes through CRACI's package-aware proxy. The default policy observes that traffic but still allows unmatched destinations; a policy that blocks them is what stops a compromised dependency exfiltrating your source, secrets, or build output, and what earns the job a complete SBOM. To a workflow author it behaves like a self-hosted Linux runner: `run:` steps, actions, caching, services, and artifacts all work as usual. Reference documentation: . Every page is also served as markdown at the same path with `.md` appended, and indexes the site. This skill covers one job: a repository that already has workflows running on other runners, which should now run on CRACI. ## Prerequisite: the GitHub connection CRACI must already be connected to the GitHub organization or repository. That is a one-time click-through in the CRACI web app under **Settings → Builds → Install GitHub App**, documented at . You cannot do this step for the user — it requires a GitHub org owner in a browser. Check whether it is done before editing, and say so if it is not. The symptom of a missing connection is that migrated jobs queue forever and never pick up a runner. ## The only requirement A job runs on CRACI when `runs-on` contains the label `craci`. That is the whole migration. ```yaml jobs: build: runs-on: craci steps: - uses: actions/checkout@v4 - run: make build ``` The job runs on the default baseline: `size: 2` (2 vCPU, 6 GiB RAM) with the `default` image. For most jobs that is the finished migration — nothing else is added. Full reference: ## Optional: `cracicorp/setup@v1` Add this step only to change something about the baseline. A job that is happy with the defaults should not have it. CRACI reads its inputs when it schedules the job, so the step's position does not matter; convention is to put it first. ```yaml - uses: cracicorp/setup@v1 with: size: "8" ``` The step's inputs are what select the job's resources, its image and architecture, its runner template, its extra disks, and its egress network policy. Read the documentation before setting any of them rather than guessing at values: - — the inputs and their defaults - — image names and architectures - — egress policies Set only the inputs you are actually changing. CRACI resolves these inputs when it schedules the job and never evaluates the step's `if:`. In a matrix a condition therefore cannot scope them — only the input values can vary per leg. Gate the step with `if:` so it does not execute on non-CRACI legs, and make every value an expression. Reasons to add the step, in rough order of how often they apply: - The job is CPU-bound and the baseline is too slow — raise `size`. - The job must run on arm64 — set `image: craci-slim-arm`. - The job needs GitHub's full preinstalled software set or hosted tool cache — set `image: ubuntu-24.04`. - The CRACI org publishes a runner template this job should use. - The job runs under a restricted-network template and needs an egress policy. ## Architecture CRACI runs both amd64 and arm64 natively. Architecture is chosen by the `image` input — there is no separate arch input and no arch label in `runs-on`. | Architecture | `image` | Notes | | ------------ | ------------------ | --------------------------------------------------------- | | amd64 | omitted, `default` | The baseline. Also `ubuntu-24.04` for the full GHA image. | | arm64 | `craci-slim-arm` | CRACI Slim (ARM64), scheduled on ARM execution nodes. | An arm64 job is therefore the one common case where the setup step is mandatory: without `image: craci-slim-arm` the job silently runs on amd64. The full `ubuntu-24.04` image is amd64 only, so a job needing both the GitHub preinstalled software set and arm64 cannot have both — flag that to the user rather than picking one. ## Rules that govern the migration **CRACI runners are Linux only.** There is no macOS and no Windows runner. Jobs needing those platforms keep their current runner. **Change routing, preserve logic.** Migrating a job means changing `runs-on`. Do not rewrite build or test commands, reorder steps, drop caching, or improve things along the way. Keep the diff minimal and reviewable. **Resources never live in `runs-on`.** Hosted-runner size suffixes such as `-4` or `-8` are dropped from the label. Re-express them as `size` in a setup step only if the default is genuinely too small. **Match the repo.** Mirror the existing workflows' formatting, action pin style (`@v4` vs full SHA), naming, and trigger conventions. ## Phase 1 — analyze, do not edit Produce a plan first: 1. List every file in `.github/workflows/`. 2. For each workflow, list its jobs and each job's `runs-on` value. 3. Classify each job with the routing table below: migrate, keep as-is, or needs a decision from the user. 4. Note the few jobs that plausibly need a `size` above the default, using the sizing guide. Assume the default otherwise. Report it as a plain list — `ci.yml` → job `build` (`ubuntu-latest`) → craci, default size; job `mac-build` (`macos-14`) → keep — then confirm with the user before editing, unless they have already said to proceed. ## Phase 2 — routing table Per job, keyed on its current `runs-on`: | Current `runs-on` | Action | | --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | | `ubuntu-latest`, `ubuntu-24.04`, `ubuntu-22.04` | Migrate: `runs-on: craci`. | | `depot-ubuntu-*`, `namespace-*`, `blacksmith-*`, `buildjet-*`, `warp-*`, `self-hosted` Linux x64 | Migrate. Drop the vCPU suffix from the label; add `size` only if the default is too small. | | Already `craci` | Nothing to do. | | `macos-*` | Keep as-is. No macOS runner exists. | | `windows-*` | Keep as-is. No Windows runner exists. | | `ubuntu-*-arm`, `*-arm64`, native Linux arm64 | Migrate: `runs-on: craci` plus a setup step with `image: craci-slim-arm`. | | Matrix expression (`${{ matrix.os }}`, `${{ matrix.runner }}`) | Migrate the Linux legs, amd64 and arm64 alike. See the matrix recipes. | ### Per-feature decisions - **`container:` jobs** — keep the `container:` block, switch `runs-on`. Docker is available on the runner. - **`services:` sidecars** — keep them; they behave as on any Docker-capable self-hosted runner. - **Reusable workflows (`workflow_call`)** — migrate `runs-on` inside the reusable workflow's own jobs; callers are unaffected. If the runner arrives as an input, change the input default and the caller that sets it. - **`actions/cache`, `upload-artifact`, `download-artifact`, `checkout`, `setup-*`, `docker/build-push-action`, buildx** — no change. - **Org runner groups and self-hosted group labels** — remove them. Routing is the single `craci` label; the runner install is an org admin concern, not a workflow concern. - **Pure GitHub-API chores** — labelers, stale bots, PR triage, `github-script` housekeeping. Leave on `ubuntu-latest`; they build nothing. - **Release and publish jobs that compile** — migrate the whole job by default so the build runs on CRACI; the publish step works unchanged. A publish job that only moves existing artifacts is a chore, so leave it. - **Security scanners** — migrate the Linux/amd64 legs. CodeQL's autobuild compiles the project, so it benefits. Keep non-Linux legs where they are. - **Benchmark jobs** — keep on their current runner. Different hardware shifts benchmark baselines, so measurement jobs should not move. ## Recipes ### Simple Linux job ```yaml # before jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: yarn install && yarn test ``` ```yaml # after jobs: test: runs-on: craci steps: - uses: actions/checkout@v4 - run: yarn install && yarn test ``` One line. `runs-on: [craci]` as a list is equivalent if the repo's style favours it. ### A job that needs more than the baseline ```yaml jobs: build: runs-on: craci steps: - uses: cracicorp/setup@v1 with: size: "8" - uses: actions/checkout@v4 - run: cargo build --locked ``` ### An arm64 job ```yaml # before jobs: build-arm: runs-on: ubuntu-24.04-arm steps: - uses: actions/checkout@v4 - run: make build ``` ```yaml # after jobs: build-arm: runs-on: craci steps: - uses: cracicorp/setup@v1 with: image: "craci-slim-arm" - uses: actions/checkout@v4 - run: make build ``` ### Mixed-OS matrix with `include:` Linux legs move, on either architecture. macOS and Windows legs keep their runners. Carry the image per leg so the arm leg lands on ARM hardware. ```yaml jobs: build: runs-on: ${{ matrix.runs_on || matrix.runner }} strategy: fail-fast: false matrix: include: - runner: ubuntu-24.04 target: x86_64-unknown-linux-gnu runs_on: craci - runner: ubuntu-24.04-arm target: aarch64-unknown-linux-gnu runs_on: craci craci_image: craci-slim-arm - runner: macos-15-xlarge target: aarch64-apple-darwin - runner: windows-x64 target: x86_64-pc-windows-msvc steps: - uses: cracicorp/setup@v1 if: ${{ matrix.runs_on == 'craci' && matrix.craci_image }} with: image: ${{ matrix.craci_image }} - uses: actions/checkout@v4 # shared build steps unchanged ``` The `if:` keeps the step from executing on the macOS and Windows legs; the per-leg `image` expression is what routes the arm leg to ARM hardware. If some legs also need a non-default `size`, add it as an expression too — a literal would apply to every CRACI leg in the matrix. ### Cross-product matrix with no `include:` rows to flag Compute `runs-on` from the OS value. ```yaml jobs: test: runs-on: ${{ startsWith(matrix.os, 'ubuntu') && 'craci' || matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-22.04, macos-latest, windows-latest] python: ["3.11", "3.12"] steps: - uses: actions/checkout@v4 # shared build steps unchanged ``` Any `include:` legs pinning a specific OS follow the same rule. Watch the arm trap: `startsWith(matrix.os, 'ubuntu')` also matches `ubuntu-24.04-arm`, which would route an ARM leg to CRACI and silently run it on amd64. When the matrix mixes Ubuntu architectures, key the image off the same value as an expression — an empty value leaves the image unset: ```yaml - uses: cracicorp/setup@v1 if: ${{ startsWith(matrix.os, 'ubuntu') }} with: image: ${{ endsWith(matrix.os, '-arm') && 'craci-slim-arm' || '' }} ``` Gating the step on `endsWith(matrix.os, '-arm')` and hardcoding the image does not work: CRACI reads no step condition, so every Ubuntu leg would be scheduled on ARM. ## Sizing, when the default is not enough The baseline is `size: 2`. Raise it only for jobs where the default is measurably slow. Each unit is 1 vCPU + 3 GiB RAM. | Workload | Suggested `size` | | ------------------------------------------------------------------- | ---------------- | | Lint, format check, small unit tests, small Docker build | default | | Typical app build and test suite (Node, Go, Python) | 4 | | Large compiles: Rust or C++ workspaces, multi-arch, monorepo lanes | 8+ | | Very heavy parallel builds | up to 32 | Start with the default and tune from observed run times. Do not reflexively map an old `-4` or `-8` hosted-runner suffix to the same number — that suffix was a billing tier on another provider, not a measured requirement. ## What does not change - Triggers, `concurrency`, `permissions`, `env`, `timeout-minutes`, `defaults`, step `if:`, outputs, and `needs` are all standard GitHub Actions. Leave them alone. - Secrets are referenced the normal way with `${{ secrets.* }}`. - SBOM generation is automatic for every build — there is no step to add. Marking the SBOM complete additionally needs a network policy that blocks unmatched traffic; treat that as a follow-up, not part of a routing migration. See . Two things to know that are not visible in the diff: - **Multi-arch container builds.** With native arm64 available, a job that built `linux/arm64` under QEMU emulation is usually better off as a real arm64 job, or as two native legs feeding a manifest. Emulation still works if the existing shape is worth keeping — treat replacing it as a separate change, not part of the migration. - **Custom CAs.** Jobs run behind a managed network and carry a CRACI CA certificate in the system trust store. CRACI already exports `SSL_CERT_FILE`, `REQUESTS_CA_BUNDLE`, `CURL_CA_BUNDLE`, `NIX_SSL_CERT_FILE`, `NODE_EXTRA_CA_CERTS`, and `JAVA_TOOL_OPTIONS` — do not overwrite them. A certificate error in a migrated job is almost always a tool that ships its own CA bundle and ignores all of them; point that tool at `/etc/ssl/certs/ca-certificates.crt`. See . ## Verification checklist For each migrated job: - [ ] `runs-on` resolves to `craci`. - [ ] Build and test commands, caching, and artifacts are unchanged. - [ ] No `cracicorp/setup@v1` step was added unless the job genuinely needs a non-default size, image, template, or network policy. - [ ] Every job that was on an arm64 runner has `image: craci-slim-arm`. A missing image here is silent — the job passes, on the wrong architecture. - [ ] Where a setup step is present in a matrix, every `with:` value is a per-leg expression — an `if:` does not scope CRACI's scheduling — and the step is gated so it never executes on macOS or Windows legs. - [ ] The YAML parses — run `actionlint` if it is available. - [ ] The diff contains only routing changes, plus any deliberate setup steps. Then confirm the first run actually picks up a runner. A job stuck in queue means the GitHub App connection is missing or does not cover this repository.