TypeScript CircleCI Configs: Only 3 Lines
You can now create TypeScript CircleCI configs with 3 lines of code:
import { createConfig, JobNames } from "@getlocalci/create-config";
createConfig(JobNames.JsTest, JobNames.JsLint, JobNames.Vsix);
Code language: TypeScript (typescript)
No more copy-pasting .yml
between repos.
Most of the time, you’ll only need to edit one repo.
For example, let’s say you want to bump the resource class to "large"
on all of your repos:
You could make the edit in 1 repo, and it’ll apply to all of them.
Same if you want to update a deprecated image.
How?
With the new TypeScript Config SDK, thanks to Kyle Tryon and Jaryt Bustard.
You can keep your common config in a single repo, like @getlocalci/create-config.
Like how Kyle Tryon shows in his great post.
And you can import
that config into other repos with only:
import { createConfig, JobNames } from "@getlocalci/create-config";
createConfig(JobNames.JsTest, JobNames.JsLint, JobNames.Vsix);
Code language: TypeScript (typescript)
You don’t even have to publish the config repo as an npm
package, or compile the TypeScript.
You can import it with:
$ npm install https://github.com/getlocalci/create-config/ -D
Code language: Bash (bash)
…and npm
will install it from the default branch on GithHub.
There are 2 parts:
- Your common config repo
- Consuming repos that import that repo and call
createConfig()
. You might have dozens.
Here’s a real example of both:
1. Common Config Repo
This will have the jobs you usually use.
And it’ll export createConfig()
so consuming repos can run these jobs.
This is just an example.
Your repo might have completely different jobs.
Create a new repo, and add an index.ts
in the root.
We’ll start with some imports, and an object JobNames
:
import CircleCI from "@circleci/circleci-config-sdk";
import fs from "fs";
import glob from "glob";
import path from "path";
/**
* Job names you can pass to createConfig().
* These already have jobs created for them.
*/
export const JobNames = {
E2eTest: "e2e-test",
JsLint: "js-lint",
JsTest: "js-test",
PhpLint: "php-lint",
PhpTest: "php-test",
Vsix: "vsix",
Zip: "zip",
};
Code language: TypeScript (typescript)
Consuming repos will import JobNames
, so they know which jobs they can add.
On line 25, we’ll add preCreatedJobs
that any config can use:
import CircleCI from "@circleci/circleci-config-sdk";
import fs from "fs";
import glob from "glob";
import path from "path";
/**
* Job names you can pass to createConfig().
* These already have jobs created for them.
*/
export const JobNames = {
E2eTest: "e2e-test",
JsLint: "js-lint",
JsTest: "js-test",
PhpLint: "php-lint",
PhpTest: "php-test",
Vsix: "vsix",
Zip: "zip",
};
const nodeExecutor = new CircleCI.executors.DockerExecutor(
"cimg/node:lts",
"large"
);
const preCreatedJobs = [
new CircleCI.Job(JobNames.JsLint, nodeExecutor, [
new CircleCI.commands.Checkout(),
new CircleCI.commands.Run({
command: "npm ci && npm run lint",
}),
]),
new CircleCI.Job(JobNames.JsTest, nodeExecutor, [
new CircleCI.commands.Checkout(),
new CircleCI.commands.Run({ command: "npm ci && npm test" }),
]),
Code language: TypeScript (typescript)
Each value in preCreatedJobs
will be a CircleCI.Job
.
The constructor of CircleCI.Job
accepts:
- Job name
- Executor
- Steps
You’ll get type-hinting for each of these:
On line 103, we’ll add functions to create the config:
import CircleCI from "@circleci/circleci-config-sdk";
import fs from "fs";
import glob from "glob";
import path from "path";
/**
* Job names you can pass to createConfig().
* These already have jobs created for them.
*/
export const JobNames = {
E2eTest: "e2e-test",
JsLint: "js-lint",
JsTest: "js-test",
PhpLint: "php-lint",
PhpTest: "php-test",
Vsix: "vsix",
Zip: "zip",
};
const nodeExecutor = new CircleCI.executors.DockerExecutor(
"cimg/node:lts",
"large"
);
const preCreatedJobs = [
new CircleCI.Job(JobNames.JsLint, nodeExecutor, [
new CircleCI.commands.Checkout(),
new CircleCI.commands.Run({
command: "npm ci && npm run lint",
}),
]),
new CircleCI.Job(JobNames.JsTest, nodeExecutor, [
new CircleCI.commands.Checkout(),
new CircleCI.commands.Run({ command: "npm ci && npm test" }),
]),
new CircleCI.Job(
JobNames.PhpLint,
new CircleCI.executors.DockerExecutor("cimg/php:8.0", "large"),
[
new CircleCI.commands.Checkout(),
new CircleCI.commands.Run({
command: "composer i && composer lint",
}),
]
),
new CircleCI.Job(
JobNames.PhpTest,
new CircleCI.executors.DockerExecutor("cimg/php:8.1", "large"),
[
new CircleCI.commands.Checkout(),
new CircleCI.commands.Run({ command: "composer i && composer test" }),
]
),
new CircleCI.Job(
JobNames.E2eTest,
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" }),
]
),
new CircleCI.Job(
JobNames.Vsix,
new CircleCI.executors.DockerExecutor("cimg/node:16.8.0-browsers", "large"),
[
new CircleCI.commands.Checkout(),
new CircleCI.commands.Run({ command: "npm ci && npm run vsix" }),
new CircleCI.commands.Run({
command: `mkdir /tmp/artifacts
mv ${
JSON.parse(
fs.existsSync("../../package.json")
? fs.readFileSync("../../package.json")?.toString()
: "{}"
).name
}*.vsix /tmp/artifacts`,
}),
new CircleCI.commands.StoreArtifacts({ path: "/tmp/artifacts" }),
]
),
new CircleCI.Job(
JobNames.Zip,
new CircleCI.executors.DockerExecutor("cimg/php:8.0"),
[
new CircleCI.commands.Checkout(),
new CircleCI.commands.Run({ command: "composer zip" }),
new CircleCI.commands.StoreArtifacts({
path: `${path.basename(
glob.sync("../../*.php")?.[0] ?? "",
".php"
)}.zip`,
}),
]
),
];
/** Not needed for the public API, simply use createConfig(). */
export function getJobs(...jobs: (string | CircleCI.Job)[]): CircleCI.Job[] {
return jobs.map((job) => {
return typeof job === "string"
? preCreatedJobs.find((preCreatedJob) => {
return job === preCreatedJob.name;
})
: job;
});
}
/** Creates and writes the config, given the passed jobs. */
export function createConfig(...jobs: (string | CircleCI.Job)[]) {
const config = new CircleCI.Config();
const workflow = new CircleCI.Workflow("test-lint");
config.addWorkflow(workflow);
getJobs(...jobs).forEach((job) => {
if (job) {
config.addJob(job);
workflow.addJob(job);
}
});
fs.writeFile("./dynamicConfig.yml", config.stringify(), () => {});
}
Code language: TypeScript (typescript)
createConfig()
on line 114 is the main function other repos will call.
That even writes the config file on line 126.
Next, create a package.json.
Most of the lines don’t matter, but you’ll need:
"main": "index.ts",
"type": "module",
Code language: JSON / JSON with Comments (json)
…and:
"dependencies": {
"@circleci/circleci-config-sdk": "^0.11.0",
}
Code language: JSON / JSON with Comments (json)
You’ll also need:
{
"compilerOptions": {
"module": "ESNext",
"target": "ESNext",
"moduleResolution": "Node",
"noEmit": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
}
}
Code language: JSON / JSON with Comments (json)
"allowSyntheticDefaultImports"
is important, it’ll let you import CircleCI
with:
import CircleCI from "@circleci/circleci-config-sdk";
Code language: TypeScript (typescript)
2. Consuming Repo
This will import
the common config above, and create a config from it.
You might have dozens of consuming repos.
import { createConfig, JobNames } from "@getlocalci/create-config";
createConfig(JobNames.PhpLint, JobNames.PhpTest, JobNames.Zip);
Code language: TypeScript (typescript)
This will run the index.ts
file via npm start
:
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)
.circleci/dynamic/package.json
{
"name": "adapter-gravity-add-on-dynamic-config",
"version": "0.1.0",
"description": "Dynamic config for AGA",
"author": "Ryan Kienstra",
"main": "index.ts",
"type": "module",
"scripts": {
"lint": "prettier --check index.ts",
"lint:fix": "prettier --write index.ts",
"start": "ts-node --esm --skipIgnore index.ts"
},
"license": "GPL-2.0-or-later",
"devDependencies": {
"@getlocalci/create-config": "github:getlocalci/create-config",
"@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)
Lines 7 and 11 above are important.
The --skipIgnore
flag on line 11 interprets @getlocalci/create-config as TypeScript, so we don’t have to compile it earlier.
.circleci/dynamic/tysconfig.json
{
"compilerOptions": {
"module": "ESNext",
"target": "ESNext",
"moduleResolution": "Node",
"esModuleInterop": true,
"noEmit": true
}
}
Code language: JSON / JSON with Comments (json)
Finally, to enable the Config SDK, go to your repo’s CircleCI Advanced Settings:
https://app.circleci.com/settings/project/github/<GitHub org>/<GitHub repo>/advanced
For this example, the URL is:
https://app.circleci.com/settings/project/github/kienstra/adapter-gravity-add-on/advanced
Then, select ‘Enable dynamic config using setup workflows’:
Here’s the result:
Live TypeScript CircleCI Configs
These are all importing the common config:
Updating Consuming Repos
Let’s say in the common repo, you bump a container from "cimg/node:16.8.0"
to "cimg/node:18.10.0"
.
And you want consuming repos to get that change.
In the common repo, merge the change into the default branch.
You don’t even have to compile the TypeScript, or do npm publish
.
The consuming repo will interpret it as TypeScript.
In each consuming repo’s .circleci/dynamic, do:
$ npm update @getlocalci/create-config
Code language: Bash (bash)
…but substitute the name of your common repo.
This Changes CircleCI
The TypeScript Config SDK is a huge feature.
Though of course, you can keep using the traditional config.yml
if you prefer.
You can now keep your config in one repo, and mainly edit that repo.
This is like orbs, which can have commands and jobs.
But orbs can’t create the entire config for you.
The Config SDK can.
Here, you can reuse a config.
But you don’t get the typical headaches that come with reuse.
Like tagging and publishing an npm package.
If you like TypeScript…
This is now the way to write CircleCI configs.
Reader interactions
One Reply to “TypeScript CircleCI Configs: Only 3 Lines”
Comments are closed.
This makes it really easy to keep your configs up to date.
If you need to bump an image version, you can do it in one place.