From febaa1ef548e65ad5a842b3b0ba8d7bada14d5d7 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Fri, 18 Sep 2026 12:44:55 +0000 Subject: [PATCH] fix(upgrade): capture an AGENTS.md composition failure instead of aborting on it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit upgrade.sh runs under `set -e`, and regenerate_agents_md is the only phase function in it with a non-zero return path. Called bare, one stale memory file did not degrade the run, it ended it: runtime guidance projection, claude-code runtime sync, runtime signature and instructions, subagents, both chat-bridge unit writers, the WordPress and worker service reconciliation, the installation profile, and print_summary all never executed. Nothing said so. The run stopped on the last warn line of the composition attempt, so the log reads as though it simply ended — no "Upgrade complete", no Updated list, no Pending list, and no statement that the remaining phases were skipped. Diagnosing the h44 nightly cost real time for exactly this reason: the output ends nowhere near the actual exit point. Capture it the way the same block already captures convergence_run, run the remaining phases, print the summary, and exit non-zero at the end. A stale AGENTS.md is worth failing the job over — that is how the underlying composition bug surfaced at all — but it is not worth abandoning service reconciliation halfway through. print_summary now names this case, because with every other phase converged a non-zero exit would otherwise have no visible cause in the summary. The test runs the real execute block against stubbed phases rather than asserting on source text, so dropping the capture fails it however the call is written. Against the unfixed script the transcript stops at regenerate_agents_md and all twelve later phases are reported missing, reproducing the live failure exactly. Fixes #607 --- .github/workflows/shell.yml | 1 + ...gents-md-failure-does-not-abort-upgrade.sh | 115 ++++++++++++++++++ upgrade.sh | 17 ++- 3 files changed, 132 insertions(+), 1 deletion(-) create mode 100755 tests/agents-md-failure-does-not-abort-upgrade.sh diff --git a/.github/workflows/shell.yml b/.github/workflows/shell.yml index a234cde..b869223 100644 --- a/.github/workflows/shell.yml +++ b/.github/workflows/shell.yml @@ -288,6 +288,7 @@ jobs: test: - agent-state-ownership - agents-md-backup-retention + - agents-md-failure-does-not-abort-upgrade - carried-claude-code-plugin - ci-coverage - codex-runtime diff --git a/tests/agents-md-failure-does-not-abort-upgrade.sh b/tests/agents-md-failure-does-not-abort-upgrade.sh new file mode 100755 index 0000000..f03eb57 --- /dev/null +++ b/tests/agents-md-failure-does-not-abort-upgrade.sh @@ -0,0 +1,115 @@ +#!/bin/bash +# tests/agents-md-failure-does-not-abort-upgrade.sh +# +# A failed AGENTS.md composition must fail the run WITHOUT truncating it. +# +# upgrade.sh runs under `set -e`, and regenerate_agents_md is the only phase +# function in it with a non-zero return path. Called bare, one stale memory file +# ended the run at that line: eleven reconciliation phases and print_summary +# never executed, and the log stopped with nothing saying the rest had been +# skipped (#607). +# +# This runs the real execute block from upgrade.sh against stubbed phases rather +# than asserting on its source text, so it fails if the capture is dropped no +# matter how the call is written. +set -eu + +ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +UPGRADE="$ROOT_DIR/upgrade.sh" +WORK="$(mktemp -d "${TMPDIR:-/tmp}/wpca-agents-md-abort.XXXXXX")" +trap 'rm -rf "$WORK"' EXIT + +# The execute block is everything from the first status accumulator to EOF. +sed -n '/^PLUGIN_ONLY_EXIT_STATUS=0$/,$p' "$UPGRADE" > "$WORK/execute.sh" +if [ ! -s "$WORK/execute.sh" ]; then + echo "FAIL: could not extract the execute block from upgrade.sh" >&2 + exit 1 +fi + +# Every phase reached after regenerate_agents_md. If the abort returns, these +# stop appearing in the transcript. +PHASES_AFTER=( + runtime_guidance_sync_managed_codex_projection + sync_claude_code_runtime + sync_runtime_signature + sync_runtime_instructions + opencode_project_subagents_optional + update_chat_bridge_systemd + update_chat_bridge_launchd + reconcile_wordpress_service + reconcile_datamachine_worker_service + refresh_opencode_runtime_signature_phase + installation_profile_write + print_summary +) + +{ + echo 'set -eu' + # Narrow-scope flags off; this is the full default upgrade path. + for flag in PLUGINS_ONLY SKILLS_ONLY KIMAKI_ONLY AGENTS_MD_ONLY RECONCILE_SERVICES_ONLY DRY_RUN; do + echo "${flag}=false" + done + echo 'SITE_PATH=/tmp/site' + echo 'SCRIPT_DIR=/tmp/wpca' + echo 'INSTALLATION_OPERATION_UPGRADE=upgrade' + echo '_run_filter_active() { return 0; }' + echo 'update_data_machine_plugins() { :; }' + echo 'convergence_run() { :; }' + echo 'reconciler_print_partial_evidence() { echo "RAN reconciler_print_partial_evidence"; }' + + # Phases before the failing one, plus the unconditional ones. + for phase in reconcile_provider_and_service_state sync_cli_transport_runtime \ + update_ai_gateway sync_chat_bridge_config systems_capabilities_apply \ + check_opencode_json_drift ai_gateway_configure_opencode sync_skills \ + "${PHASES_AFTER[@]}"; do + echo "${phase}() { echo \"RAN ${phase}\"; }" + done + + # The failure under test. + echo 'regenerate_agents_md() { echo "RAN regenerate_agents_md"; return 1; }' + + cat "$WORK/execute.sh" +} > "$WORK/harness.sh" + +set +e +transcript="$(bash "$WORK/harness.sh" 2>&1)" +status=$? +set -e + +failed=0 + +if [ "$status" -eq 0 ]; then + echo "FAIL: a failed AGENTS.md composition must still exit non-zero (got 0)" >&2 + failed=1 +fi + +for phase in "${PHASES_AFTER[@]}"; do + case "$transcript" in + *"RAN ${phase}"*) ;; + *) + echo "FAIL: ${phase} did not run after the AGENTS.md failure" >&2 + failed=1 + ;; + esac +done + +# Guard the inverse: the capture must not swallow the failure into a clean exit +# reported as success by the summary. +case "$transcript" in + *"RAN regenerate_agents_md"*) ;; + *) + echo "FAIL: harness never reached regenerate_agents_md" >&2 + failed=1 + ;; +esac + +if [ "$failed" -ne 0 ]; then + echo "--- transcript ---" >&2 + echo "$transcript" >&2 + echo "--- exit status: $status ---" >&2 + exit 1 +fi + +echo "ok remaining phases run after an AGENTS.md composition failure" +echo "ok the run still exits non-zero (${status})" +echo "OK (tests/agents-md-failure-does-not-abort-upgrade.sh)" diff --git a/upgrade.sh b/upgrade.sh index 13081a8..bd89b2e 100755 --- a/upgrade.sh +++ b/upgrade.sh @@ -1302,6 +1302,10 @@ print_summary() { warn "Plugin upgrade partially completed." elif [ "${CONVERGENCE_EXIT_STATUS:-0}" -ne 0 ]; then warn "Desired-state convergence partially completed." + elif [ "${AGENTS_MD_EXIT_STATUS:-0}" -ne 0 ]; then + # Named here rather than left to the reader: every other phase converged, + # so without this line a non-zero exit has no visible cause in the summary. + warn "Upgrade completed except AGENTS.md — composition failed; see above." else log "Upgrade complete." fi @@ -1457,6 +1461,7 @@ _print_plugins_only_verify_block() { PLUGIN_ONLY_EXIT_STATUS=0 CONVERGENCE_EXIT_STATUS=0 +AGENTS_MD_EXIT_STATUS=0 update_data_machine_plugins || PLUGIN_ONLY_EXIT_STATUS=$? if [ "$PLUGINS_ONLY" != true ]; then CONVERGENCE_ENTRYPOINT="$SCRIPT_DIR/upgrade.sh" @@ -1482,7 +1487,14 @@ if _run_filter_active reconciliation; then ai_gateway_configure_opencode fi sync_skills -regenerate_agents_md +# Captured, not aborted. This is the only phase function here with a non-zero +# return path, and under `set -e` a bare call made one stale memory file end the +# run: eleven reconciliation phases and print_summary never happened, and the +# log simply stopped mid-phase with nothing saying the rest had been skipped. +# A failed compose is still worth a non-zero exit at the end — see the +# AGENTS_MD_EXIT_STATUS check below — but not worth abandoning service +# reconciliation halfway through. +regenerate_agents_md || AGENTS_MD_EXIT_STATUS=$? if _run_filter_active agents-md; then runtime_guidance_sync_managed_codex_projection fi @@ -1508,3 +1520,6 @@ if [ "$CONVERGENCE_EXIT_STATUS" -ne 0 ]; then reconciler_print_partial_evidence exit "$CONVERGENCE_EXIT_STATUS" fi +if [ "$AGENTS_MD_EXIT_STATUS" -ne 0 ]; then + exit "$AGENTS_MD_EXIT_STATUS" +fi