name: CI on: push: branches: [main] pull_request: # A second push to the same branch makes the first run's result irrelevant. concurrency: group: ci-${{ github.ref }} cancel-in-progress: true permissions: contents: read jobs: go: name: Go runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: "1.26" cache: true - name: Formatting run: | unformatted=$(gofmt -l .) if [ -n "$unformatted" ]; then echo "These files are not gofmt'd:" echo "$unformatted" exit 1 fi - name: Vet run: go vet ./... # -race is the whole point of running tests in CI for a server with a # WebSocket fan-out: a data race is exactly the bug that passes locally. - name: Test # -timeout, fordi internal/httpapi nærmer sig Go's standardgrænse på 600s: # 498s i CI på #95, 656s på udviklingsmaskinen. Når den krydser, er # beskeden "panic: test timed out after 10m0s" plus et goroutine-dump, og # det læser som en deadlock i koden frem for som en suite, der blev for # stor. Grænsen er der stadig; den er bare sat et sted, hvor den betyder # "noget hænger" i stedet for "maskinen var langsom i dag". run: go test -race -count=1 -timeout 20m ./... - name: Build run: CGO_ENABLED=0 go build ./... spec: name: OpenAPI is valid runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: "22" # Structural validity only. That the spec matches the *router* is checked by # a Go test, which is the half that actually goes stale. - run: npx --yes @redocly/cli@latest lint docs/openapi.yaml e2e: name: End-to-end runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: "1.26" cache: true - uses: actions/setup-node@v4 with: node-version: "22" cache: npm cache-dependency-path: web/package-lock.json - name: Install working-directory: web run: npm ci --no-audit --no-fund # Chromium for the suite, WebKit for one test. # # A smoke test exists to catch a build that does not work at all, and running # the whole thing in three engines finds the same breakage three times for # three times the cost. Focus is the exception, and it took a real report to # learn it: Safari ignores `autofocus` on anything inserted after the page has # loaded, so every inline form in the app opened with no cursor in it. WebKit # here is the same engine Safari is, and `webkit-forms` in the Playwright # config greps a single test out of the suite to run in it. - name: Install browsers working-directory: web run: npx playwright install --with-deps chromium webkit # The suite builds the frontend, embeds it and starts the real binary — see # playwright.config.js. There is nothing to build here first. - name: Test working-directory: web run: npm run test:e2e - if: failure() uses: actions/upload-artifact@v4 with: name: playwright-report path: | web/test-results/ web/playwright-report/ retention-days: 7 docker: name: Docker image builds runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: docker/setup-buildx-action@v3 # Build only — publishing happens on a tag, in release.yml. This job exists # so a Dockerfile that no longer builds fails on the pull request rather # than at release time. # # Loaded into the local daemon rather than discarded, because the step below # has to inspect what came out. - uses: docker/build-push-action@v6 with: context: . push: false load: true tags: verdande:ci cache-from: type=gha cache-to: type=gha,mode=max - uses: docker/build-push-action@v6 with: context: . push: false load: true tags: urd:ci build-args: EDITION=notes cache-from: type=gha cache-to: type=gha,mode=max # The claim this repository makes about urd is that it is the same program, # not a fork — and that claim is checkable rather than a sentence in a README. # # Stages 1 and 2 are identical for both builds, and the edition is set with # ENV, which is image *config* rather than a layer. So the two images must # have the same layer digests and differ only in that one variable. A change # that made them diverge — a second COPY, a conditional RUN, a separate build # of the binary — would quietly turn "one source" into two, and the only # symptom would be a bug fixed in one product and not the other. # # This was impossible until the frontend build became deterministic: SvelteKit # put Date.now() in version.json, so no two builds ever matched. See the note # in web/svelte.config.js. - name: The two images are one build run: | set -eu layers() { docker image inspect "$1" --format '{{json .RootFS.Layers}}'; } if [ "$(layers verdande:ci)" != "$(layers urd:ci)" ]; then echo "The verdande and urd images do not share their layers:" layers verdande:ci layers urd:ci echo "They are built from one source, so this is either a Dockerfile that" echo "now builds the binary twice, or a stage that is no longer shared." exit 1 fi echo "Shared layers: $(layers verdande:ci | tr ',' '\n' | wc -l)" # And the half that stops the check above from passing over two identical # images: they must actually differ in the one way they are supposed to. edition() { docker image inspect "$1" --format '{{json .Config.Env}}' | tr ',' '\n' | grep VERDANDE_EDITION; } echo "verdande: $(edition verdande:ci)" echo "urd: $(edition urd:ci)" edition verdande:ci | grep -q 'VERDANDE_EDITION=full' edition urd:ci | grep -q 'VERDANDE_EDITION=notes'