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), runapt-get install gitwith 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.allmachine 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:
- 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_URLtohttps://gitlab-ci-token:${CI_JOB_TOKEN}@<host>/<group>/Nerthus.Core.gitand tick the variable's Expand variable reference flag — UI-defined variables are not expanded by default — so each job substitutes its own short-livedCI_JOB_TOKEN. Such a variable cannot be masked (a masked variable may not reference another), which is acceptable for a per-job token. - Public visibility. A public module project clones with a plain
https://<host>/<group>/Nerthus.Core.gitand no credential. Internal visibility is not enough — an anonymous clone from a job still fails; use recipe 1. - Deploy token. On the module project (Settings → Repository → Deploy tokens)
mint a
read_repositorytoken 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¶
- 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'
- Create the monthly schedule (CI/CD → Schedules) on the default branch, e.g.
cron
17 3 1 * *. - Set
NERTHUS_SETTLE=trueas a variable on that schedule — the guard that keeps every other schedule from settling. - 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:
- Include
ci/map-checkup.gitlab-ci.ymlalongside the settle template. - Create a daily schedule, e.g. cron
47 4 * * *. - Set
NERTHUS_MAP_CHECKUP=trueon that schedule. - Reuse the same two CI/CD variables —
NERTHUS_SETTLE_PUSH_TOKENis 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 (sourceweb) 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 sourceschedule— 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 asettle/<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-NoPushskips: 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.jsonthe ephemeral daemon boots from - API reference —
Invoke-NerthusSettleandPOST /workflows/settle