CI and exit codes
The usual place for a sync is CI: every merge into the branch your team syncs from uploads the library’s current tokens and components. This page covers the workflow, the exit codes a step can branch on, and what a run reports.
The workflow
The package ships the workflow as a template (templates/grails-cli-sync.yml, inside grails-cli), and Grails shows it
under Team Settings › Integrations. It is the one to copy. Its essentials:
on: push: branches: [main] # the same branch grails.code.json declares
jobs: sync: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22 - name: Sync the design tokens and the components to Grails id: grails env: GRAILS_API_KEY: ${{ secrets.GRAILS_API_KEY }} run: npx --yes grails-cli sync --trigger ciTo set it up:
- In Grails, create an API key with the
write:codescope: Team Settings › Integrations › API Keys. - Save it as a repository secret named
GRAILS_API_KEY: Settings › Secrets and variables › Actions. - Make sure
grails.code.jsondeclares the branch, for example"branch": "main", and put the same branch underbranches:in the workflow. - Commit the file as
.github/workflows/grails-cli-sync.yml.
The step runs npx --yes grails-cli, which fetches the latest release from npm on every run, so there is nothing to install
in advance (see Overview). To choose when a new release reaches your pushes, pin a version:
npx --yes grails-cli@<version> sync --trigger ci.
In CI the key comes from GRAILS_API_KEY and the keychain is never asked. A run also ends with one line on stderr while
the installed grails-cli is behind the latest release, which is easy to miss when a workflow pins a version. Set
GRAILS_CLI_NO_UPDATE_CHECK=1 in the step’s env: to turn it off.
Three rules the template follows, and you should keep:
- The key stays in the step’s
env:. It is never written into arun:line, never echoed, and never passed as a flag. - Do not run it on
pull_request. A fork’s run has no secrets, and a pull request should not replace what the team sees. - Install the project’s dependencies before the sync if the library uses Tailwind. The CLI reads the theme with
the project’s own
tailwindcss. Without the dependencies the run exits 2. The template has the line commented out.
What a run does
A real sync in CI does the same as on a laptop: it uploads the tokens, then the components, and then compares the
tokens with Figma. The template passes --trigger ci.
From a branch the grails.code.json does not declare, the sync uploads nothing and exits 0, so the job stays green.
The run says so in four places: the terminal, a notice on the run, the job summary, and the step output result.
Without a key a real sync stops with exit code 2 before it uploads anything:
Step outputs
The step has id: grails, so a later step can read what happened:
| Output | Values |
|---|---|
steps.grails.outputs.result | synced, preview (a dry run with a key), offline (a dry run without one), skipped-branch, or failed. |
steps.grails.outputs.exit_code | The exit code of the run, as the table below. |
- name: Fail the job only when a retry could help if: steps.grails.outputs.exit_code == '4' run: echo "Grails could not be reached; the job can be run again."The job summary
The CLI adds a report to the job summary of the run (GITHUB_STEP_SUMMARY). It shows:
- the counts of tokens and components, and the source and commit;
- the tokens that changed, and the components that would be deleted, folded so a long list does not fill the page;
- the code ↔ Figma table, what differs, the conflicts, and the naming rules applied and suggested (see Code ↔ Figma).
There is no comment on the pull request. The summary lives on the run.
Exit codes
Each code is a contract a step can branch on. Only 4 is worth retrying.
| Code | Meaning | Retry? |
|---|---|---|
| 0 | Done: synced, nothing changed, a dry run, or a sync on a branch the configuration does not declare (its own result, skipped-branch, is in the step output). | – |
| 1 | The component sync ran and ended incomplete: components could not be read, or deletions were held back. Read the warnings. | No |
| 2 | Misconfigured: a flag, grails.code.json (including no declared branch on a real sync), no key, or a project Grails cannot read (its Tailwind is missing, or is a major version other than 3 or 4). For connect: a key of the wrong shape, an empty or unreadable clipboard, or no keychain. | No: fix it |
| 3 | Refused by the server: the key (also the one connect was given), the plan, content it does not accept, or a CLI older than the minimum. | No: fix it |
| 4 | The server could not be reached or failed: a network error, a 5xx, or a rate limit after one retry. For update, the npm registry. | Yes |
Two cases that stop a run on purpose:
- Deletions are held back. A sync that did not read the whole library never marks a component as deleted. The server also refuses to delete five or more components, or more than 20 % of the library, in one sync. Both show up as exit 1, with the reason in the warnings.
- A sync left open. If a sync fails after it was opened, the CLI closes it as incomplete before it exits. If it could not, the source stays locked for two hours, and the next run says another sync is running.