mirror of
https://github.com/redwoodjs/local-ci.git
synced 2026-09-19 07:54:29 +08:00
edce84b8b1
* 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>
100 lines
3.2 KiB
Bash
Executable File
100 lines
3.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Smoke #289: `agent-ci run --json` emits a well-formed NDJSON event stream
|
|
# on stdout. Asserts run.start (with schemaVersion=1), step.start/finish for
|
|
# every step, job.finish, and a final run.finish with status=passed. Also
|
|
# asserts every JSON-shaped line on stdout parses and carries a known event.
|
|
#
|
|
# Requires Docker. Run from anywhere:
|
|
#
|
|
# ./scripts/smoke-289.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)"
|
|
trap 'rm -rf "$TMP"' EXIT
|
|
|
|
mkdir -p "$TMP/.github/workflows"
|
|
cat > "$TMP/.github/workflows/ok.yml" <<'YAML'
|
|
name: ok
|
|
on: push
|
|
jobs:
|
|
ok-job:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: first
|
|
run: echo first
|
|
- name: second
|
|
run: echo second
|
|
YAML
|
|
|
|
cd "$TMP"
|
|
git init -q
|
|
git config user.email smoke@289
|
|
git config user.name smoke
|
|
git remote add origin https://github.com/smoke/issue-289.git
|
|
git add . >/dev/null
|
|
git commit -q -m init
|
|
export GITHUB_REPO=smoke/issue-289
|
|
|
|
OUT="$TMP/events.ndjson"
|
|
echo "▶ agent-ci run --json -w ok.yml…"
|
|
"$AGENT_CI" run --json -w ok.yml > "$OUT" 2>"$TMP/stderr.log"
|
|
EC=$?
|
|
echo "▶ exit code: $EC"
|
|
|
|
if [ "$EC" -ne 0 ]; then
|
|
echo "✗ FAIL: agent-ci exited $EC (expected 0)"
|
|
echo "── stderr ─────"; cat "$TMP/stderr.log"
|
|
echo "── stdout ─────"; cat "$OUT"
|
|
exit 1
|
|
fi
|
|
|
|
# Filter to JSON-shaped lines only and parse each one. Any malformed JSON or
|
|
# unknown event aborts.
|
|
JSON_LINES="$(grep -E '^\{' "$OUT" || true)"
|
|
if [ -z "$JSON_LINES" ]; then
|
|
echo "✗ FAIL: no JSON events on stdout"
|
|
cat "$OUT"
|
|
exit 1
|
|
fi
|
|
|
|
# Every JSON line must parse and have a known `event` field.
|
|
KNOWN='run.start|run.finish|run.paused|job.start|job.finish|step.start|step.finish|diagnostic'
|
|
while IFS= read -r line; do
|
|
ev="$(echo "$line" | jq -er '.event' 2>/dev/null || echo "<no-event>")"
|
|
if ! echo "$ev" | grep -qE "^($KNOWN)$"; then
|
|
echo "✗ FAIL: unrecognized event '$ev' on line: $line"
|
|
exit 1
|
|
fi
|
|
done <<< "$JSON_LINES"
|
|
|
|
# Required events.
|
|
require() {
|
|
local pred="$1" desc="$2"
|
|
if ! echo "$JSON_LINES" | jq -e "select($pred)" > /dev/null; then
|
|
echo "✗ FAIL: missing required event — $desc"
|
|
echo "── events ─────"; echo "$JSON_LINES"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
require '.event=="run.start" and .schemaVersion==1' "run.start with schemaVersion=1"
|
|
require '.event=="step.start" and .step=="first"' "step.start for 'first'"
|
|
require '.event=="step.finish" and .step=="first" and .status=="passed"' "step.finish passed for 'first'"
|
|
require '.event=="step.start" and .step=="second"' "step.start for 'second'"
|
|
require '.event=="step.finish" and .step=="second" and .status=="passed"' "step.finish passed for 'second'"
|
|
require '.event=="job.finish" and .status=="passed"' "job.finish passed"
|
|
require '.event=="run.finish" and .status=="passed"' "run.finish passed"
|
|
|
|
echo
|
|
echo "✓ PASS: NDJSON event stream is well-formed and complete."
|