Skip to content

Development: Pre-pull the e2e exercise images before the tests run - #13495

Merged
krusche merged 9 commits into
developfrom
chore/prepull-e2e-exercise-images
Aug 12, 2026
Merged

Development: Pre-pull the e2e exercise images before the tests run#13495
krusche merged 9 commits into
developfrom
chore/prepull-e2e-exercise-images

Conversation

@krusche

@krusche krusche commented Aug 12, 2026

Copy link
Copy Markdown
Member

Summary

The E2E stack mounts the runner's Docker socket, so exercise builds run against the host daemon — but nothing provisions the exercise images. The first Java build of a run therefore discovers that the ~1 GB Maven image is missing and pulls it while the suite is already running, competing with the test workload. This adds a pre-pull step to the shared E2E setup action so no build job ever races an on-demand pull.

Checklist

General

Motivation and Context

A handful of E2E tests have been failing on every pull request, and the flakiness classifier scores them as real regressions, so they block the required gate. They are not regressions.

On run 31575061135 the app container log shows:

```
BuildJobManagementService : Error while executing build job 3151786522675528:
LocalCIException: Could not pull Docker image ls1tum/artemis-maven-template:java17-25
Caused by: ... BuildAgentDockerService.doPullDockerImage
Caused by: com.github.dockerjava.api.exception.DockerClientException:
Could not pull image: Extracting
```

docker-java's `PullImageResultCallback` throws with the last status it saw, so "Extracting" means the pull stream ended part-way through unpacking layers. In that run:

  • the image was absent at stack start and 261 build jobs failed on it, from 07:55:43 to 08:20:04;
  • once it landed, later Java builds passed in ~10s (`ProgrammingExerciseParticipation › Team members make git submissions`, 08:24);
  • the tests that fail run at 08:15–08:20, i.e. inside the dead window.

Because test order is stable, the same early-scheduled tests fail every time: both `ProgrammingExercisePracticeMode` submission tests and `Static code analysis tests › Verifies SCA feedback is displayed correctly after submission`. They fail on unrelated PRs too (#13493, #13492) and pass locally, where the images are already on the machine — I ran the practice-mode spec against a local stack and all 3 tests pass in 1.5 minutes with real Maven builds reaching 100%.

The symptom is misleading on its own: the assertion reports `Status 0%, Build failed`, which reads as a grading regression and says nothing about the missing image.

Ruled out along the way: Artemis's own pull timeout and stall detector never fire (zero `did not finish within` / `reported no progress` messages, so `image-pull-timeout-seconds` is not involved); the `java17-25` tag does exist on Docker Hub; and `.ci/E2E-tests/cleanup.sh` stops containers but does not remove images.

Description

  • New `.ci/E2E-tests/prepull-exercise-images.sh` — pulls the four images the suite's programming exercises actually build in (Java/Kotlin Maven template, C minimal, C fact, Python). Images already present are skipped, so on a warm self-hosted runner this is a no-op. Failed pulls are retried with backoff; a partial pull leaves its layers behind, so a retry resumes rather than restarts.
  • Drift guard — each tag must still appear in `application.yml`. Bumping a tag there without updating the script fails with an explicit message instead of silently reintroducing the on-demand pull this step exists to prevent.
  • Wired into `.github/actions/e2e-setup` after the cleanup step (which stops containers but leaves images) and before the tests, so all three E2E jobs get it.

This moves a cost that was already being paid in front of the suite, where it competes with nothing and can be retried, and turns an unobtainable image into one loud setup failure rather than a handful of unexplained assertion failures.

What this does not do: it does not explain why the host's pulls die mid-extraction — disk pressure, concurrent E2E jobs on shared self-hosted runners and registry flakiness are all candidates, and telling them apart needs host-level access. If the host cannot pull at all, the job now fails fast at setup with the real reason instead of producing 261 opaque build failures. Happy to downgrade that to a warning if you would rather a degraded run still executed.

Steps for Testing

This only affects CI, so the check is the E2E run on this PR itself:

  1. Open the `E2E / Phase 1` job and confirm the new Pre-pull exercise images step runs after the cleanup step and reports each image as either pulled or already present.
  2. Confirm the app container log contains no `Could not pull Docker image` errors.
  3. Confirm the three previously-failing tests now pass:
    • `Programming exercise practice mode › Keeps the practice mode selectable when switching back to graded`
    • `Programming exercise practice mode › Shows the submission state when submitting in the practice mode code editor`
    • `Static code analysis tests › Verifies SCA feedback is displayed correctly after submission`

Locally verified all four script paths: images already present (no-op), pull needed (pulled successfully), tag no longer in `application.yml` (fails with the stale-list error), and unpullable image (retries, then fails with the provisioning error). `shellcheck` and `bash -n` are clean.

Testserver States

Not applicable — this changes CI setup only and cannot be exercised on a test server.

Review Progress

Code Review

  • Code Review 1
  • Code Review 2

Summary by CodeRabbit

  • New Features
    • Added automated setup for Docker images required by end-to-end programming exercises.
    • Validates configured images and identifies mismatches before provisioning.
    • Skips images already available locally and retries missing image downloads with timeouts, backoff, and a shared deadline.
    • Provides clear error messages when configuration validation, required tooling, or image provisioning fails.
    • Improves reliability and visibility when preparing exercise environments.

The E2E stack mounts the runner's Docker socket, so exercise builds run
against the host daemon — but nothing provisions the exercise images.
The first Java build of a run therefore discovers that the ~1 GB Maven
image is missing and pulls it while the suite is already competing for
CPU, disk and network.

On a recent PR run every pull of ls1tum/artemis-maven-template died
mid-extraction ("DockerClientException: Could not pull image:
Extracting"), 261 build jobs failed over the first 25 minutes, and each
test asserting on a Java build result failed with "0%, Build failed" —
which reads as a grading regression and says nothing about the cause.
Once the image finally landed, later Java builds passed in ~10s. Because
test order is stable, the same early-scheduled tests failed on every
pull request: both ProgrammingExercisePracticeMode submission tests and
the static-code-analysis feedback test. They pass locally, where the
images are already on the machine.

Pulling the images in the setup action instead moves that one-off cost
in front of the suite, where it competes with nothing and can be
retried, and turns an unobtainable image into one loud setup failure
rather than a handful of unexplained assertion failures. Images already
present are left alone, so on a warm self-hosted runner this is a no-op.

The image list is guarded against drift: each tag must still appear in
application.yml, so bumping a tag there without updating the script
fails with an explicit message instead of silently reintroducing the
on-demand pull.

Note that this does not explain why the host's pulls die mid-extraction
(disk pressure, concurrent jobs and registry flakiness are all
candidates and need host-level access to tell apart). If the host cannot
pull at all, the job now fails fast at setup with the real reason.
@krusche
krusche requested a review from a team as a code owner August 12, 2026 11:27
Copilot AI lite review requested due to automatic review settings August 12, 2026 11:27
@github-project-automation github-project-automation Bot moved this to Work In Progress in Artemis Development Aug 12, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.ci/E2E-tests/prepull-exercise-images.sh:
- Around line 41-43: Update the image validation loop in
prepull-exercise-images.sh to match only active
continuous-integration.build.images properties in CONFIG_FILE, rather than
grepping the entire YAML text. Handle the YAML value forms used by the
configuration, including quoted and unquoted tags, while preserving the existing
stale-image error behavior.
- Around line 55-65: Update the docker pull invocation in the attempt loop to
run under a supported per-command timeout, ensuring stalled pulls terminate with
a nonzero status so the existing backoff and MAX_ATTEMPTS logic proceeds
unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 57b2d40a-9e48-4f0b-a904-ced3dd712506

📥 Commits

Reviewing files that changed from the base of the PR and between ff19136 and 53c4c35.

⛔ Files ignored due to path filters (1)
  • .github/actions/e2e-setup/action.yml is excluded by !**/*.yml
📒 Files selected for processing (1)
  • .ci/E2E-tests/prepull-exercise-images.sh

Comment thread .ci/E2E-tests/prepull-exercise-images.sh
Comment thread .ci/E2E-tests/prepull-exercise-images.sh
@github-project-automation github-project-automation Bot moved this from Work In Progress to Ready For Review in Artemis Development Aug 12, 2026
@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown

End-to-End Test Results

Phase Status Details
Phase 1 (Relevant) ✅ Passed
TestsPassed ✅SkippedFailedTime ⏱
Phase 1: E2E Test Report17 ran17 passed0 skipped0 failed2m 7s
Phase 2 (Remaining) ❌ Failed
TestsPassed ☑️Skipped ⚠️Failed ❌️Time ⏱
Phase 2: E2E Test Report342 ran334 passed7 skipped1 failed38m 50s

Test Strategy: Two-phase execution

  • Phase 1: e2e/Login.spec.ts e2e/Logout.spec.ts e2e/SystemHealth.spec.ts
  • Phase 2: e2e/Passkey.spec.ts e2e/PasskeyReminderPersistence.spec.ts e2e/admin/ e2e/atlas/ e2e/course/ e2e/exam/ExamAssessment.spec.ts e2e/exam/ExamChecklists.spec.ts e2e/exam/ExamCreationDeletion.spec.ts e2e/exam/ExamDateVerification.spec.ts e2e/exam/ExamManagement.spec.ts e2e/exam/ExamParticipation.spec.ts e2e/exam/ExamResults.spec.ts e2e/exam/ExamTestRun.spec.ts e2e/exam/test-exam/ e2e/exercise/ExerciseImport.spec.ts e2e/exercise/file-upload/ e2e/exercise/modeling/ e2e/exercise/programming/ e2e/exercise/quiz-exercise/ e2e/exercise/text/ e2e/iris/ e2e/lecture/ e2e/shared/
❌ Failed Tests (Phase 2)
  • Programming exercise advanced participation › Programming exercise team participation › Students of other teams have their own submission (54s)

Overall: ❌ E2E: real (non-flaky) test failure

🔗 Workflow Run · 📊 Test Report Phase 1 · 📊 Test Report Phase 2

@krusche
krusche temporarily deployed to playwright-e2e-tests August 12, 2026 12:05 — with GitHub Actions Inactive
Both points from the review, and the first one mattered: the premise of
this script is that these pulls stall, so an unbounded `docker pull` in
the setup step would hang the job until its timeout — the same
undiagnosable failure the script exists to remove, only earlier. The
pull now runs under a `timeout` capped at 10 minutes, which is generous
for a cold ~1 GB image but turns a stall into a non-zero exit the
existing backoff can act on. `--kill-after` escalates to SIGKILL for a
pull wedged badly enough to ignore the SIGTERM. macOS names the
coreutils binary `gtimeout`, so both are accepted and the bound also
holds for local runs.

The drift check no longer greps the whole configuration. It reads the
image values out of the build agent's own `images:` block, which the
first version got wrong in a way worth noting: application.yml holds a
second, unrelated `images:` block for Kubernetes app definitions, and
matching the raw file text meant an image named there could have
satisfied the check. Values are accepted quoted or bare, trailing
comments are stripped, and a layout change that yields no images now
fails with its own message instead of silently passing.
@krusche

krusche commented Aug 12, 2026

Copy link
Copy Markdown
Member Author

Both review points addressed in 336d87d.

Bounded pull — this was the one that mattered. The premise of the script is that these pulls stall, so an unbounded docker pull in the setup step would hang the job until its timeout: the same undiagnosable failure the script exists to remove, just moved earlier. The pull now runs under timeout --kill-after=30s 600s, generous for a cold ~1 GB image but enough to turn a stall into a non-zero exit that the existing backoff acts on. --kill-after covers a pull wedged badly enough to ignore SIGTERM. macOS names the coreutils binary gtimeout, so both are accepted and the bound also holds for local runs; if neither exists the script says so rather than pretending the pull is bounded.

Scoped drift check — no longer greps the whole file. It now reads the image values out of the build agent's own images: block. Worth noting why that was right to raise: application.yml contains a second, unrelated images: block for Kubernetes app definitions, so the original raw-text match meant an image named there could have satisfied the check. Values are accepted quoted or bare, trailing comments are stripped, and a layout change that yields no images fails with its own message instead of passing silently.

Verified locally, five paths:

Path Result
all images cached no-op, exit 0
stalled pull (bounded via a GNU-timeout-alike shim) capped, retried, exit 1 with the provisioning error — 5s instead of 30s+
tag missing from build.images stale-list error, exit 1
tag present only in the Kubernetes block stale-list error, exit 1 (regression guard for the point raised)
config layout yields no images layout error, exit 1

shellcheck clean. One limitation I should state rather than paper over: this machine has neither timeout nor gtimeout, so the real coreutils binary could not be exercised here — the stall test used a shim that reproduces its contract (enforce the cap, exit 124). What that verifies is my retry logic, which is the part under review; timeout itself is present on the runners.

Still not ready to merge, for a reason unrelated to the review: Phase 1 selected only Login/Logout/SystemHealth (17 tests) for this CI-only change, so the tests this fix targets are in Phase 2 and had not reported yet. The verification that matters is those three tests going green here.

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

The pull request adds a Bash script for E2E exercise-image preparation. It validates configured images, skips local images, and pulls missing images with timeouts, retries, backoff, and termination handling.

Changes

E2E exercise image preparation

Layer / File(s) Summary
Configuration validation and pull setup
.ci/E2E-tests/prepull-exercise-images.sh
The script defines required images, parses the nested build-agent image configuration, detects configuration drift, and selects timeout or gtimeout for bounded pulls.
Docker image provisioning
.ci/E2E-tests/prepull-exercise-images.sh
The script checks local images and pulls missing images with retries, backoff, termination handling, and explicit failure reporting.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant E2E setup script
  participant application.yml
  participant Docker
  E2E setup script->>application.yml: Read and validate exercise images
  E2E setup script->>Docker: Check local image availability
  E2E setup script->>Docker: Pull missing images with timeout and retries
  Docker-->>E2E setup script: Return provisioning status
Loading

Suggested labels: buildagent, programming

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: pre-pulling E2E exercise images before tests run.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/prepull-e2e-exercise-images

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
.ci/E2E-tests/prepull-exercise-images.sh (3)

35-35: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Resolve CONFIG_FILE relative to the repository, not the caller's directory.

CONFIG_FILE is a relative path, so the script only works when the caller's working directory is the repository root. If a developer runs it from another directory, awk fails with can't open file, and set -e aborts before any of the curated ::error messages can print. The header states the script is also used locally, so this path is reachable.

♻️ Proposed fix to anchor the path
-CONFIG_FILE="src/main/resources/config/application.yml"
+REPO_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
+CONFIG_FILE="${REPO_ROOT}/src/main/resources/config/application.yml"
+if [ ! -r "$CONFIG_FILE" ]; then
+    echo "::error title=Could not read E2E exercise images::${CONFIG_FILE} is missing or unreadable."
+    exit 1
+fi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.ci/E2E-tests/prepull-exercise-images.sh at line 35, Update CONFIG_FILE in
prepull-exercise-images.sh to resolve from the repository or script location
rather than the caller’s current working directory, while preserving the
existing application.yml target so the script works when invoked locally from
any directory.

104-135: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Bound the total time spent on one image, not only each attempt.

timeout terminates the docker pull client process. The Docker daemon continues the pull in the background, so the next attempt attaches to the same in-progress pull. If the daemon or the registry is genuinely stalled, each attempt consumes the full PULL_TIMEOUT_SECONDS. The worst case for a single image is 3 × 600s plus 30s of backoff, which is about 30 minutes before the step reports the failure. Four images give a worst case near two hours, and the surrounding job timeout would cut the step before the curated ::error message on Line 138 prints.

Add a deadline for the whole per-image loop, or reduce PULL_TIMEOUT_SECONDS so that MAX_ATTEMPTS × the per-attempt bound stays inside the step budget.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.ci/E2E-tests/prepull-exercise-images.sh around lines 104 - 135, Bound the
total duration of the per-image retry loop, not just each docker pull attempt.
Update the loop around the pull array and MAX_ATTEMPTS to enforce a shared
deadline that includes retries and backoff, ensuring failure reaches the
existing curated error path before the job timeout; preserve the current retry
behavior within that deadline.

78-78: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Use a Bash 4+ requirement or replace mapfile.

macOS’s default Bash 3.2 does not provide mapfile. With #!/usr/bin/env bash, this line fails unless a newer Bash appears first in PATH. Replace it with a while IFS= read -r loop, or document and enforce the Bash 4+ requirement.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.ci/E2E-tests/prepull-exercise-images.sh at line 78, Update the
configured_images output handling around CONFIGURED to avoid Bash 4-only mapfile
usage: replace it with a Bash 3.2-compatible while IFS= read -r loop that
preserves each image entry, or explicitly enforce a Bash 4+ interpreter
requirement for the script.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.ci/E2E-tests/prepull-exercise-images.sh:
- Around line 43-73: Update configured_images so build_indent is cleared when
the build: mapping ends, preventing later nested images: blocks from being
selected. Require the images: mapping to use exactly one indentation level
deeper than build_indent, while preserving parsing of the intended build-agent
block.

---

Nitpick comments:
In @.ci/E2E-tests/prepull-exercise-images.sh:
- Line 35: Update CONFIG_FILE in prepull-exercise-images.sh to resolve from the
repository or script location rather than the caller’s current working
directory, while preserving the existing application.yml target so the script
works when invoked locally from any directory.
- Around line 104-135: Bound the total duration of the per-image retry loop, not
just each docker pull attempt. Update the loop around the pull array and
MAX_ATTEMPTS to enforce a shared deadline that includes retries and backoff,
ensuring failure reaches the existing curated error path before the job timeout;
preserve the current retry behavior within that deadline.
- Line 78: Update the configured_images output handling around CONFIGURED to
avoid Bash 4-only mapfile usage: replace it with a Bash 3.2-compatible while
IFS= read -r loop that preserves each image entry, or explicitly enforce a Bash
4+ interpreter requirement for the script.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: cefe2ac7-4472-49bd-b90b-d5d45dd8122e

📥 Commits

Reviewing files that changed from the base of the PR and between ff19136 and 336d87d.

⛔ Files ignored due to path filters (1)
  • .github/actions/e2e-setup/action.yml is excluded by !**/*.yml
📒 Files selected for processing (1)
  • .ci/E2E-tests/prepull-exercise-images.sh

Comment thread .ci/E2E-tests/prepull-exercise-images.sh
@krusche
krusche temporarily deployed to playwright-e2e-tests August 12, 2026 13:01 — with GitHub Actions Inactive
@krusche
krusche temporarily deployed to playwright-e2e-tests August 12, 2026 13:04 — with GitHub Actions Inactive
Three more points from the review. The one that mattered: `timeout` only
kills the `docker pull` client, the daemon keeps pulling, and the next
attempt attaches to that same pull — so against a genuinely stalled
daemon every attempt burned its full cap. Worst case was 3 x 600s per
image, near two hours across four, and the job timeout would have killed
the step before it could report why. That is the failure this script
exists to remove, so the retries now share a per-image deadline and give
up inside it.

The per-attempt cap also drops from 600s to 300s, informed by the run
this change was written for: pulling all four images took 30 seconds and
the Maven image 11 of them, so 300s is still a large multiple of the real
cost.

CONFIG_FILE is now resolved from the script's own location instead of the
caller's working directory, with an explicit readability check, so
running it from anywhere behaves the same. The configured-image list is
collected with `while read` rather than `mapfile`, which macOS's default
Bash 3.2 does not provide — the header claims local usability, so it
should hold there too.
@krusche

krusche commented Aug 12, 2026

Copy link
Copy Markdown
Member Author

The fix is confirmed working

Phase 2 has now run the tests this PR exists to fix. Direct evidence from the Phase 2 job:

Pulling ls1tum/artemis-maven-template:java17-25 (attempt 1/3)...
Pulled: ls1tum/artemis-maven-template:java17-25          # 13:04:26 -> 13:04:37
...
All E2E exercise images are available.                    # all four by 13:04:55
Metric Before (run 31575061135) After
Could not pull Docker image errors 261 0
Practice-repo builds completed 0 2 (10.46s, 10.50s — real Maven builds)
practice mode › Keeps the practice mode selectable…
practice mode › Shows the submission state…code editor
Static code analysis › SCA feedback after submission
Programming exercise participation using SSH

Phase 2: 342 ran, 333 passed, 7 skipped, 2 failed. Note the pull that repeatedly died mid-extraction for 25 minutes under test load takes 30 seconds when done alone — which is the contention argument made concrete.

The 2 remaining failures are unrelated

  • Programming exercise team participation › Students of other teams have their own submission — also fails on Development: Allow to update the internal field of users #13492 (50s there, 44s here), so pre-existing.
  • Course onboarding wizard › Walks through all wizard steps and finishes setupexpect(locator('.onboarding-wizard')).toBeHidden() receiving visible after navigation. A UI assertion with no build or image involvement.

Both are fast failures (44s, 55s), not the multi-minute build timeouts this PR addresses. Report E2E Overall Status is therefore still red, but for different tests.

Review round 2 addressed in 31f1a08

All three were valid, and the retry-loop one was material:

  • Shared deadline for the retry loop. Correct and important: timeout only kills the docker pull client, the daemon keeps pulling, and the next attempt attaches to that same pull — so against a stalled daemon every attempt burned its full cap. Worst case was 3 × 600s per image, near two hours across four, and the job timeout would have killed the step before printing the curated error. Exactly the failure mode this script exists to remove. Retries now share a per-image deadline. The per-attempt cap also drops 600s → 300s, informed by the measured 11s real pull.
  • CONFIG_FILE resolved from the script's location rather than the caller's cwd, with an explicit readability check.
  • mapfilewhile read, since macOS's default Bash 3.2 lacks it.

Verified: runs correctly from another directory, runs correctly under /bin/bash 3.2, and the deadline demonstrably stops the loop early (1 attempt + backoff, "Giving up … after 3s spent across 2 attempts" with MAX_ATTEMPTS=5). shellcheck clean.

@krusche
krusche temporarily deployed to playwright-e2e-tests August 12, 2026 13:55 — with GitHub Actions Inactive

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.ci/E2E-tests/prepull-exercise-images.sh:
- Around line 142-147: Update the retry loop around the shared deadline and
pull/backoff logic in the prepull script so each attempt’s timeout, including
the 30-second kill-after grace period, cannot exceed the seconds remaining
before deadline. Also cap each backoff sleep to the remaining deadline budget,
preventing either pulls or sleeps from running past the shared timeout.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 8e6df478-fc34-4c53-aeba-f39f27c091b7

📥 Commits

Reviewing files that changed from the base of the PR and between 336d87d and 31f1a08.

📒 Files selected for processing (1)
  • .ci/E2E-tests/prepull-exercise-images.sh

Comment thread .ci/E2E-tests/prepull-exercise-images.sh Outdated
@krusche

krusche commented Aug 12, 2026

Copy link
Copy Markdown
Member Author

@Claudia-Anthropica review

@Claudia-Anthropica

Copy link
Copy Markdown
Contributor

@krusche on it! Taking a look now 👀

…scoping

The per-image deadline was only checked between attempts, so a single
hanging pull could overrun it by a further PULL_TIMEOUT_SECONDS and the
backoff could push past it again — the budget was advisory rather than a
bound. Each attempt is now capped by whatever is left of the budget, and
the backoff is clamped to it, so the time spent on one image cannot
exceed PULL_DEADLINE_SECONDS however long an individual pull hangs.
Verified with a stalled pull: a 5s budget with a 10s per-attempt cap
finishes in 5s.

The extractor's comment claimed the `images:` block had to be nested
under `build:`, but `build_indent` was only cleared when an images block
ended. A `build:` block without an images child therefore left it set,
and an `images:` key in any later section would have been read. It is now
cleared on leaving the build block, so the code enforces what the comment
says. Covered both ways with synthetic configurations: a `build:` block
with no images child followed by a deeply nested `images:` yields
nothing, while a real `build.images` block is still read in full and an
unrelated `k8s.images` block is ignored.

The timeout binary is also detected once instead of per image, since it
does not vary.

@Claudia-Anthropica Claudia-Anthropica left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@krusche [medium] The latest commit still gives timeout the full remaining budget before the 30-second kill-after grace, so the per-image deadline can be exceeded. [medium] The extractor also still accepts any deeper images: mapping under build: instead of requiring the direct child; both defects are already covered by unresolved CodeRabbit threads, so I have not duplicated inline comments. The captured CI snapshot has no required checks pending or failing.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.ci/E2E-tests/prepull-exercise-images.sh:
- Around line 125-132: Update the missing-image pull flow in
prepull-exercise-images.sh to check whether timeout_bin is empty before invoking
docker pull. Preserve the cached-image path, but exit with an appropriate
failure message instead of starting an unbounded pull when neither timeout nor
gtimeout is available.
- Around line 152-157: Update the timeout calculation near attempt_cap and the
pull command so the --kill-after grace period is reserved within the shared
deadline. Ensure each timeout invocation cannot run past deadline; when the
remaining budget cannot accommodate the grace period, use an immediate SIGKILL
timeout while preserving the existing docker pull retry behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 90746e7b-1985-4f96-9cb6-48ed65b9dea6

📥 Commits

Reviewing files that changed from the base of the PR and between 31f1a08 and e163c16.

📒 Files selected for processing (1)
  • .ci/E2E-tests/prepull-exercise-images.sh

Comment thread .ci/E2E-tests/prepull-exercise-images.sh
Comment thread .ci/E2E-tests/prepull-exercise-images.sh Outdated
@krusche
krusche temporarily deployed to playwright-e2e-tests August 12, 2026 16:10 — with GitHub Actions Inactive
@krusche
krusche temporarily deployed to playwright-e2e-tests August 12, 2026 16:13 — with GitHub Actions Inactive
@krusche krusche changed the title Development: Pre-pull the E2E exercise images before the tests run Development: Pre-pull the e2e exercise images before the tests run Aug 12, 2026
…he cap

Two more review points, both about the bound not holding in cases the
comments claimed it did.

Without `timeout` or `gtimeout` the script previously fell through to an
unbounded `docker pull`, which on a stall would hold the setup step open
until the job timeout — the failure this script exists to remove. It now
refuses, with a message naming the fix. The check sits where an image
turns out to be missing rather than up front, so a machine with every
image already cached still succeeds without coreutils.

`timeout --kill-after=D T` sends SIGTERM at T and only escalates at T+D,
so charging the 30s grace on top of the attempt cap let a pull that
ignores SIGTERM run past the deadline. The grace now comes out of the
attempt's own share: above 30s the cap is split into SIGTERM at cap-30
and SIGKILL at cap, and at or below 30s there is no room to be graceful,
so it kills outright at the cap. Verified by inspecting the constructed
command: a 100s cap yields `--kill-after=30s 70s`, a 20s cap yields
`--signal=KILL 20s`.
coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 12, 2026
Three review rounds went into a shared per-image deadline, a min() against
the remaining budget, a clamped backoff and a SIGKILL grace period
subtracted from the cap. All of it existed to bound the worst case, and a
smaller per-attempt cap bounds it just as well without the bookkeeping.

Two attempts of up to 120s replaces it. Measurement supports the number:
the Maven image pulled in 11s on the runner and all four in 30s, so 120s
is a wide margin, and 2 x 120s + 30s of kill grace puts a totally
unreachable registry at roughly 8 minutes across four images before the
step says why. That is well inside the job budget, which is the property
the deadline machinery was there to guarantee.

Kept: the drift guard, the refusal to start a pull that cannot be
bounded, the retry, and the loud final error. Those carry their weight.
@krusche

krusche commented Aug 12, 2026

Copy link
Copy Markdown
Member Author

Trimmed the retry machinery back in 6d3c94a, per the point that this was drifting into unrealistic edge cases.

Three review rounds had accumulated a shared per-image deadline, a min() against the remaining budget, a clamped backoff and a SIGKILL grace period subtracted from the cap. All of it existed to bound the worst case — and a smaller per-attempt cap bounds it just as well without the bookkeeping. Two attempts of up to 120s replaces the lot: 50 lines out, 14 in.

The number comes from the run this PR was written for: the Maven image pulled in 11s, all four in 30s. So 120s is a wide margin, and 2 × 120s plus kill grace puts a totally unreachable registry at roughly 8 minutes across four images before the step reports why — comfortably inside the job budget, which is the only property the deadline arithmetic was guaranteeing.

Kept, because they carry their weight:

  • the drift guard against build.images (a stale tag silently reintroduces the original bug)
  • the refusal to start a pull that cannot be bounded
  • the retry and the loud final error

Re-verified after the trim: all-cached path exits 0 under Bash 3.2 from an arbitrary directory; a missing image with no timeout refuses with exit 1; a failing pull makes both attempts then errors; the extractor still returns the 23 configured images and a stale tag is still caught. shellcheck clean.

Also rebased onto the develop merge that landed on the branch.

On merging

I would not merge this second. The E2E run that proved the fix was on 336d87d; 31f1a08, e163c16, 1d30dc5 and now 6d3c94a all rewrote the pull path and none has been through a full E2E run. The pending run on this head is the one to wait for — the pre-pull step output and a green ProgrammingExercisePracticeMode are the confirmation.

After that the only real gap is human review: five commits of shell, so far read only by bots.

The drift check entered the images block on any `images:` key indented
deeper than `build:`, so a mapping nested further down - inside
`default-docker-flags:`, say - was taken for the build agent's image list.
It read that block instead of the real one: given both, the old extractor
returned the nested entry and none of the configured tags, which would
fail the drift check against a list that is actually correct.

Take the indent of the first key below `build:` as the indent its direct
children sit at, and accept `images:` only there.
@krusche

krusche commented Aug 12, 2026

Copy link
Copy Markdown
Member Author

Both findings are closed at 46cf55d.

Per-image deadline could be exceeded. Resolved, though by a different route than suggested: 6d3c94a dropped the shared-budget arithmetic entirely in favour of a single per-attempt timeout --kill-after=30s 120s. There is no longer a per-image deadline for the grace to overrun — each attempt is capped on its own, two attempts per image, and no comment claims a ceiling the code cannot hold. (The intermediate 1d30dc5 had already made the grace come out of the cap rather than be added to it, 13 minutes after the review was written.)

The extractor accepted any deeper images: under build:. Correct, and it was worse than accepting an extra block — it read the wrong one. Verified against the pre-fix version: given

build:
    default-docker-flags:
        images:
            java:
                default: "nested-too-deep:1"
    images:
        java:
            default: "real-direct-child:2"

the old extractor returned nested-too-deep:1 and none of the real tags, which would have failed the drift check against a perfectly correct IMAGES list.

Fixed by taking the indent of the first key below build: as the indent its direct children sit at, and accepting images: only there. Verified on four inputs: the real application.yml still yields all 23 configured tags (and the unrelated Theia images: block stays out); a direct child is read; a deeper images: preceding the real one is skipped in favour of the real one; and a file with only a deeper images: yields nothing, which trips the script's own "Found no images" error rather than passing silently.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.ci/E2E-tests/prepull-exercise-images.sh (1)

58-89: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Scope extraction to continuous-integration.build.images.

Line 60 accepts every build: mapping. The function then emits values from every direct build.images block. An unrelated block can therefore supply a required tag and make drift validation pass when the build-agent block does not contain it.

Track the continuous-integration: parent and accept build: only within that mapping. Based on learnings: scripts that validate build-agent Docker images must scope parsing to continuous-integration.build.images.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.ci/E2E-tests/prepull-exercise-images.sh around lines 58 - 89, Update
configured_images() so the awk parser tracks the continuous-integration: mapping
and recognizes build: only when it is a direct child of that mapping. Keep
extracting image values solely from continuous-integration.build.images,
ignoring unrelated build/images blocks while preserving the existing indentation
and comment handling.

Source: Learnings

♻️ Duplicate comments (1)
.ci/E2E-tests/prepull-exercise-images.sh (1)

151-154: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Keep the grace period inside the 120-second cap.

--kill-after=30s 120s sends SIGTERM after 120 seconds and can wait 30 more seconds before SIGKILL. A pull that ignores SIGTERM can run for 150 seconds. This violates the stated 120-second attempt cap and reintroduces the previously resolved timeout-accounting issue.

Reserve the grace period within PULL_TIMEOUT_SECONDS. Use --signal=KILL when the cap cannot include the grace period.

Proposed fix
+KILL_GRACE_SECONDS=30
+
-        if "$timeout_bin" --kill-after=30s "${PULL_TIMEOUT_SECONDS}s" docker pull --quiet "$image"; then
+        if [ "$PULL_TIMEOUT_SECONDS" -gt "$KILL_GRACE_SECONDS" ]; then
+            "$timeout_bin" --kill-after="${KILL_GRACE_SECONDS}s" \
+                "$((PULL_TIMEOUT_SECONDS - KILL_GRACE_SECONDS))s" \
+                docker pull --quiet "$image"
+        else
+            "$timeout_bin" --signal=KILL "${PULL_TIMEOUT_SECONDS}s" docker pull --quiet "$image"
+        fi
+        if [ "$?" -eq 0 ]; then
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.ci/E2E-tests/prepull-exercise-images.sh around lines 151 - 154, Update the
timeout invocation in the image-pull retry flow around the docker pull command
so each attempt never exceeds PULL_TIMEOUT_SECONDS, including any termination
grace period. Reserve the grace interval within that cap, or use timeout’s
--signal=KILL option when the configured cap cannot accommodate a grace period;
do not retain --kill-after=30s with the full PULL_TIMEOUT_SECONDS value.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In @.ci/E2E-tests/prepull-exercise-images.sh:
- Around line 58-89: Update configured_images() so the awk parser tracks the
continuous-integration: mapping and recognizes build: only when it is a direct
child of that mapping. Keep extracting image values solely from
continuous-integration.build.images, ignoring unrelated build/images blocks
while preserving the existing indentation and comment handling.

---

Duplicate comments:
In @.ci/E2E-tests/prepull-exercise-images.sh:
- Around line 151-154: Update the timeout invocation in the image-pull retry
flow around the docker pull command so each attempt never exceeds
PULL_TIMEOUT_SECONDS, including any termination grace period. Reserve the grace
interval within that cap, or use timeout’s --signal=KILL option when the
configured cap cannot accommodate a grace period; do not retain --kill-after=30s
with the full PULL_TIMEOUT_SECONDS value.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f22575fc-5062-4f10-ab15-90558c014fab

📥 Commits

Reviewing files that changed from the base of the PR and between 1d30dc5 and 46cf55d.

📒 Files selected for processing (1)
  • .ci/E2E-tests/prepull-exercise-images.sh

@Claudia-Anthropica Claudia-Anthropica left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@krusche The current head resolves the previous blockers: image extraction is restricted to the direct build.images child, and missing images cannot enter an unbounded pull. The simplified retry loop is consistently bounded per attempt, and the captured CI snapshot contains no required check pending or failing.

@krusche
krusche temporarily deployed to playwright-e2e-tests August 12, 2026 17:09 — with GitHub Actions Inactive
@krusche
krusche temporarily deployed to playwright-e2e-tests August 12, 2026 17:59 — with GitHub Actions Inactive
@krusche krusche added this to the 9.9 milestone Aug 12, 2026
@krusche
krusche merged commit 253b969 into develop Aug 12, 2026
38 of 40 checks passed
@krusche
krusche deleted the chore/prepull-e2e-exercise-images branch August 12, 2026 19:05
@github-project-automation github-project-automation Bot moved this from Ready For Review to Merged in Artemis Development Aug 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Merged

Development

Successfully merging this pull request may close these issues.

3 participants