diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml
index 984db3d..01d3f3d 100644
--- a/.github/workflows/e2e.yml
+++ b/.github/workflows/e2e.yml
@@ -686,7 +686,14 @@ jobs:
- name: "[Report] Job summary"
if: ${{ always() }}
+ env:
+ GH_TOKEN: ${{ github.token }}
run: |
+ mkdir -p report
+ if [ ! -f out/readback-first.md ]; then
+ FAILED_STEP=$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID/attempts/$GITHUB_RUN_ATTEMPT/jobs" \
+ --jq ".jobs[] | select(.runner_name == \"$RUNNER_NAME\") | .steps[] | select(.conclusion == \"failure\") | .name" | head -1) || true
+ fi
{
echo "### $RUNTIME $ARCH \`$NAME\`"
echo
@@ -694,15 +701,34 @@ jobs:
[ ! -f invocations.jsonl ] || jq -rs 'to_entries[] | "- invocation \(.key + 1) at \(.value.invoked_at / 1000 | strftime("%H:%M:%S")), took \((.value.returned_at - .value.invoked_at) / 1000) s\(if .value.init_ms then ", a cold start with \(.value.init_ms) ms of init" else "" end)"' invocations.jsonl
echo
for readback in out/readback-first.md out/readback-final.md; do [ ! -f "$readback" ] || { cat "$readback"; echo; }; done
- [ -f out/readback-first.md ] || echo "No read-back, the job failed before it."
- } >> "$GITHUB_STEP_SUMMARY"
+ [ -f out/readback-first.md ] || echo "❌ Stopped${FAILED_STEP:+ at **$FAILED_STEP**}, no read-back to show."
+ } > report/summary.md
+ cat report/summary.md >> "$GITHUB_STEP_SUMMARY"
+ # The Linear issue gets the cleanup only when it left something behind.
+ grep -q '❌' out/cleanup.md 2>/dev/null && { echo; cat out/cleanup.md; } >> report/summary.md
[ -s out/rows.json ] && [ "$(jq length out/rows.json)" -gt 0 ] && { echo; echo 'stored rows
'; echo; echo '```json'; cat out/rows.json; echo '```'; echo ' '; } >> "$GITHUB_STEP_SUMMARY"
[ -s out/function.log ] && { echo; echo "/aws/lambda/$NAME
"; echo; echo '```'; cat out/function.log; echo '```'; echo ' '; } >> "$GITHUB_STEP_SUMMARY"
[ -f out/cleanup.md ] && { echo; cat out/cleanup.md; } >> "$GITHUB_STEP_SUMMARY"
true
+ - name: "[Report] Hand the summary to the Linear issue"
+ if: ${{ always() }}
+ uses: actions/upload-artifact@v7
+ with:
+ name: e2e-summary-${{ matrix.runtime }} ${{ matrix.arch }}
+ path: report/summary.md
+ retention-days: 7
+ overwrite: true
+
notify-linear:
if: ${{ failure() && (github.event_name == 'schedule' || (github.event_name == 'push' && github.ref == 'refs/heads/main')) }}
needs: [docs, layer, lambda]
+ permissions:
+ contents: read
+ actions: read
+ checks: read
uses: ./.github/workflows/notify_linear_failure.yml
+ with:
+ test_name: AWS Lambda extension
+ job_summaries: true
secrets: inherit
diff --git a/.github/workflows/notify_linear_failure.yml b/.github/workflows/notify_linear_failure.yml
index 529be28..72f9f2d 100644
--- a/.github/workflows/notify_linear_failure.yml
+++ b/.github/workflows/notify_linear_failure.yml
@@ -1,14 +1,28 @@
name: "Notify Linear on failure"
+# A caller that sets neither input gets the plain issue: "E2E failure: " listing the failed jobs and steps.
on:
workflow_call:
+ inputs:
+ test_name:
+ description: "Names the test in the issue title instead of the workflow name"
+ type: string
+ default: ""
+ job_summaries:
+ description: >-
+ Adds each failed job's errors and summary to the issue. A job hands its summary over as an artifact named
+ "e2e-summary-" holding summary.md.
+ type: boolean
+ default: false
jobs:
create-linear-issue:
runs-on: ubuntu-latest
+ # A caller must grant checks: read as well (the errors come from the annotations), or this job cannot start.
permissions:
contents: read
actions: read
+ checks: read
steps:
- name: "Gather failure context"
id: ctx
@@ -16,19 +30,42 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RUN_ID: ${{ github.event.workflow_run.id || github.run_id }}
RUN_ATTEMPT: ${{ github.event.workflow_run.run_attempt || github.run_attempt }}
+ JOB_SUMMARIES: ${{ inputs.job_summaries }}
run: |
- FAILED=$(gh api \
+ JOBS=$(gh api \
"repos/${{ github.repository }}/actions/runs/$RUN_ID/attempts/$RUN_ATTEMPT/jobs" \
--paginate \
- --jq '[.jobs[]
+ --jq '.jobs[]
| select(.conclusion == "failure")
- | "- **\(.name)** — failed at _\([.steps[] | select(.conclusion == "failure") | .name] | join(", "))_"]
- | join("\n")')
-
+ | {id, name, steps: ([.steps[] | select(.conclusion == "failure") | .name] | join(", "))}
+ | @json')
+
+ FAILED=""
+ while read -r job; do
+ [ -n "$job" ] || continue
+ NAME=$(jq -r .name <<<"$job")
+ STEPS=$(jq -r .steps <<<"$job")
+ if [ "$JOB_SUMMARIES" != true ]; then
+ FAILED="$FAILED- **$NAME** — failed at _${STEPS}_"$'\n'
+ continue
+ fi
+ # The job's ::error:: lines, without the runner's closing "Process completed with exit code 1."
+ ERRORS=$(gh api "repos/${{ github.repository }}/check-runs/$(jq -r .id <<<"$job")/annotations" \
+ --jq '.[] | select(.annotation_level == "failure" and (.message | startswith("Process completed with exit code") | not)) | "- \(.message | gsub("\n"; " "))"') || ERRORS=""
+ FAILED="$FAILED### $NAME"$'\n\n'"Failed at **${STEPS}**"$'\n\n'"${ERRORS:+$ERRORS$'\n\n'}"
+ # A job that failed before writing its summary has no artifact and gets the errors alone.
+ SUMMARY_DIR="summaries/$(jq -r .id <<<"$job")"
+ gh run download "$RUN_ID" -R "${{ github.repository }}" -n "e2e-summary-$NAME" -D "$SUMMARY_DIR" >/dev/null 2>&1 || true
+ [ ! -f "$SUMMARY_DIR/summary.md" ] || FAILED="$FAILED$(cat "$SUMMARY_DIR/summary.md")"$'\n\n'
+ done <<<"$JOBS"
+
+ while [[ "$FAILED" == *$'\n' ]]; do FAILED="${FAILED%$'\n'}"; done
if [ -z "$FAILED" ]; then
FAILED="_(no failed jobs reported by the API yet)_"
+ elif [ "${#FAILED}" -gt 20000 ]; then
+ FAILED="${FAILED:0:20000}"$'\n\n'"_(cut at 20,000 characters, the rest is in the run)_"
fi
-
+
{
echo "failed_jobs<