No description
  • Shell 58.6%
  • Python 11%
  • Go Template 7.8%
  • HCL 7.5%
  • C# 4.6%
  • Other 10.5%
Find a file
Kamil Kiełbasa 72f648bace
workshop: 45 independent per-client versioning of one .NET csproj with GitVersion
One project, three build configurations (Release_ClientA/B/C), three SemVer
lines. Each client has a GitVersion config whose ignore.paths hides every
commit that did not touch that client's inputs; scoped Conventional Commits
decide how much to bump; tags are client-a/vX.Y.Z.

- build/versioning/generate-paths.sh asks MSBuild (-getItem) what each
  configuration compiles and ships, collapses it to folders and generates
  GitVersion.<client>.yml, rules:changes for CI and the release path list.
  check-generated fails the pipeline when they are stale.
- .gitlab-ci.yml: build:<client> with rules:changes from the generated list,
  version rewritten into Clients/<X>/AssemblyInfo.cs, dotenv artifact;
  manual release:<client> tags the newest commit touching the client's paths
  (a tag on an ignored commit is invisible to GitVersion); tag pipelines
  build the release artifact and refuse a version/tag mismatch.
- verify-scenarios.sh replays 8 scenarios on a throwaway repo (10/10 with
  GitVersion.Tool 6.8.2, pinned in .config/dotnet-tools.json).
- README lists the measured gotchas: invisible tags, non-accumulating bumps,
  same-second commits, locale-dependent sort, #if in shared code, CS0579.

Verified on the local GitLab 19.4 lab: rules:changes, manual release,
tag pipeline with the version baked into the DLL.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-29 20:45:25 +02:00
dependency-track Initial commit: GitLab CE workshop lab 2026-07-26 13:55:37 +02:00
docs workshop: components ship tools as release assets/packages, fix six module YAMLs, runner push fix 2026-09-29 18:32:40 +02:00
exercises workshop: components ship tools as release assets/packages, fix six module YAMLs, runner push fix 2026-09-29 18:32:40 +02:00
feature-flags Initial commit: GitLab CE workshop lab 2026-07-26 13:55:37 +02:00
features Initial commit: GitLab CE workshop lab 2026-07-26 13:55:37 +02:00
gitlab-agent Initial commit: GitLab CE workshop lab 2026-07-26 13:55:37 +02:00
gitops-capstone Initial commit: GitLab CE workshop lab 2026-07-26 13:55:37 +02:00
harbor workshop: components ship tools as release assets/packages, fix six module YAMLs, runner push fix 2026-09-29 18:32:40 +02:00
hetzner-workshop workshop: components ship tools as release assets/packages, fix six module YAMLs, runner push fix 2026-09-29 18:32:40 +02:00
k8s-runner Initial commit: GitLab CE workshop lab 2026-07-26 13:55:37 +02:00
monitoring workshop: five new modules, monitoring overhaul, Harbor Trivy, reviewdog/renovate rewrites 2026-07-31 08:27:59 +02:00
review-apps Initial commit: GitLab CE workshop lab 2026-07-26 13:55:37 +02:00
runner-config workshop: components ship tools as release assets/packages, fix six module YAMLs, runner push fix 2026-09-29 18:32:40 +02:00
sample-apps workshop: security pipeline for sample apps, npm registry module, simplify modules 07/08/13 2026-07-29 08:51:30 +02:00
scripts workshop: components ship tools as release assets/packages, fix six module YAMLs, runner push fix 2026-09-29 18:32:40 +02:00
terraform Initial commit: GitLab CE workshop lab 2026-07-26 13:55:37 +02:00
vault Initial commit: GitLab CE workshop lab 2026-07-26 13:55:37 +02:00
wireguard Initial commit: GitLab CE workshop lab 2026-07-26 13:55:37 +02:00
workshop workshop: 45 independent per-client versioning of one .NET csproj with GitVersion 2026-09-29 20:45:25 +02:00
.env.example Initial commit: GitLab CE workshop lab 2026-07-26 13:55:37 +02:00
.gitignore workshop: components ship tools as release assets/packages, fix six module YAMLs, runner push fix 2026-09-29 18:32:40 +02:00
docker-compose.yml workshop: five new modules, monitoring overhaul, Harbor Trivy, reviewdog/renovate rewrites 2026-07-31 08:27:59 +02:00
README.md workshop: five new modules, monitoring overhaul, Harbor Trivy, reviewdog/renovate rewrites 2026-07-31 08:27:59 +02:00

GitLab CE local playground

A self-contained GitLab CE + GitLab Runner lab for learning, running on Docker. Works on macOS (Docker Desktop) and Linux (Docker Engine), on amd64 and arm64. On Ubuntu, read docs/UBUNTU.md first — it covers the host setup that differs (apt repo, kernel limits, firewall, file ownership).

What's here

File Purpose
docker-compose.yml GitLab CE + one GitLab Runner (Docker executor)
.env Ports, hostname, root password, bind address — copy from .env.example
scripts/preflight.sh Read-only host check (Docker, RAM, ports, kernel limits)
scripts/health-check.sh Read-only health check across every stack (GitLab, runner, Harbor, Dependency-Track, Vault, monitoring, kind)
docs/UBUNTU.md Host setup + Linux-specific behaviour
scripts/register-runner.sh Registers the runner with one command (applies runner-config/runner-template.toml)
runner-config/ The runner's /etc/gitlab-runner, bind-mounted: concurrent, docker socket for jobs
scripts/seed-groups.sh Seeds a group/subgroup hierarchy + 4 users with different roles (see §16)
scripts/security-audit.sh Read-only security posture review of the instance (see §17)
harbor/ Harbor container registry — its own compose stack (see §5)
dependency-track/ OWASP Dependency-Track (SBOM monitoring) — its own compose stack (see §6)
vault/ HashiCorp Vault (secrets via OIDC) — its own compose stack (see §7)
k8s-runner/ GitLab Runner on a kind Kubernetes cluster (see §8)
monitoring/ Prometheus + Grafana dashboards for runner/CI metrics (see §9)
terraform/ IaC pipeline with GitLab-managed state (see §10)
feature-flags/ Flag-gated pipeline using GitLab's built-in Unleash API
wireguard/ WireGuard VPN + an isolated service, for the CI-over-VPN exercise (see §15)
gitlab-agent/ GitLab Agent for Kubernetes (agentk ↔ KAS) — cluster connection (see §11)
review-apps/ Per-MR ephemeral environments on kind + OWASP ZAP DAST (see §12)
gitops-capstone/ 🏆 build→SAST→SBOM→sign→push→ArgoCD deploy, end-to-end (see §13)
features/ 24 CE feature showcases: registries/packages, CI automation, reporting, Flux, auth, governance… (see §14)
workshop/ GitLab CI/CD workshop: 36 modules — pipelines, runners, security, automation, plus a self-managed GitLab install/upgrade/restore drill
sample-apps/ Deliberately vulnerable apps + a SAST/SCA/SBOM/DAST pipeline; setup.sh publishes them as a security-demo group

Data lives in Docker named volumes (gitlab_config, gitlab_data, gitlab_logs), so it survives docker compose down and restarts.

The runner is the exception: its /etc/gitlab-runner is bind-mounted from runner-config/ in this repo, so its settings are versioned and survive even docker compose down -v. config.toml itself is gitignored because gitlab-runner register writes runner auth tokens into it; what is committed is config.toml.example (globals, concurrent = 4) and runner-template.toml (per-runner docker settings, including the host Docker socket that lets jobs build and push images).

1. Start it

cp .env.example .env      # first time only; edit the root password
./scripts/preflight.sh    # optional: checks Docker, RAM, free ports
docker compose up -d

First boot takes 5–8 minutes (GitLab reconfigures itself). Watch progress:

docker compose logs -f gitlab      # Ctrl-C to stop following
docker compose ps                  # wait until gitlab is "healthy"

2. Log in

  • Open http://localhost:8929
  • User: root
  • Password: value of GITLAB_ROOT_PASSWORD in .env (default ChangeMe_local123)

Change the password after first login. The initial_root_password only applies on the very first boot with an empty data volume.

3. Register a runner

GitLab 16+ registers runners with an authentication token (glrt-...) created in the UI:

  1. In GitLab: Admin Area → CI/CD → Runners → New instance runner
    • Pick a tag or check "Run untagged jobs" so your first pipeline is picked up.
    • Click Create runner, copy the glrt-... token.
  2. Register it:
./scripts/register-runner.sh glrt-xxxxxxxxxxxxxxxxxxxx
  1. Verify:
docker compose exec runner gitlab-runner list

The runner should now show as online (green) in the Runners page.

4. Try a pipeline

Create a new project in the UI, then add this .gitlab-ci.yml at the repo root:

stages: [build, test]

build-job:
  stage: build
  image: alpine:latest
  script:
    - echo "Building..."
    - echo "hello from CI" > artifact.txt
  artifacts:
    paths: [artifact.txt]

test-job:
  stage: test
  image: alpine:latest
  script:
    - echo "Testing..."
    - cat artifact.txt
    - test -f artifact.txt && echo "OK"

Commit it — the pipeline runs under Build → Pipelines.

Pushing code via SSH

SSH is mapped to host port 2222:

git remote add origin ssh://git@localhost:2222/root/my-project.git
# add your SSH key in GitLab: User Settings -> SSH Keys

Or over HTTP:

git remote add origin http://localhost:8929/root/my-project.git

5. Harbor container registry

Harbor is a separate compose stack under harbor/ (it's a ~9-container app distributed via its own installer, so it can't be merged into the GitLab file).

cd harbor && ./setup-lab.sh     # renders harbor.yml for this checkout, then starts it

setup-lab.sh does three things Harbor's own installer can't: it renders harbor.yml for this checkout (data_volume must be an absolute path, so the committed source is harbor.yml.lab.tmpl), bounces the proxy after ./prepare so it re-resolves recreated backends, and joins the proxy to the GitLab lab network as harbor — that alias is what makes http://harbor:8080 work from CI jobs, the kind node and Terraform. Once up it's reachable at:

  • Web UI: http://localhost:8090
  • User: admin
  • Password: Harbor12345 (set in harbor/harbor.yml as harbor_admin_password)

Push / pull from the host

localhost registries are treated as insecure-OK by Docker, so plain HTTP just works:

docker login localhost:8090 -u admin -p Harbor12345
docker tag alpine:latest localhost:8090/library/alpine:lab
docker push localhost:8090/library/alpine:lab

(library is Harbor's built-in public project; create your own projects in the UI.)

Push from a GitLab CI pipeline

The runner shares the host Docker socket with job containers, so a job's docker CLI talks to the host daemon — where localhost:8090 reaches Harbor, even though Harbor sits on its own docker network. That wiring comes from runner-config/runner-template.toml and is applied at registration time, so a re-registered runner keeps it. Verified end to end: a job built an image and pushed localhost:8090/library/socket-test to Harbor.

Example .gitlab-ci.yml:

build-image:
  stage: build
  image: docker:27
  variables:
    HARBOR: localhost:8090
    DOCKER_HOST: unix:///var/run/docker.sock   # use the mounted host socket
  script:
    - echo "Harbor12345" | docker login $HARBOR -u admin --password-stdin
    - |
      cat > Dockerfile <<'EOF'
      FROM alpine:latest
      RUN echo "built in CI" > /hello.txt
      EOF
    - docker build -t $HARBOR/library/ci-demo:$CI_COMMIT_SHORT_SHA .
    - docker push $HARBOR/library/ci-demo:$CI_COMMIT_SHORT_SHA

For real projects use a Harbor robot account instead of the admin password, and store it in a masked CI/CD variable rather than inline.

Managing the Harbor stack

cd harbor
docker compose ps                 # status of the 9 Harbor containers
docker compose stop               # stop Harbor (keeps data)
docker compose up -d              # start Harbor
docker compose down               # remove containers (data survives in harbor/data)
docker compose down -v && rm -rf data   # full reset

After editing harbor/harbor.yml, regenerate the compose: cd harbor && ./prepare && docker compose up -d.

Harbor gotchas

  • amd64 emulation (Apple Silicon only): Harbor ships amd64-only images; on Apple Silicon they run under emulation (works, just slower) and the platform ... does not match warnings are expected. On amd64 Linux they run natively — no warnings, noticeably faster.
  • First up may fail one container with a virtiofs single-file-mount error — Docker Desktop only; just run docker compose up -d again (it's intermittent).
  • Root-owned files (Linux only): prepare and the containers write as root, so a reset needs sudo rm -rf harbor/data harbor/common.

6. Dependency-Track (SBOM monitoring)

OWASP Dependency-Track ingests CycloneDX SBOMs and continuously tracks the components for new CVEs. It's a separate compose stack under dependency-track/, joined to the GitLab network so CI jobs can upload SBOMs.

cd dependency-track
docker compose up -d      # apiserver takes ~2-3 min on first boot
docker compose ps
docker compose down       # stop (data persists in the dtrack_data volume)

Already wired for you: an Automation API key with upload permissions is stored as the masked GitLab instance CI/CD variable DTRACK_API_KEY, so workshop modules 16 & 19 upload SBOMs with no extra setup. A demo project workshop-demo (106 components) already exists proving the flow. Full details + the CycloneDX version-compatibility note are in workshop/SECURITY.md.

Multi-arch images (apiserver + frontend) — native on amd64 and arm64.

7. HashiCorp Vault (secrets via OIDC)

Vault (dev mode, multi-arch image) in vault/, joined to the GitLab network. CI jobs fetch secrets with no stored Vault credentials — they exchange a short-lived GitLab OIDC JWT (id_tokens) for a scoped Vault token.

cd vault
docker compose up -d          # vault + one-shot init that wires JWT auth to GitLab
docker compose logs vault-init
docker compose down           # stop (dev mode: secrets reset, re-seeded on next up)

Pre-wired & verified: JWT auth trusting GitLab, role gitlab-ci (aud=vault.local, namespace_path=root, policy ci-read), and secret secret/ci/myapp. A real pipeline (workshop module 20) logged in via OIDC and read the secret end-to-end. Per-project isolation (module 21) is also wired and verified: a single templated policy scopes each project to secret/ci/<its-path>/* (roles gitlab-ci-isolated and, for protected refs, gitlab-ci-prod). Details in workshop/SECURITY.md.

Runner note: this lab's runner has clone_url=http://gitlab:8929 set so job containers can clone (GitLab's localhost:8929 external URL isn't reachable from inside a container). The register scripts apply this automatically.

8. GitLab Runner on Kubernetes (kind)

A 1-node kind cluster running the GitLab Runner Kubernetes executor — every CI job becomes a pod. Lives in k8s-runner/.

cd k8s-runner
./setup.sh glrt-XXXX     # create a runner in GitLab (tags: kubernetes,kind), pass its token
kubectl -n gitlab-runner get pods -w     # watch job pods appear when a tagged job runs
./teardown.sh            # remove the cluster + runner

Jobs route to it via tags: [kubernetes] (see workshop/24-kubernetes-executor.yml). Verified end-to-end: a pipeline ran 5 jobs as pods (incl. parallel:3 → 3 pods and a Postgres sidecar). The README documents the four gotchas solved (cluster↔GitLab networking, clone_url, helper-image architecture, and unregisterRunners: false).

The kind node is joined to the gitlab-ce-lab_default Docker network so pods resolve gitlab:8929. All images are multi-arch, and setup.sh picks the matching runner-helper build from uname -m.

9. Monitoring (Prometheus + Grafana)

Runner/CI metrics dashboards in monitoring/, joined to the GitLab network so Prometheus scrapes the runner's metrics endpoint (gitlab-runner:9252).

cd monitoring && docker compose up -d

Open the provisioned "GitLab Runner / CI" dashboard: jobs processed, jobs/min, job duration p50/p95, jobs by stage, concurrency, API errors. The runner exposes metrics because its config.toml has listen_address=":9252". GitLab app metrics are off by default (resources) — the README shows the one-block opt-in. Verified: after running pipelines, sum(gitlab_runner_jobs_total) reflected real job counts.

10. Terraform with GitLab-managed state

terraform/ — an IaC pipeline that provisions a Harbor project with Terraform, storing state in GitLab's managed Terraform HTTP backend (CE feature, no S3). Published to the root/terraform-harbor project: validate → plan (shown on MR) → apply (manual). Harbor is reached at http://harbor:8080 (its proxy is aliased onto the GitLab network). Verified: apply created the Harbor project tf-managed; state is visible under Operate → Terraform states. See terraform/README.md.

CI/CD Catalog component

The source for a CI/CD Catalog component now lives with the module that teaches it: workshop/13-components/component/. Publish it as root/ci-components, release it as trivy-scan@1.0.0, and consume it with include: - component: $CI_SERVER_FQDN/root/ci-components/trivy-scan@1.0.0 — step by step in workshop/13-components/README.md. Verified by a consumer pipeline that ran the component's trivy-scan job.

11. GitLab Agent for Kubernetes

gitlab-agent/ connects the kind cluster to GitLab the official way: an agentk pod holds a persistent link to GitLab's KAS, so the cluster is managed from GitLab (Operate → Kubernetes clusters) and deployments show in the Environments UI.

cd gitlab-agent
./setup.sh <agent-token>      # helm-install agentk into kind, pointed at KAS

Verified CONNECTED (GraphQL reports active connections, agent v19.1.0). The CI/CD tunnel (kubectl-from-CI with no stored kubeconfig) does not complete on this lab — KAS binds the tunnel token to GitLab's canonical localhost host, the same external_url theme seen elsewhere. gitlab-agent/README.md documents the root cause and the clean fix (a both-sides-reachable hostname). For CI-driven deploys the capstone uses a direct kubeconfig to the kind API instead.

12. Review Apps → kind + DAST

review-apps/ gives every merge request an ephemeral environment: the app is deployed into its own namespace in the kind cluster, OWASP ZAP scans the live URL, and the namespace is torn down when the MR is merged/closed. Project root/review-apps-demo.

Because the Agent tunnel can't complete here (§11), deploy jobs use a direct kubeconfig (file-type CI/CD variable KUBECONFIG_CI) authenticating as a least-privilege SA ci-deployer against https://gitlab-ci-control-plane:6443. The scanner reaches the app via a per-MR NodePort on the kind node. Verified: MR !1 deployed to ns review-mr-1, ZAP produced a 10-finding report, the environment showed the live URL, and stop_review deleted the namespace (env → stopped). See review-apps/README.md.

13. 🏆 GitOps capstone

gitops-capstone/ is the showcase that ties everything together. One git push runs a full secure-supply-chain pipeline — test → SAST (Semgrep) → build & push to Harbor → SBOM (Trivy → Dependency-Track) → sign (cosign) → bump the k8s manifest — and then ArgoCD (running in the kind cluster) pulls the change and deploys it. Project root/gitops-capstone.

CI never runs kubectl: it produces a signed image + desired state in git; ArgoCD converges the cluster. Verified end-to-end — a commit flowed all the way to 2/2 running pods on the exact signed image, and a second commit rolled the live version automatically (65ab3210 → 47cd8322). This exercises Harbor, Dependency- Track, cosign, kind, and ArgoCD at once. Full write-up (incl. the containerd insecure-registry and cosign-over-HTTP details) in gitops-capstone/README.md.

14. Feature showcases (features/)

18 CE features, each with a verified demo. Full index + commands in features/README.md. By theme:

Registry & packages — built-in Container Registry (now enabled, port 5050), npm, Helm, Dependency Proxy (Docker Hub cache), Terraform Module Registry, Generic + PyPI packages, and a registry cleanup policy.

Automation & ops — pipeline schedules, trigger tokens, resource_group

  • interruptible/auto-cancel, project webhooks → a sink, alerts → incidents
  • error tracking, and Auto DevOps (one toggle → build+scan pipeline).

Runners & GitOps — Flux pull-GitOps (GitLab's recommended integration; verified a git change auto-reconciling to the cluster) and group runners + autoscaling notes.

CI reporting & quality — JUnit test reports, coverage visualization + badge, Code Quality (hadolint), and Accessibility (pa11y) — all parsed by GitLab.

Ops governance & supply chain — CI/CD Secure Files, deploy freeze periods, OIDC id_token federation (verified against GitLab's JWKS), and GitLab self-monitoring metrics (the gitlab Prometheus target is now up with a "GitLab / Application" Grafana dashboard — see §9).

Platform & auth — deploy tokens & keys, project/group/personal access tokens, gitlab-backup, and push mirroring.

All CI auth is the job token (no stored creds). The recurring localhost-in- returned-URLs theme is handled per-tool (socat shim for npm/Helm/PyPI; host Docker daemon for the registry/proxy; raw protocol for Terraform; gitlab:8929 for mirroring) — see features/README.md.

Enabling the container registry required a one-time GitLab reconfigure (already done). A first boot after registry['enable'] = true takes a few extra minutes.

15. WireGuard VPN (CI into a private network)

wireguard/ runs a VPN gateway plus an isolated nginx (10.20.0.10) on a docker network nothing else joins — there is no route to it from the lab, so the tunnel is the only way in. A CI job dials it with wg-quick, the real shape of deploying to a network that isn't on the internet.

cd wireguard && ./setup.sh        # generates keys, starts the gateway, prints the CI variables

The job needs NET_ADMIN + /dev/net/tun, which are runner settings — so the capability lives on a dedicated vpn-tagged runner rather than on the runner that executes every merge request. Verified: the tunnel job fetched the private app while the same job on a runner without the capability failed at RTNETLINK answers: Operation not permitted. Walkthrough in exercises/22-vpn-wireguard.md; details in wireguard/README.md.

16. Groups, subgroups & permissions

Everything else in this lab lives under the single root user, which hides the half of GitLab that is a hierarchy: membership, roles, CI/CD variables, runners and milestones all flow downhill from a group to the projects inside it.

./scripts/seed-groups.sh <admin-token>            # build it
./scripts/seed-groups.sh <admin-token> --reset    # tear it down

Creates acme (top-level) with subgroups platform and apps, three projects and four users — alice (Owner), bob (Maintainer of platform), carol (Developer in apps), dave (Guest on acme). Nobody is added to a project; access is inherited. Verified: bob gets 404 on apps/web while carol gets 200, and vice-versa for platform/shared-ci — GitLab returns not found, never forbidden, for things you can't see. The same pipeline in two sibling projects resolves DEPLOY_TARGET differently (from-project-web vs from-subgroup-apps), proving precedence is project → subgroup → group → instance.

"Organizations" exist as a model in 19.1 (one row, Default) but are pre-release cells-architecture plumbing: no REST API, UI behind a disabled feature flag. In CE today the top-level group is the organization.

Details in features/groups-and-permissions/README.md; hands-on in exercises/25-groups-and-permissions.md.

17. Managing security (instance posture)

The lab covers application security thoroughly (§ the security modules in workshop/SECURITY.md). This is the other half: securing GitLab itself.

./scripts/security-audit.sh <admin-token>     # read-only; changes nothing

Audits sign-up, 2FA, sessions, token policy, project visibility, CI job-token scope, runner exposure, webhook SSRF and rate limits, printing the fix for each finding. On this lab it reports roughly 9 pass, 17 warn, 1 fail — and several of those warnings are deliberate (webhooks must reach local addresses for the webhook demo; runners mount the Docker socket so exercise 08 can build images). Knowing which doors are open, and why, is the point.

features/instance-security/README.md explains every finding, maps which controls exist in CE versus Premium (audit events API: 404 in CE), and lists the incident-response one-liners for revoking tokens and blocking accounts.

Everyday commands

docker compose up -d            # start
docker compose stop             # stop (keeps data)
docker compose down             # remove containers (keeps named volumes)
docker compose down -v          # nuke EVERYTHING including data
docker compose logs -f gitlab   # tail GitLab logs
docker compose exec gitlab gitlab-ctl status     # internal service status
docker compose exec gitlab gitlab-rake gitlab:check   # health check

Notes / gotchas

  • Memory: GitLab wants ~4 GB. On macOS give Docker Desktop 6–8 GB; on Linux containers use host RAM directly, so just keep 8 GB+ free.
  • Runner networking: the runner reaches GitLab via the compose service name gitlab on the internal network — that's why register-runner.sh uses http://gitlab:8929 rather than localhost. Job containers join the same gitlab-ce-lab_default network.
  • Docker socket: the runner uses socket-binding to your host Docker. Fine for a local lab; don't do this on a shared/production host. Assumes rootful Docker — see docs/UBUNTU.md if you run rootless.
  • Port binding: LAB_BIND_IP in .env (default 127.0.0.1) controls which interface the stacks publish on. On Linux, Docker's iptables rules bypass ufw, so 0.0.0.0 really does expose these default-password services to the network.
  • Architecture: amd64 and arm64 both work — the arch-specific downloads (cosign, hadolint, the runner helper image) resolve from uname -m.