# CI for the self-hosted GitLab that replaced GitHub Actions when the account # was suspended on 2026-09-20. This is a port of .github/workflows/ci.yml and # publish.yml, which are kept in the tree for reference and for the day the # appeal succeeds. # # Every `image:` here is pinned to a digest, with the tag it belonged to in the # trailing comment. That is the direct replacement for the SHA-pinned `uses:` # in the Actions workflows: GitLab has no equivalent of an action allowlist, so # the only thing standing between this pipeline and whatever the publisher # pushes to a tag next is the digest. Read the comment for the version; the # digest is what runs. Do not "simplify" one back to a bare tag. # # The runner is a group runner on Web_Host with the host docker socket bound # in, reached over the internal container network rather than # https://git.coffeylabs.org -- that name is Cloudflare-proxied on the Free # plan, which caps request bodies at 100 MB and would break artifact uploads. stages: [test, build, publish] variables: # Jobs talk to the registry directly on its DNS-only name, never through the # proxy, for the same 100 MB reason. IMAGE: $CI_REGISTRY_IMAGE GIT_DEPTH: "0" default: interruptible: true # ---------------------------------------------------------------- test ------ node: stage: test image: node:26-bookworm-slim@sha256:582460f614631b59b824ac6020533b9bf339c7fdf3a6d7db31abb6b4065f0212 # 26-bookworm-slim cache: key: files: [package-lock.json] paths: [.npm/] before_script: - npm config set cache .npm --global script: - npm ci --ignore-scripts - npm run typecheck - npm test - npm run build artifacts: paths: [dist/] expire_in: 1 week rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH - if: $CI_COMMIT_TAG # --------------------------------------------------------------- build ------ # Proves the Dockerfile still builds on every change, without pushing. The # equivalent of ci.yml's final `docker build -t ihasmail:ci .` step. # # Not called `image`: that is a reserved keyword, and a job by that name is # silently read as the global image: setting instead ("image name should be a # string"). Same trap for `stages`, `cache`, `services` and `variables`. docker-build: stage: build image: docker:28-cli@sha256:625d9431a9f54c5a2bc90f24f0e1c3d55b1349fd857dd85035f98c2c9acbdd4d # 28-cli needs: [node] script: - docker build -t ihasmail:ci-$CI_COMMIT_SHORT_SHA . - docker image rm ihasmail:ci-$CI_COMMIT_SHORT_SHA rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH # ------------------------------------------------------------- publish ------ # Tag-driven, replacing the release -> publish workflow_call chain. GitHub # needed that dance because a release cut with GITHUB_TOKEN raises no event; # GitLab has no such rule, so a tag pipeline is enough. # # arm64 is built under QEMU on this amd64 host, not on a native runner as # GitHub's free `ubuntu-24.04-arm` did. It is slow -- tens of minutes for the # npm install and Vite build through instruction translation -- which is # tolerable for a weekly tag and would not be for every push. That is why this # job is tag-only. If arm64 ever starts timing out, the fix is an arm64 runner, # not dropping the platform: TrueNAS and Unraid users pull it. publish: stage: publish image: docker:28-cli@sha256:625d9431a9f54c5a2bc90f24f0e1c3d55b1349fd857dd85035f98c2c9acbdd4d # 28-cli needs: [node] variables: DOCKER_BUILDKIT: "1" before_script: - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY" - docker run --privileged --rm tonistiigi/binfmt --install arm64 - docker buildx create --use --name ci-builder --driver docker-container || docker buildx use ci-builder script: - | docker buildx build \ --platform linux/amd64,linux/arm64 \ --tag "$IMAGE:$CI_COMMIT_TAG" \ --tag "$IMAGE:latest" \ --push . after_script: - docker logout "$CI_REGISTRY" || true rules: - if: $CI_COMMIT_TAG