◆ docs

Migrating from GitHub Actions

Bring your workflows as they are. rickub reads the same .github/workflows files, runs the same actions, and evaluates the same expressions — this page covers what moves untouched, what you have to move by hand, and what genuinely differs.

A migration is three steps: push the repository, re-create your secrets, push a commit. You do not rewrite your workflow YAML — rickub parses GitHub Actions workflows, not a dialect of them. The same .github/workflows/ directory that runs on GitHub runs here.

That is not a claim we make from a spec: rickub builds and ships itself from workflow files that run on both hosts. The same repository's files are gated on github.server_url so each host runs its half — one set of YAML, two CI systems, no fork of the config.

Note

Compatibility runs one way: a .github/workflows/ file that runs on github.com runs here unchanged. The reverse is not a goal — .rickub/workflows/ is rickub's own dialect and may use keys github.com would reject, which is where rickub adds what GitHub has no syntax for. Keep a file in .github/workflows/ and it stays portable; move it to .rickub/workflows/ and you trade portability for the extra keys.

What the dialect adds today

The first of those extra keys is reports: — a job-level declaration of the test reports it produces, which rickub parses into a Tests tab on the run page (see CI / Actions). GitHub has no syntax for it, and the shape it makes possible is the reason it lives here rather than being squeezed into a uses: step:

# .rickub/workflows/ci.yml — a parse error in .github/workflows/
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - run: go test ./... -json > report.json
    reports:
      go:
        tests: report.json

Because it is a key on the job rather than a step, it cannot be skipped by the very test failure it exists to describe. Every product that models the report as a step documents a workaround for that — when: always, post { always }, succeededOrFailed(), if: !cancelled(). Here there is none to remember.

Heads up

Putting reports: in .github/workflows/ is a parse error, and deliberately so — that file is also meant to parse on github.com, where the key is invalid. rickub tells you to move the file rather than accepting a workflow that would behave differently on the two hosts. That is the same promise read from the other side: what runs on github.com runs here, so what runs here must not stop running there.

needs-workflows: waiting for another workflow's verdict

The second dialect key is needs-workflows:. Where needs: orders jobs inside one workflow, needs-workflows: makes a job wait for another workflow's run on the same commit — and refuse to run if that workflow did not succeed. It is how you keep a deploy from shipping a commit whose tests are red:

# .rickub/workflows/deploy.yml — a parse error in .github/workflows/
on:
  push:
    branches: [main]
  workflow_dispatch:
    inputs:
      deploy_red_sha:
        description: "Break glass: deploy even if CI failed"
        type: boolean
        default: false

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - run: make image        # runs in parallel with CI — nothing is held yet

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - run: ./release.sh
    needs-workflows:
      workflows: [CI]          # every name here must be green at this commit
      timeout-minutes: 240     # optional; how long to wait for a verdict
      override-input: deploy_red_sha   # optional break-glass, see below

needs-workflows: CI and needs-workflows: [CI, Security] are shorthands meaning the same thing with the defaults. The names are workflow display names — the name: at the top of the file, or its filename if it has none.

The gate is enforced by rickub itself, not by a step in your job: a gated job is created unclaimable and only becomes runnable when rickub has seen the required runs succeed for that commit. So there is no window in which a runner can pick it up early, and nothing to remember to write. Every other outcome refuses the job — it goes red with a log saying which workflow stopped it, and nothing it would have done happens:

  • the required run failed, was cancelled, or timed out (its jobs failed) → refused immediately;
  • the required run is still going → the job waits;
  • the required workflow never ran at this commit — renamed, deleted, or filtered out by its own on: — → the job waits until timeout-minutes and is then refused. It never waits forever.
Note

A re-run counts. The gate reads the latest run of each named workflow at that commit, so re-running a red CI until it is green and then re-running the deploy is the ordinary way to recover — and, in the other direction, an old green run cannot authorise a commit whose re-run went red.

override-input: is the break-glass path, and it names a workflow_dispatch input for a reason: an input can only come from a manual dispatch by a repository admin on the Run workflow form — so an automatic push can never take it. Using it is recorded in the audit trail and raises a critical alert — it is never silent. Break-glass is deliberately a signed-in human's act: the REST dispatch API and the MCP dispatch_workflow tool refuse a dispatch that sets an override-input: input (422 breakglass_refused), so a script or an AI agent holding an admin's token cannot open the deploy gate — an admin has to tick the box themselves.

What runs unchanged

Workflows live where they already live — .github/workflows/*.yml. rickub also reads .rickub/workflows/, which takes precedence: if a repository has a .rickub/workflows/ directory, rickub reads its workflows from there and ignores .github/workflows/ entirely (the two never both run). That lets you keep GitHub's directory for github.com and add rickub-native workflows here without running each pipeline twice. With no .rickub/workflows/, rickub falls back to .github/workflows/ unchanged. Everything in this list is parsed and honoured with GitHub's semantics:

  • Triggers — push (branches, tags, path filters), pull_request (dispatched when the merge request opens and re-run on every push to its head branch, including a push to a fork's branch), workflow_dispatch with typed inputs, schedule cron entries, and release. types:, branch and path filters all apply.
  • Job graph — needs: ordering, job outputs:, if: conditions including failure() and always(), and continue-on-error.
  • Matrices — strategy.matrix with include/exclude, fail-fast, and max-parallel.
  • Environment — env: at workflow, job and step level, defaults.run.shell / working-directory, and timeout-minutes.
  • Reusable workflows — jobs.<id>.uses against a workflow_call workflow, with with: inputs and secrets: (including inherit).
  • Service containers — services: and container jobs run against a real Docker daemon inside the job's machine. A postgres:16-alpine service is exactly how rickub's own test jobs get their database.
  • Step summaries — anything a step appends to $GITHUB_STEP_SUMMARY is rendered on the run page.

The actions you already use

  • actions/checkout — pinned to a commit SHA or written as @v4, either is fine, because the version is never consulted: rickub clones your repository onto the job's work disk before the first step runs, so the tree is already there when checkout looks for it. That clone is complete — full history, every branch, every tag — so fetch-depth: 0 and git describe --tags need nothing extra, and with: ref: (a tag, a branch, a SHA) moves the workspace to that commit, failing the step if it cannot be resolved. submodules: true/recursive initializes submodules this job can fetch — public github.com URLs and relative URLs that resolve to the repository itself; a submodule pointing at another repository of this forge is left uninitialized with a ::warning:: naming it and why (cross-repo fetches with the job token are not available). persist-credentials: false drops the job's git credential after the checkout step. An input rickub does not act on (ssh-key, filter, …) prints a ::warning:: naming it rather than being dropped silently. (A checkout of a different repository — with: repository: — does run a real clone. It resolves the name on THIS forge first: a repository hosted here is checked out from here through the job's own token, and only a name this forge does not have falls back to github-server-url/github.com, anonymously and with a ::warning:: saying so. Cross-repository reads on this forge are limited to public repositories — a private one fails the step naming the repository, because which private repositories a job may read is not something rickub guesses.)
  • Any JavaScript action — there is no allowlist. rickub runs every node action (runs.using: node12/16/20/24), fetched from github.com at the ref you pinned, exactly as a GitHub-hosted runner would. Verified in the wild: actions/checkout, actions/setup-node, actions/setup-go, actions/setup-python, oven-sh/setup-bun, pnpm/action-setup, actions/cache, actions/upload-artifact, and actions/github-script — but the point is the mechanism, not the list: a node action you have never heard of runs the same way.
  • Composite actions — both uses: ./.github/actions/whatever (resolved against the checked-out tree) and published composite actions from github.com (runs.using: composite) run, including composites that themselves nest further uses: steps.
  • Docker container actions and plain docker build — a marketplace action with runs.using: docker (built from its Dockerfile or pulled from docker://…), docker/login-action, docker build and docker push all run against a real Docker daemon inside the job. The standard ubuntu-latest class has Docker; a heavy docker build (compiling a toolchain inside the image) wants the large class (see below).
Tip

Keep your SHA pins. An action is fetched at the exact ref you pinned, so a supply-chain posture you already enforce travels with the file.

How actions are fetched (and rate limits)

Actions are fetched on the runner host — never inside the (network-locked) job sandbox — from github.com at the ref you pinned, then cached across jobs so a given owner/repo@ref is fetched once and reused. The first time a pipeline references a new action, that fetch goes out to github.com.

Note

GitHub rate-limits anonymous git fetches from a datacenter IP far more tightly than authenticated ones. A runner fleet configured with an actions token authenticates those fetches, so a heavy pipeline pulling many new actions is not throttled. Without a token, fetches are anonymous and a burst of brand-new actions can be rate-limited by GitHub.

If a fetch is rate-limited or otherwise fails, the step fails with a message that says so plainly — "could not fetch the action from github.com (this can be GitHub rate-limiting anonymous requests; a maintainer can configure an actions token)" — rather than implying the action is unsupported. It is retried a few times with backoff first, so a momentary github.com hiccup does not fail an otherwise-healthy job.

What an action can't do

The runtime runs the three execution models above — JavaScript, composite, and Docker container actions. The one thing it will not run is an action that declares a runs.using it does not recognize (anything other than node12/16/20/24, composite, or docker). Such a step fails with a clear "unsupported action model" message naming the runs.using it saw — distinct from a fetch failure, so you can tell a transient github.com problem from a real incompatibility.

Runner classes

runs-on: resolves against rickub's runner catalog. Every job runs in its own Linux x86-64 microVM, thrown away when the job ends.

runs-on Machine Docker CI minutes
ubuntu-latest · aliases: ubuntu-24.04, ubuntu-22.04, ubuntu, linux 2 vCPU · 4 GiB, 16 GiB scratch Service containers (services:) and docker actions run here — yes, a services: postgres, redis or the like boots against a real Docker daemon. A heavy docker build (compiling a toolchain inside the image) wants large. 1×
large · alias: ubuntu-latest-large 4 vCPU · 8 GiB, 32 GiB scratch Yes — a Docker daemon inside the job, sized for docker build 2×

The standard class runs service containers, so a job with a services: block needs no special runs-on:. Ask for large when a job does a heavy docker build — compiling a toolchain inside the image needs the extra RAM and scratch. A job that names no runs-on: gets the repository's default class from Settings → Actions.

Heads up

windows-latest and macos-latest are not available, and are deliberately not aliased to Linux: a job asking for one fails immediately with the list of supported labels rather than queueing forever waiting for a runner that is never coming.

GitHub's mapping form of runs-on: (runs-on: {group: …, labels: […]}) resolves through its labels: exactly like a plain label list — {labels: [large]} is the large class. The group: half has no rickub equivalent (there are no runner groups): it is ignored, and the Actions tab names every workflow that declares one so the omission is visible rather than silent.

timeout-minutes: is honoured per job and accepts a ${{ }} expression (timeout-minutes: ${{ matrix.minutes }}), evaluated with the job's matrix bindings. A job that declares none gets 60 minutes; the ceiling for a job that declares one is 360 minutes. continue-on-error:, strategy.fail-fast and strategy.max-parallel accept expressions the same way.

Secrets

Secrets do not travel with a git push — this is the one thing you genuinely have to move by hand. Re-create them under the repository's Settings → Secrets and variables (organizations have org-level secrets too, scoped to the repositories an owner has allowed). Non-sensitive values go in Variables and resolve as ${{ vars.NAME }}.

Heads up

Secret names may not start with GITHUB_ or RICKUB_ — those prefixes belong to the values rickub injects into every job, so the form rejects them. If a workflow reads a secret called something like GITHUB_DEPLOY_KEY, rename it (for example to DEPLOY_KEY) in both the settings page and the YAML.

About secrets.GITHUB_TOKEN

Every job still gets a GITHUB_TOKEN — minted per job, scoped to the repository, masked in logs, and resolvable as ${{ secrets.GITHUB_TOKEN }} and ${{ github.token }}. It authorizes rickub's GitHub-compatible REST endpoints, so actions that cut releases, comment on merge requests or read contents work unmodified.

Heads up

It is not a github.com credential. rickub's GITHUB_TOKEN cannot authenticate to github.com or to ghcr.io — it is signed by rickub and github.com has never heard of it. This is the classic migration trap: a job that pushed to GHCR with password: ${{ secrets.GITHUB_TOKEN }} will fail on authentication here. For anything on GitHub's side, bring your own PAT and store it as a rickub secret.

Pushing to GHCR from a rickub job, with a classic PAT that has write:packages:

- name: Log in to GHCR
  uses: docker/login-action@v3
  with:
    registry: ghcr.io
    username: your-github-user
    password: ${{ secrets.GHCR_PAT }}   # a github.com PAT you created

The default token is read-only. A job that writes must say so with a permissions: block (contents: write, issues: write, …) — the same grant you would declare on GitHub, and it travels with the workflow file.

The registry

If your workflow builds images, the shortest migration is to point them at registry.rickub.com — because then there is no login step at all. Every trusted job gets a per-job registry credential injected as RICKUB_REGISTRY_TOKEN, the host as RICKUB_REGISTRY_HOST, and the job's Docker is logged in before the first step runs.

jobs:
  image:
    runs-on: large
    permissions:
      packages: write   # needed the first time a name is pushed
    steps:
      - uses: actions/checkout@v4
      - run: docker build -t "$RICKUB_REGISTRY_HOST/acme/api:${{ github.sha }}" .
      - run: docker push  "$RICKUB_REGISTRY_HOST/acme/api:${{ github.sha }}"

permissions: packages: write is what lets a job claim a new image name — the first push binds that name to this repository. Without it a job may only push names already bound to its repository. See the Container registry page for visibility, bindings and deploy tokens.

Note

Jobs from forks get no registry credential at all, by design — see what's different.

Artifacts and cache

actions/upload-artifact, actions/download-artifact and actions/cache work unchanged, against rickub's own endpoints. Uploaded artifacts are listed and downloadable from the run page. The limits:

  • 10 GiB per artifact or cache entry — the same as the per-run and per-repository totals, so one big artifact is as legal as a hundred small ones — plus 100 artifacts per run. A bigger upload is refused with a 413 before it transfers, rather than accepted and lost. (A cache-to: type=gha,mode=max export fits here now; it did not before September 2026.)
  • What actually limits a very large artifact is time, not size: one upload or download has 30 minutes, so 10 GiB needs about 6 MB/s sustained end to end. A transfer that misses it is cut with a 408. If you regularly move artifacts that big, split them.
  • 90-day artifact retention. A shorter retention-days: is honoured; a longer one is clamped to 90.
  • 10 GiB of cache per repository, evicted least-recently-used when it fills, and an entry unused for 7 days is swept. Cache entries are scoped per branch, exactly as on GitHub.

What's different

Everything below is a real gap or a real difference. Read this section before you cut over a deploy pipeline.

Feature On rickub
environment: protections Not enforced. The key is parsed, validated and shown (the run page badges the job, the Actions tab warns), and environment-scoped secrets work: a secret saved with an Environment on the repository's secrets page reaches only jobs declaring that environment, overriding a same-named repository secret. But there are no protection rules — no required reviewers, no wait timers, no deployment branches — so a job that names an environment simply runs, and every run of it says so on the run page. Do not leave one in place as an approval gate; see the pattern below. environment.url is accepted but unused (no deployment records yet).
Job-level concurrency: Ignored. Workflow-level concurrency: works: runs share a group, and cancel-in-progress cancels the group's earlier in-flight runs. Group values are matched literally, except the common ${{ github.ref }} idiom, which is resolved per ref. cancel-in-progress takes a literal or an expression over github.*, so the usual cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} works — keep it, or your default branch's runs cancel each other and you lose the record of which commits were green. An expression that cannot be evaluated does not cancel.
on: triggers pull_request, push, release, schedule, workflow_call and workflow_dispatch fire — that exact list is generated from the parser's own event set (actions.WiredEvents), and a test fails if this row and the code drift apart. Any other declared event — issue_comment, pull_request_target, workflow_run, repository_dispatch, check_run, merge_group, create, delete and friends, or a plain typo — is inert: the workflow parses, its wired triggers still fire, but the unwired verbs never dispatch anything. rickub does not stay quiet about it: the Actions tab names every workflow with unwired triggers (and says which ones will never run automatically at all), every run of a mixed workflow repeats the inert verbs on its run page, and a schedule: cron entry that fails to parse is named and dropped rather than vanishing.
GITHUB_TOKEN Valid for rickub only. Never for github.com or ghcr.io.
GITHUB_API_URL Points at rickub's GitHub-compatible REST endpoints — a subset covering repositories, releases and assets, issue comments, refs, contents and pull creation. Actions outside that surface will get a 404 from a real endpoint list, not a silent wrong answer.
GITHUB_SERVER_URL Carries a host form the official artifact and cache clients accept; ${{ github.server_url }} and RICKUB_GITHUB_SERVER_URL carry your real rickub URL.
github.repository_owner The rickub-side owner handle. Anything that hardcodes your GitHub org name needs updating.
Fork pull requests Run untrusted: no secrets, a read-only token, no registry credential, and — by default — held until a maintainer approves the run. Configurable under Settings → Actions.
Runner platforms Linux x86-64 only. No Windows, no macOS, no self-hosted labels.
CI minutes Metered against your plan's monthly allowance; large is published at 2× weight. An exhausted allowance blocks dispatch and leaves a red commit status on the pushed commit.

Gating a deploy without environment:

Since environment approvals are not enforced (the run page warns about every run that names one), gate the deploy on something that is enforced: the presence of a secret. Seeding the secret is the deliberate act that turns deployment on; deleting it turns deployment off — and no workflow edit is involved either way.

- name: Deploy
  env:
    DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
  run: |
    set -uo pipefail
    if [ -z "${DEPLOY_TOKEN}" ]; then
      echo "DEPLOY_TOKEN is not set — continuous deployment is OFF."
      exit 0
    fi
    set -e
    ./deploy.sh
Tip

This is the pattern rickub uses on itself. The absence of the secret is the off switch, and it is visible in one place — the repository's secrets page — instead of buried in a settings screen nobody reads.

Migration checklist

  1. Push the repository — or import it, which brings code, issues and pull requests along.
  2. Leave .github/workflows/ alone. Resist the urge to rewrite; start from what already works.
  3. Re-create your secrets under Settings → Secrets and variables, renaming anything that starts with GITHUB_ or RICKUB_ (in the settings page and in the YAML). An environment secret gets the environment's name in the form's Environment field; the repository's own secrets leave it blank.
  4. Pick runner classes — add runs-on: large to any job that builds or runs containers; leave the rest on ubuntu-latest.
  5. Re-point image pushes at registry.rickub.com (no login step, add permissions: packages: write) — or keep pushing to GHCR with your own PAT stored as a secret.
  6. Audit anything that talked to github.com — GITHUB_TOKEN, github.repository_owner, hardcoded org names.
  7. Replace environment: approval gates with the secret-presence pattern above — the name, the secrets layer and the run-page badge carry over, but nothing waits for a reviewer.
  8. Push a commit and watch it run under the repository's Actions tab. Fix forward from a real run, not from a guess.
Note

If a workflow behaves differently here than it did on GitHub, that is a bug worth reporting, not a rewrite you owe us.

Was this page helpful?