-
Notifications
You must be signed in to change notification settings - Fork 384
ci: add daily checkpoint-availability audit job #1881
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
gtrrz-victor
wants to merge
5
commits into
main
Choose a base branch
from
feat/checkpoint-audit-job
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
a00238f
ci: add daily checkpoint-availability audit job
gtrrz-victor f7df391
ci(checkpoint-audit): drop App token — checkpoint remote is public
gtrrz-victor e33365f
ci(checkpoint-audit): address review feedback
gtrrz-victor 4bb3ab9
ci(checkpoint-audit): address second Copilot review
gtrrz-victor 593c99f
Merge remote-tracking branch 'origin/main' into feat/checkpoint-audit…
gtrrz-victor File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| name: Checkpoint Audit | ||
|
|
||
| on: | ||
| schedule: | ||
| - cron: "0 7 * * *" # 07:00 UTC daily (GitHub does not honor sub-daily cron reliably) | ||
| workflow_dispatch: | ||
| inputs: | ||
| window: | ||
| description: 'git --since window to audit' | ||
| default: '24 hours ago' | ||
|
|
||
| permissions: | ||
| contents: read | ||
| actions: read # required for actions/upload-artifact under a restricted token (matches e2e.yml) | ||
|
|
||
| concurrency: | ||
| group: checkpoint-audit-${{ github.ref }} | ||
| cancel-in-progress: true | ||
|
|
||
| jobs: | ||
| audit: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| # The checkpoint remote (entireio/cli-checkpoints) is public, so the audit's | ||
| # read-only `git ls-remote` needs no credentials. If it is ever made private, | ||
| # set ENTIRE_CHECKPOINT_TOKEN on the audit step from a token with read access. | ||
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
| with: | ||
| fetch-depth: 0 | ||
|
|
||
| - name: Fetch all branches | ||
| run: git fetch --all --prune | ||
|
|
||
| # Capture the script's exit code (0 = all present, 1 = missing, 2 = | ||
| # remote/setup error) without failing the step, so the report, Slack, and | ||
| # gate steps below can distinguish "missing" from "could not run". | ||
| - name: Run checkpoint audit | ||
| id: audit | ||
| env: | ||
| AUDIT_WINDOW: ${{ github.event.inputs.window || '24 hours ago' }} | ||
| run: | | ||
| set +e | ||
| scripts/checkpoint-audit.sh | ||
| echo "code=$?" >> "$GITHUB_OUTPUT" | ||
| exit 0 | ||
|
|
||
| - name: Upload audit report | ||
| if: always() | ||
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | ||
| with: | ||
| name: checkpoint-audit-report | ||
| path: | | ||
| checkpoint-audit-report.md | ||
| checkpoint-audit-report.json | ||
| retention-days: 7 | ||
| if-no-files-found: ignore | ||
|
|
||
| # Distinct wording per outcome: exit 1 asserts missing checkpoints (a report | ||
| # artifact exists); exit 2 is a remote/setup failure with no report, so the | ||
| # alert must not claim checkpoints are missing (avoids false positives). | ||
| - name: Compose Slack message | ||
| id: msg | ||
| if: ${{ always() && steps.audit.outputs.code != '0' }} | ||
| run: | | ||
| if [ "${{ steps.audit.outputs.code }}" = "1" ]; then | ||
| echo 'text=:red_circle: *Checkpoint audit: missing checkpoints*\n\nOne or more commits reference a checkpoint that is not on the checkpoint remote. See the report artifact for the full table.' >> "$GITHUB_OUTPUT" | ||
| else | ||
| echo 'text=:red_circle: *Checkpoint audit failed to run*\n\nThe audit could not complete (remote or setup error); no report was produced. See the run log.' >> "$GITHUB_OUTPUT" | ||
| fi | ||
|
|
||
| - name: Notify Slack | ||
| if: ${{ always() && steps.audit.outputs.code != '0' }} | ||
| uses: slackapi/slack-github-action@dcb1066f776dd043e64d0e8ba94ca15cc7e1875d # v4.0.0 | ||
| with: | ||
| webhook: ${{ secrets.E2E_SLACK_WEBHOOK_URL }} | ||
| webhook-type: incoming-webhook | ||
| payload: | | ||
| { | ||
| "attachments": [ | ||
| { | ||
| "color": "#d50200", | ||
| "blocks": [ | ||
| { | ||
| "type": "section", | ||
| "text": { | ||
| "type": "mrkdwn", | ||
| "text": "${{ steps.msg.outputs.text }}\n\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View run details>" | ||
| } | ||
| } | ||
| ] | ||
| } | ||
| ] | ||
| } | ||
|
|
||
| - name: Fail the job on audit error | ||
| if: ${{ steps.audit.outputs.code != '0' }} | ||
| run: | | ||
| echo "::error::checkpoint audit exited ${{ steps.audit.outputs.code }} (1 = missing checkpoints, 2 = remote/setup error)" | ||
| exit 1 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,183 @@ | ||
| #!/usr/bin/env bash | ||
| set -euo pipefail | ||
|
|
||
| # Audit checkpoint availability on the checkpoint remote. | ||
| # | ||
| # Walks every commit created within a time window across all branches, extracts | ||
| # each `Entire-Checkpoint:` trailer, and checks whether the matching checkpoint | ||
| # ref exists on the checkpoint remote (github.com/<CHECKPOINT_REPO>). Any commit | ||
| # whose checkpoint ref is missing from the remote is reported with context and | ||
| # makes the script exit non-zero. | ||
| # | ||
| # This catches the failure mode where a commit reaches origin but its git-refs | ||
| # checkpoint ref (refs/entire/checkpoints/<shard>/<id>) was never pushed to the | ||
| # checkpoint remote — the checkpoint is then unrecoverable on any other machine | ||
| # (e.g. `entire trail resume` / `entire explain` fail with "checkpoint not found"). | ||
| # | ||
| # Trailer parsing and ref-set membership use git's own primitives rather than | ||
| # hand-rolled regex/shard math: `git show --format='%(trailers:...)'` parses | ||
| # trailers (handling squash-merge commits with multiple checkpoint trailers), and | ||
| # the checkpoint ID is simply the ref leaf, so membership is a plain string-set | ||
| # lookup that works for both legacy-hex and ULID IDs. | ||
| # | ||
| # Env (all optional except the token for a private remote): | ||
| # ENTIRE_CHECKPOINT_TOKEN GitHub token with read access to the checkpoint repo. | ||
| # Sent as an RFC 7617 basic auth header (matching the | ||
| # CLI), so it never appears in a remote URL. | ||
| # CHECKPOINT_REPO owner/repo of the checkpoint remote | ||
| # (default: entireio/cli-checkpoints). | ||
| # AUDIT_WINDOW git `--since` window (default: "24 hours ago"). | ||
| # AUDIT_REPORT_FILE markdown report sink (default: checkpoint-audit-report.md). | ||
| # AUDIT_JSON_FILE machine-readable report sink (default: checkpoint-audit-report.json). | ||
| # GITHUB_STEP_SUMMARY if set, the markdown report is appended to it. | ||
| # | ||
| # Exit codes: 0 = all present, 1 = one or more checkpoints missing, 2 = setup or | ||
| # remote error (a broken remote must never read as "0 missing"). | ||
|
|
||
| CHECKPOINT_REPO="${CHECKPOINT_REPO:-entireio/cli-checkpoints}" | ||
| AUDIT_WINDOW="${AUDIT_WINDOW:-24 hours ago}" | ||
| AUDIT_REPORT_FILE="${AUDIT_REPORT_FILE:-checkpoint-audit-report.md}" | ||
| AUDIT_JSON_FILE="${AUDIT_JSON_FILE:-checkpoint-audit-report.json}" | ||
| TOKEN="${ENTIRE_CHECKPOINT_TOKEN:-}" | ||
|
|
||
| CHECKPOINT_URL="https://github.com/${CHECKPOINT_REPO}.git" | ||
|
|
||
| REMOTE_IDS_FILE=$(mktemp "${TMPDIR:-/tmp}/checkpoint-audit.XXXXXX") | ||
| ROWS_FILE=$(mktemp "${TMPDIR:-/tmp}/checkpoint-audit.XXXXXX") | ||
| trap 'rm -f "$REMOTE_IDS_FILE" "$ROWS_FILE"' EXIT | ||
|
|
||
| # json_str emits a JSON string literal. Git commit subjects can legally contain | ||
| # tabs and other control characters, so escape backslash, double quote, and the | ||
| # C0 controls that would otherwise produce invalid JSON. | ||
| json_str() { | ||
| local s=${1//\\/\\\\} | ||
| s=${s//\"/\\\"} | ||
| s=${s//$'\t'/\\t} | ||
| s=${s//$'\r'/\\r} | ||
| s=${s//$'\n'/\\n} | ||
| s=${s//$'\b'/\\b} | ||
| s=${s//$'\f'/\\f} | ||
| printf '"%s"' "$s" | ||
| } | ||
|
|
||
| # 1. Enumerate the checkpoint refs present on the remote (names only, no object | ||
| # transfer). The ID is the ref leaf: refs/entire/checkpoints/<shard>/<ID>. | ||
| echo "Enumerating checkpoint refs on ${CHECKPOINT_REPO} ..." >&2 | ||
| if [ -n "$TOKEN" ]; then | ||
| auth_b64=$(printf 'x-access-token:%s' "$TOKEN" | base64 | tr -d '\n') | ||
| # Standard header name/scheme casing, matching the CLI (Authorization: Basic). | ||
| remote_refs=$(git -c "http.extraheader=Authorization: Basic ${auth_b64}" \ | ||
| ls-remote "$CHECKPOINT_URL" 'refs/entire/checkpoints/*') || { | ||
| echo "::error::failed to ls-remote ${CHECKPOINT_REPO} (check ENTIRE_CHECKPOINT_TOKEN and repo access)" >&2 | ||
| exit 2 | ||
| } | ||
| else | ||
| remote_refs=$(git ls-remote "$CHECKPOINT_URL" 'refs/entire/checkpoints/*') || { | ||
| echo "::error::failed to ls-remote ${CHECKPOINT_REPO} (no ENTIRE_CHECKPOINT_TOKEN set; is the repo private?)" >&2 | ||
| exit 2 | ||
| } | ||
| fi | ||
|
|
||
| printf '%s\n' "$remote_refs" \ | ||
| | awk '$2 ~ /^refs\/entire\/checkpoints\// { id = $2; sub(/.*\//, "", id); print id }' \ | ||
| | sort -u > "$REMOTE_IDS_FILE" | ||
| remote_count=$(grep -c . "$REMOTE_IDS_FILE" || true) | ||
|
|
||
| # 2. Candidate commits: every branch commit in the window (local heads + remotes), | ||
| # de-duplicated while preserving order. A git-log failure (e.g. run outside a | ||
| # git repo) is a setup error, not "0 missing" — surface it as exit 2 rather | ||
| # than letting git's raw 128 escape. | ||
| if ! commits=$(git log --branches --remotes --since="$AUDIT_WINDOW" --format='%H'); then | ||
| echo "::error::failed to enumerate commits (is this a git repository?)" >&2 | ||
| exit 2 | ||
| fi | ||
| commits=$(printf '%s\n' "$commits" | awk '!seen[$0]++') | ||
|
|
||
| # 3. For each commit, diff its checkpoint trailers against the remote set. | ||
| commit_count=0 | ||
| cp_count=0 | ||
| missing_count=0 | ||
| while IFS= read -r sha; do | ||
| [ -z "$sha" ] && continue | ||
| commit_count=$((commit_count + 1)) | ||
| cps=$(git show -s --format='%(trailers:key=Entire-Checkpoint,valueonly=true)' "$sha") | ||
| while IFS= read -r cp; do | ||
| cp="${cp//[[:space:]]/}" | ||
| [ -z "$cp" ] && continue | ||
| cp_count=$((cp_count + 1)) | ||
| if grep -Fxq "$cp" "$REMOTE_IDS_FILE"; then | ||
| continue | ||
| fi | ||
| missing_count=$((missing_count + 1)) | ||
| meta=$(git show -s --format='%h%x1f%an%x1f%aI%x1f%s' "$sha") | ||
| short=${meta%%$'\x1f'*}; meta=${meta#*$'\x1f'} | ||
| author=${meta%%$'\x1f'*}; meta=${meta#*$'\x1f'} | ||
| cdate=${meta%%$'\x1f'*}; subject=${meta#*$'\x1f'} | ||
| # `|| true`: grep exits 1 when every branch is filtered out (or there are | ||
| # none), which would otherwise abort the whole run under `set -e`. Join with | ||
| # a single-char delimiter then expand to ", " — `paste -sd', '` treats the | ||
| # delimiter as a circular char list and would alternate "," and " ". | ||
| branches=$(git branch -a --contains "$sha" --format='%(refname:short)' 2>/dev/null \ | ||
| | sed -e 's#^remotes/##' -e 's#^origin/##' \ | ||
| | grep -v '^entire/' \ | ||
| | awk 'NF && !s[$0]++' \ | ||
| | paste -sd',' - \ | ||
| | sed 's/,/, /g' || true) | ||
| printf '%s\x1e%s\x1e%s\x1e%s\x1e%s\x1e%s\n' \ | ||
| "$cp" "$short" "$author" "${branches:-?}" "$cdate" "$subject" >> "$ROWS_FILE" | ||
| done <<EOF | ||
| $cps | ||
| EOF | ||
| done <<EOF | ||
| $commits | ||
| EOF | ||
|
|
||
| # 4. Render the markdown report (stdout + file + optional step summary). | ||
| { | ||
| echo "# Checkpoint availability audit" | ||
| echo | ||
| echo "- Window: commits since \`${AUDIT_WINDOW}\`" | ||
| echo "- Checkpoint remote: \`${CHECKPOINT_REPO}\` (${remote_count} refs present)" | ||
| echo "- Scanned: ${commit_count} commit(s), ${cp_count} checkpoint trailer(s)" | ||
| echo "- **Missing from remote: ${missing_count}**" | ||
| echo | ||
| if [ "$missing_count" -gt 0 ]; then | ||
| echo "| Checkpoint | Commit | Author | Branch(es) | Date | Subject |" | ||
| echo "|---|---|---|---|---|---|" | ||
| while IFS=$'\x1e' read -r cp short author branches cdate subject; do | ||
| # Escape every free-form cell: author names and (legal) branch names can | ||
| # contain a pipe, which would otherwise break the markdown table. | ||
| author=${author//|/\\|} | ||
| branches=${branches//|/\\|} | ||
| subject=${subject//|/\\|} | ||
| echo "| \`${cp}\` | \`${short}\` | ${author} | ${branches} | ${cdate} | ${subject} |" | ||
| done < "$ROWS_FILE" | ||
| else | ||
| echo "All checkpoints referenced in the window are present on the remote. :white_check_mark:" | ||
| fi | ||
| } | tee "$AUDIT_REPORT_FILE" | ||
|
|
||
| if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then | ||
| cat "$AUDIT_REPORT_FILE" >> "$GITHUB_STEP_SUMMARY" | ||
| fi | ||
|
|
||
| # 5. Machine-readable report for the artifact. | ||
| { | ||
| echo "[" | ||
| first=1 | ||
| while IFS=$'\x1e' read -r cp short author branches cdate subject; do | ||
| if [ "$first" -eq 1 ]; then first=0; else echo ","; fi | ||
| printf ' {"checkpoint":%s,"commit":%s,"author":%s,"branches":%s,"date":%s,"subject":%s}' \ | ||
| "$(json_str "$cp")" "$(json_str "$short")" "$(json_str "$author")" \ | ||
| "$(json_str "$branches")" "$(json_str "$cdate")" "$(json_str "$subject")" | ||
| done < "$ROWS_FILE" | ||
| echo | ||
| echo "]" | ||
| } > "$AUDIT_JSON_FILE" | ||
|
|
||
| if [ "$missing_count" -gt 0 ]; then | ||
| echo "::error::${missing_count} checkpoint(s) missing from ${CHECKPOINT_REPO}" >&2 | ||
| exit 1 | ||
| fi | ||
| echo "All ${cp_count} checkpoint(s) present on ${CHECKPOINT_REPO}." >&2 | ||
| exit 0 |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.