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<