From c89e6d75405362de91200f98a9bed0b8ca3af7eb Mon Sep 17 00:00:00 2001 From: Mohab Mohie Date: Sat, 1 Aug 2026 20:10:29 +0300 Subject: [PATCH 1/4] Fix(ci): retry live launcher Maven build (#4373) --- .../build-shaft-mcp-live-jar/action.yml | 14 +++++++-- .../scripts/test_build_shaft_mcp_live_jar.py | 29 +++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 tests/scripts/test_build_shaft_mcp_live_jar.py diff --git a/.github/actions/build-shaft-mcp-live-jar/action.yml b/.github/actions/build-shaft-mcp-live-jar/action.yml index 0f9982eadbb..7ae80f70215 100644 --- a/.github/actions/build-shaft-mcp-live-jar/action.yml +++ b/.github/actions/build-shaft-mcp-live-jar/action.yml @@ -22,8 +22,18 @@ runs: - name: Build shaft-mcp with dependencies shell: bash run: | - mvn --batch-mode -pl shaft-mcp -am install -DskipTests -Dgpg.skip - mvn --batch-mode -pl shaft-mcp dependency:copy-dependencies -DincludeScope=runtime '-DoutputDirectory=${maven.multiModuleProjectDirectory}/shaft-mcp/target/dependency' -DskipTests -Dgpg.skip + for attempt in 1 2 3; do + mvn --batch-mode -pl shaft-mcp -am install -DskipTests -Dgpg.skip \ + && mvn --batch-mode -pl shaft-mcp dependency:copy-dependencies -DincludeScope=runtime '-DoutputDirectory=${maven.multiModuleProjectDirectory}/shaft-mcp/target/dependency' -DskipTests -Dgpg.skip + status=$? + if [ "$status" -eq 0 ]; then + exit 0 + fi + if [ "$attempt" -lt 3 ]; then + sleep "$((attempt * 10))" + fi + done + exit "$status" - name: Write shaft-mcp launcher argfile shell: bash run: | diff --git a/tests/scripts/test_build_shaft_mcp_live_jar.py b/tests/scripts/test_build_shaft_mcp_live_jar.py new file mode 100644 index 00000000000..e5a082aa33b --- /dev/null +++ b/tests/scripts/test_build_shaft_mcp_live_jar.py @@ -0,0 +1,29 @@ +import re +import unittest +from pathlib import Path + + +ACTION_PATH = Path(__file__).resolve().parents[2] / ".github" / "actions" / "build-shaft-mcp-live-jar" / "action.yml" + + +class BuildShaftMcpLiveJarTest(unittest.TestCase): + def test_launcher_build_retries_the_complete_maven_sequence(self): + action = ACTION_PATH.read_text(encoding="utf-8") + build_script = action.split(" - name: Write shaft-mcp launcher argfile", 1)[0] + + self.assertIn("for attempt in 1 2 3; do", build_script) + self.assertIn('sleep "$((attempt * 10))"', build_script) + self.assertIn('exit "$status"', build_script) + self.assertRegex( + build_script, + re.compile( + r"mvn --batch-mode -pl shaft-mcp -am install .*?\n" + r"\s*&& mvn --batch-mode -pl shaft-mcp dependency:copy-dependencies .*?\n" + r"\s*status=\$\?", + re.DOTALL, + ), + ) + + +if __name__ == "__main__": + unittest.main() From 4e1af2906c77719a6b996825760e448419b357e1 Mon Sep 17 00:00:00 2001 From: Mohab Mohie Date: Sat, 1 Aug 2026 20:10:57 +0300 Subject: [PATCH 2/4] Docs(guard): clarify bounded CI reactor build (#4365) --- scripts/agents/guard.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/agents/guard.py b/scripts/agents/guard.py index 9eca16a9e0b..e6c9a0459d5 100644 --- a/scripts/agents/guard.py +++ b/scripts/agents/guard.py @@ -9,6 +9,10 @@ # and .memory/memory/gotchas/ # unscoped-am-mvn-test-can-crash-the-jvm-across-the-whole-reactor.md -- # both repo-tracked so they travel with every clone/worktree) +# CI exception: e2eTests.yml's JUnit E2E reactor job deliberately builds +# dependencies with -am. It remains bounded by -pl shaft-engine, its +# -Dtest=Junit*,MoonTests default, and -DheadlessExecution=true, unlike +# the local test commands this guard protects. # R2 Never auto-open/serve Allure reports # R3 Never run GUI-opening commands on Windows (AGENTS.md Windows/Codex Safety) # R8 Deny mutating `git stash` subcommands (pop/drop/apply/clear/push, and From e38b7a0414e6c3b393d7315466ecbc285da8192a Mon Sep 17 00:00:00 2001 From: Mohab Mohie Date: Sat, 1 Aug 2026 20:17:06 +0300 Subject: [PATCH 3/4] Fix(ci): preserve launcher retries under errexit (#4373) --- .../build-shaft-mcp-live-jar/action.yml | 10 ++- .../scripts/test_build_shaft_mcp_live_jar.py | 88 ++++++++++++++++++- 2 files changed, 92 insertions(+), 6 deletions(-) diff --git a/.github/actions/build-shaft-mcp-live-jar/action.yml b/.github/actions/build-shaft-mcp-live-jar/action.yml index 7ae80f70215..218482bf6a4 100644 --- a/.github/actions/build-shaft-mcp-live-jar/action.yml +++ b/.github/actions/build-shaft-mcp-live-jar/action.yml @@ -23,10 +23,12 @@ runs: shell: bash run: | for attempt in 1 2 3; do - mvn --batch-mode -pl shaft-mcp -am install -DskipTests -Dgpg.skip \ - && mvn --batch-mode -pl shaft-mcp dependency:copy-dependencies -DincludeScope=runtime '-DoutputDirectory=${maven.multiModuleProjectDirectory}/shaft-mcp/target/dependency' -DskipTests -Dgpg.skip - status=$? - if [ "$status" -eq 0 ]; then + if { + mvn --batch-mode -pl shaft-mcp -am install -DskipTests -Dgpg.skip \ + && mvn --batch-mode -pl shaft-mcp dependency:copy-dependencies -DincludeScope=runtime '-DoutputDirectory=${maven.multiModuleProjectDirectory}/shaft-mcp/target/dependency' -DskipTests -Dgpg.skip + status=$? + [ "$status" -eq 0 ] + }; then exit 0 fi if [ "$attempt" -lt 3 ]; then diff --git a/tests/scripts/test_build_shaft_mcp_live_jar.py b/tests/scripts/test_build_shaft_mcp_live_jar.py index e5a082aa33b..12db7202fe6 100644 --- a/tests/scripts/test_build_shaft_mcp_live_jar.py +++ b/tests/scripts/test_build_shaft_mcp_live_jar.py @@ -1,4 +1,7 @@ +import os import re +import subprocess +import tempfile import unittest from pathlib import Path @@ -7,9 +10,15 @@ class BuildShaftMcpLiveJarTest(unittest.TestCase): - def test_launcher_build_retries_the_complete_maven_sequence(self): + @staticmethod + def _build_script() -> str: action = ACTION_PATH.read_text(encoding="utf-8") - build_script = action.split(" - name: Write shaft-mcp launcher argfile", 1)[0] + return action.split(" run: |\n", 1)[1].split( + " - name: Write shaft-mcp launcher argfile", 1 + )[0] + + def test_launcher_build_retries_the_complete_maven_sequence(self): + build_script = self._build_script() self.assertIn("for attempt in 1 2 3; do", build_script) self.assertIn('sleep "$((attempt * 10))"', build_script) @@ -24,6 +33,81 @@ def test_launcher_build_retries_the_complete_maven_sequence(self): ), ) + def test_launcher_build_retries_after_copy_dependencies_fails_under_errexit(self): + with tempfile.TemporaryDirectory() as temporary_directory: + temporary_path = Path(temporary_directory) + maven_log = temporary_path / "maven.log" + fake_maven = temporary_path / "mvn" + fake_maven.write_bytes( + b"#!/usr/bin/env bash\n" + b"log=\"$(dirname \"$0\")/maven.log\"\n" + b"printf '%s\\n' \"$*\" >> \"$log\"\n" + b"if [ \"$(wc -l < \"$log\")\" -eq 2 ]; then\n" + b" exit 17\n" + b"fi\n" + ) + fake_sleep = temporary_path / "sleep" + fake_sleep.write_bytes(b"#!/usr/bin/env bash\n") + fake_maven.chmod(0o755) + fake_sleep.chmod(0o755) + environment = os.environ | { + "PATH": f"{temporary_path}{os.pathsep}{os.environ['PATH']}", + } + + completed = subprocess.run( + ["bash", "-e"], + input=( + b'PATH="$(dirname "$(command -v mvn)"):$PATH"\n' + + self._build_script().encode() + ), + capture_output=True, + env=environment, + ) + + self.assertEqual(completed.returncode, 0, completed.stderr.decode()) + maven_commands = maven_log.read_text(encoding="utf-8").splitlines() + self.assertEqual(len(maven_commands), 4) + self.assertIn("install", maven_commands[0]) + self.assertIn("dependency:copy-dependencies", maven_commands[1]) + self.assertIn("install", maven_commands[2]) + self.assertIn("dependency:copy-dependencies", maven_commands[3]) + + def test_launcher_build_propagates_the_final_copy_dependencies_failure(self): + with tempfile.TemporaryDirectory() as temporary_directory: + temporary_path = Path(temporary_directory) + maven_log = temporary_path / "maven.log" + fake_maven = temporary_path / "mvn" + fake_maven.write_bytes( + b"#!/usr/bin/env bash\n" + b"log=\"$(dirname \"$0\")/maven.log\"\n" + b"printf '%s\\n' \"$*\" >> \"$log\"\n" + b"case \"$*\" in *dependency:copy-dependencies*) exit 17;; esac\n" + ) + fake_sleep = temporary_path / "sleep" + fake_sleep.write_bytes(b"#!/usr/bin/env bash\n") + fake_maven.chmod(0o755) + fake_sleep.chmod(0o755) + environment = os.environ | { + "PATH": f"{temporary_path}{os.pathsep}{os.environ['PATH']}", + } + + completed = subprocess.run( + ["bash", "-e"], + input=( + b'PATH="$(dirname "$(command -v mvn)"):$PATH"\n' + + self._build_script().encode() + ), + capture_output=True, + env=environment, + ) + + self.assertEqual(completed.returncode, 17, completed.stderr.decode()) + maven_commands = maven_log.read_text(encoding="utf-8").splitlines() + self.assertEqual(len(maven_commands), 6) + for install, copy_dependencies in zip(maven_commands[::2], maven_commands[1::2]): + self.assertIn("install", install) + self.assertIn("dependency:copy-dependencies", copy_dependencies) + if __name__ == "__main__": unittest.main() From 2f8548f058f2d4133554706d44a720ccf22f0986 Mon Sep 17 00:00:00 2001 From: Mohab Mohie Date: Sat, 1 Aug 2026 20:20:13 +0300 Subject: [PATCH 4/4] Docs(memory): record live launcher retry gotcha (#4373) --- .memory/events.jsonl | 1 + ...launcher-retries-must-be-errexit-safe.json | 25 +++++++++++++++++++ ...e-launcher-retries-must-be-errexit-safe.md | 1 + 3 files changed, 27 insertions(+) create mode 100644 .memory/memory/gotchas/live-launcher-retries-must-be-errexit-safe.json create mode 100644 .memory/memory/gotchas/live-launcher-retries-must-be-errexit-safe.md diff --git a/.memory/events.jsonl b/.memory/events.jsonl index c3ca2a7aa95..feb0dda2444 100644 --- a/.memory/events.jsonl +++ b/.memory/events.jsonl @@ -641,3 +641,4 @@ {"actor":"agent","event":"memory.deleted","id":"decision.guidance-budgets-are-per-surface-since-3745-no-global-byte-pool","timestamp":"2026-08-01T16:13:37+03:00"} {"actor":"agent","event":"memory.updated","id":"workflow.user-level-claude-harness-syncs-from-canonical-claude-user-harness-via-scripts-agents-sync-user-harness-py","timestamp":"2026-08-01T16:14:23+03:00"} {"actor":"agent","event":"memory.updated","id":"workflow.verify-intellij-platform-sdk-api-shapes-against-the-real-installed-jars-jar-tf-javap-not-memory-before-writing-an-implementation-spec","timestamp":"2026-08-01T16:14:23+03:00"} +{"actor":"agent","event":"memory.created","id":"gotcha.live-launcher-retries-must-be-errexit-safe","timestamp":"2026-08-01T20:20:05+03:00"} diff --git a/.memory/memory/gotchas/live-launcher-retries-must-be-errexit-safe.json b/.memory/memory/gotchas/live-launcher-retries-must-be-errexit-safe.json new file mode 100644 index 00000000000..bfb73bc6156 --- /dev/null +++ b/.memory/memory/gotchas/live-launcher-retries-must-be-errexit-safe.json @@ -0,0 +1,25 @@ +{ + "body_path": "memory/gotchas/live-launcher-retries-must-be-errexit-safe.md", + "content_hash": "sha256:6211d78737bca116c53138fe0bd1e7bee75cebf25800911a23a848025be963ba", + "created_at": "2026-08-01T20:20:05+03:00", + "evidence": [], + "facets": { + "category": "gotcha" + }, + "id": "gotcha.live-launcher-retries-must-be-errexit-safe", + "scope": { + "branch": null, + "kind": "project", + "project": "project.shaft-engine", + "task": null + }, + "source": { + "kind": "agent", + "task": "Resolve #4373 Maven Central nightly failure" + }, + "status": "active", + "tags": [], + "title": "Live launcher retries must be errexit-safe", + "type": "gotcha", + "updated_at": "2026-08-01T20:20:05+03:00" +} diff --git a/.memory/memory/gotchas/live-launcher-retries-must-be-errexit-safe.md b/.memory/memory/gotchas/live-launcher-retries-must-be-errexit-safe.md new file mode 100644 index 00000000000..ea0d42c05d4 --- /dev/null +++ b/.memory/memory/gotchas/live-launcher-retries-must-be-errexit-safe.md @@ -0,0 +1 @@ +The shared build-shaft-mcp-live-jar composite action runs under Bash -e. Keep its Maven install and dependency-copy pair inside an if condition; a bare `mvn && mvn; status=$?` exits on a failing second command before retry. Behavioral tests must cover retry after the second command fails and final failure propagation. \ No newline at end of file