CircleCI Config SDK Tutorial
You can now write your CircleCI® config files with TypeScript.
- Less code
- Reusable configs
- 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:
Type Hinting
This is much better than .yml
.
What are the possible commands?
Easy:
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.
Reader interactions
One Reply to “CircleCI Config SDK Tutorial”
Comments are closed.
This Config SDK is a big step for CircleCI. It’s easier to write and reuse configs.