Files
Arnaud Héritier f78c6dd7c3 ci(update-models): sign refresh commits, add semantic catalog diff
Two independent improvements to the weekly models.dev snapshot refresh
(.github/workflows/update-models.yml), which previously opened PRs
with an unsigned commit attributed to whoever happened to trigger the
run, and a static, uninformative PR body.

Signed commits
---------------
Add `sign-commits: true` to the existing `peter-evans/create-pull-request`
step. This builds the commit through GitHub's API instead of a local
`git push`, so it is Verified regardless of which token creates it.
On the default `GITHUB_TOKEN` (used explicitly via `token: github.token`
for clarity) the commit author/committer becomes `github-actions[bot]`
consistently, rather than the human who happened to run the workflow.

`persist-credentials: false` is set on checkout as a minor hardening
measure, since `create-pull-request` is given its token explicitly and
never needs checkout's persisted git credential.

Semantic catalog diff, no AI
-----------------------------
`pkg/modelsdev/snapshot.json` is a single-line, multi-megabyte JSON
file, so its raw git diff is one useless line — this is why the PR
body was static. Instead:

- `.github/scripts/models-delta.jq` computes a structural
  added/removed/changed diff between two snapshot.json files.
- `.github/scripts/models-delta-render.jq` renders that diff to
  markdown tables, with a `safe()`/`safe_diff()` sanitiser clamping
  every third-party (models.dev) string to a short, bounded,
  control-character-free charset before it reaches the PR body — that
  body is consumed by this repo's automated PR-review pipeline, so
  untrusted text needs bounding before it gets there.
- `scripts/models-delta.sh` is the single wrapper around both jq
  scripts; a new `task models-delta` target and the workflow's
  "Compute catalog delta" step both call it, so the diff logic exists
  in exactly one place. With no arguments it diffs HEAD's
  pkg/modelsdev/snapshot.json against the working tree — the same
  comparison the workflow makes after `task update-models`, so the
  local and CI code paths never drift apart.
- The workflow uses the rendered markdown as the PR body/title/commit
  subject. The delta step is `continue-on-error: true` with a
  static-body fallback step, so a jq/shell bug degrades the PR text
  instead of silently skipping the weekly refresh. A refresh still
  opens a PR even when only snapshot_date.txt changed (no catalog
  content diff), preserving today's behaviour and avoiding any
  interaction with the check-models-fresh staleness test.
- `scripts/models-delta-test.sh` is a golden-fixture regression test
  for the two jq scripts (fixtures under .github/scripts/testdata/),
  since they have no Go-test equivalent and only run for real,
  unattended, once a week. `.github/workflows/models-delta-lint.yml`
  runs it (plus shellcheck) as its own path-filtered workflow — mirrors
  the repo's existing docs-lint.yml pattern — rather than unconditionally
  on every PR via ci.yml's blanket lint job.
- A concurrency group and a canonical-repo job guard prevent an
  overlapping workflow_dispatch from racing the weekly schedule run.

Phase 3 of the original proposal (an LLM-written PR summary via
docker/docker-agent-action) is deferred to a future change.
2026-09-02 15:08:34 +00:00

81 lines
2.2 KiB
Bash
Executable File

#!/usr/bin/env bash
# scripts/models-delta.sh — semantic diff of the models.dev catalog snapshot.
#
# Usage:
# scripts/models-delta.sh [--json-out FILE] [OLD NEW]
#
# With no positional arguments, compares the version of
# pkg/modelsdev/snapshot.json at HEAD against the current working-tree copy —
# i.e. "what would `task update-models` change if I committed it right now".
# This is also exactly what the update-models workflow runs after refreshing
# the snapshot, so the local and CI code paths never drift apart.
#
# OLD and NEW, when given, are paths to two snapshot.json files to compare
# instead (e.g. two saved copies from different runs).
#
# The rendered markdown always goes to stdout. --json-out additionally writes
# the structural delta (added/removed/changed) as JSON, which the workflow
# uses to compute counts and a commit subject without re-running jq.
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
DELTA_JQ="$ROOT/.github/scripts/models-delta.jq"
RENDER_JQ="$ROOT/.github/scripts/models-delta-render.jq"
json_out=""
positional=()
end_of_opts=false
while [ $# -gt 0 ]; do
if [ "$end_of_opts" = false ]; then
case "$1" in
--json-out)
[ $# -ge 2 ] || { echo "models-delta.sh: --json-out requires a path" >&2; exit 2; }
json_out="$2"
shift 2
continue
;;
--)
end_of_opts=true
shift
continue
;;
-*)
echo "models-delta.sh: unknown option $1" >&2
exit 2
;;
esac
fi
positional+=("$1")
shift
done
old="${positional[0]:-}"
new="${positional[1]:-}"
cleanup_old=""
delta_json=""
trap '[ -n "$cleanup_old" ] && rm -f "$cleanup_old"; [ -n "$delta_json" ] && rm -f "$delta_json"' EXIT
if [ -z "$old" ]; then
cleanup_old="$(mktemp)"
old="$cleanup_old"
git -C "$ROOT" show "HEAD:pkg/modelsdev/snapshot.json" > "$old"
fi
if [ -z "$new" ]; then
new="$ROOT/pkg/modelsdev/snapshot.json"
fi
for f in "$old" "$new"; do
[ -r "$f" ] || { echo "models-delta.sh: cannot read $f" >&2; exit 1; }
done
delta_json="$(mktemp)"
jq -n --slurpfile old "$old" --slurpfile new "$new" -f "$DELTA_JQ" > "$delta_json"
if [ -n "$json_out" ]; then
cp "$delta_json" "$json_out"
fi
jq -r -f "$RENDER_JQ" "$delta_json"