How do I use orbs in the circleci-sdk-config?

Hi!

I’m trying to use orbs with the SDK that has been released but don’t understand quite how it works. I have imported the orb

const general = new orb.OrbImport('general', 'edahlseng', 'general', '1.29.0');

But now how do I use it?

Hey @MarCC ,

While orbs do work in the SDK, they are still a little rough around the edges.

We have a guide in our Wiki on using orbs as they exist currently: Use Orbs · CircleCI-Public/circleci-config-sdk-ts Wiki · GitHub

I am currently looking into better ways to support orbs. The main issue is you will lack type information about orbs, since the source isn’t available in TypeScript at “dev time”, the orb has to be fetched from somewhere remotely, where it also only exists as YAML.

This isn’t terribly different from the problem Prisma.io faces with their Prisma Schema file and needing to keep the Prisma Client up-to-date. To solve this, Prisma has implemented Generators, and I think we will do the same. We would implement a CLI with a generate function like Prisma, which would generate the code for the orb for you, and import it as a package in your project.

For now…

Until then, we have to work manually. You can see how orbs are configured in the current tests for orbs.

First, we describe the orb so that we have access to its type information in our config. We do this by creating a “manifest”, and this is what currently requires the most manual effort.

  const manifest: CircleCI.types.orb.OrbImportManifest = {
    jobs: {
      say_hello: new CircleCI.parameters.CustomParametersList([
        new CircleCI.parameters.CustomParameter('greeting', 'string'),
      ]),
    },
    commands: {
      say_it: new CircleCI.parameters.CustomParametersList([
        new CircleCI.parameters.CustomParameter('what', 'string'),
      ]),
    },
    executors: {
      python: new CircleCI.parameters.CustomParametersList([
        new CircleCI.parameters.CustomParameter('greeting', 'string', '1.0.0'),
      ]),
    },
  };

Notice the manifest describes only the components on the orb, and their parameters.

We then create an OrbImport using the manifest for types and add the rest of the metadata.

  const exampleOrb = new CircleCI.orb.OrbImport(
    "orb-alias",
    "orbNamespace",
    "orbName",
    "orbVersion",
    "description",
    manifest,
  );

Now with the orb fully defined, we can use it in our config.

Import the orb in the config:
config.importOrb(exampleOrb);

Under CircleCI.reusable you can define a Reused Executor, Job, or Command. We pass in the definitions from our orb to create these reused implementations. In the test you will see we define a normal CirlceCI Job, using the reused Executor and Command from the orb we imported, and we are able to pass parameters thanks to the manifest.

  const job = new CircleCI.Job(
    'test',
    new CircleCI.reusable.ReusedExecutor(pythonExecutor, { version: '1.2.3' }),
    [
      new CircleCI.reusable.ReusedCommand(sayItCommand, {
        what: 'cheese',
      }),
    ],
  );

You’ll also see we can use orb jobs in our workflow. Since all Jobs are reusable by default, the syntax did not need to change here, so this is a little more straightforward.

  const workflow = new CircleCI.Workflow('default', [
    new CircleCI.workflow.WorkflowJob(exampleOrb.jobs['say_hello'], {
      greeting: 'hello',
    }),
  ]);

The doc linked above in the wiki shows a realistic example using the Slack orb.

Hi!

Thank you so much! This is def. enough to get started, I’m also looking forward to the improvements.

A follow up question, how can I add parameters to a job?

1 Like


(couldn’t resist)

Assuming we are talking about “normal” jobs, that’s actually the neat part about the Config SDK. All of the features of CircleCI’s 2.1 config (vs 2.0) such as parameters, and “reusable” config, are analogs for programming constructs. We don’t need them because we have a real programming language to use instead.

A “reusable” job with parameters in CircleCI 2.1 config is like the YAML version of a function in a programming language, so instead, we just use functions!

So take this example:

version: 2.1

executors:
  linux:
    docker:
      - image: cimg/base:2020.01
  macos:
    macos:
      xcode: 11.4

orbs:
  node: circleci/node@2.0.0

jobs:
  test:
    parameters:
      os:
        type: executor
      node-version:
        type: string
    executor: << parameters.os >>
    steps:
      - checkout
      - node/install:
          node-version: << parameters.node-version >>
          install-yarn: true
      - run: yarn test

In this example we have a job named test that we want to run on both linux and maxos, we also want to test multiple node versions. So we use parameters and then we pass values to those parameters using the matrix key in workflows like so:

workflows:
  all-tests:
    jobs:
      - test:
          matrix:
            parameters:
              os: [linux, macos]
              node-version: ["10.9.0", "11.9.0", "12.9.1", "13.9.0"]

This will create a job for each version of node (node-version), on each os. This is like a function.

Doing it in TypeScript

I am going to steal from an example I already have so I am going to skip the OS part and focus just on the node version part, but it is the same.

Job definition as a function:

const testJob = (version: string) =>
  new CircleCI.Job(`test-${version.replace(".", "-")}`, nodeExecutor(version), [
    new CircleCI.commands.Checkout(),
    new CircleCI.commands.Run({
      command: "npm install && npm run test",
    }),
  ]);

We now call testJob() and get back a job with the version we pass.

// Support the three latest versions of Node
const nodeVersions = ["16.19", "18.13", "19.7"];

const testJobs = nodeVersions.map((version) => testJob(version));
testJobs.forEach((job) => {
  myConfig.addJob(job);
  myWorkflow.addJob(job)
});

Here I am generating an array of jobs based on the node versions defined in nodeVersions using map to call testJob(version), there are more than a few ways to do this but this gets to the point.

Once I have my jobs in the testJobs variable, I run forEach to add each of those jobs to both the config and the workflow.

This will generate more YAML at the end than you would typically write, but it is actually exactly what happens when you write parameterized YAML config anyway.

Here is my full config application:

import * as CircleCI from '@circleci/circleci-config-sdk';
import { testJob, deployJob } from './jobs';
import { isDeployable } from './utils';

const myConfig = new CircleCI.Config();
const myWorkflow = new CircleCI.Workflow('my-workflow');

// Support the three latest versions of Node
const nodeVersions = ["16.19", "18.13", "19.7"];

const testJobs = nodeVersions.map((version) => testJob(version));
testJobs.forEach((job) => {
  myConfig.addJob(job);
  myWorkflow.addJob(job)
});

if (isDeployable()) {
  myConfig.addJob(deployJob);
  myWorkflow.addJob(deployJob, {
    requires: testJobs.map((job) => job.name)
  });
}

myConfig.addWorkflow(myWorkflow);
myConfig.writeFile('dynamicConfig.yml')

Hi!

Okay that is awesome, is there a dynamic-config file size limit? Because in our case we will generate a test, build, plan, apply step for each changed directory in our system. I think it could become quite a big config file (a max case could be around ~20 directories * 4 jobs).

There is but it is multiple megabytes. Apologies, I will try to find the correct number. It used to be 2mb, but was recently increased. There is a max size, but for text files, it’s fairly huge.

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.