In this section, we'll explore how this gateway can easily be used to implement CI/CD pipelines.
We do begin with a fair bit of background. If you're in a hurry, you might want to skip to the CI/CD Recipe section.
When implementing CI/CD, the GitHub Checks API is of particular significance. A check is some assertion that can be made upon a code base. Examples would include executing a battery of tests or subjecting code to some kind of static analysis such as linting rules. A check suite, as one can infer from the name, is a collection of checks.
Provided you subscribed to them when you set up your
GitHub App, GitHub will
send a check_suite
webhook with action requested
to your gateway anytime new
commits are pushed directly to a repository into which your GitHub App is
installed. This intuitively makes good sense: When GitHub becomes aware of new
code, it requests for that code be validated.
Often, however, code is not committed directly to a given repository.
Typically, pull requests are opened instead, with a source branch belonging to a
fork since most projects, rightfully, do not permit untrusted contributors to
push code directly to their repository. For such cases, GitHub does not
automatically send a check_suite
webhook to your gateway. There is sound
rationale for this: If a contributor lacks permissions to push code directly to
a given repository, then their contributions cannot be trusted to an extent that
they can safely be tested automatically. After all, malicious modifications to
the code or tests could put a software project at great risk. Imagine, for
instance, if any random person on the internet could hijack your CI pipelines to
steal project secrets or mine crypto coins.
To recap: New PRs with a source branch belonging to a fork do not
automatically result in check_suite
webhooks being sent to your gateway. In
such cases, however, provided you subscribed to them when you set up your
GitHub App, pull_request
webhooks with action opened
are sent.
When this gateway receives a pull_request
webhook with a value of opened
,
reopened
, or synchronized
in the JSON payload's action
field, it further
scrutinizes the JSON payload to determine the PR author's relationship to the
target repository. If the author is determined to be a trusted contributor (an
OWNER
of the repository, for instance), the gateway uses the GitHub Checks API
to create a check suite and request that a check_suite
webhook with action
rerequested
be sent along to the gateway. We call this process check suite
forwarding. If the PR author is determined not to be a trusted contributor,
then this does not occur.
⚠️ For check suite forwarding to work for new/updated PRs your GitHub App should be subscribed topull_request
webhooks, but there is no need for Brigade projects to subscribe topull_request:opened
,pull_request:reopened
, orpull_request:synchronized
events. Check suite forwarding is purely a function of the gateway and individual projects do not need to do anything to enable it.
In cases where no check suite forwarding occurred, a trusted contributor may
review the PR and, if they deem it safe, can comment either /brig run
or
/brig check
. Provided you subscribed to them when you set up your GitHub App,
this results in an issue_comment
webhook with action created
being sent to
the gateway.
The check suite forwarding process described above for pull_request
webhooks
also applies to issue_comment
webhooks. If an issue_comment
webhook with an
action
value of created
is received by this gateway and scrutiny of the
webhook's JSON payload reveals the comment author is a trusted contributor, then
the check suite forwarding process proceeds as if the comment author had
authored the PR themselves.
⚠️ For check suite forwarding to work for/brig run
or/brig check
comments, your GitHub App should be subscribed toissue_comment
webhooks, but there is no need for Brigade projects to subscribe toissue_comment:created
events. Check suite forwarding is purely a function of the gateway and individual projects do not need to do anything to enable it.
In any cases where the gateway emits a check_suite:requested
or
check_suite:rerequested
event into Brigade's event bus (regardless of whatever
check suite forwarding may or may not have been involved in getting to that
point), the gateway will also monitor the status of all jobs associated with
those events and utilize the Checks API to report results back to GitHub. In
this way, the result of every such job becomes the result of a single
corresponding check in the corresponding check suite. This is how Brigade job
results and logs become viewable in the GitHub web UI.
In the event that any individual job fails, the corresponding check can be
re-run by an authorized user via GitHub's web UI. This results in a check_run
webhook with action rerequested
being sent to the gateway and a
check_run:requested
event being emitted into Brigade's event bus. This permits
Brigade projects to subscribe to and handle requests to re-run a single job.
When an authorized user creates a new release or makes a release draft public
using the GitHub web UI, and provided you subscribed to it when setting up your
GitHub App, GitHub will send a release
webhook with action published
to your
gateway. Brigade projects may wish to subscribe to the corresponding
release:published
event to trigger their continuous delivery/deployment
pipelines.
For convenience, Brigade emits a second event for many of the scenarios discussed above. The intent behind this is to distill the many nuanced details of those scenarios into a small and consistently named set of event types that can be readily understood.
Since script authors rarely, if ever, need to differentiate between a
check_suite:requested
event and a check_suite:rerequested
event, when
emitting either of these into Brigade's event bus, this gateway also emits a
ci:pipeline_requested
event. Apart from effectively collapsing two similarly
named and nearly identical events into one, the name ci:pipeline_requested
very clearly denotes exactly what any subscribed project's script should do to
handle such an event -- namely, run the CI pipeline. Better still, it eliminates
any potential confusion arising from questions like, "What even is a check
suite?" Since the gateway can handle such things all on its own, it is perhaps
better for Brigade's end-users not to get bogged down in such details and simply
focus on the fact that a ci:pipeline_requested
event means CI should run.
Again, to help end-users avoid getting bogged down in the complexities of things
such as the GitHub Checks API, when emitting a check_run:rerequested
event,
this gateway will also emit a ci:job_requested
. Again, this name clearly
denotes exactly what any subscribed project's script should do to handle such an
event -- namely, run some discreet segment of the CI pipeline. As an added
convenience, ci:job_requested
events have a job
label that indicates which
specific job is to be re-run. This spares script authors from digging into the
event payload to make this determination.
Last, and primarily for consistency with the ci:pipeline_requested
and
ci:job_requested
names, when emitting a release:published
event, this
gateway will also emit a cd:pipeline_requested
event. Once again, this name
clearly denotes exactly what any subscribed project's script should do in
response to such an event. As an added convenience, cd:pipeline_requested
have
a release
label that indicates the release name. This spares script authors
from inferring this information themselves by digging into the event payload or
other event details.
⚠️ These custom events are emitted in addition to the original events; not instead of. This preserves the flexibility for script authors to "drop down" to the original events if/when necessary. Script authors should take care not to subscribe to the original events and their corresponding custom events, as the net effect would be that every singular logical event would be received and processed twice.
To summarize, the existence of the custom events discussed in this section means that script authors who are concerned only with CI/CD need only concern themselves with the following three events:
ci:pipeline_requested
ci:job_requested
cd:pipeline_requested
This section presents a reliable CI/CD recipe for Brigade and the GitHub Gateway.
Our project definition only needs to subscribe to three specific events:
apiVersion: brigade.sh/v2
description: A CI/CD example
kind: Project
metadata:
id: ci-cd-example
spec:
eventSubscriptions:
- source: brigade.sh/github
qualifiers:
repo: brigadecore/ci-cd-example
types:
- ci:pipeline_requested
- ci:job_requested
- cd:pipeline_requested
workerTemplate:
git:
cloneURL: https://github.com/brigadecore/ci-cd-example.git
In a first iteration of our script, we define how to handle two of the three events to which we subscribed:
import { events, Event, Job, ConcurrentGroup, Container } from "@brigadecore/brigadier"
events.on("brigade.sh/github", "ci:pipeline_requested", async event => {
// Chain some jobs together to implement CI. For example:
await new ConcurrentGroup(
// For brevity, we're omitting the definitions of each job.
testJob0,
testJob1,
// ...,
testJobN
).run()
})
events.on("brigade.sh/github", "cd:pipeline_requested", async event => {
// Chain some jobs together to implement CD. For example:
await new ConcurrentGroup(
// For brevity, we're omitting the definitions of each job.
releaseJob0,
releaseJob1,
// ...,
releaseJobN,
).run()
}
events.process()
Unaccounted for in the first iteration of our script are ci:job_requested
events which indicate that a specific job should be re-run. Modifying the
previous script slightly, we can account for such events. The strategy makes use
of a map of job factory functions indexed by name:
import { events, Event, Job, ConcurrentGroup, Container } from "@brigadecore/brigadier"
// A map of job factory functions indexed by name. When a ci:job_requested
// event wants to re-run a single job, this allows us to easily find it.
const jobs: {[key: string]: (event: Event) => Job } = {}
const testJob0Name = "testJob0"
const testJob0 = (event: Event) => {
return new Job(testJob0Name, "some/image:tag", event)
}
jobs[testJob0Name] = testJob0
// Remaining job factory function definitions are omitted for brevity
// ...
events.on("brigade.sh/github", "ci:pipeline_requested", async event => {
// Chain some jobs together to implement CI. For example:
await new ConcurrentGroup(
testJob0(event),
testJob1(event),
// ...,
testJobN(event)
).run()
})
events.on("brigade.sh/github", "ci:job_requested", async event => {
// Starting with Brigade/brigadier v2.2.0, the job name can be found in a
// label. Prior to that, an event's labels were not accessible via script.
const job = jobs[event.labels.job]
if (job) {
await job(event).run()
return
}
throw new Error(`No job found with name: ${event.labels.job}`)
})
events.on("brigade.sh/github", "cd:pipeline_requested", async event => {
// Chain some jobs together to implement CD. For example:
await new ConcurrentGroup(
releaseJob0(event),
releaseJob1(event),
// ...,
releaseJobN(event),
).run()
}
events.process()