diff --git a/README.md b/README.md index 8cb76d8..13b54cc 100644 --- a/README.md +++ b/README.md @@ -315,9 +315,10 @@ card above. | `color` | no | `''` | Alert-card bar color (hex, e.g. `#F9A825`). Only takes effect in alert mode. Overrides the result-derived green/red. | | `status` | no | `''` | When set, the alert card's first field renders as **Status** with this text in place of the **Version** field. Only takes effect in alert mode. | | `channel` | no | `slack` | Chat channel; only `slack` is implemented today. | -| `github-token` | no | `${{ github.token }}` | Token used to read the PR/commit, the workflow run, and (on failure) the run's jobs. The default job token covers a same-repo deploy; for a cross-org deploy pass a token minted in the caller from a GitHub App with read access to `target-repo`. | +| `github-token` | no | `${{ github.token }}` | Token used to read the PR/commit that `target-repo`/`target-sha` describes. The default job token covers a same-repo deploy; for a cross-org deploy pass a token minted in the caller from a GitHub App with read access to `target-repo`. | | `target-repo` | no | `${{ github.repository }}` | `owner/repo` whose PR/commit the card describes. Override for a cross-repo deploy. | | `target-sha` | no | `${{ github.sha }}` | Exact built/deployed commit SHA used to resolve the PR/commit. | +| `run-token` | no | falls back to `github-token` | Token used ONLY for the `/actions/runs/{id}` + `/jobs` lookups that populate `DURATION` and `FAILED AT`. Runs always live in the caller's repo (`github.repository`), so a cross-org `github-token` scoped to `target-repo` can't read them. Pass `${{ github.token }}` when overriding `github-token` cross-org; otherwise leave unset. | Example — same-repo deploy (token/target default to this repo): @@ -360,6 +361,9 @@ Example — cross-org deploy (card describes a PR/commit in another repo): github-token: ${{ steps.app-token.outputs.token }} target-repo: other-org/other-repo target-sha: ${{ needs.build.outputs.app_sha }} + # github-token is scoped to other-org/other-repo and can't read this + # repo's Actions API — pass the job's own token for DURATION + FAILED AT. + run-token: ${{ github.token }} ``` ## enforce — inputs & outputs diff --git a/actions/notify/action.yml b/actions/notify/action.yml index 3e665e8..5cd0589 100644 --- a/actions/notify/action.yml +++ b/actions/notify/action.yml @@ -80,6 +80,17 @@ inputs: commit shown on the card. Defaults to this workflow's commit. required: false default: ${{ github.sha }} + run-token: + description: >- + Token used ONLY for the /actions/runs/{id} + /jobs API calls that + populate DURATION and FAILED AT. Defaults to `github-token` (fine when + that token has `actions: read` on the current repo). Override when + `github-token` is a cross-org token minted for `target-repo` and thus + has no access to the current repo's Actions API: pass the job's own + `github.token` so the run metadata is read with it. Requires + `actions: read` on the notify job. + required: false + default: "" runs: using: composite @@ -98,9 +109,11 @@ runs: WEBHOOK: ${{ inputs.webhook-url }} TARGET_REPO: ${{ inputs.target-repo }} TARGET_SHA: ${{ inputs.target-sha }} + RUN_REPO: ${{ github.repository }} RUN_ID: ${{ github.run_id }} RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_TOKEN: ${{ inputs.github-token }} + RUN_TOKEN: ${{ inputs.run-token }} run: | set -uo pipefail @@ -109,6 +122,11 @@ runs: exit 0 fi + # Runs live in github.repository (RUN_REPO), even when target-repo is + # a cross-org repo for PR attribution. Fall the run-token back to the + # PR-attribution token so single-repo callers keep working unchanged. + RUN_TOKEN="${RUN_TOKEN:-$GH_TOKEN}" + # Status icon only. Header emoji conveys success/failure — no separate # subtitle line under it. icon=":x:"; is_success=0 @@ -200,10 +218,10 @@ runs: # -- Duration: pull run_started_at, compute elapsed vs now. # Falls back silently if the API call fails. duration="" - run_json="$(gh api "repos/${TARGET_REPO}/actions/runs/${RUN_ID}" 2>/dev/null || echo '{}')" + run_json="$(GH_TOKEN="$RUN_TOKEN" gh api "repos/${RUN_REPO}/actions/runs/${RUN_ID}" 2>/dev/null || echo '{}')" started_at="$(jq -r '.run_started_at // .created_at // empty' <<<"$run_json")" if [ -z "$started_at" ]; then - echo "::warning::notify: /actions/runs/${RUN_ID} returned no run_started_at. Check that the notify job grants 'actions: read' — DURATION will not render." + echo "::warning::notify: /actions/runs/${RUN_ID} in ${RUN_REPO} returned no run_started_at. Check that the notify job grants 'actions: read' and (for cross-org callers) run-token has access — DURATION will not render." fi if [ -n "$started_at" ]; then started_epoch="$(date -u -d "$started_at" +%s 2>/dev/null || echo '')" @@ -227,9 +245,9 @@ runs: # always human-meaningful and match what users see in the Actions UI. failed_step="" if [ "$is_success" -eq 0 ]; then - jobs_json="$(gh api "repos/${TARGET_REPO}/actions/runs/${RUN_ID}/jobs" 2>/dev/null || echo '{}')" + jobs_json="$(GH_TOKEN="$RUN_TOKEN" gh api "repos/${RUN_REPO}/actions/runs/${RUN_ID}/jobs" 2>/dev/null || echo '{}')" if [ "$(jq -r '(.jobs // []) | length' <<<"$jobs_json")" = "0" ]; then - echo "::warning::notify: /actions/runs/${RUN_ID}/jobs returned no jobs. Check that the notify job grants 'actions: read' — FAILED AT will not render." + echo "::warning::notify: /actions/runs/${RUN_ID}/jobs in ${RUN_REPO} returned no jobs. Check that the notify job grants 'actions: read' and (for cross-org callers) run-token has access — FAILED AT will not render." fi failed_step="$(jq -r '[.jobs[]? | select(.conclusion == "failure")] | .[0].name // ""' <<<"$jobs_json")" fi