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.

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.

  1. Install the Tokens Studio plugin in Figma (Resources panel, search “Tokens Studio”)
  2. Open the plugin and go to Settings
  3. Under “Sync providers,” select GitHub
  4. Enter your repository details:
    • Repository: your-org/design-tokens
    • Branch: main (or a specific branch like figma-sync)
    • Token directory: tokens/
    • File type: JSON (multiple files)
  5. Authenticate with a GitHub Personal Access Token (PAT) that has repo scope

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:

  1. Go to your GitHub repository settings
  2. Navigate to Secrets and Variables, then Actions
  3. Add a new secret: NPM_TOKEN with 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 main bypasses 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.
Exercise

Ship one token change through the full pipeline

20 min
  1. 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., #F3F4F6 to #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)
  2. 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 versions shows the new patch version within a minute of merge
    • build/css/variables.css in 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".

Lesson
7 / 13
Progress
54%
Read time
90 min
Free to try Cancel anytime
The guides alone saved me a full day of work every sprint.
Senior Design Systems Lead
Enterprise SaaS
Pro
Full access to everything.
$39 /month
  • All guides, prompts, and templates
  • Starter kits and templates
  • New content every week
  • Priority support