Set Up a Token CI/CD Pipeline
Build an end-to-end pipeline from Figma to npm: Tokens Studio syncs to GitHub, Style Dictionary transforms, GitHub Actions publishes.
What you will need
A dedicated repo for tokens. Free tier covers everything in this workflow.
Pro tier required for GitHub sync. The free tier works if you are willing to push JSON manually.
Version 20 or later. Used by Style Dictionary and the validation script in step 5.
Installed inside your tokens repo. Set it up first using the linked workflow below.
For publishing the built tokens as a versioned package consumers can install.
Initial setup. Once running, future changes flow through the pipeline in seconds.
Last year I shipped a token update that broke three products. I changed spacing.md from 16px to 20px in a JSON file, committed it, and walked away. No build step verified the change. No test caught the ripple effect. No review gate asked “are you sure?” The fix took four hours and one very apologetic Slack message.
A CI/CD pipeline for tokens is not about automation for the sake of automation. It is about guardrails. Every token change gets versioned, built, tested, and published through a process that catches mistakes before they reach production. This workflow sets up the full loop: Figma to Tokens Studio to GitHub to Style Dictionary to npm. Once it is running, you will never manually copy a hex value again.
Step 1: Set Up Your Token Repository
Create a dedicated GitHub repository for your design tokens. This repository is the single source of truth, not Figma, not a spreadsheet, not a Slack message.
mkdir design-tokens
cd design-tokens
git init
npm init -y
npm install -D style-dictionary
Create the folder structure:
design-tokens/
tokens/
color/
primitives.json
semantic.json
spacing/
base.json
typography/
base.json
config.json
package.json
.github/
workflows/
build-tokens.yml
Add your token files. Here is a minimal tokens/color/primitives.json:
{
"color": {
"palette": {
"blue": {
"500": { "$value": "#3B82F6", "$type": "color" },
"600": { "$value": "#2563EB", "$type": "color" },
"700": { "$value": "#1D4ED8", "$type": "color" }
},
"gray": {
"100": { "$value": "#F3F4F6", "$type": "color" },
"500": { "$value": "#6B7280", "$type": "color" },
"900": { "$value": "#111827", "$type": "color" }
}
}
}
}
And tokens/color/semantic.json:
{
"color": {
"background": {
"primary": { "$value": "{color.palette.gray.100}" },
"inverse": { "$value": "{color.palette.gray.900}" }
},
"action": {
"primary": { "$value": "{color.palette.blue.500}" },
"primaryHover": { "$value": "{color.palette.blue.600}" }
},
"text": {
"primary": { "$value": "{color.palette.gray.900}" },
"secondary": { "$value": "{color.palette.gray.500}" }
}
}
}
Why this step matters
A dedicated repository means tokens have their own versioning, their own CI pipeline, and their own release cycle. Mixing tokens into your component library repository creates coupling: a token change triggers component builds, and a component change triggers token builds. Separate repositories, separate concerns.
Step 2: Connect Tokens Studio to GitHub
Tokens Studio syncs Figma variables to your GitHub repository. When a designer changes a token in Figma, Tokens Studio pushes the updated JSON to a branch.
- Install the Tokens Studio plugin in Figma (Resources panel, search “Tokens Studio”)
- Open the plugin and go to Settings
- Under “Sync providers,” select GitHub
- Enter your repository details:
- Repository:
your-org/design-tokens - Branch:
main(or a specific branch likefigma-sync) - Token directory:
tokens/ - File type: JSON (multiple files)
- Repository:
- Authenticate with a GitHub Personal Access Token (PAT) that has
reposcope
Test the connection by making a small change in Figma and pushing it through Tokens Studio. You should see a new commit (or pull request) in your GitHub repository.
Recommended: push to a branch, not main. Configure Tokens Studio to push to a figma-updates branch. Then create pull requests from that branch to main. This gives you a review step before tokens go live.
Why this step matters
Without this connection, every token change requires a designer to export JSON, open a code editor, paste the values, and commit. That is four steps where things can go wrong. Tokens Studio reduces it to one click in Figma. The PR-based flow adds a review gate so no change goes live without someone checking it.
Step 3: Add the Style Dictionary Build
Create a Style Dictionary configuration that transforms your tokens into platform-specific outputs. Add config.json to your project root:
{
"source": ["tokens/**/*.json"],
"platforms": {
"css": {
"transformGroup": "css",
"buildPath": "build/css/",
"files": [
{
"destination": "variables.css",
"format": "css/variables"
}
]
},
"js": {
"transformGroup": "js",
"buildPath": "build/js/",
"files": [
{
"destination": "tokens.js",
"format": "javascript/es6"
}
]
},
"json": {
"transformGroup": "js",
"buildPath": "build/json/",
"files": [
{
"destination": "tokens.json",
"format": "json/flat"
}
]
}
}
}
Add build scripts to package.json:
{
"scripts": {
"build": "style-dictionary build",
"build:clean": "rm -rf build && style-dictionary build"
}
}
Test locally:
npm run build
Verify the output files in build/css/variables.css, build/js/tokens.js, and build/json/tokens.json.
Why this step matters
Style Dictionary is the transformation engine. It takes your single-format token JSON and produces every format your consumers need: CSS for web, JS for React/Vue, JSON for documentation tools. Adding a new platform later (iOS Swift, Android XML, Tailwind config) is just a new block in the config file.
Step 4: Create the GitHub Actions Workflow
This is the automation core. Create .github/workflows/build-tokens.yml:
name: Build and Publish Tokens
on:
push:
branches: [main]
paths: ['tokens/**']
pull_request:
branches: [main]
paths: ['tokens/**']
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: npm ci
- name: Build tokens
run: npm run build
- name: Verify build output
run: |
test -f build/css/variables.css || exit 1
test -f build/js/tokens.js || exit 1
test -f build/json/tokens.json || exit 1
echo "All build outputs verified."
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: built-tokens
path: build/
publish:
needs: build
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: npm ci
- name: Build tokens
run: npm run build
- name: Publish to npm
run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
Add the npm token to your repository secrets:
- Go to your GitHub repository settings
- Navigate to Secrets and Variables, then Actions
- Add a new secret:
NPM_TOKENwith your npm access token
Why this step matters
The workflow has two jobs. The build job runs on every push and pull request, verifying that token changes do not break the build. The publish job only runs on pushes to main, meaning only reviewed and merged changes get published. This two-stage approach catches errors in PRs before they reach production.
Step 5: Add Validation and Safety Checks
A build that succeeds is not enough. Add checks that catch design mistakes:
Create a validate-tokens.js script:
const fs = require('fs');
const path = require('path');
const errors = [];
function walkDir(dir) {
const files = fs.readdirSync(dir);
for (const file of files) {
const filePath = path.join(dir, file);
if (fs.statSync(filePath).isDirectory()) {
walkDir(filePath);
} else if (file.endsWith('.json')) {
validateFile(filePath);
}
}
}
function validateFile(filePath) {
const content = JSON.parse(fs.readFileSync(filePath, 'utf8'));
validateTokens(content, filePath, '');
}
function validateTokens(obj, file, prefix) {
for (const [key, value] of Object.entries(obj)) {
const tokenPath = prefix ? `${prefix}.${key}` : key;
if (value.$value !== undefined) {
// Check for raw hex in semantic files
if (file.includes('semantic') && typeof value.$value === 'string'
&& value.$value.match(/^#[0-9A-Fa-f]{6,8}$/)) {
errors.push(`${file}: ${tokenPath} uses a raw hex value. Semantic tokens should reference primitives.`);
}
// Check naming convention (kebab or dot notation)
if (key.match(/[A-Z]/)) {
errors.push(`${file}: ${tokenPath} uses camelCase. Use kebab-case or dot notation.`);
}
} else if (typeof value === 'object') {
validateTokens(value, file, tokenPath);
}
}
}
walkDir('tokens');
if (errors.length > 0) {
console.error('Token validation failed:\n');
errors.forEach(e => console.error(` - ${e}`));
process.exit(1);
} else {
console.log('All tokens passed validation.');
}
Add it to your workflow before the build step:
- name: Validate tokens
run: node validate-tokens.js
And to your package.json:
{
"scripts": {
"validate": "node validate-tokens.js",
"build": "npm run validate && style-dictionary build"
}
}
Why this step matters
Validation catches the mistakes that break design systems: a semantic token pointing to a raw hex instead of a primitive, a camelCase name in a kebab-case system, a missing value in a mode. These are the errors that slip through manual review because a JSON file with 200 entries is impossible to proofread. Automated validation never gets tired and never skips a line.
Step 6: Set Up Versioning and Changelogs
Token changes need version numbers. Your consumers need to know what changed and when.
Add a version bump script. The simplest approach uses npm’s built-in versioning:
{
"scripts": {
"release:patch": "npm version patch && git push && git push --tags",
"release:minor": "npm version minor && git push && git push --tags",
"release:major": "npm version major && git push && git push --tags"
}
}
Use semantic versioning:
- Patch (1.0.1): Value changes (updated a color, adjusted spacing)
- Minor (1.1.0): New tokens added (new component tokens, new category)
- Major (2.0.0): Breaking changes (renamed tokens, removed tokens, restructured hierarchy)
For changelogs, add a CHANGELOG.md and update it with each release:
# Changelog
## 1.1.0 (2026-04-13)
### Added
- color.action.primaryHover token
- spacing.2xl (48px) and spacing.3xl (64px)
### Changed
- color.palette.blue.500: #3B82F6 to #2563EB
## 1.0.0 (2026-03-15)
### Initial release
- 52 color tokens (24 primitive, 28 semantic)
- 18 spacing tokens
- 8 border radius tokens
Why this step matters
Without versioning, a token change is invisible to consumers. They do not know when to update or what changed. With semantic versioning, a patch means “safe to update automatically.” A major version means “read the changelog before updating.” This contract between your design system and its consumers builds trust.
What You Get
After completing this workflow, you have:
- A dedicated token repository with a clean folder structure
- Figma-to-GitHub sync through Tokens Studio (designer changes flow to code automatically)
- Style Dictionary building CSS, JS, and JSON outputs from a single source
- GitHub Actions running validation and builds on every pull request
- Automated npm publishing on merge to main
- Semantic versioning and changelogs for every release
- A pipeline that catches errors before they reach production
Common Pitfalls
- Skipping the PR review step. Pushing directly from Tokens Studio to
mainbypasses all safety checks. Always push to a feature branch and create a PR. The CI pipeline runs on PRs, catching errors before merge. - Not testing the pipeline with a real change. After setup, make a small deliberate change (update one color value), push it through the entire pipeline, and verify the published package. Do not wait for a real change to discover the pipeline is broken.
- Forgetting to update the version. If you merge token changes without bumping the version, the publish step will fail (npm rejects duplicate versions). Make version bumping part of your merge checklist.
Ship one token change through the full pipeline
-
Pick one primitive to change
In your tokens repo, pick one low-risk primitive value (a gray, a spacing step, a radius you rarely use). Open a feature branch called
pipeline-smoke-test. Change the value to something obviously different (e.g.,#F3F4F6to#EEEEEE). Commit the change.- The change is in a feature branch, never directly on main
- Only one token value changed, everything else untouched
- The commit message names the token that changed (e.g.,
chore: bump color.palette.gray.100 for pipeline smoke test)
-
Open a PR, watch CI, merge, verify npm
Push the branch and open a pull request against main. Watch the GitHub Actions run in the Checks tab. Once validation and build pass, bump the patch version, merge. Confirm the new version appears on your npm registry and that the built CSS/JS output reflects the new value.
- CI runs validation, build, and verify steps without errors
- The publish job only runs after the merge to main, not on the PR build
npm view your-tokens-package versionsshows the new patch version within a minute of mergebuild/css/variables.cssin the published package contains the new hex value- If any step failed, you now know which link of the pipeline is broken before it costs you a real release
Finished this lesson?
Mark it complete to track your progress through "Automation for DS Teams".
The guides alone saved me a full day of work every sprint.
- All guides, prompts, and templates
- Starter kits and templates
- New content every week
- Priority support