diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index adcc23c7..d8faf024 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -19,6 +19,8 @@ # Interlisp/Interlisp.staging (skips on fork PRs or when # the STAGING_APP_ID variable is unset) # deploy - deploy the built site to GitHub Pages +# notify - email ci_failures@interlisp.org on any failure or +# build warnings (notify-only; warnings still deploy) # # The workflow is executed either on a push or via scheduled run times. When # started at a scheduled run time we only do a deploy if the cached bibliography @@ -159,11 +161,12 @@ jobs: repo: Interlisp/Interlisp.staging ref: main token: ${{ steps.app-token.outputs.token }} - inputs: '{ - "pr_number": "${{ github.event.pull_request.number }}", - "pr_sha": "${{ github.event.pull_request.head.sha }}", - "hugo_version": "${{ env.HUGO_VERSION }}" - }' + inputs: >- + { + "pr_number": "${{ github.event.pull_request.number }}", + "pr_sha": "${{ github.event.pull_request.head.sha }}", + "hugo_version": "${{ env.HUGO_VERSION }}" + } # ---------------------------------------------------------------------------- # Deploy the built site to GitHub Pages. @@ -182,3 +185,111 @@ jobs: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v5 + + # ---------------------------------------------------------------------------- + # Notify ci_failures@interlisp.org when the workflow fails or produces + # warnings. GitHub does not notify on workflow failure by default, so + # without this job a broken nightly build can go undetected for days. + # + # Triggers on: + # - any job failure (validate-docs, hugo-version, build, preview, deploy) + # - warnings surfaced by the shared build workflow + # (needs.build.outputs.has-warnings == 'true'; see + # Interlisp/shared-workflows build-site.yml proposal) + # + # Warnings are notify-only: the build still passes and deploys. + # + # Required secrets (set in repo or org Settings > Secrets > Actions): + # MAIL_SERVER e.g. smtp.gmail.com + # MAIL_USERNAME SMTP login (also used as default From address) + # MAIL_PASSWORD SMTP password (Gmail: use an App password) + # Optional vars (Settings > Variables > Actions): + # MAIL_PORT default 587 (STARTTLS) + # MAIL_FROM default is MAIL_USERNAME + # e.g. "Interlisp CI " + # MAIL_TO default ci_failures@interlisp.org + # + notify: + if: always() && (needs.validate-docs.result == 'failure' || needs.hugo-version.result == 'failure' || needs.build.result == 'failure' || needs.preview.result == 'failure' || needs.deploy.result == 'failure' || needs.build.outputs.has-warnings == 'true') + runs-on: ubuntu-latest + needs: [validate-docs, hugo-version, build, preview, deploy] + steps: + - name: Check mail secrets configured + id: mail-check + env: + MAIL_SERVER: ${{ secrets.MAIL_SERVER }} + MAIL_USERNAME: ${{ secrets.MAIL_USERNAME }} + MAIL_PASSWORD: ${{ secrets.MAIL_PASSWORD }} + run: | + if [[ -z "$MAIL_SERVER" || -z "$MAIL_USERNAME" || -z "$MAIL_PASSWORD" ]]; then + echo "::warning::Mail secrets (MAIL_SERVER/MAIL_USERNAME/MAIL_PASSWORD) not configured; skipping email to ci_failures@interlisp.org" + echo "configured=false" >> "$GITHUB_OUTPUT" + else + echo "configured=true" >> "$GITHUB_OUTPUT" + fi + + - name: Send failure email + if: steps.mail-check.outputs.configured == 'true' && (needs.validate-docs.result == 'failure' || needs.hugo-version.result == 'failure' || needs.build.result == 'failure' || needs.preview.result == 'failure' || needs.deploy.result == 'failure') + uses: dawidd6/action-send-mail@v22 + with: + server_address: ${{ secrets.MAIL_SERVER }} + server_port: ${{ vars.MAIL_PORT || '587' }} + secure: false + require_tls: true + username: ${{ secrets.MAIL_USERNAME }} + password: ${{ secrets.MAIL_PASSWORD }} + subject: "[${{ github.repository }} CI] FAILED: ${{ github.workflow }} run ${{ github.run_number }} (${{ github.event_name }}, ${{ github.ref_name }})" + to: ${{ vars.MAIL_TO || 'ci_failures@interlisp.org' }} + from: ${{ vars.MAIL_FROM || secrets.MAIL_USERNAME }} + body: | + CI workflow FAILED for ${{ github.repository }}. + + Workflow: ${{ github.workflow }} + Event: ${{ github.event_name }} + Ref: ${{ github.ref }} (${{ github.ref_name }}) + SHA: ${{ github.sha }} + Actor: ${{ github.actor }} + Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} + + Job results: + validate-docs: ${{ needs.validate-docs.result }} + hugo-version: ${{ needs.hugo-version.result }} + build: ${{ needs.build.result }} (skipped=${{ needs.build.outputs.skipped }}, has-warnings=${{ needs.build.outputs.has-warnings }}) + preview: ${{ needs.preview.result }} + deploy: ${{ needs.deploy.result }} + + Please review the failed run logs at the URL above. + + - name: Send warnings email + if: steps.mail-check.outputs.configured == 'true' && needs.validate-docs.result != 'failure' && needs.hugo-version.result != 'failure' && needs.build.result != 'failure' && needs.preview.result != 'failure' && needs.deploy.result != 'failure' && needs.build.outputs.has-warnings == 'true' + uses: dawidd6/action-send-mail@v3 + with: + server_address: ${{ secrets.MAIL_SERVER }} + server_port: ${{ vars.MAIL_PORT || '587' }} + secure: false + require_tls: true + username: ${{ secrets.MAIL_USERNAME }} + password: ${{ secrets.MAIL_PASSWORD }} + subject: "[${{ github.repository }} CI] WARNINGS: ${{ github.workflow }} run ${{ github.run_number }} (${{ github.event_name }}, ${{ github.ref_name }})" + to: ${{ vars.MAIL_TO || 'ci_failures@interlisp.org' }} + from: ${{ vars.MAIL_FROM || secrets.MAIL_USERNAME }} + body: | + CI workflow completed with WARNINGS for ${{ github.repository }} (build passed, deploy continued). + + Workflow: ${{ github.workflow }} + Event: ${{ github.event_name }} + Ref: ${{ github.ref }} (${{ github.ref_name }}) + SHA: ${{ github.sha }} + Actor: ${{ github.actor }} + Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} + + Warnings summary: ${{ needs.build.outputs.warnings-summary }} + + Job results: + validate-docs: ${{ needs.validate-docs.result }} + hugo-version: ${{ needs.hugo-version.result }} + build: ${{ needs.build.result }} (skipped=${{ needs.build.outputs.skipped }}, has-warnings=${{ needs.build.outputs.has-warnings }}) + preview: ${{ needs.preview.result }} + deploy: ${{ needs.deploy.result }} + + Please review the warnings in the run logs at the URL above. diff --git a/.github/workflows/preview-cleanup.yml b/.github/workflows/preview-cleanup.yml index fac87f73..6fbc90f6 100644 --- a/.github/workflows/preview-cleanup.yml +++ b/.github/workflows/preview-cleanup.yml @@ -41,6 +41,54 @@ jobs: repo: Interlisp/Interlisp.staging ref: main token: ${{ steps.app-token.outputs.token }} - inputs: '{ - "pr_number": "${{ github.event.pull_request.number }}" - }' + inputs: '{"pr_number":"${{ github.event.pull_request.number }}"}' + + # Notify ci_failures@interlisp.org if preview cleanup fails. Uses the + # same MAIL_* secrets/vars as gh-pages.yml; skips gracefully when they + # are not configured. + notify: + if: always() && needs.remove-preview.result == 'failure' + runs-on: ubuntu-latest + needs: [remove-preview] + steps: + - name: Check mail secrets configured + id: mail-check + env: + MAIL_SERVER: ${{ secrets.MAIL_SERVER }} + MAIL_USERNAME: ${{ secrets.MAIL_USERNAME }} + MAIL_PASSWORD: ${{ secrets.MAIL_PASSWORD }} + run: | + if [[ -z "$MAIL_SERVER" || -z "$MAIL_USERNAME" || -z "$MAIL_PASSWORD" ]]; then + echo "::warning::Mail secrets (MAIL_SERVER/MAIL_USERNAME/MAIL_PASSWORD) not configured; skipping email to ci_failures@interlisp.org" + echo "configured=false" >> "$GITHUB_OUTPUT" + else + echo "configured=true" >> "$GITHUB_OUTPUT" + fi + + - name: Send failure email + if: steps.mail-check.outputs.configured == 'true' + uses: dawidd6/action-send-mail@v22 + with: + server_address: ${{ secrets.MAIL_SERVER }} + server_port: ${{ vars.MAIL_PORT || '587' }} + secure: false + require_tls: true + username: ${{ secrets.MAIL_USERNAME }} + password: ${{ secrets.MAIL_PASSWORD }} + subject: "[${{ github.repository }} CI] FAILED: ${{ github.workflow }} run ${{ github.run_number }} (PR #${{ github.event.pull_request.number }})" + to: ${{ vars.MAIL_TO || 'ci_failures@interlisp.org' }} + from: ${{ vars.MAIL_FROM || secrets.MAIL_USERNAME }} + body: | + Preview cleanup FAILED for ${{ github.repository }}. + + Workflow: ${{ github.workflow }} + PR: #${{ github.event.pull_request.number }} + Ref: ${{ github.ref }} (${{ github.ref_name }}) + SHA: ${{ github.sha }} + Actor: ${{ github.actor }} + Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} + + Job results: + remove-preview: ${{ needs.remove-preview.result }} + + Please review the failed run logs at the URL above. diff --git a/scripts/update_bibliography.sh b/scripts/update_bibliography.sh index 91f24309..ab9dae23 100755 --- a/scripts/update_bibliography.sh +++ b/scripts/update_bibliography.sh @@ -1,6 +1,11 @@ #!/usr/bin/env bash -set -e +# Fail fast and loudly so CI does not pass silently on errors. +# -e: exit on any command failure +# -u: exit on use of an unset variable +# -o pipefail: a failed curl/jq in a pipeline fails the whole pipeline +# (without pipefail, e.g. `curl ... | jq ...` hides curl failures) +set -euo pipefail function usage () { echo -e "\nUpdate Bibliography for interlisp.org from Zotero\n" @@ -63,7 +68,16 @@ function showInfo() { # The $1 is the level for this message ($showInfoLevel must be >= this) # The $2 is the message string if [[ $showInfoLevel -ge $1 ]] ; then - echo "$2" + # Level 2 messages are warnings. When running under GitHub Actions, + # also emit them as ::warning:: annotations so they are visible in + # the workflow UI (and can trigger the ci_failures@interlisp.org + # notification) instead of hiding in the log. Warnings are + # notify-only: they never fail the build. + if [[ "$1" -eq 2 && "${GITHUB_ACTIONS:-}" == "true" ]] ; then + echo "::warning::$2" + else + echo "$2" + fi fi } @@ -103,7 +117,7 @@ function add_items_from_collection () { local limit=100 while :; do showInfo 10 "curl of $collection_key at start $start" - local this_page=$(curl -s "https://api.zotero.org/groups/$GROUP_ID/collections/$collection_key/items?include=data,csljson&start=$start&limit=$limit&v=3") + local this_page=$(curl -fsSL "https://api.zotero.org/groups/$GROUP_ID/collections/$collection_key/items?include=data,csljson&start=$start&limit=$limit&v=3") local item_count=$(jq '. | length' <<< "$this_page") showInfo 10 "curl returned $item_count items" if [[ $item_count > 0 ]] ; then @@ -124,7 +138,7 @@ function add_items_from_collection () { local numCollections=$(jq -r '.numCollections' <<< "$collection_item") if [[ $numCollections -gt 0 ]] ; then showInfo 5 "Getting subcollections of $collection_key: $collection_name" - local subcollections=$(curl -s "https://api.zotero.org/groups/$GROUP_ID/collections/$collection_key/collections" | jq -r 'map(.data+.meta)') + local subcollections=$(curl -fsSL "https://api.zotero.org/groups/$GROUP_ID/collections/$collection_key/collections" | jq -r 'map(.data+.meta)') collections=$(jq -s 'add' <<< "$collections$subcollections") # Recurse into subcollections @@ -215,7 +229,7 @@ fi root_collection="FSK5IX4F" if [[ $# -eq 0 ]] ; then # Initialize with the root collection. - collections=$(curl -s "https://api.zotero.org/groups/$GROUP_ID/collections/$root_collection" | jq -s -r 'map(.data+.meta)') + collections=$(curl -fsSL "https://api.zotero.org/groups/$GROUP_ID/collections/$root_collection" | jq -s -r 'map(.data+.meta)') add_items_from_collection $root_collection if $collectionsFiles ; then echo "$collections" > collections.json @@ -224,7 +238,7 @@ if [[ $# -eq 0 ]] ; then while read neededUrl; do showInfo 10 "curl of $neededUrl" - item=$(curl -s "$neededUrl?include=data,csljson&v=3") + item=$(curl -fsSL "$neededUrl?include=data,csljson&v=3") showInfo 10 "curl returned" items=$(jq -s 'add' <<< "[$item]$items") showInfo 10 "items updated" @@ -246,7 +260,7 @@ fi showInfo 1 "Got $(jq '. | length' <<< "$items") items" # Piece-wise processing for debugging: -items=$(jq 'include "./bib-fns";walk(if type == "object" then removeEmptyKeys end)' <<< $items) +items=$(jq 'include "./bib-fns";walk(if type == "object" then removeEmptyKeys end)' <<< "$items") showInfo 8 "Remove all empty fields and the keys." if $debugFiles ; then dfn=$(debugFileName "removeEmptyKeys" $dfn) @@ -426,7 +440,48 @@ export BIBITEMS_DIR # Ensure target directories exist mkdir -p "$BIBLIOGRAPHY_DIR" "$BIBITEMS_DIR" +# Run bibSplit.pl, capturing its stderr. bibSplit.pl emits +# `Warning: unhandled type ...` lines for Zotero types it does not know how +# to render. Those warnings previously sat silently in bibSplit.err while +# CI stayed green. Now: +# - a non-zero bibSplit.pl exit is a hard ERROR (fails the build, which +# triggers the ci_failures@interlisp.org failure email), and +# - a non-empty bibSplit.err is surfaced as ::warning:: annotations (and +# in the job summary) but does NOT fail the build (notify-only). +: > bibSplit.err +set +e ./bibSplit.pl bibliography-items-by-line.json 2>bibSplit.err +bibSplitStatus=$? +set -e +if [[ $bibSplitStatus -ne 0 ]]; then + echo "::error::bibSplit.pl failed with exit code $bibSplitStatus" >&2 + cat bibSplit.err >&2 || true + exit $bibSplitStatus +fi +if [[ -s bibSplit.err ]]; then + bibWarnCount=$(wc -l < bibSplit.err | tr -d ' ') + bibWarnSummary=$(sort bibSplit.err | uniq -c | sort -rn | head -n 10) + if [[ "${GITHUB_ACTIONS:-}" == "true" ]]; then + echo "::warning::$bibWarnCount bibSplit.pl warnings (see bibSplit.err; first occurrence per message):" + # Emit one annotation per distinct warning to avoid flooding the log + # with hundreds of duplicate lines, then dump the full file. + echo "$bibWarnSummary" | while IFS= read -r line; do + echo "::warning::bibSplit: $line" + done + if [[ -n "${GITHUB_STEP_SUMMARY:-}" ]]; then + { + echo "### Bibliography warnings" + echo "" + echo "$bibWarnCount warnings from bibSplit.pl:" + echo '```' + echo "$bibWarnSummary" + echo '```' + } >> "$GITHUB_STEP_SUMMARY" + fi + else + echo "Warn: $bibWarnCount bibSplit.pl warnings (see bibSplit.err)" >&2 + fi +fi # Cleanup (uncomment once working) # rm bibliography-items-by-line.json