Migrate GitHub Actions to Latchkey CI with latchkey migrate
To migrate GitHub Actions to Latchkey CI, run latchkey migrate plan <owner/name>, answer the items it marks as needing a person, and run latchkey migrate apply <plan-id>: Latchkey opens one pull request that adds files under .latchkey/workflows/, and a person merges it. It is a move to a different runtime, not a label swap, because Latchkey CI runs only its own latchkey/ steps and no GitHub actions; if you want the runners and nothing else, change runs-on and stay on GitHub Actions.
There are two ways to put a repository on Latchkey, and they are easy to confuse. One keeps GitHub Actions as the workflow engine and changes only the machine each job runs on. The other moves the workflow itself to Latchkey CI, which reads its own files, writes its own checks and refuses anything it cannot run rather than skipping it.
This page is about the second, and about the command that does it. Everything here comes from the --help output of @latchkeydev/cli 0.16.0 and from latchkey.dev/documentation, read 2026-11-12. Where Latchkey CI does less than GitHub Actions, the page says so.

Two moves, the same runners
The first move is a routing change. Latchkey's documentation says only the runs-on: label changes, and that actions, secrets, environments and caches on GitHub's side keep working untouched. Every job gets a single-use VM, self-healing is on, and latchkey-small is $0.0025 a minute against GitHub-hosted $0.006. Rollback is reverting the line.
The second move is the subject of this page. Latchkey CI takes the same runner sizes, written runs-on: small or runs-on: latchkey-small, but the workflow file moves to .latchkey/workflows/, and Latchkey compiles it, plans the run and reports the checks.
jobs:
test:
runs-on: latchkey-small # was: ubuntu-latestWhat latchkey migrate does
The command has three subcommands, and the repository must already be connected to Latchkey CI in your workspace.
plan <owner/name>reads the GitHub Actions workflow files at the head of the default branch and prints, for each pipeline, whether it moves, and each step, job or change that needs a person's answer. The plan is saved on your machine with the answers it was made with.--answers <file>passes those answers as JSON, in the shape the plan names them.apply <plan-id>opens the one pull request that adds the Latchkey CI workflow files, once every item has an answer, and closes any earlier apply's open pull request. A person merges it. The key needs themigration:applyscope, and that works only while its creator is an owner or admin.status <owner/name>shows the newest migration pull request: open, merged, closed or superseded.
Running it
apply is sent once. If its answer is lost, the help says to run status before applying again, so a retry does not open a second pull request. Exit code 2 means a usage error or a plan id this machine has no saved plan for, 1 means Latchkey refused or could not be reached, and 0 means the plan, the pull request or the status was printed.
npm install -g @latchkeydev/cli
latchkey login
latchkey migrate plan acme/api
latchkey migrate plan acme/api --answers answers.json
latchkey migrate apply <plan-id>
latchkey migrate status acme/apiThe checklist: what changes in each file
These rows are the documented replacements. A file that still holds the left column after the move is refused at the line, with the reason, rather than run with the key ignored.
| In GitHub Actions | On Latchkey CI |
|---|---|
runs-on: ubuntu-latest | runs-on: small or latchkey-small, up to xlarge; never mapped silently |
secrets.X anywhere | Set with latchkey secrets set; read as the whole value of a step's env: variable |
actions/cache with restore-keys | latchkey/cache-restore and latchkey/cache-save: exact key and a version, no fallback, kept 14 days |
strategy.matrix, fail-fast: true | Matrix kept; every leg runs, so fail-fast: true and max-parallel are refused |
actions/upload-artifact, download-artifact | latchkey/artifact-upload and latchkey/artifact-download, kept 7 days |
concurrency: with cancel-in-progress: true | Groups kept; the holder is never cancelled, so cancel-in-progress: true is refused |
actions/checkout, actions/setup-* | Removed: checkout is automatic, and the image carries the toolchains |
| A third-party action | A run: step. Latchkey CI does not run GitHub actions |
environment: with reviewers or a wait timer | An environment an owner or admin approves; reviewer lists and wait timers are not supported yet |
Before: a GitHub Actions workflow
A sharded test matrix with a prefix-matching cache, a build that uploads dist, and a concurrency group that cancels the previous run on the same ref.
name: ci
on:
push:
branches: [main]
pull_request:
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
shard: [1, 2, 3]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.npm
key: npm-${{ hashFiles('package-lock.json') }}
restore-keys: npm-
- run: npm ci
- run: npm test -- --shard=${{ matrix.shard }}/3
build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- run: npm ci && npm run build
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: dist
path: distAfter: the Latchkey CI workflow
The checkout step and fail-fast are gone, cancel-in-progress is gone because a newer run waits for the group instead, and the cache is two steps with an exact key. Version 1.1.0 of the cache steps appends a hash of the files named in hash-files to the key, as hashFiles() did. A lockfile change starts a cold cache rather than falling back to an older one.
name: ci
on:
push:
branches: [main]
pull_request:
concurrency:
group: ci-${{ github.ref }}
jobs:
test:
runs-on: small
strategy:
matrix:
shard: [1, 2, 3]
steps:
- uses: latchkey/cache-restore@1.1.0
with:
key: npm
version: '1'
hash-files: package-lock.json
into: '~'
- run: npm ci
- uses: latchkey/cache-save@1.1.0
with:
key: npm
version: '1'
hash-files: package-lock.json
from: '~'
path: .npm
- run: npm test -- --shard=${{ matrix.shard }}/3
build:
needs: test
runs-on: small
steps:
- run: npm ci && npm run build
- uses: latchkey/artifact-upload@1.0.0
with:
name: dist
path: distAdvisory mode, side by side, then the switch
Merging the migration pull request does not turn GitHub Actions off, and it does not make Latchkey CI's verdict count yet. While the GitHub Actions file a Latchkey file replaces is still in the commit, that Latchkey pipeline runs on trial, where its failure fails no check. Enrolling the repository with Move CI to Latchkey, under Migrate in the dashboard, goes further and puts it in advisory mode: every failure is written as a neutral check titled with the real outcome, such as Advisory: failed. In a trial run, a job that names an environment is held unless its file writes advisory: run and gives it a dry run, and even then it still waits for its approval unless the environment needs none.
You compare results commit by commit on Side by side or the Runs page, and choose Switch to Latchkey CI when you trust it. The switch opens one pull request that removes the GitHub Actions files the Latchkey files replace. Until it merges, a job that exists in both files can run in both places, and Latchkey never cancels or disables a GitHub Actions workflow to prevent that, so a deploy job can deploy twice. Take any check that comes from a removed workflow out of branch protection before you merge the switch.
Check a file before you push it
validate compiles one file as Latchkey would and prints either the first refusal with its line or each job it compiles to, with the most one run can cost at list rates. preflight runs that file's jobs on your working tree before you commit, with no secrets, no identity token and no artifacts, so a job that needs one is reported as not run, with the reason.
latchkey validate .latchkey/workflows/ci.yml
latchkey preflight --workflow .latchkey/workflows/ci.yml --repository acme/api
printf '%s' "$NPM_TOKEN" | latchkey secrets set NPM_TOKEN --repository acme/api
latchkey environments list --repository acme/apiWhen to stay on GitHub Actions
Stay if your workflows lean on marketplace actions. Every one becomes a run: step you write and maintain, and Latchkey states it does not promise an Actions-compatible runtime. Stay if you need Windows, macOS or arm64: Latchkey runners are Ubuntu 24.04 on x86_64. Stay if you depend on reviewer lists, wait timers, cancelled(), outputs from a matrix job, or cancel-in-progress, none of which Latchkey CI accepts today.
Move when you want what only Latchkey CI does: a file that is refused at the exact line instead of half-running, a plan check that lists jobs added, removed and changed on every pull request, dry runs for deploy jobs during a trial, and preflight on your working tree before a commit exists. Both moves use the same runner sizes and the same self-healing. If price and repairs are the reason, the runs-on line gets you both for an afternoon's work.
Key takeaways
latchkey migrate planchanges nothing;applyopens one pull request that a person merges.- Latchkey CI runs only
latchkey/steps: checkout and setup actions go, third-party actions becomerun:. - Advisory mode runs both CIs side by side; the switch pull request removes the replaced GitHub files.
- For cheaper, self-healing runners alone, change
runs-onand stay on GitHub Actions.
Frequently asked questions
Does latchkey migrate change my repository directly?
plan only reads the workflow files on the default branch and saves the plan on your machine. apply opens a pull request that adds the Latchkey CI files, and nothing changes until a person merges it. The later switch is a second pull request, which removes the replaced GitHub Actions files.Can Latchkey CI run GitHub marketplace actions?
uses: step must name one of Latchkey's own steps, written uses: latchkey/<name>@<version> with an exact version, and any other uses: is refused at its line. A third-party action becomes a run: step that does the same work.Do I need Latchkey CI to use Latchkey runners?
runs-on to a label such as latchkey-small, and every action, secret and environment keeps working.Related guides
References
- Latchkey documentation: run your workflow file on Latchkey (verified 2026-11-12)
- Latchkey documentation: Latchkey steps (verified 2026-11-12)
- Latchkey documentation: migrate from GitHub-hosted runners (verified 2026-11-12)
- Latchkey CLI documentation and changelog (verified 2026-11-12)
- Latchkey pricing: runner sizes and rates (verified 2026-11-12)
- GitHub Actions documentation