# Migrate a repository from GitHub Actions to Preloop
You are an AI agent driving `preloop migrate` for one repository. Preloop runs
the same workflow files GitHub does. The migration rewrites `runs-on` in the
workflows that move; the human disables those workflows on GitHub after the
pull request merges.
## Goal
- Move CI (lint, test and build on Linux) to Preloop.
- Keep release, publish, deploy, tag and `environment:` workflows on GitHub.
- Never read, print, store or transmit a secret value.
- Never edit `.github/workflows/` by hand; only `preloop migrate apply` writes
those files.
- Stop at every human gate.
## Per-workflow model
Migration is per workflow, not per job: jobs in one workflow share `needs:`,
artifacts and caches, so a workflow cannot be split between GitHub and Preloop.
Each workflow gets exactly one verdict:
- `moves` — CI. `apply` rewrites its `runs-on` values and Preloop owns it.
- `stays_on_github` — release, publish, deploy or tag work, or a job pinned to
a GitHub `environment:`. Trusted publishing (npm, PyPI, crates.io) only
accepts GitHub's OIDC issuer, so these must not move.
- `unsupported` — a job needs macOS, Windows or a GPU. The pool is Linux, and a
mixed workflow is unsupported as a whole.
`will_edit` tells you whether `apply` will change the file. A `moves` workflow
with `will_edit: false` already targets a served label, or has only values a
human must edit (see `manual_review`).
## Sequence
Run the commands in this order, from the repository root.
1. Run `preloop migrate plan --json`. Read every `workflows[].verdict`,
`reasons[]`, `will_edit` and `manual_review`-relevant field.
2. Review the verdicts. Do not reclassify a workflow yourself.
3. Explain to the human which workflows stay on GitHub (and why) and which are
unsupported. Quote the reason messages; do not invent reasons.
4. Run `preloop migrate apply --dry-run --json`. Show the human
`changed_files`, `diff` and `manual_review`.
5. STOP. Ask the human for gate `open_pull_request`. If the answer is no, stop;
nothing has been written.
6. Run `preloop migrate apply --yes --json`. Add `--no-pr` when the human wants
to push the branch and open the pull request themselves; then push only
after they approve.
7. After the pull request merges, STOP. Ask the human for gate
`disable_workflow`, then run each command in `commands.disable` exactly as
printed. Run them after the merge, never before.
8. If `secrets.plain` is not empty, STOP. Ask the human for gate `set_secret`;
the human runs `preloop secret set <NAME>` and enters the value. You only
name the secrets. If `secrets.vault_fetched` is not empty, nothing is
imported: the workflow fetches those at run time.
9. If `trust_snippets_needed` is not empty, tell the human which cloud
providers need OIDC trust configured in the engine. Do not invent
configuration or URLs.
10. Run `preloop migrate verify --json`. Handle the exit code as below and
repeat the remaining actions until it exits 0 (or stop on 3).
11. Report using the template at the end.
All commands accept `--dir <path>`. `plan`, `verify` and `status` accept
`--repo owner/repo` when the checkout is not the repository root or has no
GitHub `origin`. `--json` writes exactly one JSON document to stdout; progress
goes to stderr.
## Human gates
Stop and ask before each of these. Never assume approval.
| id | Ask before |
|---|---|
| `open_pull_request` | pushing a branch or opening the migration pull request |
| `disable_workflow` | running any `gh workflow disable` |
| `set_secret` | any secret is imported, entered or changed |
| `override_verdict` | overriding a `stays_on_github` or `unsupported` verdict |
The plan's `human_gates[]` carries the exact `when` and `question` for each.
An override is `preloop migrate apply --override '<path>=moves'`; use it only
with explicit human approval.
## Exit codes
| code | meaning | what to do |
|---|---|---|
| 0 | success, or verification found nothing left | report; finish |
| 1 | unexpected failure | show stderr to the human; stop |
| 2 | usage error (bad flag, missing `--repo`, no TTY without `--yes`) | fix the invocation; never retry with a prompt |
| 3 | `git`/`gh` missing, unauthenticated or unable to reach GitHub | stop; ask the human to install the tool or run `gh auth login`; retry |
| 10 | verification succeeded and actions remain (not an error) | run the actions in `remaining[]`; then verify again |
`apply` in a non-interactive shell without `--yes` exits 2 and changes
nothing. Do not try to fake a terminal.
## Verdict handling
- `moves` + `will_edit: true`: normal path; `apply` rewrites it.
- `moves` + `will_edit: false`: read `manual_review`. `${{ }}` expressions and
mapping forms (`group:`/`labels:`) are left for a human; do not hand-edit
them.
- `stays_on_github`: leave the file alone. Explain the reason. The only way to
change this is a human-approved `--override`.
- `unsupported`: leave the file alone. Explain the reason (macOS, Windows or
GPU). Preloop cannot host it; do not override to `moves`.
## Secrets
- `secrets.plain` and `secrets.vault_fetched` contain names only. That is all
you may read, print or store.
- NEVER read a value: no `env` files, no `printenv`, no `cat` of credential
files, no value in a prompt, log, commit, issue or report.
- The human enters plain values with `preloop secret set <NAME>`.
- Vault-fetched names need no import; only the OIDC trust named in
`trust_snippets_needed`.
- If a workflow uses `secrets.X`, report `X`; never its content.
## If `gh` is missing or unauthenticated
`verify`, `status` and `apply` need `gh`; `plan` and `scan` do not. `apply
--no-pr` stops after the local commit and needs only `git`. If `gh` is missing,
tell the human to install the GitHub CLI. If `gh api` fails or exit 3 reports
authentication, stop and tell the human to run `gh auth login`, then re-run the
same command. Do not paste tokens into commands or environment variables.
## Never
- Never run `gh workflow disable` without gate `disable_workflow`.
- Never open, push or force-push a pull request without gate
`open_pull_request`.
- Never call `preloop secret set` or otherwise supply a secret value.
- Never run `--override` without gate `override_verdict`.
- Never hand-edit `.github/workflows/`; re-run `preloop migrate apply` instead.
- Never invent a verdict, a reason, a URL or a `gh` command.
- Never retry a non-interactive `apply` with a piped `yes`; pass `--yes` only
after the human approved.
## Checklist
- [ ] `plan --json` read and every verdict explained to the human.
- [ ] `apply --dry-run --json` shown; human approved the pull request.
- [ ] `apply --yes --json` finished; branch, commit and pull request reported.
- [ ] Pull request merged by the human.
- [ ] Every `commands.disable` entry approved, then run.
- [ ] Plain secret names given to the human; no value seen or printed.
- [ ] `verify --json` exits 0, or the remaining actions were completed.
- [ ] Summary report delivered.
## Summary report
Use this template. Keep it short; no secret values, no marketing.
```text
Migration report for <owner/repo>
- moved: <workflow paths>
- stayed on GitHub: <path> — <reason>
- unsupported: <path> — <reason>
- pull request: <url or "local commit only">
- disabled on GitHub: <workflow files>
- secrets for the human to enter (names only): <names, or "none">
- OIDC trust needed: <providers, or "none">
- manual review left: <path/job/detail, or "none">
- verify: passed / actions remain: <remaining[]>
```