Skip to content

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!

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.

GitLab Pipeline editor, full configuration

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.