Gate merges from any CI
Run M1rror's design check from GitHub Actions, GitLab CI or any other pipeline, and fail the build when a commit drifts from your design system.
On this page
The M1rror GitHub App already checks every pull request. If you want the result to gate a pipeline too, for example a GitLab CI or Jenkins job that builds a GitHub repository, your pipeline can start the same check for one commit, wait for it and fail when it finds drift.
It's the same check as pull request checks: the same rules, the same verdicts, and the same result in Code mode › Pull requests.
Before you start
- The repository is on GitHub and connected to the project through the M1rror GitHub App. M1rror reads the changed files from GitHub. A repository hosted only on GitLab or Bitbucket can't be checked yet.
- Agent tokens need the Studio plan or above.
- You need the project id. It's in the address bar of the project:
/app/<workspace id>/p/<project id>. The CLI reads the workspace from the token. A token limited to one project needs no project id either.
Create a token for CI
Create an agent token
In Settings › Agent access, create a token. Keep Tools › Implementer, and keep Prepare implementation plans selected: starting a check needs it. Limit the token to the project if you like.
Store it as a secret
Save the token as a masked CI secret named
M1RROR_TOKEN. Tokens last at most 90 days, so note when to replace it.
What the check compares
The check reads the lines the commit adds compared with a base: a commit SHA or a branch. The CLI picks both from your CI:
| CI | Commit | Base |
|---|---|---|
| GitHub Actions, pull request | The pull request head (not GitHub's merge commit) | The pull request base, plus the pull request number |
| GitHub Actions, push | The pushed commit | The commit before the push |
| GitLab CI, merge request | CI_COMMIT_SHA | CI_MERGE_REQUEST_DIFF_BASE_SHA |
| GitLab CI, branch pipeline | CI_COMMIT_SHA | CI_COMMIT_BEFORE_SHA |
| Anything else | --ref, or git rev-parse HEAD | --base, or the repository's branch in M1rror |
With a pull request number, M1rror reuses the check the GitHub App already made for that commit, so nothing is checked twice.
The m1rror CLI
m1rror check --project <project id> --fail-on error| Option | Meaning |
|---|---|
--project | The project id, or M1RROR_PROJECT. Optional when the token is limited to one project. |
--workspace | The workspace id, or M1RROR_WORKSPACE. Optional: by default the CLI asks M1rror which workspace the token belongs to (GET /v1/token). |
--repo <id> | Needed only when the project has more than one GitHub repository. |
--ref <sha>, --base <sha or branch>, --pr <number> | Override what the CLI reads from your CI. |
--fail-on error|warning|none | error (default) fails on a Failing verdict. warning also fails on Needs attention. none only reports. |
--junit <file>, --sarif <file> | Write a JUnit XML or SARIF 2.1.0 report. |
--timeout <seconds> | How long to wait. Default 600. |
--allow-not-run | Pass when the check couldn't run. |
--api-url <url> | For a self-hosted M1rror. |
The CLI reads the token from M1RROR_TOKEN and never prints it. It prints the verdict, the findings and a link to the result in M1rror. In GitHub Actions, findings also appear as notes on the changed lines.
| Exit code | Meaning |
|---|---|
0 | Pass, or findings below --fail-on. |
1 | Findings at or above --fail-on. |
2 | A missing option, a wrong id or an invalid token. |
3 | The check couldn't run or finish, for example usage ran out or it timed out. This never counts as a pass unless you add --allow-not-run. |
GitHub Actions
name: Design check
on:
pull_request:
push:
branches: [main]
jobs:
m1rror:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npx --yes @m1rror/cli check --fail-on error --junit m1rror-junit.xml
env:
M1RROR_TOKEN: ${{ secrets.M1RROR_TOKEN }}
M1RROR_PROJECT: <project id>To require the result before merging, add this job as a required status check in your branch protection rules. The GitHub App's own M1rror design check run can stay as it is: both show the same result.
GitLab CI
For a GitHub repository built on GitLab CI, for example a mirror.
m1rror-design-check:
image: node:22
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
variables:
M1RROR_PROJECT: <project id>
script:
- npx --yes @m1rror/cli check --fail-on error --junit m1rror-junit.xml
artifacts:
when: always
reports:
junit: m1rror-junit.xmlAdd M1RROR_TOKEN under Settings › CI/CD › Variables, masked and protected.
Any other CI
Run the CLI with the commit and base:
npx --yes @m1rror/cli check --ref "$COMMIT_SHA" --base "$BASE_SHA" --fail-on errorOr call the REST API directly. This works today. It starts the check, then asks for the result until it's done:
set -eu
AUTH="Authorization: Bearer $M1RROR_TOKEN"
# The token's workspace (no secrets in the answer).
M1RROR_WORKSPACE=$(curl -fsS https://m1rror.com/v1/token -H "$AUTH" | jq -r .workspace.id)
API="https://m1rror.com/v1/workspaces/$M1RROR_WORKSPACE/projects/$M1RROR_PROJECT"
ID=$(curl -fsS -X POST "$API/ci-checks" -H "$AUTH" -H 'Content-Type: application/json' \
-d "{\"head_sha\":\"$COMMIT_SHA\",\"base_sha\":\"$BASE_SHA\"}" | jq -r .id)
while :; do
CHECK=$(curl -fsS "$API/pr-checks/$ID" -H "$AUTH")
STATUS=$(echo "$CHECK" | jq -r .status)
[ "$STATUS" = queued ] || [ "$STATUS" = running ] || break
sleep 10
done
echo "$CHECK" | jq -r '"\(.status) \(.verdict // "") \(.link)"'
[ "$STATUS" = completed ] && [ "$(echo "$CHECK" | jq -r .verdict)" != failing ]| Request | What it does |
|---|---|
POST …/projects/<project id>/ci-checks | Starts the check. Body: head_sha (required), base_sha or base_ref, pr_number, head_ref, repository_id. Returns the check with its id, status and link, and reused: true when this commit and base were already checked. |
GET …/projects/<project id>/pr-checks/<id> | The check: status (queued, running, completed, not_run, superseded, cancelled), verdict (pass, needs_attention, failing), counts, findings with file and line, and link. |
Agents connected over MCP can do the same with taste_start_ci_check and taste_get_pr_check.
Cost and limits
A check uses 1 usage unit when UI files changed and nothing when none did. Running it again for the same commit and base returns the earlier result and costs nothing. Checks started from CI run even when automatic pull request checks are off in your settings.
If usage has run out, or M1rror has paused checks, the check says it couldn't run, and the CLI exits with code 3. No result is made up.
Last updated
Edit this page on GitHub