From a1a30368170bf14b919731c3e697c9bb09e1e0b8 Mon Sep 17 00:00:00 2001 From: Petr Heinz Date: Thu, 22 Oct 2026 09:33:30 +0200 Subject: [PATCH 1/2] T-22072 [CI] Name the test in the E2E Linear issue and include the failure summary The issue title names the test (E2E failure: AWS Lambda extension) and its body carries each failed job's errors and summary: the invocations and the read-back tables. A job that stops before its read-back names the step it stopped at. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/e2e.yml | 30 ++++++++- .github/workflows/notify_linear_failure.yml | 72 ++++++++++++++++----- 2 files changed, 85 insertions(+), 17 deletions(-) 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..49813ab 100644 --- a/.github/workflows/notify_linear_failure.yml +++ b/.github/workflows/notify_linear_failure.yml @@ -1,7 +1,19 @@ 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. The calling job needs checks: read for the errors. + type: boolean + default: false jobs: create-linear-issue: @@ -9,26 +21,56 @@ jobs: permissions: contents: read actions: read + checks: read steps: + - name: "Download the jobs' summaries" + if: ${{ inputs.job_summaries }} + continue-on-error: true + uses: actions/download-artifact@v8 + with: + pattern: e2e-summary-* + path: summaries + run-id: ${{ github.event.workflow_run.id || github.run_id }} + github-token: ${{ secrets.GITHUB_TOKEN }} - name: "Gather failure context" id: ctx env: 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"; " "))"' || true) + FAILED="$FAILED### $NAME"$'\n\n'"Failed at **${STEPS}**"$'\n\n'"${ERRORS:+$ERRORS$'\n\n'}" + SUMMARY="summaries/e2e-summary-$NAME/summary.md" + [ ! -f "$SUMMARY" ] || FAILED="$FAILED$(cat "$SUMMARY")"$'\n\n' + done <<<"$JOBS" + 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< Date: Thu, 22 Oct 2026 09:42:52 +0200 Subject: [PATCH 2/2] Fetch each failed job's summary by its artifact name download-artifact unpacks a lone matching artifact straight into its path, so in a run with one job the summary was never found. Also keep an annotations API error out of the issue, and keep the plain issue text byte-identical to before. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/notify_linear_failure.yml | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/.github/workflows/notify_linear_failure.yml b/.github/workflows/notify_linear_failure.yml index 49813ab..72f9f2d 100644 --- a/.github/workflows/notify_linear_failure.yml +++ b/.github/workflows/notify_linear_failure.yml @@ -11,27 +11,19 @@ on: 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. The calling job needs checks: read for the errors. + "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: "Download the jobs' summaries" - if: ${{ inputs.job_summaries }} - continue-on-error: true - uses: actions/download-artifact@v8 - with: - pattern: e2e-summary-* - path: summaries - run-id: ${{ github.event.workflow_run.id || github.run_id }} - github-token: ${{ secrets.GITHUB_TOKEN }} - name: "Gather failure context" id: ctx env: @@ -59,12 +51,15 @@ jobs: 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"; " "))"' || true) + --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'}" - SUMMARY="summaries/e2e-summary-$NAME/summary.md" - [ ! -f "$SUMMARY" ] || FAILED="$FAILED$(cat "$SUMMARY")"$'\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