CircleCI: cd Into Directory

Here’s how to cd into a directory in CircleCI.

If No working_directory

If there’s no working_directory value in the step, you’ll start in the directory /home/circleci/project:

      - checkout
      - run:
          name: Running e2e tests
          command: |
            pwd
            # prints /home/circleci/project
            ls
            # prints the files in your repo: babel.config.json  LICENSE  php etc…Code language: YAML (yaml)

That directory will also have your repo, assuming your checkout command didn’t have a path value.

If the checkout command had a path value, it’ll be checked out there:

      - checkout:
          path: /tmp/e2e
      - run:
          name: Running e2e tests
          command: |
            pwd
            # prints /home/circleci/project
            cd /tmp/e2e
            ls
            # prints babel.config.json  LICENSE  php etc…Code language: YAML (yaml)

If working_directory

If there’s a working_directory value in the step, you’ll start in that directory, relative to /home/circleci/project

      - run:
          name: Running e2e tests
          working_directory: e2e
          command: |
            pwd
            # prints /home/circleci/project/e2e
Code language: YAML (yaml)

Because that doesn’t start with /, it is relative to ~/project/

So it’ll be ~/project/e2e

If it started with / or ~/ it’d be an absolute path:

      - run:
          name: Running e2e tests
          working_directory: /tmp/e2e
          command: |
            pwd
            # prints /tmp/e2e
Code language: YAML (yaml)

Don’t Do This

      - run: cd foo
      - run: npm testCode language: YAML (yaml)

Each run command goes back to /home/circleci/project or its working_directory:

  - run: cd foo && pwd # prints /home/circleci/project/foo
  - run: pwd # prints /home/circleci/projectCode language: YAML (yaml)

Checking Out Repo To Another Directory

version: 2.1

references:
  REPO_PATH: &REPO_PATH
    /tmp/e2e

jobs:
  js-build:
    docker:
      - image: cimg/node:14.18
    steps:
      - checkout:
          path: *REPO_PATH
      - run:
          working_directory: *REPO_PATH
          command: |
            pwd
            # prints /tmp/e2e
      - run:
          working_directory: *REPO_PATH
          command: npm test
Code language: YAML (yaml)

But you’ll only need REPO_PATH if you have multiple steps.

Usually, you can simply pass a string literal to working_directory.

And this should only be needed if you’re checking out multiple repos.

Like a repo for e2e tests, and a repo for your project.

Instant Feedback

Here’s how to cd into directory and see where you are, right away.

In VS Code, install the extension Local CI.

1. Click its icon on the left

2. Click ‘Select Repo’

3. Select the repo

4. You’ll see the jobs:

CircleCI cd into directory

5. Click the job you want bash access to

6. Click ‘debugging’

7. Run bash commands, like cd into directory:

$ whoami
circleci
$ pwd
/home/circleci
$ ls
project
$ cd project
$ ls
babel.config.json  package-lock.json
composer.json      php
composer.lock      README.md
LICENSE            src
node_modules       tests
package.jsonCode language: Bash (bash)

Most of the time, you shouldn’t need to cd in CircleCI commands.

working_directory should get you into the right directory.

But if you need to change directories, do it in the same command.

Local CI can help get instant feedback on where you are.

But if you’d prefer to use the CircleCI CLI on your own…

Here’s how to do that.

CircleCI cd Into Directory

Did this help?

If this was terrible, leave a comment below.

Or email me at ryan @ this domain.

CI/CD can be hard…

But instant feedback makes it easier.

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.

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:

CircleCI customize your resource class

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/ -DCode language: Bash (bash)

…and npm will install it from the default branch on GithHub.

There are 2 parts:

  1. Your common config repo
  2. 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:

index.ts

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:

  1. Job name
  2. Executor
  3. Steps

You’ll get type-hinting for each of these:

TypeScript CircleCI Config SDK command type hinting

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:

tsconfig.json

{
  "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.

.circleci/dynamic/index.ts

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:

.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)

.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’:

TypeScript CircleCI Config enable setup workflows

Here’s the result:

TypeScript CircleCI configs jobs

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-configCode 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.

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.

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.

Caching Dependencies in CircleCI

It’s usually really easy to cache dependencies in CircleCI.

Most of the time…

You don’t even need to set cache keys.

Or know how its caching works.

Below are examples of caching Node, Composer, and Gradle dependencies.

CircleCI takes care of it under the hood when you use orbs for caching.

Orbs For Caching Dependencies

There’s usually a certified CircleCI orb that will handle caching for you.

An orb is packaged YAML for a CircleCI config.

Like an npm package, but for CircleCI.

So caching dependencies in CircleCI usually means finding the right orb for the environment:

Node Example

Here’s how to do caching with the Node orb:

diff --git a/.circleci/config.yml b/.circleci/config.yml
index 74a030f..a6e0f84 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -1,27 +1,31 @@
 version: 2.1
 
+orbs:
+  node: circleci/node@5.0
+
 jobs:
   lint-js:
     docker:
       - image: cimg/node:14.19
     steps:
       - checkout
-      - run: npm ci
+      - node/install-packages
       - run: npm run lint:js
   test-js:
     docker:
       - image: cimg/node:14.19
     steps:
       - checkout
-      - run: npm ci
+      - node/install-packages
       - run: npm run test:js
Code language: Diff (diff)

We reference that orb on lines 18 and 26 by starting with node/, then adding the command name of install-packages.

The resulting node/install-packages command will cache npm ci.

So every time it runs, it’ll first check if there’s a cache for the same lockfile.

If there is, it restores that cache, instead of installing it all from scratch.

Here’s a small part of what that node/install-packages command does:

- restore_cache:
    keys:
      - >-
        node-deps-{{ arch
        }}-<<parameters.cache-version>>-<<#parameters.include-branch-in-cache-key>>{{
        .Branch
        }}-<</parameters.include-branch-in-cache-key>><<^parameters.cache-only-lockfile>>{{
        checksum "/tmp/node-project-package.json"
        }}-<</parameters.cache-only-lockfile>>{{ checksum
        "/tmp/node-project-lockfile" }}
      - >-
        node-deps-{{ arch
        }}-<<parameters.cache-version>>-<<#parameters.include-branch-in-cache-key>>{{
        .Branch }}-<</parameters.include-branch-in-cache-key>>{{
        checksum "/tmp/node-project-package.json" }}
      - >-
        node-deps-{{ arch
        }}-<<parameters.cache-version>>-<<#parameters.include-branch-in-cache-key>>{{
        .Branch }}-<</parameters.include-branch-in-cache-key>>Code language: YAML (yaml)

But you don’t have to know about that.

It takes care of caching dependencies in CircleCI for you.

PHP Example

Again, we’ll use an install-packages command to do caching.

But we’ll prefix it with php.

The full command is php/install-packages, on line 18:

diff --git a/.circleci/config.yml b/.circleci/config.yml
index 20bc520..07bc337 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -2,6 +2,7 @@ version: 2.1
 
 orbs:
   node: circleci/node@5.0
+  php: circleci/php@1.1
 
 jobs:
   lint-js:
@@ -26,7 +27,7 @@ jobs:
     steps:
       - checkout
       - node/install-packages
-      - run: composer install
+      - php/install-packages
       - run: npm run build
       - run: composer test
Code language: Diff (diff)

Here’s a small part of the source of that command:

- restore_cache:
    keys:
      - >-
        composer-deps-<<parameters.cache-version>>-{{ checksum
        "<<parameters.app-dir>>/<<parameters.cache-key>>" }}Code language: YAML (yaml)

You might be worried about whether you’ll have to flush this cache when CircleCI runs.

Like if tests fail.

I haven’t had to do that, its caching has been reliable.

Gradle Example

This is different than the install-packages commands earlier.

This wraps steps in a caching command: gradle/with_cache

-- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -2,6 +2,7 @@ version: 2.1
 
 orbs:
   android: circleci/android@2.1
+  gradle: circleci/gradle@3.0
 
 executors:
   android:
@@ -13,9 +14,11 @@ jobs:
     executor: android
     steps:
       - checkout
-      - run:
-          name: Building the APK
-          command: ./gradlew -s assembleDebug
+      - gradle/with_cache:
+          steps:
+            - run:
+                name: Building the APK
+                command: ./gradlew -s assembleDebug
       - store_artifacts:
           path: app/build/outputs/apk/debug/app-debug.apk
Code language: Diff (diff)

Now, this caches that command in CircleCI:

Caching Dependencies in CircleCI in a Gradle job

Caching Dependencies in CircleCI

The install-packages or with_cache commands will be enough in many environments.

CircleCI makes it easy for you to cache.

Without creating your own cache keys, or learning the internals of how it caches.

Another great part about CircleCI is how they make it easy to debug.

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.

CircleCI CLI

The CircleCI CLI lets you run a job locally.

So if your build ever fails, you don’t have to:

  • Guess what’s wrong
  • Push a commit
  • Wait for it to fail
  • Guess what’s wrong again
  • Push another commit…

For me, this makes CircleCI® the clear leader.

Here’s how to get bash access to the job locally, using the CLI.

So you’ll be able to see what’s wrong.

Real Example

We’re going to debug a failed CI/CD build:

/bin/bash: svn: command not foundCode language: Bash (bash)
CircleCI CLI svn command not found

The solution isn’t clear, at least to me.

We don’t know:

  • What package management system does the container use?
  • What package should we install to run the svn command?

So you’re going to get bash access to debug this.

Start by cloning the example repo, where the failed build was:

$ git clone -b add/wp-org-svn https://github.com/kienstra/adapter-responsive-video
$ cd adapter-responsive-videoCode language: Bash (bash)

Of course, you can use your own repo instead, if there was a failed build there.

Adding Time To Debug

Add this to .circleci/config.yml, right before the step that failed:

diff --git a/.circleci/config.yml b/.circleci/config.yml
index e27995a..1c93a1d 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -9,6 +9,7 @@ jobs:
       - image: cimg/base:current
     steps:
       - checkout
+      - run: sleep 1000
       - run: svn co https://plugins.svn.wordpress.org/adapter-responsive-video --depth=empty .
 
 workflows:Code language: Diff (diff)

This will keep the job running, so you have time to debug it with bash access.

Installing the CircleCI CLI (if you haven’t yet)

Mac:

$ brew install circleciCode language: Bash (bash)

Mac and Linux:

$ curl -fLSs https://raw.githubusercontent.com/CircleCI-Public/circleci-cli/master/install.sh | bashCode language: JavaScript (javascript)

This CircleCI CLI will run the job in your local.

Please also ensure Docker is running on your machine.

Running the CLI

$ circleci local execute --job deployCode language: Bash (bash)

This will run the job on your machine:

CircleCI CLI running in terminal

You might see that pulling some images to run the job.

Getting the Job Image Name

Open a new terminal tab, and run this:

$ docker psCode language: Bash (bash)

That should show the image that’s running the job:

CONTAINER ID   IMAGE               COMMAND                  CREATED          STATUS          PORTS     NAMES
b6e09b7009ab   cimg/base:current   "/bin/sh"                13 seconds ago   Up 10 seconds             naughty_jones
b992e2d2c0a8   circleci/picard     "/opt/circleci/linux…"   16 seconds ago   Up 16 seconds             naughty_colden
5c233ff5c3b9   cimg/base:current   "/bin/sh"                6 minutes ago    Up 5 minutes              elastic_brattain
Code language: Bash (bash)

Look for an image right above circleci/picard.

If you don’t see circleci/picard, you’ll probably have to wait 10-20 seconds while the circleci local command above pulls that image.

In this case, it’s the cimg/base:current image.

Copy the container ID of that image to use it below.

In this example, it’s b6e09b7009ab.

That’s the image where your job is running, and you’re going to get bash access to it.

Bash

Then, run this in your terminal to debug the job.

$ docker exec -it b6e09b7009ab /bin/shCode language: Bash (bash)

The argument b6e09b7009ab is the container ID that you got above, from running docker ps.

You should then have bash access to the container:

CircleCI CLI bash access to the container

$ whoami
circleci
$ pwd
/home/circleci/projectCode language: Bash (bash)

Then, you can debug the container, and find out how to install the svn command:

CircleCI CLI

This faster debugging makes CircleCI the leader, in my opinion.

No more guessing and waiting.

You can fix your CI locally.

Now that you’ve debugged your config…

Here’s a way to make it faster and simpler.

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.

How do I debug my CI/CD?

Here’s how I debug my CI/CD.

This is a real example of debugging CircleCI jobs on a GitHub repo.

We’ll debug it locally…

Without pushing commits to the repo and waiting.

First, we notice that CI/CD is failing:

To debug my CI/CD job

When we click Details, the errors mention node-gyp:

npm ERR!   g++ '-DNODE_GYP_MODULE_NAME=libsass' '-DUSING_UV_SHARED=1' '-DUSING_V8_SHARED=1' '-DV8_DEPRECATION_WARNINGS=1' '-DV8_DEPRECATION_WARNINGS' '-DV8_IMMINENT_DEPRECATION_WARNINGS' '-D_GLIBCXX_USE_CXX11_ABI=1'
Debug my CI/CD with node-gyp

From experience, node-gyp has failed when changing the Node version.

Looking at package-lock.json, we’d probably expect the Node version to be 14 or lower.

That’s because the lockfileVersion is 1, which usually infers Node 14 or lower:

So let’s see if the Node version in CI is 14 or lower.

We’ll open VS Code and install the extension Local CI:

Installing Local CI in VS Code

It’ll start a free trial automatically, without entering a credit card.

Then, we’ll run the CI job on our local machine.

And enter node --version to see what the version is.

Using Local CI in VS Code

We can run the job without pushing a commit or clicking anything in the repo.

It turns out that the Node version is 16, where it should probably be 14, at least with the current dependencies.

So let’s see if changing the Node version to 14 makes this job pass.

We’ll test this without pushing any code to the repo, or triggering another failed build there.

First, we’ll add an orb for Node:

Orbs are like utility functions for CircleCI, though they’re written in .yml.

And add a node/install step to set Node to 14.18.1:

Then, we’ll commit that change in git.

And we’ll rerun this job locally, no need to push the commit yet:

Local CI job failed with an error

It failed with an error:

====>> Checkout code
Error: 
Directory (/home/circleci/project) you are trying to checkout to is not empty and not a git repository

Step failed

Maybe there was a problem with the order of the checkout step.

So let’s see if moving the checkout step before the node/install step fixes it:

We’ll rerun the job locally:

CI/CD job passes locally

It passed!

Now, let’s make sure the next job doesn’t have an obvious problem.

We’ll run the next job, py39:

Python job in CI/CD

The first few minutes of the job went well.

So let’s push to the repo, as it looks like CI should pass.

All of the the jobs passed:

All of the CI builds passed

The ideal solution might have been to upgrade the package.json dependencies so they work with Node 16 or 17.

But the idea here is to show how to debug CI/CD locally.

This debugging feedback is one of CircleCI‘s huge advantages.

No endless cycle of guessing, pushing, waiting for it to fail, guessing, pushing…

Now that you’ve seen how I debug my CI/CD, here’s a fast way to speed up your builds.

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.