Skip to content

Commit 492de86

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 492de86

File tree

2 files changed

+375
-0
lines changed

2 files changed

+375
-0
lines changed

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+
- XXX: [Driving Buildkite pipelines with CUE](XXX_buildkite_importing_pipelines/README.md)
1314

1415
## Contributing
1516

Diff for: XXX_buildkite_importing_pipelines/README.md

+374
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,374 @@
1+
# Driving Buildkite Pipelines with CUE
2+
<sup>by [Jonathan Matthews](https://jonathanmatthews.com)</sup>
3+
4+
This guide explains how to convert Buildkite pipeline files from YAML to CUE,
5+
check those pipelines are valid, and then use CUE's tooling layer to regenerate
6+
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 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 Buildkite pipeline files. The examples shown in this guide
18+
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 Buildkite
34+
pipeline files, and ensure you start this process with a clean git state, with
35+
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/buildkite.pipeline.schema.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+
#### :arrow_right: Create a CUE tool file
210+
211+
Create a CUE "tool" file at `internal/ci/buildkite/ci_tool.cue` and adapt the
212+
element commented with `TODO`:
213+
214+
:floppy_disk: `internal/ci/buildkite/ci_tool.cue`
215+
```CUE
216+
package buildkite
217+
218+
import (
219+
"path"
220+
"encoding/yaml"
221+
"tool/file"
222+
)
223+
224+
_goos: string @tag(os,var=os)
225+
226+
// Regenerate all pipeline files
227+
command: regenerate: {
228+
pipeline_files: {
229+
// TODO: update _toolFile to reflect the directory hierarchy containing this file.
230+
let _toolFile = "internal/ci/buildkite/ci_tool.cue"
231+
let _pipelineDir = path.FromSlash(".buildkite", path.Unix)
232+
let _donotedit = "Code generated by \(_toolFile); DO NOT EDIT."
233+
234+
clean: {
235+
glob: file.Glob & {
236+
glob: path.Join([_pipelineDir, "*.yml"], _goos)
237+
files: [...string]
238+
}
239+
for _, _filename in glob.files {
240+
"Delete \(_filename)": file.RemoveAll & {path: _filename}
241+
}
242+
}
243+
244+
create: {
245+
for _pipelineName, _pipeline in pipelines
246+
let _filename = _pipelineName + ".yml" {
247+
"Generate \(_filename)": file.Create & {
248+
$after: [ for v in clean {v}]
249+
filename: path.Join([_pipelineDir, _filename], _goos)
250+
contents: "# \(_donotedit)\n\n\(yaml.Marshal(_pipeline))"
251+
}
252+
}
253+
}
254+
}
255+
}
256+
```
257+
258+
Make the modification indicated by the `TODO` comment.
259+
260+
This tool will export each CUE-based pipeline back into its required YAML file,
261+
on demand.
262+
263+
#### :arrow_right: Test the CUE tool file
264+
265+
With the modified `ci_tool.cue` file in place, check that the `regenerate`
266+
command is available **from a shell sitting at the repo root**. For example:
267+
268+
:computer: `terminal`
269+
```sh
270+
cd $(git rev-parse --show-toplevel) # make sure we're sitting at the repository root
271+
cue help cmd regenerate ./internal/ci/buildkite # the "./" prefix is required
272+
```
273+
274+
Your output **must** begin with the following:
275+
276+
```text
277+
Regenerate all pipeline files
278+
Usage:
279+
cue cmd regenerate [flags]
280+
[... output continues ...]
281+
```
282+
283+
| :exclamation: WARNING :exclamation: |
284+
|:--------------------------------------- |
285+
| 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.
286+
287+
#### :arrow_right: Regenerate the YAML pipeline files
288+
289+
Run the `regenerate` command to produce YAML pipeline files from CUE. For
290+
example:
291+
292+
:computer: `terminal`
293+
```sh
294+
cue cmd regenerate ./internal/ci/buildkite # the "./" prefix is required
295+
```
296+
297+
#### :arrow_right: Audit changes to the YAML pipeline files
298+
299+
Check that each YAML pipeline file has a single *material* change from the
300+
original:
301+
302+
:computer: `terminal`
303+
```sh
304+
git diff .buildkite/
305+
```
306+
307+
Your output should look similar to the following example:
308+
309+
```diff
310+
diff --git a/.buildkite/pipeline.deploy.yml b/.buildkite/pipeline.deploy.yml
311+
index 4af2b9d..e0fc010 100644
312+
--- a/.buildkite/pipeline.deploy.yml
313+
+++ b/.buildkite/pipeline.deploy.yml
314+
@@ -1,6 +1,8 @@
315+
+# Code generated by internal/ci/buildkite/ci_tool.cue; DO NOT EDIT.
316+
+
317+
steps:
318+
- command: echo 'Deploy'
319+
diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml
320+
index 7446201..10cefad 100644
321+
--- a/.buildkite/pipeline.yml
322+
+++ b/.buildkite/pipeline.yml
323+
@@ -1,12 +1,14 @@
324+
+# Code generated by internal/ci/buildkite/ci_tool.cue; DO NOT EDIT.
325+
+
326+
steps:
327+
- command: echo 'Tests'
328+
label: ':hammer:'
329+
- wait
330+
- trigger: dependent-pipeline-example-deploy
331+
```
332+
333+
The only *material* change in each YAML file is the addition of a header that
334+
warns the reader not to edit the file directly.
335+
336+
#### :arrow_right: Add and commit files to git
337+
338+
Add your files to git. For example:
339+
340+
:computer: `terminal`
341+
```sh
342+
git add .buildkite/ internal/ci/buildkite/ cue.mod/module.cue
343+
```
344+
345+
Make sure to include your slightly modified YAML pipeline files in
346+
`.buildkite/` along with all the new files in `internal/ci/buildkite/` and
347+
your `cue.mod/module.cue` file.
348+
349+
Commit your files to git, with an appropriate commit message:
350+
351+
:computer: `terminal`
352+
```sh
353+
git commit -m "ci: create CUE sources for Buildkite pipelines"
354+
```
355+
356+
## Conclusion
357+
358+
**Well done - your Buildkite pipeline files have been imported into CUE!**
359+
360+
They can now be managed using CUE, leading to safer and more predictable
361+
changes. The use of a schema to check your pipelines means that you
362+
will catch and fix many types of mistake earlier than before, without waiting
363+
for the slow "git add/commit/push; check if CI fails" cycle.
364+
365+
From now on, each time you make a change to a CUE pipeline file, immediately
366+
regenerate the YAML files required by Buildkite, and commit your changes
367+
to all the CUE and YAML files. For example:
368+
369+
:computer: `terminal`
370+
```sh
371+
cue cmd regenerate ./internal/ci/buildkite/ # the "./" prefix is required
372+
git add .buildkite/ internal/ci/buildkite/
373+
git commit -m "ci: added new release pipeline" # example message
374+
```

0 commit comments

Comments
 (0)