Skip to content

Set up the GitLab pipelines

This guide wires the two scheduled jobs onto the lore repository (repozytorium-fabularne): the monthly settlement and the daily map checkup. Both are ephemeral-daemon drivers the module ships as includable job templates, and both publish through a merge request the Council reviews. What a settlement is — pending derivation, the report shape, the driver workflow — lives in the settlement model; the map sweep contract lives in the locations model.

Prerequisites

  • An adopted lore repo. Everything the daemon needs — nerthus.entities.md, the .nerthus/ config, the data tables — is committed state, so a fresh CI clone boots with no extra setup (adoption). Council edits to the committed tables govern the very next scheduled run with no CI change.
  • A Docker-executor runner that can pull mcr.microsoft.com/powershell:latest (Ubuntu-based, unpinned tag), run apt-get install git with network egress to the Ubuntu apt mirrors, and reach the module's clone URL. There is no shell-executor or air-gapped variant.
  • No daemon-auth secret. The ephemeral daemon mints its own admin.all machine token at boot (permissions).

1. Create the credentials

Set two CI/CD variables on the lore repo (Settings → CI/CD → Variables):

Variable Holds Masking
NERTHUS_MODULE_URL Clone URL of the module maskable only in its deploy-token form
NERTHUS_SETTLE_PUSH_TOKEN A lore-repo project access token Masked

The push token. Create it on the lore repo (Settings → Access Tokens): role Developer or higher, scopes api + write_repository. Both scheduled jobs share it to push write-back branches and open merge requests, so no protected-branch rule may cover settle/* or map-checkup/*. Three aging facts the UI will not remind you of:

  • GitLab forces an expiry date — 365 days by default; a self-managed administrator can raise or drop the limit. Note the date and re-mint before it lapses: an expired token fails every write-back with exit 2 (§5).
  • The merge requests are authored by the token's bot user (project_<id>_bot_…), not by any Gracz. That is expected; the Council review happens on the merge.
  • One tier fact: GitLab.com Free blocks project access tokens — they are a Premium/Ultimate feature, though self-managed instances allow them on any tier. On the free tier the substitute is a dedicated bot account whose personal credential does the pushing — the identity model the VM instance already uses (deploy the VM).

The Protected flag is a trap

A protected variable is invisible to a pipeline on an unprotected ref, and the driver checks the token only after settling. The job then exits 2 with the settlement applied on the runner but never pushed. Nothing is lost (§5), but the failure reads as mysterious. Leave the flag off unless you know your refs are protected.

The module URL. The runner must be able to git clone the module. Three recipes, best first:

  1. Job-token allowlist (no long-lived secret). On the module project: Settings → CI/CD → Job token permissions → add the lore repo. Then set NERTHUS_MODULE_URL to https://gitlab-ci-token:${CI_JOB_TOKEN}@<host>/<group>/Nerthus.Core.git and tick the variable's Expand variable reference flag — UI-defined variables are not expanded by default — so each job substitutes its own short-lived CI_JOB_TOKEN. Such a variable cannot be masked (a masked variable may not reference another), which is acceptable for a per-job token.
  2. Public visibility. A public module project clones with a plain https://<host>/<group>/Nerthus.Core.git and no credential. Internal visibility is not enough — an anonymous clone from a job still fails; use recipe 1.
  3. Deploy token. On the module project (Settings → Repository → Deploy tokens) mint a read_repository token and embed it: https://<token-user>:<token>@<host>/…. Least preferred — a long-lived secret in a variable; set it Masked (a static URL satisfies the masking rules) or Masked and hidden.

One visibility rule sits outside the variables: include: project: resolves against the user who triggers the pipeline, so the schedule owner needs at least the Reporter role on a private module project.

2. Schedule the settlement

  1. Include the job template in the lore repo's .gitlab-ci.yml:
# repozytorium-fabularne/.gitlab-ci.yml
include:
  - project: 'nerthus/Nerthus.Core'   # the module's real project path
    file: 'ci/settle.gitlab-ci.yml'
  1. Create the monthly schedule (CI/CD → Schedules) on the default branch, e.g. cron 17 3 1 * *.
  2. Set NERTHUS_SETTLE=true as a variable on that schedule — the guard that keeps every other schedule from settling.
  3. Confirm the two CI/CD variables of §1 are set on the lore repo.

Fire after midnight UTC

Schedules fire in the timezone picked on the schedule, but the daemon computes the default month in UTC. 17 3 1 * * is safe for any European timezone; a 17 0 1 * * schedule in Warsaw summer time (UTC+2) would fire at 22:17 UTC on the last day of the old month and settle the wrong month.

The template pins resource_group: nerthus-settle, so two settle jobs of the same repo never run concurrently. Two committed states break the job with the same "dry-run gate failed" exit: server.read_only: true in config.json boots the ephemeral daemon ReadOnly (configuration), and a repo whose .nerthus/schema.json pointer lags the module's expected index-format version 403s every write as SchemaTooOld — until an operator re-runs Initialize-NerthusRepo locally and commits the result. In both cases the dry-run report logged above the failure line carries the real error id.

3. Schedule the map checkup

The sibling template ci/map-checkup.gitlab-ci.yml drives ci/Invoke-NerthusMapCheckupCi.ps1 — the CDN version sweep over the Mapa catalogue. Setup mirrors settlement:

  1. Include ci/map-checkup.gitlab-ci.yml alongside the settle template.
  2. Create a daily schedule, e.g. cron 47 4 * * *.
  3. Set NERTHUS_MAP_CHECKUP=true on that schedule.
  4. Reuse the same two CI/CD variables — NERTHUS_SETTLE_PUSH_TOKEN is the one write-back token both drivers share.

The job runs one full sweep, applied directly — each version bump is an independent single-tag write and the merge request is the review, so a settlement-style dry-run gate would only double the CDN traffic. No newer renders (gone-map flags are report-only job-log output): exit 0. Otherwise it opens a map-checkup/<timestamp> merge request with the report embedded; an exhausted failure budget fails the job after the write-back so the CDN trouble gets triaged. It pins the same resource_group: nerthus-settle, so a sweep and a settlement never mutate the index concurrently. Daily is the honest frequency ceiling: a full sweep is thousands of HEAD requests against the Margonem CDN — raise request_delay_ms before raising frequency (configuration).

4. Test a run

  • In GitLab. The job's rules require pipeline source schedule, so the New pipeline button (source web) silently produces no job — this is the guard working, not a broken setup. Use the ▶ play button on the schedule itself (CI/CD → Schedules), which runs with source schedule — but with your permissions, so the tester too needs read access to the module project. A test run is safe by construction: the worst it can do is push a settle/<timestamp> branch and open a merge request — close it unmerged and delete the branch.
  • Locally. The driver runs outside GitLab: pwsh ci/Invoke-NerthusSettleCi.ps1 -Repo <clone> -NoPush. Know what -NoPush skips: only the branch/push/merge-request stage. The settle itself still writes into the clone's working tree — only a dry run (Invoke-NerthusSettle -WhatIf) is write-free. Use a disposable clone.

5. Triage a red run

The driver's exit-code contract:

Exit Meaning
0 Settled, or nothing was pending
1 The dry-run gate or the settle reported a failure
2 Infrastructure: daemon boot, missing token or GitLab env, push or merge-request failure

On exit 1, read the report in the job log and fix its findings — a partial month or an unresolved name; semantics in the settlement model. A gate failure writes nothing; a settle failure leaves its partial result on the runner's throwaway clone only. On exit 2, no durable state is at risk: pending work is derived from committed state, so the next run redoes whatever the failed one applied.

Where the evidence lives. The runner's .nerthus/log/audit.jsonl evaporates with the job; the durable record of a CI settlement is the merge request diff plus the echo lines. The driver prints the full report into the job log and embeds it in the merge-request description, so the reviewer holds both. One diagnostic gap: a daemon boot failure points at .nerthus/log/daemon-ci.out.err, which the template does not export as an artifact — it dies with the runner. Reproduce boot failures locally with the driver.

6. Settle a month whose execution date has passed

The job template passes the driver no month override; every scheduled run settles the previous calendar month and nothing else. Distribution repairs and transfers are month-agnostic, so a missed run's leftovers are caught by the next one — but a past month's PU is reachable only through the month override, which CI does not expose. The rule, stated plainly: once a month's execution date has passed, only an operator-driven commit can settle it. Re-triggering the schedule does not help — it settles the same default month again, never the missed one. This lands on the schedule most often through a partial PU month, which the dry-run gate refuses by design; the 1st passes while a human untangles the split.

Worked example — settling July in September. The August 1st run exited 2 (the push token had expired in July) and nobody re-minted it before September, so the September run settled only August. The Koordynator settles July by hand:

git clone https://gitlab.com/margonem/nerthus/narratorzy/repozytorium-fabularne.git; cd repozytorium-fabularne
Invoke-NerthusSettle -Month 2026-07 -WhatIf   # preview: PU pending, rest settled
Invoke-NerthusSettle -Month 2026-07           # applies to this working tree

The preview shows why only PU is left: the September run already caught July's drifted copy. The manual run awards the stranded PU — Eraster's pu_suma moves 127,90 → 129,10 — and appends the July echo line to nerthus.ledger.md. Then the operator-driven commit publishes it:

git checkout -b settle/2026-07-reczne
git add -A && git commit -m "Rozliczenie 2026-07 (ręczne)"
git push origin settle/2026-07-reczne     # then open the merge request

The merge is the Council's review, exactly as for a scheduled run. Invoke-NerthusSettle parameters and envelope: API reference.

The module's own verification pipeline

The module repo carries its own .gitlab-ci.yml (repo root, distinct from the ci/ templates the lore repo includes). Two test jobs run on the pwsh image: pester runs ./tests/ — the smoke suite and the contract suite, which boots a real loopback daemon inside the job — and publishes JUnitXml to testResults.xml; analyze runs PSScriptAnalyzer at Error severity, any finding fails the job. Two site jobs run on a python image: pages builds this documentation site and publishes it as GitLab Pages on the default branch; docs-build-check gives merge requests that touch the site a build check without publishing. The adoption rehearsal is env-gated and skips itself here — it needs a disposable clone of repozytorium-fabularne (run the tests).

See also

  • Settlement model — what the job settles: derivation, the report, the driver's workflow
  • Sync design — the long-lived VM daemon this CI model complements
  • Deploy the VM — the bot identity and SSH key the free-tier substitute reuses
  • Configuration — the committed config.json the ephemeral daemon boots from
  • API referenceInvoke-NerthusSettle and POST /workflows/settle