CI/CD
Reference for blue-ci-cd.yml and the image presets Steve builds.
image: bluegit/base
unit:
stage: test
image: bluegit/node
script:
- node --version
plain:
stage: test
image: node:22
script:
- node --version
Pipeline
| Keyword | Meaning |
|---|---|
stages | Stage order. When omitted, stages follow the order in which jobs name them. A job without a stage uses test. |
variables | A string, or a value field. A job overrides the same name. Available in rules and in the container. |
image | Default image, as text or as a name field. default.image wins when that is set. A preset such as bluegit/base is allowed. |
default | image, before_script, after_script, tags, timeout, cache, retry and interruptible for jobs that do not set them. |
include | Only a local path. A job with the same name is merged and the outer file wins each field. |
before_script | Shell lines that run before script. |
after_script | Shell lines that run after script, also when script fails. |
cache | paths and a text key. Kept on this machine for one repository. |
workflow | rules, like a job. No match, or when never, creates no pipeline. |
jobs | Optional older map of jobs. Jobs may also sit at the top level, but not in both places. |
Jobs
| Keyword | Meaning |
|---|---|
stage | Stage name. Omitted means test. |
image | Default image, as text or as a name field. default.image wins when that is set. A preset such as bluegit/base is allowed. |
before_script | Shell lines that run before script. |
script | Shell lines. The job fails when a line fails. |
after_script | Shell lines that run after script, also when script fails. |
needs | Job names, or job and optional. An empty list starts at once. Omitted, the job waits for earlier stages. A need on a parallel job waits for every copy. |
rules | The first match wins. Fields are if, changes, exists and when. Without rules, only and except decide. |
only | A branch name, branches, tags, merge_requests, or a regex between slashes. |
except | The opposite of only. |
when | on_success is the default, also on_failure, always and manual. A rule may say never. |
allow_failure | A failure does not stop the rest. The job itself becomes a warning. |
artifacts | paths and expire_in. Units are seconds, minutes, hours, days, weeks, or never. |
cache | paths and a text key. Kept on this machine for one repository. |
resource_group | Jobs that share this name in one repository do not run at the same time. |
timeout | Replaces the server timeout for this job. The runner stops the container when the time is up. |
retry | A number, or max and when. max runs from 0 to 2 and a higher number is cut off. when names script_failure, runner_system_failure, job_execution_timeout, stuck_or_timeout_failure or always. |
parallel | A number up to 50, or a matrix. Numbered jobs are called name 1/N and receive CI_NODE_INDEX and CI_NODE_TOTAL. A matrix is the product of each list, then those products joined. Keys become variables. |
services | Sidecar containers on one network with the job. Text, or name, alias, command, entrypoint and variables. Without an alias the name drops the registry and the tag. There is no health check beyond a started container. |
environment | A name, or name and url. Sets the environment variables. There is no deployment and no stop job. |
interruptible | Off by default. A newer pipeline on the same ref cancels older jobs that set this and are still queued or running. |
doNotInterrupt | true or false (default false). A running job with doNotInterrupt is never stopped automatically: a newer pipeline on the same ref waits until it has finished, and manual cancel asks for confirmation (API: force=true). |
tags | Stored on the job. Steve runs every job. Tags do not choose a runner. |
extends | One name or a list. A job whose name starts with a dot is a template and does not run. |
Rules
The first matching rule wins. && binds more tightly than ||. Parentheses group an expression. An empty variable is null. == and != compare. =~ and !~ match a RE2 pattern. A slash in a pattern is escaped with a backslash. The flag i sits right after the closing slash. A pattern stored in a variable must be valid when the rule runs.
| Keyword | Meaning |
|---|---|
if | An expression. The first rule whose if, changes and exists all match is the winner. |
changes | Paths in the push diff. When that diff cannot be known, the condition matches. |
exists | Every listed path must be in the commit. When the tree is unknown, the condition matches. |
when | on_success is the default, also on_failure, always and manual. A rule may say never. |
Variables
| Keyword | Available | Meaning |
|---|---|---|
CI | Rules and container | Always true. |
CI_CONFIG_PATH | Rules and container | Path of the pipeline file. |
CI_SERVER_NAME | Rules and container | Always BlueGit. |
CI_COMMIT_SHA | Rules and container | Full commit id. |
CI_COMMIT_SHORT_SHA | Rules and container | First eight characters of the commit id. |
CI_COMMIT_REF | Rules and container | Full git ref. |
CI_COMMIT_REF_NAME | Rules and container | Branch or tag name. |
CI_COMMIT_REF_SLUG | Rules and container | Ref name made safe for a URL. |
CI_COMMIT_TITLE | Rules and container | Commit subject. |
CI_COMMIT_TIMESTAMP | Rules and container | Committer time in ISO 8601. |
CI_COMMIT_BRANCH | Rules and container | Branch name. Empty on a tag. |
CI_COMMIT_TAG | Rules and container | Tag name. Empty on a branch. |
CI_DEFAULT_BRANCH | Rules and container | Default branch of the repository. |
CI_PIPELINE_ID | Rules and container | Pipeline number. Zero while rules are evaluated. |
CI_PIPELINE_SOURCE | Rules and container | push, web, or merge_request_event. |
CI_PIPELINE_URL | Rules and container | Link to the pipeline. |
CI_PROJECT_PATH | Rules and container | Owner and repository. |
CI_PROJECT_NAME | Rules and container | Repository name. |
CI_PROJECT_NAMESPACE | Rules and container | Owner path. |
CI_PROJECT_URL | Rules and container | Link to the repository. |
CI_API_URL | Rules and container | Root of the API. |
CI_JOB_ID | Container only | Job number. |
CI_JOB_NAME | Container only | Job name. |
CI_JOB_STAGE | Container only | Stage name. |
CI_JOB_URL | Container only | Link to the job. |
CI_JOB_TOKEN | Container only | Token for this job. It is masked in the log. |
CI_NODE_INDEX | Container only | Index of this parallel or matrix job. |
CI_NODE_TOTAL | Container only | How many parallel or matrix jobs there are. |
CI_ENVIRONMENT_NAME | Container only | Environment name. |
CI_ENVIRONMENT_URL | Container only | Environment address. |
CI_MERGE_REQUEST_ID | Merge request | Internal id of the merge request. |
CI_MERGE_REQUEST_IID | Merge request | Number of the merge request in the project. |
CI_MERGE_REQUEST_TITLE | Merge request | Title of the merge request. |
CI_MERGE_REQUEST_SOURCE_BRANCH_NAME | Merge request | Source branch. |
CI_MERGE_REQUEST_SOURCE_BRANCH_SHA | Merge request | Commit of the source branch. |
CI_MERGE_REQUEST_TARGET_BRANCH_NAME | Merge request | Target branch. |
CI_MERGE_REQUEST_TARGET_BRANCH_SHA | Merge request | Tip of the target branch. |
CI_MERGE_REQUEST_PROJECT_PATH | Merge request | Project path of the merge request. |
CI_MERGE_REQUEST_PROJECT_URL | Merge request | Link to that project. |
Differences with GitLab
- The file is blue-ci-cd.yml, not the GitLab filename.
- There is one runner. Its name is runner.name and the default is Steve. tags do not select another runner.
- include only reads a local file. A project, remote or template include is ignored.
- pages, spec, trigger pipelines, child pipelines, coverage, id_tokens, inherit and hooks are ignored with a warning.
- services at the top level is ignored. Job services share a network and have no health check beyond a started container.
- environment only sets variables. There is no deployment and no stop job.
- retry max is capped at 2.
- parallel is capped at 50.
- The pipeline number is zero while rules run. Job, node and environment variables exist only in the container.
- A merge request pipeline runs on the source commit, not a merged tree. It starts when the request opens, on every push to the source, and again when someone pushes the target.
- cache stays on this machine and is not shared.
- An image name that matches a preset is built locally. Every other name is a normal Docker image.
Image presets
A job picks a preset with its image keyword. Steve builds that Dockerfile on first use and reuses the result. Any other name, such as node:22, stays a normal Docker image. Add or replace a preset under ci.presets in config.yml with image, dockerfile or from.
| Preset | Image | Contents |
|---|---|---|
bluegit/base |
alpine:3.20Alpine 3.20 |
bash, ca-certificates, curl, git |
bluegit/node |
node:22-alpineNode.js 22 |
bash, ca-certificates, curl, git |
bluegit/python |
python:3.12-alpinePython 3.12 |
bash, ca-certificates, curl, git |
bluegit/go |
golang:1.26-alpineGo 1.26 |
bash, ca-certificates, curl, git, git-daemon |
bluegit/rust |
rust:1.81-alpineRust 1.81 |
bash, ca-certificates, curl, git |
bluegit/java |
eclipse-temurin:21-jdk-alpineJava 21 |
bash, ca-certificates, curl, git |
bluegit/dotnet |
mcr.microsoft.com/dotnet/sdk:8.0.NET SDK 8 |
bash, ca-certificates, curl, git |
API
Everything the web interface can do is also available under /api/v1. The machine-readable description is the OpenAPI document.
Send the token as Authorization: Bearer, as the PRIVATE-TOKEN header, or as the password of HTTP basic authentication. Git over HTTP uses the same token. Scopes are api, read_api, read_repository, write_repository and runners (runner management only). api covers the whole API and git. A token can expire.
An SSH key can call the same handlers: ssh git@host api METHOD /api/v1/... with a JSON body on stdin. The exit status is 0 when the HTTP status is below 400. The numeric status is written to stderr.
Webhook payloads
An empty body template sends this JSON document. pipeline_success and pipeline_failure include the pipeline id, status, ref, sha, commit message, author and duration, and every job with its name, stage, status, duration, allow_failure, web URL and console output. Each log is the last 200 lines, at most 64 KiB. Secret values of at least 8 characters are replaced with [MASKED]. log_truncated is true when the log was cut. log_url is then present and is GET /api/v1/repos/{owner}/{repo}/jobs/{id}/log. That request needs Authorization: Bearer and a personal token that can read the repo. A push includes ref, before, after and up to 20 commits. Each commit has sha, message, author, timestamp, URL, changed files and diff_url. A file has status added, modified, removed or renamed, plus added and deleted line counts. The payload has no diff text. diff_url is GET /api/v1/repos/{owner}/{repo}/commits/{sha}/diff. A merge request includes id, title, description, source branch, target branch, status, author, action, reviewers, comments, up to 20 commits, diff statistics and diff_url, and no diff text. That diff_url is GET /api/v1/repos/{owner}/{repo}/merge-requests/{id}/diff. Both diff requests need Authorization: Bearer and a personal token that can read the repo. A comment action also includes the comment text. A release includes the tag, name, commit and assets with URLs.
{
"object_kind": "pipeline",
"type": "pipeline_failure",
"event_name": "pipeline_failure",
"project": "tim/app",
"actor": "tim",
"summary": "pipeline #26 voor tim/app is mislukt",
"url": "https://git.example/tim/app/-/pipelines/26",
"branch": "master",
"commit": "725e6947",
"pipeline_id": "26",
"pipeline": {
"id": 26,
"status": "failed",
"ref": "refs/heads/master",
"sha": "725e6947",
"commit_message": "fix de build",
"author": "tim",
"duration": 12.4,
"url": "https://git.example/tim/app/-/pipelines/26"
},
"jobs": [
{
"name": "test",
"stage": "test",
"status": "failed",
"duration": 4.1,
"allow_failure": false,
"web_url": "https://git.example/tim/app/-/pipelines/26/jobs/7",
"log": "go: token [MASKED]\nFAIL",
"log_truncated": true,
"log_url": "https://git.example/api/v1/repos/tim/app/jobs/7/log"
},
{
"name": "build",
"stage": "build",
"status": "success",
"duration": 8,
"allow_failure": false,
"web_url": "https://git.example/tim/app/-/pipelines/26/jobs/8",
"log": "built bluegit",
"log_truncated": false
}
]
}
{
"object_kind": "push",
"type": "push",
"event_name": "push",
"project": "tim/app",
"actor": "tim",
"branch": "master",
"ref": "refs/heads/master",
"before": "0000000000000000000000000000000000000000",
"after": "abc123",
"commits": [
{
"sha": "abc123",
"message": "fix de build",
"author": "Tim \u003ctim@example.com\u003e",
"timestamp": "2026-10-22T12:00:00Z",
"url": "https://git.example/tim/app/-/commit/abc123",
"files": [
{
"path": "README.md",
"status": "modified",
"additions": 1,
"deletions": 1
}
],
"diff_url": "https://git.example/api/v1/repos/tim/app/commits/abc123/diff"
}
]
}
{
"object_kind": "merge_request",
"type": "merge_request",
"event_name": "merge_request",
"project": "tim/app",
"actor": "ada",
"merge_request": {
"id": 4,
"number": 4,
"title": "Toon de kloon-URL",
"description": "SSH en HTTPS.",
"source_branch": "feature",
"target_branch": "master",
"status": "open",
"author": "ada",
"action": "opened",
"url": "https://git.example/tim/app/-/merge-requests/4",
"reviewers": [
{
"username": "tim",
"status": "approved"
}
],
"comments": [],
"commits": [
{
"sha": "abc123",
"message": "fix de build",
"author": "Tim \u003ctim@example.com\u003e",
"timestamp": "2026-10-22T12:00:00Z",
"url": "https://git.example/tim/app/-/commit/abc123",
"files": [
{
"path": "README.md",
"status": "modified",
"additions": 1,
"deletions": 1
}
],
"diff_url": "https://git.example/api/v1/repos/tim/app/commits/abc123/diff"
}
],
"diff_stats": {
"additions": 1,
"deletions": 1,
"files": [
{
"path": "README.md",
"status": "modified",
"additions": 1,
"deletions": 1
}
]
},
"diff_url": "https://git.example/api/v1/repos/tim/app/merge-requests/4/diff"
}
}
{
"object_kind": "release",
"type": "release_published",
"event_name": "release_published",
"project": "tim/app",
"commit": "abc123",
"release_tag": "v1.2.0",
"release": {
"tag": "v1.2.0",
"name": "BlueGit 1.2.0",
"commit": "abc123",
"assets": [
{
"name": "bluegit",
"url": "https://git.example/tim/app/-/releases/v1.2.0/assets/bluegit",
"size": 8123456
}
]
}
}
Issues
Each repository has an Issues tab. Readonly may create issues, like a GitLab Guest: create, comment, edit and close their own issue, and set confidential when creating it. Reporter may label and assign, set a milestone and a due date, and close, reopen or edit any issue. A Developer has the same issue rights as a Reporter and can still push code. A Maintainer creates the label and milestone lists and can delete an issue. Anonymous visitors of a public repository can read issues, except confidential ones. Those stay hidden unless you wrote the issue or you are a Reporter or higher. Issues are numbered per repository, #1 then #2. In Markdown, #12 becomes a link to that issue and !5 becomes a link to that merge request, except inside code. Closes #12, Fixes #12, Resolves #12 or Implements #12 in a merge request description or in its commits closes the issue when the merge request is merged. The same words in a commit on the default branch close the issue on push. You can start a new issue from a merge request comment, or as a related issue from another issue. The API uses a personal token: GET and POST /api/v1/repos/{owner}/{repo}/issues, GET and PUT /api/v1/repos/{owner}/{repo}/issues/{number}, POST .../notes, and GET and POST .../labels and .../milestones. The issue webhook has object_kind issue and action opened, updated, closed, reopened or commented. The body includes title, description, status, confidential, author, assignees, labels, milestone and due date. A commented action also includes the comment text.
{
"object_kind": "issue",
"type": "issue",
"event_name": "issue_opened",
"project": "tim/app",
"actor": "ada",
"summary": "issue #12 opened: Inloggen faalt",
"url": "https://git.example/tim/app/-/issues/12",
"issue": {
"id": 12,
"number": 12,
"title": "Inloggen faalt",
"description": "Zie #3 en !4.",
"status": "open",
"confidential": false,
"author": "ada",
"assignees": [
"tim"
],
"labels": [
"bug"
],
"milestone": "v1",
"due_date": "2026-11-01",
"action": "opened",
"url": "https://git.example/tim/app/-/issues/12"
}
}
Templates and task lists
Description templates live on the default branch. Put Markdown files in .bluegit/merge_request_templates and .bluegit/issue_templates. Default.md fills an empty description. The create form has a template menu, and choosing one replaces the description. The same names under .gitlab/ are a fallback, and a .bluegit file wins when both exist. When neither directory is present the form stays empty, with no template menu and no message. A merge request template may use %{source_branch}, %{target_branch}, %{first_commit}, %{all_commits}, %{co_authored_by}, %{first_multiline_commit} and %{closes_issue}. Those are filled in when the merge request is created. %{closes_issue} becomes Closes #12 when the source branch starts with a number and a hyphen, such as 12-fix. Up to 100 commits are used. Merge commits and very long messages are left out. Quick actions on an issue or an issue comment run on save when you may triage: /label ~name, /unlabel ~name, /assign @user, /unassign @user, /milestone %name, /due YYYY-MM-DD and /confidential. A line you may not run stays in the text. Anyone who may create an issue may use /confidential while creating it. Labels and milestones must already exist. Lines that start with - [ ] or - [x], or the same with a star, render as checkboxes in merge requests, issues and comments. With permission to edit that text you can click a checkbox and the source Markdown is updated. A list that contains tasks shows how many are done, for example 1 of 2 tasks completed. Preview and the repository README show the boxes without writing.
Link previews
BlueGit adds Open Graph, Twitter Card and oEmbed tags so a public link can show a card in chat. A repository card shows the name, the owner, the description or the first paragraph of the README, the main language, the stars, the latest commit, the avatar and a theme color. A merge request card shows the title, the status, the source and target branches, the author, the diff size, the pipeline status and the start of the description. An issue card shows the title, the status, the labels, the author and the start of the description. A group or user card shows the name, the description, the avatar and the number of repositories. Commits, pipelines, releases and files have their own cards. A file link accepts a line range as #L10-20 or as the L query, and that range is drawn as a code snippet. Each card image is a PNG drawn from SVG on the server, then cached, with an icon for the page type. The site name is BlueGit and the favicon stays the logo. A private or internal repository or group, and a confidential issue, always uses one generic card: BlueGit – inloggen vereist. The card is built as an anonymous visitor, even when the request sends a session cookie, and it never includes the name, the description or the README.