Skip to content

About

Posts the state of a CI workflow task to GitHub

Resources

Stars

52 stars

Watchers

0 watching

Forks

Repository files navigation

ci-github-notifier

A lightweight container to post the status of a CI task to GitHub, allowing GitHub users to see the status of a PR or Branch. Designed for cloud native workflows (eg Argo Workflows, or Tekton), but will run wherever a container can be run. Compatible with regular Github and Github Enterprise URLs.

CI Code Quality Release

Environment Variables

We pass key information to the container using environment variables. First, we provide the necessary values for the GitHub status API:

Environment Variable Type Description
state string The state of the status. Can be one of pending, success, error, or failure (Github Docs).
target_url string The target URL to associate with this status. This URL will be linked from the GitHub UI to allow users to easily see the ‘source’ of the Status.
description string A short description of the status.
context string A string label to differentiate this status from the status of other systems.

Then we provide GitHub authentication information, as either a personal access token or GitHub App credentials.

For a personal access token, provide one of the following:

Environment Variable Type Description
access_token string (Optional if tokenFile is set): A GitHub Access Token for a user with push access
tokenFile string (Optional if access_token is set): Path to a file within the container that contains the GitHub access token. Useful for Vault secrets injection or similar. access_token takes precedence over this

For a GitHub App, provide app_id and one of the two key variables. See Authenticating as a GitHub App below:

Environment Variable Type Description
app_id string The GitHub App's ID, from the App's settings page
app_private_key string (Optional if app_private_key_file is set): The App's private key, base64 encoded. Encoding keeps the multi-line PEM out of environment variables. Takes precedence over app_private_key_file
app_private_key_file string (Optional if app_private_key is set): Path to a file within the container holding the App's private key as a raw PEM. Useful for mounted secrets or Vault injection

When app_id and a key are both set, App credentials are used and access_token/tokenFile are ignored. Setting only one of app_id and a key is an error rather than a silent fall back to the token, so a half-finished migration fails loudly instead of quietly using the old credential.

By default the commit statuses API is used. To post a check run instead, which requires GitHub App credentials, set:

Environment Variable Type Description
api string (OPTIONAL) statuses (default) or checks. See Posting check runs below
check_run_id string (OPTIONAL, checks only) The ID of an existing check run to update. When unset a new check run is created
check_run_id_file string (OPTIONAL, checks only) Path to write the new check run's ID to, so a later pipeline step can pass it back as check_run_id

Finally we provide Environment Variables that make up the values of the GitHub API url:

Environment Variable Type Description
organisation string The GitHub organisation/username for the notification. e.g. given https://github.com/crumbhole/ci-github-notifier, the organisation is "crumbhole"
app_repo string The GitHub repo for the notification. e.g. given https://github.com/crumbhole/ci-github-notifier, the app_repo is "ci-github-notifier"
git_sha string The SHA1 of the PR or branch you wish to notify
gh_url string (OPTIONAL) The URL of the GitHub API. If omitted, will default to api.github.com

Docker run examples

You are unlikely to want to run these in production using docker run, but the following examples give a clear indication of how to execute the container.

Running with environment variables

docker run \
    -e state=pending \
    -e target_url=https://sendible.com \
    -e description="This is an example description" \
    -e context="Example context" \
    -e access_token="123ABC123ABC" \
    -e organisation=crumbhole \
    -e app_repo="ci-github-notifier" \
    -e git_sha="123abc123abc" \
    -e gh_url="api.mydomain.biz" \
    ghcr.io/crumbhole/ci-github-notifier:stable

Mounting tokenFile

docker run \
    -e state=pending \
    -e target_url=https://sendible.com \
    -e description="This is an example description" \
    -e context="Example context" \
    -e tokenFile="/tmp/access_token" \
    -e organisation=crumbhole \
    -e app_repo="ci-github-notifier" \
    -e git_sha="123abc123abc" \
    -e gh_url="api.mydomain.biz" \
    -v /path/to/file:/tmp/access_token \
    ghcr.io/crumbhole/ci-github-notifier:stable

Authenticating as a GitHub App

A personal access token belongs to a user, expires, and grants whatever that user can reach. A GitHub App is scoped to the repositories it is installed on and the permissions it was granted, and the token it uses lasts an hour. When app_id and a private key are supplied, ci-github-notifier mints one of those short-lived installation tokens itself, so nothing long-lived has to be stored in your CI system.

Setting one up:

  1. Create a GitHub App (organisation settings, or your user settings for a personal repo). It needs no webhook and no callback URL.
  2. Under Repository permissions, grant Commit statuses: Read and write. Nothing else is required to post statuses.
  3. Install the App on the repositories you want to post statuses to. The tool looks the installation up from organisation and app_repo, so there is no installation ID to configure.
  4. Generate a private key. GitHub downloads a .pem file.
  5. Note the App ID from the App's settings page.

Supply the key as a mounted file:

docker run \
    -e state=pending \
    -e target_url=https://argo-workflows.mydomain.biz \
    -e description="This is an example description" \
    -e context="Example context" \
    -e app_id="123456" \
    -e app_private_key_file="/secrets/app.pem" \
    -e organisation=crumbhole \
    -e app_repo="ci-github-notifier" \
    -e git_sha="123abc123abc" \
    -v /path/to/app.pem:/secrets/app.pem \
    ghcr.io/crumbhole/ci-github-notifier:stable

Or base64 encoded, which avoids a volume mount when your CI passes everything as parameters:

export app_private_key=$(base64 -w0 < app.pem)

If the App is not installed on the repository you name, the run fails with a message saying so rather than a bare HTTP error.

The installation token is requested for the single repository named by organisation/app_repo and for one permission only — statuses: write, or checks: write when api=checks — rather than for everything the App holds. If the App is installed across an organisation, the token this tool uses still reaches only the one repository it is posting to.

Posting check runs

Commit statuses are keyed by commit and context, so posting pending and later success against the same context replaces the first with the second. Check runs are not: they are objects with their own IDs, and posting twice creates two of them. A pipeline that reports progress therefore has to carry the ID from the first call into the second.

Set check_run_id_file on the first call and check_run_id on the later one:

# At the start of the build
check_run_id_file=/tmp/check_run_id state=pending ... ci-github-notifier

# At the end, updating the same check run rather than creating a second
check_run_id=$(cat /tmp/check_run_id) state=success ... ci-github-notifier

The ID is also printed to stdout. In Argo Workflows the file is read back as an output parameter; see examples/argo-workflows/check-run-example.yml.

The image ships an empty, writable /tmp for this file. If you run it with a read-only root filesystem, mount an emptyDir (or similar) there, or point check_run_id_file at another writable path. If the file can't be written the run exits non-zero after the check run was created, so a retry would create a second one.

Check runs are only writable by GitHub Apps — a personal access token cannot create one, whatever its scopes. In checks mode the installation token is minted with checks: write rather than statuses: write, so the App needs Checks: Read and write in addition to, or instead of, Commit statuses. api=checks without app_id and a private key fails at startup rather than letting GitHub reject the request.

The tool's four states map onto the check run API's split of status and conclusion:

state status conclusion
pending in_progress -
success completed success
failure completed failure
error completed failure

context becomes the check run's name, target_url becomes details_url, and description becomes output.summary.

Argo Workflows example

A simple Argo Workflows template can be found in the examples directory. To post notifications without starting a pod for each one, see Argo Workflows executor plugin below. simple-example.yml uses a personal access token; github-app-example.yml authenticates as a GitHub App with the private key mounted from a Kubernetes secret. check-run-example.yml posts a check run and threads its ID from the pending report through to the final result.

Argo Workflows executor plugin

Each notification in the examples above starts a pod. The same image can instead run as an Argo Workflows executor plugin: a sidecar in each workflow's agent pod that posts a notification each time a plugin: template runs, without starting a pod for it. The files are in examples/argo-workflows/executor-plugin/. It needs Argo Workflows v3.3.1 or later, and has been tested with v4.0.5.

A notification is a template with the same parameters as the environment variables above:

- name: github-notifier
  plugin:
    ci-github-notifier:
      state: pending
      target_url: "https://argo-workflows.mydomain.biz/workflows/{{workflow.namespace}}/{{workflow.name}}"
      description: Build running
      context: ci/build
      organisation: crumbhole
      app_repo: ci-github-notifier
      git_sha: "{{workflow.parameters.git_sha}}"

The accepted parameters are state, target_url, description, context, organisation, app_repo, git_sha, api and check_run_id. Any other key fails the step. That covers typos, and also the credentials and gh_url: those are configured once on the plugin, so a workflow cannot send the credentials to another host. A failed notification fails its step, and the step's message says why.

With api: checks the step outputs the check run's ID as the check_run_id output parameter, and a later step passes it back to update the same check run. Declare the output on the template as valueFrom: {supplied: {}} so Argo lets later steps refer to it. check-run-example.yml shows the whole thing.

Installing

  1. Enable executor plugins on the workflow controller by setting ARGO_EXECUTOR_PLUGINS=true in its environment.

  2. Install the plugin, in the Argo namespace to make it available to every namespace, or in a single workflow namespace:

    kubectl -n argo apply -f examples/argo-workflows/executor-plugin/ci-github-notifier-executor-plugin-configmap.yaml
    

    The ConfigMap is generated from plugin.yaml by argo executor-plugin build. To change the plugin, edit plugin.yaml and rebuild the ConfigMap rather than editing the ConfigMap.

  3. Create the credentials in every namespace that runs workflows using the plugin: the sidecar runs in the workflow's own namespace, so that is where its secret is looked up. For a GitHub App:

    kubectl -n <namespace> create secret generic ci-github-notifier \
        --from-literal=app_id=123456 \
        --from-literal=app_private_key="$(base64 -w0 < app.pem)"
    

    The key has to be the base64 app_private_key form, because a plugin sidecar cannot mount a secret as a file. To use a personal access token, change the env in plugin.yaml to an access_token variable and rebuild. For GitHub Enterprise, set gh_url there as well.

  4. Give workflows a service account the agent can run as. rbac.yaml creates one, with the agent's permissions, the permission the workflow's other pods need, and the long-lived token secret that the agent pod mounts. That secret is easy to miss: without it the agent pod waits in Init forever, failing to mount it. Apply it in each workflow namespace and set serviceAccountName: ci-github-notifier-agent on the workflow.

Development

Go sources are linted with golangci-lint against .golangci.yaml. The same configuration runs locally and in CI, so a clean make lint means a clean CI lint job.

make lint    # golangci-lint run ./...
make build   # lint, then go build ./...
make test    # go test ./...

make lint needs golangci-lint v2 on your PATH; CI pins the version it installs in .github/workflows/ci.yaml. Keep the two in step when bumping.

About

Posts the state of a CI workflow task to GitHub

Resources

Stars

52 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages