Skip to content

Commit 0b451b9

Browse files
committed
content: driving buildkite pipelines with CUE
Here's a new cue-by-example, showing how to manage a set of Buildkite pipeline files in CUE instead of YAML. Because Buildkite (in its non-dynamic-pipeline mode) still needs to see a YAML file serialised in the repo, the guide includes a CUE _tool that turns the CUE back into YAML on demand. It also includes a schema for the pipeline's representation, but this schema is very trivial, and not hugely useful - it could do with improving. I wasn't able to use the upstream schema (https://github.com/buildkite/pipeline-schema) because of issues now tracked as cue-lang/cue#2698 and cue-lang/cue#2699. This is related to cue-labs#21 but doesn't /close/ it, as that issue tracks a cue-by-example for Buildkite's "dynamic" pipeline mode, which this guide doesn't address. Signed-off-by: Jonathan Matthews <[email protected]>
1 parent b8f0a5a commit 0b451b9

File tree

2 files changed

+383
-0
lines changed

2 files changed

+383
-0
lines changed

Diff for: 006_buildkite_importing_pipelines/README.md

+382
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,382 @@
1+
# Driving Buildkite Pipelines with CUE
2+
<sup>by [Jonathan Matthews](https://jonathanmatthews.com)</sup>
3+
4+
This guide explains how to convert static Buildkite pipeline files from YAML to
5+
CUE, check those pipelines are valid, and then use CUE's tooling layer to
6+
regenerate YAML.
7+
8+
This allows you to switch to CUE as a source of truth for Buildkite pipelines
9+
and perform client-side validation, without Buildkite needing to know you're
10+
managing your static pipelines with CUE.
11+
12+
## Prerequisites
13+
14+
- You have
15+
[CUE installed](https://alpha.cuelang.org/docs/introduction/installation/)
16+
locally. This allows you to run `cue` commands.
17+
- You have a set of static Buildkite pipeline files. The examples shown in this
18+
guide use a specific commit from the Buildkite
19+
[dependent-pipeline-example](https://github.com/buildkite/dependent-pipeline-example/tree/a7419a24bee24068d4d79399bccd9093569c3013/.buildkite)
20+
repository, *but you don't need to use that repository in any way.*
21+
- You have [`git` installed](https://git-scm.com/downloads).
22+
<!-- TODO: curl isn't needed until the doc includes fetching the upstream schema
23+
- You have [`curl` installed](https://curl.se/dlwiz/), or can fetch a file from
24+
a website some other way.
25+
-->
26+
27+
## Steps
28+
29+
### Convert YAML pipelines to CUE
30+
31+
#### :arrow_right: Begin with a clean git state
32+
33+
Change directory into the root of the repository that contains your static
34+
Buildkite pipeline files, and ensure you start this process with a clean git
35+
state, with no modified files. For example:
36+
37+
:computer: `terminal`
38+
```sh
39+
cd dependent-pipeline-example # our example repository
40+
git status # should report "working tree clean"
41+
```
42+
43+
#### :arrow_right: Initialise a CUE module
44+
45+
Initialise a CUE module named after the organisation and repository you're
46+
working with. For example:
47+
48+
:computer: `terminal`
49+
```sh
50+
cue mod init github.com/buildkite/dependent-pipeline-example
51+
```
52+
53+
#### :arrow_right: Import YAML pipelines
54+
55+
Use `cue` to import your YAML pipeline files:
56+
57+
:computer: `terminal`
58+
```sh
59+
cue import ./.buildkite/*.yml --with-context -p buildkite -f -l pipelines: -l 'strings.TrimSuffix(path.Base(filename),path.Ext(filename))'
60+
```
61+
62+
Check that a CUE file has been created for each YAML pipeline in the
63+
`.buildkite` directory. For example:
64+
65+
:computer: `terminal`
66+
```sh
67+
ls .buildkite/
68+
```
69+
70+
Your output should look similar to this, with matching pairs of YAML and CUE
71+
files:
72+
73+
```text
74+
pipeline.cue pipeline.deploy.cue pipeline.deploy.yml pipeline.yml
75+
```
76+
77+
Observe that each pipeline has been imported into the `pipelines` struct, at a
78+
location derived from its original file name:
79+
80+
:computer: `terminal`
81+
```sh
82+
head .buildkite/*.cue
83+
```
84+
85+
The output should reflect your pipelines. In our example:
86+
87+
```text
88+
==> .buildkite/pipeline.cue <==
89+
package buildkite
90+
91+
pipelines: pipeline: steps: [{
92+
command: "echo 'Tests'"
93+
label: ":hammer:"
94+
},
95+
"wait", {
96+
trigger: "dependent-pipeline-example-deploy"
97+
label: ":rocket:"
98+
branches: "master"
99+
100+
==> .buildkite/pipeline.deploy.cue <==
101+
package buildkite
102+
103+
pipelines: "pipeline.deploy": steps: [{
104+
command: "echo 'Deploy'"
105+
label: ":rocket:"
106+
concurrency_group: "$BUILDKITE_PIPELINE_SLUG-deploy"
107+
concurrency: 1
108+
branches: "master"
109+
}]
110+
```
111+
112+
#### :arrow_right: Store CUE pipelines in a dedicated directory
113+
114+
Create a directory called `buildkite` to hold your CUE-based Buildkite pipeline
115+
files. For example:
116+
117+
:computer: `terminal`
118+
```sh
119+
mkdir -p internal/ci/buildkite
120+
```
121+
122+
You may change the hierarchy and naming of `buildkite`'s **parent** directories
123+
to suit your repository layout. If you do so, you will need to adapt some
124+
commands and CUE code as you follow this guide.
125+
126+
Move the newly-created CUE files into their dedicated directory. For example:
127+
128+
:computer: `terminal`
129+
```sh
130+
mv ./.buildkite/*.cue internal/ci/buildkite
131+
```
132+
133+
### Validate workflows
134+
135+
<!-- TODO: upstream JSONSchema isn't usable
136+
cf. https://github.com/cue-lang/cue/issues/2698
137+
cf. https://github.com/cue-lang/cue/issues/2699
138+
139+
#### :arrow_right: Fetch a pipeline schema
140+
141+
Fetch a schema for Buildkite pipelines, as defined by Buildkite themselves, and
142+
place it in the `internal/ci/buildkite` directory:
143+
144+
:computer: `terminal`
145+
```sh
146+
curl -o internal/ci/buildkite/buildkite.pipeline.schema.json https://raw.githubusercontent.com/buildkite/pipeline-schema/6396f68d5e983e0d2acbf829c565027a4cfd69bc/schema.json
147+
```
148+
149+
We use a specific commit from the upstream repository to make sure that this
150+
process is reproducible.
151+
152+
#### :arrow_right: Import the schema
153+
154+
Import the schema into CUE:
155+
156+
:computer: `terminal`
157+
```sh
158+
cue import -f -l '#Pipeline:' internal/ci/buildkite/buildkite.pipeline.schema.json
159+
```
160+
-->
161+
162+
#### :arrow_right: Create a pipeline schema
163+
164+
Create a very basic CUE schema for Buildkite pipelines, adapted from
165+
[Buildkite's documentation](https://buildkite.com/docs/pipelines/configuration-overview),
166+
and place it in the `internal/ci/buildkite` directory:
167+
168+
:floppy_disk: `internal/ci/buildkite/pipelineSchema.cue`
169+
170+
```CUE
171+
package buildkite
172+
173+
#Pipeline: {
174+
steps!: [...]
175+
env?: [string]: string
176+
agents?: {[string]: string} | [ ..._#kv]
177+
_#kv: =~".+="
178+
notify?: [...]
179+
}
180+
```
181+
182+
| :grey_exclamation: Info :grey_exclamation: |
183+
|:------------------------------------------- |
184+
| It would be great if we could use [Buildkite's authoritative pipeline schema](https://github.com/buildkite/pipeline-schema) here. Unfortunately, CUE's JSONSchema support can't currently import it. This is being tracked in CUE Issues [#2698](https://github.com/cue-lang/cue/issues/2698) and [#2699](https://github.com/cue-lang/cue/issues/2699), and this guide should be updated once the schema is useable.
185+
186+
#### :arrow_right: Apply the schema
187+
188+
We need to tell CUE to apply the schema to each pipeline.
189+
190+
To do this we'll create a file at `internal/ci/buildkite/pipelines.cue` in our
191+
example.
192+
193+
However, **if the pipeline imports that you performed earlier *already* created
194+
a file with that same path and name**, then simply select a different CUE
195+
filename that *doesn't* already exist. Place the file in the
196+
`internal/ci/buildkite/` directory.
197+
198+
:floppy_disk: `internal/ci/buildkite/pipelines.cue`
199+
200+
```
201+
package buildkite
202+
203+
// each member of the pipelines struct must be a valid #Pipeline
204+
pipelines: [_]: #Pipeline
205+
```
206+
207+
### Generate YAML from CUE
208+
209+
We need to replace the static YAML pipeline files that we originally imported
210+
with YAML generated from the CUE files we've created. No material changes will
211+
be made to the YAML files, but comments will be removed (as they now live
212+
in the CUE sources, where users will see them), some unimportant YAML
213+
formatting may change, and a comment will be added at the top of each generated
214+
YAML file pointing users towards the new source of truth: CUE.
215+
216+
#### :arrow_right: Create a CUE tool file
217+
218+
Create a CUE "tool" file at `internal/ci/buildkite/ci_tool.cue` and adapt the
219+
element commented with `TODO`:
220+
221+
:floppy_disk: `internal/ci/buildkite/ci_tool.cue`
222+
```CUE
223+
package buildkite
224+
225+
import (
226+
"path"
227+
"encoding/yaml"
228+
"tool/file"
229+
)
230+
231+
_goos: string @tag(os,var=os)
232+
233+
// Regenerate all pipeline files
234+
command: regenerate: {
235+
pipeline_files: {
236+
// TODO: update _toolFile to reflect the directory hierarchy containing this file.
237+
let _toolFile = "internal/ci/buildkite/ci_tool.cue"
238+
let _pipelineDir = path.FromSlash(".buildkite", path.Unix)
239+
let _donotedit = "Code generated by \(_toolFile); DO NOT EDIT."
240+
241+
clean: {
242+
glob: file.Glob & {
243+
glob: path.Join([_pipelineDir, "*.yml"], _goos)
244+
files: [...string]
245+
}
246+
for _, _filename in glob.files {
247+
"Delete \(_filename)": file.RemoveAll & {path: _filename}
248+
}
249+
}
250+
251+
create: {
252+
for _pipelineName, _pipeline in pipelines
253+
let _filename = _pipelineName + ".yml" {
254+
"Generate \(_filename)": file.Create & {
255+
$after: [ for v in clean {v}]
256+
filename: path.Join([_pipelineDir, _filename], _goos)
257+
contents: "# \(_donotedit)\n\n\(yaml.Marshal(_pipeline))"
258+
}
259+
}
260+
}
261+
}
262+
}
263+
```
264+
265+
Make the modification indicated by the `TODO` comment.
266+
267+
This tool will export each CUE-based pipeline back into its required YAML file,
268+
on demand.
269+
270+
#### :arrow_right: Test the CUE tool file
271+
272+
With the modified `ci_tool.cue` file in place, check that the `regenerate`
273+
command is available **from a shell sitting at the repo root**. For example:
274+
275+
:computer: `terminal`
276+
```sh
277+
cd $(git rev-parse --show-toplevel) # make sure we're sitting at the repository root
278+
cue help cmd regenerate ./internal/ci/buildkite # the "./" prefix is required
279+
```
280+
281+
Your output **must** begin with the following:
282+
283+
```text
284+
Regenerate all pipeline files
285+
Usage:
286+
cue cmd regenerate [flags]
287+
[... output continues ...]
288+
```
289+
290+
| :exclamation: WARNING :exclamation: |
291+
|:--------------------------------------- |
292+
| If you *don't* see the usage explanation for the `regenerate` command (or if you receive an error message) then your tool file isn't set up as CUE requires. Double check the contents of the `ci_tool.cue` file and the modifications you made to it, as well as its location in the repository. Ensure the filename is *exactly* `ci_tool.cue`. Make sure you've followed all the steps in this guide, and that you invoked the `cue help` command from the root of the repository.
293+
294+
#### :arrow_right: Regenerate the YAML pipeline files
295+
296+
Run the `regenerate` command to produce YAML pipeline files from CUE. For
297+
example:
298+
299+
:computer: `terminal`
300+
```sh
301+
cue cmd regenerate ./internal/ci/buildkite # the "./" prefix is required
302+
```
303+
304+
#### :arrow_right: Audit changes to the YAML pipeline files
305+
306+
Check that each YAML pipeline file has a single *material* change from the
307+
original:
308+
309+
:computer: `terminal`
310+
```sh
311+
git diff .buildkite/
312+
```
313+
314+
Your output should look similar to the following example:
315+
316+
```diff
317+
diff --git a/.buildkite/pipeline.deploy.yml b/.buildkite/pipeline.deploy.yml
318+
index 4af2b9d..e0fc010 100644
319+
--- a/.buildkite/pipeline.deploy.yml
320+
+++ b/.buildkite/pipeline.deploy.yml
321+
@@ -1,6 +1,8 @@
322+
+# Code generated by internal/ci/buildkite/ci_tool.cue; DO NOT EDIT.
323+
+
324+
steps:
325+
- command: echo 'Deploy'
326+
diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml
327+
index 7446201..10cefad 100644
328+
--- a/.buildkite/pipeline.yml
329+
+++ b/.buildkite/pipeline.yml
330+
@@ -1,12 +1,14 @@
331+
+# Code generated by internal/ci/buildkite/ci_tool.cue; DO NOT EDIT.
332+
+
333+
steps:
334+
- command: echo 'Tests'
335+
label: ':hammer:'
336+
- wait
337+
- trigger: dependent-pipeline-example-deploy
338+
```
339+
340+
The only *material* change in each YAML file is the addition of a header that
341+
warns the reader not to edit the file directly.
342+
343+
#### :arrow_right: Add and commit files to git
344+
345+
Add your files to git. For example:
346+
347+
:computer: `terminal`
348+
```sh
349+
git add .buildkite/ internal/ci/buildkite/ cue.mod/module.cue
350+
```
351+
352+
Make sure to include your slightly modified YAML pipeline files in
353+
`.buildkite/` along with all the new files in `internal/ci/buildkite/` and
354+
your `cue.mod/module.cue` file.
355+
356+
Commit your files to git, with an appropriate commit message:
357+
358+
:computer: `terminal`
359+
```sh
360+
git commit -m "ci: create CUE sources for Buildkite pipelines"
361+
```
362+
363+
## Conclusion
364+
365+
**Well done - your static Buildkite pipeline files have been imported into
366+
CUE!**
367+
368+
They can now be managed using CUE, leading to safer and more predictable
369+
changes. The use of a schema to check your pipelines means that you
370+
will catch and fix many types of mistake earlier than before, without waiting
371+
for the slow "git add/commit/push; check if CI fails" cycle.
372+
373+
From now on, each time you make a change to a CUE pipeline file, immediately
374+
regenerate the YAML files required by Buildkite, and commit your changes
375+
to all the CUE and YAML files. For example:
376+
377+
:computer: `terminal`
378+
```sh
379+
cue cmd regenerate ./internal/ci/buildkite/ # the "./" prefix is required
380+
git add .buildkite/ internal/ci/buildkite/
381+
git commit -m "ci: added new release pipeline" # example message
382+
```

Diff for: README.md

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ integrating with 3rd-party tools, services, and systems.
1010
- 003: [Controlling Kubernetes with CUE](003_kubernetes_tutorial/README.md)
1111
- 004: [Managing Mythic Beasts DNS zones with CUE](004_mythic_beasts_dns/README.md)
1212
- 005: [Driving GitLab CI/CD pipelines with CUE](005_gitlab_ci/README.md)
13+
- 006: [Driving Buildkite pipelines with CUE](006_buildkite_importing_pipelines/README.md)
1314

1415
## Contributing
1516

0 commit comments

Comments
 (0)