Files
redwoodjs__local-ci/scripts/repro-315.sh
T
Peter Pistorius edce84b8b1 fix(cli): unblock pause-on-failure pipes + NDJSON event stream (#315, #289) (#323)
* fix(cli): unblock --pause-on-failure in non-TTY pipes (#315)

Spawn the run as a detached worker when stdout isn't a TTY (and we're
not under an LLM harness), then exit 77 the moment the worker emits a
`run.paused` NDJSON event — freeing the caller's pipe while the worker
keeps the container + DTU alive for `agent-ci retry`. Retry tails the
same log so a re-failure surfaces as another exit-77.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(cli): NDJSON event stream behind --json (#289)

Extends the existing pause/finish sentinel emitter into a full lifecycle
event stream. New `--json` flag (and `AGENT_CI_JSON=1` env var) emits
`run.start`, `job.start`/`finish`, `step.start`/`finish`, `run.paused`,
`run.finish`, `diagnostic` events on stdout — one JSON object per line —
each carrying an `event` discriminator and a `schemaVersion: 1` on
`run.start`.

`--json` is decoupled from `--quiet` so existing `-q` callers keep silent
stdout; the diff renderer is auto-suppressed under `--json` so ANSI
sequences don't collide with the stream. Detached-worker mode (#315)
keeps emitting only sentinels — the launcher filters non-sentinel events
anyway, so non-JSON callers don't pay the cost.

Renames `run.completed` -> `run.finish` to match the issue's naming.

Closes #289.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(cli): suppress printSummary under --json

The ━━━ FAILURES ━━━ / ━━━ SUMMARY ━━━ blocks write to stdout
unconditionally, which interleaves with the NDJSON event stream and
breaks line-based parsers. Skip them when isJsonMode() — consumers get
the same information from job.finish + run.finish events.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(cli): AGENT_CI_DETACHED=1 forces detached launcher on TTY

Overload AGENT_CI_DETACHED so it can serve both roles:

  - `=<absolute path>` → set by the launcher on its worker child;
    `isDetachedWorker()` returns true.
  - `=1` (or any non-absolute value) → caller-side opt-in: force the
    launcher path even when stdout is a TTY. Useful for manual
    verification of the #315 pause-then-exit-77 flow without having to
    construct a non-TTY pipe.

The two are distinguished by whether the value starts with `/`. New
`isForceDetachedRequested()` helper drives a `forceDetached` arg into
`shouldLaunchDetached`, which still respects `alreadyWorker`,
`agentMode`, and the `pauseOnFailure` requirement.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ci): smoke for --json NDJSON event stream (#289)

scripts/smoke-289.sh creates a tmp repo with a 2-step passing workflow,
runs `agent-ci run --json`, and asserts:
  - run.start carries schemaVersion=1
  - step.start + step.finish for each declared step
  - job.finish status=passed
  - run.finish status=passed
  - every JSON-shaped line on stdout has a recognized event field

.github/workflows/smoke-json-events.yml runs the script in CI on every
PR (gated by the same safe-to-run / collaborator predicate as the other
smokes).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(ci): stop smoke-resource-mismatch from hanging on real GitHub runners

The fixture in .github/workflows/smoke-resource-mismatch.yml declares
`runs-on: ubuntu-latest-999-cores` so agent-ci's resource-fidelity
classifier deterministically tags the job as `degraded` on any host.
With its previous `pull_request_target` trigger, GitHub Actions also
picked it up on every PR and queued a job that no real runner can
satisfy — five queued runs piled up on PR #323 alone, each timing out
after hours.

Switch the workflow to `workflow_dispatch:`-only so GitHub never
auto-dispatches it, and teach `isWorkflowRelevant` to treat
`workflow_dispatch:`-only workflows as relevant for `agent-ci run --all`
so the local smoke keeps running. The opt-in is gated to the sole-event
case to avoid overriding `pull_request` / `push` path/branch filters on
workflows that legitimately combine triggers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 20:39:04 +02:00

96 lines
2.6 KiB
Bash
Executable File

#!/usr/bin/env bash
# Reproduce issue #315: `agent-ci run --pause-on-failure` blocks indefinitely
# when stdout is piped or redirected. With the fix in place, the launcher
# exits with code 77 the instant the worker pauses, freeing the caller's pipe.
#
# Without the fix this script's `timeout 60` fires (exit 124).
# With the fix the script exits 77 within a few seconds of the step failing.
#
# Requires Docker. Run from anywhere:
#
# ./scripts/repro-315.sh
set -euo pipefail
SOURCE="${BASH_SOURCE[0]}"
while [ -L "$SOURCE" ]; do
DIR="$(cd "$(dirname "$SOURCE")" && pwd)"
SOURCE="$(readlink "$SOURCE")"
[[ "$SOURCE" != /* ]] && SOURCE="$DIR/$SOURCE"
done
SCRIPT_DIR="$(cd "$(dirname "$SOURCE")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
AGENT_CI="$REPO_ROOT/scripts/agent-ci-dev.sh"
TMP="$(mktemp -d)"
RUNNER_NAME=""
cleanup() {
# Best-effort: abort whatever runner the worker launched and tear down the
# tmp repo. We don't know the runner name until the sentinel arrives, so
# parse it from the captured output if available.
if [ -n "$RUNNER_NAME" ]; then
"$AGENT_CI" abort --name "$RUNNER_NAME" >/dev/null 2>&1 || true
fi
rm -rf "$TMP"
}
trap cleanup EXIT
# Tiny throwaway repo with a workflow whose single step fails immediately.
mkdir -p "$TMP/.github/workflows"
cat > "$TMP/.github/workflows/fail.yml" <<'YAML'
name: fail
on: push
jobs:
fail-job:
runs-on: ubuntu-latest
steps:
- run: |
echo "step about to fail"
exit 1
YAML
cd "$TMP"
git init -q
git config user.email repro@315
git config user.name repro
git remote add origin https://github.com/repro/issue-315.git
git add . >/dev/null
git commit -q -m init
export GITHUB_REPO=repro/issue-315
OUT="$TMP/out.log"
echo "▶ agent-ci run -w fail.yml -p (piped through cat, 60s timeout)…"
START=$SECONDS
set +e
timeout 60 "$AGENT_CI" run -w fail.yml -p 2>&1 | tee "$OUT" | cat >/dev/null
EC="${PIPESTATUS[0]}"
set -e
ELAPSED=$((SECONDS - START))
# Grab the runner name from the run.paused NDJSON event for cleanup.
RUNNER_NAME="$(grep -oE '"event":"run.paused"[^}]*"runner":"[^"]+"' "$OUT" | grep -oE '"runner":"[^"]+"' | head -1 | cut -d'"' -f4 || true)"
echo
echo "▶ exit code: $EC elapsed: ${ELAPSED}s runner: ${RUNNER_NAME:-<none>}"
echo
echo "▶ tail of captured output:"
tail -10 "$OUT"
case "$EC" in
124)
echo
echo "✗ FAIL: command timed out at 60s — issue #315 is still reproducible."
exit 1
;;
77)
echo
echo "✓ PASS: launcher exited 77 in ${ELAPSED}s — fix works."
exit 0
;;
*)
echo
echo "✗ UNEXPECTED: exit $EC (expected 77 for paused, 124 for hang)."
exit 1
;;
esac