Saturday, 12 September 2026

Gitlab Integration

VSIT GitLab CI/CD Curriculum

GitLab CI/CD Java Web Application Lab Series

Updated: September 10, 2026
Edition: GitLab.com-hosted runners
Primary stack: GitLab CI/CD, GitLab-hosted runners, GitHub import, Maven, Java 21, SonarQube Community Build, Snyk, JFrog Artifactory, Tomcat, and Slack

Course-wide runner rule

Students use GitLab.com-hosted runners only. They do not install or register GitLab Runner, create a runner VM, generate a runner authentication token, edit config.toml, or assign classroom-specific runner tags.

Architecture

Existing GitHub repository
        |
        | one-time import for the Free/classroom route
        v
GitLab project + .gitlab-ci.yml
        |
        v
GitLab.com-hosted runners
  | fresh isolated VM for every job
  | image: selects Maven/Java/Alpine/Debian tools
  | cache: reuses dependencies when available
  | artifacts: passes reports and binaries between jobs
        |
        +--> SonarQube over HTTPS
        +--> Snyk public API
        +--> Artifactory over HTTPS
        +--> Tomcat deployment endpoint over HTTPS
        +--> Slack notifications

Hosted-runner behavior used throughout the labs

  • GitLab.com-hosted runners are enabled by default for projects unless disabled at the project or parent-group level.
  • Untagged jobs use the default small Linux x86-64 hosted runner.
  • The default small Linux runner currently provides 2 vCPUs, 8 GB memory, and 30 GB storage.
  • Every job runs on a newly provisioned ephemeral VM that is deleted after the job.
  • Each job must specify a suitable image: or install its required tools during the job.
  • Cache is a performance optimization; artifacts are the supported way to preserve and transfer build output.
  • Hosted runners can connect outward to public internet endpoints but do not automatically join an Azure VNet.
  • Ordinary GitLab.com hosted-runner jobs do not have one fixed outbound IP.
  • Free GitLab.com namespaces currently include 400 compute minutes per month.
  • Hosted-runner jobs have a maximum runtime of three hours.
Lab Capability Outcome
Lab 00 Project onboarding Import the existing GitHub repository into GitLab and confirm hosted-runner readiness.
Lab 05.6 Hosted runner readiness Enable and verify GitLab.com’s default untagged hosted runner.
Lab 06 First pipeline Compile, test, package, and preserve a Maven WAR.
Lab 07 Code-change workflow Use VS Code, feature branches, pipelines, and merge requests.
Lab 08 Automatic pipeline triggers Use GitLab repository events and duplicate-safe workflow rules.
Lab 09 SonarQube installation Install SonarQube on Azure and expose a secured HTTPS endpoint.
Lab 09.5 Java modernization Upgrade the Maven application from JDK 8 to JDK 21.
Lab 10 SonarQube integration Run SonarScanner for Maven and enforce the Quality Gate.
Lab 16 Hosted execution model Learn images, ephemeral jobs, cache, artifacts, and hosted-runner networking.
Lab 17 Artifactory installation Install JFrog Artifactory and expose it through secured HTTPS.
Lab 18 Artifactory integration Resolve and publish Maven artifacts with JFrog CLI.
Lab 18.5 Artifact versioning Use pipeline IID/SHA snapshots and semantic release tags.
Lab 19 Slack notifications Send pipeline and deployment notifications through the GitLab for Slack app.
Lab 20 Test and coverage reports Publish JUnit and JaCoCo reports in GitLab.
Lab 20.5 Application security Run Snyk Open Source, optional Snyk Code, and monitoring.
Lab 20.6 Tomcat deployment Deploy a WAR from a hosted runner over a secured HTTPS route.
Lab 21 End-to-end mini project Combine quality, security, publishing, deployment, and notifications.

Repository files supplied

.gitlab-ci-minimal.yml                 starter pipeline for Lab 6
.gitlab-ci.yml                         complete end-to-end pipeline
VARIABLES-CHECKLIST.md                 GitLab variable and secret settings
HOSTED-RUNNER-NETWORKING.md            Azure/public-endpoint design
REVISION-NOTES.md                      changes from the self-managed-runner edition
snippets/pom-java21.xml                Java 21/versioning/coverage POM reference
scripts/print-jacoco-coverage.sh       stable GitLab coverage output
scripts/deploy-tomcat.sh               HTTPS Tomcat Manager API deployment
labs/*.md                              individual Markdown labs
blogger/*.html                         Blogger-ready HTML copies

Required network design

GitLab-hosted jobs cannot use the Azure VM’s loopback or private VNet address:

Do not use: http://localhost:9000
Do not use: http://10.0.0.5:9000
Do not use: http://172.16.0.10:8082
Do not use: http://192.168.1.25:8080

Use secured endpoints such as:

https://sonarqube.example.com
https://artifactory.example.com
https://tomcat.example.com

Keep PostgreSQL private. Place the application services behind a trusted TLS certificate and controlled ingress. Use least-privilege service tokens and expose only the routes required by CI.

Variable inventory

Category Variables
SonarQube SONAR_HOST_URL, SONAR_TOKEN, SONAR_PROJECT_KEY
Snyk SNYK_TOKEN, optional SNYK_ORG, SNYK_SEVERITY_THRESHOLD
Artifactory JF_URL, JF_USER, JF_ACCESS_TOKEN, Maven repository keys
Tomcat TOMCAT_URL, TOMCAT_USER, TOMCAT_PASSWORD, TOMCAT_CONTEXT_PATH, APP_URL

Instructor checkpoints

  1. Students can enable and verify instance runners without creating a project runner.
  2. Students can distinguish the hosted runner VM from the container image:.
  3. Students understand that every job starts clean.
  4. Students use cache for reusable downloads and artifacts for evidence/binaries.
  5. Students can diagnose pending jobs caused by disabled runners, compute quota, identity verification, or accidental tags.
  6. Students can explain why private Azure addresses are unreachable from the hosted runner.
  7. Students can trace a release from commit through tests, SonarQube, Snyk, Artifactory, Tomcat, and Slack.

Individual labs

Official platform references


Lab 0 — Import the Existing GitHub Repository into GitLab

Series assumptions

  • A Java Maven web application already exists in a GitHub repository.
  • For the Free/classroom path, import the repository into GitLab once and use the GitLab copy for branches, merge requests, and pipelines.
  • The examples assume the application POM is at MyWebApp/pom.xml. Set APP_DIR: "." when pom.xml is at the repository root.
  • Every CI/CD job uses GitLab.com-hosted runners. Students do not install Docker for a runner, install GitLab Runner, create a project runner, register a token, or edit config.toml.
  • Jobs are intentionally untagged, so GitLab.com assigns them to the default small Linux x86-64 hosted runner.
  • Every job receives a fresh, isolated, ephemeral VM. Use cache for reusable downloads and artifacts for files that later jobs must receive.
  • SonarQube, Artifactory, and Tomcat must be reachable from hosted runners through secured public HTTPS endpoints. localhost, 10.x.x.x, 172.16-31.x.x, and 192.168.x.x addresses on Azure are not reachable from the hosted runner.
  • GitLab.com does not provide one fixed outbound IP for normal hosted-runner jobs. Protect exposed services with TLS, strong service authentication, least privilege, and narrowly exposed routes rather than relying on one runner IP allow-list.
  • Never commit passwords, tokens, private keys, or service credentials.

Objective

Create the GitLab project that will host the new pipeline while preserving the original GitHub repository as a reference remote.

Existing GitHub repository
          |
          | one-time import
          v
GitLab repository
          |
          | .gitlab-ci.yml
          v
GitLab.com-hosted runner

Why this class uses an import

GitLab can run CI/CD for an external GitHub repository, but that external-repository workflow requires a qualifying paid GitLab tier. The classroom path is simpler:

  1. Import the GitHub repository into GitLab once.
  2. Keep the original GitHub repository as a reference or backup.
  3. Use the imported GitLab project for the remaining labs.

Prerequisites

  • Owner or administrator access to the GitHub repository.
  • A GitLab.com account.
  • Permission to create a project in a personal namespace or class group.
  • Git installed locally.
  • The default branch is known, normally main.

Part A — Inspect the GitHub repository

  1. Open the GitHub repository.
  2. Confirm the application source and pom.xml exist.
  3. Determine whether the POM is at pom.xml or MyWebApp/pom.xml.
  4. Confirm the default branch.
  5. Remove generated target/ directories from source control if they were committed.
  6. Search for exposed passwords, tokens, keys, or connection strings before importing.
  7. Create a backup tag:
git clone https://github.com/GITHUB-OWNER/GITHUB-REPOSITORY.git
cd GITHUB-REPOSITORY

git tag pre-gitlab-ci-migration
git push origin pre-gitlab-ci-migration

Part B — Import GitHub into GitLab

  1. Sign in to GitLab.com.
  2. Select Create new → New project/repository.
  3. Select Import project.
  4. Select GitHub.
  5. Authorize GitLab, or provide the supported GitHub token when prompted.
  6. Find the repository and select Import.
  7. Choose the destination namespace.
  8. Wait until the import completes.
  9. Open Code → Repository.
  10. Verify files, branches, and tags.
  11. Open Settings → Repository and confirm the default branch.

Part C — Update the local remotes

Preserve GitHub under the remote name github and make GitLab the active origin:

git remote rename origin github
git remote add origin https://gitlab.com/GITLAB-GROUP/GITLAB-PROJECT.git

git fetch --all --prune
git remote -v
git log --oneline --decorate --graph --all -20

Set the upstream for main:

git branch --set-upstream-to=origin/main main

For a new local checkout, clone GitLab directly:

git clone https://gitlab.com/GITLAB-GROUP/GITLAB-PROJECT.git
cd GITLAB-PROJECT

Do not force-push until both remotes have been compared.

Part D — Add the starter CI/CD files

Copy the following files from this package to the repository root:

.gitlab-ci-minimal.yml
scripts/print-jacoco-coverage.sh
scripts/deploy-tomcat.sh

Rename the starter pipeline:

cp .gitlab-ci-minimal.yml .gitlab-ci.yml
chmod +x scripts/*.sh

git add .gitlab-ci.yml scripts .gitignore
git commit -m "Add GitLab CI/CD foundation"
git push origin main

Part E — Verify hosted runners before troubleshooting the code

Open:

GitLab project
  → Settings
  → CI/CD
  → Runners

Confirm instance runners are enabled for the project. Do not select Create project runner.

Then open:

Build → Pipelines

An untagged job should be assigned to GitLab.com's hosted runner automatically. Lab 5.6 performs a dedicated runner-verification exercise.

A job can remain pending when instance runners are disabled, the namespace has exhausted its compute quota, account identity verification is required, or the YAML contains custom tags that the hosted runner does not match.

Part F — Protect the default branch

  1. Open Settings → Repository → Branch rules.
  2. Protect main.
  3. Require merge requests where the course workflow permits.
  4. Restrict direct pushes to approved maintainers.
  5. Protect release tags matching v*.

Validation

  • The GitLab project contains the source files, branches, and tags.
  • The default branch is correct.
  • Local origin points to GitLab.
  • The original GitHub repository remains available under the optional github remote.
  • .gitlab-ci.yml is visible in GitLab.
  • A simple untagged job is accepted by a GitLab-hosted runner.

Deliverable

Submit:

git remote -v
git status
git branch -vv

Also submit the GitLab project URL and a screenshot of the first pipeline.

Official references


Lab 5.6 — Enable and Verify the Default GitLab-Hosted Runner

Series assumptions

  • A Java Maven web application already exists in a GitHub repository.
  • For the Free/classroom path, import the repository into GitLab once and use the GitLab copy for branches, merge requests, and pipelines.
  • The examples assume the application POM is at MyWebApp/pom.xml. Set APP_DIR: "." when pom.xml is at the repository root.
  • Every CI/CD job uses GitLab.com-hosted runners. Students do not install Docker for a runner, install GitLab Runner, create a project runner, register a token, or edit config.toml.
  • Jobs are intentionally untagged, so GitLab.com assigns them to the default small Linux x86-64 hosted runner.
  • Every job receives a fresh, isolated, ephemeral VM. Use cache for reusable downloads and artifacts for files that later jobs must receive.
  • SonarQube, Artifactory, and Tomcat must be reachable from hosted runners through secured public HTTPS endpoints. localhost, 10.x.x.x, 172.16-31.x.x, and 192.168.x.x addresses on Azure are not reachable from the hosted runner.
  • GitLab.com does not provide one fixed outbound IP for normal hosted-runner jobs. Protect exposed services with TLS, strong service authentication, least privilege, and narrowly exposed routes rather than relying on one runner IP allow-list.
  • Never commit passwords, tokens, private keys, or service credentials.

Objective

Verify that the GitLab.com-hosted instance runner is enabled, run an untagged job on the default small Linux x86-64 runner, and identify the most common account, quota, and configuration problems.

GitLab project
    |
    | untagged CI job
    v
GitLab.com default hosted runner
    |
    | fresh ephemeral VM
    v
Container image + job script

Part A — Verify the project runner setting

  1. Open the GitLab project.
  2. Go to Settings → CI/CD.
  3. Expand Runners.
  4. In the Instance runners area, verify that Turn on instance runners for this project is enabled.
  5. Do not select Create project runner.
  6. Do not install GitLab Runner, Docker, or a runner VM.

GitLab.com enables instance runners by default, but the setting can be disabled by the project or parent group.

Part B — Check account readiness

Before running the first job:

  1. Confirm the namespace has available compute minutes.
  2. Complete identity verification if GitLab displays Identity verification is required in order to run CI jobs.
  3. Confirm the project is not read-only and the default branch accepts your push or merge request.

GitLab.com Free namespaces currently receive 400 compute minutes per month. Usage is charged to the top-level namespace that owns the project.

Part C — Create the hosted-runner verification job

Create or temporarily replace .gitlab-ci.yml with:

stages:
  - verify

verify_hosted_runner:
  stage: verify
  image: alpine:3.22
  interruptible: true
  script:
    - echo "GitLab-hosted runner is working."
    - echo "Runner description: $CI_RUNNER_DESCRIPTION"
    - echo "Runner ID: $CI_RUNNER_ID"
    - echo "Runner tags: $CI_RUNNER_TAGS"
    - echo "Runner executable architecture: $CI_RUNNER_EXECUTABLE_ARCH"
    - echo "Runner version: $CI_RUNNER_VERSION"
    - echo "Job image: $CI_JOB_IMAGE"
    - echo "Project directory: $CI_PROJECT_DIR"
    - uname -a
    - cat /etc/os-release

There is intentionally no tags: keyword. Untagged jobs use GitLab.com's default small Linux x86-64 hosted runner.

Part D — Commit and run

git add .gitlab-ci.yml
git commit -m "Verify GitLab hosted runner"
git push

Open:

Build → Pipelines → latest pipeline → verify_hosted_runner

The job should enter running and then passed without any runner installation.

Part E — Read the job log

Record:

  • CI_RUNNER_DESCRIPTION
  • CI_RUNNER_ID
  • CI_RUNNER_TAGS
  • CI_RUNNER_EXECUTABLE_ARCH
  • CI_JOB_IMAGE

The hosted runner is the GitLab-managed execution machine. The image: is the container environment placed on that runner for the job.

Part F — Understand the default capacity

The default untagged Linux hosted runner currently uses the small x86-64 machine class:

2 vCPUs
8 GB memory
30 GB storage shared by the OS, image, repository, and job files

Each job gets a new VM, and GitLab deletes it after the job finishes. A later job must not assume that the earlier job's local disk still exists.

Troubleshooting

Job remains pending

Check, in this order:

  1. Instance runners are enabled for the project and parent group.
  2. No custom tags: remain in the job or under default:.
  3. The namespace still has compute minutes.
  4. GitLab is not asking for identity verification.
  5. The selected container image exists and supports Linux x86-64.

Identity verification is required

Select Verify my account in the GitLab banner and complete the requested email, phone, or payment-method verification. Then retry the job or push another commit.

Compute quota exhausted

Review Settings → Usage quotas for the owning namespace. Wait for the monthly reset, purchase additional compute minutes, reduce unnecessary jobs, or move heavy scans to manual/scheduled execution.

Job says no GitLab-hosted runner

Remove custom classroom tags such as:

tags: [linux, docker, java21, maven]

This edition intentionally uses untagged jobs.

Validation checklist

  • Instance runners are enabled.
  • No project runner was created.
  • The job contains no tags: keyword.
  • verify_hosted_runner passes.
  • The log identifies a GitLab runner and a Linux x86-64 environment.
  • The student can explain runner VM versus container image.

Official references


Lab 6 — Automate the First Java Web Application with GitLab CI/CD

Series assumptions

  • A Java Maven web application already exists in a GitHub repository.
  • For the Free/classroom path, import the repository into GitLab once and use the GitLab copy for branches, merge requests, and pipelines.
  • The examples assume the application POM is at MyWebApp/pom.xml. Set APP_DIR: "." when pom.xml is at the repository root.
  • Every CI/CD job uses GitLab.com-hosted runners. Students do not install Docker for a runner, install GitLab Runner, create a project runner, register a token, or edit config.toml.
  • Jobs are intentionally untagged, so GitLab.com assigns them to the default small Linux x86-64 hosted runner.
  • Every job receives a fresh, isolated, ephemeral VM. Use cache for reusable downloads and artifacts for files that later jobs must receive.
  • SonarQube, Artifactory, and Tomcat must be reachable from hosted runners through secured public HTTPS endpoints. localhost, 10.x.x.x, 172.16-31.x.x, and 192.168.x.x addresses on Azure are not reachable from the hosted runner.
  • GitLab.com does not provide one fixed outbound IP for normal hosted-runner jobs. Protect exposed services with TLS, strong service authentication, least privilege, and narrowly exposed routes rather than relying on one runner IP allow-list.
  • Never commit passwords, tokens, private keys, or service credentials.

Objective

Compile, test, package, and preserve a Maven WAR with pipeline-as-code and GitLab.com's default hosted runner.

Git push → hosted runner → Maven compile → unit tests → WAR artifact

Prerequisites

  • Lab 0 completed.
  • Lab 5.6 completed.
  • The repository contains a valid Maven Java web application.
  • The project can use GitLab.com instance runners.

Part A — Confirm the application builds locally

From the repository root:

java -version
mvn -version
mvn -f MyWebApp/pom.xml clean verify
find MyWebApp/target -maxdepth 1 -type f -name '*.war' -print

Use pom.xml instead of MyWebApp/pom.xml when the POM is at the root.

Part B — Create .gitlab-ci.yml

workflow:
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
    - if: '$CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS && $CI_PIPELINE_SOURCE == "push"'
      when: never
    - if: '$CI_COMMIT_TAG'
    - if: '$CI_COMMIT_BRANCH'

stages:
  - build
  - test
  - package

default:
  interruptible: true
  retry:
    max: 1
    when:
      - runner_system_failure
      - stuck_or_timeout_failure

variables:
  APP_DIR: "MyWebApp"
  MAVEN_OPTS: "-Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository"

.maven_job:
  image: maven:3.9.16-eclipse-temurin-21-noble
  cache:
    key: "maven-${CI_PROJECT_ID}"
    paths:
      - .m2/repository/
  before_script:
    - java -version
    - mvn -version
    - test -f "$APP_DIR/pom.xml" || (echo "POM not found at $APP_DIR/pom.xml. Change APP_DIR." && exit 1)

compile:
  extends: .maven_job
  stage: build
  script:
    - mvn -f "$APP_DIR/pom.xml" -B -ntp clean compile

test:
  extends: .maven_job
  stage: test
  script:
    - mvn -f "$APP_DIR/pom.xml" -B -ntp test
  artifacts:
    when: always
    expire_in: 7 days
    paths:
      - "$APP_DIR/target/surefire-reports/"
    reports:
      junit: "$APP_DIR/target/surefire-reports/TEST-*.xml"

package_war:
  extends: .maven_job
  stage: package
  script:
    - mvn -f "$APP_DIR/pom.xml" -B -ntp -DskipTests package
  artifacts:
    name: "${CI_PROJECT_NAME}-${CI_PIPELINE_IID}"
    paths:
      - "$APP_DIR/target/*.war"
    expire_in: 7 days

There is intentionally no tags: block. The job's image: provides Maven and Java 21; the GitLab-hosted runner provides the temporary execution VM.

Part C — Understand cache versus artifacts

  • The Maven cache reduces repeated dependency downloads.
  • Cache is an optimization and is not the authoritative build output.
  • JUnit XML and WAR files are uploaded as artifacts.
  • A fresh VM runs each job, so one job cannot assume another job's local disk still exists.

Part D — Commit and run

git switch -c feature/first-gitlab-pipeline
git add .gitlab-ci.yml
git commit -m "Create first GitLab Maven pipeline"
git push -u origin feature/first-gitlab-pipeline

Open Build → Pipelines and select the pipeline.

Part E — Inspect the jobs

  1. In compile, verify the Java and Maven versions.
  2. In test, verify GitLab displays the JUnit report.
  3. In package_war, download the WAR artifact.
  4. Confirm the artifact name contains the GitLab project and pipeline IID.
  5. Review the cache log to see whether Maven dependencies were restored or uploaded.

Part F — Add a pipeline badge

Open Settings → CI/CD → General pipelines, copy the pipeline-status badge Markdown, and add it to README.md.

Hosted-runner troubleshooting

Job is pending

Check, in this order:

  1. Settings → CI/CD → Runners: instance runners are enabled.
  2. The job and default: section contain no custom tags:.
  3. The namespace still has hosted-runner compute quota.
  4. GitLab is not requesting identity verification for hosted-runner use.
  5. The pipeline was not intentionally skipped by workflow:rules.

pom.xml is not found

For a root POM:

variables:
  APP_DIR: "."

Java release error

Verify both the image and POM use Java 21:

image: maven:3.9.16-eclipse-temurin-21-noble
<maven.compiler.release>21</maven.compiler.release>

WAR is not produced

Verify the POM includes <packaging>war</packaging> and the application has the expected web structure.

Compute-minute practice

During early labs, separate jobs make stages easier to learn. Later, combine repeated Maven work when practical—for example, a single mvn clean verify job can test and package while preserving reports and the WAR. Keep interruptible: true and use rules so expensive scanners do not run on every throwaway commit.

Validation

  • No runner was installed or registered by the student.
  • No custom tags: appear in the pipeline.
  • The pipeline is green.
  • JUnit results appear in GitLab.
  • The WAR is downloadable as a job artifact.

Official references


Lab 7 — Make Code Changes in VS Code and Deliver Them Through a GitLab Merge Request

Series assumptions

  • A Java Maven web application already exists in a GitHub repository.
  • For the Free/classroom path, import the repository into GitLab once and use the GitLab copy for branches, merge requests, and pipelines.
  • The examples assume the application POM is at MyWebApp/pom.xml. Set APP_DIR: "." when pom.xml is at the repository root.
  • Every CI/CD job uses GitLab.com-hosted runners. Students do not install Docker for a runner, install GitLab Runner, create a project runner, register a token, or edit config.toml.
  • Jobs are intentionally untagged, so GitLab.com assigns them to the default small Linux x86-64 hosted runner.
  • Every job receives a fresh, isolated, ephemeral VM. Use cache for reusable downloads and artifacts for files that later jobs must receive.
  • SonarQube, Artifactory, and Tomcat must be reachable from hosted runners through secured public HTTPS endpoints. localhost, 10.x.x.x, 172.16-31.x.x, and 192.168.x.x addresses on Azure are not reachable from the hosted runner.
  • GitLab.com does not provide one fixed outbound IP for normal hosted-runner jobs. Protect exposed services with TLS, strong service authentication, least privilege, and narrowly exposed routes rather than relying on one runner IP allow-list.
  • Never commit passwords, tokens, private keys, or service credentials.

Objective

Use VS Code, Git branches, GitLab pipelines, and a merge request to change the application safely.

VS Code edit → feature branch → push → pipeline → merge request → review → merge

Part A — Clone the GitLab repository in VS Code

git clone https://gitlab.com/GITLAB-GROUP/GITLAB-PROJECT.git
cd GITLAB-PROJECT
code .

In VS Code, install the official GitLab Workflow extension if your organization permits it. The lab also works using VS Code's built-in Source Control view and the GitLab web UI.

Part B — Create a feature branch

git switch main
git pull --ff-only origin main
git switch -c feature/update-home-page

Never perform the exercise directly on protected main.

Part C — Change the web application

Locate the page rendered by the application, for example:

MyWebApp/src/main/webapp/index.jsp

Make a visible change such as:

<h1>Deployed by GitLab CI/CD</h1>
<p>Commit: ${CI_COMMIT_SHORT_SHA}</p>

The environment variable will not automatically become a JSP variable at runtime; use it only when the application build is explicitly configured to filter resources. For a basic lab, simply use static text or update a version property that Maven filters.

Part D — Test before pushing

mvn -f MyWebApp/pom.xml clean verify

Review the change:

git status
git diff

Part E — Commit and push

git add MyWebApp/src/main/webapp/index.jsp
git commit -m "Update application home page"
git push -u origin feature/update-home-page

The push automatically creates a branch pipeline because .gitlab-ci.yml is in the repository.

Part F — Create the merge request

  1. Open the GitLab project.
  2. Select Code → Merge requests → New merge request.
  3. Source: feature/update-home-page.
  4. Target: main.
  5. Use a clear title and description.
  6. Include validation steps and a screenshot if useful.
  7. Create the merge request.
  8. Wait for the merge request pipeline to pass.
  9. Review the Changes, Pipelines, and Tests tabs.
  10. Merge only when required checks are green.

Part G — Pull the merged result

git switch main
git pull --ff-only origin main
git branch -d feature/update-home-page

Create .gitlab/merge_request_templates/Default.md:

## What changed

## Why

## How it was tested

- [ ] `mvn clean verify` passes
- [ ] GitLab pipeline passes
- [ ] No secret or generated build output was committed
- [ ] Deployment impact reviewed

## Evidence

## Rollback

Validation

  • The change was made on a feature branch.
  • The branch pipeline passed.
  • A merge request documented the change.
  • The merge request pipeline passed before merge.
  • main contains the approved change.

Official references


Lab 8 — Configure Automatic GitLab Pipeline Triggers

Series assumptions

  • A Java Maven web application already exists in a GitHub repository.
  • For the Free/classroom path, import the repository into GitLab once and use the GitLab copy for branches, merge requests, and pipelines.
  • The examples assume the application POM is at MyWebApp/pom.xml. Set APP_DIR: "." when pom.xml is at the repository root.
  • Every CI/CD job uses GitLab.com-hosted runners. Students do not install Docker for a runner, install GitLab Runner, create a project runner, register a token, or edit config.toml.
  • Jobs are intentionally untagged, so GitLab.com assigns them to the default small Linux x86-64 hosted runner.
  • Every job receives a fresh, isolated, ephemeral VM. Use cache for reusable downloads and artifacts for files that later jobs must receive.
  • SonarQube, Artifactory, and Tomcat must be reachable from hosted runners through secured public HTTPS endpoints. localhost, 10.x.x.x, 172.16-31.x.x, and 192.168.x.x addresses on Azure are not reachable from the hosted runner.
  • GitLab.com does not provide one fixed outbound IP for normal hosted-runner jobs. Protect exposed services with TLS, strong service authentication, least privilege, and narrowly exposed routes rather than relying on one runner IP allow-list.
  • Never commit passwords, tokens, private keys, or service credentials.

Objective

Understand how GitLab starts pipelines from repository events and configure workflow:rules so pushes, merge requests, and tags produce the intended pipelines without duplicates.

Key difference

When the repository is hosted in GitLab, no repository-to-CI webhook has to be created. GitLab sees the commit and evaluates .gitlab-ci.yml natively.

Push to GitLab repository
       |
       v
GitLab reads .gitlab-ci.yml
       |
       v
Pipeline is created
       |
       v
GitLab-hosted runner executes jobs

A webhook is still appropriate when GitLab must notify an external service, but it is not required merely to start GitLab CI/CD for a GitLab-hosted project.

Part A — Add duplicate-safe workflow rules

Place this at the top of .gitlab-ci.yml:

workflow:
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
    - if: '$CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS && $CI_PIPELINE_SOURCE == "push"'
      when: never
    - if: '$CI_COMMIT_TAG'
    - if: '$CI_COMMIT_BRANCH'

Behavior:

  • A merge request gets an MR pipeline.
  • When that branch has an open MR, the duplicate push pipeline is suppressed.
  • Tags get tag pipelines.
  • Ordinary branch pushes still get branch pipelines.

Part B — Add job-level rules

Use lightweight checks on branches/MRs, and reserve publishing/deployment for the default branch or tags.

compile_and_test:
  stage: test
  script: mvn -f "$APP_DIR/pom.xml" -B -ntp verify
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
    - if: '$CI_COMMIT_BRANCH'

publish_release:
  stage: publish
  script: echo "Publish $CI_COMMIT_TAG"
  rules:
    - if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/'

Part C — Test a branch push

git switch -c feature/trigger-test
echo "Trigger test" >> README.md
git add README.md
git commit -m "Test branch pipeline trigger"
git push -u origin feature/trigger-test

Verify a branch pipeline appears.

Part D — Test a merge request pipeline

Create an MR from feature/trigger-test to main. Push another commit to the same branch and verify that the MR pipeline updates without an unnecessary duplicate branch pipeline.

Part E — Test a tag pipeline

After merging an approved commit:

git switch main
git pull --ff-only origin main
git tag -a v1.0.0 -m "Release 1.0.0"
git push origin v1.0.0

Verify a tag pipeline is created.

Part F — Optional scheduled pipeline

  1. Go to Build → Pipeline schedules.
  2. Select New schedule.
  3. Name it nightly-security-scan.
  4. Choose a cron/timezone.
  5. Target the default branch.
  6. Add a schedule-only rule to the desired job:
nightly_scan:
  stage: security
  script: echo "Run deep nightly security scan"
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'

When GitHub must remain canonical

The main course uses a one-time GitHub import and makes GitLab active. When GitHub must stay canonical, GitLab's dedicated external-repository CI/CD connection is Premium/Ultimate. That workflow uses repository mirroring and a GitHub webhook. Do not build a classroom Free-tier dependency around it unless the required tier is available.

Validation matrix

Action Expected pipeline
Push to branch without MR Branch pipeline
Open/update MR Merge request pipeline
Push to branch with open MR MR pipeline; duplicate branch pipeline suppressed
Push semantic release tag Tag pipeline
Run schedule Only schedule-enabled jobs

Official references


Lab 9 — Install SonarQube Community Build on Azure for GitLab CI/CD

Series assumptions

  • A Java Maven web application already exists in a GitHub repository.
  • For the Free/classroom path, import the repository into GitLab once and use the GitLab copy for branches, merge requests, and pipelines.
  • The examples assume the application POM is at MyWebApp/pom.xml. Set APP_DIR: "." when pom.xml is at the repository root.
  • Every CI/CD job uses GitLab.com-hosted runners. Students do not install Docker for a runner, install GitLab Runner, create a project runner, register a token, or edit config.toml.
  • Jobs are intentionally untagged, so GitLab.com assigns them to the default small Linux x86-64 hosted runner.
  • Every job receives a fresh, isolated, ephemeral VM. Use cache for reusable downloads and artifacts for files that later jobs must receive.
  • SonarQube, Artifactory, and Tomcat must be reachable from hosted runners through secured public HTTPS endpoints. localhost, 10.x.x.x, 172.16-31.x.x, and 192.168.x.x addresses on Azure are not reachable from the hosted runner.
  • GitLab.com does not provide one fixed outbound IP for normal hosted-runner jobs. Protect exposed services with TLS, strong service authentication, least privilege, and narrowly exposed routes rather than relying on one runner IP allow-list.
  • Never commit passwords, tokens, private keys, or service credentials.

Objective

Install SonarQube Community Build 26.9.0.129388 with Java 21 and PostgreSQL on Ubuntu 24.04, then publish it through a protected HTTPS endpoint that GitLab.com-hosted runners can reach.

GitLab-hosted runner
        |
        | HTTPS 443
        v
sonarqube.example.com / reverse proxy
        |
        | localhost:9000
        v
SonarQube
        |
        | localhost:5432
        v
PostgreSQL

Part A — Prepare the Azure VM

Recommended classroom VM:

Setting Lab value
OS Ubuntu Server 24.04 LTS x64
CPU 2 vCPU minimum
RAM 8 GB recommended
Disk 64 GB SSD
SSH TCP 22 from the administrator's IP only
Public CI endpoint HTTPS 443
SonarQube service Port 9000 bound locally/private
PostgreSQL Port 5432 local only

Create a DNS record such as:

sonarqube.example.com → AZURE_PUBLIC_IP

Do not expose PostgreSQL publicly.

Part B — Install packages

sudo apt update
sudo apt upgrade -y
sudo apt install -y \
  openjdk-21-jdk \
  postgresql \
  postgresql-contrib \
  unzip \
  curl \
  nginx \
  certbot \
  python3-certbot-nginx

java -version
psql --version
nginx -v

Part C — Configure PostgreSQL

Generate and securely record a strong database password, then run:

sudo -u postgres psql
CREATE USER sonarqube WITH ENCRYPTED PASSWORD 'REPLACE_WITH_STRONG_PASSWORD';
CREATE DATABASE sonarqube OWNER sonarqube ENCODING 'UTF8';
\q

Test locally:

psql -h 127.0.0.1 -U sonarqube -d sonarqube -W -c 'select current_database(), current_user;'

Part D — Configure Linux limits

sudo tee /etc/sysctl.d/99-sonarqube.conf >/dev/null <<'EOF'
vm.max_map_count=524288
fs.file-max=131072
EOF

sudo sysctl --system
sysctl vm.max_map_count
sysctl fs.file-max

Create the service account and limits:

sudo useradd --system --home /opt/sonarqube --shell /bin/bash sonarqube

sudo tee /etc/security/limits.d/99-sonarqube.conf >/dev/null <<'EOF'
sonarqube   -   nofile   131072
sonarqube   -   nproc    8192
EOF

Part E — Download SonarQube

export SONAR_VERSION='26.9.0.129388'
cd /tmp

curl -fLO \
  "https://binaries.sonarsource.com/Distribution/sonarqube/sonarqube-${SONAR_VERSION}.zip"

sudo unzip "sonarqube-${SONAR_VERSION}.zip" -d /opt
sudo mv "/opt/sonarqube-${SONAR_VERSION}" /opt/sonarqube
sudo chown -R sonarqube:sonarqube /opt/sonarqube

Part F — Configure SonarQube

Edit:

sudo nano /opt/sonarqube/conf/sonar.properties

Set:

sonar.jdbc.username=sonarqube
sonar.jdbc.password=REPLACE_WITH_STRONG_PASSWORD
sonar.jdbc.url=jdbc:postgresql://127.0.0.1/sonarqube

sonar.web.host=127.0.0.1
sonar.web.port=9000

Binding SonarQube to 127.0.0.1 prevents direct public access to port 9000. Nginx will accept HTTPS and proxy requests locally.

Part G — Create the systemd service

sudo tee /etc/systemd/system/sonarqube.service >/dev/null <<'EOF'
[Unit]
Description=SonarQube Community Build
After=network.target postgresql.service

[Service]
Type=simple
User=sonarqube
Group=sonarqube
WorkingDirectory=/opt/sonarqube
ExecStart=/opt/sonarqube/bin/linux-x86-64/sonar.sh console
Restart=on-failure
RestartSec=10
LimitNOFILE=131072
LimitNPROC=8192
TimeoutStopSec=60

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now sonarqube
sudo systemctl status sonarqube --no-pager

Watch startup:

sudo journalctl -u sonarqube -f

Local health check:

curl --fail http://127.0.0.1:9000/api/system/status

Part H — Configure Nginx

Replace sonarqube.example.com with your DNS name:

sudo tee /etc/nginx/sites-available/sonarqube >/dev/null <<'EOF'
server {
    listen 80;
    server_name sonarqube.example.com;

    location / {
        proxy_pass http://127.0.0.1:9000;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
EOF

sudo ln -s /etc/nginx/sites-available/sonarqube /etc/nginx/sites-enabled/sonarqube
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx

Part I — Enable HTTPS

After DNS resolves to the VM:

sudo certbot --nginx -d sonarqube.example.com
sudo certbot renew --dry-run

Open only the required Azure NSG ports:

22  — administrator IP only
80  — required for initial certificate flow/redirect
443 — public HTTPS endpoint

Do not open 5432. Port 9000 can remain closed externally.

Part J — Complete the initial setup

Open:

https://sonarqube.example.com

Initial credentials are admin / admin. Change the administrator password immediately.

Then configure the server base URL in SonarQube to the public HTTPS URL.

Part K — Prove hosted-runner reachability

Before integrating a scanner, temporarily add this job:

sonarqube_connectivity:
  image: alpine:3.22
  stage: validate
  before_script:
    - apk add --no-cache curl ca-certificates
  script:
    - test "$SONAR_HOST_URL" = "${SONAR_HOST_URL#http://}" || (echo "SONAR_HOST_URL must use HTTPS" && exit 1)
    - curl --fail --show-error --silent "$SONAR_HOST_URL/api/system/status"

Set:

SONAR_HOST_URL=https://sonarqube.example.com

A private Azure IP or localhost:9000 will not work from a GitLab.com-hosted runner.

Security boundaries

  • Use a publicly trusted TLS certificate.
  • Keep PostgreSQL private.
  • Keep SonarQube's native port private and expose it through Nginx.
  • Create a SonarQube project token for CI; do not use the administrator password.
  • Apply SonarQube permissions so the CI token can analyze only what it needs.
  • Because ordinary GitLab.com runner egress has no single fixed IP, do not build the lab around a one-IP NSG allow-list.

Troubleshooting

SonarQube does not start

sudo journalctl -u sonarqube -n 200 --no-pager
sudo tail -n 200 /opt/sonarqube/logs/sonar.log
sudo tail -n 200 /opt/sonarqube/logs/web.log
sudo tail -n 200 /opt/sonarqube/logs/es.log

Check Java 21, PostgreSQL credentials, ownership, free disk space, and kernel limits.

Nginx returns 502

curl -v http://127.0.0.1:9000/api/system/status
sudo ss -lntp | grep 9000
sudo nginx -t

Browser works but hosted-runner job fails

Check DNS, certificate chain, HTTPS port 443, Azure NSG, and any web application firewall. Do not test with a private IP URL.

Validation

  • SonarQube and PostgreSQL start after reboot.
  • SonarQube listens locally on 9000.
  • The public UI uses HTTPS.
  • PostgreSQL is not internet-exposed.
  • A temporary untagged GitLab CI job can query /api/system/status.

Official references


Lab 9.5 — Upgrade the Maven Web Application from JDK 8 to JDK 21 in GitLab CI/CD

Series assumptions

  • A Java Maven web application already exists in a GitHub repository.
  • For the Free/classroom path, import the repository into GitLab once and use the GitLab copy for branches, merge requests, and pipelines.
  • The examples assume the application POM is at MyWebApp/pom.xml. Set APP_DIR: "." when pom.xml is at the repository root.
  • Every CI/CD job uses GitLab.com-hosted runners. Students do not install Docker for a runner, install GitLab Runner, create a project runner, register a token, or edit config.toml.
  • Jobs are intentionally untagged, so GitLab.com assigns them to the default small Linux x86-64 hosted runner.
  • Every job receives a fresh, isolated, ephemeral VM. Use cache for reusable downloads and artifacts for files that later jobs must receive.
  • SonarQube, Artifactory, and Tomcat must be reachable from hosted runners through secured public HTTPS endpoints. localhost, 10.x.x.x, 172.16-31.x.x, and 192.168.x.x addresses on Azure are not reachable from the hosted runner.
  • GitLab.com does not provide one fixed outbound IP for normal hosted-runner jobs. Protect exposed services with TLS, strong service authentication, least privilege, and narrowly exposed routes rather than relying on one runner IP allow-list.
  • Never commit passwords, tokens, private keys, or service credentials.

Objective

Modernize the project deliberately: establish a baseline, update the compiler/runtime, upgrade compatible dependencies and plugins, run tests, and verify the WAR on the intended Tomcat version.

JDK 21 migration and Jakarta namespace migration are separate decisions. A legacy application using javax.servlet.* can remain on a current Tomcat 9 release while it is moved to JDK 21. Moving to Tomcat 10+ normally requires changing Java EE javax.* APIs to Jakarta jakarta.* APIs.

Part A — Establish a JDK 8 baseline

Create a migration branch:

git switch main
git pull --ff-only origin main
git switch -c upgrade/jdk21

Record the old build:

java -version
mvn -version
mvn -f MyWebApp/pom.xml -B -ntp clean verify
mvn -f MyWebApp/pom.xml -B -ntp dependency:tree > dependency-tree-jdk8.txt

Commit only useful migration notes—not generated target/ output.

Part B — Update the GitLab build image

Use:

image: maven:3.9.16-eclipse-temurin-21-noble

Part C — Update the POM compiler configuration

At minimum:

<properties>
  <maven.compiler.release>21</maven.compiler.release>
  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-compiler-plugin</artifactId>
      <version>3.16.0</version>
      <configuration>
        <release>21</release>
      </configuration>
    </plugin>
  </plugins>
</build>

Using release is preferable to independently setting source and target, because it also constrains the available Java API surface.

Part D — Enforce the toolchain

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-enforcer-plugin</artifactId>
  <version>3.6.3</version>
  <executions>
    <execution>
      <id>enforce-java-and-maven</id>
      <goals><goal>enforce</goal></goals>
      <configuration>
        <rules>
          <requireJavaVersion><version>[21,22)</version></requireJavaVersion>
          <requireMavenVersion><version>[3.9,4)</version></requireMavenVersion>
        </rules>
      </configuration>
    </execution>
  </executions>
</plugin>

Part E — Upgrade build plugins

Recommended current versions used in this package:

maven-compiler-plugin 3.16.0
maven-surefire-plugin 3.6.0
maven-war-plugin 3.5.1
maven-enforcer-plugin 3.6.3
maven-deploy-plugin 3.1.4
jacoco-maven-plugin 0.8.15
flatten-maven-plugin 1.8.0
sonar-maven-plugin 5.8.0.7211

Copy snippets/pom-java21.xml as a reference, but merge it with the application's real dependencies rather than replacing the POM blindly.

Part F — Find migration problems

Run:

mvn -f MyWebApp/pom.xml -B -ntp clean verify
mvn -f MyWebApp/pom.xml -B -ntp dependency:tree
mvn -f MyWebApp/pom.xml -B -ntp versions:display-dependency-updates
mvn -f MyWebApp/pom.xml -B -ntp versions:display-plugin-updates

Review, test, and approve upgrades individually. Do not automatically accept every latest version.

Typical issues include:

  • Removed JDK modules previously bundled in JDK 8.
  • Old annotation processors or bytecode tools.
  • Libraries using deep reflection blocked by the module system.
  • Tests relying on old JVM internals.
  • Old servlet APIs conflicting with the target Tomcat generation.
  • Source files using identifiers or syntax that changed across Java versions.

Part G — Validate in GitLab

Push the migration branch:

git add .gitlab-ci.yml MyWebApp/pom.xml
git commit -m "Upgrade Maven application to Java 21"
git push -u origin upgrade/jdk21

Create an MR. Require the compile, unit test, coverage, Snyk, and Sonar jobs that are available to pass.

Part H — Runtime smoke test

Deploy the WAR to a nonproduction Tomcat environment that is known to support the application's servlet API generation. Confirm:

  • Tomcat starts on JDK 21.
  • The application deploys without class-loading errors.
  • Health page returns HTTP 200.
  • Main application path works.
  • Logs contain no unsupported reflective-access or linkage failures.

Validation

mvn -f MyWebApp/pom.xml -version
mvn -f MyWebApp/pom.xml clean verify
javap -verbose MyWebApp/target/classes/PATH/TO/A/CLASS.class | grep "major version"

Java 21 class files normally show major version 65.

Official references


Lab 10 — Integrate SonarQube with GitLab CI/CD

Series assumptions

  • A Java Maven web application already exists in a GitHub repository.
  • For the Free/classroom path, import the repository into GitLab once and use the GitLab copy for branches, merge requests, and pipelines.
  • The examples assume the application POM is at MyWebApp/pom.xml. Set APP_DIR: "." when pom.xml is at the repository root.
  • Every CI/CD job uses GitLab.com-hosted runners. Students do not install Docker for a runner, install GitLab Runner, create a project runner, register a token, or edit config.toml.
  • Jobs are intentionally untagged, so GitLab.com assigns them to the default small Linux x86-64 hosted runner.
  • Every job receives a fresh, isolated, ephemeral VM. Use cache for reusable downloads and artifacts for files that later jobs must receive.
  • SonarQube, Artifactory, and Tomcat must be reachable from hosted runners through secured public HTTPS endpoints. localhost, 10.x.x.x, 172.16-31.x.x, and 192.168.x.x addresses on Azure are not reachable from the hosted runner.
  • GitLab.com does not provide one fixed outbound IP for normal hosted-runner jobs. Protect exposed services with TLS, strong service authentication, least privilege, and narrowly exposed routes rather than relying on one runner IP allow-list.
  • Never commit passwords, tokens, private keys, or service credentials.

Objective

Run a Maven SonarQube analysis from an untagged GitLab.com-hosted runner and make the pipeline wait for the Quality Gate result.

Prerequisites

  • Lab 9 completed.
  • SonarQube is reachable at a public HTTPS URL such as https://sonarqube.example.com.
  • The TLS certificate is trusted by standard Linux CA bundles.
  • The GitLab project can use hosted runners.
  • The Maven project builds on Java 21.

Part A — Create the SonarQube project and token

  1. Sign in to SonarQube.
  2. Select Create project → Local project.
  3. Use a stable project key, for example mywebapp.
  4. Create a token dedicated to GitLab CI.
  5. Copy the token once and store it securely.

Do not place the token in pom.xml, .gitlab-ci.yml, shell history, screenshots, or source control.

Part B — Add GitLab variables

Open Settings → CI/CD → Variables and add:

Variable Example Controls
SONAR_HOST_URL https://sonarqube.example.com Protected as appropriate
SONAR_TOKEN generated token Masked + hidden + protected
SONAR_PROJECT_KEY mywebapp Protected as appropriate

A protected token is available only to protected branches and tags. Make the job rules and branch protection agree.

Part C — Add a connectivity preflight

sonarqube_connectivity:
  image: alpine:3.22
  stage: validate
  before_script:
    - apk add --no-cache curl ca-certificates
  script:
    - test -n "$SONAR_HOST_URL"
    - test "$SONAR_HOST_URL" = "${SONAR_HOST_URL#http://}" || (echo "Use HTTPS" && exit 1)
    - curl --fail --show-error --silent "$SONAR_HOST_URL/api/system/status"
  rules:
    - if: '$SONAR_HOST_URL != null && $SONAR_HOST_URL != ""'

This proves public DNS, TLS, NSG/firewall routing, and the SonarQube service before Maven starts.

Part D — Add the analysis job

sonarqube_check:
  image: maven:3.9.16-eclipse-temurin-21-noble
  stage: quality
  variables:
    GIT_DEPTH: "0"
    SONAR_USER_HOME: "$CI_PROJECT_DIR/.sonar"
    MAVEN_OPTS: "-Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository"
  cache:
    key: "sonar-${CI_PROJECT_ID}"
    paths:
      - .sonar/cache/
      - .m2/repository/
  script:
    - >-
      mvn -f "$APP_DIR/pom.xml" -B -ntp clean verify
      org.sonarsource.scanner.maven:sonar-maven-plugin:5.8.0.7211:sonar
      -Dsonar.projectKey="$SONAR_PROJECT_KEY"
      -Dsonar.host.url="$SONAR_HOST_URL"
      -Dsonar.token="$SONAR_TOKEN"
      -Dsonar.qualitygate.wait=true
  rules:
    - if: '$SONAR_TOKEN == null || $SONAR_TOKEN == "" || $SONAR_HOST_URL == null || $SONAR_HOST_URL == ""'
      when: never
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'

There is no tags: block. GitLab.com schedules the job on its default hosted runner.

Pinning the scanner plugin version prevents an unreviewed scanner update from changing the build unexpectedly.

Part E — Run the first analysis

git add .gitlab-ci.yml
git commit -m "Integrate SonarQube with GitLab CI"
git push

Open the pipeline and confirm:

  1. Maven compiles the project.
  2. Tests run.
  3. The scanner uploads the analysis.
  4. The job waits for the Quality Gate.
  5. The SonarQube project shows the commit and metrics.

Part F — Prove the Quality Gate controls CI

For a safe classroom demonstration:

  1. Create a temporary quality gate or threshold that the current project will fail.
  2. Assign it to the lab project.
  3. Run the pipeline and observe the job fail.
  4. Restore the intended gate or improve the code/tests.
  5. Rerun and confirm the job passes.

Do not weaken the main policy merely to make a red build green.

Hosted-runner networking rules

The runner can reach a normal public HTTPS endpoint. It cannot reach:

http://localhost:9000
http://10.0.0.4:9000
http://192.168.1.20:9000

localhost would refer to the temporary job environment, not the Azure SonarQube VM. GitLab.com does not publish one fixed outbound IP for ordinary hosted-runner jobs, so a firewall rule for one observed job IP is fragile.

Common failures

Connection refused or timeout

Run the connectivity job. Confirm the DNS record, Azure NSG port 443, reverse proxy, and SonarQube service.

TLS or certificate error

Use a publicly trusted certificate and include the full certificate chain. Do not permanently disable certificate verification.

Unauthorized

Verify SONAR_TOKEN, its permissions, variable protection, and whether the job runs on a protected ref.

Project key not found or duplicated

Verify SONAR_PROJECT_KEY exactly matches the SonarQube project key.

Quality Gate timeout

Check SonarQube Compute Engine logs, webhook/network health, server capacity, and scanner timeout settings before increasing timeouts.

Validation

  • The job is untagged and runs on a GitLab-hosted runner.
  • SonarQube is reached over HTTPS.
  • The token is stored only as a GitLab variable.
  • Analysis appears in SonarQube.
  • sonar.qualitygate.wait=true can block the pipeline.

Official references


Lab 16 — Use the Default GitLab-Hosted Runner and Control the Job Environment

Series assumptions

  • A Java Maven web application already exists in a GitHub repository.
  • For the Free/classroom path, import the repository into GitLab once and use the GitLab copy for branches, merge requests, and pipelines.
  • The examples assume the application POM is at MyWebApp/pom.xml. Set APP_DIR: "." when pom.xml is at the repository root.
  • Every CI/CD job uses GitLab.com-hosted runners. Students do not install Docker for a runner, install GitLab Runner, create a project runner, register a token, or edit config.toml.
  • Jobs are intentionally untagged, so GitLab.com assigns them to the default small Linux x86-64 hosted runner.
  • Every job receives a fresh, isolated, ephemeral VM. Use cache for reusable downloads and artifacts for files that later jobs must receive.
  • SonarQube, Artifactory, and Tomcat must be reachable from hosted runners through secured public HTTPS endpoints. localhost, 10.x.x.x, 172.16-31.x.x, and 192.168.x.x addresses on Azure are not reachable from the hosted runner.
  • GitLab.com does not provide one fixed outbound IP for normal hosted-runner jobs. Protect exposed services with TLS, strong service authentication, least privilege, and narrowly exposed routes rather than relying on one runner IP allow-list.
  • Never commit passwords, tokens, private keys, or service credentials.

Objective

Translate the old Jenkins “build on this agent” concept into the hosted-runner model. Students will use the default untagged runner, choose tools with image:, inspect runner metadata, and pass files between fresh jobs with artifacts.

Key concept

With GitLab.com-hosted runners, you do not select one persistent server by name. GitLab assigns an ephemeral VM to each job. The pipeline controls the software environment with a container image.

Jenkins model                    Hosted-runner model
-------------                    -------------------
Persistent agent label     →     Untagged default hosted runner
Tools installed on agent   →     image: Maven/Alpine/Debian image
Agent workspace            →     Fresh job directory
Copy between workspaces    →     GitLab job artifacts
Dependency reuse           →     GitLab distributed cache

Part A — Confirm the default runner policy

Open Settings → CI/CD → Runners and confirm instance runners are enabled. Do not create a project runner.

Search .gitlab-ci.yml for tags::

grep -n "^[[:space:]]*tags:" .gitlab-ci.yml || true

For this course, the command should return no matching job tags.

Part B — Run two jobs with different images

Add this temporary pipeline:

stages:
  - inspect
  - build

inspect_runner:
  stage: inspect
  image: alpine:3.22
  script:
    - echo "First job runner ID: $CI_RUNNER_ID"
    - echo "First job runner description: $CI_RUNNER_DESCRIPTION"
    - echo "First job image: $CI_JOB_IMAGE"
    - uname -a
    - cat /etc/os-release
    - printf 'created-by-job=%s\n' "$CI_JOB_ID" > runner-evidence.txt
  artifacts:
    name: "runner-evidence-$CI_PIPELINE_IID"
    paths:
      - runner-evidence.txt
    expire_in: 1 day

verify_maven_environment:
  stage: build
  image: maven:3.9.16-eclipse-temurin-21-noble
  needs:
    - job: inspect_runner
      artifacts: true
  script:
    - echo "Second job runner ID: $CI_RUNNER_ID"
    - echo "Second job image: $CI_JOB_IMAGE"
    - cat runner-evidence.txt
    - java -version
    - mvn -version

Neither job has tags:. Both use the default hosted runner fleet, but each receives a fresh job environment.

Part C — Commit and inspect

git add .gitlab-ci.yml
git commit -m "Explore the GitLab hosted runner environment"
git push

Open both job logs and compare:

  • runner ID and description
  • container image
  • operating-system information
  • available commands
  • presence of runner-evidence.txt in the second job

The artifact, not shared local disk, makes the file available to the second job.

Part D — Understand image:

Use a Maven image when the job needs Maven and Java:

java_build:
  image: maven:3.9.16-eclipse-temurin-21-noble
  script:
    - java -version
    - mvn -version

Use a small Alpine image for lightweight shell or API work:

api_check:
  image: alpine:3.22
  before_script:
    - apk add --no-cache curl
  script:
    - curl --version

The image selects tools; it does not create a long-lived machine.

Part E — Cache versus artifacts

Use cache for dependencies that can be regenerated:

cache:
  key: "maven-$CI_PROJECT_ID"
  paths:
    - .m2/repository/

Use artifacts for output that another job or a student must consume:

artifacts:
  paths:
    - MyWebApp/target/*.war

A cache is an optimization and might be absent. A job must still work by downloading dependencies again. An artifact is the supported pipeline output-transfer mechanism.

Part F — Compute-efficient design

Every separate job consumes hosted-runner compute time. During early labs, separate jobs make stages easy to understand. In the final pipeline, avoid rebuilding the same code unnecessarily, use rules: to skip irrelevant jobs, mark replaceable branch jobs interruptible: true, and reserve deep scans for the default branch, tags, or schedules.

Part G — Hosted-runner network boundary

Hosted runners can make outbound connections to the public internet. They do not automatically join your Azure VNet. Therefore:

Works:      https://sonarqube.example.com
Does not:   http://10.0.1.10:9000
Does not:   http://localhost:9000

GitLab does not provide one static outbound IP for ordinary hosted-runner jobs. Do not build SonarQube, Artifactory, or Tomcat access around a single source-IP allowlist.

Troubleshooting

Job is pending

  • Confirm instance runners are enabled.
  • Remove accidental tags: entries.
  • Check compute quota and identity-verification banners.
  • Confirm the job image exists.

Command not found

The command is not in the selected image. Choose a more suitable image or install the package in before_script.

File from the previous job is missing

Declare it under artifacts:paths in the producing job and use a later stage or needs:artifacts in the consuming job.

Maven dependencies download every time

Confirm .m2/repository/ is under cache:paths. Remember cache availability is not guaranteed, so the build must remain correct without it.

Validation checklist

  • No self-managed runner was installed.
  • Jobs are untagged.
  • Alpine and Maven jobs both run successfully.
  • The second job receives the first job's artifact.
  • The student can explain runner, image, cache, and artifact as four different concepts.
  • The student understands the public-internet-only network boundary.

Official references


Lab 17 — Install JFrog Artifactory OSS for GitLab CI/CD

Series assumptions

  • A Java Maven web application already exists in a GitHub repository.
  • For the Free/classroom path, import the repository into GitLab once and use the GitLab copy for branches, merge requests, and pipelines.
  • The examples assume the application POM is at MyWebApp/pom.xml. Set APP_DIR: "." when pom.xml is at the repository root.
  • Every CI/CD job uses GitLab.com-hosted runners. Students do not install Docker for a runner, install GitLab Runner, create a project runner, register a token, or edit config.toml.
  • Jobs are intentionally untagged, so GitLab.com assigns them to the default small Linux x86-64 hosted runner.
  • Every job receives a fresh, isolated, ephemeral VM. Use cache for reusable downloads and artifacts for files that later jobs must receive.
  • SonarQube, Artifactory, and Tomcat must be reachable from hosted runners through secured public HTTPS endpoints. localhost, 10.x.x.x, 172.16-31.x.x, and 192.168.x.x addresses on Azure are not reachable from the hosted runner.
  • GitLab.com does not provide one fixed outbound IP for normal hosted-runner jobs. Protect exposed services with TLS, strong service authentication, least privilege, and narrowly exposed routes rather than relying on one runner IP allow-list.
  • Never commit passwords, tokens, private keys, or service credentials.

Objective

Install a single-node Artifactory OSS lab server with Docker Compose, create Maven repositories, create a least-privilege CI identity, and expose only a secured HTTPS endpoint to GitLab.com-hosted runners.

Part A — Prepare the VM

Recommended shared-class VM:

Setting Value
OS Ubuntu 24.04 LTS
CPU 4 vCPU recommended
RAM 8–16 GB
Disk 100 GB or more on persistent SSD
SSH Port 22 from administrator IP only
Local Artifactory router Port 8082 on the VM/private network
Hosted-runner endpoint Public HTTPS 443 through reverse proxy/load balancer
PostgreSQL Private/internal only

Create DNS:

artifactory.example.com → ARTIFACTORY_PUBLIC_IP

Artifactory is resource intensive. Do not use the smallest burstable VM for a multi-student class.

Part B — Install Docker

sudo apt update
sudo apt install -y ca-certificates curl gnupg nginx certbot python3-certbot-nginx

sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
  | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

. /etc/os-release
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $VERSION_CODENAME stable" \
  | sudo tee /etc/apt/sources.list.d/docker.list >/dev/null

sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker
sudo docker run --rm hello-world
sudo docker compose version

Part C — Download the current JFrog Compose package

Use the version supplied by the current JFrog installation page. This package is structured for version 7.161.15:

mkdir -p ~/artifactory-install
cd ~/artifactory-install

export ARTIFACTORY_VERSION='7.161.15'

curl -g -L -O -J \
  "https://releases.jfrog.io/artifactory/bintray-artifactory/org/artifactory/oss/docker/jfrog-artifactory-oss/${ARTIFACTORY_VERSION}/jfrog-artifactory-oss-${ARTIFACTORY_VERSION}-compose.tar.gz"

tar -xvf "jfrog-artifactory-oss-${ARTIFACTORY_VERSION}-compose.tar.gz"
ART_DIR="$(find . -maxdepth 1 -type d -name 'artifactory-oss-*' | head -n 1)"
cd "$ART_DIR"

When JFrog publishes a newer package, use the current full version and verify its installation notes before changing the variable.

Part D — Run JFrog's configuration helper

sudo bash ./config.sh

Use generated strong secrets and store them safely. For this classroom lab, use the supplied PostgreSQL Compose configuration. A production design should use JFrog's supported production architecture and backup requirements.

Part E — Start PostgreSQL and Artifactory

sudo docker compose -p rt-postgres -f docker-compose-postgres.yaml up -d
sudo docker compose -p rt up -d

Inspect:

sudo docker compose -p rt-postgres -f docker-compose-postgres.yaml ps
sudo docker compose -p rt ps
sudo docker compose -p rt logs --tail=200 -f

Readiness can take several minutes. Use health checks and logs rather than treating “container running” as “application ready.”

Part F — Complete local onboarding

From an administrator network, first verify locally:

curl -I http://127.0.0.1:8082

Complete the first-login wizard, change bootstrap credentials immediately, and record the repository base URL.

Part G — Publish Artifactory through HTTPS

Configure Nginx or an Azure Application Gateway/load balancer so:

https://artifactory.example.com → http://127.0.0.1:8082

A basic Nginx starting point is:

server {
    listen 80;
    server_name artifactory.example.com;

    client_max_body_size 2g;

    location / {
        proxy_pass http://127.0.0.1:8082;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_read_timeout 900;
        proxy_send_timeout 900;
    }
}

After DNS resolves:

sudo certbot --nginx -d artifactory.example.com
sudo certbot renew --dry-run

Do not expose PostgreSQL. Avoid exposing raw 8082 publicly when HTTPS 443 is available.

Part H — Create Maven repositories

Create or verify:

libs-release-local       local releases
libs-snapshot-local      local snapshots
maven-central            remote Maven Central proxy
maven-all                virtual repository aggregating the above

Record the exact repository keys.

Part I — Create a least-privilege CI identity

Create a service user or service identity and token with only:

  • Read access to maven-all.
  • Deploy/write permissions to libs-release-local and libs-snapshot-local.
  • Permission to publish the required build information.
  • No platform-administrator permission.

Do not use the global administrator password in GitLab CI.

Part J — Verify from a hosted runner

Add variables:

JF_URL=https://artifactory.example.com
JF_USER=gitlab-ci
JF_ACCESS_TOKEN=<secret>

Temporarily run:

artifactory_connectivity:
  image: alpine:3.22
  stage: validate
  before_script:
    - apk add --no-cache curl ca-certificates
  script:
    - test "$JF_URL" = "${JF_URL#http://}" || (echo "JF_URL must use HTTPS" && exit 1)
    - curl --fail --show-error --silent --user "$JF_USER:$JF_ACCESS_TOKEN" "${JF_URL%/}/artifactory/api/system/ping"

Because hosted-runner egress is dynamic, do not approve one observed CI IP and assume future jobs will use it.

Part K — Persistence and operations

  • Place Artifactory and PostgreSQL data on durable disks.
  • Back up the database, filestore, and configuration.
  • Monitor disk capacity, inodes, memory, and container health.
  • Protect the CI token and rotate it.
  • Do not delete Compose data directories during routine restarts.

Validation

  • PostgreSQL and Artifactory containers are healthy.
  • The UI is available over HTTPS.
  • The raw database port is private.
  • Maven local, remote, and virtual repositories exist.
  • The CI identity is not an administrator.
  • An untagged hosted-runner job can authenticate and receive the Artifactory ping response.

Official references


Lab 18 — Integrate GitLab CI/CD with JFrog Artifactory

Series assumptions

  • A Java Maven web application already exists in a GitHub repository.
  • For the Free/classroom path, import the repository into GitLab once and use the GitLab copy for branches, merge requests, and pipelines.
  • The examples assume the application POM is at MyWebApp/pom.xml. Set APP_DIR: "." when pom.xml is at the repository root.
  • Every CI/CD job uses GitLab.com-hosted runners. Students do not install Docker for a runner, install GitLab Runner, create a project runner, register a token, or edit config.toml.
  • Jobs are intentionally untagged, so GitLab.com assigns them to the default small Linux x86-64 hosted runner.
  • Every job receives a fresh, isolated, ephemeral VM. Use cache for reusable downloads and artifacts for files that later jobs must receive.
  • SonarQube, Artifactory, and Tomcat must be reachable from hosted runners through secured public HTTPS endpoints. localhost, 10.x.x.x, 172.16-31.x.x, and 192.168.x.x addresses on Azure are not reachable from the hosted runner.
  • GitLab.com does not provide one fixed outbound IP for normal hosted-runner jobs. Protect exposed services with TLS, strong service authentication, least privilege, and narrowly exposed routes rather than relying on one runner IP allow-list.
  • Never commit passwords, tokens, private keys, or service credentials.

Objective

Use JFrog CLI inside an untagged GitLab-hosted job to resolve Maven dependencies, publish snapshot/release artifacts, and attach build information to the GitLab pipeline.

Prerequisites

  • Lab 17 completed.
  • Artifactory is reachable through a publicly resolvable HTTPS URL.
  • The TLS certificate is trusted by standard Linux CA bundles.
  • A least-privilege CI token exists.
  • Repository keys are known.

Part A — Create GitLab variables

Open Settings → CI/CD → Variables:

Variable Example Recommended controls
JF_URL https://artifactory.example.com Protected as appropriate
JF_USER gitlab-ci Protected as appropriate
JF_ACCESS_TOKEN access token Masked + hidden + protected
JF_MAVEN_VIRTUAL_REPO maven-all Protected as appropriate
JF_MAVEN_RELEASE_REPO libs-release-local Protected as appropriate
JF_MAVEN_SNAPSHOT_REPO libs-snapshot-local Protected as appropriate

Do not use a private Azure IP or localhost:8082 as JF_URL.

Part B — Test connectivity and credentials

artifactory_ping:
  image: alpine:3.22
  stage: validate
  before_script:
    - apk add --no-cache curl ca-certificates
  script:
    - test "$JF_URL" = "${JF_URL#http://}" || (echo "JF_URL must use HTTPS" && exit 1)
    - curl --fail --show-error --silent --user "$JF_USER:$JF_ACCESS_TOKEN" "${JF_URL%/}/artifactory/api/system/ping"
  rules:
    - if: '$JF_ACCESS_TOKEN != null && $JF_ACCESS_TOKEN != ""'

Part C — Configure JFrog CLI and Maven

publish_artifactory:
  image: maven:3.9.16-eclipse-temurin-21-noble
  stage: publish
  before_script:
    - apt-get update && apt-get install -y --no-install-recommends curl ca-certificates
    - curl -fL https://install-cli.jfrog.io | sh
    - jf --version
    - jf config add ci-artifactory --url="$JF_URL" --user="$JF_USER" --access-token="$JF_ACCESS_TOKEN" --interactive=false
    - jf config use ci-artifactory
    - jf rt ping
  script:
    - cd "$APP_DIR"
    - >-
      jf mvn-config
      --server-id-resolve=ci-artifactory
      --repo-resolve-releases="$JF_MAVEN_VIRTUAL_REPO"
      --repo-resolve-snapshots="$JF_MAVEN_VIRTUAL_REPO"
      --server-id-deploy=ci-artifactory
      --repo-deploy-releases="$JF_MAVEN_RELEASE_REPO"
      --repo-deploy-snapshots="$JF_MAVEN_SNAPSHOT_REPO"
    - >-
      jf mvn -B -ntp clean deploy
      --build-name="$CI_PROJECT_PATH_SLUG"
      --build-number="$CI_PIPELINE_IID"
    - jf rt build-add-git "$CI_PROJECT_PATH_SLUG" "$CI_PIPELINE_IID"
    - jf rt build-publish "$CI_PROJECT_PATH_SLUG" "$CI_PIPELINE_IID" --build-url="$CI_PIPELINE_URL"
  rules:
    - if: '$JF_ACCESS_TOKEN == null || $JF_ACCESS_TOKEN == "" || $JF_URL == null || $JF_URL == ""'
      when: never
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH || $CI_COMMIT_TAG'

There is no tags: section. The tools are installed inside the temporary job environment each time.

jf mvn-config creates non-secret project configuration in the job workspace. JFrog credentials remain in the ephemeral runtime and must not be committed.

Part D — Run and verify

  1. Push a default-branch commit or release tag.
  2. Open the publish_artifactory job log.
  3. Confirm dependency resolution uses the virtual repository.
  4. Locate the POM and WAR in the correct snapshot or release repository.
  5. Open JFrog build information.
  6. Verify the build name, CI_PIPELINE_IID, Git commit, and pipeline URL.

Hosted-runner network model

The job originates from GitLab-managed infrastructure and connects outward to JF_URL. Artifactory must therefore have:

  • Public DNS.
  • HTTPS 443.
  • A publicly trusted certificate.
  • Strong service authentication.
  • Least-privilege repository permissions.

Do not permanently disable TLS checks. Do not depend on one observed hosted-runner IP address.

Common errors

401 or 403

Check the token, username, token expiry, repository permissions, variable protection, and exact repository keys.

TLS failure

Install a publicly trusted certificate with the complete chain. For a private CA, the CA would need to be injected into every ephemeral job, which complicates a beginner lab.

Artifact goes to the wrong repository

A Maven version ending in -SNAPSHOT goes to the snapshot repository; a release version does not. Complete Lab 18.5.

jf mvn says the project is not configured

Run jf mvn-config in the directory containing the root POM before jf mvn.

Job passes but artifact is absent

Inspect the Maven packaging type, JFrog deployment repository configuration, permissions, and the deploy goal output.

Validation

  • The job runs without custom runner tags.
  • Artifactory is reached over HTTPS.
  • One versioned POM and WAR are published to the correct repository.
  • JFrog build information links back to the GitLab pipeline.
  • No administrator credential appears in the job or variables.

Official references


Lab 18.5 — Version Maven Artifacts with GitLab Pipeline Metadata and Release Tags

Series assumptions

  • A Java Maven web application already exists in a GitHub repository.
  • For the Free/classroom path, import the repository into GitLab once and use the GitLab copy for branches, merge requests, and pipelines.
  • The examples assume the application POM is at MyWebApp/pom.xml. Set APP_DIR: "." when pom.xml is at the repository root.
  • Every CI/CD job uses GitLab.com-hosted runners. Students do not install Docker for a runner, install GitLab Runner, create a project runner, register a token, or edit config.toml.
  • Jobs are intentionally untagged, so GitLab.com assigns them to the default small Linux x86-64 hosted runner.
  • Every job receives a fresh, isolated, ephemeral VM. Use cache for reusable downloads and artifacts for files that later jobs must receive.
  • SonarQube, Artifactory, and Tomcat must be reachable from hosted runners through secured public HTTPS endpoints. localhost, 10.x.x.x, 172.16-31.x.x, and 192.168.x.x addresses on Azure are not reachable from the hosted runner.
  • GitLab.com does not provide one fixed outbound IP for normal hosted-runner jobs. Protect exposed services with TLS, strong service authentication, least privilege, and narrowly exposed routes rather than relying on one runner IP allow-list.
  • Never commit passwords, tokens, private keys, or service credentials.

Objective

Replace mutable or ambiguous artifact names with deterministic snapshot and release versions built from GitLab metadata.

Version policy

Pipeline type Example version Destination
Branch/default-branch build 1.0.417-a1b2c3d4-SNAPSHOT Snapshot repository
Tag v2.3.0 2.3.0 Release repository

Use CI_PIPELINE_IID, not CI_PIPELINE_ID, when you want a project-local incrementing number.

Part A — Enable Maven CI-friendly versions

Set the project version:

<version>${revision}${sha1}${changelist}</version>

<properties>
  <revision>1.0.0</revision>
  <sha1></sha1>
  <changelist>-SNAPSHOT</changelist>
</properties>

For Maven 3 install/deploy, add the Flatten Maven Plugin:

<plugin>
  <groupId>org.codehaus.mojo</groupId>
  <artifactId>flatten-maven-plugin</artifactId>
  <version>1.8.0</version>
  <configuration>
    <updatePomFile>true</updatePomFile>
    <flattenMode>resolveCiFriendliesOnly</flattenMode>
  </configuration>
  <executions>
    <execution>
      <id>flatten</id>
      <phase>process-resources</phase>
      <goals><goal>flatten</goal></goals>
    </execution>
    <execution>
      <id>flatten-clean</id>
      <phase>clean</phase>
      <goals><goal>clean</goal></goals>
    </execution>
  </executions>
</plugin>

Part B — Generate version variables once

prepare_version:
  stage: prepare
  image: alpine:3.22
  script:
    - |
      if [ -n "${CI_COMMIT_TAG:-}" ]; then
        case "$CI_COMMIT_TAG" in
          v*) MAVEN_REVISION="${CI_COMMIT_TAG#v}" ;;
          *)  MAVEN_REVISION="$CI_COMMIT_TAG" ;;
        esac
        MAVEN_SHA1=""
        MAVEN_CHANGELIST=""
      else
        MAVEN_REVISION="1.0.${CI_PIPELINE_IID}"
        MAVEN_SHA1="-${CI_COMMIT_SHORT_SHA}"
        MAVEN_CHANGELIST="-SNAPSHOT"
      fi
      echo "MAVEN_REVISION=$MAVEN_REVISION" >> build.env
      echo "MAVEN_SHA1=$MAVEN_SHA1" >> build.env
      echo "MAVEN_CHANGELIST=$MAVEN_CHANGELIST" >> build.env
      echo "APP_VERSION=${MAVEN_REVISION}${MAVEN_SHA1}${MAVEN_CHANGELIST}" >> build.env
  artifacts:
    reports:
      dotenv: build.env

Part C — Apply the version to Maven

mvn -f "$APP_DIR/pom.xml" -B -ntp   -Drevision="$MAVEN_REVISION"   -Dsha1="$MAVEN_SHA1"   -Dchangelist="$MAVEN_CHANGELIST"   clean deploy

Part D — Test a snapshot

Push a normal commit to the default branch. Verify:

  • Version ends with -SNAPSHOT.
  • Pipeline IID and short SHA make it traceable.
  • Artifact lands in libs-snapshot-local.
  • Build information references the GitLab pipeline.

Part E — Test a release

Create an annotated semantic tag on an approved commit:

git switch main
git pull --ff-only origin main
git tag -a v2.3.0 -m "Release 2.3.0"
git push origin v2.3.0

Verify:

  • Version is 2.3.0 with no -SNAPSHOT.
  • Artifact lands in libs-release-local.
  • Reusing the same release version is rejected by repository immutability policy where configured.

Part F — Add tag validation

release_tag_guard:
  stage: validate
  image: alpine:3.22
  script:
    - echo "$CI_COMMIT_TAG" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$'
  rules:
    - if: '$CI_COMMIT_TAG'

Rules

  • Never release from an unreviewed commit.
  • Never overwrite a released binary with different content.
  • Preserve SHA-256 checksums.
  • Keep Git tag, Maven version, JFrog build info, and deployment record linked.
  • Use a new patch version rather than deleting/replacing an existing release.

Official references


Lab 19 — Send GitLab Pipeline and Deployment Notifications to Slack

Series assumptions

  • A Java Maven web application already exists in a GitHub repository.
  • For the Free/classroom path, import the repository into GitLab once and use the GitLab copy for branches, merge requests, and pipelines.
  • The examples assume the application POM is at MyWebApp/pom.xml. Set APP_DIR: "." when pom.xml is at the repository root.
  • Every CI/CD job uses GitLab.com-hosted runners. Students do not install Docker for a runner, install GitLab Runner, create a project runner, register a token, or edit config.toml.
  • Jobs are intentionally untagged, so GitLab.com assigns them to the default small Linux x86-64 hosted runner.
  • Every job receives a fresh, isolated, ephemeral VM. Use cache for reusable downloads and artifacts for files that later jobs must receive.
  • SonarQube, Artifactory, and Tomcat must be reachable from hosted runners through secured public HTTPS endpoints. localhost, 10.x.x.x, 172.16-31.x.x, and 192.168.x.x addresses on Azure are not reachable from the hosted runner.
  • GitLab.com does not provide one fixed outbound IP for normal hosted-runner jobs. Protect exposed services with TLS, strong service authentication, least privilege, and narrowly exposed routes rather than relying on one runner IP allow-list.
  • Never commit passwords, tokens, private keys, or service credentials.

Objective

Install the native GitLab for Slack app and notify a class/team channel about pipeline and deployment status without adding webhook secrets to pipeline code.

Part A — Prepare Slack

  1. Create or choose a Slack channel, for example #devops-pipeline.
  2. Confirm you have permission to install apps in the workspace.
  3. For a private channel, plan to add the GitLab app to the channel explicitly.

Part B — Install from GitLab

  1. Open the GitLab project or group.
  2. Go to Settings → Integrations.
  3. Select GitLab for Slack app.
  4. Select Install GitLab for Slack app.
  5. Choose the Slack workspace.
  6. Select Allow.
  7. Return to the GitLab integration page.

Use the native app; the older Slack notifications integration is deprecated.

Part C — Configure notifications

Under Trigger, enable the events useful to this curriculum:

  • Pipeline.
  • Deployment.
  • Merge request.
  • Tag push.

For each selected event, enter the Slack channel, for example:

#devops-pipeline

Optional noise controls:

  • Notify only broken pipelines for failure-focused alerts.
  • Notify only when status changes to avoid repeated same-state messages.
  • Limit notifications to the default branch.

GitLab allows up to ten comma-separated channel names for each selected event.

For a private channel, mention @GitLab in Slack and select Add to Channel.

Part D — Test the integration

  1. Select Test settings in GitLab.
  2. Save the integration.
  3. Push a harmless commit.
  4. Verify a pipeline notification appears.
  5. Run the manual deployment job in Lab 21.
  6. Verify deployment start/finish notifications.

Part E — Deliberately test a failure

On a temporary branch, add a controlled failing command:

notification_failure_test:
  stage: validate
  image: alpine:3.22
  script:
    - echo "Controlled failure for Slack notification lab"
    - exit 1

Push, verify Slack reports failure, then remove the temporary job immediately.

Optional ChatOps

The GitLab for Slack app supports project commands such as /gitlab help. A configured manual deployment action can also be exposed through GitLab's Slack deployment command, subject to the user's GitLab permissions. Keep production approvals and authorization controls in GitLab.

Validation

  • Test message reaches the correct channel.
  • Pipeline success/failure behavior matches the notification settings.
  • Deployment notifications contain a link back to GitLab.
  • No Slack webhook or token is committed to the repository.

Official references


Lab 20 — Publish JUnit and JaCoCo Coverage in GitLab

Series assumptions

  • A Java Maven web application already exists in a GitHub repository.
  • For the Free/classroom path, import the repository into GitLab once and use the GitLab copy for branches, merge requests, and pipelines.
  • The examples assume the application POM is at MyWebApp/pom.xml. Set APP_DIR: "." when pom.xml is at the repository root.
  • Every CI/CD job uses GitLab.com-hosted runners. Students do not install Docker for a runner, install GitLab Runner, create a project runner, register a token, or edit config.toml.
  • Jobs are intentionally untagged, so GitLab.com assigns them to the default small Linux x86-64 hosted runner.
  • Every job receives a fresh, isolated, ephemeral VM. Use cache for reusable downloads and artifacts for files that later jobs must receive.
  • SonarQube, Artifactory, and Tomcat must be reachable from hosted runners through secured public HTTPS endpoints. localhost, 10.x.x.x, 172.16-31.x.x, and 192.168.x.x addresses on Azure are not reachable from the hosted runner.
  • GitLab.com does not provide one fixed outbound IP for normal hosted-runner jobs. Protect exposed services with TLS, strong service authentication, least privilege, and narrowly exposed routes rather than relying on one runner IP allow-list.
  • Never commit passwords, tokens, private keys, or service credentials.

Objective

Generate JUnit and JaCoCo reports, display the total percentage in GitLab, preserve the HTML report, and provide line-level coverage information to merge requests.

Part A — Add JaCoCo to pom.xml

<properties>
  <jacoco.maven.plugin.version>0.8.15</jacoco.maven.plugin.version>
</properties>

<plugin>
  <groupId>org.jacoco</groupId>
  <artifactId>jacoco-maven-plugin</artifactId>
  <version>${jacoco.maven.plugin.version}</version>
  <executions>
    <execution>
      <id>prepare-agent</id>
      <goals>
        <goal>prepare-agent</goal>
      </goals>
    </execution>
    <execution>
      <id>report</id>
      <phase>verify</phase>
      <goals>
        <goal>report</goal>
      </goals>
    </execution>
  </executions>
</plugin>

Test locally:

mvn -f MyWebApp/pom.xml clean verify
find MyWebApp/target/site/jacoco -maxdepth 1 -type f -print

Expected files include:

index.html
jacoco.xml
jacoco.csv

Part B — Add the coverage helper

Use the supplied script:

chmod +x scripts/print-jacoco-coverage.sh

It prints a stable line such as:

Total 82.47%

Part C — Publish reports

unit_tests:
  image: maven:3.9.16-eclipse-temurin-21-noble
  stage: test
  script:
    - mvn -f "$APP_DIR/pom.xml" -B -ntp clean verify
    - ./scripts/print-jacoco-coverage.sh "$APP_DIR/target/site/jacoco/jacoco.csv"
  coverage: '/Total.*?([0-9]{1,3}\.[0-9]{2})%/'
  artifacts:
    when: always
    expire_in: 14 days
    paths:
      - "$APP_DIR/target/site/jacoco/"
      - "$APP_DIR/target/surefire-reports/"
      - "$APP_DIR/target/failsafe-reports/"
    reports:
      junit:
        - "$APP_DIR/target/surefire-reports/TEST-*.xml"
        - "$APP_DIR/target/failsafe-reports/TEST-*.xml"
      coverage_report:
        coverage_format: jacoco
        path: "$APP_DIR/target/site/jacoco/jacoco.xml"

The job is intentionally untagged. GitLab.com's hosted runner starts a fresh VM, runs the Maven image, and uploads the reports before deleting the VM. Without artifacts:, the reports disappear with that temporary job environment.

Part D — Verify in GitLab

  1. Run a pipeline.
  2. Open unit_tests and find the Total nn.nn% line.
  3. Open the pipeline test report.
  4. Download and open the JaCoCo HTML artifact.
  5. Open a merge request and inspect coverage visualization after processing.

Part E — Add an optional threshold

After establishing a realistic baseline, add a JaCoCo check. Example 60% line coverage:

<execution>
  <id>coverage-check</id>
  <phase>verify</phase>
  <goals>
    <goal>check</goal>
  </goals>
  <configuration>
    <rules>
      <rule>
        <element>BUNDLE</element>
        <limits>
          <limit>
            <counter>LINE</counter>
            <value>COVEREDRATIO</value>
            <minimum>0.60</minimum>
          </limit>
        </limits>
      </rule>
    </rules>
  </configuration>
</execution>

Coverage is evidence of exercised code, not proof that the assertions are meaningful.

Troubleshooting

  • No XML report: run verify, not only test, and confirm the report execution phase.
  • 0% displayed: inspect jacoco.csv and the helper output/regex.
  • Tests absent: confirm Surefire emits TEST-*.xml and artifacts use when: always.
  • Report existed during the job but is unavailable later: ensure its directory is listed under artifacts.paths.
  • SonarQube coverage absent: run analysis after verify and verify the JaCoCo XML path.

Validation

  • The untagged job runs on a hosted runner.
  • JUnit test results appear in GitLab.
  • The coverage percentage appears in pipeline/MR views.
  • JaCoCo XML is provided as a coverage report.
  • JaCoCo HTML is downloadable as an artifact.

Official references


Lab 20.5 — Integrate Snyk Security Scanning into GitLab CI/CD

Series assumptions

  • A Java Maven web application already exists in a GitHub repository.
  • For the Free/classroom path, import the repository into GitLab once and use the GitLab copy for branches, merge requests, and pipelines.
  • The examples assume the application POM is at MyWebApp/pom.xml. Set APP_DIR: "." when pom.xml is at the repository root.
  • Every CI/CD job uses GitLab.com-hosted runners. Students do not install Docker for a runner, install GitLab Runner, create a project runner, register a token, or edit config.toml.
  • Jobs are intentionally untagged, so GitLab.com assigns them to the default small Linux x86-64 hosted runner.
  • Every job receives a fresh, isolated, ephemeral VM. Use cache for reusable downloads and artifacts for files that later jobs must receive.
  • SonarQube, Artifactory, and Tomcat must be reachable from hosted runners through secured public HTTPS endpoints. localhost, 10.x.x.x, 172.16-31.x.x, and 192.168.x.x addresses on Azure are not reachable from the hosted runner.
  • GitLab.com does not provide one fixed outbound IP for normal hosted-runner jobs. Protect exposed services with TLS, strong service authentication, least privilege, and narrowly exposed routes rather than relying on one runner IP allow-list.
  • Never commit passwords, tokens, private keys, or service credentials.

Objective

Run Snyk Open Source dependency analysis, optionally run Snyk Code, retain machine-readable evidence, and monitor the default branch—all on GitLab.com's hosted runner.

Part A — Add the Snyk token

  1. Create a Snyk CI/service-account token according to your organization policy.
  2. In GitLab, open Settings → CI/CD → Variables.
  3. Add SNYK_TOKEN as masked, hidden, and protected.
  4. Optionally add SNYK_ORG.
  5. Never echo the token or store it in the repository.

Part B — Add an Open Source scan

snyk_open_source:
  image: maven:3.9.16-eclipse-temurin-21-noble
  stage: security
  before_script:
    - apt-get update && apt-get install -y --no-install-recommends curl ca-certificates
    - curl --compressed --fail --location https://downloads.snyk.io/cli/stable/snyk-linux -o /usr/local/bin/snyk
    - chmod +x /usr/local/bin/snyk
    - snyk --version
  script:
    - cd "$APP_DIR"
    - snyk test --severity-threshold="$SNYK_SEVERITY_THRESHOLD" --json-file-output=snyk-open-source.json
  artifacts:
    when: always
    expire_in: 14 days
    paths:
      - "$APP_DIR/snyk-open-source.json"
  rules:
    - if: '$SNYK_TOKEN == null || $SNYK_TOKEN == ""'
      when: never
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH || $CI_COMMIT_TAG'

For a Maven multi-module reactor, evaluate whether --maven-aggregate-project is appropriate from the root POM.

Part C — Add Snyk Code

snyk_code:
  image: maven:3.9.16-eclipse-temurin-21-noble
  stage: security
  before_script:
    - apt-get update && apt-get install -y --no-install-recommends curl ca-certificates
    - curl --compressed --fail --location https://downloads.snyk.io/cli/stable/snyk-linux -o /usr/local/bin/snyk
    - chmod +x /usr/local/bin/snyk
    - snyk --version
  script:
    - snyk code test "$APP_DIR" --severity-threshold=high --sarif-file-output="$APP_DIR/snyk-code.sarif"
  allow_failure: true
  artifacts:
    when: always
    expire_in: 14 days
    paths:
      - "$APP_DIR/snyk-code.sarif"
  rules:
    - if: '$SNYK_TOKEN == null || $SNYK_TOKEN == ""'
      when: never
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'

Start SAST as nonblocking while the baseline and exception process are being established; move it to blocking after triage.

Part D — Monitor the default branch

snyk_monitor:
  image: maven:3.9.16-eclipse-temurin-21-noble
  stage: publish
  before_script:
    - apt-get update && apt-get install -y --no-install-recommends curl ca-certificates
    - curl --compressed --fail --location https://downloads.snyk.io/cli/stable/snyk-linux -o /usr/local/bin/snyk
    - chmod +x /usr/local/bin/snyk
  script:
    - cd "$APP_DIR"
    - snyk monitor --project-name="$CI_PROJECT_PATH" --target-reference="${CI_COMMIT_TAG:-$CI_COMMIT_REF_NAME}"
  rules:
    - if: '$SNYK_TOKEN == null || $SNYK_TOKEN == ""'
      when: never
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH || $CI_COMMIT_TAG'

test evaluates the current build; monitor uploads a dependency snapshot for later vulnerability alerts.

Part E — Understand the hosted-runner behavior

Each Snyk job begins in a clean temporary VM/container. The pipeline therefore downloads the CLI during every job. That is predictable for training, but it consumes network time and hosted-runner compute minutes.

Ways to reduce unnecessary usage:

  • Run scans only on the default branch, release tags, or merge requests that require them.
  • Use workflow:rules to avoid duplicate branch and merge-request pipelines.
  • Mark safe jobs interruptible: true so superseded pipelines can be canceled.
  • Combine compatible Snyk checks only when it does not reduce clarity or evidence.
  • Retain JSON/SARIF artifacts for a limited period.

Part F — Establish an enforcement policy

Recommended progression:

  1. Record findings without blocking.
  2. Block new high/critical dependency findings.
  3. Require approved ignores with owner, reason, and expiration.
  4. Block high SAST findings after baseline triage.
  5. Monitor default-branch and release snapshots.

Do not permanently add || true to conceal a failing security gate.

Validation

  • The jobs contain no custom runner tags.
  • The Snyk CLI version appears in the log.
  • JSON and SARIF evidence is downloadable.
  • A controlled vulnerable dependency can fail the policy gate.
  • Removing or upgrading it returns the pipeline to green.
  • snyk monitor creates/updates the intended Snyk project.

Official references


Lab 20.6 — Deploy the WAR to Tomcat from a GitLab-Hosted Runner

Series assumptions

  • A Java Maven web application already exists in a GitHub repository.
  • For the Free/classroom path, import the repository into GitLab once and use the GitLab copy for branches, merge requests, and pipelines.
  • The examples assume the application POM is at MyWebApp/pom.xml. Set APP_DIR: "." when pom.xml is at the repository root.
  • Every CI/CD job uses GitLab.com-hosted runners. Students do not install Docker for a runner, install GitLab Runner, create a project runner, register a token, or edit config.toml.
  • Jobs are intentionally untagged, so GitLab.com assigns them to the default small Linux x86-64 hosted runner.
  • Every job receives a fresh, isolated, ephemeral VM. Use cache for reusable downloads and artifacts for files that later jobs must receive.
  • SonarQube, Artifactory, and Tomcat must be reachable from hosted runners through secured public HTTPS endpoints. localhost, 10.x.x.x, 172.16-31.x.x, and 192.168.x.x addresses on Azure are not reachable from the hosted runner.
  • GitLab.com does not provide one fixed outbound IP for normal hosted-runner jobs. Protect exposed services with TLS, strong service authentication, least privilege, and narrowly exposed routes rather than relying on one runner IP allow-list.
  • Never commit passwords, tokens, private keys, or service credentials.

Objective

Replace Jenkins' Deploy-to-Container plugin with a manual GitLab deployment job that downloads the WAR artifact from an earlier job and uploads it to Tomcat's Manager text API over HTTPS.

Hosted-runner constraint

The GitLab-hosted runner cannot reach a private Tomcat IP inside Azure. TOMCAT_URL must resolve publicly, preferably through a reverse proxy that exposes only the required authenticated deployment route over HTTPS.

GitLab-hosted runner
      |
      | HTTPS PUT
      v
Reverse proxy / controlled ingress
      |
      v
Tomcat Manager text API
      |
      v
/mywebapp

This direct endpoint is appropriate for a controlled classroom lab. For a production private network, use a private deployment architecture rather than broadly exposing Tomcat Manager.

Part A — Create a deployment-only Tomcat identity

Edit $CATALINA_BASE/conf/tomcat-users.xml:

<role rolename="manager-script"/>
<user username="gitlab-deployer"
      password="REPLACE_WITH_A_STRONG_RANDOM_PASSWORD"
      roles="manager-script"/>

Do not grant manager-gui to the CI identity. Restart Tomcat after editing the file.

Part B — Secure the public route

  1. Put Tomcat behind HTTPS with a publicly trusted certificate.
  2. Keep the normal Manager HTML interface unavailable to the public internet.
  3. Route only the minimum Manager text path required by the deployment job.
  4. Require Tomcat authentication.
  5. Apply rate limiting, logging, and any available web-application firewall controls.
  6. Keep port 8005 and other Tomcat control ports closed.
  7. Do not depend on one GitLab runner IP; hosted-runner egress is dynamic.

Test the exact public endpoint from a trusted terminal:

curl --fail --show-error --silent   --user 'gitlab-deployer:REPLACE'   'https://tomcat.example.com/manager/text/list'

Part C — Add GitLab variables

Under Settings → CI/CD → Variables, add:

Variable Example Controls
TOMCAT_URL https://tomcat.example.com Protected
TOMCAT_USER gitlab-deployer Protected as appropriate
TOMCAT_PASSWORD strong password Masked + hidden + protected
TOMCAT_CONTEXT_PATH /mywebapp Protected
APP_URL https://app.example.com/mywebapp Protected

Part D — Preserve the WAR as an artifact

The package job must upload the WAR:

package_war:
  stage: package
  image: maven:3.9.16-eclipse-temurin-21-noble
  script:
    - mvn -f "$APP_DIR/pom.xml" -B -ntp -DskipTests package
  artifacts:
    paths:
      - "$APP_DIR/target/*.war"
    expire_in: 30 days

Part E — Add the deployment job

Use the supplied scripts/deploy-tomcat.sh and add:

deploy_tomcat:
  stage: deploy
  image: debian:13-slim
  needs:
    - job: package_war
      artifacts: true
  before_script:
    - apt-get update
    - apt-get install -y --no-install-recommends curl ca-certificates
  script:
    - ./scripts/deploy-tomcat.sh
  environment:
    name: production
    url: "$APP_URL"
  resource_group: production
  rules:
    - if: '$TOMCAT_USER == null || $TOMCAT_USER == "" || $TOMCAT_PASSWORD == null || $TOMCAT_PASSWORD == ""'
      when: never
    - if: '$CI_COMMIT_TAG'
      when: manual
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
      when: manual
    - when: never

There is no tags: block. The default GitLab-hosted runner executes the job.

needs:artifacts downloads the WAR into the new deployment VM. resource_group: production prevents two production deployments from running concurrently.

Part F — Run and verify

  1. Run a successful default-branch or release-tag pipeline.
  2. Select the manual deploy_tomcat job.
  3. Confirm the job log contains Tomcat's OK response.
  4. Open APP_URL.
  5. Confirm GitLab records the production environment and deployment.
  6. Verify the deployed version/checksum matches the pipeline artifact.

Troubleshooting

Connection timeout

The endpoint is not publicly reachable from the hosted runner. Check public DNS, HTTPS ingress, firewall rules, and reverse-proxy routing.

401 Unauthorized

Check the deployment username/password, variable protection, and whether the user has manager-script.

403 Forbidden

Check Tomcat Manager access restrictions and the reverse proxy. A source-IP rule designed for one runner IP will fail because hosted-runner egress changes.

404 Not Found

Verify the path /manager/text/deploy is routed to the Tomcat Manager application.

WAR missing

Confirm package_war uploaded the file and deploy_tomcat uses needs with artifacts: true.

Validation checklist

  • No deployment runner was created.
  • Deployment job is untagged.
  • WAR arrives through GitLab artifacts.
  • The deployment is manual and serialized.
  • Tomcat uses a deployment-only identity.
  • The request uses HTTPS.
  • The application version matches the pipeline artifact.

Official references


Lab 21 — End-to-End Java CI/CD with GitLab-Hosted Runners

Series assumptions

  • A Java Maven web application already exists in a GitHub repository.
  • For the Free/classroom path, import the repository into GitLab once and use the GitLab copy for branches, merge requests, and pipelines.
  • The examples assume the application POM is at MyWebApp/pom.xml. Set APP_DIR: "." when pom.xml is at the repository root.
  • Every CI/CD job uses GitLab.com-hosted runners. Students do not install Docker for a runner, install GitLab Runner, create a project runner, register a token, or edit config.toml.
  • Jobs are intentionally untagged, so GitLab.com assigns them to the default small Linux x86-64 hosted runner.
  • Every job receives a fresh, isolated, ephemeral VM. Use cache for reusable downloads and artifacts for files that later jobs must receive.
  • SonarQube, Artifactory, and Tomcat must be reachable from hosted runners through secured public HTTPS endpoints. localhost, 10.x.x.x, 172.16-31.x.x, and 192.168.x.x addresses on Azure are not reachable from the hosted runner.
  • GitLab.com does not provide one fixed outbound IP for normal hosted-runner jobs. Protect exposed services with TLS, strong service authentication, least privilege, and narrowly exposed routes rather than relying on one runner IP allow-list.
  • Never commit passwords, tokens, private keys, or service credentials.

Goal

Assemble the labs into one traceable delivery pipeline without Jenkins and without student-managed GitLab runners.

Developer / VS Code
        |
        | branch, commit, merge request
        v
GitLab repository
        |
        v
GitLab.com-hosted runners
  | fresh VM for each job
  | Maven + Java 21 images
  | JUnit + JaCoCo
  | Snyk
  | SonarQube over HTTPS
  | WAR + SHA-256 artifact
  | Artifactory publication over HTTPS
  | manual Tomcat deployment over HTTPS
        |
        v
Slack pipeline/deployment notification

Required completed labs

  • Lab 0: GitHub repository imported into GitLab.
  • Lab 5.6: hosted runner verified.
  • Lab 6: base Maven pipeline.
  • Labs 7 and 8: VS Code workflow and automatic pipeline triggering.
  • Lab 9.5: Java 21 upgrade.
  • Labs 9 and 10: SonarQube and Quality Gate.
  • Lab 16: hosted-runner execution model, images, cache, and artifacts.
  • Labs 17, 18, and 18.5: Artifactory and versioning.
  • Lab 19: Slack.
  • Lab 20: JUnit and JaCoCo.
  • Lab 20.5: Snyk.
  • Lab 20.6: Tomcat deployment from a hosted runner.

Part A — Use the supplied production pipeline

The root .gitlab-ci.yml contains:

stages:
  - prepare
  - validate
  - test
  - security
  - quality
  - package
  - publish
  - deploy

All jobs are intentionally untagged. Do not add the old classroom tags such as linux, docker, jdk21, maven, deploy, or tomcat.

Part B — Configure variables

SonarQube

SONAR_HOST_URL=https://sonarqube.example.com
SONAR_TOKEN=<secret>
SONAR_PROJECT_KEY=mywebapp

Snyk

SNYK_TOKEN=<secret>
SNYK_ORG=<optional>
SNYK_SEVERITY_THRESHOLD=high

Artifactory

JF_URL=https://artifactory.example.com
JF_USER=gitlab-ci
JF_ACCESS_TOKEN=<secret>
JF_MAVEN_VIRTUAL_REPO=maven-all
JF_MAVEN_RELEASE_REPO=libs-release-local
JF_MAVEN_SNAPSHOT_REPO=libs-snapshot-local

Tomcat

TOMCAT_URL=https://tomcat.example.com
TOMCAT_USER=gitlab-deployer
TOMCAT_PASSWORD=<secret>
TOMCAT_CONTEXT_PATH=/mywebapp
APP_URL=https://app.example.com/mywebapp

Mark tokens and passwords masked, hidden, and protected. Align protected variables with protected branches/tags and the job rules.

Part C — Verify public HTTPS dependencies

From a temporary untagged CI job, verify:

curl --fail "$SONAR_HOST_URL/api/system/status"
curl --fail --user "$JF_USER:$JF_ACCESS_TOKEN" "${JF_URL%/}/artifactory/api/system/ping"
curl --fail --user "$TOMCAT_USER:$TOMCAT_PASSWORD" "$TOMCAT_URL/manager/text/list"

Do not use localhost or a private Azure address in these variables. Do not build an allow-list around one observed hosted-runner IP.

Only the minimum required service routes should be exposed. Keep PostgreSQL and internal administration ports private.

Part D — Understand job-to-job data flow

Every job receives a different temporary VM. The pipeline therefore uses:

Mechanism Purpose
cache Reuse Maven/Sonar downloads where possible
artifacts Preserve reports, WAR, checksum, and dotenv data
needs: artifacts: true Download a specific earlier job's artifact
GitLab CI/CD variables Inject configuration and secrets at runtime
resource_group Serialize production deployments

A local file created in one job is unavailable to the next job unless it is transferred explicitly.

Part E — Pipeline jobs

Job Purpose
prepare_version Creates snapshot/release Maven metadata
validate_pom Validates the POM and Java/Maven requirements
unit_tests Runs tests and publishes JUnit/JaCoCo
snyk_open_source Enforces dependency policy
snyk_code Saves SAST SARIF; initially nonblocking
sonarqube_check Waits for the SonarQube Quality Gate
package_war Creates the versioned WAR and SHA-256 checksum
publish_artifactory Deploys Maven output and JFrog build information
snyk_monitor Creates the Snyk monitoring snapshot
deploy_tomcat Performs a manual serialized HTTPS deployment

Part F — Feature-branch exercise

git switch -c feature/end-to-end-test
# Make a visible code change and update/add a unit test.
mvn -f MyWebApp/pom.xml clean verify

git add .
git commit -m "Exercise end-to-end GitLab pipeline"
git push -u origin feature/end-to-end-test

Expected behavior:

  • Validation and tests run.
  • Jobs permitted by their rules run.
  • No production deployment occurs automatically.
  • No release is published from an arbitrary feature branch.

Part G — Merge-request exercise

  1. Create a merge request to main.
  2. Review code changes.
  3. Review JUnit and coverage evidence.
  4. Confirm required gates pass.
  5. Merge according to branch rules.

Expected default-branch behavior:

  • SonarQube analysis and Quality Gate run.
  • Snyk jobs run when configured.
  • A versioned snapshot can be published.
  • Slack reports pipeline status.
  • Deployment remains manual.

Part H — Release exercise

git switch main
git pull --ff-only origin main
git tag -a v1.0.0 -m "Release 1.0.0"
git push origin v1.0.0

Expected release behavior:

  • Maven version becomes 1.0.0.
  • The release enters libs-release-local.
  • JFrog build information identifies the GitLab pipeline and commit.
  • WAR and SHA256SUMS remain downloadable in GitLab.
  • deploy_tomcat is available as a manual job.

Part I — Deploy and verify

Select the manual deploy_tomcat job. Then verify:

curl --fail --show-error --silent "$APP_URL"

Also confirm:

  • GitLab Environment shows the deployment and commit.
  • Tomcat lists the expected context.
  • The application serves the new release.
  • Slack received the deployment notification.
  • The deployed WAR checksum matches the pipeline artifact.

Part J — Rollback exercise

Rollback should redeploy an already approved immutable artifact rather than rebuilding old source with current dependencies.

  1. Identify the last known-good version and checksum in Artifactory/GitLab.
  2. Download it in a controlled manual CI job.
  3. Deploy it with the same authenticated Tomcat API.
  4. Verify application health.
  5. Record the change/incident reference.
  6. Preserve the failed release for evidence.

Part K — Hosted-runner compute management

A Free GitLab.com namespace has a finite monthly compute quota. The total is the sum of job durations, not merely the wall-clock length of a parallel pipeline.

Use these controls:

  • workflow:rules to prevent duplicate branch and MR pipelines.
  • interruptible: true for safe jobs.
  • rules so SonarQube, Snyk, Artifactory, and deployment jobs run only where required.
  • Maven cache for dependencies.
  • Combined build/test/package work when repeated Maven startup is wasteful.
  • Short artifact retention appropriate to the class.

Acceptance criteria

  • No Jenkins or Bitbucket dependency remains.
  • No student installs or registers a GitLab runner.
  • All normal jobs are untagged and use GitLab-hosted runners.
  • Job-to-job files move through artifacts, not assumed local disk.
  • Java 21 is enforced.
  • JUnit and JaCoCo evidence is visible.
  • SonarQube can block the default-branch pipeline.
  • Snyk evidence is retained and policy failures are intentional.
  • The WAR has an immutable version and SHA-256 checksum.
  • Artifactory stores the correct snapshot or release.
  • SonarQube, Artifactory, and Tomcat are accessed through HTTPS endpoints.
  • PostgreSQL and unnecessary administration ports remain private.
  • Deployment is manual and serialized with resource_group.
  • Slack links back to pipeline/deployment evidence.
  • A documented rollback can restore a previous immutable build.

Final student submission

Submit:

  1. GitLab project URL.
  2. Successful merge-request pipeline URL.
  3. Successful release pipeline URL.
  4. Hosted-runner job log showing runner metadata.
  5. SonarQube project screenshot.
  6. Snyk evidence.
  7. Artifactory artifact and build-information screenshots.
  8. GitLab Environment/deployment screenshot.
  9. Slack notification screenshot.
  10. Application health response.
  11. A one-page explanation of gates, secrets, network boundaries, compute usage, and rollback.

Official references

No comments:

Post a Comment

Gitlab Integration

VSIT GitLab CI/CD Curriculum GitLab CI/CD Java Web Application Lab Series Updated: September 10, 2026 Edition: GitLab.com-hosted runne...