Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,6 @@ After adding or editing a caller workflow:
- Machine-oriented index: `llms.txt`
- Release notes: `CHANGELOG.md`
- Real consumer: `LLazyEmail/_playing_with_lit` → `.github/workflows/render-email-action.yml` (explained in `docs/real-world-example.md`)

## Failure annotations
A non-zero build, validation, or command exits with an `::error::` annotation. The annotation includes the first cause line from the command (for example `error TS2307: Cannot find module ...`), and the failure block reprints that cause plus the last 20 lines.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ Your own `env:` on the job is passed through as usual, so secrets such as `NODE_

## Failure handling

- A template that fails to build, fails validation, or writes an empty file fails the job with an error annotation naming it.
- A template that fails to build, fails validation, or writes an empty file fails the job with an error annotation naming it. The annotation also includes the first cause line from the command (for example `error TS2307: Cannot find module ...`), and the failure block reprints that cause plus the last lines of the command.
- With `fail-fast: false` the other templates still render. The artifact, the job summary and the pull request comment are produced, then the job fails. `failed.txt` in the artifact and a **Failed to render** section in the comment list what broke.
- If every template fails there is nothing to publish and the job fails immediately.
- An email over `size-limit-kb` produces a warning annotation, not a failure.
Expand Down
68 changes: 59 additions & 9 deletions scripts/render.sh
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,62 @@ trim() {
printf '%s' "$1" | tr -d '\r' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//'
}

# Last failing command's cause line and tail. Set by run_captured.
FAILURE_CAUSE=""
FAILURE_TAIL=""

# Pull the most useful line out of a failed command's log.
# Prefers compiler and module errors over a generic exit line.
extract_cause() {
local file=$1 line
line=$(grep -E 'error TS[0-9]+|Cannot find module|ERR_MODULE_NOT_FOUND|SyntaxError|TypeError|^Error:|error:' "$file" | head -1 || true)
if [ -z "$line" ]; then
line=$(grep -E 'failed|exited' "$file" | head -1 || true)
fi
# Annotations are one line. Collapse whitespace.
printf '%s' "$line" | tr '\n\r' ' ' | sed 's/[[:space:]]\{1,\}/ /g;s/^ //;s/ $//'
}

# Run a command, stream its output, and keep the cause plus the last lines.
# Usage: run_captured command [args...]
run_captured() {
local log rc
log=$(mktemp)
set +e
"$@" >"$log" 2>&1
rc=$?
set -e
cat "$log"
FAILURE_CAUSE=$(extract_cause "$log")
FAILURE_TAIL=$(tail -n 20 "$log")
rm -f "$log"
return "$rc"
}

# Print an ::error:: annotation followed by a short block that says what failed
# and what to check next. $2 may contain \n sequences.
# When FAILURE_CAUSE is set, it is appended to the annotation so the check
# summary shows the real error (tsc, missing module, ...) without scrolling.
explain_failure() {
local what=$1 detail=$2
local annotation=$what
if [ -n "$FAILURE_CAUSE" ]; then
annotation="$what: $FAILURE_CAUSE"
fi
echo ""
echo "::error::${what}"
echo "::error::${annotation}"
echo "----------------------------------------"
printf '%b\n' "$detail"
if [ -n "$FAILURE_CAUSE" ]; then
echo ""
echo "Cause:"
echo " $FAILURE_CAUSE"
fi
if [ -n "$FAILURE_TAIL" ]; then
echo ""
echo "Last lines of the failed command:"
printf '%s\n' "$FAILURE_TAIL"
fi
echo "----------------------------------------"
echo "Common next checks:"
if [ -n "$BUILD_COMMAND" ]; then
Expand Down Expand Up @@ -148,13 +196,15 @@ render_one() {
local input=$1 output=$2
FAIL_WHAT=""
FAIL_DETAIL=""
FAILURE_CAUSE=""
FAILURE_TAIL=""
export EMAIL_TEMPLATE_INPUT=$input
export EMAIL_TEMPLATE_OUTPUT=$output
if [ -n "$BUILD_COMMAND" ]; then
echo " build-command: $BUILD_COMMAND"
if ! bash -c "$BUILD_COMMAND"; then
if ! run_captured bash -c "$BUILD_COMMAND"; then
FAIL_WHAT="render failed for $input"
FAIL_DETAIL="The build-command exited non-zero while rendering:\n input: $input\n output: $output\n command: $BUILD_COMMAND\n\nThe command's own output is above. Fix the command or its dependencies, then re-run."
FAIL_DETAIL="The build-command exited non-zero while rendering:\n input: $input\n output: $output\n command: $BUILD_COMMAND\n\nThe cause is below. Fix the command or its dependencies, then re-run."
return 1
fi
else
Expand All @@ -165,15 +215,15 @@ render_one() {
ARGS+=("${EXTRA[@]}")
fi
echo " script: $PM run $BUILD_SCRIPT ${ARGS[*]}"
if ! run_script "$BUILD_SCRIPT" "${ARGS[@]}"; then
if ! run_captured run_script "$BUILD_SCRIPT" "${ARGS[@]}"; then
FAIL_WHAT="render failed for $input"
FAIL_DETAIL="$PM run $BUILD_SCRIPT exited non-zero while rendering:\n input: $input\n output: $output\n\nThe script's own output is above (for example a tsc error, a missing template, or a non-zero exit from the node process). Fix the consumer script or its dependencies, then re-run."
FAIL_DETAIL="$PM run $BUILD_SCRIPT exited non-zero while rendering:\n input: $input\n output: $output\n\nThe cause is below (for example a tsc error, a missing template, or a non-zero exit from the node process). Fix the consumer script or its dependencies, then re-run."
return 1
fi
fi
if ! validate_one "$output"; then
if ! run_captured validate_one "$output"; then
FAIL_WHAT="validation failed for $output"
FAIL_DETAIL="Validation exited non-zero on:\n $output\n\nCheck the validation output above."
FAIL_DETAIL="Validation exited non-zero on:\n $output\n\nThe cause is below."
return 1
fi
if [ ! -s "$output" ]; then
Expand All @@ -196,8 +246,8 @@ if [ -n "$COMMAND" ]; then
exit 1
fi
echo "running command: $COMMAND"
if ! bash -c "$COMMAND"; then
explain_failure "command mode failed" "The command exited non-zero:\n $COMMAND\n\nNo HTML was indexed. Check the command output above."
if ! run_captured bash -c "$COMMAND"; then
explain_failure "command mode failed" "The command exited non-zero:\n $COMMAND\n\nNo HTML was indexed. The cause is below."
exit 1
fi
outdir=$ARTIFACT_DIRECTORY
Expand Down
13 changes: 13 additions & 0 deletions tests/render.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,19 @@ render SINGLE_INPUT=src/newsletter.md SINGLE_OUTPUT=generated/n.html BUILD_COMMA
assert_contains "build-command failure names the command" "$LOG" "The build-command exited non-zero"
assert_contains "build-command failure gives command-specific advice" "$LOG" "Does the build-command run on its own"

new_project
cat > "$PROJ/fail-tsc.js" << 'EOF'
console.error("scripts/samples/hackernoon/hackernoon-data.ts(1,42): error TS2307: Cannot find module '../../../src/types.js'")
process.exit(1)
EOF
# Use a relative path inside the project. An absolute $PROJ path is a Git Bash
# path that Node on Windows cannot open (ENOENT on D:\tmp\...\package.json).
(cd "$PROJ" && node -e 'const fs=require("fs"); const p=JSON.parse(fs.readFileSync("package.json","utf8")); p.scripts["fail-tsc"]="node fail-tsc.js"; fs.writeFileSync("package.json", JSON.stringify(p,null,2));')
render SINGLE_INPUT=src/newsletter.md SINGLE_OUTPUT=generated/n.html BUILD_SCRIPT=fail-tsc
assert_eq "tsc-style failure fails the render" 1 "$RC"
assert_contains "annotation includes the tsc cause" "$LOG" "::error::render failed for src/newsletter.md: scripts/samples/hackernoon/hackernoon-data.ts(1,42): error TS2307: Cannot find module"
assert_contains "failure block repeats the cause" "$LOG" "Cause:"

new_project
render SINGLE_INPUT=src/newsletter.md SINGLE_OUTPUT=generated/n.html BUILD_COMMAND='true' VALIDATE_SCRIPT=
assert_eq "a build that writes nothing fails" 1 "$RC"
Expand Down
Loading