blue-ci-cd.yml
Het bestand ligt in de root van de commit die de pipeline start. De vorm volgt GitLab CI zo ver dat een bestaand .gitlab-ci.yml meestal hernoemd kan worden naar blue-ci-cd.yml. Jobs mogen los op het hoogste niveau staan. Een ouder bestand met een jobs:-map blijft geldig.
stages: [test, build]
variables:
CGO_ENABLED: "0"
image: alpine:3.20
jobs:
test:
stage: test
image: golang:1.22
script:
- go test ./...
rules:
- if: $CI_COMMIT_BRANCH == "master"
- if: $CI_COMMIT_BRANCH =~ /^release\/.*/
build:
stage: build
needs: [test]
script:
- go build -o bluegit ./cmd/bluegit
artifacts:
paths: [bluegit]
expire_in: 14d
allow_failure: false
Velden
| Veld | Betekenis |
|---|---|
stages | Volgorde. Weggelaten: de stages in de volgorde waarin de jobs ze noemen. Een job zonder stage valt in test. |
image | Standaardimage, als tekst of als name:. default.image wint als die gezet is. Een preset zoals bluegit/base mag. node:22 blijft een gewoon image. |
default | image, before_script, after_script, tags, timeout, cache voor jobs die het zelf niet zetten. |
include | Alleen local. Een job met dezelfde naam wordt samengevoegd; het buitenste bestand wint per veld. |
variables | Tekst, of {value: ...}. Een job overschrijft dezelfde naam. |
before_script, script, after_script | after_script draait ook als script mislukt. |
workflow | rules zoals bij een job. Geen passende rule, of when: never, maakt geen pipeline. |
needs | Namen, of {job, optional}. needs: [] start meteen. Weggelaten: wacht op eerdere stages. Een needs op een parallel-job wacht op elke instantie. |
rules | De eerste rule die klopt wint. if, changes, exists en when. Zonder rules gelden only en except. |
only, except | Taknaam, branches, tags, of /regex/. |
when | on_success (standaard), on_failure, always, manual. In een rule ook never. |
allow_failure | Een mislukking houdt de rest niet tegen. De job zelf wordt een waarschuwing. |
artifacts | paths en expire_in (30s, 30m, 12h, 14d, 2w, never). |
cache | paths en een tekstuele key. Lokaal per repository, niet gedeeld met een andere machine. |
resource_group | Jobs met dezelfde naam in één repository draaien niet tegelijk. |
timeout | Overschrijft ci.timeout_seconds voor die job. De runner stopt de container als de tijd om is. |
retry | Een getal, of {max, when}. max is 0 tot 2; hoger wordt afgekapt. retry: 2 herhaalt elke mislukking behalve een annulering. when noemt script_failure, runner_system_failure, job_execution_timeout, stuck_or_timeout_failure of always. |
parallel | Een getal, hoogstens 50. De jobs heten naam 1/N en krijgen CI_NODE_INDEX en CI_NODE_TOTAL. |
parallel:matrix | Per lijstitem het cartesisch product, daarna aan elkaar. De job heet naam: [waarde, waarde]. De sleutels worden variabelen, in de volgorde uit het bestand. |
services | Sidecar-containers op een eigen Docker-netwerk met de job. Een tekst, of name, alias, command, entrypoint en variables. Zonder alias is dat het imagenaam zonder registry en tag. |
environment | Een naam, of {name, url}. Zet CI_ENVIRONMENT_NAME en CI_ENVIRONMENT_URL. Geen deployment en geen on_stop. |
interruptible | Standaard uit. Een nieuwe pipeline op dezelfde ref annuleert oudere jobs die dit aan hebben en nog in de wachtrij staan of lopen. Zo'n job heet in de UI Obsolete (tooltip: canceled because a newer pipeline started for the same ref). |
doNotInterrupt | Standaard false. Een lopende job met doNotInterrupt: true stopt BlueGit nooit automatisch. Een nieuwere pipeline op dezelfde ref wacht (status Wacht, waiting_for_job) tot de job klaar is; handmatig annuleren vraagt om bevestiging (API/bg: force=true / --force). Zie hieronder. |
tags | Bewaard bij de job. Tags kiezen geen runner; welke runner een job krijgt staat in runner.md. |
extends | Eén naam of een lijst. Een job waarvan de naam met . begint is een sjabloon en draait niet. |
Een if volgt de GitLab-vorm. && bindt sterker dan ||. Haakjes groeperen. Een lege variabele is null.
$NAAM == "waarde" && ($ANDERE == "x" || $ANDERE == null)
$NAAM != "waarde"
$NAAM =~ /reguliere-expressie/
$NAAM =~ /^pad\/met\/slash/
$NAAM =~ /hoofdletters/i
$NAAM !~ /reguliere-expressie/
$NAAM =~ $PATROON
=~ en !~ gebruiken RE2, zonder anker tot je ^ en $ zelf zet. Een / in het patroon schrijf je als \/. De vlag i staat direct achter de sluithaak. Een patroon in een variabele moet bij het beoordelen een geldige expressie zijn.
changes kijkt naar de diff van de push. Is die diff niet te bepalen, dan geldt de voorwaarde. exists eist dat elk pad in de commit staat.
Voorgedefinieerd, in rules en in de container: CI, CI_COMMIT_SHA, CI_COMMIT_SHORT_SHA, CI_COMMIT_BRANCH, CI_COMMIT_TAG, CI_COMMIT_REF, CI_COMMIT_REF_NAME, CI_COMMIT_REF_SLUG, CI_COMMIT_TITLE, CI_COMMIT_TIMESTAMP, CI_DEFAULT_BRANCH, CI_PIPELINE_ID, CI_PIPELINE_SOURCE, CI_PIPELINE_URL, CI_PROJECT_PATH, CI_PROJECT_NAME, CI_PROJECT_NAMESPACE, CI_PROJECT_URL, CI_JOB_ID, CI_JOB_NAME, CI_JOB_STAGE, CI_JOB_URL, CI_JOB_TOKEN, CI_API_URL, CI_CONFIG_PATH, CI_SERVER_NAME. CI_COMMIT_TIMESTAMP is de committer-tijd in ISO 8601. Op een tag is CI_COMMIT_BRANCH leeg. CI_PIPELINE_SOURCE is push, web of merge_request_event.
Een merge-request-pipeline start bij het openen van de merge request, bij een push naar de bronbranch, en opnieuw wanneer iemand naar de doelbranch pusht en de doelcommit daardoor verandert. Die draait op de commit van de bronbranch, niet op een samengevoegde boom. Dezelfde combinatie van ref, sha, bron, merge request en doelcommit start geen tweede pipeline. Kiezen de rules voor de push en voor de merge request dezelfde jobs, dan blijft één pipeline over en wordt die aan de merge request gekoppeld. Kiezen ze andere jobs, dan blijven beide bestaan en staat op de merge-request-pipeline het label Merge request met het nummer. CI_PIPELINE_SOURCE is merge_request_event op een pipeline die als merge request is gestart. Daarbij horen CI_MERGE_REQUEST_ID, CI_MERGE_REQUEST_IID, CI_MERGE_REQUEST_TITLE, CI_MERGE_REQUEST_SOURCE_BRANCH_NAME, CI_MERGE_REQUEST_SOURCE_BRANCH_SHA, CI_MERGE_REQUEST_TARGET_BRANCH_NAME, CI_MERGE_REQUEST_TARGET_BRANCH_SHA, CI_MERGE_REQUEST_PROJECT_PATH en CI_MERGE_REQUEST_PROJECT_URL, in de rules én in de container. De pipeline draait op de commit van de bronbranch. Jobs zonder rules en zonder only: merge_requests doen daar niet aan mee. only: merge_requests en except: merge_requests werken wel. Zodra zo'n bestand jobs voor de merge request kiest, mag er pas gemerged worden als de nieuwste merge-request-pipeline geslaagd is. Een job met allow_failure mag mislukken, maar moet wel af zijn. Een job die nog loopt, in de wachtrij staat, op manual staat of geannuleerd is, houdt de merge tegen.
CI_PIPELINE_ID is tijdens het beoordelen van rules nog 0. CI_JOB_ID, CI_JOB_NAME, CI_JOB_STAGE, CI_JOB_URL, CI_JOB_TOKEN, CI_NODE_INDEX, CI_NODE_TOTAL, CI_ENVIRONMENT_NAME en CI_ENVIRONMENT_URL bestaan pas in de container.
Steve zet in elke job GIT_CONFIG_COUNT, GIT_CONFIG_KEY_0=safe.directory en GIT_CONFIG_VALUE_0=*, tenzij de job safe.directory zelf al op * of /workspace zet. De checkout is van de host-gebruiker en de container-user is dat niet. Git 2.35+ weigert die repo anders (dubious ownership, exit 128). Scripts in de checkout geven git -c safe.directory mee, zodat dit ook werkt op een runner die die variabelen nog niet zet.
Bewust niet
pages, spec, een include van een ander project of van een URL, trigger- en child-pipelines, coverage, id_tokens, inherit en hooks. Die sleutels geven een waarschuwing in de editor en houden de rest van het bestand niet tegen. Er is één runner, in het BlueGit-proces, die Docker op dezelfde host aanroept. Zijn naam is runner.name in de configuratie, standaard Steve. In Preparing the docker executor staat altijd Using docker image …, en de image-id met digest als het image al lokaal aanwezig is. Een preset schrijft daar ook Using image preset … based on … en, na een bouw, Underlying image … with digest …. In Preparing environment staan de runnernaam, de host, de werkmap /workspace en het pipeline- en jobnummer. Die naam staat bij de runner op de jobpagina, op de pipelinedetails en boven het joblog. tags kiezen die runner niet. services delen één netwerk met de job; er is geen healthcheck verder dan een gestarte container.
Image-presets
Steve heeft geen aparte runners per taal. image: bluegit/base en de andere aliassen zijn Dockerfiles in deze repository. Bij het eerste gebruik bouwt Steve ze lokaal en hergebruikt hij het resultaat zolang de Dockerfile hetzelfde blijft.
| Preset | Basis |
|---|---|
bluegit/base | Alpine 3.20, met git, curl en bash |
bluegit/node | Node.js 22 |
bluegit/python | Python 3.12 |
bluegit/go | Go 1.26, plus git-daemon zodat git http-backend aanwezig is |
bluegit/rust | Rust 1.81 |
bluegit/java | Java 21 (Temurin) |
bluegit/dotnet | .NET SDK 8 |
Elk image krijgt daar bash, ca-certificates, curl en git bij. bluegit/go installeert ook git-daemon: daar zit git-http-backend in. Zonder die binary valt git push over HTTP terug op dumb HTTP en eindigt dat met code 22. ci.presets in config.yml voegt een alias toe of vervangt een ingebouwde: image mapt direct, dockerfile wijst naar een bestand, from herschrijft de eerste FROM.
/-/info toont deze specificatie en de presets. GET /api/v1/ci/spec geeft dezelfde inhoud als JSON. De pagina zet die JSON ook in <script type="application/json" id="bluegit-ci-spec">.
Editor
/-/ci/editor opent blue-ci-cd.yml van een branch. De pagina kleurt YAML en zet fouten en waarschuwingen op het regelnummer. Ontbreekt het bestand, dan opent de editor een uitgecommentarieerd voorbeeld. Opslaan maakt het bestand aan met één commit op de gekozen branch. Bestaat die branch nog niet, dan wordt hij vanaf de geopende branch gemaakt. Een beschermde branch accepteert alleen een beheerder voor een directe commit, dezelfde regel als een gewone push. Een ontwikkelaar krijgt dan de keuze om op een nieuwe branch te committen en een merge request te openen.
Mail bij een mislukte pipeline
Komt een pipeline op failed, dan gaat er één mail naar de gebruiker in actor (wie pushte, of wie de run met de hand startte). Dat loopt via POST /api/email op auth.timfalken.com, met de clientgegevens van BlueGit, niet met het token van de gebruiker. Onderwerp en tekst volgen users.language. De mail noemt de repository, het pipeline-nummer, de branch of tag, de jobs die zonder allow_failure faalden, en links naar de pipeline en het joblog. Zonder client_secret (de dev-modus) gaat er niets weg. Een mailfout laat de pipeline met rust.
Geheimen
Repositorygeheimen komen als omgevingsvariabele mee, zoals CI/CD-variabelen. Bij een groepsrepository eerst de groep, daarna de repository; dezelfde naam laat de repository winnen. Een geheim dat als beschermd is gemarkeerd, komt alleen mee als de branch of tag op een beschermd patroon past. Waarden van acht tekens of meer worden in het joblog vervangen door [MASKED]. CI_JOB_TOKEN ook.
Release vanuit een job
curl -fsS -X POST "$CI_API_URL/repos/$CI_PROJECT_PATH/releases" \
-H "Authorization: Bearer $CI_JOB_TOKEN" \
-H "Content-Type: application/json" \
-d '{"tag":"v1.2.3","commit":"'"$CI_COMMIT_SHA"'","name":"1.2.3","assets":[{"job":"build","path":"bluegit","name":"bluegit"}]}'
Het commit moet in de repository bestaan. Het asset moet een artifact zijn van een geslaagde job in dezelfde pipeline. Een beheerder kan met een persoonlijk token een release zonder assets maken. Een tag is A-Za-z0-9._-, tot 80 tekens, en wordt ook een annotated git-tag.
Eigen release vervangen
Een release bestaat maar één keer: dezelfde tag nog eens publiceren geeft 409. Een job kan zijn eigen release wel vervangen met "replace": true in de body, bijvoorbeeld voor een doorlopende experimentele release. BlueGit verwijdert dan de oude release, de assets en de git-tag en maakt ze opnieuw op het nieuwe commit. Er komt nooit een tweede release met dezelfde tag. Dat mag alleen als al deze regels gelden:
- Het verzoek komt met
CI_JOB_TOKENvan een lopende job in hetzelfde project. Een persoonlijk token of een job uit een ander project kan niet vervangen (403of401). - De pipeline draait op een branch (
refs/heads/…) die niet beschermd is. Een beschermde branch zoalsmaster, een tag-pipeline enforks/*krijgen dit recht nooit (403). Een stabiele release vanafmasterblijft daardoor altijd staan. - De tag past niet op een beschermd tagpatroon.
- De pipeline is veroorzaakt door een maintainer of hoger van het project (de
actor: wie pushte, mergede of de run startte), of door een instance-admin. Een ontwikkelaar, een onbekende of lege actor krijgt403. - De bestaande release is door een job gemaakt in een pipeline op dezelfde branch. Een release die met de hand of vanaf een andere ref is gemaakt, blijft staan (
409).
De assets worden eerst gecontroleerd, pas daarna gaat de oude release weg. Elke vervanging schrijft een regel release audit met action=replace, repository, tag, oud en nieuw commit, job, pipeline, ref en actor in het serverlog. Zonder replace blijft alles zoals het was.
Wanneer een job start
Gate in internal/ci geeft ready, wait, skip of fail. skip volgt op een harde mislukking bij when: on_success, of op succes bij when: on_failure. when: always wacht tot de voorgangers klaar zijn en draait dan toch. Een job met allow_failure telt niet als harde mislukking. De runner verwerkt pipelines elke twee seconden, met ci.concurrency tegelijk (standaard 1). Jobs die al lopen vullen die slots. Een startklare job die nog wacht toont In wachtrij (#n) in de pipelines-lijst, de stage-popover en op de jobpagina. Het nummer is de plek in Steves globale wachtrij, over alle repositories; de eerstvolgende wachtende job is #1.
Zolang er een pipeline of job pending of running is, halen de pipelines-lijst en de pipelinedetails elke drie seconden JSON op en werken de rijen bij zonder de pagina te herladen. Open popovers en de scrollpositie blijven staan. Zijn er geen actieve runs meer, dan stopt dat pollen. Een lopende job is een blauw icoon met een spinner; pending en created zijn een holle grijze cirkel. De commitbalk op het repo-overzicht toont diezelfde status.
Een commit die BlueGit zelf maakt loopt door één post-receive-route, dezelfde als een git push. Dat geldt voor de Pipeline Editor, het toepassen van een suggestie, het mergen van een merge request en een tag die in de interface of via de release-API ontstaat. Die route start de pipeline, de push-webhook en de activity.
Ontbreekt Docker, dan faalt de job met een regel in het log. De time-out is ci.timeout_seconds (standaard 1800).
Live log
De jobpagina vraagt nieuwe regels op zolang de job in de wachtrij staat of loopt, met ?offset= op het log-adres. Diezelfde poll werkt de wachtrijplek bij zolang de job nog queued is. De console scrolt mee zolang je onderaan staat; scrol je omhoog, dan blijft de plek staan. ANSI-kleuren worden classes. Geheimen van acht tekens of meer zijn [MASKED] op het moment dat de regel wordt weggeschreven, ook in die stream.
Canceled, Obsolete en timeouts
- Obsolete: een
interruptiblejob die stopte omdat een nieuwere pipeline op dezelfde ref startte. De API houdtstatus: canceled(achterwaarts compatibel) en geeftcancel_reason: supersededendisplay_status: obsolete. Een pipeline waarvan alle geannuleerde jobs obsolete zijn, toont ook Obsolete. - Canceled: handmatig gestopt (knop of
POST …/cancel),cancel_reason: manual. Het joblog eindigt metcanceled by a user. - Timeout: status
failedmetfailure_reason: job_execution_timeout. Het log eindigt metJob timed out after <duur> (limit: <bron>). De limiet is detimeout:van de job, andersci.timeout_secondsvan de server (standaard 30 minuten); de runnerinstelling Maximum job duration kapt beide af. - Webhook-jobs hebben dezelfde
cancel_reasonenfailure_reason. Pipeline-events gaan alleen bij geslaagd of mislukt; een geannuleerde of obsolete pipeline stuurt niets. - Bestaande jobs zijn bij de upgrade omgezet: alleen een
interruptiblejob met de melding in het log wordt obsolete. Vóór deze versie schreef de runner die melding ook bij handmatig annuleren.
doNotInterrupt
Voor jobs die je niet halverwege wilt stoppen, zoals een FTP-upload of een databasemigratie:
deploy:
stage: deploy
doNotInterrupt: true
script: ./upload.sh
- Mag ook in
default:, viaextends:en templates. Een waarde die geen boolean is, geeft een configuratiefout. - Alleen een job die loopt wordt beschermd. Wachtende jobs van een oude pipeline kunnen gewoon geannuleerd of obsolete worden, zoals nu.
- Nieuwere pipeline op dezelfde ref: die start geen jobs zolang een oudere pipeline op die ref een lopende doNotInterrupt-job heeft. De UI toont Wacht op niet-onderbreekbare job X in pipeline #N, de API
status: waiting_for_jobpluswaiting_for. Daarna gaat de oude pipeline af (Obsolete als de rest door de nieuwe pipeline werd geannuleerd) en start de nieuwe. Een tussenliggende pipeline wordt gewoon obsolete (als zijn jobsinterruptiblezijn). Er ontstaat geen deadlock: alleen oudere pipelines houden nieuwere tegen, en de job-timeout blijft gelden. Faalt of timet de job uit, dan gaat de nieuwe pipeline gewoon verder. - Handmatig annuleren (job of pipeline): in de UI een waarschuwingsvenster met de naam van de job(s). Via de API
POST …/cancel?force=true, viabg pipeline cancel|cancel-job … --force. Zonder force: HTTP 409 met uitleg.
Alle paden die een lopende job kunnen stoppen
| Pad | Gedrag met doNotInterrupt |
|---|---|
Nieuwe pipeline op dezelfde ref (interruptible) | Lopende job loopt uit; de nieuwe pipeline wacht. |
Job of pipeline annuleren (UI, API, bg) | Waarschuwing in de UI; API/bg alleen met force=true/--force. |
| Repository verwijderen | Geweigerd zolang de job loopt (UI: wachten; API: force=true). |
| Aparte runner intrekken (account, groep, globaal; UI en API) | Geweigerd zolang die runner de job draait (API: force=true). |
| Aparte runner geeft geen hartslag meer | Job wordt niet na 5 minuten als mislukt afgerond; alleen na timeout + 5 minuten. |
| BlueGit stoppen of herstarten (systemd, reboot) | BlueGit wacht met afsluiten tot de job klaar is (TimeoutStopSec=49h); de website blijft zolang bereikbaar. |
| Self-update | Wachtte al op nul lopende jobs en breekt nooit iets af. |
bluegit-runner stoppen (SIGTERM) | De runner wacht tot de job klaar is (TimeoutStopSec=49h). |
| Runner pauzeren, instellingen wijzigen | Stopte lopende jobs al niet. |
| Branch verwijderen, MR sluiten of mergen, zichtbaarheid wijzigen | BlueGit annuleert hierbij geen jobs. |
Uitzonderingen (bewust): 1. Job-timeout: blijft gelden, anders kan een vastgelopen job alles eeuwig blokkeren. Een timeout is geen annulering maar failed met job_execution_timeout. 2. Crash, kill -9, stroomuitval van BlueGit of de runnerhost: het proces is weg, dus de job ook. Na de herstart staat de job op failed met de melding dat BlueGit herstartte. 3. Expliciet forceren door een gebruiker (force=true, of Toch stoppen in het venster). 4. Aparte runner die langer dan timeout + 5 minuten onbereikbaar is: dan wordt de job als runner_system_failure afgerond, omdat BlueGit niet kan weten of hij nog loopt.
Verlopen handmatige jobs
Een handmatige job (when: manual) die nog wacht, verloopt automatisch als zijn pipeline verouderd is:
- Nieuwere pipeline: zodra er een nieuwere pipeline is op dezelfde ref (en hetzelfde merge request) in dezelfde repository. Een pipeline die alleen een kapotte
blue-ci-cd.ymlmeldt, telt niet mee. De handmatige job in de nieuwste pipeline blijft gewoon startbaar. - Leeftijd: een pipeline die alleen nog op handmatige jobs wacht en ouder is dan N dagen (Beheer > Pipelines, standaard 7, 0 = uit; per project te overschrijven in de projectinstellingen, leeg = instance-instelling).
Een verlopen job krijgt status canceled met cancel_reason superseded of stale, display_status obsolete en een regel in het log. Jobs die via needs alleen op die handmatige job wachtten, verlopen mee. Lopende of wachtende niet-handmatige jobs blijven ongemoeid. Log en artifacts blijven staan. De pipelinestatus volgt de bestaande regels: een verlopen job zonder allow_failure maakt de pipeline canceled (getoond als obsolete), een verlopen job met allow_failure telt niet mee. Starten of opnieuw starten van een verlopen job geeft 409. De controle draait één minuut na de start en daarna elk uur; POST /api/v1/admin/pipelines/expire-stale draait hem direct.