Skip to content

feat: ecosystem-wide [compat] pruning workflow - #95

Merged
ocots merged 8 commits into
mainfrom
feat/compat-prune
Oct 16, 2026
Merged

ocots merged 8 commits into
mainfrom
feat/compat-prune

Conversation

@ocots

@ocots ocots commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

Implements control-toolbox/OptimalControl.jl#986: the organization's monthly [compat] prune. It finds the specifiers that no supported Julia version selects across the CT* ecosystem, proves that dropping them leaves every resolved manifest unchanged, and maintains one PR per affected repository plus one tracking issue in CTActions. Blockers (an entry's newest specifier is never selected) are reported, without PR. Everything lives in CTActions; no package carries a caller (OptimalControl.jl#990 was closed for that reason). Plan: control-toolbox/OptimalControl.jl#986 (comment).

Contents

Path Role
.github/workflows/compat-prune.yml Reusable workflow. audit job: read-only, report and diff in the job summary, compat-prune artifact. publish job: the only writer, skipped on dry runs, uses a GitHub App token
.github/workflows/compat-prune-monthly.yml The monthly run: cron (1st, 04:00 UTC) and workflow_dispatch (dry_run). Dry run on every trigger until the GitHub App exists
.github/workflows/compat-prune-tests.yml Self-test, run when the scripts or workflows change: the test suite on Julia 1.10, 1.12, 1.13, then a real dry-run audit through the reusable workflow
scripts/compat-prune/audit.jl Orchestrator: one registry snapshot, every Julia version (julia +<v>), validation repeated until stable
collect.jl, envs.jl Audited versions come from the joint resolution. Files are read from the registered tree. 17 environments: joint, plus the test and docs environments of every package
resolve.jl, classify.jl, validate.jl Pkg's resolver only (resolve_versions!: nothing downloaded or installed). Classification: prune / keep / blocked / idle / withheld. Prunes are simulated by intersecting compat in each environment where they apply, and rejected if any manifest moves
edit.jl, checkout.sh One [compat] line rewritten in place on fresh checkouts of the default branches. Outcome per prune: edited / already / drift
report.jl, publish.jl Tracking-issue body (it opens with the audit record: environments resolved per Julia version, entries examined by status, registry snapshot, entries left as they are with why) and PR bodies. Idempotent publication: a fixed branch ct/compat-prune, push only when the content changes, PRs updated or closed, the issue reopened and never recreated
scripts/compat-prune/test/ One test file per script. Fakes for gh, the simulator and GitHub. Golden files. A non-regression snapshot of the real 2026-10-15 audit. Integration tests (real resolutions, scripts run as commands, fake gh on PATH, local git origins)
docs/WORKFLOWS.md, README.md Catalog, how the prune works, self-test, secrets

Checks

  • Tests: 366 pass on Julia 1.10.11, 1.12.6 and 1.13.1, locally and in the self-test.
  • Dry-run audit on a runner: about 4 min, with the same results as locally.
    • 17/17 environments resolved on each Julia version; 45 entries examined.
    • 42 prunes accepted, 0 rejected, 2 blocked, 1 kept (CTParser's CTBase = "0.29, 0.30": both lanes in use).
    • 35 lines edited in 12 files of 6 repositories.
    • 7 drifts, all in CTParser, whose default branch is behind its registered version (CTParser#326).
  • Negative control: dropping 0.7 from CTSolvers' CUDSS is rejected, because OptimalControl's tests and docs select it.
  • Publication: replayed against a stateful fake GitHub and real git origins.
    • First run: creates the issue, the branch and the PR.
    • Second run: changes nothing.
    • After the merge: closes the PR and the issue, and deletes the branch.
    • When a new prune appears: reopens the same issue.

Before the first live run (maintainer)

  • Create the organization's GitHub App:
    • permissions: Contents, Pull requests and Issues (read and write), Metadata (read);
    • installed on the 8 CT* repositories and on CTActions, which holds the tracking issue;
    • secrets CT_APP_CLIENT_ID and CT_APP_PRIVATE_KEY, as organization secrets or CTActions secrets.
  • Merge this PR. workflow_dispatch only lists workflows that are on main.
  • First live run: run Compat prune (monthly) from the Actions tab of CTActions with dry_run: false. Then switch the schedule to publishing, a one-line change in compat-prune-monthly.yml.
  • Update the text of #986: CI uses force_latest_compatible_version=auto (ci: restore force_latest_compatible_version=auto, skip it on Julia 1.10 #93), not false; the workflow lives in CTActions.

🤖 Generated with Claude Code

ocots and others added 3 commits October 15, 2026 22:42
Stdlib-only Julia scripts that find the [compat] specifiers no supported Julia version
ever selects across the control-toolbox ecosystem, and prove that dropping them changes
no resolved manifest (control-toolbox/OptimalControl.jl#986).

- collect.jl: audited versions = joint resolution; files read from the registered tree
- envs.jl: joint env + test and docs env of every package, owner pinned
- resolve.jl: baseline, or prunes simulated by per-environment compat intersection
- classify.jl: prune / keep / blocked / idle per multi-specifier entry
- validate.jl: rejects prunes that change a manifest, names the blockers
- audit.jl: one registry snapshot, every Julia version, validation until stable

Phases A and B of the plan; the workflow, the report and the publishing come next.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- edit.jl: rewrites one [compat] value in place on checkouts of the default branches
  (layout, quoting and comments kept, result re-parsed); outcome edited / already /
  drift per prune. Edits are re-applied to the current branch, never copied from the
  collected files: copying reverted CTSolvers' version line in the dry run.
- report.jl: deterministic Markdown body of the tracking issue, with a hidden marker.
- resolution calls Pkg.Operations.resolve_versions! directly: Pkg.resolve downloads
  every source and artifact, and offline mode restricts to installed versions.
- every script is split into functions and a main guard; gh and the resolver are
  injectable.
- tests split per script, with fakes (gh, simulator), golden files, a non-regression
  snapshot of the real 2026-10-15 audit, and opt-in integration tests (real resolutions,
  scripts run as commands, fake gh on PATH): 297 pass on Julia 1.10, 1.12, 1.13.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- compat-prune.yml: reusable workflow, read-only `audit` job for now: juliaup with the
  audited Julia versions, General + ct-registry (public, over HTTPS), script tests, the
  audit, the prunes applied to fresh checkouts of the default branches, report in the job
  summary (with the diff), `compat-prune` artifact. `dry_run` defaults to true.
- compat-prune-tests.yml: when the scripts or the workflows change, the test suite on
  Julia 1.10, 1.12, 1.13 (integration included), then a dry-run audit through the
  reusable workflow at the PR head.

A full local audit now takes 145 s (eight packages, 17 environments, three Julia
versions): 42 prunes accepted, 0 rejected, 2 blocked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ocots added a commit to control-toolbox/OptimalControl.jl that referenced this pull request Oct 15, 2026
Caller of CTActions' compat-prune workflow (control-toolbox/CTActions#95), monthly and on
demand. Dry run on every trigger until the publishing job is enabled.

Part of #986.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ocots

ocots commented Oct 15, 2026

Copy link
Copy Markdown
Member Author

Progress: phases C and D done

Phase C — report, edits, tests (36bef06)

  • edit.jl rewrites one [compat] value in place on checkouts of the default branches, keeping layout, quoting and comments, and re-parses the result. Each prune ends up edited, already (merged, awaiting release) or drift.
  • report.jl renders the tracking-issue body: deterministic, with a hidden marker.
  • Two problems found on the way, both now covered by tests:
    • Copying collected files would revert concurrent changes. The first dry run turned CTSolvers' version = "0.6.1" back into 0.6.0, because main had moved since collection. Edits are now always re-applied line by line to the current branch.
    • Pkg.resolve downloads every source and artifact (GBs of CUDA on a fresh runner), and Pkg.offline(true) would only see installed versions. The integration tests caught it. Resolution now calls Pkg.Operations.resolve_versions! directly. That function is internal, but its signature is identical on 1.10, 1.12 and 1.13, and the integration tests guard it. A full audit dropped from more than 45 min to 145 s locally.
  • Tests, split per script (297 on Julia 1.10, 1.12, 1.13):
    • fakes for gh and the simulator;
    • golden files for the report body;
    • non-regression on a snapshot of the real 2026-10-15 audit, with golden environments, classification and edits;
    • opt-in integration: real resolutions, every script run as a command, a fake gh on PATH.

Phase D — workflows (37cc299)

  • compat-prune.yml (reusable): the read-only audit job. It installs juliaup with the audited versions and adds General plus ct-registry (public, over HTTPS, no SSH key). It then runs the script tests, the audit, and the edits on fresh checkouts. The report and the diff go to the job summary, and everything is uploaded as the compat-prune artifact.
  • compat-prune-tests.yml: the self-test, triggered when the scripts or the workflows change. It runs the suite on the three Julia versions, then a real dry-run audit through the reusable workflow.
  • First run: green.
    • Tests take about 1 min per Julia version; the dry-run audit takes 4 min.
    • Results are identical to the local run: 42 prunes accepted, 0 rejected, 2 blocked; 35 lines edited in 6 repositories, 7 drifts.
    • The 7 drifts are all in CTParser, whose default branch is behind its registered version (CTParser#326).
    • Blocked entries: CTBase's DocStringExtensions 0.10 (capped by CTFlows, CTLie, CTModels, CTParser, Documenter, OrdinaryDiffEq, SciMLBase) and CTParser's test/ CTBase 0.30 (capped by CTModels 0.18).
  • Caller in OptimalControl: ci: monthly [compat] prune audit (CompatPrune) OptimalControl.jl#990 (draft; merge after this PR). Monthly plus on demand, dry run on every trigger until publishing is enabled.

Next

  • Phase E: publish.jl and the publish job. Implemented and tested with a fake gh, but not run live: it needs the GitHub App, which the maintainer has to create.
  • Phase F: documentation.

ocots and others added 3 commits October 15, 2026 23:20
…issue)

- publish.jl: idempotent publication. The tracking issue is found by its hidden marker
  (open or closed), its body rewritten, reopened when something is to prune and closed
  when nothing is left, never created twice. Each repository has one bot-owned branch
  ct/compat-prune rebuilt from its default branch, pushed only when its content changes;
  its PR is updated or created, and closed (branch deleted) when nothing is left.
- report.jl: PR body (entries of the repository, links to the issue and the run).
- checkout.sh: shallow clones of the default branches, shared by both jobs.
- compat-prune.yml: `publish` job, skipped on dry runs, the only writer, with a token of
  the organization's GitHub App (CT_APP_CLIENT_ID, CT_APP_PRIVATE_KEY); fails early and
  clearly when the secrets are missing. actionlint flags `client-id` of
  create-github-app-token@v3 from outdated metadata: v3 takes `client-id`.
- test_publish.jl: a stateful fake GitHub and real local git origins replay four runs
  (create; rerun changes nothing; after the merge everything is closed; a new prune
  reopens the same issue). 336 tests pass on Julia 1.10, 1.12, 1.13.

Not run live: the GitHub App does not exist yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`sort` on Pairs compares the values as well (`isless(::Pair)` evaluates both sides), and
the index values are dicts: the dry run of 8808247 failed there. Sorted by name only;
COMPAT_PRUNE_GIT_BASE lets the new integration test clone from local repositories.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WORKFLOWS.md §3.1: catalog row and how the monthly prune works (what is audited, what is
pruned, what is reported, idempotence, dry run, local run, caller); §3.2: the self-test;
§4: OptimalControl's CompatPrune; §5: CT_APP_CLIENT_ID / CT_APP_PRIVATE_KEY.
README: catalog row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ocots

ocots commented Oct 15, 2026

Copy link
Copy Markdown
Member Author

Progress: phases E and F done (8808247, 156b806, cc32a54)

Phase E — publication

  • publish.jl publishes idempotently:
    • the tracking issue is found by its hidden marker, open or closed;
    • each repository gets one bot-owned branch ct/compat-prune, rebuilt from the default branch and pushed only when its content changes;
    • PRs are updated or created, and closed (branch deleted) when a repository has nothing left;
    • the issue is closed when nothing is left anywhere, and reopened rather than recreated later.
  • publish job in compat-prune.yml: skipped on dry runs, and the only job that writes. It uses an App token (actions/create-github-app-token@v3, client-id; actionlint's metadata for that action is outdated) and fails early when the secrets are missing.
  • test_publish.jl replays four consecutive runs against a stateful fake GitHub and real local git origins (39 tests):
    • create;
    • rerun with no change;
    • merge, then everything closes;
    • a new prune reopens the same issue.
  • Not run live: the GitHub App does not exist yet.

A bug the dry run caught

checkout.sh sorted the index with sort(collect(dict)). That compares the values as well, because isless(::Pair) evaluates both sides, and the values are dicts, so the audit failed on the runner. It is fixed in 156b806, with an integration test that clones from local git origins through COMPAT_PRUNE_GIT_BASE.

Phase F — documentation

docs/WORKFLOWS.md:

  • §3.1: catalog row, plus how the monthly prune works (what is audited, pruned and reported, idempotence, dry run, local run, caller);
  • §3.2: the self-test;
  • §4: OptimalControl's CompatPrune;
  • §5: the App secrets.

README.md: catalog row.

State

  • Tests: 340 pass on Julia 1.10, 1.12, 1.13; the self-test is green.
  • Remaining (maintainer, see the checklist in the description): create the GitHub App, merge, then the first live run.
  • Out of scope, noticed on the way:
    • WORKFLOWS.md §5 still calls ct-registry private; it is public.
    • CTParser's main is behind its registered 0.9.5-beta (CTParser#326), hence the 7 drifts.

@ocots
ocots marked this pull request as ready for review October 15, 2026 21:37
ocots and others added 2 commits October 16, 2026 09:06
…dit record

The prune is the organization's, never a package's: the monthly run moves from an
OptimalControl caller to compat-prune-monthly.yml in CTActions (cron + workflow_dispatch,
dry run on every trigger until the GitHub App exists), and the tracking issue lives in
CTActions (new input `tracking_repo`, default control-toolbox/CTActions).

The tracking issue now opens with the record of the audit: environments resolved per Julia
version (and which failed), multi-specifier entries examined with one status each (prune,
rejected, blocked, keep, idle, withheld), registry snapshot (General tree, ct-registry
commit), and, folded, the entries left as they are with why. audit.jl is split into
registry_snapshot / coverage / summarize, tested (test_audit.jl, report golden, integration).
366 tests pass on Julia 1.10, 1.12, 1.13.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WORKFLOWS.md: compat-prune leaves §3.1 (no package calls it) for §3.2 (workflows that live
in CTActions), with compat-prune-monthly.yml, the tracking issue in CTActions and the audit
record; §4 no longer lists a CompatPrune caller in OptimalControl; §5 secrets moved to the
monthly workflow. README: from the reusable table to the scheduled one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ocots

ocots commented Oct 16, 2026

Copy link
Copy Markdown
Member Author

The prune runs from CTActions; the audit record is in the issue (4b5aa65, bd46094)

The prune is the organization's, never a package's, so nothing lives in OptimalControl any more.

  • compat-prune-monthly.yml, here in CTActions: cron on the 1st at 04:00 UTC, plus workflow_dispatch with dry_run. It calls the reusable compat-prune.yml, like the self-test does. Every trigger stays a dry run until the GitHub App exists.
  • Tracking issue in CTActions: new input tracking_repo, default control-toolbox/CTActions.
  • OptimalControl.jl#990 is closed, and its branch deleted.

The tracking issue now opens with an Audit section:

  • environments resolved per Julia version, and which ones failed;
  • the number of multi-specifier entries examined, with one status each: prune, rejected, blocked, keep, idle, withheld;
  • the registry snapshot: General tree, ct-registry commit;
  • folded, the entries left as they are, with why.

In the self-test run: 17/17 environments on 1.10, 1.12 and 1.13; 45 entries examined, with 42 prunes, 2 blocked and 1 kept (CTParser's CTBase = "0.29, 0.30", both lanes in use).

  • Code: audit.jl is split into registry_snapshot, coverage and summarize.
  • Tests: new test_audit.jl, an extended report golden file, and registry_snapshot in the integration tests. 366 pass on 1.10, 1.12 and 1.13.
  • Docs: compat-prune moves from §3.1 to §3.2 of WORKFLOWS.md (workflows that live in CTActions), and the README lists it among the scheduled workflows. The checklist in the description is updated.

@ocots
ocots merged commit 7cefffb into main Oct 16, 2026
5 checks passed
@ocots
ocots deleted the feat/compat-prune branch October 16, 2026 09:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant