CircleCI Config SDK Tutorial

You can now write your CircleCI® config files with TypeScript.

  • Less code
  • Reusable configs
  • Type hinting:
CircleCI Config SDK type hinting

You’ll see a real example in this CircleCI config SDK tutorial.

Config File

We’ll start with a .circleci/config.yml file.

This will run the TypeScript file we create.

.circleci/config.yml

version: 2.1
orbs:
  continuation: circleci/continuation@0.3
  node: circleci/node@5.0
setup: true
jobs:
  generate-config:
    executor: node/default
    steps:
      - checkout
      - node/install-packages:
          app-dir: .circleci/dynamic
      - run:
          name: Generate config
          command: npm start
          working_directory: .circleci/dynamic
      - continuation/continue:
          configuration_path: .circleci/dynamic/dynamicConfig.yml
workflows:
  dynamic-workflow:
    jobs:
      - generate-config
Code language: YAML (yaml)

This is mainly copied from Kyle Tryon’s great post on the Config SDK.

It’ll run the index.ts file, and read the .yml file that creates.

Feel free to copy this file, you shouldn’t need any custom code.

Next, create a directory .circleci/dynamic/

This is where your code for the Config SDK will go.

We’ll start with a package.json file for the dependencies:

.circleci/dynamic/package.json

{
  "name": "circleci-tutorial-config-sdk-dynamic-config",
  "version": "0.1.0",
  "description": "Config SDK example",
  "author": "Ryan Kienstra",
  "main": "index.ts",
  "type": "module",
  "scripts": {
    "lint": "prettier --check index.ts",
    "lint:fix": "prettier --write index.ts",
    "start": "ts-node --esm index.ts"
  },
  "license": "GPL-2.0-or-later",
  "devDependencies": {
    "@circleci/circleci-config-sdk": "^0.10.1",
    "@types/node": "^18.7.20",
    "prettier": "^2.7.1",
    "ts-node": "^10.9.1",
    "tslib": "^2.4.0",
    "typescript": "^4.8.3"
  }
}
Code language: JSON / JSON with Comments (json)

The ts-node dependency lets you write your index file in TypeScript, but you can also run it with node to keep package.json simpler.

We’ll also add a tsconfig.json file:

.circleci/dynamic/tsconfig.json

{
  "compilerOptions": {
    "module": "ESNext",
    "target": "ESNext",
    "moduleResolution": "Node",
    "esModuleInterop": true,
    "noEmit": true
  }
}
Code language: JSON / JSON with Comments (json)

You won’t need that if you prefer JavaScript.

Main SDK File

.circleci/dynamic/index.ts

import * as fs from "fs";
import CircleCI from "@circleci/circleci-config-sdk";

const config = new CircleCI.Config();
const workflow = new CircleCI.Workflow("test-lint");
config.addWorkflow(workflow);
Code language: TypeScript (typescript)

This imports CircleCI from the SDK package.

For each job we create, we’ll add it to the config and workflow.

First Job

This shows the power of the TypeScript SDK:

import * as fs from "fs";
import CircleCI from "@circleci/circleci-config-sdk";

const config = new CircleCI.Config();
const workflow = new CircleCI.Workflow("test-lint");
config.addWorkflow(workflow);

function createPhpTestJobs(...phpVersions: string[]) {
  return phpVersions.map((phpVersion) => {
    return new CircleCI.Job(
      `php-test-${phpVersion.replace(".", "-")}`,
      new CircleCI.executors.DockerExecutor(`cimg/php:${phpVersion}`),
      [
        new CircleCI.commands.Checkout(),
        new CircleCI.commands.Run({ command: "composer i && composer test" }),
      ]
    );
  });
}
Code language: TypeScript (typescript)

On line 8, we define the function createPhpTestJobs(), which will create 4 jobs.

Before, this used a matrix in .yml, and passed parameters.

But it’s easier to see what this does.

It simply returns a new CircleCI.Job for each php version passed.

No need to define parameters for the job, or read the docs about how to create a matrix.

Job Array

On line 21, this creates an array for the jobs.

At the end of this array, later in this post, we’ll call .forEach() to add each job to the config and workflow.

import * as fs from "fs";
import CircleCI from "@circleci/circleci-config-sdk";

const config = new CircleCI.Config();
const workflow = new CircleCI.Workflow("test-lint");
config.addWorkflow(workflow);

function createPhpTestJobs(...phpVersions: string[]) {
  return phpVersions.map((phpVersion) => {
    return new CircleCI.Job(
      `php-test-${phpVersion.replace(".", "-")}`,
      new CircleCI.executors.DockerExecutor(`cimg/php:${phpVersion}`),
      [
        new CircleCI.commands.Checkout(),
        new CircleCI.commands.Run({ command: "composer i && composer test" }),
      ]
    );
  });
}

[
  ...createPhpTestJobs("7.3", "7.4", "8.0", "8.1"),
  new CircleCI.Job(
    "php-lint",
    new CircleCI.executors.DockerExecutor("cimg/php:8.1"),
    [
      new CircleCI.commands.Checkout(),
      new CircleCI.commands.Run({ command: "composer i && composer lint" }),
    ]
  ),
Code language: TypeScript (typescript)

On line 22, we spread the php-test jobs from the function above into the jobs array.

Then, on line 23, we add a new job.

Like before, the CircleCI.Job constructor accepts the job name, the executor, and the steps.

The first command in each is Checkout().

On line 28, the constructor for CircleCI.commands.Run doesn’t need a name value.

Last 2 Jobs

On line 31, this adds more jobs:

import * as fs from "fs";
import CircleCI from "@circleci/circleci-config-sdk";

const config = new CircleCI.Config();
const workflow = new CircleCI.Workflow("test-lint");
config.addWorkflow(workflow);

function createPhpTestJobs(...phpVersions: string[]) {
  return phpVersions.map((phpVersion) => {
    return new CircleCI.Job(
      `php-test-${phpVersion.replace(".", "-")}`,
      new CircleCI.executors.DockerExecutor(`cimg/php:${phpVersion}`),
      [
        new CircleCI.commands.Checkout(),
        new CircleCI.commands.Run({ command: "composer i && composer test" }),
      ]
    );
  });
}

[
  ...createPhpTestJobs("7.3", "7.4", "8.0", "8.1"),
  new CircleCI.Job(
    "php-lint",
    new CircleCI.executors.DockerExecutor("cimg/php:8.1"),
    [
      new CircleCI.commands.Checkout(),
      new CircleCI.commands.Run({ command: "composer i && composer lint" }),
    ]
  ),
  new CircleCI.Job(
    "js-build",
    new CircleCI.executors.DockerExecutor("cimg/node:14.18"),
    [
      new CircleCI.commands.Checkout(),
      new CircleCI.commands.Run({ command: "npm ci" }),
      new CircleCI.commands.Run({
        name: "Running JS linting and unit test",
        command: "npm run lint:js && npm run test:js",
      }),
    ]
  ),
  new CircleCI.Job(
    "e2e-test",
    new CircleCI.executors.MachineExecutor("large", "ubuntu-2004:202111-02"),
    [
      new CircleCI.commands.Checkout(),
      new CircleCI.commands.Run({ command: "npm ci" }),
      new CircleCI.commands.Run({
        name: "Running e2e tests",
        command: "npm run wp-env start && npm run test:e2e",
      }),
      new CircleCI.commands.StoreArtifacts({ path: "artifacts" }),
    ]
  ),
].forEach((job) => {
  config.addJob(job);
  workflow.addJob(job);
});

fs.writeFile("./dynamicConfig.yml", config.stringify(), () => {});
Code language: TypeScript (typescript)

And on line 53, this stores test errors in artifacts, so we can debug.

This calls forEach() on line 56, adding each job to the config and workflow.

Generated .yml File

On line 61, this writes the config to .circleci/dynamic/dynamicConfig.yml.

You don’t have to commit this, the index.ts file generates it on every pipeline run:

.circleci/dynamic/dynamicConfig.yml

# This configuration has been automatically generated by the CircleCI Config SDK.
# For more information, see https://github.com/CircleCI-Public/circleci-config-sdk-ts
# SDK Version: 0.0.0-development

version: 2.1
setup: false
jobs:
  php-test-7-3:
    docker:
      - image: cimg/php:7.3
    resource_class: medium
    steps:
      - checkout
      - run:
          command: composer i && composer test
  php-test-7-4:
    docker:
      - image: cimg/php:7.4
    resource_class: medium
    steps:
      - checkout
      - run:
          command: composer i && composer test
  php-test-8-0:
    docker:
      - image: cimg/php:8.0
    resource_class: medium
    steps:
      - checkout
      - run:
          command: composer i && composer test
  php-test-8-1:
    docker:
      - image: cimg/php:8.1
    resource_class: medium
    steps:
      - checkout
      - run:
          command: composer i && composer test
  php-lint:
    docker:
      - image: cimg/php:8.1
    resource_class: medium
    steps:
      - checkout
      - run:
          command: composer i && composer lint
  js-build:
    docker:
      - image: cimg/node:14.18
    resource_class: medium
    steps:
      - checkout
      - run:
          command: npm ci
      - run:
          name: Running JS linting and unit test
          command: npm run lint:js && npm run test:js
  e2e-test:
    machine:
      image: ubuntu-2004:202111-02
    resource_class: large
    steps:
      - checkout
      - run:
          command: npm ci
      - run:
          name: Running e2e tests
          command: npm run wp-env start && npm run test:e2e
      - store_artifacts:
          path: artifacts
workflows:
  test-lint:
    jobs:
      - php-test-7-3
      - php-test-7-4
      - php-test-8-0
      - php-test-8-1
      - php-lint
      - js-build
      - e2e-test
Code language: YAML (yaml)

This .yml file is 81 lines, where index.ts is 61.

Settings

Finally, go to your repo’s CircleCI Advanced Settings:

https://app.circleci.com/settings/project/github/<GitHub org>/<GitHub repo>/advanced

For this tutorial, the URL is:

https://app.circleci.com/settings/project/github/getlocalci/circleci-tutorial-config-sdk/advanced

Then, select ‘Enable dynamic config using setup workflows’:

This enables the index.ts file above to generate the config.

Here are the jobs it generates:

CircleCI workflow from generated jobs

Type Hinting

This is much better than .yml.

What are the possible commands?

Easy:

CircleCI command type hinting

You don’t have to leave your IDE to look at docs.

Reusable Configs

The normal way of reusing CircleCI configs is writing a custom orb.

But that can be overkill if you have a simple setup you want to share between projects.

So you could write a common config with the TypeScript SDK, and publish it as a package.

And you could import that into any project.

CircleCI Config SDK Tutorial

Here’s the GitHub repo with this tutorial’s code.

This TypeScript config is easier to write and reusable.

And you won’t have to keep checking the documentation.

You can create configs entirely in your IDE.

Be the first to get CI/CD tips like this

You'll get these tips before they're on the blog page. See most up-to-date ways to use CircleCI®. No spam.

Reader interactions

One Reply to “CircleCI Config SDK Tutorial”

  1. This Config SDK is a big step for CircleCI. It’s easier to write and reuse configs.

Comments are closed.