Release design tokens
Pipeline: export from Figma → build CSS → review → version → commit → tag → hand off.
Normally CI does all of this — see 5b. The automated path below, and prefer it. The manual steps remain for when CI is unavailable.
Run every command from the repo root (design-core/). Do not skip the review step — once
the user publishes it cannot be undone (unpublish is only allowed within 72h and burns the
version number for good).
0. Preflight
git branch --show-current # expect main
but status # working tree should be clean before exporting
npm whoami # must print a user with @gitbutler publish rights
If the tree is dirty, stop and ask whether to release those changes too or stash them.
If npm whoami errors, tell the user to run npm login — do not attempt to authenticate.
Being logged in is not enough to publish: 2FA is auth-and-writes, so step 6 needs a
one-time code from the user. Say this up front, at the start of the run — not after the
version is already bumped and tagged.
1. Export tokens from Figma
Two routes into the same pipeline — pick whichever fits the current setup.
Both produce the same three files in tokens/json/: core.tokens.json,
semantic.tokens.json, fx.tokens.json. Continue from step 2 regardless of
which option was used.
Option A: tokens-bruecke CLI (needs Enterprise plan + PAT)
Credentials live in .env (gitignored): FIGMA_API_KEY, FIGMA_FILE_KEY.
The exporter is the tokens-bruecke CLI at /Users/pavellaptev/Documents/GitHub/figma-plugin/bin/cli.js
(same tool as the Figma plugin; npx tokens-bruecke also works).
Verify the token first — PATs expire every 90 days:
set -a && . ./.env && set +a
curl -s -H "X-Figma-Token: $FIGMA_API_KEY" https://api.figma.com/v1/me
Then export straight into tokens/json/:
set -a && . ./.env && set +a
node /Users/pavellaptev/Documents/GitHub/figma-plugin/bin/cli.js \
-a "$FIGMA_API_KEY" \
-f "$FIGMA_FILE_KEY" \
-c .claude/skills/release-tokens/figma-export.config.json \
-o tokens/json \
--split-by-collection
figma-export.config.json in this skill folder reproduces the settings the committed
tokens were generated with — DTCG 2025.10, hex colors, no scopes, no Figma metadata,
effect styles exported as the fx collection. Do not change it casually: a config change
rewrites every token file and produces a huge, unreviewable diff.
Failure modes (both surface as HTTP 403):
| Message | Meaning | Fix |
|---|---|---|
Token expired |
PAT past its 90-day life | User regenerates the PAT in Figma → Settings → Security |
Invalid scope(s): … requires the file_variables:read scope |
PAT missing the variables scope | Regenerate with file_variables:read checked — only offered on Enterprise plans |
Both need the user. Stop and ask; never work around by hand-editing token JSON.
Option B: Figma agent's /export-tokens (any plan, no PAT)
If there is no Enterprise plan, the PAT has expired, or you are running inside the Figma agent, variables can be extracted through the Plugin API instead — no personal access token and no REST API required.
- Open the design tokens file in Figma.
- Run
/export-tokensin the Figma agent chat. - On first run the skill asks about color format, style inclusion, and output
layout. To match the committed tokens, choose: hex, everything
(text, colors, effects, grids), one file per collection. These map to
the same settings as
figma-export.config.json. - The skill extracts a Plugin API snapshot and pipes it through
tokens-bruecke --inputin snapshot mode (requires tokens-bruecke ≥ 3.6.0). - Copy the exported
core.tokens.json,semantic.tokens.json, andfx.tokens.jsonintotokens/json/in this repo.
The snapshot path bypasses the Variables REST API entirely, so it works on any Figma plan. The output is byte-for-byte compatible with Option A — same DTCG shape, same collection split.
Continue from step 2.
2. Detect whether anything actually changed
Every export rewrites the createdAt stamp in each file's
$extensions["tokens-bruecke-meta"], so a no-op export still shows a diff.
git diff --stat tokens/json
git diff -U0 tokens/json | grep '^[+-]' | grep -v '^[+-][+-]' | grep -v createdAt
If that last command prints nothing, only the timestamps moved — there is nothing to
release. Run git checkout -- tokens/json, tell the user the Figma file has no changes,
and stop. Do not bump or publish a version whose only diff is a timestamp.
3. Build the CSS
npm run build
This runs scripts/postprocess-light-dark.mjs, which:
- spawns
npx tz build— Terrazzo readscore.tokens.json+semantic.tokens.json(perterrazzo.config.js) and writestokens/tokens.css; - merges the
:root/:root.darkblocks intolight-dark(…)values; - appends shadow vars generated from
fx.tokens.jsonviascripts/generate-shadow-vars.mjs.
tokens/tokens.css is generated — never edit it by hand. If a token changed in JSON but
not in the CSS, the token is probably in a collection Terrazzo does not read; say so
rather than patching the CSS.
Review the result:
git diff tokens/tokens.css
Sanity-check that the CSS diff matches the JSON diff. Nothing else in the repo should change.
4. Choose the version bump
Read the diff and pick the level — package.json currently drives npm consumers of
--var(...) names, so removed or renamed CSS custom properties are breaking:
| Change | Bump |
|---|---|
| Token values tweaked (colors, sizes, opacity) | patch |
| New tokens added; new collection | minor |
| Tokens removed or renamed; CSS var names changed | major |
State the chosen level and the reason, list the affected tokens, and confirm with the user before publishing. If unsure between two levels, choose the higher one and say so.
5. Commit, version, tag
House style (see git log): the version bump lives in the same commit as the token
changes, tagged with a bare X.Y.Z annotated tag — no v prefix.
npm version <patch|minor|major> --no-git-tag-version # edits package.json only
Commit with GitButler (this repo uses the but CLI — use the gitbutler skill for the
commit itself, not raw git commit). Message: a short summary of what changed in the
tokens, e.g. Adjust dark-mode change status colors. Include package.json,
tokens/json/*.tokens.json, and tokens/tokens.css.
Then tag the commit:
VERSION=$(node -p "require('./package.json').version")
git tag -a "$VERSION" -m "$VERSION"
5b. The automated path (preferred)
CI can do steps 3–7 on its own. Instead of bumping and publishing locally, push the token JSON on a branch and open a PR:
.github/workflows/tokens-pr.ymlruns on PRs touchingtokens/json/**. It rebuildstokens/tokens.css, commits it back onto the PR branch, bumps the version from the CSS custom-property diff, and comments the added/changed/removed tokens. A bump that would be major fails the check until the PR carries therelease:majorlabel — removed or renamed vars break every consumer. If onlycreatedAtmoved, it says so and bumps nothing.- Merging the PR triggers
.github/workflows/release.yml, which publishes to npm via trusted publishing (OIDC — no token, no OTP), pushes the bareX.Y.Ztag, and cuts a GitHub release.
So the automated release is: export from Figma (step 1) → PR → review the bot's comment → merge. Use the manual steps below only when CI is unavailable or the release must go out without a PR.
6. Stop here — hand the publish to the user
The agent never publishes. The npm account has 2FA set to auth-and-writes, so
every publish needs a one-time code. Everything up to this point is prepared and
committed; the release itself is the user's single manual action.
Run the dry run first — it needs no OTP and confirms the package contents:
npm publish --dry-run
Check the file list covers tokens/, fonts/, styles/, core.css, README.md, and
that the version in the output is the one just bumped. Then present this block to the
user, with the real version substituted in — no placeholders:
cd /Users/pavellaptev/Documents/GitHub/gitbutler/design-core
npm publish # prompts for your 2FA code
git push origin main
git push origin 3.12.5 # <- the tag you just created
Run in their own terminal, npm publish prompts for the OTP interactively — no --otp
flag needed.
Offer to run the two git push commands yourself once they confirm the publish
succeeded; those need no OTP. Do not push before the publish lands — if the version turns
out to be wrong, an unpushed commit and tag are trivial to amend, a pushed one is not.
Never attempt to work around the 2FA prompt: no --force, no editing ~/.npmrc, no
improvised automation tokens.
7. After the publish
Once the user confirms it went through:
git push origin main
git push origin "$(node -p "require('./package.json').version")"
npm view @gitbutler/design-core version # should match
Pushing tokens/tokens.css triggers .github/workflows/deploy-hue-dini.yml, which
redeploys the hue-dini palette site to GitHub Pages (it is styled with these tokens).
Report back
When you stop at step 6, tell the user in one block: the new version and the bump level with its reason, a short list of tokens added/changed/removed, that the commit and tag exist locally but are not pushed, and the copy-paste publish command. After they confirm and you push, report the pushed tag and the live npm version. If you stopped early (expired token, no changes, user declined the bump, waiting on an OTP), say exactly where and what is needed to resume.