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.
Recommended order
| 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
- Students can enable and verify instance runners without creating a project runner.
- Students can distinguish the hosted runner VM from the container
image:. - Students understand that every job starts clean.
- Students use cache for reusable downloads and artifacts for evidence/binaries.
- Students can diagnose pending jobs caused by disabled runners, compute quota, identity verification, or accidental tags.
- Students can explain why private Azure addresses are unreachable from the hosted runner.
- Students can trace a release from commit through tests, SonarQube, Snyk, Artifactory, Tomcat, and Slack.
Individual labs
- Lab 0 — Import the Existing GitHub Repository into GitLab
- Lab 5.6 — Enable and Verify the Default GitLab-Hosted Runner
- Lab 6 — Automate the First Java Web Application with GitLab CI/CD
- Lab 7 — Make Code Changes in VS Code and Deliver Them Through a GitLab Merge Request
- Lab 8 — Configure Automatic GitLab Pipeline Triggers
- Lab 9 — Install SonarQube Community Build on Azure for GitLab CI/CD
- Lab 9.5 — Upgrade the Maven Web Application from JDK 8 to JDK 21 in GitLab CI/CD
- Lab 10 — Integrate SonarQube with GitLab CI/CD
- Lab 16 — Use the Default GitLab-Hosted Runner and Control the Job Environment
- Lab 17 — Install JFrog Artifactory OSS for GitLab CI/CD
- Lab 18 — Integrate GitLab CI/CD with JFrog Artifactory
- Lab 18.5 — Version Maven Artifacts with GitLab Pipeline Metadata and Release Tags
- Lab 19 — Send GitLab Pipeline and Deployment Notifications to Slack
- Lab 20 — Publish JUnit and JaCoCo Coverage in GitLab
- Lab 20.5 — Integrate Snyk Security Scanning into GitLab CI/CD
- Lab 20.6 — Deploy the WAR to Tomcat from a GitLab-Hosted Runner
- Lab 21 — End-to-End Java CI/CD with GitLab-Hosted Runners
Official platform references
- GitLab-hosted runners — https://docs.gitlab.com/ci/runners/hosted_runners/
- Hosted runners on Linux — https://docs.gitlab.com/ci/runners/hosted_runners/linux/
- Compute minutes — https://docs.gitlab.com/ci/pipelines/compute_minutes/
- GitLab.com network/IP settings — https://docs.gitlab.com/user/gitlab_com/
- Job artifacts — https://docs.gitlab.com/ci/jobs/job_artifacts/
- Cache — https://docs.gitlab.com/ci/caching/
- CI/CD variables — https://docs.gitlab.com/ci/variables/
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. SetAPP_DIR: "."whenpom.xmlis 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, and192.168.x.xaddresses 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:
- Import the GitHub repository into GitLab once.
- Keep the original GitHub repository as a reference or backup.
- 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
- Open the GitHub repository.
- Confirm the application source and
pom.xmlexist. - Determine whether the POM is at
pom.xmlorMyWebApp/pom.xml. - Confirm the default branch.
- Remove generated
target/directories from source control if they were committed. - Search for exposed passwords, tokens, keys, or connection strings before importing.
- 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-migrationPart B — Import GitHub into GitLab
- Sign in to GitLab.com.
- Select Create new → New project/repository.
- Select Import project.
- Select GitHub.
- Authorize GitLab, or provide the supported GitHub token when prompted.
- Find the repository and select Import.
- Choose the destination namespace.
- Wait until the import completes.
- Open Code → Repository.
- Verify files, branches, and tags.
- 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 -20Set the upstream for main:
git branch --set-upstream-to=origin/main mainFor a new local checkout, clone GitLab directly:
git clone https://gitlab.com/GITLAB-GROUP/GITLAB-PROJECT.git
cd GITLAB-PROJECTDo 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 mainPart 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
- Open Settings → Repository → Branch rules.
- Protect
main. - Require merge requests where the course workflow permits.
- Restrict direct pushes to approved maintainers.
- Protect release tags matching
v*.
Validation
- The GitLab project contains the source files, branches, and tags.
- The default branch is correct.
- Local
originpoints to GitLab. - The original GitHub repository remains available under the optional
githubremote. .gitlab-ci.ymlis visible in GitLab.- A simple untagged job is accepted by a GitLab-hosted runner.
Deliverable
Submit:
git remote -v
git status
git branch -vvAlso submit the GitLab project URL and a screenshot of the first pipeline.
Official references
- GitLab: GitLab-hosted runners — https://docs.gitlab.com/ci/runners/hosted_runners/
- GitLab: Hosted runners on Linux — https://docs.gitlab.com/ci/runners/hosted_runners/linux/
- GitLab: CI/CD YAML — https://docs.gitlab.com/ci/yaml/
- GitLab: CI/CD variables — https://docs.gitlab.com/ci/variables/
- GitLab: Job artifacts — https://docs.gitlab.com/ci/jobs/job_artifacts/
- GitLab: Cache — https://docs.gitlab.com/ci/caching/
- GitLab: Compute minutes — https://docs.gitlab.com/ci/pipelines/compute_minutes/
- GitLab: Import a project from GitHub — https://docs.gitlab.com/user/project/import/github/
- GitLab: CI/CD for external repositories — https://docs.gitlab.com/ci/ci_cd_for_external_repos/
- GitLab: Manage runners — https://docs.gitlab.com/ci/runners/runners_scope/
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. SetAPP_DIR: "."whenpom.xmlis 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, and192.168.x.xaddresses 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
- Open the GitLab project.
- Go to Settings → CI/CD.
- Expand Runners.
- In the Instance runners area, verify that Turn on instance runners for this project is enabled.
- Do not select Create project runner.
- 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:
- Confirm the namespace has available compute minutes.
- Complete identity verification if GitLab displays
Identity verification is required in order to run CI jobs. - 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-releaseThere 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 pushOpen:
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_DESCRIPTIONCI_RUNNER_IDCI_RUNNER_TAGSCI_RUNNER_EXECUTABLE_ARCHCI_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:
- Instance runners are enabled for the project and parent group.
- No custom
tags:remain in the job or underdefault:. - The namespace still has compute minutes.
- GitLab is not asking for identity verification.
- 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_runnerpasses.- The log identifies a GitLab runner and a Linux x86-64 environment.
- The student can explain runner VM versus container image.
Official references
- GitLab-hosted runners — https://docs.gitlab.com/ci/runners/hosted_runners/
- Hosted runners on Linux — https://docs.gitlab.com/ci/runners/hosted_runners/linux/
- Manage runners — https://docs.gitlab.com/ci/runners/runners_scope/
- Compute minutes — https://docs.gitlab.com/ci/pipelines/compute_minutes/
- Debugging CI/CD pipelines — https://docs.gitlab.com/ci/debugging/
- Predefined CI/CD variables — https://docs.gitlab.com/ci/variables/predefined_variables/
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. SetAPP_DIR: "."whenpom.xmlis 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, and192.168.x.xaddresses 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' -printUse 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 daysThere 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-pipelineOpen Build → Pipelines and select the pipeline.
Part E — Inspect the jobs
- In
compile, verify the Java and Maven versions. - In
test, verify GitLab displays the JUnit report. - In
package_war, download the WAR artifact. - Confirm the artifact name contains the GitLab project and pipeline IID.
- 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:
- Settings → CI/CD → Runners: instance runners are enabled.
- The job and
default:section contain no customtags:. - The namespace still has hosted-runner compute quota.
- GitLab is not requesting identity verification for hosted-runner use.
- 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
- GitLab: GitLab-hosted runners — https://docs.gitlab.com/ci/runners/hosted_runners/
- GitLab: Hosted runners on Linux — https://docs.gitlab.com/ci/runners/hosted_runners/linux/
- GitLab: CI/CD YAML — https://docs.gitlab.com/ci/yaml/
- GitLab: CI/CD variables — https://docs.gitlab.com/ci/variables/
- GitLab: Job artifacts — https://docs.gitlab.com/ci/jobs/job_artifacts/
- GitLab: Cache — https://docs.gitlab.com/ci/caching/
- GitLab: Compute minutes — https://docs.gitlab.com/ci/pipelines/compute_minutes/
- Official Maven Docker image — https://hub.docker.com/_/maven
- GitLab: JUnit test reports — https://docs.gitlab.com/ci/testing/unit_test_reports/
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. SetAPP_DIR: "."whenpom.xmlis 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, and192.168.x.xaddresses 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-pageNever 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 verifyReview the change:
git status
git diffPart 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-pageThe push automatically creates a branch pipeline because
.gitlab-ci.yml is in the repository.
Part F — Create the merge request
- Open the GitLab project.
- Select Code → Merge requests → New merge request.
- Source:
feature/update-home-page. - Target:
main. - Use a clear title and description.
- Include validation steps and a screenshot if useful.
- Create the merge request.
- Wait for the merge request pipeline to pass.
- Review the Changes, Pipelines, and Tests tabs.
- 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-pageRecommended merge request template
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
## RollbackValidation
- 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.
maincontains the approved change.
Official references
- GitLab: Import a project from GitHub — https://docs.gitlab.com/user/project/import/github/
- GitLab: CI/CD for external repositories — https://docs.gitlab.com/ci/ci_cd_for_external_repos/
- GitLab: GitLab-hosted runners — https://docs.gitlab.com/ci/runners/hosted_runners/
- GitLab: Hosted runners on Linux — https://docs.gitlab.com/ci/runners/hosted_runners/linux/
- GitLab: Manage runners — https://docs.gitlab.com/ci/runners/runners_scope/
- GitLab: Compute minutes — https://docs.gitlab.com/ci/pipelines/compute_minutes/
- GitLab: CI/CD YAML — https://docs.gitlab.com/ci/yaml/
- GitLab: CI/CD variables — https://docs.gitlab.com/ci/variables/
- GitLab: Job artifacts — https://docs.gitlab.com/ci/jobs/job_artifacts/
- GitLab: Test coverage visualization — https://docs.gitlab.com/ci/testing/code_coverage/
- GitLab for Slack app — https://docs.gitlab.com/user/project/integrations/gitlab_slack_application/
- SonarQube Community Build — https://docs.sonarsource.com/sonarqube-community-build/
- SonarScanner for Maven — https://docs.sonarsource.com/sonarqube-community-build/analyzing-source-code/scanners/sonarscanner-for-maven/
- JFrog Artifactory Docker Compose installation — https://jfrog.com/help/r/jfrog-installation-setup-documentation/install-artifactory-single-node-with-docker-compose
- JFrog CLI Maven integration — https://docs.jfrog-applications.jfrog.io/jfrog-applications/jfrog-cli/binaries-management-with-jfrog-artifactory/package-managers-integration
- Snyk CLI — https://docs.snyk.io/developer-tools/snyk-cli
- Apache Maven CI-friendly versions — https://maven.apache.org/maven-ci-friendly.html
- Apache Tomcat Manager application — https://tomcat.apache.org/tomcat-9.0-doc/manager-howto.html
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. SetAPP_DIR: "."whenpom.xmlis 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, and192.168.x.xaddresses 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-testVerify 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.0Verify a tag pipeline is created.
Part F — Optional scheduled pipeline
- Go to Build → Pipeline schedules.
- Select New schedule.
- Name it
nightly-security-scan. - Choose a cron/timezone.
- Target the default branch.
- 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
- GitLab: Import a project from GitHub — https://docs.gitlab.com/user/project/import/github/
- GitLab: CI/CD for external repositories — https://docs.gitlab.com/ci/ci_cd_for_external_repos/
- GitLab: GitLab-hosted runners — https://docs.gitlab.com/ci/runners/hosted_runners/
- GitLab: Hosted runners on Linux — https://docs.gitlab.com/ci/runners/hosted_runners/linux/
- GitLab: Manage runners — https://docs.gitlab.com/ci/runners/runners_scope/
- GitLab: Compute minutes — https://docs.gitlab.com/ci/pipelines/compute_minutes/
- GitLab: CI/CD YAML — https://docs.gitlab.com/ci/yaml/
- GitLab: CI/CD variables — https://docs.gitlab.com/ci/variables/
- GitLab: Job artifacts — https://docs.gitlab.com/ci/jobs/job_artifacts/
- GitLab: Test coverage visualization — https://docs.gitlab.com/ci/testing/code_coverage/
- GitLab for Slack app — https://docs.gitlab.com/user/project/integrations/gitlab_slack_application/
- SonarQube Community Build — https://docs.sonarsource.com/sonarqube-community-build/
- SonarScanner for Maven — https://docs.sonarsource.com/sonarqube-community-build/analyzing-source-code/scanners/sonarscanner-for-maven/
- JFrog Artifactory Docker Compose installation — https://jfrog.com/help/r/jfrog-installation-setup-documentation/install-artifactory-single-node-with-docker-compose
- JFrog CLI Maven integration — https://docs.jfrog-applications.jfrog.io/jfrog-applications/jfrog-cli/binaries-management-with-jfrog-artifactory/package-managers-integration
- Snyk CLI — https://docs.snyk.io/developer-tools/snyk-cli
- Apache Maven CI-friendly versions — https://maven.apache.org/maven-ci-friendly.html
- Apache Tomcat Manager application — https://tomcat.apache.org/tomcat-9.0-doc/manager-howto.html
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. SetAPP_DIR: "."whenpom.xmlis 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, and192.168.x.xaddresses 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 -vPart C — Configure PostgreSQL
Generate and securely record a strong database password, then run:
sudo -u postgres psqlCREATE USER sonarqube WITH ENCRYPTED PASSWORD 'REPLACE_WITH_STRONG_PASSWORD';
CREATE DATABASE sonarqube OWNER sonarqube ENCODING 'UTF8';
\qTest 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-maxCreate 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
EOFPart 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/sonarqubePart F — Configure SonarQube
Edit:
sudo nano /opt/sonarqube/conf/sonar.propertiesSet:
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-pagerWatch startup:
sudo journalctl -u sonarqube -fLocal health check:
curl --fail http://127.0.0.1:9000/api/system/statusPart 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 nginxPart I — Enable HTTPS
After DNS resolves to the VM:
sudo certbot --nginx -d sonarqube.example.com
sudo certbot renew --dry-runOpen 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.logCheck 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 -tBrowser 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
- GitLab: GitLab-hosted runners — https://docs.gitlab.com/ci/runners/hosted_runners/
- GitLab: Hosted runners on Linux — https://docs.gitlab.com/ci/runners/hosted_runners/linux/
- GitLab: CI/CD YAML — https://docs.gitlab.com/ci/yaml/
- GitLab: CI/CD variables — https://docs.gitlab.com/ci/variables/
- GitLab: Job artifacts — https://docs.gitlab.com/ci/jobs/job_artifacts/
- GitLab: Cache — https://docs.gitlab.com/ci/caching/
- GitLab: Compute minutes — https://docs.gitlab.com/ci/pipelines/compute_minutes/
- SonarQube Community Build download — https://www.sonarsource.com/products/sonarqube/downloads/
- SonarQube: Basic ZIP installation — https://docs.sonarsource.com/sonarqube-community-build/server-installation/from-zip-file/basic-installation
- SonarQube: Linux pre-installation — https://docs.sonarsource.com/sonarqube-community-build/server-installation/pre-installation/linux
- Certbot with Nginx — https://certbot.eff.org/instructions
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. SetAPP_DIR: "."whenpom.xmlis 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, and192.168.x.xaddresses 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 EEjavax.*APIs to Jakartajakarta.*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/jdk21Record 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.txtCommit only useful migration notes—not generated target/
output.
Part B — Update the GitLab build image
Use:
image: maven:3.9.16-eclipse-temurin-21-noblePart 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.7211Copy 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-updatesReview, 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/jdk21Create 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
- GitLab: Import a project from GitHub — https://docs.gitlab.com/user/project/import/github/
- GitLab: CI/CD for external repositories — https://docs.gitlab.com/ci/ci_cd_for_external_repos/
- GitLab: GitLab-hosted runners — https://docs.gitlab.com/ci/runners/hosted_runners/
- GitLab: Hosted runners on Linux — https://docs.gitlab.com/ci/runners/hosted_runners/linux/
- GitLab: Manage runners — https://docs.gitlab.com/ci/runners/runners_scope/
- GitLab: Compute minutes — https://docs.gitlab.com/ci/pipelines/compute_minutes/
- GitLab: CI/CD YAML — https://docs.gitlab.com/ci/yaml/
- GitLab: CI/CD variables — https://docs.gitlab.com/ci/variables/
- GitLab: Job artifacts — https://docs.gitlab.com/ci/jobs/job_artifacts/
- GitLab: Test coverage visualization — https://docs.gitlab.com/ci/testing/code_coverage/
- GitLab for Slack app — https://docs.gitlab.com/user/project/integrations/gitlab_slack_application/
- SonarQube Community Build — https://docs.sonarsource.com/sonarqube-community-build/
- SonarScanner for Maven — https://docs.sonarsource.com/sonarqube-community-build/analyzing-source-code/scanners/sonarscanner-for-maven/
- JFrog Artifactory Docker Compose installation — https://jfrog.com/help/r/jfrog-installation-setup-documentation/install-artifactory-single-node-with-docker-compose
- JFrog CLI Maven integration — https://docs.jfrog-applications.jfrog.io/jfrog-applications/jfrog-cli/binaries-management-with-jfrog-artifactory/package-managers-integration
- Snyk CLI — https://docs.snyk.io/developer-tools/snyk-cli
- Apache Maven CI-friendly versions — https://maven.apache.org/maven-ci-friendly.html
- Apache Tomcat Manager application — https://tomcat.apache.org/tomcat-9.0-doc/manager-howto.html
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. SetAPP_DIR: "."whenpom.xmlis 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, and192.168.x.xaddresses 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
- Sign in to SonarQube.
- Select Create project → Local project.
- Use a stable project key, for example
mywebapp. - Create a token dedicated to GitLab CI.
- 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 pushOpen the pipeline and confirm:
- Maven compiles the project.
- Tests run.
- The scanner uploads the analysis.
- The job waits for the Quality Gate.
- The SonarQube project shows the commit and metrics.
Part F — Prove the Quality Gate controls CI
For a safe classroom demonstration:
- Create a temporary quality gate or threshold that the current project will fail.
- Assign it to the lab project.
- Run the pipeline and observe the job fail.
- Restore the intended gate or improve the code/tests.
- 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=truecan block the pipeline.
Official references
- GitLab: GitLab-hosted runners — https://docs.gitlab.com/ci/runners/hosted_runners/
- GitLab: Hosted runners on Linux — https://docs.gitlab.com/ci/runners/hosted_runners/linux/
- GitLab: CI/CD YAML — https://docs.gitlab.com/ci/yaml/
- GitLab: CI/CD variables — https://docs.gitlab.com/ci/variables/
- GitLab: Job artifacts — https://docs.gitlab.com/ci/jobs/job_artifacts/
- GitLab: Cache — https://docs.gitlab.com/ci/caching/
- GitLab: Compute minutes — https://docs.gitlab.com/ci/pipelines/compute_minutes/
- SonarScanner for Maven — https://docs.sonarsource.com/sonarqube-community-build/analyzing-source-code/scanners/sonarscanner-for-maven/
- SonarQube analysis parameters — https://docs.sonarsource.com/sonarqube-community-build/analyzing-source-code/analysis-parameters/
- Maven Central: SonarScanner for Maven — https://central.sonatype.com/artifact/org.sonarsource.scanner.maven/sonar-maven-plugin
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. SetAPP_DIR: "."whenpom.xmlis 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, and192.168.x.xaddresses 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 || trueFor 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 -versionNeither 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 pushOpen both job logs and compare:
- runner ID and description
- container image
- operating-system information
- available commands
- presence of
runner-evidence.txtin 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 -versionUse 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 --versionThe 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/*.warA 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
- GitLab-hosted runners — https://docs.gitlab.com/ci/runners/hosted_runners/
- Hosted runners on Linux — https://docs.gitlab.com/ci/runners/hosted_runners/linux/
- Job artifacts — https://docs.gitlab.com/ci/jobs/job_artifacts/
- Caching — https://docs.gitlab.com/ci/caching/
- Predefined variables — https://docs.gitlab.com/ci/variables/predefined_variables/
- Compute minutes — https://docs.gitlab.com/ci/pipelines/compute_minutes/
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. SetAPP_DIR: "."whenpom.xmlis 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, and192.168.x.xaddresses 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 versionPart 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.shUse 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 -dInspect:
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 -fReadiness 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:8082Complete 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-runDo 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-localandlibs-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
- GitLab: GitLab-hosted runners — https://docs.gitlab.com/ci/runners/hosted_runners/
- GitLab: Hosted runners on Linux — https://docs.gitlab.com/ci/runners/hosted_runners/linux/
- GitLab: CI/CD YAML — https://docs.gitlab.com/ci/yaml/
- GitLab: CI/CD variables — https://docs.gitlab.com/ci/variables/
- GitLab: Job artifacts — https://docs.gitlab.com/ci/jobs/job_artifacts/
- GitLab: Cache — https://docs.gitlab.com/ci/caching/
- GitLab: Compute minutes — https://docs.gitlab.com/ci/pipelines/compute_minutes/
- JFrog: Artifactory single-node Docker Compose installation — https://jfrog.com/help/r/jfrog-installation-setup-documentation/install-artifactory-single-node-with-docker-compose
- JFrog: System requirements — https://jfrog.com/help/r/jfrog-installation-setup-documentation/system-requirements
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. SetAPP_DIR: "."whenpom.xmlis 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, and192.168.x.xaddresses 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
- Push a default-branch commit or release tag.
- Open the
publish_artifactoryjob log. - Confirm dependency resolution uses the virtual repository.
- Locate the POM and WAR in the correct snapshot or release repository.
- Open JFrog build information.
- 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
- GitLab: GitLab-hosted runners — https://docs.gitlab.com/ci/runners/hosted_runners/
- GitLab: Hosted runners on Linux — https://docs.gitlab.com/ci/runners/hosted_runners/linux/
- GitLab: CI/CD YAML — https://docs.gitlab.com/ci/yaml/
- GitLab: CI/CD variables — https://docs.gitlab.com/ci/variables/
- GitLab: Job artifacts — https://docs.gitlab.com/ci/jobs/job_artifacts/
- GitLab: Cache — https://docs.gitlab.com/ci/caching/
- GitLab: Compute minutes — https://docs.gitlab.com/ci/pipelines/compute_minutes/
- JFrog CLI: Package-manager integration — https://docs.jfrog-applications.jfrog.io/jfrog-applications/jfrog-cli/binaries-management-with-jfrog-artifactory/package-managers-integration
- JFrog CLI: Build integration — https://docs.jfrog-applications.jfrog.io/jfrog-applications/jfrog-cli/binaries-management-with-jfrog-artifactory/build-integration
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. SetAPP_DIR: "."whenpom.xmlis 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, and192.168.x.xaddresses 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.envPart C — Apply the version to Maven
mvn -f "$APP_DIR/pom.xml" -B -ntp -Drevision="$MAVEN_REVISION" -Dsha1="$MAVEN_SHA1" -Dchangelist="$MAVEN_CHANGELIST" clean deployPart 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.0Verify:
- Version is
2.3.0with 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
- GitLab: Import a project from GitHub — https://docs.gitlab.com/user/project/import/github/
- GitLab: CI/CD for external repositories — https://docs.gitlab.com/ci/ci_cd_for_external_repos/
- GitLab: GitLab-hosted runners — https://docs.gitlab.com/ci/runners/hosted_runners/
- GitLab: Hosted runners on Linux — https://docs.gitlab.com/ci/runners/hosted_runners/linux/
- GitLab: Manage runners — https://docs.gitlab.com/ci/runners/runners_scope/
- GitLab: Compute minutes — https://docs.gitlab.com/ci/pipelines/compute_minutes/
- GitLab: CI/CD YAML — https://docs.gitlab.com/ci/yaml/
- GitLab: CI/CD variables — https://docs.gitlab.com/ci/variables/
- GitLab: Job artifacts — https://docs.gitlab.com/ci/jobs/job_artifacts/
- GitLab: Test coverage visualization — https://docs.gitlab.com/ci/testing/code_coverage/
- GitLab for Slack app — https://docs.gitlab.com/user/project/integrations/gitlab_slack_application/
- SonarQube Community Build — https://docs.sonarsource.com/sonarqube-community-build/
- SonarScanner for Maven — https://docs.sonarsource.com/sonarqube-community-build/analyzing-source-code/scanners/sonarscanner-for-maven/
- JFrog Artifactory Docker Compose installation — https://jfrog.com/help/r/jfrog-installation-setup-documentation/install-artifactory-single-node-with-docker-compose
- JFrog CLI Maven integration — https://docs.jfrog-applications.jfrog.io/jfrog-applications/jfrog-cli/binaries-management-with-jfrog-artifactory/package-managers-integration
- Snyk CLI — https://docs.snyk.io/developer-tools/snyk-cli
- Apache Maven CI-friendly versions — https://maven.apache.org/maven-ci-friendly.html
- Apache Tomcat Manager application — https://tomcat.apache.org/tomcat-9.0-doc/manager-howto.html
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. SetAPP_DIR: "."whenpom.xmlis 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, and192.168.x.xaddresses 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
- Create or choose a Slack channel, for example
#devops-pipeline. - Confirm you have permission to install apps in the workspace.
- For a private channel, plan to add the GitLab app to the channel explicitly.
Part B — Install from GitLab
- Open the GitLab project or group.
- Go to Settings → Integrations.
- Select GitLab for Slack app.
- Select Install GitLab for Slack app.
- Choose the Slack workspace.
- Select Allow.
- 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
- Select Test settings in GitLab.
- Save the integration.
- Push a harmless commit.
- Verify a pipeline notification appears.
- Run the manual deployment job in Lab 21.
- 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 1Push, 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
- GitLab: Import a project from GitHub — https://docs.gitlab.com/user/project/import/github/
- GitLab: CI/CD for external repositories — https://docs.gitlab.com/ci/ci_cd_for_external_repos/
- GitLab: GitLab-hosted runners — https://docs.gitlab.com/ci/runners/hosted_runners/
- GitLab: Hosted runners on Linux — https://docs.gitlab.com/ci/runners/hosted_runners/linux/
- GitLab: Manage runners — https://docs.gitlab.com/ci/runners/runners_scope/
- GitLab: Compute minutes — https://docs.gitlab.com/ci/pipelines/compute_minutes/
- GitLab: CI/CD YAML — https://docs.gitlab.com/ci/yaml/
- GitLab: CI/CD variables — https://docs.gitlab.com/ci/variables/
- GitLab: Job artifacts — https://docs.gitlab.com/ci/jobs/job_artifacts/
- GitLab: Test coverage visualization — https://docs.gitlab.com/ci/testing/code_coverage/
- GitLab for Slack app — https://docs.gitlab.com/user/project/integrations/gitlab_slack_application/
- SonarQube Community Build — https://docs.sonarsource.com/sonarqube-community-build/
- SonarScanner for Maven — https://docs.sonarsource.com/sonarqube-community-build/analyzing-source-code/scanners/sonarscanner-for-maven/
- JFrog Artifactory Docker Compose installation — https://jfrog.com/help/r/jfrog-installation-setup-documentation/install-artifactory-single-node-with-docker-compose
- JFrog CLI Maven integration — https://docs.jfrog-applications.jfrog.io/jfrog-applications/jfrog-cli/binaries-management-with-jfrog-artifactory/package-managers-integration
- Snyk CLI — https://docs.snyk.io/developer-tools/snyk-cli
- Apache Maven CI-friendly versions — https://maven.apache.org/maven-ci-friendly.html
- Apache Tomcat Manager application — https://tomcat.apache.org/tomcat-9.0-doc/manager-howto.html
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. SetAPP_DIR: "."whenpom.xmlis 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, and192.168.x.xaddresses 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 -printExpected files include:
index.html
jacoco.xml
jacoco.csv
Part B — Add the coverage helper
Use the supplied script:
chmod +x scripts/print-jacoco-coverage.shIt 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
- Run a pipeline.
- Open
unit_testsand find theTotal nn.nn%line. - Open the pipeline test report.
- Download and open the JaCoCo HTML artifact.
- 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 onlytest, and confirm the report execution phase. - 0% displayed: inspect
jacoco.csvand the helper output/regex. - Tests absent: confirm Surefire emits
TEST-*.xmland artifacts usewhen: always. - Report existed during the job but is unavailable
later: ensure its directory is listed under
artifacts.paths. - SonarQube coverage absent: run analysis after
verifyand 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
- GitLab: GitLab-hosted runners — https://docs.gitlab.com/ci/runners/hosted_runners/
- GitLab: Hosted runners on Linux — https://docs.gitlab.com/ci/runners/hosted_runners/linux/
- GitLab: CI/CD YAML — https://docs.gitlab.com/ci/yaml/
- GitLab: CI/CD variables — https://docs.gitlab.com/ci/variables/
- GitLab: Job artifacts — https://docs.gitlab.com/ci/jobs/job_artifacts/
- GitLab: Cache — https://docs.gitlab.com/ci/caching/
- GitLab: Compute minutes — https://docs.gitlab.com/ci/pipelines/compute_minutes/
- GitLab: Test coverage — https://docs.gitlab.com/ci/testing/code_coverage/
- GitLab: JUnit test reports — https://docs.gitlab.com/ci/testing/unit_test_reports/
- JaCoCo Maven plugin — https://www.jacoco.org/jacoco/trunk/doc/maven.html
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. SetAPP_DIR: "."whenpom.xmlis 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, and192.168.x.xaddresses 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
- Create a Snyk CI/service-account token according to your organization policy.
- In GitLab, open Settings → CI/CD → Variables.
- Add
SNYK_TOKENas masked, hidden, and protected. - Optionally add
SNYK_ORG. - 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:rulesto avoid duplicate branch and merge-request pipelines. - Mark safe jobs
interruptible: trueso 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:
- Record findings without blocking.
- Block new high/critical dependency findings.
- Require approved ignores with owner, reason, and expiration.
- Block high SAST findings after baseline triage.
- 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 monitorcreates/updates the intended Snyk project.
Official references
- GitLab: GitLab-hosted runners — https://docs.gitlab.com/ci/runners/hosted_runners/
- GitLab: Hosted runners on Linux — https://docs.gitlab.com/ci/runners/hosted_runners/linux/
- GitLab: CI/CD YAML — https://docs.gitlab.com/ci/yaml/
- GitLab: CI/CD variables — https://docs.gitlab.com/ci/variables/
- GitLab: Job artifacts — https://docs.gitlab.com/ci/jobs/job_artifacts/
- GitLab: Cache — https://docs.gitlab.com/ci/caching/
- GitLab: Compute minutes — https://docs.gitlab.com/ci/pipelines/compute_minutes/
- Snyk CLI installation — https://docs.snyk.io/developer-tools/snyk-cli/install-or-update-the-snyk-cli
- Snyk CLI
testcommand — https://docs.snyk.io/developer-tools/snyk-cli/commands/test - Snyk CLI
monitorcommand — https://docs.snyk.io/developer-tools/snyk-cli/commands/monitor
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. SetAPP_DIR: "."whenpom.xmlis 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, and192.168.x.xaddresses 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
- Put Tomcat behind HTTPS with a publicly trusted certificate.
- Keep the normal Manager HTML interface unavailable to the public internet.
- Route only the minimum Manager text path required by the deployment job.
- Require Tomcat authentication.
- Apply rate limiting, logging, and any available web-application firewall controls.
- Keep port 8005 and other Tomcat control ports closed.
- 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 daysPart 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: neverThere 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
- Run a successful default-branch or release-tag pipeline.
- Select the manual
deploy_tomcatjob. - Confirm the job log contains Tomcat's
OKresponse. - Open
APP_URL. - Confirm GitLab records the production environment and deployment.
- 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
- GitLab job artifacts — https://docs.gitlab.com/ci/jobs/job_artifacts/
- GitLab environments — https://docs.gitlab.com/ci/environments/
- GitLab-hosted runners — https://docs.gitlab.com/ci/runners/hosted_runners/
- GitLab CI/CD variables — https://docs.gitlab.com/ci/variables/
- Apache Tomcat Manager application — https://tomcat.apache.org/tomcat-9.0-doc/manager-howto.html
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. SetAPP_DIR: "."whenpom.xmlis 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, and192.168.x.xaddresses 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
- deployAll 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-testExpected behavior:
- Validation and tests run.
- Jobs permitted by their
rulesrun. - No production deployment occurs automatically.
- No release is published from an arbitrary feature branch.
Part G — Merge-request exercise
- Create a merge request to
main. - Review code changes.
- Review JUnit and coverage evidence.
- Confirm required gates pass.
- 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.0Expected 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
SHA256SUMSremain downloadable in GitLab. deploy_tomcatis 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.
- Identify the last known-good version and checksum in Artifactory/GitLab.
- Download it in a controlled manual CI job.
- Deploy it with the same authenticated Tomcat API.
- Verify application health.
- Record the change/incident reference.
- 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:rulesto prevent duplicate branch and MR pipelines.interruptible: truefor safe jobs.rulesso 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:
- GitLab project URL.
- Successful merge-request pipeline URL.
- Successful release pipeline URL.
- Hosted-runner job log showing runner metadata.
- SonarQube project screenshot.
- Snyk evidence.
- Artifactory artifact and build-information screenshots.
- GitLab Environment/deployment screenshot.
- Slack notification screenshot.
- Application health response.
- A one-page explanation of gates, secrets, network boundaries, compute usage, and rollback.
Official references
- GitLab: GitLab-hosted runners — https://docs.gitlab.com/ci/runners/hosted_runners/
- GitLab: Hosted runners on Linux — https://docs.gitlab.com/ci/runners/hosted_runners/linux/
- GitLab: CI/CD YAML — https://docs.gitlab.com/ci/yaml/
- GitLab: CI/CD variables — https://docs.gitlab.com/ci/variables/
- GitLab: Job artifacts — https://docs.gitlab.com/ci/jobs/job_artifacts/
- GitLab: Cache — https://docs.gitlab.com/ci/caching/
- GitLab: Compute minutes — https://docs.gitlab.com/ci/pipelines/compute_minutes/
- GitLab: Environments and deployments — https://docs.gitlab.com/ci/environments/
- GitLab: Resource groups — https://docs.gitlab.com/ci/resource_groups/
- GitLab for Slack app — https://docs.gitlab.com/user/project/integrations/gitlab_slack_application/
- SonarScanner for Maven — https://docs.sonarsource.com/sonarqube-community-build/analyzing-source-code/scanners/sonarscanner-for-maven/
- JFrog CLI Maven integration — https://docs.jfrog-applications.jfrog.io/jfrog-applications/jfrog-cli/binaries-management-with-jfrog-artifactory/package-managers-integration
- Snyk CLI — https://docs.snyk.io/developer-tools/snyk-cli
- Apache Maven CI-friendly versions — https://maven.apache.org/maven-ci-friendly.html
- Apache Tomcat Manager — https://tomcat.apache.org/tomcat-9.0-doc/manager-howto.html