BlueGit

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

KeywordMeaning
stagesStage order. When omitted, stages follow the order in which jobs name them. A job without a stage uses test.
variablesA string, or a value field. A job overrides the same name. Available in rules and in the container.
imageDefault image, as text or as a name field. default.image wins when that is set. A preset such as bluegit/base is allowed.
defaultimage, before_script, after_script, tags, timeout, cache, retry and interruptible for jobs that do not set them.
includeOnly a local path. A job with the same name is merged and the outer file wins each field.
before_scriptShell lines that run before script.
after_scriptShell lines that run after script, also when script fails.
cachepaths and a text key. Kept on this machine for one repository.
workflowrules, like a job. No match, or when never, creates no pipeline.
jobsOptional older map of jobs. Jobs may also sit at the top level, but not in both places.

Jobs

KeywordMeaning
stageStage name. Omitted means test.
imageDefault image, as text or as a name field. default.image wins when that is set. A preset such as bluegit/base is allowed.
before_scriptShell lines that run before script.
scriptShell lines. The job fails when a line fails.
after_scriptShell lines that run after script, also when script fails.
needsJob 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.
rulesThe first match wins. Fields are if, changes, exists and when. Without rules, only and except decide.
onlyA branch name, branches, tags, merge_requests, or a regex between slashes.
exceptThe opposite of only.
whenon_success is the default, also on_failure, always and manual. A rule may say never.
allow_failureA failure does not stop the rest. The job itself becomes a warning.
artifactspaths and expire_in. Units are seconds, minutes, hours, days, weeks, or never.
cachepaths and a text key. Kept on this machine for one repository.
resource_groupJobs that share this name in one repository do not run at the same time.
timeoutReplaces the server timeout for this job. The runner stops the container when the time is up.
retryA 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.
parallelA 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.
servicesSidecar 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.
environmentA name, or name and url. Sets the environment variables. There is no deployment and no stop job.
interruptibleOff by default. A newer pipeline on the same ref cancels older jobs that set this and are still queued or running.
doNotInterrupttrue 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).
tagsStored on the job. Steve runs every job. Tags do not choose a runner.
extendsOne 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.

KeywordMeaning
ifAn expression. The first rule whose if, changes and exists all match is the winner.
changesPaths in the push diff. When that diff cannot be known, the condition matches.
existsEvery listed path must be in the commit. When the tree is unknown, the condition matches.
whenon_success is the default, also on_failure, always and manual. A rule may say never.

Variables

KeywordAvailableMeaning
CIRules and containerAlways true.
CI_CONFIG_PATHRules and containerPath of the pipeline file.
CI_SERVER_NAMERules and containerAlways BlueGit.
CI_COMMIT_SHARules and containerFull commit id.
CI_COMMIT_SHORT_SHARules and containerFirst eight characters of the commit id.
CI_COMMIT_REFRules and containerFull git ref.
CI_COMMIT_REF_NAMERules and containerBranch or tag name.
CI_COMMIT_REF_SLUGRules and containerRef name made safe for a URL.
CI_COMMIT_TITLERules and containerCommit subject.
CI_COMMIT_TIMESTAMPRules and containerCommitter time in ISO 8601.
CI_COMMIT_BRANCHRules and containerBranch name. Empty on a tag.
CI_COMMIT_TAGRules and containerTag name. Empty on a branch.
CI_DEFAULT_BRANCHRules and containerDefault branch of the repository.
CI_PIPELINE_IDRules and containerPipeline number. Zero while rules are evaluated.
CI_PIPELINE_SOURCERules and containerpush, web, or merge_request_event.
CI_PIPELINE_URLRules and containerLink to the pipeline.
CI_PROJECT_PATHRules and containerOwner and repository.
CI_PROJECT_NAMERules and containerRepository name.
CI_PROJECT_NAMESPACERules and containerOwner path.
CI_PROJECT_URLRules and containerLink to the repository.
CI_API_URLRules and containerRoot of the API.
CI_JOB_IDContainer onlyJob number.
CI_JOB_NAMEContainer onlyJob name.
CI_JOB_STAGEContainer onlyStage name.
CI_JOB_URLContainer onlyLink to the job.
CI_JOB_TOKENContainer onlyToken for this job. It is masked in the log.
CI_NODE_INDEXContainer onlyIndex of this parallel or matrix job.
CI_NODE_TOTALContainer onlyHow many parallel or matrix jobs there are.
CI_ENVIRONMENT_NAMEContainer onlyEnvironment name.
CI_ENVIRONMENT_URLContainer onlyEnvironment address.
CI_MERGE_REQUEST_IDMerge requestInternal id of the merge request.
CI_MERGE_REQUEST_IIDMerge requestNumber of the merge request in the project.
CI_MERGE_REQUEST_TITLEMerge requestTitle of the merge request.
CI_MERGE_REQUEST_SOURCE_BRANCH_NAMEMerge requestSource branch.
CI_MERGE_REQUEST_SOURCE_BRANCH_SHAMerge requestCommit of the source branch.
CI_MERGE_REQUEST_TARGET_BRANCH_NAMEMerge requestTarget branch.
CI_MERGE_REQUEST_TARGET_BRANCH_SHAMerge requestTip of the target branch.
CI_MERGE_REQUEST_PROJECT_PATHMerge requestProject path of the merge request.
CI_MERGE_REQUEST_PROJECT_URLMerge requestLink 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.

PresetImageContents
bluegit/base alpine:3.20
Alpine 3.20
bash, ca-certificates, curl, git
bluegit/node node:22-alpine
Node.js 22
bash, ca-certificates, curl, git
bluegit/python python:3.12-alpine
Python 3.12
bash, ca-certificates, curl, git
bluegit/go golang:1.26-alpine
Go 1.26
bash, ca-certificates, curl, git, git-daemon
bluegit/rust rust:1.81-alpine
Rust 1.81
bash, ca-certificates, curl, git
bluegit/java eclipse-temurin:21-jdk-alpine
Java 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.

OpenAPI

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.

OpenAPI

{
  "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.