Motoko GitHub CI Workflow
What This Is
This skill provides a comprehensive playbook for adding or updating a GitHub Actions CI workflow for Motoko projects. It covers automated testing, benchmarking, code formatting, and building (canisters or examples) using mops, moc, prettier, icp-cli, and dfx. It is a perfect companion to the mops-package-maintenance skill.
For general information on GitHub Actions, refer to the GitHub Actions Documentation. For Motoko-specific examples, you can explore public repositories in the research-ag organization.
Prerequisites
mopsCLI (local:npm i -g ic-mops, CI:caffeinelabs/setup-mops@v1)node(latest LTS recommended, at least v22 for Prettier)pocket-icfor runningmops bench. As of mops CLI v3.0.0, dfx-replica support is gone — PocketIC is the only runtimemops bench(and replica tests /--check-deploy) can use, and it must be explicitly pinned via[toolchain] pocket-ic = "<version>"inmops.toml(e.g.mops toolchain use pocket-ic 15.0.0). Without that pin,mops bencherrors outright. CI installs the pinned binary viamops toolchain bin pocket-ic(see Step 1).wasm-optfor benchmarks that should reflect optimized (production-representative) Wasm.mops bench/mops buildonly run Binaryen'swasm-optwhenmops.tomlhas an[optimize]section — and that section requires an explicit[toolchain] wasm-opt = "<version>"pin, same aspocket-ic(see Step 1).
How It Works
- Analyze the Repository:
- Determine if it's a Motoko package (has
[package]inmops.toml) or a canister project (has[canister]inmops.tomlordfx.json). - Check for tests: Look for a
test/directory and*.test.mofiles. If none are found, do NOT include themops teststep. - Check for benchmarks: Look for a
bench/directory and*.bench.mofiles. If none are found, do NOT include themops benchstep.
- Determine if it's a Motoko package (has
- Analyze Existing CI:
- If
.github/workflows/already contains CI files, analyze them to see which tools are used (dfxvsicp-clior none). - If tests/benchmarks exist in the repo but are missing from the CI, add them.
- If
- Select Canister Build Tool:
- If the project is a library package (no canisters, and no examples requiring a canister build tool), skip
dfxoricp-cliinstallation entirely. Note thatmoc(the Motoko compiler) is still required for tests and benchmarks, and is usually managed bymops. - For new projects with canisters/examples, prefer
icp-clias it is newer and lighter. - For existing projects, maintain the existing toolset (
dfxoricp-cli). - CRITICAL: Do not install
dfxandicp-clisimultaneously.
- If the project is a library package (no canisters, and no examples requiring a canister build tool), skip
- Inspect
mops.toml: Read the existing[toolchain]section. Unlikemoc,pocket-icis not optional when the package has benchmarks (or replica tests /--check-deploy) —mops benchrequires it to be pinned. If[toolchain] pocket-icis missing and the package has benchmarks, flag it to the user and add the pin (mops toolchain use pocket-ic <version>, e.g.15.0.0) rather than letting CI fail later. Also check for[optimize]— if the package has benchmarks and it's missing, add it (with a[toolchain] wasm-optpin) so bench numbers reflect optimized Wasm; if themops-package-maintenanceskill already ran, this is usually already done. Other[toolchain]entries and thefilesfield stay hands-off (see Step 1). - Create or Update Workflow File: Identify existing workflow files in
.github/workflows/(e.g.,pull_request_build.yml,test.yml,ci.yml). If none exist, create.github/workflows/ci.yml. Ensure parallel jobs are used for efficiency. - Configure Parallel Jobs:
testjob: Handles dependency installation,mops test, andmops bench.fmtjob: Handlesprettierformatting checks.
- Add Build Steps: If examples or canisters are present, add steps to build them using the selected canister build tool.
Implementation
Step 1 — Inspect mops.toml and pin pocket-ic/wasm-opt if needed
Before adding the CI, read mops.toml. Leave [toolchain] moc and other entries as-is.
pocket-ic is mandatory for benchmarks (mops CLI v3.0.0+): dfx-replica support was removed from mops bench entirely — PocketIC is now the only runtime, and it must be pinned in [toolchain] or mops bench fails immediately with an error naming the fix (mops toolchain use pocket-ic <version>). mops test still does not need pocket-ic (it runs via the Motoko interpreter or WASI) — the pin only matters when the package has benchmarks, replica tests, or uses --check-deploy.
Determine which CI strategy to use based on the current [toolchain] content:
- Package has benchmarks and
pocket-icis already pinned in[toolchain]: install it in CI viamops toolchain bin pocket-icand runmops bench. See Step 2 template and Pitfall #2 for the exact snippet. - Package has benchmarks and
pocket-icis absent from[toolchain]: tell the usermops benchwill fail in CI without a pin, and add one (mops toolchain use pocket-ic <version>— mops's own error message names a concrete version, e.g.15.0.0). Unlike thefilesfield, this is not something to leave hands-off — an unpinned package cannot runmops benchat all. - No benchmarks: Nothing to do —
mops testdoes not needpocket-icand no benchmark runtime is required.
[optimize] + wasm-opt — benchmarks should reflect optimized Wasm. mops bench only runs Binaryen's wasm-opt if mops.toml has an [optimize] section (even an empty one, defaulting to level = "O3"); without it, benchmarks silently measure unoptimized code. Like pocket-ic, once [optimize] is present it needs a [toolchain] wasm-opt pin or the build fails before compiling.
- Package has benchmarks and
[optimize]is already configured: installwasm-optin CI viamops toolchain bin wasm-optalongsidepocket-ic, beforemops bench. - Package has benchmarks and
[optimize]is missing: add it (with a[toolchain] wasm-optpin, e.g.mops toolchain use wasm-opt <version>) the same way as thepocket-icpin above — this is normally handled by themops-package-maintenanceskill during a maintenance pass, but add it here too if that hasn't happened yet, so CI doesn't ship benchmarks that measure unoptimized code. - No benchmarks: Nothing to do.
Also, check if mops.toml contains a files field under [package]. If it does, do NOT modify it. Trying to be smart about the files field can lead to unintended side effects. Instead, report its current configuration to the user and warn them if it seems to be missing important directories (like examples/) or if it includes unsupported extensions (only .mo, .did, .md, and .toml are allowed for mops publish).
Step 2 — Create or Update the GitHub CI Workflow
Identify existing workflow files in .github/workflows/. If one exists, update it. Otherwise, create .github/workflows/ci.yml. Use the latest versions of actions (e.g., actions/checkout@v6, actions/setup-node@v6) and parallelize the tasks.
Note: Using the latest major versions of actions ensures you get the latest features and security updates.
name: CI
on:
push:
branches: [main, master]
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
jobs:
test:
name: Tests and Benchmarks
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: latest
- name: Install mops
uses: caffeinelabs/setup-mops@v1
- name: Make sure moc is installed
run: mops toolchain bin moc
- name: Show versions
run: |
mops --version
$(mops toolchain bin moc) --version
- name: Install dependencies
run: mops install
# Bench runtime: PocketIC is the only runtime `mops bench` supports
# (dfx-replica support was removed in mops CLI v3.0.0). `[toolchain]
# pocket-ic` must already be pinned in mops.toml (see Step 1) —
# `mops toolchain bin pocket-ic` downloads that pinned version.
# `mops bench` starts (and stops) PocketIC itself, so there is no
# separate "start" step. Omit both steps if the package has no
# benchmarks.
- name: Make sure pocket-ic is installed
run: mops toolchain bin pocket-ic
# Only if mops.toml has an [optimize] section (see Step 1) — it
# requires a [toolchain] wasm-opt pin, downloaded the same way.
# Omit this step if the package has no [optimize] section.
- name: Make sure wasm-opt is installed
run: mops toolchain bin wasm-opt
- name: Run tests
run: mops test # Omit this step if the package has no tests. Note: `mops test` does not use pocket-ic.
- name: Run benchmarks
run: mops bench # Omit this step if the package has no benchmarks.
fmt:
name: Formatting Check
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: latest
- name: Prettier Check
run: |
npm install prettier prettier-plugin-motoko --no-save
npx -y prettier --plugin prettier-plugin-motoko --check '**/*.{mo,json,md}'
Step 3 — Handle Examples and Canisters (Optional)
If the repo has examples (or example) that are canisters:
Add a step to the test job (or a new job) to build examples. Use the tool already present in the project or icp-cli for new ones. Omit this section if no examples/canisters need building.
- name: Build examples
run: |
# Detect and build examples/example
EXAMPLES_DIR=""
if [ -d "examples" ]; then
EXAMPLES_DIR="examples"
elif [ -d "example" ]; then
EXAMPLES_DIR="example"
fi
if [ -n "$EXAMPLES_DIR" ]; then
cd "$EXAMPLES_DIR"
if [ -f "mops.toml" ]; then
mops install
fi
# Use icp if it exists, otherwise fallback to dfx if dfx.json is present
if [ -f "icp.yaml" ] && command -v icp >/dev/null; then
icp build --all
elif [ -f "dfx.json" ]; then
# Only run dfx build if dfx is already installed in this job
if command -v dfx >/dev/null; then
dfx build
fi
fi
cd ..
fi
If the repo is a Canister (not a package):
If the project is a canister, you should build the canisters and optionally compare .did files.
Install tools (Choose ONE):
# Option A: For new projects or those already using icp
- name: Install icp-cli
run: npm i -g icp-cli
# Option B: For existing projects already using dfx
- name: Install dfx
uses: dfinity/setup-dfx@main
Build and Compare DID:
Comparing .did files often requires didc to check the latest interface.
- name: Build canisters
run: |
if [ -f "icp.yaml" ] && command -v icp >/dev/null; then
icp build
else
dfx build
fi
- name: Get didc
run: |
mkdir -p /home/runner/bin
release=$(curl --silent "https://api.github.com/repos/dfinity/candid/releases/latest" | awk -F\" '/tag_name/ { print $4 }')
curl -fsSL https://github.com/dfinity/candid/releases/download/$release/didc-linux64 > /home/runner/bin/didc
chmod +x /home/runner/bin/didc
echo "/home/runner/bin" >> $GITHUB_PATH
- name: Check Candid interface
run: |
# Compare generated .did with tracked .did
# e.g., didc check -s did/my_canister.did .dfx/local/canisters/my_canister/my_canister.did
didc check -s did/backend.did .dfx/local/canisters/backend/backend.did
Step 4 — Run End-to-End Tests
If the repository contains end-to-end tests (e.g., in test/e2e or similar), add a step to run them. This usually requires dfx and a running local replica or pocket-ic.
- name: Run E2E tests
run: |
# Your E2E test command here
# e.g., npm run test:e2e
Common Pitfalls
Outdated Node version. Prettier and some Motoko tools require recent Node.js versions. Always use
latestor at leastv22in CI.Benchmarks require a
pocket-ictoolchain pin — there is nodfxfallback. mops CLI v3.0.0 removed dfx-replica support entirely (the--replicaflag onmops test/mops benchis also gone — it had been deprecated since 2.14). PocketIC is now the only runtime. If the package has benchmarks, make sure[toolchain] pocket-icis pinned inmops.toml(add it withmops toolchain use pocket-ic <version>if missing — an unpinned project errors out naming the exact fix), then install the pinned binary and run bench (only in the job that runsmops bench). There is no need for a separate "start" step —mops benchstarts (and stops) PocketIC itself:- name: Make sure pocket-ic is installed run: mops toolchain bin pocket-ic - run: mops benchNote:
mops testdoes not usepocket-ic(it runs via the Motoko interpreter or WASI), so the pin only matters formops bench/ replica tests /--check-deploy.Remove any leftover
dfxbench-runtime steps. If you are updating an existing workflow that still installsdfx(dfinity/setup-dfx@main) or runsdfx start --background --cleanpurely to backmops bench, delete those steps — they no longer do anything formops benchand cannot substitute for thepocket-icpin:# REMOVE these if present purely for `mops bench` — dfx-replica # support was removed in mops CLI v3.0.0: - name: Install dfx uses: dfinity/setup-dfx@main - name: Start dfx run: dfx start --background --clean(A
dfxinstall is still legitimate for other jobs — e.g. building canisters/examples or E2E tests, see Step 3/4. This pitfall is specifically about themops benchruntime.)Missing
pocket-icpin. Ifmops bench(ormops toolchain bin pocket-ic) fails immediately with an error about an unpinned toolchain,[toolchain] pocket-icis missing frommops.toml— pin it withmops toolchain use pocket-ic <version>(the error message names a concrete version) rather than trying to work around it withdfx.Benchmarking without
[optimize].mops benchonly runswasm-optifmops.tomlhas an[optimize]section — omit it and CI happily reports numbers for unoptimized Wasm, which don't represent what ships in production. If the package has benchmarks and[optimize]is missing, add it (with a[toolchain] wasm-optpin) rather than treating "CI passes" as "CI measures the right thing" — see Step 1.Not showing versions. Always include a step to show
mopsandmocversions. This helps in debugging CI issues.Not using parallel jobs. Running formatting and tests in the same job is slower. Use separate jobs so GitHub runs them in parallel.
Including
mops testwhen no tests exist. Always check if the package has atest/directory with*.test.mofiles. If not, omit themops teststep to avoid CI failures.Including
mops benchwhen no benchmarks exist. Always check if the package has abench/directory with*.bench.mofiles. If not, omit themops benchstep.Old mops installation. Avoid
npm i -g ic-mopsorZenVoich/setup-mops@v1. Usecaffeinelabs/setup-mops@v1instead.Simultaneous tool installation. Do not install both
dfxandicp-cli. Stick to one.Skipping existing tests. If you are updating an existing CI, ensure it runs all tests and benchmarks found in the repo.
Unnecessary tool installation. Do not install
dfxoricp-clifor library packages that only runmops testwith no benchmarks. If the package has benchmarks,pocket-icIS required (it is the onlymops benchruntime) — install it viamops toolchain bin pocket-icin the bench job only (pluswasm-optif[optimize]is set). Do not installdfxoricp-clijust to runmops bench.
Verify It Works
- Check GitHub Actions tab. Ensure both the
testandfmtjobs are present and running. - Review logs. Verify that
mops installcorrectly pulls dependencies andmops benchruns against PocketIC, the version pinned in[toolchain] pocket-ic(the--replicaflag no longer exists as of mops CLI v3.0.0). Ifmops.tomlhas an[optimize]section, also confirm thewasm-optstep ran — its output should appear before the benchmark results. - Check formatting. Intentionally misformat a file and open a PR to ensure the
fmtjob fails as expected.