Skip to content

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

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

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

CICommitBase
GitHub Actions, pull requestThe pull request head (not GitHub's merge commit)The pull request base, plus the pull request number
GitHub Actions, pushThe pushed commitThe commit before the push
GitLab CI, merge requestCI_COMMIT_SHACI_MERGE_REQUEST_DIFF_BASE_SHA
GitLab CI, branch pipelineCI_COMMIT_SHACI_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

Terminal
m1rror check --project <project id> --fail-on error
OptionMeaning
--projectThe project id, or M1RROR_PROJECT. Optional when the token is limited to one project.
--workspaceThe 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|noneerror (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-runPass 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 codeMeaning
0Pass, or findings below --fail-on.
1Findings at or above --fail-on.
2A missing option, a wrong id or an invalid token.
3The 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

.github/workflows/m1rror.yml
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.

.gitlab-ci.yml
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.xml

Add M1RROR_TOKEN under Settings › CI/CD › Variables, masked and protected.

Any other CI

Run the CLI with the commit and base:

Terminal
npx --yes @m1rror/cli check --ref "$COMMIT_SHA" --base "$BASE_SHA" --fail-on error

Or call the REST API directly. This works today. It starts the check, then asks for the result until it's done:

m1rror-check.sh
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 ]
RequestWhat it does
POST …/projects/<project id>/ci-checksStarts 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