diff --git a/.github/workflows/notify-main-failure.yml b/.github/workflows/notify-main-failure.yml index cabe027..f3e0321 100644 --- a/.github/workflows/notify-main-failure.yml +++ b/.github/workflows/notify-main-failure.yml @@ -114,9 +114,36 @@ jobs: WORKFLOW_REF: ${{ github.workflow_ref }} BRANCH: ${{ github.ref_name }} RUN_ID: ${{ github.run_id }} + RUN_ATTEMPT: ${{ github.run_attempt }} run: | set -uo pipefail + # A RE-RUN is the most common way a failure gets fixed, and it was the + # one case this could never report. Attempts share a run id, so the + # failing attempt is the very run the history lookup below excludes as + # "the current one" — it would then find some older, green run and + # conclude nothing had broken. Check the previous attempt first. + if [ "${RUN_ATTEMPT:-1}" -gt 1 ]; then + PREV_ATTEMPT=$((RUN_ATTEMPT - 1)) + if LAST=$(gh api "repos/${REPO}/actions/runs/${RUN_ID}/attempts/${PREV_ATTEMPT}" \ + --jq '.conclusion // ""' 2>&1); then + case "${LAST}" in + failure|timed_out|startup_failure) + echo "should_post=true" >> "$GITHUB_OUTPUT" + echo "prev_conclusion=${LAST}" >> "$GITHUB_OUTPUT" + echo "Attempt ${PREV_ATTEMPT} concluded '${LAST}', this attempt recovered it." + exit 0 + ;; + *) + echo "should_post=false" >> "$GITHUB_OUTPUT" + echo "Attempt ${PREV_ATTEMPT} concluded '${LAST:-unknown}', not a recovery." + exit 0 + ;; + esac + fi + echo "Could not read attempt ${PREV_ATTEMPT}, falling back to run history: ${LAST}" + fi + # Match the workflow FILE rather than its display name, which a # `run-name:` override changes; fall back to the name if the ref does # not resolve to a path inside this repo. diff --git a/.github/workflows/workflow-lint.yml b/.github/workflows/workflow-lint.yml index 0dad854..dbc11f1 100644 --- a/.github/workflows/workflow-lint.yml +++ b/.github/workflows/workflow-lint.yml @@ -151,13 +151,20 @@ jobs: GH_TOKEN: ${{ github.token }} run: | set -uo pipefail + # The shared policy travels with the scripts, so every repo audits under + # the same rules and a decision recorded once is not re-litigated per + # repo. A repo with its own .github/zizmor.yml still wins, since zizmor + # prefers a repo-local config over --config. + CFG=(--config .ci-shared/zizmor.yml) + [ -f .github/zizmor.yml ] && CFG=() + if [ "$BLOCKING" = "true" ]; then - uvx "zizmor@${VERSION}" --persona="$PERSONA" .github/workflows + uvx "zizmor@${VERSION}" "${CFG[@]}" --persona="$PERSONA" .github/workflows else # Advisory: report and move on. The findings land in the log and the # step summary either way, so turning this into a gate later is a # one-line input change rather than a discovery exercise. - uvx "zizmor@${VERSION}" --persona="$PERSONA" .github/workflows | tee zizmor.out || true + uvx "zizmor@${VERSION}" "${CFG[@]}" --persona="$PERSONA" .github/workflows | tee zizmor.out || true { echo "### zizmor (advisory)" echo diff --git a/zizmor.yml b/zizmor.yml new file mode 100644 index 0000000..2b5aeed --- /dev/null +++ b/zizmor.yml @@ -0,0 +1,31 @@ +# Shared zizmor policy for every repo that calls workflow-lint.yml. +# +# One file rather than seven, for the same reason the pipelines were unified: a +# per-repo copy drifts, and the drift is invisible until the signal is wrong. +# +# ONE audit is turned down here, and only partly, because it flags a DECISION +# rather than a defect. Everything else stays on: if a finding survives this file +# it is worth reading, which is the only way the report keeps meaning anything. +# +# Deliberately NOT suppressed, though it is the largest remaining group: +# `secrets-inherit`. `secrets: inherit` is our standard everywhere, so it will +# report on nearly every caller — but narrowing those to explicit secret lists is +# a legitimate hardening step nobody has decided against yet, and silencing it +# here would quietly make that decision. Suppress it when someone chooses to. + +rules: + # In-org references float on `@main` deliberately (decision 11.BH). 101 + # composite-action call sites already do, and keeping 150+ SHA pins bumped as + # the reusables evolve costs more than it buys — so the control moved to the + # source instead, where it is one gate rather than 150: `main` in + # mindsdb/github-actions requires a code-owner review, dismisses stale + # approvals, and admits no bypass actors. + # + # THIRD-PARTY actions get no such exemption. A tag there is mutable and an + # upstream account compromise reaches our pipelines with the caller's secrets in + # scope, which is a real finding and tracked as ENG-1053 hardening item 1. + unpinned-uses: + config: + policies: + mindsdb/*: ref-pin + "*": hash-pin