Run CI and image publishing on the self-hosted GitLab

Ports ci.yml and publish.yml after the GitHub account was suspended and
ghcr.io/coffey-labs/ihasvpn went dark with it. The deployment on Web_Host
was still pulling that tag, and the only surviving copy was the image
already on the host -- amd64 only. This rebuilds the multi-arch tag.

The workflow built each platform on a native runner; there is one amd64
runner here, so arm64 goes through QEMU. Slower, and tag-driven for that
reason, but shipping amd64 only is exactly what the suspension already cost
us once.

The Actions workflows stay in the tree as the reference.
This commit is contained in:
2026-09-20 20:14:55 -07:00
parent 5989a53b9b
commit 14f81a3489
+99
View File
@@ -0,0 +1,99 @@
# CI on the self-hosted GitLab, ported from .github/workflows/ci.yml and
# publish.yml when the GitHub account was suspended on 2026-09-20. The Actions
# files stay in the tree: they are the reference this was written from and work
# unchanged if the appeal succeeds.
#
# This one mattered more than most. ghcr.io/coffey-labs/ihasvpn went dark with
# the account while the deployment on Web_Host was still pulling from it, and
# the only surviving copy was the image already on that host -- amd64 only,
# because that is the platform it runs. The multi-arch tag is rebuilt here.
#
# Images are pinned by digest, with the tag in the trailing comment: the
# replacement for the workflow's SHA-pinned actions, since GitLab has no
# action allowlist.
stages: [build, check, publish]
variables:
IMAGE: $CI_REGISTRY_IMAGE
default:
interruptible: true
.on-change: &on-change
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
# The Go binary embeds the built web assets, so the frontend build comes first
# and hands its output to the Go job as an artifact.
web:
stage: build
image: node:26-bookworm-slim@sha256:582460f614631b59b824ac6020533b9bf339c7fdf3a6d7db31abb6b4065f0212 # 26-bookworm-slim
variables:
NPM_CONFIG_CACHE: "$CI_PROJECT_DIR/.npm"
cache:
key:
files: [web/package-lock.json]
paths: [.npm/]
script:
- cd web
- npm ci --ignore-scripts --no-audit --no-fund
- npm run build
artifacts:
paths: [web/dist/]
expire_in: 1 week
<<: *on-change
go:
stage: check
image: golang:1.27-bookworm@sha256:69a7b9788769bec032d238959b61854e9ae87f57be9029ec04e9885fabf99195 # 1.27-bookworm
needs: [web]
cache:
key: go-mod
paths: [.gocache/]
variables:
GOPATH: "$CI_PROJECT_DIR/.gocache"
script:
- go vet ./...
- go test -count=1 ./...
# Left as `go run ...@latest`, as the workflow had it: a vulnerability
# check wants today's database, not a pinned copy of last month's.
- go run golang.org/x/vuln/cmd/govulncheck@latest ./...
<<: *on-change
docker-build:
stage: check
image: docker:28-cli@sha256:625d9431a9f54c5a2bc90f24f0e1c3d55b1349fd857dd85035f98c2c9acbdd4d # 28-cli
needs: [web]
script:
- docker build -t ihasvpn:ci-$CI_COMMIT_SHORT_SHA .
- docker image rm ihasvpn:ci-$CI_COMMIT_SHORT_SHA
<<: *on-change
# The workflow built each platform on its own native runner and joined the two
# digests into one tag. There is a single amd64 runner here, so arm64 goes
# through QEMU instead -- slower, but this is tag-driven and the alternative is
# shipping amd64 only, which is what the account suspension already cost us
# once. TrueNAS and Unraid users pull arm64.
publish:
stage: publish
image: docker:28-cli@sha256:625d9431a9f54c5a2bc90f24f0e1c3d55b1349fd857dd85035f98c2c9acbdd4d # 28-cli
needs: [web, go]
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 \
--build-arg IHASVPN_VERSION="$CI_COMMIT_TAG" \
--provenance=false --sbom=false \
--tag "$IMAGE:$CI_COMMIT_TAG" \
--tag "$IMAGE:latest" \
--push .
after_script:
- docker logout "$CI_REGISTRY" || true
rules:
- if: $CI_COMMIT_TAG