Skip to content
Latchkey LogoLatchkey home

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 compared: a runs-on line on GitHub Actions, or latchkey migrate plan and apply to Latchkey CI
The same Latchkey runners sit under both moves. The left one changes the machine; the right one changes the workflow engine, and with it what your files may contain.

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.

.github/workflows/ci.yml (the first move)
jobs:
  test:
    runs-on: latchkey-small   # was: ubuntu-latest

What latchkey migrate does

The command has three subcommands, and the repository must already be connected to Latchkey CI in your workspace.

  1. 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.
  2. --answers <file> passes those answers as JSON, in the shape the plan names them.
  3. 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 the migration:apply scope, and that works only while its creator is an owner or admin.
  4. 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.

Terminal
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/api

The 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 ActionsOn Latchkey CI
runs-on: ubuntu-latestruns-on: small or latchkey-small, up to xlarge; never mapped silently
secrets.X anywhereSet with latchkey secrets set; read as the whole value of a step's env: variable
actions/cache with restore-keyslatchkey/cache-restore and latchkey/cache-save: exact key and a version, no fallback, kept 14 days
strategy.matrix, fail-fast: trueMatrix kept; every leg runs, so fail-fast: true and max-parallel are refused
actions/upload-artifact, download-artifactlatchkey/artifact-upload and latchkey/artifact-download, kept 7 days
concurrency: with cancel-in-progress: trueGroups 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 actionA run: step. Latchkey CI does not run GitHub actions
environment: with reviewers or a wait timerAn 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.

.github/workflows/ci.yml
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: dist

After: 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.

.latchkey/workflows/ci.yml
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: dist

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

Terminal
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/api

When 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 plan changes nothing; apply opens one pull request that a person merges.
  • Latchkey CI runs only latchkey/ steps: checkout and setup actions go, third-party actions become run:.
  • 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-on and stay on GitHub Actions.

Frequently asked questions

Does latchkey migrate change my repository directly?
No. 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?
No. A 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?
No. GitHub Actions jobs run on Latchkey runners by changing runs-on to a label such as latchkey-small, and every action, secret and environment keeps working.

References

See what would move before anything does: the Latchkey CLI's migrate plan changes nothing. Start free → 30-day trial · No credit card