Feedback - Lab 2¶
Tag your final code submission¶
When evaluating your code, we use the Lab2 tag to checkout your code, as in git checkout Lab2. If you did not tag your final code submission, we have to search for the last commit before the deadline.
Some groups however tagged code correctly, but made changes after the tag, before the deadline. This makes it hard for us to evaluate your code, since we don't know which commit to use. Please use other tags to test or mark progress and reserve the requested LabX tags for the final submission.
Tip: it is possible to remove a tag and re-add it to a different commit if you mistagged your code.
Maven lifecycle phases¶
Running both compile and package in one job¶
In maven-build many added the ./mvnw package instruction instead of replacing the ./mvnw compile instruction. This is not necessary, since ./mvnw package will also compile the code. Luckily maven will skip the compile phase if the code is already compiled, but it does cost you a bit of processing overhead since you start the command twice.
maven-build:
script:
# - mvn compile ## No longer necessary since package will also compile
- mvn package -DskipTests
Keep your Unit Tests small¶
Many groups write a Unit Test which tests e.g. all functionality of a Cleric or Miner. Maybe this is because of confusion between the term Unit Test and the DevOps game Units. A Unit Test should test a single unit of code, e.g. a single method. If you want to test all functionality of a Cleric or Miner, you should write multiple Unit Tests, each testing a single method. This makes it easier to find the cause of a failing test and makes it easier to write the test. If you want to avoid code duplication in your Unit Tests, you can use the @Before annotation to create a setup method which is run before each test or extract functionality to a separate method. You can go even further and make additional test suites, one for each Game Unit for instance, to group common functionality together.
Using the wrong image version in the integration test¶
We noticed a fair amount of integration tests still using the latest version of the image instead of the commit hash. This essentially means that you are not testing the code of the commit that triggered the pipeline, but a quasi random version since as the pipeline is set up now, multiple branches can push that latest tag.
YAML anchors vs extends: Understanding the difference¶
The idea of reducing duplication is to A) make the code more readable and B) make it easier to change. However, choosing the wrong approach or misunderstanding how YAML anchors work can lead to subtle bugs and maintenance issues.
GitLab provides comprehensive guidance on this topic in their official documentation: Optimize GitLab CI/CD configuration files. This section highlights common student mistakes and provides practical guidance.
The critical difference: merge vs override¶
YAML anchors (& and <<) are a standard YAML feature that performs a shallow merge. When you override a key, you completely replace that entire section—you don't extend it.
GitLab CI extends is a GitLab-specific keyword that performs a deep merge designed for CI configuration. Variables, scripts, and other configuration are intelligently merged, preserving defaults from the parent.
GitLab recommendation: Prefer extends for GitLab CI jobs. It's more maintainable and less error-prone than anchors.
Common mistake: Losing default variables with anchors¶
Many students used YAML anchors but accidentally lost important default variables. Here's an example of the actual problematic pattern we saw:
# ❌ PROBLEMATIC: Using anchors with variables
.integration_defaults: &integration_defaults
stage: tests
image: gitlab.stud.atlantis.ugent.be:5050/utils/docker/devops-runner:latest
services:
- name: ${CI_REGISTRY_IMAGE}/logic-service:${CI_COMMIT_SHORT_SHA}
alias: logic-service
needs:
- maven-build
variables:
ENABLE_FETCH_LOGS: "true" # Important default!
ABORT_ON_LOGIC_ERROR: "true" # Important default!
before_script:
- echo "Starting integration runner"
script:
- ./run-devops-runner.sh --map-width ${MAP_WIDTH:-50} --map-height ${MAP_HEIGHT:-50}
test-integration-default:
<<: *integration_defaults
variables:
MAP_WIDTH: "50"
MAP_HEIGHT: "50"
CPU_PLAYERS: "3"
TURN_LIMIT: "200"
# ⚠️ BUG: ENABLE_FETCH_LOGS and ABORT_ON_LOGIC_ERROR are now GONE!
# The entire variables: section was replaced, not merged!
test-integration-smallmap:
<<: *integration_defaults
variables:
MAP_WIDTH: "20"
MAP_HEIGHT: "20"
CPU_PLAYERS: "2"
TURN_LIMIT: "100"
# ⚠️ BUG: Same problem here - defaults lost!
What happened? When you define variables: in the child job, it completely replaces the variables: section from the anchor. The defaults (ENABLE_FETCH_LOGS, ABORT_ON_LOGIC_ERROR) are lost, causing integration tests to fail silently or behave unexpectedly.
The workaround with anchors requires manually repeating all variables:
test-integration-default:
<<: *integration_defaults
variables:
ENABLE_FETCH_LOGS: "true" # Must repeat!
ABORT_ON_LOGIC_ERROR: "true" # Must repeat!
MAP_WIDTH: "50"
MAP_HEIGHT: "50"
CPU_PLAYERS: "3"
TURN_LIMIT: "200"
But this defeats the purpose—you're now duplicating the defaults everywhere!
Recommended: Use extends for GitLab CI jobs¶
GitLab's extends keyword properly merges variables and other configuration:
# ✅ RECOMMENDED: Using extends
.integration-tmpl:
cache: []
dependencies: []
stage: tests
image:
name: ${UTIL_REGISTRY}/devops-runner:latest
entrypoint:
- ""
script:
- java -cp /app/resources:/app/classes:/app/libs/* be.ugent.devops.gamehost.services.runner.LauncherKt
services:
- name: ${CI_REGISTRY_IMAGE}/logic-service:${CI_COMMIT_SHORT_SHA}
alias: logic-service
variables:
PLAYER_NAME: "CI/CD Player"
LOGIC_URL: http://logic-service:8080
ENABLE_FETCH_LOGS: "true" # Default preserved!
ABORT_ON_LOGIC_ERROR: "true" # Default preserved!
test-integration-default:
extends: .integration-tmpl
variables:
TURN_INTERVAL_MS: 25
TURN_LIMIT: 100
CPU_PLAYERS: 5
MAP_WIDTH: 50
MAP_HEIGHT: 50
# ✅ ENABLE_FETCH_LOGS and ABORT_ON_LOGIC_ERROR are automatically inherited!
test-integration-tiny-duel:
extends: .integration-tmpl
variables:
TURN_INTERVAL_MS: 25
TURN_LIMIT: 1000
CPU_PLAYERS: 1
MAP_WIDTH: 5
MAP_HEIGHT: 5
# ✅ All defaults are preserved here too!
When anchors are actually better¶
Anchors excel in specific scenarios where extends either doesn't work or creates unnecessary complexity. Here are the best use cases:
1. Reusing identical rules: blocks
When multiple jobs need the exact same conditional logic, anchors prevent accidental modifications:
# ✅ GOOD: Anchors for identical rules
.deploy-rules: &deploy-rules
rules:
- if: $CI_COMMIT_TAG
when: on_success
- if: $CI_PIPELINE_SOURCE == "web"
when: manual
- when: never
deploy-backend:
stage: deploy
script:
- helm upgrade backend ./helm
rules:
- *deploy-rules
deploy-frontend:
stage: deploy
script:
- helm upgrade frontend ./helm
rules:
- *deploy-rules
deploy-database:
stage: deploy
script:
- helm upgrade database ./helm
rules:
- *deploy-rules
Why anchors work better here: rules: is a list structure—using extends would require awkwardly wrapping it in a template job. With anchors, the intent is clear and changes propagate to all jobs automatically.
2. Repeating non-job configuration (Kubernetes, Docker Compose)
In non-GitLab YAML files where extends isn't available, anchors are the only option:
# ✅ GOOD: Anchors in docker-compose.yml
x-logging: &default-logging
driver: json-file
options:
max-size: "10m"
max-file: "3"
services:
backend:
image: my-backend:latest
logging: *default-logging
frontend:
image: my-frontend:latest
logging: *default-logging
Quick decision guide¶
Default: Use extends (safer, GitLab-recommended)
- Jobs with shared configuration + custom variables
- When you need deep merging of variables, scripts, or multiple templates
Use anchors only for:
- Identical
rules:blocks across jobs - Non-GitLab YAML (Kubernetes, Docker Compose)
Don't over-engineer:
- Readability beats eliminating every duplication
- If the section is small and it only repeats 2-3 times, being explicit is fine
- Your team needs to understand the pipeline too
Also don't sleep on variables at the global level or in default:—these can eliminate a lot of duplication without complexity.
See GitLab's YAML optimization guide for more details.
Debugging tip: Full Configuration view¶
Use the GitLab pipeline editor's Full Configuration view to see the final rendered YAML after all anchors and extends are processed. This helps verify that variables and configuration are merged correctly.
Mixing Quarkus configuration options¶
It is possible to configure Quarkus and your Logic Service using multiple methods. You can use environment variables,
system properties, application.properties, application.yaml, profiles, etc. However, it is important to be
intentional and consistent in how you configure your service.
In some jobs we see mixing of environment variables and system properties for the same configuration option.
Setting both the variable QUARKUS_CONTAINER_IMAGE_PUSH=true and the system property
-Dquarkus.container-image.push=true will lead to the system property taking precedence.
Now when you decide to change the configuration, you might change the environment variable, but the system property
will still be set and take precedence. This can lead to unexpected behavior and confusion.
Clean-up policy: case sensitivity¶
Be mindful that the regex for the clean up policy is case sensitive! Many groups had a policy such as lab.* but then
pushed Lab 2. This will not match and thus the tag will be removed.
An example regex which sets case insensitivity and matches both none and some whitespace is (?i)lab ?\d+,
see this example on regex101.com for full explanation.
Keep low expiration on build artifacts¶
As we aren't using artifacts to release or deploy your Logic Service, there is no need to keep them for a long time. Keep the expiration time low (e.g. 1 hour) to avoid filling up the storage of the GitLab instance.
