Skip to content

Set up a CI test repository

Nerthus.Core (until cutover). This page describes the frozen system that runs today and is deleted at cutover. Replaced by: not yet written.

This guide seeds a disposable lore repository to exercise the pipeline templates — settlement, map checkup, the close reminder, the session report, and the /zamknij-sesję close — against a real GitLab project, without ever pointing them at the live lore repo. One script, ci/New-NerthusCiTestRepo.ps1, builds it. The test bed is repozytorium-fabularne-prodkopia, in the same it group as the module itself, nerthus.core.

Read this before you run anything on this page

This guide used to name nerthus.testrepo, a public project you could raze at will. That project does not existglab api projects/margonem%2Fnerthus%2Fit%2Fnerthus.testrepo answers 404 Project Not Found, on a credential that reads repozytorium-fabularne-prodkopia in the same breath, and the page called it public, so a 404 is absence rather than permissions.

What it names now is not disposable, and three measured facts follow from that (repozytorium-fabularne-prodkopia, id 85880243, read 2026-08-29):

  • It is private. The reasoning further down about a public bed mirroring production does not hold here: an include: project: and a runner clone both need a credential.
  • Its main is protected. The reseed loop this page is built on force-pushes over the default branch, and a protected main refuses that. Step 1 below does not run against this project as it stands. Unprotecting main on a shared corpus is a Rada act with a blast radius, not a step in a how-to.
  • It is the shared dev corpus. Every host in the dev fleet converges on it, it holds a real clone of the campaign rather than a fixture, and other people and other tooling read it. A page that used to say push freely is now talking about somebody else's tree.

So: use this page's .env and wiring halves against it, and do not run the -Force reseed unless you have been told, by a person, that razing that history is wanted.

Prerequisites

  • A module checkout whose daemon builds and boots — the script starts a real ephemeral nerthusd from it. pwsh and git on PATH.
  • A test project with main unprotected. Reseeding force-pushes over the default branch (step 3), and a protected main rejects a force-push. repozytorium-fabularne-prodkopia has main protected today, so the reseed loop below does not run against it unmodified — see the box at the top. A public bed would also mirror production, because the module clone URL is public and an include: project: and a runner clone then resolve with no per-user role; a private bed needs a credential for both.

Put the credentials in .env

Every script below reads its credentials from a gitignored .env at the root of the module (nerthus.core) checkout, so the disposable test-bed secrets stay off the command line. The file, its full key catalogue, the loader, and the precedence rules are owned by Set up credentials — create it there with the test project's values (GITLAB_PROJECT_PATH = repozytorium-fabularne-prodkopia's path; leave GITLAB_TRIGGER_TOKEN blank — the bootstrap in step 3 mints it), then load it:

./vm/Import-NerthusEnv.ps1

1. Seed and push

Run the script from a module checkout. Point -Remote at the test repo's clone URL and -ModuleProject at the module's project path — not a clone URL; the path is what GitLab needs in the generated include: project: lines. -Force razes whatever history is on the remote and force-pushes the fresh seed:

./ci/New-NerthusCiTestRepo.ps1 `
    -Remote git@gitlab.com:margonem/nerthus/it/repozytorium-fabularne-prodkopia.git `
    -ModuleProject margonem/nerthus/it/nerthus.core `
    -Force

The include: ref auto-detects to the module checkout's current branch, so the test bed pulls the branch-under-test's templates, and the script also pins NERTHUS_MODULE_REF to that branch so the runner clones the same driver. Pass -ModuleRef '' to force GitLab's default-branch behavior once the templates have merged, or -ModuleRef main explicitly.

-Force is the disposable-repo default. Without it:

  • A bare run (./ci/New-NerthusCiTestRepo.ps1) seeds and commits into a temp directory only, touching no remote — it prints the path.
  • -Remote <url> alone adds the origin remote (a local, reversible act) but leaves the push to you; pushing is outward-facing.
  • -Push pushes without force — a rejected non-fast-forward is an obstacle, so use -Force on a repo you mean to reseed.

The example uses the SSH clone URL and pushes with your ambient key. On a headless box that holds only the PAT, use the https clone URL instead: -Token defaults from GITLAB_ACCESS_TOKEN, and the push goes over an oauth2:<token>@ URL — no SSH key, and origin stays the clean URL (the token never lands in .git/config).

2. What lands in the repo

The commit carries the adopted state — .nerthus/schema.json, the config, the data tables, and the generated nerthus.entities.md index — and none of the daemon's transient runtime files (the layout gitignores runtime/log/state/local/cache under .nerthus/). Alongside it, a .gitlab-ci.yml that includes all five templates against margonem/nerthus/it/nerthus.core:

include:
  - project: 'margonem/nerthus/it/nerthus.core'
    ref: '<branch-under-test>'
    file: 'ci/settle.gitlab-ci.yml'
  - project: 'margonem/nerthus/it/nerthus.core'
    ref: '<branch-under-test>'
    file: 'ci/map-checkup.gitlab-ci.yml'
  - project: 'margonem/nerthus/it/nerthus.core'
    ref: '<branch-under-test>'
    file: 'ci/report.gitlab-ci.yml'
  - project: 'margonem/nerthus/it/nerthus.core'
    ref: '<branch-under-test>'
    file: 'ci/close.gitlab-ci.yml'      # Discord /zamknij-sesję (trigger-fired)
  - project: 'margonem/nerthus/it/nerthus.core'
    ref: '<branch-under-test>'
    file: 'ci/reminder.gitlab-ci.yml'   # close-reminder (scheduled)

The ref: line is omitted when -ModuleRef is empty, pinning the includes to the module's default branch instead.

3. Wire GitLab and the edge

Seeding does not touch GitLab, Cloudflare, or Discord — the script prints the wiring checklist on exit. For the test bed, one command stands up all three legs from .env:

./vm/Initialize-NerthusCiTestBed.ps1 -WhatIf   # preview every underlying call
./vm/Initialize-NerthusCiTestBed.ps1           # provision GitLab + deploy Worker + register commands

It chains, in order: Set-NerthusGitLabPipelines (CI/CD variables, the three schedules, and the pipeline trigger token — captured with -PassThruTriggerToken into GITLAB_TRIGGER_TOKEN), then Publish-NerthusReportWorker (deploys the Worker with the trigger token and the Discord app vars from .env, overriding the committed production literals), then Register-NerthusDiscordCommands (the four slash/message commands). It leaves main unprotected — the test-bed default (a close pushes main; -Force reseeds over it). The one manual step it cannot do is set the Discord Interactions Endpoint URL to the Worker.

The underlying provisioning — every flag, and the production protected-main variant — is owned by the two pipeline how-tos; run them directly instead of the bootstrap when you need that control:

  • The scheduled jobs — variables, the settle / map-checkup / close-reminder schedules, and the trigger token — follow Set up the GitLab pipelines. The seed hands both ticks to the primary (schedules.*.owner = fleet), so a forced CI schedule runs and exits as a no-op until that owner reads ci.
  • The event-driven report and /zamknij-sesję close — the Cloudflare Worker and its push credentials — follow Set up session reports.

Two things differ on a test repo:

  • The module clone URL is public, so NERTHUS_MODULE_URL needs no credential — set it to https://gitlab.com/margonem/nerthus/it/nerthus.core.git and you do not pass -ModuleUrl to the seed script at all.
  • Keep main unprotected while you reseed (step 2's prerequisite). Production main is unprotected too, so the report and close variables are set unprotected; keep them that way on the test bed. A Protected variable would arrive empty on the unprotected ref and the clone would fail with The empty string is not a valid path.

4. Reseed and iterate

The repo is disposable by design. Re-run step 1's -Force command to raze and reseed after each change to the templates or the fixture. Because the include: ref and NERTHUS_MODULE_REF both auto-detect the module checkout's current branch, checking out a feature branch and reseeding is all it takes to point the whole test bed at that branch's drivers — no GitLab edit.

5. Test a run

With the repo wired, trigger a job from the ▶ play button on the schedule (CI/CD → Schedules), not the New pipeline button — the scheduled jobs' rules require pipeline source schedule, so a web run silently produces no job. The mechanics of a safe test run, and the exit-code triage when one goes red, are in Set up the GitLab pipelines §5–6.

For the trigger-fired legs (the Discord report / /zamknij-sesję close), an opt-in Pester harness fires a real trigger pipeline at the test project and polls it to a terminal state:

./vm/Import-NerthusEnv.ps1
$env:NERTHUS_E2E_FIRE = '1'
Invoke-Pester ./tests/e2e/ -Output Detailed

It skips entirely without NERTHUS_E2E_FIRE=1 and the .env trigger credentials, so the normal suite never touches the network. It fires at the GitLab trigger, not Discord — the .env holds only the Ed25519 public key, so a live signed interaction cannot be forged locally.

See also