From d9f5a8fc2c8b33401cfe2bbe94e9b77e21a7b830 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Thu, 15 Oct 2026 22:42:56 +0200 Subject: [PATCH 1/8] feat(compat-prune): audit scripts (collect, resolve, classify, validate) 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 --- scripts/compat-prune/audit.jl | 81 ++++++++++++++ scripts/compat-prune/classify.jl | 117 +++++++++++++++++++ scripts/compat-prune/collect.jl | 55 +++++++++ scripts/compat-prune/common.jl | 155 ++++++++++++++++++++++++++ scripts/compat-prune/envs.jl | 66 +++++++++++ scripts/compat-prune/resolve.jl | 27 +++++ scripts/compat-prune/test/runtests.jl | 144 ++++++++++++++++++++++++ scripts/compat-prune/validate.jl | 98 ++++++++++++++++ 8 files changed, 743 insertions(+) create mode 100644 scripts/compat-prune/audit.jl create mode 100644 scripts/compat-prune/classify.jl create mode 100644 scripts/compat-prune/collect.jl create mode 100644 scripts/compat-prune/common.jl create mode 100644 scripts/compat-prune/envs.jl create mode 100644 scripts/compat-prune/resolve.jl create mode 100644 scripts/compat-prune/test/runtests.jl create mode 100644 scripts/compat-prune/validate.jl diff --git a/scripts/compat-prune/audit.jl b/scripts/compat-prune/audit.jl new file mode 100644 index 0000000..9e6f42a --- /dev/null +++ b/scripts/compat-prune/audit.jl @@ -0,0 +1,81 @@ +# Run a whole audit: one registry snapshot, every Julia version. +# +# usage: julia audit.jl '' '' +# +# Each Julia version is run as `julia +` (juliaup). Steps: +# 1. update the registries once; every later step runs offline on this snapshot +# 2. collect.jl and envs.jl, on the last (newest) Julia version +# 3. resolve.jl: the baseline of every environment, on each Julia version +# 4. classify.jl, from all baselines +# 5. validate.jl on each Julia version, repeated with the rejected prunes removed until no +# version rejects anything new (a smaller set of prunes is re-checked as a whole) +# Output: /audit.toml (accepted prunes, rejected prunes, blockers, other entries), +# next to the intermediate files of every step. + +using Dates, Pkg, TOML + +const HERE = @__DIR__ +packages, versions, out = ARGS[1], ARGS[2], abspath(ARGS[3]) +include(joinpath(HERE, "common.jl")) # for parse_list only; Pkg is offline from here on +jlv = parse_list(versions) +short(v) = join(split(v, ".")[1:2], ".") + +function run_step(v, script, args...) + cmd = `julia +$v --startup-file=no $(joinpath(HERE, script)) $args` + println("\n\$ julia +$v $script ", join(args, " ")) + run(cmd) +end + +Pkg.offline(false) +Pkg.Registry.update() +Pkg.offline(true) +mkpath(out) + +run_step(last(jlv), "collect.jl", packages, out) +run_step(last(jlv), "envs.jl", out) +for v in jlv + run_step(v, "resolve.jl", out, joinpath(out, "baseline-$(short(v)).toml")) +end +run_step(last(jlv), "classify.jl", out, joinpath(out, "classified.toml"), + (joinpath(out, "baseline-$(short(v)).toml") for v in jlv)...) + +key(p) = (p["owner"], p["file"], p["dep"]) +rejected = Dict{String,Any}[] +blocked = Dict{String,Any}[] +for round in 1:5 + open(io -> TOML.print(io, Dict("rejected" => rejected)), joinpath(out, "rejected.toml"), "w") + new = Dict{String,Any}[] + empty!(blocked) + for v in jlv + res = joinpath(out, "validated-$(short(v)).toml") + run_step(v, "validate.jl", out, joinpath(out, "classified.toml"), + joinpath(out, "baseline-$(short(v)).toml"), res, joinpath(out, "rejected.toml")) + r = TOML.parsefile(res) + append!(new, r["rejected"]) + append!(blocked, r["blocked"]) + end + known = Set(key.(rejected)) + fresh = [p for p in new if !(key(p) in known)] + isempty(fresh) && break + round == 5 && error("validation did not settle after 5 rounds") + # one record per prune, the first reason found + for p in fresh + key(p) in known || (push!(rejected, p); push!(known, key(p))) + end +end + +c = TOML.parsefile(joinpath(out, "classified.toml")) +gone = Set(key.(rejected)) +index = TOML.parsefile(joinpath(out, "index.toml")) +audit = Dict( + "date" => Dates.format(now(UTC), "yyyy-mm-dd HH:MM") * " UTC", + "julia" => jlv, + "packages" => index["packages"], + "prune" => [p for p in c["prune"] if !(key(p) in gone)], + "rejected" => rejected, + "blocked" => blocked, + "other" => [r for r in c["other"] if r["status"] != "blocked"], +) +open(io -> TOML.print(io, audit), joinpath(out, "audit.toml"), "w") +println("\n", length(audit["prune"]), " prune(s) accepted, ", length(rejected), " rejected, ", + length(unique(key.(blocked))), " blocked entr(y/ies)") diff --git a/scripts/compat-prune/classify.jl b/scripts/compat-prune/classify.jl new file mode 100644 index 0000000..200bb82 --- /dev/null +++ b/scripts/compat-prune/classify.jl @@ -0,0 +1,117 @@ +# Classify every multi-specifier compat entry from the baselines of all Julia versions. +# +# usage: julia classify.jl ... +# +# A specifier of the entry of `owner` on `dep` is *used* when, in an environment where that +# entry applies (see `applies` in common.jl) and on some Julia version, the resolved `dep` +# falls in it. Then, ordering specifiers by their lower bound: +# prune unused specifiers older than the newest used one: obsolete lanes; +# blocked the newest specifier is unused: something caps `dep` below it (reported, the +# blocker is identified by validate.jl); +# idle `dep` is resolved nowhere the entry applies (e.g. a weak dependency no audited +# environment loads): nothing can be proven, nothing is touched. +# Unused specifiers between the newest used one and the newest declared one are blocked +# lanes too, never pruned. If any baseline environment failed to resolve, the lanes it would +# have used are unknown: every prune is then withheld, and the failure is reported. + +include("common.jl") + +const FILES = ("Project.toml", "test/Project.toml", "docs/Project.toml") + +"Specifiers of a compat string, in declaration order." +specifiers(s) = String.(strip.(split(s, ","))) + +"Lower bound of one specifier, for ordering (`\"0.7 - 0.8\"` → v0.7, `\"^1.2\"` → v1.2)." +lower(s) = VersionNumber(match(r"\d+(\.\d+){0,2}", s).match) + +""" + classify_entry(owner, file, dep, compat, results, audited) -> Dict + +`results` maps `"@"` to the parsed result of that environment. Returns a record +with `status` (`prune`, `blocked`, `keep`, `idle`), the specifiers used, and the evidence: +where each used specifier was selected. +""" +function classify_entry(owner, file, dep, compat, results, audited) + parts = specifiers(compat) + evidence = Dict(p => String[] for p in parts) + for (key, r) in sort(collect(results); by=first) + r["ok"] || continue + name = first(split(key, "@")) + v = get(r["versions"], dep, nothing) + (v === nothing || !applies(owner, file, name, r["versions"], audited)) && continue + for p in parts + VersionNumber(v) in spec(p) && push!(evidence[p], "$key=$v") + end + end + record = Dict{String,Any}("owner" => owner, "file" => file, "dep" => dep, "before" => compat) + used = [p for p in parts if !isempty(evidence[p])] + record["evidence"] = Dict(p => evidence[p] for p in used) + isempty(used) && return merge(record, Dict("status" => "idle")) + newest_used = maximum(lower, used) + obsolete = [p for p in parts if isempty(evidence[p]) && lower(p) < newest_used] + if maximum(lower, parts) > newest_used + record["status"] = "blocked" + record["newest"] = parts[argmax(lower.(parts))] + record["applies_in"] = sort(unique(first.(split.(reduce(vcat, values(evidence)), "@")))) + # obsolete lanes of a blocked entry are still obsolete: report both + isempty(obsolete) || (record["prunable"] = obsolete) + return record + end + isempty(obsolete) && return merge(record, Dict("status" => "keep")) + return merge(record, Dict("status" => "prune", "removed" => obsolete, + "after" => join(setdiff(parts, obsolete), ", "))) +end + +"Every multi-specifier entry of the audited files, classified." +function classify(root, results, audited) + records = Dict{String,Any}[] + for owner in sort(collect(keys(audited))), file in FILES + path = joinpath(root, "registered", owner, file) + isfile(path) || continue + for (dep, compat) in sort(collect(get(TOML.parsefile(path), "compat", Dict()))) + dep == "julia" && continue + length(specifiers(compat)) > 1 || continue + push!(records, classify_entry(owner, file, dep, compat, results, audited)) + end + end + return records +end + +""" + withhold(prunes, results) -> (prunes, withheld) + +Withhold every prune when a baseline environment (any `"@"` of `results`) failed. +""" +function withhold(prunes, results) + failed = sort([k for (k, r) in results if !r["ok"]]) + isempty(failed) && return prunes, Dict{String,Any}[] + why = "baseline failed in $(join(failed, ", ")): lanes used there are unknown" + return Dict{String,Any}[], [merge(p, Dict("status" => "withheld", "reason" => why)) for p in prunes] +end + +if abspath(PROGRAM_FILE) == @__FILE__ + root, outfile = ARGS[1], ARGS[2] + _, audited, _ = load_collected(root) + results = Dict{String,Any}() + for f in ARGS[3:end] + b = TOML.parsefile(f) + j = join(split(b["julia"], ".")[1:2], ".") + for (name, r) in b["env"]; results["$name@$j"] = r; end + end + records = classify(root, results, audited) + # A blocked entry may also carry obsolete lanes: they are prunes in their own right. + prunes = [r for r in records if r["status"] == "prune"] + for r in records + r["status"] == "blocked" && haskey(r, "prunable") || continue + parts = specifiers(r["before"]) + push!(prunes, merge(r, Dict("status" => "prune", "removed" => r["prunable"], + "after" => join(setdiff(parts, r["prunable"]), ", ")))) + end + prunes, withheld = withhold(prunes, results) + others = vcat([r for r in records if r["status"] != "prune"], withheld) + open(io -> TOML.print(io, Dict("prune" => prunes, "other" => others)), outfile, "w") + for r in records + println(rpad("$(r["owner"]) $(r["file"])", 34), rpad(r["dep"], 26), rpad(r["before"], 18), r["status"], + haskey(r, "removed") ? " → $(r["after"])" : "", haskey(r, "prunable") ? " (prunable: $(join(r["prunable"], ", ")))" : "") + end +end diff --git a/scripts/compat-prune/collect.jl b/scripts/compat-prune/collect.jl new file mode 100644 index 0000000..0a7bd92 --- /dev/null +++ b/scripts/compat-prune/collect.jl @@ -0,0 +1,55 @@ +# Collect the audited files of every package. +# +# usage: julia collect.jl '' +# +# The audited version of each package is the one the joint environment (all packages +# together) selects on the running Julia, i.e. what a user of the root package gets. Its +# files are read from the registered tree (manifest `git-tree-sha1`), not from a tag or a +# branch: default branches may have moved on, or a version may have been registered from +# another branch. The same files are also read from the default branch, to detect drift +# before editing anything. +# +# Output: +# /index.toml name => uuid, version, repo, tree, default_branch +# /registered// files of the registered tree +# /default// files of the default branch + +include("common.jl") + +const FILES = ("Project.toml", "test/Project.toml", "docs/Project.toml") + +"Download the audited files present in tree `treeish` of `repo` into `dest`; return their paths." +function fetch_files(repo, treeish, dest) + jq = ".tree[] | select(.type == \"blob\") | .path + \" \" + .sha" + blobs = Dict(Tuple(split(l)) for l in split(gh_jq("repos/$repo/git/trees/$treeish?recursive=1", jq), '\n')) + found = String[] + for f in FILES + haskey(blobs, f) || continue + path = joinpath(dest, f) + mkpath(dirname(path)) + write(path, gh_raw("repos/$repo/git/blobs/$(blobs[f])")) + push!(found, f) + end + return found +end + +packages = parse_list(ARGS[1]) +out = ARGS[2] + +lookup = Dict(n => registry_lookup(n) for n in packages) +joint = resolve_env(Dict(n => lookup[n][1] for n in packages), Dict()) +joint["ok"] || error("The joint environment does not resolve on Julia $VERSION:\n$(joint["error"])") +versions = manifest_versions(joint["manifest"]) +trees = manifest_trees(joint["manifest"]) + +index = Dict{String,Any}() +for n in packages + uuid, repo = lookup[n] + branch = gh_jq("repos/$repo", ".default_branch") + registered = fetch_files(repo, trees[n], joinpath(out, "registered", n)) + default = fetch_files(repo, branch, joinpath(out, "default", n)) + index[n] = Dict("uuid" => uuid, "version" => versions[n], "repo" => repo, "tree" => trees[n], + "default_branch" => branch, "files" => registered, "default_files" => default) + println(rpad(n, 16), rpad(versions[n], 14), repo, " [", join(registered, ", "), "]") +end +open(io -> TOML.print(io, Dict("julia" => string(VERSION), "packages" => index)), joinpath(out, "index.toml"), "w") diff --git a/scripts/compat-prune/common.jl b/scripts/compat-prune/common.jl new file mode 100644 index 0000000..6ff5d33 --- /dev/null +++ b/scripts/compat-prune/common.jl @@ -0,0 +1,155 @@ +# Helpers shared by the compat-prune scripts. Stdlib only. + +using Pkg, TOML + +# Resolution only: never precompile the throwaway environments. +ENV["JULIA_PKG_PRECOMPILE_AUTO"] = "0" +# Every resolution of an audit must see the same registry snapshot, or a release published +# meanwhile shows up as a difference: registries are updated once, by `audit.jl`, before +# anything else, and never again. +Pkg.offline(true) + +"Run `gh` with `args` and return its standard output." +gh(args...) = read(`gh $args`, String) + +"`gh api` call returning the raw body (blobs, file contents)." +gh_raw(path) = gh("api", path, "-H", "Accept: application/vnd.github.raw") + +"`gh api` call returning one field extracted with `--jq`." +gh_jq(path, jq) = strip(gh("api", path, "--jq", jq)) + +""" + resolve_env(deps, compat) -> Dict + +Resolve a throwaway environment whose `[deps]` are `deps` (name => uuid) and whose +`[compat]` is `compat` (name => specifier), on the running Julia. Returns +`Dict("ok" => true, "manifest" => …)` with the parsed `Manifest.toml`, or +`Dict("ok" => false, "error" => …)` when the requirements are unsatisfiable. +""" +function resolve_env(deps::AbstractDict, compat::AbstractDict) + dir = mktempdir() + open(joinpath(dir, "Project.toml"), "w") do io + TOML.print(io, Dict("deps" => deps, "compat" => compat)) + end + current = Base.active_project() + try + Pkg.activate(dir; io=devnull) + Pkg.resolve(; io=devnull) + return Dict("ok" => true, "manifest" => TOML.parsefile(joinpath(dir, "Manifest.toml"))) + catch e + # Only an unsatisfiable resolution is a result; anything else is a bug of the caller. + e isa Pkg.Resolve.ResolverError || rethrow() + return Dict("ok" => false, "error" => sprint(showerror, e)) + finally + Pkg.activate(current; io=devnull) + rm(dir; recursive=true, force=true) + end +end + +"Map name => version string for every entry of a parsed manifest (stdlibs: `\"stdlib\"`)." +manifest_versions(m) = Dict(k => get(v[1], "version", "stdlib") for (k, v) in m["deps"]) + +"Map name => git-tree-sha1 for every registered entry of a parsed manifest." +manifest_trees(m) = + Dict(k => v[1]["git-tree-sha1"] for (k, v) in m["deps"] if haskey(v[1], "git-tree-sha1")) + +""" + registry_lookup(name) -> (uuid, repo) + +UUID and GitHub `owner/repo` of a registered package, read from the first reachable registry +that knows it. +""" +function registry_lookup(name) + for reg in Pkg.Registry.reachable_registries() + uuids = Pkg.Registry.uuids_from_name(reg, name) + isempty(uuids) && continue + entry = reg[uuids[1]] + # Signature changed in Julia 1.13: registry_info(entry) → registry_info(reg, entry). + info = applicable(Pkg.Registry.registry_info, reg, entry) ? + Pkg.Registry.registry_info(reg, entry) : Pkg.Registry.registry_info(entry) + m = match(r"github\.com[:/]([^/]+/[^/]+?)(?:\.git)?$", info.repo) + m === nothing && error("$name: repository $(info.repo) is not on GitHub") + return (string(uuids[1]), String(m[1])) + end + error("$name is not in any reachable registry") +end + +const spec = Pkg.Types.semver_spec + +"Render a VersionSpec back into a compat string (bounds carry no prerelease, so `-` only separates)." +render(vs) = join([replace(string(r), r"\s*-\s*" => " - ") for r in vs.ranges], ", ") + +""" + applies(owner, file, name, versions, audited) -> Bool + +Does the compat entry of `owner` read from `file` constrain environment `name`, whose +resolved versions are `versions`? An entry of `Project.toml` applies wherever `owner` is +resolved at its audited version (another version carries other entries); an entry of +`test/` or `docs/Project.toml` applies only in that very environment. +""" +function applies(owner, file, name, versions, audited) + file == "Project.toml" && return get(versions, owner, nothing) == audited[owner] + return name == "$owner/$(first(splitpath(file)))" +end + +""" + simulate(name, env, narrowings, uuids) -> Dict + +Resolve environment `name` (an entry of `envs.toml`) with each `dep => specifier` of +`narrowings` intersected into its compat. Pkg intersects the compat of every package of an +environment, so this is exactly what dropping specifiers from an entry that applies here +would do, without touching any file. Returns `Dict("ok" => true, "versions" => …)` or +`Dict("ok" => false, "error" => …)`; an empty intersection or a pinned package resolved at +another version than the audited one are failures too. +""" +function simulate(name, env, narrowings, uuids) + deps, compat = copy(env["deps"]), copy(env["compat"]) + for (dep, s) in narrowings + compat[dep] = render(spec(get(compat, dep, ">= 0")) ∩ spec(s)) + deps[dep] = uuids[dep] # a transitive dep must be listed to carry a compat entry + end + empty = sort([k for (k, v) in compat if isempty(v)]) + isempty(empty) || return Dict("ok" => false, "error" => "empty compat intersection for $(join(empty, ", "))") + res = resolve_env(deps, compat) + res["ok"] || return res + versions = manifest_versions(res["manifest"]) + off = ["$k $(versions[k]) (audited $v)" for (k, v) in env["pins"] if versions[k] != v] + isempty(off) || return Dict("ok" => false, "error" => "pinned versions not selected: $(join(off, ", "))") + return Dict("ok" => true, "versions" => versions) +end + +""" + narrowings(prunes, name, versions, audited) -> Dict + +The `dep => kept specifiers` that `prunes` impose on environment `name` (baseline +`versions`): intersection over every prune whose entry applies here and whose dep is present. +""" +function narrowings(prunes, name, versions, audited) + out = Dict{String,String}() + for p in prunes + haskey(versions, p["dep"]) && applies(p["owner"], p["file"], name, versions, audited) || continue + out[p["dep"]] = haskey(out, p["dep"]) ? render(spec(out[p["dep"]]) ∩ spec(p["after"])) : p["after"] + end + return out +end + +""" + load_collected(root) -> (envs, audited, uuids) + +`envs.toml`, the audited versions, and the uuid of every package named in an audited file +(`[deps]`, `[weakdeps]`, `[extras]`): a pruned dep is always declared by its owner, even +when it is only a transitive dependency of the environments. +""" +function load_collected(root) + envs = TOML.parsefile(joinpath(root, "envs.toml"))["env"] + audited = Dict(n => p["version"] for (n, p) in TOML.parsefile(joinpath(root, "index.toml"))["packages"]) + uuids = Dict{String,Any}() + for (dir, _, files) in walkdir(joinpath(root, "registered")), f in files + t = TOML.parsefile(joinpath(dir, f)) + for s in ("deps", "weakdeps", "extras"); merge!(uuids, get(t, s, Dict())); end + end + return envs, audited, uuids +end + +"Parse a JSON list of strings such as `[\"a\",\"b\"]` (inputs of the reusable workflow)." +parse_list(s) = String[m[1] for m in eachmatch(r"\"([^\"]+)\"", s)] diff --git a/scripts/compat-prune/envs.jl b/scripts/compat-prune/envs.jl new file mode 100644 index 0000000..0040031 --- /dev/null +++ b/scripts/compat-prune/envs.jl @@ -0,0 +1,66 @@ +# Build the audited environments from the collected files. +# +# usage: julia envs.jl (writes /envs.toml) +# +# Environments: +# joint every package together (what a user of the root package resolves) +# /test the test environment of a package: `test/Project.toml` when the package has +# one, else the package plus its `[targets] test` with the package's own compat +# /docs `docs/Project.toml` plus the package itself (developed by `docs/make.jl`) +# +# The package that owns an environment (every package, for `joint`) is pinned to its audited +# version, so that the files read are exactly the ones resolved. Other packages float within +# the environment's own compat: `CTParser/test` may well require an older `CTModels`. +# Compat cannot name a prerelease (`=1.1.8-beta` does not parse), but `=1.1.8` admits +# `1.1.8-beta`: the pin drops the suffix and `resolve.jl` checks that the audited version is +# the one selected. + +include("common.jl") + +"Keep the compat entries that apply to `deps` (plus `julia`)." +restrict(compat, deps) = Dict(k => v for (k, v) in compat if k == "julia" || haskey(deps, k)) + +function test_env(name, uuid, dir) + file = joinpath(dir, "test", "Project.toml") + if isfile(file) + t = TOML.parsefile(file) + deps = merge(Dict(name => uuid), get(t, "deps", Dict())) + return deps, restrict(get(t, "compat", Dict()), deps) + end + t = TOML.parsefile(joinpath(dir, "Project.toml")) + known = merge(get(t, "deps", Dict()), get(t, "weakdeps", Dict()), get(t, "extras", Dict())) + targets = get(get(t, "targets", Dict()), "test", collect(keys(get(t, "extras", Dict())))) + deps = merge(Dict(name => uuid), Dict(k => known[k] for k in targets)) + return deps, restrict(get(t, "compat", Dict()), deps) +end + +function docs_env(name, uuid, dir) + file = joinpath(dir, "docs", "Project.toml") + isfile(file) || return nothing + t = TOML.parsefile(file) + deps = merge(Dict(name => uuid), get(t, "deps", Dict())) + return deps, restrict(get(t, "compat", Dict()), deps) +end + +root = ARGS[1] +index = TOML.parsefile(joinpath(root, "index.toml"))["packages"] +pins = Dict(n => VersionNumber(p["version"]) for (n, p) in index) + +envs = Dict{String,Any}() +envs["joint"] = (Dict(n => p["uuid"] for (n, p) in index), Dict{String,Any}()) +for (n, p) in index + dir = joinpath(root, "registered", n) + envs["$n/test"] = test_env(n, p["uuid"], dir) + docs = docs_env(n, p["uuid"], dir) + docs === nothing || (envs["$n/docs"] = docs) +end + +out = Dict{String,Any}() +for (name, (deps, compat)) in envs + owner, kind = name == "joint" ? ("", "joint") : split(name, "/") + pinned = Dict(string(k) => string(pins[k]) for k in (kind == "joint" ? keys(pins) : (owner,))) + compat = merge(compat, Dict(k => (v = VersionNumber(v); "=$(v.major).$(v.minor).$(v.patch)") for (k, v) in pinned)) + out[name] = Dict("owner" => owner, "kind" => kind, "deps" => deps, "compat" => compat, "pins" => pinned) +end +open(io -> TOML.print(io, Dict("env" => out)), joinpath(root, "envs.toml"), "w") +println(length(out), " environments: ", join(sort(collect(keys(out))), ", ")) diff --git a/scripts/compat-prune/resolve.jl b/scripts/compat-prune/resolve.jl new file mode 100644 index 0000000..85c826a --- /dev/null +++ b/scripts/compat-prune/resolve.jl @@ -0,0 +1,27 @@ +# Resolve every audited environment on the running Julia. +# +# usage: julia resolve.jl [ ] +# +# Without prunes this is the baseline. With prunes, they are simulated all together (see +# `simulate` and `narrowings` in common.jl); environments that failed at baseline are copied +# as they are, there is nothing to compare them with. + +include("common.jl") + +root, outfile = ARGS[1], ARGS[2] +prunes = length(ARGS) ≥ 4 ? TOML.parsefile(ARGS[3])["prune"] : [] +baseline = length(ARGS) ≥ 4 ? TOML.parsefile(ARGS[4])["env"] : Dict() +envs, audited, uuids = load_collected(root) + +results = Dict{String,Any}() +for name in sort(collect(keys(envs))) + if isempty(prunes) + r = simulate(name, envs[name], Dict(), uuids) + else + base = baseline[name] + r = base["ok"] ? simulate(name, envs[name], narrowings(prunes, name, base["versions"], audited), uuids) : base + end + results[name] = r + println(VERSION, " ", rpad(name, 24), r["ok"] ? "ok" : "UNSAT") +end +open(io -> TOML.print(io, Dict("julia" => string(VERSION), "env" => results)), outfile, "w") diff --git a/scripts/compat-prune/test/runtests.jl b/scripts/compat-prune/test/runtests.jl new file mode 100644 index 0000000..4b3ddff --- /dev/null +++ b/scripts/compat-prune/test/runtests.jl @@ -0,0 +1,144 @@ +# Unit tests of the pure logic of compat-prune (no resolution, no network). +# +# usage: julia scripts/compat-prune/test/runtests.jl + +using Test + +include(joinpath(@__DIR__, "..", "classify.jl")) +include(joinpath(@__DIR__, "..", "validate.jl")) + +# Baseline results keyed "@", as classify.jl builds them. +ok(versions...) = Dict("ok" => true, "versions" => Dict(versions...)) + +const AUDITED = Dict("CTSolvers" => "0.5.8", "CTParser" => "0.9.5-beta", "CTBase" => "0.30.9") + +# Captured from real runs (2026-10-15): DocStringExtensions restricted to 0.10. +const CONFLICT_ON_DEP = """ +Unsatisfiable requirements detected for package DocStringExtensions [ffbed154]: + DocStringExtensions [ffbed154] log: + ├─possible versions are: 0.9.3 - 0.10.0 or uninstalled + ├─restricted to versions 0.10 by project [4e53ef5c], leaving only versions: 0.10.0 + │ └─project [4e53ef5c] log: + │ ├─possible versions are: 0.0.0 or uninstalled + │ └─project [4e53ef5c] is fixed to version 0.0.0 + └─restricted by compatibility requirements with SciMLBase [0bca4576] to versions: 0.9.5 — no versions left + └─SciMLBase [0bca4576] log: + ├─possible versions are: 2.77.2 - 3.58.0 or uninstalled + └─restricted by compatibility requirements with Deeper [00000000] to versions: 3 +""" +const CONFLICT_ON_OTHER = """ +Unsatisfiable requirements detected for package DocumenterVitepress [4710194d]: + DocumenterVitepress [4710194d] log: + ├─possible versions are: 0.3.4 - 0.3.8 or uninstalled + └─restricted by compatibility requirements with DocStringExtensions [ffbed154] to versions: uninstalled — no versions left + └─DocStringExtensions [ffbed154] log: + └─restricted to versions 0.10 by project [064cce67], leaving only versions: 0.10.0 +""" + +@testset "compat-prune" begin + @testset "specifiers" begin + @test specifiers("0.9, 0.11,0.12") == ["0.9", "0.11", "0.12"] + @test lower("0.7 - 0.8") == v"0.7" + @test lower("^1.2") == v"1.2" + @test lower("=1.1.8") == v"1.1.8" + @test render(spec("0.6, 0.7, 0.8") ∩ spec("0.7, 0.8")) == "0.7 - 0.8" + @test render(spec("0.9, 0.11, 0.12") ∩ spec("0.9, 0.12")) == "0.9, 0.12" + @test isempty(render(spec("0.7") ∩ spec("0.8"))) + end + + @testset "applies" begin + v = Dict("CTSolvers" => "0.5.8") + @test applies("CTSolvers", "Project.toml", "OptimalControl/test", v, AUDITED) + # another version of the owner carries other compat entries + @test !applies("CTSolvers", "Project.toml", "X/test", Dict("CTSolvers" => "0.4.0"), AUDITED) + @test !applies("CTSolvers", "Project.toml", "X/test", Dict{String,String}(), AUDITED) + # test/ and docs/ entries apply in their own environment only + @test applies("CTParser", "test/Project.toml", "CTParser/test", v, AUDITED) + @test !applies("CTParser", "test/Project.toml", "CTParser/docs", v, AUDITED) + @test !applies("CTParser", "docs/Project.toml", "OptimalControl/docs", v, AUDITED) + end + + @testset "owner scoping keeps CTSolvers' CUDSS 0.7" begin + # OptimalControl's tests need CUDSS 0.7, CTDirect's take 0.8: both are CTSolvers users. + results = Dict( + "OptimalControl/test@1.12" => ok("CTSolvers" => "0.5.8", "CUDSS" => "0.7.0"), + "CTDirect/test@1.12" => ok("CTSolvers" => "0.5.8", "CUDSS" => "0.8.1"), + # an older CTSolvers selecting CUDSS 0.6 is not evidence for the audited file + "Old/test@1.12" => ok("CTSolvers" => "0.4.0", "CUDSS" => "0.6.3"), + ) + r = classify_entry("CTSolvers", "Project.toml", "CUDSS", "0.6, 0.7, 0.8", results, AUDITED) + @test r["status"] == "prune" + @test r["removed"] == ["0.6"] + @test r["after"] == "0.7, 0.8" + @test r["evidence"]["0.7"] == ["OptimalControl/test@1.12=0.7.0"] + end + + @testset "a lane used on Julia 1.10 only is kept" begin + results = Dict( + "CTBase/test@1.10" => ok("CTBase" => "0.30.9", "JET" => "0.9.18"), + "CTBase/test@1.12" => ok("CTBase" => "0.30.9", "JET" => "0.12.3"), + ) + r = classify_entry("CTBase", "Project.toml", "JET", "0.9, 0.11, 0.12", results, AUDITED) + @test r["status"] == "prune" + @test r["after"] == "0.9, 0.12" + end + + @testset "blocked, idle, keep" begin + results = Dict("CTParser/test@1.12" => ok("CTParser" => "0.9.5-beta", "CTBase" => "0.29.3", "CUDSS" => "0.7.0")) + r = classify_entry("CTParser", "test/Project.toml", "CTBase", "0.29, 0.30", results, AUDITED) + @test r["status"] == "blocked" + @test r["newest"] == "0.30" + @test r["applies_in"] == ["CTParser/test"] + @test !haskey(r, "prunable") + # blocked, with an obsolete lane below the one in use + r = classify_entry("CTParser", "test/Project.toml", "CUDSS", "0.6, 0.7, 0.8", results, AUDITED) + @test r["status"] == "blocked" + @test r["prunable"] == ["0.6"] + # resolved nowhere the entry applies + r = classify_entry("CTParser", "test/Project.toml", "CUDA", "5, 6", results, AUDITED) + @test r["status"] == "idle" + # every lane in use + results = Dict("CTParser/test@1.10" => ok("CTParser" => "0.9.5-beta", "JET" => "0.9.18"), + "CTParser/test@1.12" => ok("CTParser" => "0.9.5-beta", "JET" => "0.12.3")) + r = classify_entry("CTParser", "test/Project.toml", "JET", "0.9, 0.12", results, AUDITED) + @test r["status"] == "keep" + end + + @testset "a failed baseline withholds every prune" begin + # On its own, the entry would lose 0.9: the 1.10 environment that uses it failed. + results = Dict("CTBase/test@1.10" => Dict("ok" => false, "error" => "unsatisfiable"), + "CTBase/test@1.12" => ok("CTBase" => "0.30.9", "JET" => "0.12.3")) + r = classify_entry("CTBase", "Project.toml", "JET", "0.9, 0.12", results, AUDITED) + @test r["status"] == "prune" + kept, withheld = withhold([r], results) + @test isempty(kept) + @test only(withheld)["status"] == "withheld" + @test occursin("CTBase/test@1.10", only(withheld)["reason"]) + # all baselines resolved: nothing withheld + kept, withheld = withhold([r], Dict("CTBase/test@1.12" => results["CTBase/test@1.12"])) + @test kept == [r] && isempty(withheld) + end + + @testset "narrowings" begin + prunes = [Dict("owner" => "CTSolvers", "file" => "Project.toml", "dep" => "CUDSS", "after" => "0.7, 0.8"), + Dict("owner" => "CTSolvers", "file" => "docs/Project.toml", "dep" => "CUDSS", "after" => "0.8"), + Dict("owner" => "CTBase", "file" => "Project.toml", "dep" => "JET", "after" => "0.9, 0.12")] + v = Dict("CTSolvers" => "0.5.8", "CUDSS" => "0.8.1", "CTBase" => "0.30.9") + # JET absent: nothing to bind; the docs entry intersects with the root one + @test narrowings(prunes, "CTSolvers/docs", v, AUDITED) == Dict("CUDSS" => "0.8") + @test narrowings(prunes, "CTDirect/test", v, AUDITED) == Dict("CUDSS" => "0.7, 0.8") + @test isempty(narrowings(prunes, "CTDirect/test", Dict("CUDSS" => "0.8.1"), AUDITED)) + end + + @testset "validate helpers" begin + @test blockers(CONFLICT_ON_DEP, "DocStringExtensions") == ["SciMLBase"] + @test blockers(CONFLICT_ON_OTHER, "DocStringExtensions") == ["DocumenterVitepress"] + @test isempty(blockers("empty compat intersection for CUDSS", "CUDSS")) + base = Dict("A" => "1.0.0", "B" => "2.0.0") + @test departure(Dict("ok" => true, "versions" => copy(base)), base) === nothing + @test departure(Dict("ok" => true, "versions" => Dict("A" => "1.0.0", "B" => "2.1.0")), base) == + "manifest differs: B 2.0.0 → 2.1.0" + @test departure(Dict("ok" => false, "error" => "empty compat intersection for CUDSS"), base) == + "empty compat intersection for CUDSS" + end +end diff --git a/scripts/compat-prune/validate.jl b/scripts/compat-prune/validate.jl new file mode 100644 index 0000000..0820752 --- /dev/null +++ b/scripts/compat-prune/validate.jl @@ -0,0 +1,98 @@ +# Validate the prunes and identify the blockers, on the running Julia. +# +# usage: julia validate.jl [] +# +# Prunes: all of them (minus those already rejected) are simulated together; an environment +# whose manifest differs from the baseline, or that no longer resolves, is a failure. The +# prunes that apply in a failing environment are then simulated one by one there: those that +# fail alone are rejected; if none does, the failure comes from their combination and all of +# them are rejected. `audit.jl` repeats until no Julia version rejects anything. +# +# Blockers: for each blocked entry, each environment where it applies is resolved with the +# dependency restricted to the newest specifier; the resolver's explanation names the +# packages that cap it. + +include("common.jl") + +"Short description of how `r` departs from the baseline versions `base`." +function departure(r, base) + r["ok"] || return first(r["error"], 600) + d = sort(["$k $(get(base, k, "—")) → $(get(r["versions"], k, "—"))" + for k in union(keys(base), keys(r["versions"])) if get(base, k, "") != get(r["versions"], k, "")]) + isempty(d) ? nothing : "manifest differs: " * join(first(d, 6), ", ") * (length(d) > 6 ? ", …" : "") +end + +""" + blockers(msg, dep) -> Vector{String} + +Packages that cap `dep`, read from the resolver's error when `dep` is restricted to its newest +specifier. The resolver reports the first conflict it meets, not every one: when that conflict +is on another package, that package is the blocker (its compat excludes the newest `dep`); +when it is on `dep` itself, the blockers are the first-level `restricted by compatibility +requirements with …` lines of its log (deeper lines explain those blockers, they are not +blockers of `dep`). +""" +function blockers(msg, dep) + head = match(r"Unsatisfiable requirements detected for package (\S+) \[", msg) + head === nothing && return String[] + head[1] == dep || return [String(head[1])] + first_level = r"^ [├└]─restricted by compatibility requirements with (\S+) \["m + return sort(unique(String[m[1] for m in eachmatch(first_level, msg)])) +end + +key(p) = (p["owner"], p["file"], p["dep"]) + +if abspath(PROGRAM_FILE) == @__FILE__ + root, classified, basefile, outfile = ARGS[1:4] + rejected_before = length(ARGS) ≥ 5 ? Set(key.(get(TOML.parsefile(ARGS[5]), "rejected", []))) : Set() + envs, audited, uuids = load_collected(root) + c = TOML.parsefile(classified) + prunes = [p for p in c["prune"] if !(key(p) in rejected_before)] + baseline = TOML.parsefile(basefile)["env"] + julia = join(split(string(VERSION), ".")[1:2], ".") + + rejected = Dict{String,Any}[] + for name in sort(collect(keys(envs))) + base = baseline[name] + base["ok"] || continue + n = narrowings(prunes, name, base["versions"], audited) + isempty(n) && continue + why = departure(simulate(name, envs[name], n, uuids), base["versions"]) + why === nothing && continue + println(VERSION, " ", rpad(name, 24), "fails together: ", why) + suspects = [p for p in prunes if !isempty(narrowings([p], name, base["versions"], audited))] + alone = Dict{Any,String}() + for p in suspects + w = departure(simulate(name, envs[name], narrowings([p], name, base["versions"], audited), uuids), base["versions"]) + w === nothing || (alone[key(p)] = w) + end + culprits = isempty(alone) ? suspects : [p for p in suspects if haskey(alone, key(p))] + for p in culprits + reason = get(alone, key(p), "fails only combined with other prunes: $why") + push!(rejected, Dict("owner" => p["owner"], "file" => p["file"], "dep" => p["dep"], + "where" => "$name@$julia", "reason" => reason)) + println(" rejected: $(p["owner"]) $(p["file"]) $(p["dep"]) → $(p["after"])") + end + rejected_now = Set(key.(culprits)) + filter!(p -> !(key(p) in rejected_now), prunes) + end + + found = Dict{String,Any}[] + for b in get(c, "other", []) + b["status"] == "blocked" || continue + for name in b["applies_in"] + base = baseline[name] + base["ok"] && haskey(base["versions"], b["dep"]) || continue + r = simulate(name, envs[name], Dict(b["dep"] => b["newest"]), uuids) + push!(found, Dict("owner" => b["owner"], "file" => b["file"], "dep" => b["dep"], + "newest" => b["newest"], "where" => "$name@$julia", + "blockers" => r["ok"] ? String[] : blockers(r["error"], b["dep"]), + "detail" => r["ok"] ? "resolves, but the resolver prefers other versions" : first(r["error"], 2000))) + println(VERSION, " blocked: $(b["owner"]) $(b["file"]) $(b["dep"]) $(b["newest"]) in $name, by ", + r["ok"] ? "nothing (resolver preference)" : join(found[end]["blockers"], ", ")) + end + end + + open(io -> TOML.print(io, Dict("julia" => string(VERSION), "rejected" => rejected, "blocked" => found)), outfile, "w") + println(VERSION, " ", length(rejected), " prune(s) rejected, ", length(found), " blocker probe(s)") +end From 36bef06479a2fd541d6f41c549520901fe4d3956 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Thu, 15 Oct 2026 23:05:12 +0200 Subject: [PATCH 2/8] feat(compat-prune): report, textual edits, test suite split per script - 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 --- scripts/compat-prune/audit.jl | 22 +- scripts/compat-prune/classify.jl | 33 +- scripts/compat-prune/collect.jl | 49 +- scripts/compat-prune/common.jl | 58 +- scripts/compat-prune/edit.jl | 98 +++ scripts/compat-prune/envs.jl | 47 +- scripts/compat-prune/report.jl | 107 +++ scripts/compat-prune/resolve.jl | 39 +- .../collected/default/Alpha/Project.toml | 12 + .../collected/default/Alpha/docs/Project.toml | 5 + .../collected/default/Beta/Project.toml | 24 + .../test/fixtures/collected/index.toml | 19 + .../collected/registered/Alpha/Project.toml | 12 + .../registered/Alpha/docs/Project.toml | 5 + .../registered/Alpha/test/Project.toml | 7 + .../collected/registered/Beta/Project.toml | 24 + .../snapshot-2026-10-15/baseline-1.10.toml | 699 ++++++++++++++++++ .../snapshot-2026-10-15/baseline-1.12.toml | 699 ++++++++++++++++++ .../snapshot-2026-10-15/baseline-1.13.toml | 699 ++++++++++++++++++ .../default/CTBase/Project.toml | 64 ++ .../default/CTBase/docs/Project.toml | 28 + .../default/CTDirect/Project.toml | 58 ++ .../default/CTDirect/docs/Project.toml | 13 + .../default/CTFlows/Project.toml | 76 ++ .../default/CTFlows/docs/Project.toml | 38 + .../default/CTLie/Project.toml | 38 + .../default/CTLie/docs/Project.toml | 22 + .../default/CTModels/Project.toml | 61 ++ .../default/CTModels/docs/Project.toml | 26 + .../default/CTParser/Project.toml | 21 + .../default/CTParser/docs/Project.toml | 12 + .../default/CTParser/test/Project.toml | 32 + .../default/CTSolvers/Project.toml | 107 +++ .../default/CTSolvers/docs/Project.toml | 59 ++ .../default/OptimalControl/Project.toml | 80 ++ .../default/OptimalControl/docs/Project.toml | 73 ++ .../fixtures/snapshot-2026-10-15/index.toml | 73 ++ .../registered/CTBase/Project.toml | 64 ++ .../registered/CTBase/docs/Project.toml | 28 + .../registered/CTDirect/Project.toml | 58 ++ .../registered/CTDirect/docs/Project.toml | 13 + .../registered/CTFlows/Project.toml | 76 ++ .../registered/CTFlows/docs/Project.toml | 38 + .../registered/CTLie/Project.toml | 38 + .../registered/CTLie/docs/Project.toml | 22 + .../registered/CTModels/Project.toml | 61 ++ .../registered/CTModels/docs/Project.toml | 26 + .../registered/CTParser/Project.toml | 30 + .../registered/CTParser/docs/Project.toml | 12 + .../registered/CTParser/test/Project.toml | 34 + .../registered/CTSolvers/Project.toml | 107 +++ .../registered/CTSolvers/docs/Project.toml | 56 ++ .../registered/OptimalControl/Project.toml | 80 ++ .../OptimalControl/docs/Project.toml | 72 ++ .../test/golden/classified-2026-10-15.txt | 45 ++ .../test/golden/edits-2026-10-15.txt | 42 ++ .../test/golden/envs-2026-10-15.txt | 17 + scripts/compat-prune/test/golden/report.md | 78 ++ scripts/compat-prune/test/helpers.jl | 88 +++ scripts/compat-prune/test/runtests.jl | 156 +--- scripts/compat-prune/test/test_classify.jl | 141 ++++ scripts/compat-prune/test/test_collect.jl | 91 +++ scripts/compat-prune/test/test_common.jl | 104 +++ scripts/compat-prune/test/test_edit.jl | 153 ++++ scripts/compat-prune/test/test_envs.jl | 69 ++ scripts/compat-prune/test/test_integration.jl | 206 ++++++ scripts/compat-prune/test/test_regression.jl | 66 ++ scripts/compat-prune/test/test_report.jl | 91 +++ scripts/compat-prune/test/test_validate.jl | 107 +++ scripts/compat-prune/validate.jl | 91 ++- 70 files changed, 5631 insertions(+), 268 deletions(-) create mode 100644 scripts/compat-prune/edit.jl create mode 100644 scripts/compat-prune/report.jl create mode 100644 scripts/compat-prune/test/fixtures/collected/default/Alpha/Project.toml create mode 100644 scripts/compat-prune/test/fixtures/collected/default/Alpha/docs/Project.toml create mode 100644 scripts/compat-prune/test/fixtures/collected/default/Beta/Project.toml create mode 100644 scripts/compat-prune/test/fixtures/collected/index.toml create mode 100644 scripts/compat-prune/test/fixtures/collected/registered/Alpha/Project.toml create mode 100644 scripts/compat-prune/test/fixtures/collected/registered/Alpha/docs/Project.toml create mode 100644 scripts/compat-prune/test/fixtures/collected/registered/Alpha/test/Project.toml create mode 100644 scripts/compat-prune/test/fixtures/collected/registered/Beta/Project.toml create mode 100644 scripts/compat-prune/test/fixtures/snapshot-2026-10-15/baseline-1.10.toml create mode 100644 scripts/compat-prune/test/fixtures/snapshot-2026-10-15/baseline-1.12.toml create mode 100644 scripts/compat-prune/test/fixtures/snapshot-2026-10-15/baseline-1.13.toml create mode 100644 scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTBase/Project.toml create mode 100644 scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTBase/docs/Project.toml create mode 100644 scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTDirect/Project.toml create mode 100644 scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTDirect/docs/Project.toml create mode 100644 scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTFlows/Project.toml create mode 100644 scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTFlows/docs/Project.toml create mode 100644 scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTLie/Project.toml create mode 100644 scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTLie/docs/Project.toml create mode 100644 scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTModels/Project.toml create mode 100644 scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTModels/docs/Project.toml create mode 100644 scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTParser/Project.toml create mode 100644 scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTParser/docs/Project.toml create mode 100644 scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTParser/test/Project.toml create mode 100644 scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTSolvers/Project.toml create mode 100644 scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTSolvers/docs/Project.toml create mode 100644 scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/OptimalControl/Project.toml create mode 100644 scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/OptimalControl/docs/Project.toml create mode 100644 scripts/compat-prune/test/fixtures/snapshot-2026-10-15/index.toml create mode 100644 scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTBase/Project.toml create mode 100644 scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTBase/docs/Project.toml create mode 100644 scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTDirect/Project.toml create mode 100644 scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTDirect/docs/Project.toml create mode 100644 scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTFlows/Project.toml create mode 100644 scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTFlows/docs/Project.toml create mode 100644 scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTLie/Project.toml create mode 100644 scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTLie/docs/Project.toml create mode 100644 scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTModels/Project.toml create mode 100644 scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTModels/docs/Project.toml create mode 100644 scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTParser/Project.toml create mode 100644 scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTParser/docs/Project.toml create mode 100644 scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTParser/test/Project.toml create mode 100644 scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTSolvers/Project.toml create mode 100644 scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTSolvers/docs/Project.toml create mode 100644 scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/OptimalControl/Project.toml create mode 100644 scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/OptimalControl/docs/Project.toml create mode 100644 scripts/compat-prune/test/golden/classified-2026-10-15.txt create mode 100644 scripts/compat-prune/test/golden/edits-2026-10-15.txt create mode 100644 scripts/compat-prune/test/golden/envs-2026-10-15.txt create mode 100644 scripts/compat-prune/test/golden/report.md create mode 100644 scripts/compat-prune/test/helpers.jl create mode 100644 scripts/compat-prune/test/test_classify.jl create mode 100644 scripts/compat-prune/test/test_collect.jl create mode 100644 scripts/compat-prune/test/test_common.jl create mode 100644 scripts/compat-prune/test/test_edit.jl create mode 100644 scripts/compat-prune/test/test_envs.jl create mode 100644 scripts/compat-prune/test/test_integration.jl create mode 100644 scripts/compat-prune/test/test_regression.jl create mode 100644 scripts/compat-prune/test/test_report.jl create mode 100644 scripts/compat-prune/test/test_validate.jl diff --git a/scripts/compat-prune/audit.jl b/scripts/compat-prune/audit.jl index 9e6f42a..fc69e77 100644 --- a/scripts/compat-prune/audit.jl +++ b/scripts/compat-prune/audit.jl @@ -3,7 +3,7 @@ # usage: julia audit.jl '' '' # # Each Julia version is run as `julia +` (juliaup). Steps: -# 1. update the registries once; every later step runs offline on this snapshot +# 1. update the registries once; no later step updates them (see `resolve_env`) # 2. collect.jl and envs.jl, on the last (newest) Julia version # 3. resolve.jl: the baseline of every environment, on each Julia version # 4. classify.jl, from all baselines @@ -16,7 +16,7 @@ using Dates, Pkg, TOML const HERE = @__DIR__ packages, versions, out = ARGS[1], ARGS[2], abspath(ARGS[3]) -include(joinpath(HERE, "common.jl")) # for parse_list only; Pkg is offline from here on +include(joinpath(HERE, "common.jl")) # for parse_list jlv = parse_list(versions) short(v) = join(split(v, ".")[1:2], ".") @@ -26,9 +26,7 @@ function run_step(v, script, args...) run(cmd) end -Pkg.offline(false) Pkg.Registry.update() -Pkg.offline(true) mkpath(out) run_step(last(jlv), "collect.jl", packages, out) @@ -39,7 +37,6 @@ end run_step(last(jlv), "classify.jl", out, joinpath(out, "classified.toml"), (joinpath(out, "baseline-$(short(v)).toml") for v in jlv)...) -key(p) = (p["owner"], p["file"], p["dep"]) rejected = Dict{String,Any}[] blocked = Dict{String,Any}[] for round in 1:5 @@ -48,34 +45,35 @@ for round in 1:5 empty!(blocked) for v in jlv res = joinpath(out, "validated-$(short(v)).toml") + blockers = v == last(jlv) ? ("--blockers",) : () run_step(v, "validate.jl", out, joinpath(out, "classified.toml"), - joinpath(out, "baseline-$(short(v)).toml"), res, joinpath(out, "rejected.toml")) + joinpath(out, "baseline-$(short(v)).toml"), res, joinpath(out, "rejected.toml"), blockers...) r = TOML.parsefile(res) append!(new, r["rejected"]) append!(blocked, r["blocked"]) end - known = Set(key.(rejected)) - fresh = [p for p in new if !(key(p) in known)] + known = Set(entry_key.(rejected)) + fresh = [p for p in new if !(entry_key(p) in known)] isempty(fresh) && break round == 5 && error("validation did not settle after 5 rounds") # one record per prune, the first reason found for p in fresh - key(p) in known || (push!(rejected, p); push!(known, key(p))) + entry_key(p) in known || (push!(rejected, p); push!(known, entry_key(p))) end end c = TOML.parsefile(joinpath(out, "classified.toml")) -gone = Set(key.(rejected)) +gone = Set(entry_key.(rejected)) index = TOML.parsefile(joinpath(out, "index.toml")) audit = Dict( "date" => Dates.format(now(UTC), "yyyy-mm-dd HH:MM") * " UTC", "julia" => jlv, "packages" => index["packages"], - "prune" => [p for p in c["prune"] if !(key(p) in gone)], + "prune" => [p for p in c["prune"] if !(entry_key(p) in gone)], "rejected" => rejected, "blocked" => blocked, "other" => [r for r in c["other"] if r["status"] != "blocked"], ) open(io -> TOML.print(io, audit), joinpath(out, "audit.toml"), "w") println("\n", length(audit["prune"]), " prune(s) accepted, ", length(rejected), " rejected, ", - length(unique(key.(blocked))), " blocked entr(y/ies)") + length(unique(entry_key.(blocked))), " blocked entr(y/ies)") diff --git a/scripts/compat-prune/classify.jl b/scripts/compat-prune/classify.jl index 200bb82..2e654af 100644 --- a/scripts/compat-prune/classify.jl +++ b/scripts/compat-prune/classify.jl @@ -16,8 +16,6 @@ include("common.jl") -const FILES = ("Project.toml", "test/Project.toml", "docs/Project.toml") - "Specifiers of a compat string, in declaration order." specifiers(s) = String.(strip.(split(s, ","))) @@ -65,7 +63,7 @@ end "Every multi-specifier entry of the audited files, classified." function classify(root, results, audited) records = Dict{String,Any}[] - for owner in sort(collect(keys(audited))), file in FILES + for owner in sort(collect(keys(audited))), file in AUDITED_FILES path = joinpath(root, "registered", owner, file) isfile(path) || continue for (dep, compat) in sort(collect(get(TOML.parsefile(path), "compat", Dict()))) @@ -89,17 +87,26 @@ function withhold(prunes, results) return Dict{String,Any}[], [merge(p, Dict("status" => "withheld", "reason" => why)) for p in prunes] end -if abspath(PROGRAM_FILE) == @__FILE__ - root, outfile = ARGS[1], ARGS[2] - _, audited, _ = load_collected(root) +"Baseline results of every Julia version, keyed `\"@\"`." +function load_results(files) results = Dict{String,Any}() - for f in ARGS[3:end] + for f in files b = TOML.parsefile(f) j = join(split(b["julia"], ".")[1:2], ".") for (name, r) in b["env"]; results["$name@$j"] = r; end end + return results +end + +""" + classify_all(root, results) -> (prunes, others) + +Classify every entry; obsolete lanes of blocked entries become prunes of their own; every +prune is withheld when a baseline failed. +""" +function classify_all(root, results) + _, audited, _ = load_collected(root) records = classify(root, results, audited) - # A blocked entry may also carry obsolete lanes: they are prunes in their own right. prunes = [r for r in records if r["status"] == "prune"] for r in records r["status"] == "blocked" && haskey(r, "prunable") || continue @@ -109,9 +116,15 @@ if abspath(PROGRAM_FILE) == @__FILE__ end prunes, withheld = withhold(prunes, results) others = vcat([r for r in records if r["status"] != "prune"], withheld) + return prunes, others +end + +if abspath(PROGRAM_FILE) == @__FILE__ + root, outfile = ARGS[1], ARGS[2] + prunes, others = classify_all(root, load_results(ARGS[3:end])) open(io -> TOML.print(io, Dict("prune" => prunes, "other" => others)), outfile, "w") - for r in records + for r in sort(vcat(prunes, others); by=r -> (r["owner"], r["file"], r["dep"])) println(rpad("$(r["owner"]) $(r["file"])", 34), rpad(r["dep"], 26), rpad(r["before"], 18), r["status"], - haskey(r, "removed") ? " → $(r["after"])" : "", haskey(r, "prunable") ? " (prunable: $(join(r["prunable"], ", ")))" : "") + haskey(r, "after") ? " → $(r["after"])" : "") end end diff --git a/scripts/compat-prune/collect.jl b/scripts/compat-prune/collect.jl index 0a7bd92..31245c7 100644 --- a/scripts/compat-prune/collect.jl +++ b/scripts/compat-prune/collect.jl @@ -16,14 +16,13 @@ include("common.jl") -const FILES = ("Project.toml", "test/Project.toml", "docs/Project.toml") - "Download the audited files present in tree `treeish` of `repo` into `dest`; return their paths." function fetch_files(repo, treeish, dest) jq = ".tree[] | select(.type == \"blob\") | .path + \" \" + .sha" - blobs = Dict(Tuple(split(l)) for l in split(gh_jq("repos/$repo/git/trees/$treeish?recursive=1", jq), '\n')) + listing = gh_jq("repos/$repo/git/trees/$treeish?recursive=1", jq) + blobs = Dict(Tuple(split(l)) for l in split(listing, '\n') if !isempty(l)) found = String[] - for f in FILES + for f in AUDITED_FILES haskey(blobs, f) || continue path = joinpath(dest, f) mkpath(dirname(path)) @@ -33,23 +32,31 @@ function fetch_files(repo, treeish, dest) return found end -packages = parse_list(ARGS[1]) -out = ARGS[2] +""" + collect_files(out, lookup, versions, trees) -> Dict -lookup = Dict(n => registry_lookup(n) for n in packages) -joint = resolve_env(Dict(n => lookup[n][1] for n in packages), Dict()) -joint["ok"] || error("The joint environment does not resolve on Julia $VERSION:\n$(joint["error"])") -versions = manifest_versions(joint["manifest"]) -trees = manifest_trees(joint["manifest"]) +Fetch the registered and default-branch files of every package of `lookup` (name => +(uuid, repo)) into `out`, write `/index.toml` and return the index. +""" +function collect_files(out, lookup, versions, trees) + index = Dict{String,Any}() + for n in sort(collect(keys(lookup))) + uuid, repo = lookup[n] + branch = gh_jq("repos/$repo", ".default_branch") + registered = fetch_files(repo, trees[n], joinpath(out, "registered", n)) + default = fetch_files(repo, branch, joinpath(out, "default", n)) + index[n] = Dict("uuid" => uuid, "version" => versions[n], "repo" => repo, "tree" => trees[n], + "default_branch" => branch, "files" => registered, "default_files" => default) + println(rpad(n, 16), rpad(versions[n], 14), repo, " [", join(registered, ", "), "]") + end + open(io -> TOML.print(io, Dict("julia" => string(VERSION), "packages" => index)), joinpath(out, "index.toml"), "w") + return index +end -index = Dict{String,Any}() -for n in packages - uuid, repo = lookup[n] - branch = gh_jq("repos/$repo", ".default_branch") - registered = fetch_files(repo, trees[n], joinpath(out, "registered", n)) - default = fetch_files(repo, branch, joinpath(out, "default", n)) - index[n] = Dict("uuid" => uuid, "version" => versions[n], "repo" => repo, "tree" => trees[n], - "default_branch" => branch, "files" => registered, "default_files" => default) - println(rpad(n, 16), rpad(versions[n], 14), repo, " [", join(registered, ", "), "]") +if abspath(PROGRAM_FILE) == @__FILE__ + packages, out = parse_list(ARGS[1]), ARGS[2] + lookup = Dict(n => registry_lookup(n) for n in packages) + joint = resolve_env(Dict(n => lookup[n][1] for n in packages), Dict()) + joint["ok"] || error("The joint environment does not resolve on Julia $VERSION:\n$(joint["error"])") + collect_files(out, lookup, joint["versions"], joint["trees"]) end -open(io -> TOML.print(io, Dict("julia" => string(VERSION), "packages" => index)), joinpath(out, "index.toml"), "w") diff --git a/scripts/compat-prune/common.jl b/scripts/compat-prune/common.jl index 6ff5d33..53b322e 100644 --- a/scripts/compat-prune/common.jl +++ b/scripts/compat-prune/common.jl @@ -2,15 +2,17 @@ using Pkg, TOML -# Resolution only: never precompile the throwaway environments. -ENV["JULIA_PKG_PRECOMPILE_AUTO"] = "0" -# Every resolution of an audit must see the same registry snapshot, or a release published -# meanwhile shows up as a difference: registries are updated once, by `audit.jl`, before -# anything else, and never again. -Pkg.offline(true) +# Resolution only: environments are resolved by calling Pkg's resolver directly (see +# `resolve_env`), which never downloads, installs, precompiles, or updates a registry. An +# audit therefore sees one registry snapshot from start to end: `audit.jl` updates the +# registries once, before anything else. + +# The command runner behind `gh`: a function of the argument vector returning standard +# output. Tests replace it with a fake to run without network; a second include keeps it. +@isdefined(GH) || global GH = Ref{Any}(args -> read(`gh $args`, String)) "Run `gh` with `args` and return its standard output." -gh(args...) = read(`gh $args`, String) +gh(args...) = GH[](collect(String, args)) "`gh api` call returning the raw body (blobs, file contents)." gh_raw(path) = gh("api", path, "-H", "Accept: application/vnd.github.raw") @@ -23,8 +25,14 @@ gh_jq(path, jq) = strip(gh("api", path, "--jq", jq)) Resolve a throwaway environment whose `[deps]` are `deps` (name => uuid) and whose `[compat]` is `compat` (name => specifier), on the running Julia. Returns -`Dict("ok" => true, "manifest" => …)` with the parsed `Manifest.toml`, or -`Dict("ok" => false, "error" => …)` when the requirements are unsatisfiable. +`Dict("ok" => true, "versions" => …, "trees" => …)` (name => version string, name => +git-tree-sha1 of the registered packages), or `Dict("ok" => false, "error" => …)` when the +requirements are unsatisfiable. + +`Pkg.resolve` is not used: it downloads every source and artifact of the result (GBs of CUDA +on a fresh runner), and `Pkg.offline(true)` would restrict it to the versions already +installed. `Pkg.Operations.resolve_versions!` only resolves; it is internal, but its +signature is the same from Julia 1.10 to 1.13 (checked by the integration tests). """ function resolve_env(deps::AbstractDict, compat::AbstractDict) dir = mktempdir() @@ -34,8 +42,12 @@ function resolve_env(deps::AbstractDict, compat::AbstractDict) current = Base.active_project() try Pkg.activate(dir; io=devnull) - Pkg.resolve(; io=devnull) - return Dict("ok" => true, "manifest" => TOML.parsefile(joinpath(dir, "Manifest.toml"))) + ctx = Pkg.Types.Context() + pkgs = Pkg.Operations.load_direct_deps(ctx.env) + Pkg.Operations.resolve_versions!(ctx.env, ctx.registries, pkgs, ctx.julia_version, false) + return Dict("ok" => true, + "versions" => Dict(p.name => string(p.version) for p in pkgs), + "trees" => Dict(p.name => string(p.tree_hash) for p in pkgs if p.tree_hash !== nothing)) catch e # Only an unsatisfiable resolution is a result; anything else is a bug of the caller. e isa Pkg.Resolve.ResolverError || rethrow() @@ -46,13 +58,6 @@ function resolve_env(deps::AbstractDict, compat::AbstractDict) end end -"Map name => version string for every entry of a parsed manifest (stdlibs: `\"stdlib\"`)." -manifest_versions(m) = Dict(k => get(v[1], "version", "stdlib") for (k, v) in m["deps"]) - -"Map name => git-tree-sha1 for every registered entry of a parsed manifest." -manifest_trees(m) = - Dict(k => v[1]["git-tree-sha1"] for (k, v) in m["deps"] if haskey(v[1], "git-tree-sha1")) - """ registry_lookup(name) -> (uuid, repo) @@ -74,6 +79,12 @@ function registry_lookup(name) error("$name is not in any reachable registry") end +"The files audited in every repository." +const AUDITED_FILES = ("Project.toml", "test/Project.toml", "docs/Project.toml") + +"Identity of a compat entry: the owner, the file it is declared in, the dependency." +entry_key(p) = (p["owner"], p["file"], p["dep"]) + const spec = Pkg.Types.semver_spec "Render a VersionSpec back into a compat string (bounds carry no prerelease, so `-` only separates)." @@ -93,16 +104,17 @@ function applies(owner, file, name, versions, audited) end """ - simulate(name, env, narrowings, uuids) -> Dict + simulate(name, env, narrowings, uuids; resolver=resolve_env) -> Dict Resolve environment `name` (an entry of `envs.toml`) with each `dep => specifier` of `narrowings` intersected into its compat. Pkg intersects the compat of every package of an environment, so this is exactly what dropping specifiers from an entry that applies here would do, without touching any file. Returns `Dict("ok" => true, "versions" => …)` or `Dict("ok" => false, "error" => …)`; an empty intersection or a pinned package resolved at -another version than the audited one are failures too. +another version than the audited one are failures too. `resolver(deps, compat)` is +`resolve_env`, replaced in tests. """ -function simulate(name, env, narrowings, uuids) +function simulate(name, env, narrowings, uuids; resolver=resolve_env) deps, compat = copy(env["deps"]), copy(env["compat"]) for (dep, s) in narrowings compat[dep] = render(spec(get(compat, dep, ">= 0")) ∩ spec(s)) @@ -110,9 +122,9 @@ function simulate(name, env, narrowings, uuids) end empty = sort([k for (k, v) in compat if isempty(v)]) isempty(empty) || return Dict("ok" => false, "error" => "empty compat intersection for $(join(empty, ", "))") - res = resolve_env(deps, compat) + res = resolver(deps, compat) res["ok"] || return res - versions = manifest_versions(res["manifest"]) + versions = res["versions"] off = ["$k $(versions[k]) (audited $v)" for (k, v) in env["pins"] if versions[k] != v] isempty(off) || return Dict("ok" => false, "error" => "pinned versions not selected: $(join(off, ", "))") return Dict("ok" => true, "versions" => versions) diff --git a/scripts/compat-prune/edit.jl b/scripts/compat-prune/edit.jl new file mode 100644 index 0000000..2fbec1e --- /dev/null +++ b/scripts/compat-prune/edit.jl @@ -0,0 +1,98 @@ +# Apply the accepted prunes to checkouts of the default branches, textually. +# +# usage: julia edit.jl +# +# / is a fresh checkout of the default branch of package . The +# prunes of /audit.toml are applied in place, line by line, to the files as they +# are *now*: never by copying a collected file, which would revert whatever the branch +# received since the audit read it. The outcome of every prune goes to +# /edits.toml: +# edited the line on the default branch is the audited one: it is rewritten; +# already the default branch already carries the pruned value (merged, awaiting release); +# drift the line differs, or the entry, the file or the checkout is missing: untouched. +# Only the value of the one `[compat]` line changes: order, quoting, spacing and comments of +# the file are kept, and the result is re-parsed to check that nothing else moved. + +include("common.jl") + +"Specifiers of a compat string, normalised for comparison." +normalised(s) = sort(String.(strip.(split(s, ",")))) + +const SECTION = r"^\s*\[\s*([^\[\]]+?)\s*\]\s*(#.*)?$" + +""" + edit_compat(text, dep, before, after) -> (text, status, detail) + +Rewrite the `[compat]` entry of `dep` from `before` to `after` in the TOML source `text`. +`status` is `"edited"`, `"already"` or `"drift"`; `text` is unchanged unless `"edited"`. +""" +function edit_compat(text, dep, before, after) + lines = split(text, '\n'; keepempty=true) + name = replace(dep, r"[^A-Za-z0-9_]" => s"\\\0") + entry = Regex("^(\\s*\"?$name\"?\\s*=\\s*)([\"'])(.*?)\\2(.*)\$") + section = "" + for (i, l) in enumerate(lines) + m = match(SECTION, l) + m === nothing || (section = m[1]; continue) + section == "compat" || continue + m = match(entry, l) + m === nothing && continue + value = m[3] + normalised(value) == normalised(after) && return (text, "already", "already `$value`") + normalised(value) == normalised(before) || + return (text, "drift", "`$value` on the default branch, `$before` audited") + lines[i] = m[1] * m[2] * after * m[2] * m[4] + edited = join(lines, '\n') + check_edit(text, edited, dep, after) + return (edited, "edited", "") + end + return (text, "drift", "no `$dep` entry in `[compat]` on the default branch") +end + +"Assert that `edited` differs from `original` by the compat entry of `dep` only." +function check_edit(original, edited, dep, after) + a, b = TOML.parse(original), TOML.parse(edited) + b["compat"][dep] == after || error("edit of $dep did not produce `$after`") + a["compat"][dep] = after + a == b || error("editing $dep changed more than its compat entry") + return nothing +end + +""" + plan_edits(audit, read_file) -> (records, texts) + +Apply every accepted prune of `audit` to the files returned by `read_file(owner, file)` +(`nothing` when absent). `records` are the prunes with their `status` and `detail`; `texts` +maps `(owner, file)` to the edited text of every file with at least one edit. +""" +function plan_edits(audit, read_file) + texts = Dict{Tuple{String,String},String}() + records = Dict{String,Any}[] + for p in sort(audit["prune"]; by=entry_key) + k = (p["owner"], p["file"]) + text = haskey(texts, k) ? texts[k] : read_file(k...) + if text === nothing + push!(records, merge(p, Dict("status" => "drift", "detail" => "file not on the default branch"))) + continue + end + edited, status, detail = edit_compat(text, p["dep"], p["before"], p["after"]) + status == "edited" && (texts[k] = edited) + push!(records, merge(p, Dict("status" => status, "detail" => detail))) + end + return records, texts +end + +"Read `//`, or `nothing` when absent." +read_checkout(dir) = (owner, file) -> (path = joinpath(dir, owner, file); isfile(path) ? read(path, String) : nothing) + +if abspath(PROGRAM_FILE) == @__FILE__ + root, checkouts = ARGS[1], ARGS[2] + records, texts = plan_edits(TOML.parsefile(joinpath(root, "audit.toml")), read_checkout(checkouts)) + for ((owner, file), text) in texts + write(joinpath(checkouts, owner, file), text) + end + open(io -> TOML.print(io, Dict("edit" => records)), joinpath(root, "edits.toml"), "w") + for s in ("edited", "already", "drift") + println(rpad(s, 8), count(r -> r["status"] == s, records)) + end +end diff --git a/scripts/compat-prune/envs.jl b/scripts/compat-prune/envs.jl index 0040031..bc0def4 100644 --- a/scripts/compat-prune/envs.jl +++ b/scripts/compat-prune/envs.jl @@ -42,25 +42,36 @@ function docs_env(name, uuid, dir) return deps, restrict(get(t, "compat", Dict()), deps) end -root = ARGS[1] -index = TOML.parsefile(joinpath(root, "index.toml"))["packages"] -pins = Dict(n => VersionNumber(p["version"]) for (n, p) in index) +""" + build_envs(root) -> Dict -envs = Dict{String,Any}() -envs["joint"] = (Dict(n => p["uuid"] for (n, p) in index), Dict{String,Any}()) -for (n, p) in index - dir = joinpath(root, "registered", n) - envs["$n/test"] = test_env(n, p["uuid"], dir) - docs = docs_env(n, p["uuid"], dir) - docs === nothing || (envs["$n/docs"] = docs) +The audited environments of a collected directory, as written to `envs.toml`: name => +`owner`, `kind`, `deps`, `compat`, `pins` (the packages pinned to their audited version). +""" +function build_envs(root) + index = TOML.parsefile(joinpath(root, "index.toml"))["packages"] + pins = Dict(n => VersionNumber(p["version"]) for (n, p) in index) + envs = Dict{String,Any}() + envs["joint"] = (Dict(n => p["uuid"] for (n, p) in index), Dict{String,Any}()) + for (n, p) in index + dir = joinpath(root, "registered", n) + envs["$n/test"] = test_env(n, p["uuid"], dir) + docs = docs_env(n, p["uuid"], dir) + docs === nothing || (envs["$n/docs"] = docs) + end + out = Dict{String,Any}() + for (name, (deps, compat)) in envs + owner, kind = name == "joint" ? ("", "joint") : split(name, "/") + pinned = Dict(string(k) => string(pins[k]) for k in (kind == "joint" ? keys(pins) : (owner,))) + compat = merge(compat, Dict(k => (v = VersionNumber(v); "=$(v.major).$(v.minor).$(v.patch)") for (k, v) in pinned)) + out[name] = Dict("owner" => owner, "kind" => kind, "deps" => deps, "compat" => compat, "pins" => pinned) + end + return out end -out = Dict{String,Any}() -for (name, (deps, compat)) in envs - owner, kind = name == "joint" ? ("", "joint") : split(name, "/") - pinned = Dict(string(k) => string(pins[k]) for k in (kind == "joint" ? keys(pins) : (owner,))) - compat = merge(compat, Dict(k => (v = VersionNumber(v); "=$(v.major).$(v.minor).$(v.patch)") for (k, v) in pinned)) - out[name] = Dict("owner" => owner, "kind" => kind, "deps" => deps, "compat" => compat, "pins" => pinned) +if abspath(PROGRAM_FILE) == @__FILE__ + root = ARGS[1] + out = build_envs(root) + open(io -> TOML.print(io, Dict("env" => out)), joinpath(root, "envs.toml"), "w") + println(length(out), " environments: ", join(sort(collect(keys(out))), ", ")) end -open(io -> TOML.print(io, Dict("env" => out)), joinpath(root, "envs.toml"), "w") -println(length(out), " environments: ", join(sort(collect(keys(out))), ", ")) diff --git a/scripts/compat-prune/report.jl b/scripts/compat-prune/report.jl new file mode 100644 index 0000000..0b1048b --- /dev/null +++ b/scripts/compat-prune/report.jl @@ -0,0 +1,107 @@ +# Render the audit as the Markdown body of the tracking issue. +# +# usage: julia report.jl [] (writes /report.md) +# +# , written by the publishing step, maps a package name to the URL of its PR. The +# output is deterministic (everything sorted) so that an unchanged audit gives an unchanged +# body, and the hidden marker lets the publishing step find the issue again. + +include("common.jl") + +const MARKER = "" +const WORKFLOW_URL = "https://github.com/control-toolbox/CTActions/blob/main/.github/workflows/compat-prune.yml" + +"Markdown table from a header and rows of strings." +table(header, rows) = join(["| " * join(header, " | ") * " |", "| " * join(fill("---", length(header)), " | ") * " |", + ("| " * join(r, " | ") * " |" for r in rows)...], "\n") + +"`0.12.3 on 1.12, 1.13; 0.9.18 on 1.10`: the versions selected for the kept lanes, per Julia version." +function selected(evidence) + on = Dict{String,Set{String}}() + for list in values(evidence), e in list + env_julia, v = split(e, "=") + push!(get!(on, v, Set{String}()), last(split(env_julia, "@"))) + end + vs = sort(collect(keys(on)); by=VersionNumber, rev=true) + join(["$v on $(join(sort(collect(on[v])), ", "))" for v in vs], "; ") +end + +code(s) = "`$s`" +entry(r) = "$(r["owner"]) $(code(r["file"])) $(code(r["dep"]))" + +""" + render_report(audit, edits; prs=Dict(), run_url="") -> String + +Markdown body of the tracking issue: what can be pruned (per repository, with its PR), what +waits for a release, what drifted, what was rejected or withheld, and the blockers. +""" +function render_report(audit, edits; prs=Dict(), run_url="") + by_status(s) = sort([e for e in edits if e["status"] == s]; by=entry_key) + edited, already, drift = by_status("edited"), by_status("already"), by_status("drift") + rejected = sort(get(audit, "rejected", []); by=entry_key) + withheld = sort([r for r in get(audit, "other", []) if r["status"] == "withheld"]; by=entry_key) + blocked = get(audit, "blocked", []) + out = String[MARKER] + + run = isempty(run_url) ? "" : " ([run]($run_url))" + push!(out, """ + Audit of the `[compat]` entries of the control-toolbox packages, by the + [`compat-prune`]($WORKFLOW_URL) workflow on $(audit["date"])$run, for Julia \ + $(join(audit["julia"], ", ")). This body is rewritten by every run: do not edit it. + + A prune drops `[compat]` specifiers that no environment selects, on no Julia version, and + leaves every resolved manifest (joint, tests and docs of every package) unchanged. PRs can + therefore be merged in any order, and need no release of their own.""") + + push!(out, "## Summary\n\n" * table(["", "entries"], [ + ["to prune (open PRs)", string(length(edited))], + ["merged, awaiting release", string(length(already))], + ["drift (not edited)", string(length(drift))], + ["rejected by validation", string(length(rejected))], + ["withheld", string(length(withheld))], + ["blocked (information)", string(length(unique(entry_key.(blocked))))], + ])) + + if !isempty(edited) + sections = String["## To prune"] + for owner in sort(unique(e["owner"] for e in edited)) + pr = haskey(prs, owner) ? " — $(prs[owner])" : "" + rows = [[code(e["file"]), code(e["dep"]), code(e["before"]), code(e["after"]), selected(e["evidence"])] + for e in edited if e["owner"] == owner] + push!(sections, "### $owner$pr\n\n" * table(["file", "dependency", "before", "after", "selected"], rows)) + end + push!(out, join(sections, "\n\n")) + end + isempty(already) || push!(out, "## Merged, awaiting release\n\n" * + table(["entry", "after"], [[entry(e), code(e["after"])] for e in already])) + isempty(drift) || push!(out, "## Drift\n\nThe default branch differs from the registered version: not edited.\n\n" * + table(["entry", "audited", "detail"], [[entry(e), code(e["before"]), e["detail"]] for e in drift])) + isempty(rejected) || push!(out, "## Rejected\n\n" * + table(["entry", "where", "reason"], [[entry(r), r["where"], replace(r["reason"], "\n" => " ", "|" => "\\|")] for r in rejected])) + isempty(withheld) || push!(out, "## Withheld\n\n$(uppercasefirst(first(withheld)["reason"])).\n\n" * + table(["entry", "would drop"], [[entry(r), join(code.(r["removed"]), ", ")] for r in withheld])) + if !isempty(blocked) + rows = Vector{String}[] + for k in sort(unique(entry_key.(blocked))) + probes = [b for b in blocked if entry_key(b) == k] + names = sort(unique(reduce(vcat, [b["blockers"] for b in probes]))) + b = first(probes) + push!(rows, [entry(b), code(b["newest"]), isempty(names) ? "resolver preference" : join(names, ", ")]) + end + push!(out, "## Blocked\n\nThe newest specifier is never selected: information only, nothing is edited.\n\n" * + table(["entry", "newest", "capped by"], rows)) + end + + versions = [[n, p["version"], "[$(p["repo"])](https://github.com/$(p["repo"]))"] for (n, p) in sort(collect(audit["packages"]); by=first)] + push!(out, "## Audited versions\n\n" * table(["package", "version", "repository"], versions)) + return join(out, "\n\n") * "\n" +end + +if abspath(PROGRAM_FILE) == @__FILE__ + root = ARGS[1] + prs = length(ARGS) ≥ 2 ? TOML.parsefile(ARGS[2]) : Dict() + audit = TOML.parsefile(joinpath(root, "audit.toml")) + edits = TOML.parsefile(joinpath(root, "edits.toml"))["edit"] + write(joinpath(root, "report.md"), render_report(audit, edits; prs, run_url=get(ENV, "RUN_URL", ""))) + println("report written to ", joinpath(root, "report.md")) +end diff --git a/scripts/compat-prune/resolve.jl b/scripts/compat-prune/resolve.jl index 85c826a..4651c23 100644 --- a/scripts/compat-prune/resolve.jl +++ b/scripts/compat-prune/resolve.jl @@ -8,20 +8,31 @@ include("common.jl") -root, outfile = ARGS[1], ARGS[2] -prunes = length(ARGS) ≥ 4 ? TOML.parsefile(ARGS[3])["prune"] : [] -baseline = length(ARGS) ≥ 4 ? TOML.parsefile(ARGS[4])["env"] : Dict() -envs, audited, uuids = load_collected(root) +""" + resolve_all(root; prunes=[], baseline=Dict(), sim=simulate) -> Dict -results = Dict{String,Any}() -for name in sort(collect(keys(envs))) - if isempty(prunes) - r = simulate(name, envs[name], Dict(), uuids) - else - base = baseline[name] - r = base["ok"] ? simulate(name, envs[name], narrowings(prunes, name, base["versions"], audited), uuids) : base +Result of every environment of a collected directory, the baseline when `prunes` is empty. +""" +function resolve_all(root; prunes=[], baseline=Dict(), sim=simulate) + envs, audited, uuids = load_collected(root) + results = Dict{String,Any}() + for name in sort(collect(keys(envs))) + if isempty(prunes) + r = sim(name, envs[name], Dict(), uuids) + else + base = baseline[name] + r = base["ok"] ? sim(name, envs[name], narrowings(prunes, name, base["versions"], audited), uuids) : base + end + results[name] = r + println(VERSION, " ", rpad(name, 24), r["ok"] ? "ok" : "UNSAT") end - results[name] = r - println(VERSION, " ", rpad(name, 24), r["ok"] ? "ok" : "UNSAT") + return results +end + +if abspath(PROGRAM_FILE) == @__FILE__ + root, outfile = ARGS[1], ARGS[2] + prunes = length(ARGS) ≥ 4 ? TOML.parsefile(ARGS[3])["prune"] : [] + baseline = length(ARGS) ≥ 4 ? TOML.parsefile(ARGS[4])["env"] : Dict() + results = resolve_all(root; prunes, baseline) + open(io -> TOML.print(io, Dict("julia" => string(VERSION), "env" => results)), outfile, "w") end -open(io -> TOML.print(io, Dict("julia" => string(VERSION), "env" => results)), outfile, "w") diff --git a/scripts/compat-prune/test/fixtures/collected/default/Alpha/Project.toml b/scripts/compat-prune/test/fixtures/collected/default/Alpha/Project.toml new file mode 100644 index 0000000..1b46810 --- /dev/null +++ b/scripts/compat-prune/test/fixtures/collected/default/Alpha/Project.toml @@ -0,0 +1,12 @@ +name = "Alpha" +uuid = "aaaaaaaa-0000-4000-8000-000000000001" +version = "0.3.2" + +[deps] +Beta = "bbbbbbbb-0000-4000-8000-000000000002" +Dep1 = "11111111-0000-4000-8000-000000000001" + +[compat] +Beta = "1" +Dep1 = "0.5, 0.6, 0.7" # lanes kept by CompatHelper +julia = "1.10" diff --git a/scripts/compat-prune/test/fixtures/collected/default/Alpha/docs/Project.toml b/scripts/compat-prune/test/fixtures/collected/default/Alpha/docs/Project.toml new file mode 100644 index 0000000..c5518e8 --- /dev/null +++ b/scripts/compat-prune/test/fixtures/collected/default/Alpha/docs/Project.toml @@ -0,0 +1,5 @@ +[deps] +Doc = "33333333-0000-4000-8000-000000000003" + +[compat] +Doc = "0.9, 1" diff --git a/scripts/compat-prune/test/fixtures/collected/default/Beta/Project.toml b/scripts/compat-prune/test/fixtures/collected/default/Beta/Project.toml new file mode 100644 index 0000000..4033244 --- /dev/null +++ b/scripts/compat-prune/test/fixtures/collected/default/Beta/Project.toml @@ -0,0 +1,24 @@ +name = "Beta" +uuid = "bbbbbbbb-0000-4000-8000-000000000002" +version = "1.2.0-beta" + +[deps] +Dep1 = "11111111-0000-4000-8000-000000000001" + +[weakdeps] +Weak = "44444444-0000-4000-8000-000000000004" + +[extras] +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +Tool = "22222222-0000-4000-8000-000000000002" +Unused = "55555555-0000-4000-8000-000000000005" + +[compat] +Dep1 = "0.7" +Tool = "2" +Unused = "9" +Weak = "0.1, 0.2" +julia = "1.10" + +[targets] +test = ["Test", "Tool", "Weak"] diff --git a/scripts/compat-prune/test/fixtures/collected/index.toml b/scripts/compat-prune/test/fixtures/collected/index.toml new file mode 100644 index 0000000..f5d57ce --- /dev/null +++ b/scripts/compat-prune/test/fixtures/collected/index.toml @@ -0,0 +1,19 @@ +julia = "1.13.1" + +[packages.Alpha] +uuid = "aaaaaaaa-0000-4000-8000-000000000001" +version = "0.3.1" +repo = "example-org/Alpha.jl" +tree = "1111111111111111111111111111111111111111" +default_branch = "main" +files = ["Project.toml", "test/Project.toml", "docs/Project.toml"] +default_files = ["Project.toml", "docs/Project.toml"] + +[packages.Beta] +uuid = "bbbbbbbb-0000-4000-8000-000000000002" +version = "1.2.0-beta" +repo = "example-org/Beta.jl" +tree = "2222222222222222222222222222222222222222" +default_branch = "dev" +files = ["Project.toml"] +default_files = ["Project.toml"] diff --git a/scripts/compat-prune/test/fixtures/collected/registered/Alpha/Project.toml b/scripts/compat-prune/test/fixtures/collected/registered/Alpha/Project.toml new file mode 100644 index 0000000..0dbbcb2 --- /dev/null +++ b/scripts/compat-prune/test/fixtures/collected/registered/Alpha/Project.toml @@ -0,0 +1,12 @@ +name = "Alpha" +uuid = "aaaaaaaa-0000-4000-8000-000000000001" +version = "0.3.1" + +[deps] +Beta = "bbbbbbbb-0000-4000-8000-000000000002" +Dep1 = "11111111-0000-4000-8000-000000000001" + +[compat] +Beta = "1" +Dep1 = "0.5, 0.6, 0.7" +julia = "1.10" diff --git a/scripts/compat-prune/test/fixtures/collected/registered/Alpha/docs/Project.toml b/scripts/compat-prune/test/fixtures/collected/registered/Alpha/docs/Project.toml new file mode 100644 index 0000000..c5518e8 --- /dev/null +++ b/scripts/compat-prune/test/fixtures/collected/registered/Alpha/docs/Project.toml @@ -0,0 +1,5 @@ +[deps] +Doc = "33333333-0000-4000-8000-000000000003" + +[compat] +Doc = "0.9, 1" diff --git a/scripts/compat-prune/test/fixtures/collected/registered/Alpha/test/Project.toml b/scripts/compat-prune/test/fixtures/collected/registered/Alpha/test/Project.toml new file mode 100644 index 0000000..8b24135 --- /dev/null +++ b/scripts/compat-prune/test/fixtures/collected/registered/Alpha/test/Project.toml @@ -0,0 +1,7 @@ +[deps] +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +Tool = "22222222-0000-4000-8000-000000000002" + +[compat] +Tool = "1, 2" +Unlisted = "3" diff --git a/scripts/compat-prune/test/fixtures/collected/registered/Beta/Project.toml b/scripts/compat-prune/test/fixtures/collected/registered/Beta/Project.toml new file mode 100644 index 0000000..1a314c0 --- /dev/null +++ b/scripts/compat-prune/test/fixtures/collected/registered/Beta/Project.toml @@ -0,0 +1,24 @@ +name = "Beta" +uuid = "bbbbbbbb-0000-4000-8000-000000000002" +version = "1.2.0-beta" + +[deps] +Dep1 = "11111111-0000-4000-8000-000000000001" + +[weakdeps] +Weak = "44444444-0000-4000-8000-000000000004" + +[extras] +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +Tool = "22222222-0000-4000-8000-000000000002" +Unused = "55555555-0000-4000-8000-000000000005" + +[compat] +Dep1 = "0.6, 0.7" +Tool = "2" +Unused = "9" +Weak = "0.1, 0.2" +julia = "1.10" + +[targets] +test = ["Test", "Tool", "Weak"] diff --git a/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/baseline-1.10.toml b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/baseline-1.10.toml new file mode 100644 index 0000000..e902fe4 --- /dev/null +++ b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/baseline-1.10.toml @@ -0,0 +1,699 @@ +julia = "1.10.11" + +[env."CTParser/docs"] +ok = true + + [env."CTParser/docs".versions] + CTParser = "0.9.5-beta" + OrderedCollections = "2.0.3" + CTBase = "0.30.10" + MLStyle = "0.4.17" + Markdown = "stdlib" + Printf = "stdlib" + Test = "stdlib" + Random = "stdlib" + MarkdownAST = "0.1.3" + DocStringExtensions = "0.9.5" + ADTypes = "1.24.0" + Parameters = "0.13.1" + Logging = "stdlib" + Documenter = "1.19.0" + Unicode = "stdlib" + +[env."CTModels/docs"] +ok = true + + [env."CTModels/docs".versions] + RecipesBase = "1.3.4" + CommonSolve = "0.2.14" + LinearAlgebra = "stdlib" + Printf = "stdlib" + DocumenterVitepress = "0.3.8" + Logging = "stdlib" + Colors = "0.13.3" + MacroTools = "0.5.16" + CTModels = "0.19.8" + OrderedCollections = "2.0.3" + MLStyle = "0.4.17" + Markdown = "stdlib" + Random = "stdlib" + Plots = "1.41.7" + DocumenterInterLinks = "1.1.0" + MarkdownAST = "0.1.3" + DocStringExtensions = "0.9.5" + StaticArrays = "1.9.23" + Parameters = "0.13.1" + Interpolations = "0.16.3" + CTBase = "0.30.10" + JLD2 = "0.6.7" + JSON3 = "1.14.3" + Makie = "0.24.16" + JET = "0.9.18" + Test = "stdlib" + ADTypes = "1.24.0" + Reexport = "1.2.2" + Documenter = "1.19.0" + CairoMakie = "0.15.16" + LiveServer = "1.6.0" + SparseArrays = "1.10.0" + Unicode = "stdlib" + +[env."CTLie/test"] +ok = true + + [env."CTLie/test".versions] + ForwardDiff = "1.4.6" + OrderedCollections = "2.0.3" + CTBase = "0.30.10" + Markdown = "stdlib" + CTLie = "0.2.3" + LinearAlgebra = "stdlib" + JET = "0.9.18" + Mooncake = "0.5.64" + Printf = "stdlib" + Test = "stdlib" + Random = "stdlib" + BenchmarkTools = "1.8.0" + Aqua = "0.8.18" + DocStringExtensions = "0.9.5" + StaticArrays = "1.9.23" + ADTypes = "1.24.0" + Logging = "stdlib" + Reexport = "1.2.2" + CUDA = "6.4.2" + DifferentiationInterface = "0.7.21" + GPUArraysCore = "0.2.1" + SparseArrays = "1.10.0" + Unicode = "stdlib" + MacroTools = "0.5.16" + KernelAbstractions = "0.9.45" + +[env."CTDirect/test"] +ok = true + + [env."CTDirect/test".versions] + RecipesBase = "1.3.4" + SolverCore = "0.3.10" + CommonSolve = "0.2.14" + LinearAlgebra = "stdlib" + Printf = "stdlib" + SparseConnectivityTracer = "1.2.4" + Logging = "stdlib" + GPUArraysCore = "0.2.1" + MacroTools = "0.5.16" + CTModels = "0.19.8" + CTParser = "0.9.5-beta" + OrderedCollections = "2.0.3" + MLStyle = "0.4.17" + Markdown = "stdlib" + CUDSS = "0.8.1" + Random = "stdlib" + NLPModelsIpopt = "0.11.3" + ADNLPModels = "0.8.15" + CUDA = "6.3.1" + MadNLP = "0.10.1" + SplitApplyCombine = "1.3.0" + KernelAbstractions = "0.9.45" + ExaModels = "0.12.1" + AMDGPU = "2.8.0" + CTSolvers = "0.5.8" + DocStringExtensions = "0.9.5" + StaticArrays = "1.9.23" + Parameters = "0.13.1" + DifferentiationInterface = "0.7.21" + NLPModels = "0.21.12" + MadNLPGPU = "0.10.2" + CTDirect = "1.1.8-beta" + ForwardDiff = "1.4.6" + CTBase = "0.30.10" + Test = "stdlib" + ADTypes = "1.24.0" + Reexport = "1.2.2" + SparseArrays = "1.10.0" + Unicode = "stdlib" + +[env."CTDirect/docs"] +ok = true + + [env."CTDirect/docs".versions] + ExaModels = "0.12.1" + CTDirect = "1.1.8-beta" + CTParser = "0.9.5-beta" + ForwardDiff = "1.4.6" + OrderedCollections = "2.0.3" + CTBase = "0.30.10" + RecipesBase = "1.3.4" + SolverCore = "0.3.10" + CommonSolve = "0.2.14" + Markdown = "stdlib" + MLStyle = "0.4.17" + LinearAlgebra = "stdlib" + Printf = "stdlib" + Test = "stdlib" + Random = "stdlib" + SparseConnectivityTracer = "1.2.4" + MarkdownAST = "0.1.3" + CTSolvers = "0.5.8" + DocumenterMermaid = "0.2.1" + ADNLPModels = "0.8.15" + DocStringExtensions = "0.9.5" + StaticArrays = "1.9.23" + ADTypes = "1.24.0" + Parameters = "0.13.1" + Logging = "stdlib" + Reexport = "1.2.2" + Documenter = "1.19.0" + DifferentiationInterface = "0.7.21" + SparseArrays = "1.10.0" + Unicode = "stdlib" + MacroTools = "0.5.16" + NLPModels = "0.21.12" + CTModels = "0.19.8" + KernelAbstractions = "0.9.45" + +[env."CTFlows/docs"] +ok = true + + [env."CTFlows/docs".versions] + RecipesBase = "1.3.4" + SolverCore = "0.3.10" + CommonSolve = "0.2.14" + LinearAlgebra = "stdlib" + Printf = "stdlib" + DocumenterVitepress = "0.3.8" + Logging = "stdlib" + DiffEqBase = "7.21.4" + GPUArraysCore = "0.2.1" + SciMLBase = "3.58.0" + Colors = "0.13.3" + MacroTools = "0.5.16" + CTModels = "0.19.8" + OrderedCollections = "2.0.3" + MLStyle = "0.4.17" + Markdown = "stdlib" + OrdinaryDiffEqRosenbrock = "2.7.5" + Random = "stdlib" + Plots = "1.41.7" + KernelAbstractions = "0.9.45" + DocumenterInterLinks = "1.1.0" + MarkdownAST = "0.1.3" + CTSolvers = "0.5.8" + DocStringExtensions = "0.9.5" + StaticArrays = "1.9.23" + Parameters = "0.13.1" + Interpolations = "0.16.3" + DifferentiationInterface = "0.7.21" + ForwardDiff = "1.4.6" + CTFlows = "0.18.1" + CTBase = "0.30.10" + Makie = "0.24.16" + Test = "stdlib" + ADTypes = "1.24.0" + Reexport = "1.2.2" + Documenter = "1.19.0" + OrdinaryDiffEqTsit5 = "2.1.5" + CairoMakie = "0.15.16" + LiveServer = "1.6.0" + SparseArrays = "1.10.0" + Unicode = "stdlib" + +[env."OptimalControl/docs"] +ok = true + + [env."OptimalControl/docs".versions] + RecipesBase = "1.3.4" + SolverCore = "0.3.10" + CommonSolve = "0.2.14" + NLPModelsKnitro = "0.10.6" + LinearAlgebra = "stdlib" + Printf = "stdlib" + NonlinearSolve = "4.32.0" + SparseConnectivityTracer = "1.2.4" + DocumenterVitepress = "0.3.8" + Logging = "stdlib" + DiffEqBase = "7.21.4" + GPUArraysCore = "0.2.1" + SciMLBase = "3.58.0" + Colors = "0.13.3" + MacroTools = "0.5.16" + CTModels = "0.19.8" + CTParser = "0.9.5-beta" + OrderedCollections = "2.0.3" + MLStyle = "0.4.17" + Markdown = "stdlib" + OrdinaryDiffEqVerner = "2.4.2" + OrdinaryDiffEqRosenbrock = "2.7.5" + CUDSS = "0.7.0" + Random = "stdlib" + NLPModelsIpopt = "0.11.3" + Plots = "1.41.7" + ADNLPModels = "0.8.15" + CUDA = "6.2.0" + MadNLP = "0.10.1" + KernelAbstractions = "0.9.45" + ExaModels = "0.12.1" + Literate = "2.21.0" + OrdinaryDiffEq = "7.8.1" + CTLie = "0.2.3" + UnoSolver = "0.3.8" + DocumenterInterLinks = "1.1.0" + MarkdownAST = "0.1.3" + CTSolvers = "0.5.8" + DocStringExtensions = "0.9.5" + StaticArrays = "1.9.23" + Parameters = "0.13.1" + OptimalControl = "2.2.1-beta" + Interpolations = "0.16.3" + DifferentiationInterface = "0.7.21" + NLPModels = "0.21.12" + MadNLPGPU = "0.10.2" + CTDirect = "1.1.8-beta" + ForwardDiff = "1.4.6" + CTFlows = "0.18.1" + CTBase = "0.30.10" + JLD2 = "0.6.7" + JSON3 = "1.14.3" + Makie = "0.24.16" + MadNCL = "0.2.3" + Test = "stdlib" + ADTypes = "1.24.0" + Reexport = "1.2.2" + Documenter = "1.19.0" + OrdinaryDiffEqTsit5 = "2.1.5" + CairoMakie = "0.15.16" + LiveServer = "1.6.0" + SparseArrays = "1.10.0" + Unicode = "stdlib" + +[env."CTModels/test"] +ok = true + + [env."CTModels/test".versions] + RecipesBase = "1.3.4" + CommonSolve = "0.2.14" + LinearAlgebra = "stdlib" + Printf = "stdlib" + Aqua = "0.8.18" + Logging = "stdlib" + Colors = "0.13.3" + MacroTools = "0.5.16" + CTModels = "0.19.8" + OrderedCollections = "2.0.3" + MLStyle = "0.4.17" + Markdown = "stdlib" + Random = "stdlib" + BenchmarkTools = "1.8.0" + Plots = "1.41.7" + DocStringExtensions = "0.9.5" + StaticArrays = "1.9.23" + Parameters = "0.13.1" + Interpolations = "0.16.3" + CTBase = "0.30.10" + JLD2 = "0.6.7" + JSON3 = "1.14.3" + Makie = "0.24.16" + JET = "0.9.18" + Test = "stdlib" + ADTypes = "1.24.0" + Reexport = "1.2.2" + CairoMakie = "0.15.16" + SparseArrays = "1.10.0" + Unicode = "stdlib" + +[env."CTLie/docs"] +ok = true + + [env."CTLie/docs".versions] + ForwardDiff = "1.4.6" + CTBase = "0.30.10" + Markdown = "stdlib" + CTLie = "0.2.3" + LinearAlgebra = "stdlib" + Printf = "stdlib" + Test = "stdlib" + DocumenterInterLinks = "1.1.0" + Random = "stdlib" + MarkdownAST = "0.1.3" + DocStringExtensions = "0.9.5" + DocumenterVitepress = "0.3.8" + ADTypes = "1.24.0" + Logging = "stdlib" + Documenter = "1.19.0" + DifferentiationInterface = "0.7.21" + LiveServer = "1.6.0" + Unicode = "stdlib" + MacroTools = "0.5.16" + +[env."CTSolvers/test"] +ok = true + + [env."CTSolvers/test".versions] + RecipesBase = "1.3.4" + Zygote = "0.7.13" + SolverCore = "0.3.10" + CommonSolve = "0.2.14" + LinearAlgebra = "stdlib" + Printf = "stdlib" + SparseConnectivityTracer = "1.2.4" + Aqua = "0.8.18" + Logging = "stdlib" + DiffEqBase = "7.21.4" + GPUArraysCore = "0.2.1" + SciMLBase = "3.58.0" + MacroTools = "0.5.16" + CTModels = "0.19.8" + OrderedCollections = "2.0.3" + MLStyle = "0.4.17" + Markdown = "stdlib" + CUDSS = "0.8.1" + Random = "stdlib" + BenchmarkTools = "1.8.0" + NLPModelsIpopt = "0.11.3" + ADNLPModels = "0.8.15" + CUDA = "6.4.1" + MadNLP = "0.10.1" + KernelAbstractions = "0.9.45" + ExaModels = "0.12.1" + UnoSolver = "0.3.8" + CTSolvers = "0.5.8" + DocStringExtensions = "0.9.5" + Enzyme = "0.13.216" + StaticArrays = "1.9.23" + Parameters = "0.13.1" + DifferentiationInterface = "0.7.21" + NLPModels = "0.21.12" + MadNLPGPU = "0.10.2" + ForwardDiff = "1.4.6" + CTBase = "0.30.10" + MadNCL = "0.2.3" + JET = "0.9.18" + Test = "stdlib" + ADTypes = "1.24.0" + Reexport = "1.2.2" + OrdinaryDiffEqTsit5 = "2.1.5" + SparseArrays = "1.10.0" + Unicode = "stdlib" + +[env."CTParser/test"] +ok = true + + [env."CTParser/test".versions] + ExaModels = "0.12.1" + CTParser = "0.9.5-beta" + OrderedCollections = "2.0.3" + CTBase = "0.29.3" + RecipesBase = "1.3.4" + MLStyle = "0.4.17" + Markdown = "stdlib" + SolverCore = "0.3.10" + LinearAlgebra = "stdlib" + Printf = "stdlib" + CUDSS = "0.8.1" + Test = "stdlib" + Random = "stdlib" + BenchmarkTools = "1.8.0" + Aqua = "0.8.18" + DocStringExtensions = "0.9.5" + StaticArrays = "1.9.23" + ADTypes = "1.24.0" + Parameters = "0.13.1" + Logging = "stdlib" + Reexport = "1.2.2" + CUDA = "6.4.2" + MadNLP = "0.10.1" + Interpolations = "0.16.3" + GPUArraysCore = "0.2.1" + SparseArrays = "1.10.0" + Unicode = "stdlib" + MacroTools = "0.5.16" + NLPModels = "0.21.12" + CTModels = "0.18.0" + KernelAbstractions = "0.9.45" + MadNLPGPU = "0.10.2" + +[env."OptimalControl/test"] +ok = true + + [env."OptimalControl/test".versions] + RecipesBase = "1.3.4" + SolverCore = "0.3.10" + CommonSolve = "0.2.14" + LinearAlgebra = "stdlib" + Printf = "stdlib" + NonlinearSolve = "4.32.0" + SparseConnectivityTracer = "1.2.4" + Logging = "stdlib" + DiffEqBase = "7.21.4" + GPUArraysCore = "0.2.1" + SciMLBase = "3.58.0" + MacroTools = "0.5.16" + CTModels = "0.19.8" + CTParser = "0.9.5-beta" + OrderedCollections = "2.0.3" + MLStyle = "0.4.17" + Markdown = "stdlib" + OrdinaryDiffEqVerner = "2.4.2" + OrdinaryDiffEqRosenbrock = "2.7.5" + CUDSS = "0.7.0" + Random = "stdlib" + BenchmarkTools = "1.8.0" + NLPModelsIpopt = "0.11.3" + ADNLPModels = "0.8.15" + CUDA = "6.2.0" + MadNLP = "0.10.1" + SplitApplyCombine = "1.3.0" + KernelAbstractions = "0.9.45" + ExaModels = "0.12.1" + OrdinaryDiffEq = "7.8.1" + CTLie = "0.2.3" + UnoSolver = "0.3.8" + CTSolvers = "0.5.8" + DocStringExtensions = "0.9.5" + StaticArrays = "1.9.23" + Parameters = "0.13.1" + OptimalControl = "2.2.1-beta" + DifferentiationInterface = "0.7.21" + NLPModels = "0.21.12" + MadNLPGPU = "0.10.2" + CTDirect = "1.1.8-beta" + ForwardDiff = "1.4.6" + CTFlows = "0.18.1" + CTBase = "0.30.10" + MadNCL = "0.2.3" + Test = "stdlib" + ADTypes = "1.24.0" + Reexport = "1.2.2" + OrdinaryDiffEqTsit5 = "2.1.5" + SparseArrays = "1.10.0" + Unicode = "stdlib" + +[env."CTBase/test"] +ok = true + + [env."CTBase/test".versions] + RecipesBase = "1.3.4" + CommonSolve = "0.2.14" + LinearAlgebra = "stdlib" + Printf = "stdlib" + Aqua = "0.8.18" + Logging = "stdlib" + Colors = "0.13.3" + MacroTools = "0.5.16" + OrderedCollections = "2.0.3" + Markdown = "stdlib" + Random = "stdlib" + BenchmarkTools = "1.8.0" + Plots = "1.41.7" + Coverage = "1.8.2" + MarkdownAST = "0.1.3" + DocStringExtensions = "0.9.5" + StaticArrays = "1.9.23" + Interpolations = "0.16.3" + DifferentiationInterface = "0.7.21" + ForwardDiff = "1.4.6" + CTBase = "0.30.10" + Makie = "0.24.16" + JET = "0.9.18" + Test = "stdlib" + ADTypes = "1.24.0" + Reexport = "1.2.2" + Documenter = "1.19.0" + CairoMakie = "0.15.16" + SparseArrays = "1.10.0" + Unicode = "stdlib" + +[env.joint] +ok = true + + [env.joint.versions] + ExaModels = "0.12.1" + CTDirect = "1.1.8-beta" + CTParser = "0.9.5-beta" + ForwardDiff = "1.4.6" + CTFlows = "0.18.1" + OrderedCollections = "2.0.3" + CTBase = "0.30.10" + RecipesBase = "1.3.4" + SolverCore = "0.3.10" + CommonSolve = "0.2.14" + MLStyle = "0.4.17" + Markdown = "stdlib" + CTLie = "0.2.3" + LinearAlgebra = "stdlib" + Printf = "stdlib" + Random = "stdlib" + SparseConnectivityTracer = "1.2.4" + CTSolvers = "0.5.8" + ADNLPModels = "0.8.15" + DocStringExtensions = "0.9.5" + StaticArrays = "1.9.23" + ADTypes = "1.24.0" + Parameters = "0.13.1" + OptimalControl = "2.2.1-beta" + Reexport = "1.2.2" + DifferentiationInterface = "0.7.21" + GPUArraysCore = "0.2.1" + SparseArrays = "1.10.0" + Unicode = "stdlib" + MacroTools = "0.5.16" + NLPModels = "0.21.12" + CTModels = "0.19.8" + KernelAbstractions = "0.9.45" + +[env."CTBase/docs"] +ok = true + + [env."CTBase/docs".versions] + RecipesBase = "1.3.4" + CommonSolve = "0.2.14" + LinearAlgebra = "stdlib" + Printf = "stdlib" + DocumenterVitepress = "0.3.8" + Logging = "stdlib" + Colors = "0.13.3" + MacroTools = "0.5.16" + OrderedCollections = "2.0.3" + Markdown = "stdlib" + Random = "stdlib" + Plots = "1.41.7" + Coverage = "1.8.2" + DocumenterInterLinks = "1.1.0" + MarkdownAST = "0.1.3" + DocStringExtensions = "0.9.5" + StaticArrays = "1.9.23" + Interpolations = "0.16.3" + DifferentiationInterface = "0.7.21" + ForwardDiff = "1.4.6" + CTBase = "0.30.10" + Makie = "0.24.16" + JET = "0.9.18" + Test = "stdlib" + ADTypes = "1.24.0" + Reexport = "1.2.2" + Documenter = "1.19.0" + CairoMakie = "0.15.16" + LiveServer = "1.5.0" + SparseArrays = "1.10.0" + Unicode = "stdlib" + +[env."CTSolvers/docs"] +ok = true + + [env."CTSolvers/docs".versions] + RecipesBase = "1.3.4" + Zygote = "0.7.13" + SolverCore = "0.3.10" + CommonSolve = "0.2.14" + NLPModelsKnitro = "0.10.6" + LinearAlgebra = "stdlib" + Printf = "stdlib" + SparseConnectivityTracer = "1.2.4" + DocumenterVitepress = "0.3.8" + Logging = "stdlib" + DiffEqBase = "7.21.4" + GPUArraysCore = "0.2.1" + SciMLBase = "3.58.0" + MacroTools = "0.5.16" + CTModels = "0.19.8" + OrderedCollections = "2.0.3" + MLStyle = "0.4.17" + Markdown = "stdlib" + OrdinaryDiffEqVerner = "2.4.2" + CUDSS = "0.8.1" + Random = "stdlib" + NLPModelsIpopt = "0.11.3" + ADNLPModels = "0.8.15" + CUDA = "6.4.1" + MadNLP = "0.10.1" + KernelAbstractions = "0.9.45" + ExaModels = "0.12.1" + UnoSolver = "0.3.8" + DocumenterInterLinks = "1.1.0" + MarkdownAST = "0.1.3" + CTSolvers = "0.5.8" + DocStringExtensions = "0.9.5" + Enzyme = "0.13.216" + StaticArrays = "1.9.23" + Parameters = "0.13.1" + DifferentiationInterface = "0.7.21" + NLPModels = "0.21.12" + MadNLPGPU = "0.10.2" + ForwardDiff = "1.4.6" + CTBase = "0.30.10" + MadNCL = "0.2.3" + JET = "0.9.18" + Test = "stdlib" + ADTypes = "1.24.0" + Reexport = "1.2.2" + Documenter = "1.19.0" + OrdinaryDiffEqTsit5 = "2.1.5" + LiveServer = "1.6.0" + SparseArrays = "1.10.0" + Unicode = "stdlib" + +[env."CTFlows/test"] +ok = true + + [env."CTFlows/test".versions] + RecipesBase = "1.3.4" + Zygote = "0.7.13" + SolverCore = "0.3.10" + CommonSolve = "0.2.14" + LinearAlgebra = "stdlib" + DiffEqGPU = "3.21.5" + Printf = "stdlib" + NonlinearSolve = "4.32.0" + Aqua = "0.8.18" + Logging = "stdlib" + DiffEqBase = "7.21.4" + GPUArraysCore = "0.2.1" + SciMLBase = "3.58.0" + Colors = "0.13.3" + MacroTools = "0.5.16" + CTModels = "0.19.8" + OrderedCollections = "2.0.3" + MLStyle = "0.4.17" + Markdown = "stdlib" + Random = "stdlib" + Plots = "1.41.7" + CUDA = "6.4.2" + KernelAbstractions = "0.9.45" + CTLie = "0.2.3" + Mooncake = "0.5.64" + CTSolvers = "0.5.8" + DocStringExtensions = "0.9.5" + StaticArrays = "1.9.23" + Parameters = "0.13.1" + Interpolations = "0.16.3" + DifferentiationInterface = "0.7.21" + ForwardDiff = "1.4.6" + CTFlows = "0.18.1" + CTBase = "0.30.10" + Makie = "0.24.16" + Test = "stdlib" + ADTypes = "1.24.0" + Reexport = "1.2.2" + OrdinaryDiffEqTsit5 = "2.1.5" + CairoMakie = "0.15.16" + SparseArrays = "1.10.0" + Unicode = "stdlib" diff --git a/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/baseline-1.12.toml b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/baseline-1.12.toml new file mode 100644 index 0000000..440566b --- /dev/null +++ b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/baseline-1.12.toml @@ -0,0 +1,699 @@ +julia = "1.12.6" + +[env."CTParser/docs"] +ok = true + + [env."CTParser/docs".versions] + CTParser = "0.9.5-beta" + OrderedCollections = "2.0.3" + CTBase = "0.30.10" + MLStyle = "0.4.17" + Markdown = "1.11.0" + Printf = "1.11.0" + Test = "1.11.0" + Random = "1.11.0" + MarkdownAST = "0.1.3" + DocStringExtensions = "0.9.5" + ADTypes = "1.24.0" + Parameters = "0.13.1" + Logging = "1.11.0" + Documenter = "1.19.0" + Unicode = "1.11.0" + +[env."CTModels/docs"] +ok = true + + [env."CTModels/docs".versions] + RecipesBase = "1.4.0" + CommonSolve = "0.2.14" + LinearAlgebra = "1.12.0" + Printf = "1.11.0" + DocumenterVitepress = "0.3.8" + Logging = "1.11.0" + Colors = "0.13.3" + MacroTools = "0.5.16" + CTModels = "0.19.8" + OrderedCollections = "2.0.3" + MLStyle = "0.4.17" + Markdown = "1.11.0" + Random = "1.11.0" + Plots = "1.41.7" + DocumenterInterLinks = "1.1.0" + MarkdownAST = "0.1.3" + DocStringExtensions = "0.9.5" + StaticArrays = "1.9.23" + Parameters = "0.13.1" + Interpolations = "0.16.3" + CTBase = "0.30.10" + JLD2 = "0.6.7" + JSON3 = "1.14.3" + Makie = "0.24.16" + JET = "0.12.3" + Test = "1.11.0" + ADTypes = "1.24.0" + Reexport = "1.2.2" + Documenter = "1.19.0" + CairoMakie = "0.15.16" + LiveServer = "1.6.0" + SparseArrays = "1.12.0" + Unicode = "1.11.0" + +[env."CTLie/test"] +ok = true + + [env."CTLie/test".versions] + ForwardDiff = "1.4.6" + OrderedCollections = "2.0.3" + CTBase = "0.30.10" + Markdown = "1.11.0" + CTLie = "0.2.3" + LinearAlgebra = "1.12.0" + JET = "0.12.3" + Mooncake = "0.5.64" + Printf = "1.11.0" + Test = "1.11.0" + Random = "1.11.0" + BenchmarkTools = "1.8.0" + Aqua = "0.8.18" + DocStringExtensions = "0.9.5" + StaticArrays = "1.9.23" + ADTypes = "1.24.0" + Logging = "1.11.0" + Reexport = "1.2.2" + CUDA = "6.4.2" + DifferentiationInterface = "0.7.21" + GPUArraysCore = "0.2.1" + SparseArrays = "1.12.0" + Unicode = "1.11.0" + MacroTools = "0.5.16" + KernelAbstractions = "0.9.45" + +[env."CTDirect/test"] +ok = true + + [env."CTDirect/test".versions] + RecipesBase = "1.4.0" + SolverCore = "0.3.10" + CommonSolve = "0.2.14" + LinearAlgebra = "1.12.0" + Printf = "1.11.0" + SparseConnectivityTracer = "1.2.4" + Logging = "1.11.0" + GPUArraysCore = "0.2.1" + MacroTools = "0.5.16" + CTModels = "0.19.8" + CTParser = "0.9.5-beta" + OrderedCollections = "2.0.3" + MLStyle = "0.4.17" + Markdown = "1.11.0" + CUDSS = "0.8.1" + Random = "1.11.0" + NLPModelsIpopt = "0.11.3" + ADNLPModels = "0.8.15" + CUDA = "6.3.1" + MadNLP = "0.10.1" + SplitApplyCombine = "1.3.0" + KernelAbstractions = "0.9.45" + ExaModels = "0.12.1" + AMDGPU = "2.8.0" + CTSolvers = "0.5.8" + DocStringExtensions = "0.9.5" + StaticArrays = "1.9.23" + Parameters = "0.13.1" + DifferentiationInterface = "0.7.21" + NLPModels = "0.21.12" + MadNLPGPU = "0.10.2" + CTDirect = "1.1.8-beta" + ForwardDiff = "1.4.6" + CTBase = "0.30.10" + Test = "1.11.0" + ADTypes = "1.24.0" + Reexport = "1.2.2" + SparseArrays = "1.12.0" + Unicode = "1.11.0" + +[env."CTDirect/docs"] +ok = true + + [env."CTDirect/docs".versions] + ExaModels = "0.12.1" + CTDirect = "1.1.8-beta" + CTParser = "0.9.5-beta" + ForwardDiff = "1.4.6" + OrderedCollections = "2.0.3" + CTBase = "0.30.10" + RecipesBase = "1.4.0" + SolverCore = "0.3.10" + CommonSolve = "0.2.14" + Markdown = "1.11.0" + MLStyle = "0.4.17" + LinearAlgebra = "1.12.0" + Printf = "1.11.0" + Test = "1.11.0" + Random = "1.11.0" + SparseConnectivityTracer = "1.2.4" + MarkdownAST = "0.1.3" + CTSolvers = "0.5.8" + DocumenterMermaid = "0.2.1" + ADNLPModels = "0.8.15" + DocStringExtensions = "0.9.5" + StaticArrays = "1.9.23" + ADTypes = "1.24.0" + Parameters = "0.13.1" + Logging = "1.11.0" + Reexport = "1.2.2" + Documenter = "1.19.0" + DifferentiationInterface = "0.7.21" + SparseArrays = "1.12.0" + Unicode = "1.11.0" + MacroTools = "0.5.16" + NLPModels = "0.21.12" + CTModels = "0.19.8" + KernelAbstractions = "0.9.45" + +[env."CTFlows/docs"] +ok = true + + [env."CTFlows/docs".versions] + RecipesBase = "1.4.0" + SolverCore = "0.3.10" + CommonSolve = "0.2.14" + LinearAlgebra = "1.12.0" + Printf = "1.11.0" + DocumenterVitepress = "0.3.8" + Logging = "1.11.0" + DiffEqBase = "7.21.4" + GPUArraysCore = "0.2.1" + SciMLBase = "3.58.0" + Colors = "0.13.3" + MacroTools = "0.5.16" + CTModels = "0.19.8" + OrderedCollections = "2.0.3" + MLStyle = "0.4.17" + Markdown = "1.11.0" + OrdinaryDiffEqRosenbrock = "2.7.5" + Random = "1.11.0" + Plots = "1.41.7" + KernelAbstractions = "0.9.45" + DocumenterInterLinks = "1.1.0" + MarkdownAST = "0.1.3" + CTSolvers = "0.5.8" + DocStringExtensions = "0.9.5" + StaticArrays = "1.9.23" + Parameters = "0.13.1" + Interpolations = "0.16.3" + DifferentiationInterface = "0.7.21" + ForwardDiff = "1.4.6" + CTFlows = "0.18.1" + CTBase = "0.30.10" + Makie = "0.24.16" + Test = "1.11.0" + ADTypes = "1.24.0" + Reexport = "1.2.2" + Documenter = "1.19.0" + OrdinaryDiffEqTsit5 = "2.1.5" + CairoMakie = "0.15.16" + LiveServer = "1.6.0" + SparseArrays = "1.12.0" + Unicode = "1.11.0" + +[env."OptimalControl/docs"] +ok = true + + [env."OptimalControl/docs".versions] + RecipesBase = "1.4.0" + SolverCore = "0.3.10" + CommonSolve = "0.2.14" + NLPModelsKnitro = "0.10.6" + LinearAlgebra = "1.12.0" + Printf = "1.11.0" + NonlinearSolve = "4.32.0" + SparseConnectivityTracer = "1.2.4" + DocumenterVitepress = "0.3.8" + Logging = "1.11.0" + DiffEqBase = "7.21.4" + GPUArraysCore = "0.2.1" + SciMLBase = "3.58.0" + Colors = "0.13.3" + MacroTools = "0.5.16" + CTModels = "0.19.8" + CTParser = "0.9.5-beta" + OrderedCollections = "2.0.3" + MLStyle = "0.4.17" + Markdown = "1.11.0" + OrdinaryDiffEqVerner = "2.4.2" + OrdinaryDiffEqRosenbrock = "2.7.5" + CUDSS = "0.7.0" + Random = "1.11.0" + NLPModelsIpopt = "0.11.3" + Plots = "1.41.7" + ADNLPModels = "0.8.15" + CUDA = "6.2.0" + MadNLP = "0.10.1" + KernelAbstractions = "0.9.45" + ExaModels = "0.12.1" + Literate = "2.21.0" + OrdinaryDiffEq = "7.8.1" + CTLie = "0.2.3" + UnoSolver = "0.3.8" + DocumenterInterLinks = "1.1.0" + MarkdownAST = "0.1.3" + CTSolvers = "0.5.8" + DocStringExtensions = "0.9.5" + StaticArrays = "1.9.23" + Parameters = "0.13.1" + OptimalControl = "2.2.1-beta" + Interpolations = "0.16.3" + DifferentiationInterface = "0.7.21" + NLPModels = "0.21.12" + MadNLPGPU = "0.10.2" + CTDirect = "1.1.8-beta" + ForwardDiff = "1.4.6" + CTFlows = "0.18.1" + CTBase = "0.30.10" + JLD2 = "0.6.7" + JSON3 = "1.14.3" + Makie = "0.24.16" + MadNCL = "0.2.3" + Test = "1.11.0" + ADTypes = "1.24.0" + Reexport = "1.2.2" + Documenter = "1.19.0" + OrdinaryDiffEqTsit5 = "2.1.5" + CairoMakie = "0.15.16" + LiveServer = "1.6.0" + SparseArrays = "1.12.0" + Unicode = "1.11.0" + +[env."CTModels/test"] +ok = true + + [env."CTModels/test".versions] + RecipesBase = "1.4.0" + CommonSolve = "0.2.14" + LinearAlgebra = "1.12.0" + Printf = "1.11.0" + Aqua = "0.8.18" + Logging = "1.11.0" + Colors = "0.13.3" + MacroTools = "0.5.16" + CTModels = "0.19.8" + OrderedCollections = "2.0.3" + MLStyle = "0.4.17" + Markdown = "1.11.0" + Random = "1.11.0" + BenchmarkTools = "1.8.0" + Plots = "1.41.7" + DocStringExtensions = "0.9.5" + StaticArrays = "1.9.23" + Parameters = "0.13.1" + Interpolations = "0.16.3" + CTBase = "0.30.10" + JLD2 = "0.6.7" + JSON3 = "1.14.3" + Makie = "0.24.16" + JET = "0.12.3" + Test = "1.11.0" + ADTypes = "1.24.0" + Reexport = "1.2.2" + CairoMakie = "0.15.16" + SparseArrays = "1.12.0" + Unicode = "1.11.0" + +[env."CTLie/docs"] +ok = true + + [env."CTLie/docs".versions] + ForwardDiff = "1.4.6" + CTBase = "0.30.10" + Markdown = "1.11.0" + CTLie = "0.2.3" + LinearAlgebra = "1.12.0" + Printf = "1.11.0" + Test = "1.11.0" + DocumenterInterLinks = "1.1.0" + Random = "1.11.0" + MarkdownAST = "0.1.3" + DocStringExtensions = "0.9.5" + DocumenterVitepress = "0.3.8" + ADTypes = "1.24.0" + Logging = "1.11.0" + Documenter = "1.19.0" + DifferentiationInterface = "0.7.21" + LiveServer = "1.6.0" + Unicode = "1.11.0" + MacroTools = "0.5.16" + +[env."CTSolvers/test"] +ok = true + + [env."CTSolvers/test".versions] + RecipesBase = "1.4.0" + Zygote = "0.7.13" + SolverCore = "0.3.10" + CommonSolve = "0.2.14" + LinearAlgebra = "1.12.0" + Printf = "1.11.0" + SparseConnectivityTracer = "1.2.4" + Aqua = "0.8.18" + Logging = "1.11.0" + DiffEqBase = "7.21.4" + GPUArraysCore = "0.2.1" + SciMLBase = "3.58.0" + MacroTools = "0.5.16" + CTModels = "0.19.8" + OrderedCollections = "2.0.3" + MLStyle = "0.4.17" + Markdown = "1.11.0" + CUDSS = "0.8.1" + Random = "1.11.0" + BenchmarkTools = "1.8.0" + NLPModelsIpopt = "0.11.3" + ADNLPModels = "0.8.15" + CUDA = "6.4.1" + MadNLP = "0.10.1" + KernelAbstractions = "0.9.45" + ExaModels = "0.12.1" + UnoSolver = "0.3.8" + CTSolvers = "0.5.8" + DocStringExtensions = "0.9.5" + Enzyme = "0.13.216" + StaticArrays = "1.9.23" + Parameters = "0.13.1" + DifferentiationInterface = "0.7.21" + NLPModels = "0.21.12" + MadNLPGPU = "0.10.2" + ForwardDiff = "1.4.6" + CTBase = "0.30.10" + MadNCL = "0.2.3" + JET = "0.12.3" + Test = "1.11.0" + ADTypes = "1.24.0" + Reexport = "1.2.2" + OrdinaryDiffEqTsit5 = "2.1.5" + SparseArrays = "1.12.0" + Unicode = "1.11.0" + +[env."CTParser/test"] +ok = true + + [env."CTParser/test".versions] + ExaModels = "0.12.1" + CTParser = "0.9.5-beta" + OrderedCollections = "2.0.3" + CTBase = "0.29.3" + RecipesBase = "1.4.0" + MLStyle = "0.4.17" + Markdown = "1.11.0" + SolverCore = "0.3.10" + LinearAlgebra = "1.12.0" + Printf = "1.11.0" + CUDSS = "0.8.1" + Test = "1.11.0" + Random = "1.11.0" + BenchmarkTools = "1.8.0" + Aqua = "0.8.18" + DocStringExtensions = "0.9.5" + StaticArrays = "1.9.23" + ADTypes = "1.24.0" + Parameters = "0.13.1" + Logging = "1.11.0" + Reexport = "1.2.2" + CUDA = "6.4.2" + MadNLP = "0.10.1" + Interpolations = "0.16.3" + GPUArraysCore = "0.2.1" + SparseArrays = "1.12.0" + Unicode = "1.11.0" + MacroTools = "0.5.16" + NLPModels = "0.21.12" + CTModels = "0.18.0" + KernelAbstractions = "0.9.45" + MadNLPGPU = "0.10.2" + +[env."OptimalControl/test"] +ok = true + + [env."OptimalControl/test".versions] + RecipesBase = "1.4.0" + SolverCore = "0.3.10" + CommonSolve = "0.2.14" + LinearAlgebra = "1.12.0" + Printf = "1.11.0" + NonlinearSolve = "4.32.0" + SparseConnectivityTracer = "1.2.4" + Logging = "1.11.0" + DiffEqBase = "7.21.4" + GPUArraysCore = "0.2.1" + SciMLBase = "3.58.0" + MacroTools = "0.5.16" + CTModels = "0.19.8" + CTParser = "0.9.5-beta" + OrderedCollections = "2.0.3" + MLStyle = "0.4.17" + Markdown = "1.11.0" + OrdinaryDiffEqVerner = "2.4.2" + OrdinaryDiffEqRosenbrock = "2.7.5" + CUDSS = "0.7.0" + Random = "1.11.0" + BenchmarkTools = "1.8.0" + NLPModelsIpopt = "0.11.3" + ADNLPModels = "0.8.15" + CUDA = "6.2.0" + MadNLP = "0.10.1" + SplitApplyCombine = "1.3.0" + KernelAbstractions = "0.9.45" + ExaModels = "0.12.1" + OrdinaryDiffEq = "7.8.1" + CTLie = "0.2.3" + UnoSolver = "0.3.8" + CTSolvers = "0.5.8" + DocStringExtensions = "0.9.5" + StaticArrays = "1.9.23" + Parameters = "0.13.1" + OptimalControl = "2.2.1-beta" + DifferentiationInterface = "0.7.21" + NLPModels = "0.21.12" + MadNLPGPU = "0.10.2" + CTDirect = "1.1.8-beta" + ForwardDiff = "1.4.6" + CTFlows = "0.18.1" + CTBase = "0.30.10" + MadNCL = "0.2.3" + Test = "1.11.0" + ADTypes = "1.24.0" + Reexport = "1.2.2" + OrdinaryDiffEqTsit5 = "2.1.5" + SparseArrays = "1.12.0" + Unicode = "1.11.0" + +[env."CTBase/test"] +ok = true + + [env."CTBase/test".versions] + RecipesBase = "1.4.0" + CommonSolve = "0.2.14" + LinearAlgebra = "1.12.0" + Printf = "1.11.0" + Aqua = "0.8.18" + Logging = "1.11.0" + Colors = "0.13.3" + MacroTools = "0.5.16" + OrderedCollections = "2.0.3" + Markdown = "1.11.0" + Random = "1.11.0" + BenchmarkTools = "1.8.0" + Plots = "1.41.7" + Coverage = "1.8.2" + MarkdownAST = "0.1.3" + DocStringExtensions = "0.9.5" + StaticArrays = "1.9.23" + Interpolations = "0.16.3" + DifferentiationInterface = "0.7.21" + ForwardDiff = "1.4.6" + CTBase = "0.30.10" + Makie = "0.24.16" + JET = "0.12.3" + Test = "1.11.0" + ADTypes = "1.24.0" + Reexport = "1.2.2" + Documenter = "1.19.0" + CairoMakie = "0.15.16" + SparseArrays = "1.12.0" + Unicode = "1.11.0" + +[env.joint] +ok = true + + [env.joint.versions] + ExaModels = "0.12.1" + CTDirect = "1.1.8-beta" + CTParser = "0.9.5-beta" + ForwardDiff = "1.4.6" + CTFlows = "0.18.1" + OrderedCollections = "2.0.3" + CTBase = "0.30.10" + RecipesBase = "1.4.0" + SolverCore = "0.3.10" + CommonSolve = "0.2.14" + MLStyle = "0.4.17" + Markdown = "1.11.0" + CTLie = "0.2.3" + LinearAlgebra = "1.12.0" + Printf = "1.11.0" + Random = "1.11.0" + SparseConnectivityTracer = "1.2.4" + CTSolvers = "0.5.8" + ADNLPModels = "0.8.15" + DocStringExtensions = "0.9.5" + StaticArrays = "1.9.23" + ADTypes = "1.24.0" + Parameters = "0.13.1" + OptimalControl = "2.2.1-beta" + Reexport = "1.2.2" + DifferentiationInterface = "0.7.21" + GPUArraysCore = "0.2.1" + SparseArrays = "1.12.0" + Unicode = "1.11.0" + MacroTools = "0.5.16" + NLPModels = "0.21.12" + CTModels = "0.19.8" + KernelAbstractions = "0.9.45" + +[env."CTBase/docs"] +ok = true + + [env."CTBase/docs".versions] + RecipesBase = "1.4.0" + CommonSolve = "0.2.14" + LinearAlgebra = "1.12.0" + Printf = "1.11.0" + DocumenterVitepress = "0.3.8" + Logging = "1.11.0" + Colors = "0.13.3" + MacroTools = "0.5.16" + OrderedCollections = "2.0.3" + Markdown = "1.11.0" + Random = "1.11.0" + Plots = "1.41.7" + Coverage = "1.8.2" + DocumenterInterLinks = "1.1.0" + MarkdownAST = "0.1.3" + DocStringExtensions = "0.9.5" + StaticArrays = "1.9.23" + Interpolations = "0.16.3" + DifferentiationInterface = "0.7.21" + ForwardDiff = "1.4.6" + CTBase = "0.30.10" + Makie = "0.24.16" + JET = "0.12.3" + Test = "1.11.0" + ADTypes = "1.24.0" + Reexport = "1.2.2" + Documenter = "1.19.0" + CairoMakie = "0.15.16" + LiveServer = "1.5.0" + SparseArrays = "1.12.0" + Unicode = "1.11.0" + +[env."CTSolvers/docs"] +ok = true + + [env."CTSolvers/docs".versions] + RecipesBase = "1.4.0" + Zygote = "0.7.13" + SolverCore = "0.3.10" + CommonSolve = "0.2.14" + NLPModelsKnitro = "0.10.6" + LinearAlgebra = "1.12.0" + Printf = "1.11.0" + SparseConnectivityTracer = "1.2.4" + DocumenterVitepress = "0.3.8" + Logging = "1.11.0" + DiffEqBase = "7.21.4" + GPUArraysCore = "0.2.1" + SciMLBase = "3.58.0" + MacroTools = "0.5.16" + CTModels = "0.19.8" + OrderedCollections = "2.0.3" + MLStyle = "0.4.17" + Markdown = "1.11.0" + OrdinaryDiffEqVerner = "2.4.2" + CUDSS = "0.8.1" + Random = "1.11.0" + NLPModelsIpopt = "0.11.3" + ADNLPModels = "0.8.15" + CUDA = "6.4.1" + MadNLP = "0.10.1" + KernelAbstractions = "0.9.45" + ExaModels = "0.12.1" + UnoSolver = "0.3.8" + DocumenterInterLinks = "1.1.0" + MarkdownAST = "0.1.3" + CTSolvers = "0.5.8" + DocStringExtensions = "0.9.5" + Enzyme = "0.13.216" + StaticArrays = "1.9.23" + Parameters = "0.13.1" + DifferentiationInterface = "0.7.21" + NLPModels = "0.21.12" + MadNLPGPU = "0.10.2" + ForwardDiff = "1.4.6" + CTBase = "0.30.10" + MadNCL = "0.2.3" + JET = "0.12.3" + Test = "1.11.0" + ADTypes = "1.24.0" + Reexport = "1.2.2" + Documenter = "1.19.0" + OrdinaryDiffEqTsit5 = "2.1.5" + LiveServer = "1.6.0" + SparseArrays = "1.12.0" + Unicode = "1.11.0" + +[env."CTFlows/test"] +ok = true + + [env."CTFlows/test".versions] + RecipesBase = "1.4.0" + Zygote = "0.7.13" + SolverCore = "0.3.10" + CommonSolve = "0.2.14" + LinearAlgebra = "1.12.0" + DiffEqGPU = "3.21.5" + Printf = "1.11.0" + NonlinearSolve = "4.32.0" + Aqua = "0.8.18" + Logging = "1.11.0" + DiffEqBase = "7.21.4" + GPUArraysCore = "0.2.1" + SciMLBase = "3.58.0" + Colors = "0.13.3" + MacroTools = "0.5.16" + CTModels = "0.19.8" + OrderedCollections = "2.0.3" + MLStyle = "0.4.17" + Markdown = "1.11.0" + Random = "1.11.0" + Plots = "1.41.7" + CUDA = "6.4.2" + KernelAbstractions = "0.9.45" + CTLie = "0.2.3" + Mooncake = "0.5.64" + CTSolvers = "0.5.8" + DocStringExtensions = "0.9.5" + StaticArrays = "1.9.23" + Parameters = "0.13.1" + Interpolations = "0.16.3" + DifferentiationInterface = "0.7.21" + ForwardDiff = "1.4.6" + CTFlows = "0.18.1" + CTBase = "0.30.10" + Makie = "0.24.16" + Test = "1.11.0" + ADTypes = "1.24.0" + Reexport = "1.2.2" + OrdinaryDiffEqTsit5 = "2.1.5" + CairoMakie = "0.15.16" + SparseArrays = "1.12.0" + Unicode = "1.11.0" diff --git a/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/baseline-1.13.toml b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/baseline-1.13.toml new file mode 100644 index 0000000..2bb8d9e --- /dev/null +++ b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/baseline-1.13.toml @@ -0,0 +1,699 @@ +julia = "1.13.1" + +[env."CTParser/docs"] +ok = true + + [env."CTParser/docs".versions] + CTParser = "0.9.5-beta" + OrderedCollections = "2.0.3" + CTBase = "0.30.10" + MLStyle = "0.4.17" + Markdown = "1.11.0" + Printf = "1.11.0" + Test = "1.11.0" + Random = "1.11.0" + MarkdownAST = "0.1.3" + DocStringExtensions = "0.9.5" + ADTypes = "1.24.0" + Parameters = "0.13.1" + Logging = "1.11.0" + Documenter = "1.19.0" + Unicode = "1.11.0" + +[env."CTModels/docs"] +ok = true + + [env."CTModels/docs".versions] + RecipesBase = "1.4.0" + CommonSolve = "0.2.14" + LinearAlgebra = "1.13.0" + Printf = "1.11.0" + DocumenterVitepress = "0.3.8" + Logging = "1.11.0" + Colors = "0.13.3" + MacroTools = "0.5.16" + CTModels = "0.19.8" + OrderedCollections = "2.0.3" + MLStyle = "0.4.17" + Markdown = "1.11.0" + Random = "1.11.0" + Plots = "1.41.7" + DocumenterInterLinks = "1.1.0" + MarkdownAST = "0.1.3" + DocStringExtensions = "0.9.5" + StaticArrays = "1.9.23" + Parameters = "0.13.1" + Interpolations = "0.16.3" + CTBase = "0.30.10" + JLD2 = "0.6.7" + JSON3 = "1.14.3" + Makie = "0.24.16" + JET = "0.12.3" + Test = "1.11.0" + ADTypes = "1.24.0" + Reexport = "1.2.2" + Documenter = "1.19.0" + CairoMakie = "0.15.16" + SparseArrays = "1.13.0" + LiveServer = "1.6.0" + Unicode = "1.11.0" + +[env."CTLie/test"] +ok = true + + [env."CTLie/test".versions] + ForwardDiff = "1.4.6" + OrderedCollections = "2.0.3" + CTBase = "0.30.10" + Markdown = "1.11.0" + CTLie = "0.2.3" + LinearAlgebra = "1.13.0" + JET = "0.12.3" + Mooncake = "0.5.64" + Printf = "1.11.0" + Test = "1.11.0" + Random = "1.11.0" + BenchmarkTools = "1.8.0" + Aqua = "0.8.18" + DocStringExtensions = "0.9.5" + StaticArrays = "1.9.23" + ADTypes = "1.24.0" + Logging = "1.11.0" + Reexport = "1.2.2" + CUDA = "6.4.2" + DifferentiationInterface = "0.7.21" + GPUArraysCore = "0.2.1" + SparseArrays = "1.13.0" + Unicode = "1.11.0" + MacroTools = "0.5.16" + KernelAbstractions = "0.9.45" + +[env."CTDirect/test"] +ok = true + + [env."CTDirect/test".versions] + RecipesBase = "1.4.0" + SolverCore = "0.3.10" + CommonSolve = "0.2.14" + LinearAlgebra = "1.13.0" + Printf = "1.11.0" + SparseConnectivityTracer = "1.2.4" + Logging = "1.11.0" + GPUArraysCore = "0.2.1" + MacroTools = "0.5.16" + CTModels = "0.19.8" + CTParser = "0.9.5-beta" + OrderedCollections = "2.0.3" + MLStyle = "0.4.17" + Markdown = "1.11.0" + CUDSS = "0.8.1" + Random = "1.11.0" + NLPModelsIpopt = "0.11.3" + ADNLPModels = "0.8.15" + CUDA = "6.3.1" + MadNLP = "0.10.1" + SplitApplyCombine = "1.3.0" + KernelAbstractions = "0.9.45" + ExaModels = "0.12.1" + AMDGPU = "2.8.0" + CTSolvers = "0.5.8" + DocStringExtensions = "0.9.5" + StaticArrays = "1.9.23" + Parameters = "0.13.1" + DifferentiationInterface = "0.7.21" + NLPModels = "0.21.12" + MadNLPGPU = "0.10.2" + CTDirect = "1.1.8-beta" + ForwardDiff = "1.4.6" + CTBase = "0.30.10" + Test = "1.11.0" + ADTypes = "1.24.0" + Reexport = "1.2.2" + SparseArrays = "1.13.0" + Unicode = "1.11.0" + +[env."CTDirect/docs"] +ok = true + + [env."CTDirect/docs".versions] + ExaModels = "0.12.1" + CTDirect = "1.1.8-beta" + CTParser = "0.9.5-beta" + ForwardDiff = "1.4.6" + OrderedCollections = "2.0.3" + CTBase = "0.30.10" + RecipesBase = "1.4.0" + SolverCore = "0.3.10" + MLStyle = "0.4.17" + CommonSolve = "0.2.14" + Markdown = "1.11.0" + LinearAlgebra = "1.13.0" + Printf = "1.11.0" + Test = "1.11.0" + Random = "1.11.0" + SparseConnectivityTracer = "1.2.4" + MarkdownAST = "0.1.3" + CTSolvers = "0.5.8" + DocumenterMermaid = "0.2.1" + ADNLPModels = "0.8.15" + DocStringExtensions = "0.9.5" + StaticArrays = "1.9.23" + ADTypes = "1.24.0" + Parameters = "0.13.1" + Logging = "1.11.0" + Reexport = "1.2.2" + Documenter = "1.19.0" + DifferentiationInterface = "0.7.21" + SparseArrays = "1.13.0" + Unicode = "1.11.0" + MacroTools = "0.5.16" + NLPModels = "0.21.12" + CTModels = "0.19.8" + KernelAbstractions = "0.9.45" + +[env."CTFlows/docs"] +ok = true + + [env."CTFlows/docs".versions] + RecipesBase = "1.4.0" + SolverCore = "0.3.10" + CommonSolve = "0.2.14" + LinearAlgebra = "1.13.0" + Printf = "1.11.0" + DocumenterVitepress = "0.3.8" + Logging = "1.11.0" + DiffEqBase = "7.21.4" + GPUArraysCore = "0.2.1" + SciMLBase = "3.58.0" + Colors = "0.13.3" + MacroTools = "0.5.16" + CTModels = "0.19.8" + OrderedCollections = "2.0.3" + MLStyle = "0.4.17" + Markdown = "1.11.0" + OrdinaryDiffEqRosenbrock = "2.7.5" + Random = "1.11.0" + Plots = "1.41.7" + KernelAbstractions = "0.9.45" + DocumenterInterLinks = "1.1.0" + MarkdownAST = "0.1.3" + CTSolvers = "0.5.8" + DocStringExtensions = "0.9.5" + StaticArrays = "1.9.23" + Parameters = "0.13.1" + Interpolations = "0.16.3" + DifferentiationInterface = "0.7.21" + ForwardDiff = "1.4.6" + CTFlows = "0.18.1" + CTBase = "0.30.10" + Makie = "0.24.16" + Test = "1.11.0" + ADTypes = "1.24.0" + Reexport = "1.2.2" + OrdinaryDiffEqTsit5 = "2.1.5" + Documenter = "1.19.0" + CairoMakie = "0.15.16" + SparseArrays = "1.13.0" + LiveServer = "1.6.0" + Unicode = "1.11.0" + +[env."OptimalControl/docs"] +ok = true + + [env."OptimalControl/docs".versions] + RecipesBase = "1.4.0" + SolverCore = "0.3.10" + CommonSolve = "0.2.14" + NLPModelsKnitro = "0.10.6" + LinearAlgebra = "1.13.0" + Printf = "1.11.0" + NonlinearSolve = "4.32.0" + SparseConnectivityTracer = "1.2.4" + DocumenterVitepress = "0.3.8" + Logging = "1.11.0" + DiffEqBase = "7.21.4" + GPUArraysCore = "0.2.1" + SciMLBase = "3.58.0" + Colors = "0.13.3" + MacroTools = "0.5.16" + CTModels = "0.19.8" + CTParser = "0.9.5-beta" + OrderedCollections = "2.0.3" + MLStyle = "0.4.17" + Markdown = "1.11.0" + OrdinaryDiffEqVerner = "2.4.2" + OrdinaryDiffEqRosenbrock = "2.7.5" + CUDSS = "0.7.0" + Random = "1.11.0" + NLPModelsIpopt = "0.11.3" + Plots = "1.41.7" + ADNLPModels = "0.8.15" + CUDA = "6.2.0" + MadNLP = "0.10.1" + KernelAbstractions = "0.9.45" + ExaModels = "0.12.1" + Literate = "2.21.0" + OrdinaryDiffEq = "7.8.1" + CTLie = "0.2.3" + UnoSolver = "0.3.8" + DocumenterInterLinks = "1.1.0" + MarkdownAST = "0.1.3" + CTSolvers = "0.5.8" + DocStringExtensions = "0.9.5" + StaticArrays = "1.9.23" + Parameters = "0.13.1" + OptimalControl = "2.2.1-beta" + Interpolations = "0.16.3" + DifferentiationInterface = "0.7.21" + NLPModels = "0.21.12" + MadNLPGPU = "0.10.2" + CTDirect = "1.1.8-beta" + ForwardDiff = "1.4.6" + CTFlows = "0.18.1" + CTBase = "0.30.10" + JLD2 = "0.6.7" + JSON3 = "1.14.3" + Makie = "0.24.16" + MadNCL = "0.2.3" + Test = "1.11.0" + ADTypes = "1.24.0" + Reexport = "1.2.2" + OrdinaryDiffEqTsit5 = "2.1.5" + Documenter = "1.19.0" + CairoMakie = "0.15.16" + SparseArrays = "1.13.0" + LiveServer = "1.6.0" + Unicode = "1.11.0" + +[env."CTModels/test"] +ok = true + + [env."CTModels/test".versions] + RecipesBase = "1.4.0" + CommonSolve = "0.2.14" + LinearAlgebra = "1.13.0" + Printf = "1.11.0" + Aqua = "0.8.18" + Logging = "1.11.0" + Colors = "0.13.3" + MacroTools = "0.5.16" + CTModels = "0.19.8" + OrderedCollections = "2.0.3" + MLStyle = "0.4.17" + Markdown = "1.11.0" + Random = "1.11.0" + BenchmarkTools = "1.8.0" + Plots = "1.41.7" + DocStringExtensions = "0.9.5" + StaticArrays = "1.9.23" + Parameters = "0.13.1" + Interpolations = "0.16.3" + CTBase = "0.30.10" + JLD2 = "0.6.7" + JSON3 = "1.14.3" + Makie = "0.24.16" + JET = "0.12.3" + Test = "1.11.0" + ADTypes = "1.24.0" + Reexport = "1.2.2" + CairoMakie = "0.15.16" + SparseArrays = "1.13.0" + Unicode = "1.11.0" + +[env."CTSolvers/test"] +ok = true + + [env."CTSolvers/test".versions] + RecipesBase = "1.4.0" + Zygote = "0.7.13" + SolverCore = "0.3.10" + CommonSolve = "0.2.14" + LinearAlgebra = "1.13.0" + Printf = "1.11.0" + SparseConnectivityTracer = "1.2.4" + Aqua = "0.8.18" + Logging = "1.11.0" + DiffEqBase = "7.21.4" + GPUArraysCore = "0.2.1" + SciMLBase = "3.58.0" + MacroTools = "0.5.16" + CTModels = "0.19.8" + OrderedCollections = "2.0.3" + MLStyle = "0.4.17" + Markdown = "1.11.0" + CUDSS = "0.8.1" + Random = "1.11.0" + BenchmarkTools = "1.8.0" + NLPModelsIpopt = "0.11.3" + ADNLPModels = "0.8.15" + CUDA = "6.4.1" + MadNLP = "0.10.1" + KernelAbstractions = "0.9.45" + ExaModels = "0.12.1" + UnoSolver = "0.3.8" + CTSolvers = "0.5.8" + DocStringExtensions = "0.9.5" + Enzyme = "0.13.216" + StaticArrays = "1.9.23" + Parameters = "0.13.1" + DifferentiationInterface = "0.7.21" + NLPModels = "0.21.12" + MadNLPGPU = "0.10.2" + ForwardDiff = "1.4.6" + CTBase = "0.30.10" + MadNCL = "0.2.3" + JET = "0.12.3" + Test = "1.11.0" + ADTypes = "1.24.0" + Reexport = "1.2.2" + OrdinaryDiffEqTsit5 = "2.1.5" + SparseArrays = "1.13.0" + Unicode = "1.11.0" + +[env."CTLie/docs"] +ok = true + + [env."CTLie/docs".versions] + ForwardDiff = "1.4.6" + CTBase = "0.30.10" + Markdown = "1.11.0" + CTLie = "0.2.3" + LinearAlgebra = "1.13.0" + Printf = "1.11.0" + Test = "1.11.0" + DocumenterInterLinks = "1.1.0" + Random = "1.11.0" + MarkdownAST = "0.1.3" + DocStringExtensions = "0.9.5" + DocumenterVitepress = "0.3.8" + ADTypes = "1.24.0" + Logging = "1.11.0" + Documenter = "1.19.0" + DifferentiationInterface = "0.7.21" + LiveServer = "1.6.0" + Unicode = "1.11.0" + MacroTools = "0.5.16" + +[env."CTParser/test"] +ok = true + + [env."CTParser/test".versions] + ExaModels = "0.12.1" + CTParser = "0.9.5-beta" + OrderedCollections = "2.0.3" + CTBase = "0.29.3" + RecipesBase = "1.4.0" + SolverCore = "0.3.10" + MLStyle = "0.4.17" + Markdown = "1.11.0" + LinearAlgebra = "1.13.0" + Printf = "1.11.0" + CUDSS = "0.8.1" + Test = "1.11.0" + Random = "1.11.0" + BenchmarkTools = "1.8.0" + Aqua = "0.8.18" + DocStringExtensions = "0.9.5" + StaticArrays = "1.9.23" + ADTypes = "1.24.0" + Parameters = "0.13.1" + Logging = "1.11.0" + Reexport = "1.2.2" + CUDA = "6.4.2" + MadNLP = "0.10.1" + Interpolations = "0.16.3" + GPUArraysCore = "0.2.1" + SparseArrays = "1.13.0" + Unicode = "1.11.0" + MacroTools = "0.5.16" + NLPModels = "0.21.12" + CTModels = "0.18.0" + KernelAbstractions = "0.9.45" + MadNLPGPU = "0.10.2" + +[env."OptimalControl/test"] +ok = true + + [env."OptimalControl/test".versions] + RecipesBase = "1.4.0" + SolverCore = "0.3.10" + CommonSolve = "0.2.14" + LinearAlgebra = "1.13.0" + Printf = "1.11.0" + NonlinearSolve = "4.32.0" + SparseConnectivityTracer = "1.2.4" + Logging = "1.11.0" + DiffEqBase = "7.21.4" + GPUArraysCore = "0.2.1" + SciMLBase = "3.58.0" + MacroTools = "0.5.16" + CTModels = "0.19.8" + CTParser = "0.9.5-beta" + OrderedCollections = "2.0.3" + MLStyle = "0.4.17" + Markdown = "1.11.0" + OrdinaryDiffEqVerner = "2.4.2" + OrdinaryDiffEqRosenbrock = "2.7.5" + CUDSS = "0.7.0" + Random = "1.11.0" + BenchmarkTools = "1.8.0" + NLPModelsIpopt = "0.11.3" + ADNLPModels = "0.8.15" + CUDA = "6.2.0" + MadNLP = "0.10.1" + SplitApplyCombine = "1.3.0" + KernelAbstractions = "0.9.45" + ExaModels = "0.12.1" + OrdinaryDiffEq = "7.8.1" + CTLie = "0.2.3" + UnoSolver = "0.3.8" + CTSolvers = "0.5.8" + DocStringExtensions = "0.9.5" + StaticArrays = "1.9.23" + Parameters = "0.13.1" + OptimalControl = "2.2.1-beta" + DifferentiationInterface = "0.7.21" + NLPModels = "0.21.12" + MadNLPGPU = "0.10.2" + CTDirect = "1.1.8-beta" + ForwardDiff = "1.4.6" + CTFlows = "0.18.1" + CTBase = "0.30.10" + MadNCL = "0.2.3" + Test = "1.11.0" + ADTypes = "1.24.0" + Reexport = "1.2.2" + OrdinaryDiffEqTsit5 = "2.1.5" + SparseArrays = "1.13.0" + Unicode = "1.11.0" + +[env."CTBase/test"] +ok = true + + [env."CTBase/test".versions] + RecipesBase = "1.4.0" + CommonSolve = "0.2.14" + LinearAlgebra = "1.13.0" + Printf = "1.11.0" + Aqua = "0.8.18" + Logging = "1.11.0" + Colors = "0.13.3" + MacroTools = "0.5.16" + OrderedCollections = "2.0.3" + Markdown = "1.11.0" + Random = "1.11.0" + BenchmarkTools = "1.8.0" + Plots = "1.41.7" + Coverage = "1.8.2" + MarkdownAST = "0.1.3" + DocStringExtensions = "0.9.5" + StaticArrays = "1.9.23" + Interpolations = "0.16.3" + DifferentiationInterface = "0.7.21" + ForwardDiff = "1.4.6" + CTBase = "0.30.10" + Makie = "0.24.16" + JET = "0.12.3" + Test = "1.11.0" + ADTypes = "1.24.0" + Reexport = "1.2.2" + Documenter = "1.19.0" + CairoMakie = "0.15.16" + SparseArrays = "1.13.0" + Unicode = "1.11.0" + +[env.joint] +ok = true + + [env.joint.versions] + ExaModels = "0.12.1" + CTDirect = "1.1.8-beta" + CTParser = "0.9.5-beta" + ForwardDiff = "1.4.6" + CTFlows = "0.18.1" + OrderedCollections = "2.0.3" + CTBase = "0.30.10" + RecipesBase = "1.4.0" + SolverCore = "0.3.10" + MLStyle = "0.4.17" + CommonSolve = "0.2.14" + Markdown = "1.11.0" + CTLie = "0.2.3" + LinearAlgebra = "1.13.0" + Printf = "1.11.0" + Random = "1.11.0" + SparseConnectivityTracer = "1.2.4" + CTSolvers = "0.5.8" + ADNLPModels = "0.8.15" + DocStringExtensions = "0.9.5" + StaticArrays = "1.9.23" + ADTypes = "1.24.0" + Parameters = "0.13.1" + Reexport = "1.2.2" + OptimalControl = "2.2.1-beta" + DifferentiationInterface = "0.7.21" + GPUArraysCore = "0.2.1" + SparseArrays = "1.13.0" + Unicode = "1.11.0" + MacroTools = "0.5.16" + NLPModels = "0.21.12" + CTModels = "0.19.8" + KernelAbstractions = "0.9.45" + +[env."CTBase/docs"] +ok = true + + [env."CTBase/docs".versions] + RecipesBase = "1.4.0" + CommonSolve = "0.2.14" + LinearAlgebra = "1.13.0" + Printf = "1.11.0" + DocumenterVitepress = "0.3.8" + Logging = "1.11.0" + Colors = "0.13.3" + MacroTools = "0.5.16" + OrderedCollections = "2.0.3" + Markdown = "1.11.0" + Random = "1.11.0" + Plots = "1.41.7" + Coverage = "1.8.2" + DocumenterInterLinks = "1.1.0" + MarkdownAST = "0.1.3" + DocStringExtensions = "0.9.5" + StaticArrays = "1.9.23" + Interpolations = "0.16.3" + DifferentiationInterface = "0.7.21" + ForwardDiff = "1.4.6" + CTBase = "0.30.10" + Makie = "0.24.16" + JET = "0.12.3" + Test = "1.11.0" + ADTypes = "1.24.0" + Reexport = "1.2.2" + Documenter = "1.19.0" + CairoMakie = "0.15.16" + SparseArrays = "1.13.0" + LiveServer = "1.5.0" + Unicode = "1.11.0" + +[env."CTSolvers/docs"] +ok = true + + [env."CTSolvers/docs".versions] + RecipesBase = "1.4.0" + Zygote = "0.7.13" + SolverCore = "0.3.10" + CommonSolve = "0.2.14" + NLPModelsKnitro = "0.10.6" + LinearAlgebra = "1.13.0" + Printf = "1.11.0" + SparseConnectivityTracer = "1.2.4" + DocumenterVitepress = "0.3.8" + Logging = "1.11.0" + DiffEqBase = "7.21.4" + GPUArraysCore = "0.2.1" + SciMLBase = "3.58.0" + MacroTools = "0.5.16" + CTModels = "0.19.8" + OrderedCollections = "2.0.3" + MLStyle = "0.4.17" + Markdown = "1.11.0" + OrdinaryDiffEqVerner = "2.4.2" + CUDSS = "0.8.1" + Random = "1.11.0" + NLPModelsIpopt = "0.11.3" + ADNLPModels = "0.8.15" + CUDA = "6.4.1" + MadNLP = "0.10.1" + KernelAbstractions = "0.9.45" + ExaModels = "0.12.1" + UnoSolver = "0.3.8" + DocumenterInterLinks = "1.1.0" + MarkdownAST = "0.1.3" + CTSolvers = "0.5.8" + DocStringExtensions = "0.9.5" + Enzyme = "0.13.216" + StaticArrays = "1.9.23" + Parameters = "0.13.1" + DifferentiationInterface = "0.7.21" + NLPModels = "0.21.12" + MadNLPGPU = "0.10.2" + ForwardDiff = "1.4.6" + CTBase = "0.30.10" + MadNCL = "0.2.3" + JET = "0.12.3" + Test = "1.11.0" + ADTypes = "1.24.0" + Reexport = "1.2.2" + OrdinaryDiffEqTsit5 = "2.1.5" + Documenter = "1.19.0" + SparseArrays = "1.13.0" + LiveServer = "1.6.0" + Unicode = "1.11.0" + +[env."CTFlows/test"] +ok = true + + [env."CTFlows/test".versions] + RecipesBase = "1.4.0" + Zygote = "0.7.13" + SolverCore = "0.3.10" + CommonSolve = "0.2.14" + LinearAlgebra = "1.13.0" + DiffEqGPU = "3.21.5" + Printf = "1.11.0" + NonlinearSolve = "4.32.0" + Aqua = "0.8.18" + Logging = "1.11.0" + DiffEqBase = "7.21.4" + GPUArraysCore = "0.2.1" + SciMLBase = "3.58.0" + Colors = "0.13.3" + MacroTools = "0.5.16" + CTModels = "0.19.8" + OrderedCollections = "2.0.3" + MLStyle = "0.4.17" + Markdown = "1.11.0" + Random = "1.11.0" + Plots = "1.41.7" + CUDA = "6.4.2" + KernelAbstractions = "0.9.45" + CTLie = "0.2.3" + Mooncake = "0.5.64" + CTSolvers = "0.5.8" + DocStringExtensions = "0.9.5" + StaticArrays = "1.9.23" + Parameters = "0.13.1" + Interpolations = "0.16.3" + DifferentiationInterface = "0.7.21" + ForwardDiff = "1.4.6" + CTFlows = "0.18.1" + CTBase = "0.30.10" + Makie = "0.24.16" + Test = "1.11.0" + ADTypes = "1.24.0" + Reexport = "1.2.2" + OrdinaryDiffEqTsit5 = "2.1.5" + CairoMakie = "0.15.16" + SparseArrays = "1.13.0" + Unicode = "1.11.0" diff --git a/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTBase/Project.toml b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTBase/Project.toml new file mode 100644 index 0000000..aecdb99 --- /dev/null +++ b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTBase/Project.toml @@ -0,0 +1,64 @@ +name = "CTBase" +uuid = "54762871-cc72-4466-b8e8-f6c8b58076cd" +version = "0.30.10" +authors = ["Olivier Cots ", "Jean-Baptiste Caillau "] + +[deps] +ADTypes = "47edcb42-4c32-4615-8424-f2b9edc5f35b" +DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" + +[weakdeps] +Coverage = "a2441757-f6aa-5fb2-8edb-039e3f45d037" +DifferentiationInterface = "a0c0ee7d-e4b9-4e03-894e-1c5f64a51d63" +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" +Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a" +MarkdownAST = "d0879d2d-cac2-40c8-9cee-1863dc0c7391" +Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[extensions] +CTBaseDifferentiationInterface = ["DifferentiationInterface"] +CTBaseMakie = ["Makie"] +CTBasePlots = ["Plots"] +CoveragePostprocessing = ["Coverage"] +DocumenterReference = ["Documenter", "Markdown", "MarkdownAST"] +TestRunner = ["Test"] + +[compat] +ADTypes = "1" +Aqua = "0.8" +BenchmarkTools = "1" +CairoMakie = "0.15" +Coverage = "1" +DifferentiationInterface = "0.7" +DocStringExtensions = "0.9, 0.10" +Documenter = "1" +ForwardDiff = "0.10, 1" +JET = "0.9, 0.11, 0.12" +Logging = "1" +Makie = "0.24" +Markdown = "1" +MarkdownAST = "0.1" +Plots = "1" +Test = "1" +julia = "1.10" + +[extras] +ADTypes = "47edcb42-4c32-4615-8424-f2b9edc5f35b" +Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" +BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" +CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" +Coverage = "a2441757-f6aa-5fb2-8edb-039e3f45d037" +DifferentiationInterface = "a0c0ee7d-e4b9-4e03-894e-1c5f64a51d63" +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" +JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" +Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" +Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a" +MarkdownAST = "d0879d2d-cac2-40c8-9cee-1863dc0c7391" +Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[targets] +test = ["Aqua", "BenchmarkTools", "CairoMakie", "Coverage", "DifferentiationInterface", "Documenter", "ForwardDiff", "JET", "Logging", "Markdown", "MarkdownAST", "Plots", "Test"] diff --git a/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTBase/docs/Project.toml b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTBase/docs/Project.toml new file mode 100644 index 0000000..848cbe1 --- /dev/null +++ b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTBase/docs/Project.toml @@ -0,0 +1,28 @@ +[deps] +ADTypes = "47edcb42-4c32-4615-8424-f2b9edc5f35b" +CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" +Coverage = "a2441757-f6aa-5fb2-8edb-039e3f45d037" +DifferentiationInterface = "a0c0ee7d-e4b9-4e03-894e-1c5f64a51d63" +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +DocumenterInterLinks = "d12716ef-a0f6-4df4-a9f1-a5a34e75c656" +DocumenterVitepress = "4710194d-e776-4893-9690-8d956a29c365" +ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" +JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" +LiveServer = "16fef848-5104-11e9-1b77-fb7a48bbb589" +MarkdownAST = "d0879d2d-cac2-40c8-9cee-1863dc0c7391" +Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" + +[compat] +ADTypes = "1" +CairoMakie = "0.15" +Coverage = "1" +DifferentiationInterface = "0.7" +Documenter = "1" +DocumenterInterLinks = "1" +DocumenterVitepress = "0.3.5" +ForwardDiff = "0.10, 1" +JET = "0.9, 0.11, 0.12" +LiveServer = "1" +MarkdownAST = "0.1" +Plots = "1" +julia = "1.10" diff --git a/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTDirect/Project.toml b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTDirect/Project.toml new file mode 100644 index 0000000..2749473 --- /dev/null +++ b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTDirect/Project.toml @@ -0,0 +1,58 @@ +name = "CTDirect" +uuid = "790bbbee-bee9-49ee-8912-a9de031322d5" +version = "1.1.8-beta" +authors = ["Pierre Martinon "] + +[workspace] +projects = ["test", "docs"] + +[deps] +ADNLPModels = "54578032-b7ea-4c30-94aa-7cbd1cce6c9a" +CTBase = "54762871-cc72-4466-b8e8-f6c8b58076cd" +CTModels = "34c4fa32-2049-4079-8329-de33c2a22e2d" +CTParser = "32681960-a1b1-40db-9bff-a1ca817385d1" +CTSolvers = "d3e8d392-8e4b-4d9b-8e92-d7d4e3650ef6" +DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" +ExaModels = "1037b233-b668-4ce9-9b63-f9f681f55dd2" +SolverCore = "ff4d7338-4cf1-434d-91df-b86cb86fb843" +SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" +SparseConnectivityTracer = "9f842d2f-2579-4b1d-911e-f412cf18a3f5" + +[compat] +ADNLPModels = "0.8" +AMDGPU = "2" +CTBase = "0.29, 0.30" +CTModels = "0.18, 0.19" +CTParser = "0.9" +CTSolvers = "0.5" +CUDA = "6" +CUDSS = "0.8" +CommonSolve = "0.2" +DocStringExtensions = "0.9" +ExaModels = "0.12" +MadNLP = "0.10" +MadNLPGPU = "0.10" +NLPModels = "0.21" +NLPModelsIpopt = "0.11" +SolverCore = "0.3" +SparseArrays = "1" +SparseConnectivityTracer = "1.2.1" +SplitApplyCombine = "1" +Test = "1" +julia = "1.10" + +[extras] +AMDGPU = "21141c5a-9bdb-4563-92ae-f87d6854732e" +CTParser = "32681960-a1b1-40db-9bff-a1ca817385d1" +CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba" +CUDSS = "45b445bb-4962-46a0-9369-b4df9d0f772e" +CommonSolve = "38540f10-b2f7-11e9-35d8-d573e4eb0ff2" +MadNLP = "2621e9c9-9eb4-46b1-8089-e8c72242dfb6" +MadNLPGPU = "d72a61cc-809d-412f-99be-fd81f4b8a598" +NLPModels = "a4795742-8479-5a88-8948-cc11e1c8c1a6" +NLPModelsIpopt = "f4238b75-b362-5c4c-b852-0801c9a21d71" +SplitApplyCombine = "03a91e81-4c3e-53e1-a0a4-9c0c8f19dd66" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[targets] +test = ["AMDGPU", "CTParser", "CUDA", "CUDSS", "CommonSolve", "MadNLP", "MadNLPGPU", "NLPModels", "NLPModelsIpopt", "SplitApplyCombine", "Test"] diff --git a/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTDirect/docs/Project.toml b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTDirect/docs/Project.toml new file mode 100644 index 0000000..50c9b08 --- /dev/null +++ b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTDirect/docs/Project.toml @@ -0,0 +1,13 @@ +[deps] +CTBase = "54762871-cc72-4466-b8e8-f6c8b58076cd" +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +DocumenterMermaid = "a078cd44-4d9c-4618-b545-3ab9d77f9177" +Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a" +MarkdownAST = "d0879d2d-cac2-40c8-9cee-1863dc0c7391" + +[compat] +CTBase = "0.29, 0.30" +Documenter = "1" +DocumenterMermaid = "0.2" +Markdown = "1" +MarkdownAST = "0.1" diff --git a/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTFlows/Project.toml b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTFlows/Project.toml new file mode 100644 index 0000000..bd35945 --- /dev/null +++ b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTFlows/Project.toml @@ -0,0 +1,76 @@ +name = "CTFlows" +uuid = "1c39547c-7794-42f7-af83-d98194f657c2" +version = "0.18.1" +authors = ["Olivier Cots "] + +[deps] +ADTypes = "47edcb42-4c32-4615-8424-f2b9edc5f35b" +CTBase = "54762871-cc72-4466-b8e8-f6c8b58076cd" +CTModels = "34c4fa32-2049-4079-8329-de33c2a22e2d" +CTSolvers = "d3e8d392-8e4b-4d9b-8e92-d7d4e3650ef6" +CommonSolve = "38540f10-b2f7-11e9-35d8-d573e4eb0ff2" +DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" +GPUArraysCore = "46192b85-c4d5-4398-a991-12ede77f4527" +RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" + +[weakdeps] +Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" +Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" +SciMLBase = "0bca4576-84f4-4d90-8ffe-ffa030f20462" +StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" + +[extensions] +CTFlowsMakie = ["Makie"] +CTFlowsPlots = ["Plots"] +CTFlowsSciMLFlows = ["SciMLBase"] +CTFlowsSciMLIntegrator = ["SciMLBase"] +CTFlowsStaticArrays = ["StaticArrays"] + +[compat] +ADTypes = "1" +Aqua = "0.8" +CTBase = "0.30.8" +CTLie = "0.3" +CTModels = "0.19" +CTSolvers = "0.6" +CUDA = "5, 6" +CairoMakie = "0.15" +CommonSolve = "0.2" +DiffEqBase = "6, 7" +DiffEqGPU = "3" +DifferentiationInterface = "0.7" +DocStringExtensions = "0.9" +ForwardDiff = "0.10, 1" +GPUArraysCore = "0.1, 0.2" +Makie = "0.24" +Mooncake = "0.5" +NonlinearSolve = "4" +OrdinaryDiffEqTsit5 = "2" +Plots = "1" +RecipesBase = "1" +SciMLBase = "3" +StaticArrays = "1" +Test = "1" +Zygote = "0.7" +julia = "1.10" + +[extras] +Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" +CTLie = "6880e05b-3a7d-4cac-887c-30cb52c5fdde" +CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba" +CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" +DiffEqBase = "2b5f629d-d688-5b77-993f-72d75c75574e" +DiffEqGPU = "071ae1c0-96b5-11e9-1965-c90190d839ea" +DifferentiationInterface = "a0c0ee7d-e4b9-4e03-894e-1c5f64a51d63" +ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" +Mooncake = "da2b9cff-9c12-43a0-ae48-6db2b0edb7d6" +NonlinearSolve = "8913a72c-1f9b-4ce2-8d82-65094dcecaec" +OrdinaryDiffEqTsit5 = "b1df2697-797e-41e3-8120-5422d3b24e4a" +Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" +SciMLBase = "0bca4576-84f4-4d90-8ffe-ffa030f20462" +StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f" + +[targets] +test = ["Aqua", "CTLie", "CUDA", "CairoMakie", "DiffEqBase", "DiffEqGPU", "DifferentiationInterface", "ForwardDiff", "Mooncake", "NonlinearSolve", "OrdinaryDiffEqTsit5", "Plots", "SciMLBase", "StaticArrays", "Test", "Zygote"] diff --git a/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTFlows/docs/Project.toml b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTFlows/docs/Project.toml new file mode 100644 index 0000000..bf59804 --- /dev/null +++ b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTFlows/docs/Project.toml @@ -0,0 +1,38 @@ +[deps] +CTBase = "54762871-cc72-4466-b8e8-f6c8b58076cd" +CTModels = "34c4fa32-2049-4079-8329-de33c2a22e2d" +CTSolvers = "d3e8d392-8e4b-4d9b-8e92-d7d4e3650ef6" +CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" +DiffEqBase = "2b5f629d-d688-5b77-993f-72d75c75574e" +DifferentiationInterface = "a0c0ee7d-e4b9-4e03-894e-1c5f64a51d63" +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +DocumenterInterLinks = "d12716ef-a0f6-4df4-a9f1-a5a34e75c656" +DocumenterVitepress = "4710194d-e776-4893-9690-8d956a29c365" +ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" +LiveServer = "16fef848-5104-11e9-1b77-fb7a48bbb589" +MarkdownAST = "d0879d2d-cac2-40c8-9cee-1863dc0c7391" +OrdinaryDiffEqRosenbrock = "43230ef6-c299-4910-a778-202eb28ce4ce" +OrdinaryDiffEqTsit5 = "b1df2697-797e-41e3-8120-5422d3b24e4a" +Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" +SciMLBase = "0bca4576-84f4-4d90-8ffe-ffa030f20462" +StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" + +[compat] +CTBase = "0.30" +CTModels = "0.19" +CTSolvers = "0.6" +CairoMakie = "0.15" +DiffEqBase = "7" +DifferentiationInterface = "0.6, 0.7" +Documenter = "1" +DocumenterInterLinks = "1" +DocumenterVitepress = "0.3" +ForwardDiff = "0.10, 1" +LiveServer = "1" +MarkdownAST = "0.1" +OrdinaryDiffEqRosenbrock = "2" +OrdinaryDiffEqTsit5 = "2" +Plots = "1" +SciMLBase = "3" +StaticArrays = "1" +julia = "1.10" diff --git a/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTLie/Project.toml b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTLie/Project.toml new file mode 100644 index 0000000..c680bdc --- /dev/null +++ b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTLie/Project.toml @@ -0,0 +1,38 @@ +name = "CTLie" +uuid = "6880e05b-3a7d-4cac-887c-30cb52c5fdde" +authors = ["Olivier Cots"] +version = "0.3.0" + +[deps] +CTBase = "54762871-cc72-4466-b8e8-f6c8b58076cd" +DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" +MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" + +[compat] +ADTypes = "1" +Aqua = "0.8" +BenchmarkTools = "1" +CTBase = "0.30" +CUDA = "5, 6" +DifferentiationInterface = "0.7" +DocStringExtensions = "0.9" +ForwardDiff = "0.10, 1" +JET = "0.9, 0.11, 0.12" +MacroTools = "0.5" +Mooncake = "0.5" +Test = "1" +julia = "1.10" + +[extras] +ADTypes = "47edcb42-4c32-4615-8424-f2b9edc5f35b" +Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" +BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" +CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba" +DifferentiationInterface = "a0c0ee7d-e4b9-4e03-894e-1c5f64a51d63" +ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" +JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" +Mooncake = "da2b9cff-9c12-43a0-ae48-6db2b0edb7d6" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[targets] +test = ["ADTypes", "Aqua", "BenchmarkTools", "CUDA", "DifferentiationInterface", "ForwardDiff", "JET", "Mooncake", "Test"] \ No newline at end of file diff --git a/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTLie/docs/Project.toml b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTLie/docs/Project.toml new file mode 100644 index 0000000..e1509c8 --- /dev/null +++ b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTLie/docs/Project.toml @@ -0,0 +1,22 @@ +[deps] +ADTypes = "47edcb42-4c32-4615-8424-f2b9edc5f35b" +CTBase = "54762871-cc72-4466-b8e8-f6c8b58076cd" +DifferentiationInterface = "a0c0ee7d-e4b9-4e03-894e-1c5f64a51d63" +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +DocumenterInterLinks = "d12716ef-a0f6-4df4-a9f1-a5a34e75c656" +DocumenterVitepress = "4710194d-e776-4893-9690-8d956a29c365" +ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" +LiveServer = "16fef848-5104-11e9-1b77-fb7a48bbb589" +MarkdownAST = "d0879d2d-cac2-40c8-9cee-1863dc0c7391" + +[compat] +ADTypes = "1" +CTBase = "0.30" +DifferentiationInterface = "0.7" +Documenter = "1" +DocumenterInterLinks = "1" +DocumenterVitepress = "0.3" +ForwardDiff = "0.10, 1" +LiveServer = "1" +MarkdownAST = "0.1" +julia = "1.10" diff --git a/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTModels/Project.toml b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTModels/Project.toml new file mode 100644 index 0000000..a26f0e9 --- /dev/null +++ b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTModels/Project.toml @@ -0,0 +1,61 @@ +name = "CTModels" +uuid = "34c4fa32-2049-4079-8329-de33c2a22e2d" +version = "0.19.8" +authors = ["Olivier Cots "] + +[deps] +CTBase = "54762871-cc72-4466-b8e8-f6c8b58076cd" +DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +MLStyle = "d8e11817-5142-5d16-987a-aa16d5891078" +MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" +OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" +Parameters = "d96e819e-fc66-5662-9728-84c9c7592b0a" +RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" + +[weakdeps] +JLD2 = "033835bb-8acc-5ee8-8aae-3f567f8a3819" +JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" +Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" +Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" + +[extensions] +CTModelsJLD = "JLD2" +CTModelsJSON = "JSON3" +CTModelsMakie = "Makie" +CTModelsPlots = "Plots" + +[compat] +Aqua = "0.8" +BenchmarkTools = "1" +CTBase = "0.30" +CairoMakie = "0.15" +DocStringExtensions = "0.9" +JET = "0.9, 0.11, 0.12" +JLD2 = "0.6" +JSON3 = "1" +LinearAlgebra = "1" +MLStyle = "0.4" +Makie = "0.24" +MacroTools = "0.5" +OrderedCollections = "1, 2" +Parameters = "0.13" +Plots = "1" +Random = "1" +RecipesBase = "1" +Test = "1" +julia = "1.10" + +[extras] +Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" +BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" +CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" +JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" +JLD2 = "033835bb-8acc-5ee8-8aae-3f567f8a3819" +JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" +Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[targets] +test = ["Aqua", "BenchmarkTools", "CairoMakie", "JET", "JLD2", "JSON3", "Plots", "Random", "Test"] diff --git a/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTModels/docs/Project.toml b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTModels/docs/Project.toml new file mode 100644 index 0000000..6a4b442 --- /dev/null +++ b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTModels/docs/Project.toml @@ -0,0 +1,26 @@ +[deps] +CTBase = "54762871-cc72-4466-b8e8-f6c8b58076cd" +CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +DocumenterInterLinks = "d12716ef-a0f6-4df4-a9f1-a5a34e75c656" +DocumenterVitepress = "4710194d-e776-4893-9690-8d956a29c365" +JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" +JLD2 = "033835bb-8acc-5ee8-8aae-3f567f8a3819" +JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" +LiveServer = "16fef848-5104-11e9-1b77-fb7a48bbb589" +MarkdownAST = "d0879d2d-cac2-40c8-9cee-1863dc0c7391" +Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" + +[compat] +CTBase = "0.30" +CairoMakie = "0.15" +Documenter = "1" +DocumenterInterLinks = "1" +DocumenterVitepress = "0.3.5" +JET = "0.9, 0.11, 0.12" +JLD2 = "0.6" +JSON3 = "1" +LiveServer = "1" +MarkdownAST = "0.1" +Plots = "1" +julia = "1.10" diff --git a/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTParser/Project.toml b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTParser/Project.toml new file mode 100644 index 0000000..8e8b5cc --- /dev/null +++ b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTParser/Project.toml @@ -0,0 +1,21 @@ +name = "CTParser" +uuid = "32681960-a1b1-40db-9bff-a1ca817385d1" +version = "0.8.17-beta" +authors = ["Jean-Baptiste Caillau "] + +[deps] +CTBase = "54762871-cc72-4466-b8e8-f6c8b58076cd" +DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" +MLStyle = "d8e11817-5142-5d16-987a-aa16d5891078" +OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" +Parameters = "d96e819e-fc66-5662-9728-84c9c7592b0a" +Unicode = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" + +[compat] +CTBase = "0.18, 0.27, 0.28" +DocStringExtensions = "0.9" +MLStyle = "0.4" +OrderedCollections = "1" +Parameters = "0.12, 0.13" +Unicode = "1" +julia = "1.10" diff --git a/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTParser/docs/Project.toml b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTParser/docs/Project.toml new file mode 100644 index 0000000..631245d --- /dev/null +++ b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTParser/docs/Project.toml @@ -0,0 +1,12 @@ +[deps] +CTBase = "54762871-cc72-4466-b8e8-f6c8b58076cd" +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a" +MarkdownAST = "d0879d2d-cac2-40c8-9cee-1863dc0c7391" + +[compat] +CTBase = "0.18, 0.27, 0.28" +Documenter = "1" +Markdown = "1" +MarkdownAST = "0.1" +julia = "1.10" diff --git a/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTParser/test/Project.toml b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTParser/test/Project.toml new file mode 100644 index 0000000..6214d50 --- /dev/null +++ b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTParser/test/Project.toml @@ -0,0 +1,32 @@ +[deps] +Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" +BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" +CTBase = "54762871-cc72-4466-b8e8-f6c8b58076cd" +CTModels = "34c4fa32-2049-4079-8329-de33c2a22e2d" +CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba" +ExaModels = "1037b233-b668-4ce9-9b63-f9f681f55dd2" +Interpolations = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59" +KernelAbstractions = "63c18a36-062a-441e-b654-da1e3ab1ce7c" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +MadNLP = "2621e9c9-9eb4-46b1-8089-e8c72242dfb6" +MadNLPGPU = "d72a61cc-809d-412f-99be-fd81f4b8a598" +NLPModels = "a4795742-8479-5a88-8948-cc11e1c8c1a6" +OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[compat] +Aqua = "0.8" +BenchmarkTools = "1" +CTBase = "0.18, 0.27, 0.28" +CTModels = "0.10, 0.14, 0.15" +CUDA = "5" +ExaModels = "0.9" +Interpolations = "0.16" +KernelAbstractions = "0.9" +LinearAlgebra = "1" +MadNLP = "0.9" +MadNLPGPU = "0.8" +NLPModels = "0.21" +OrderedCollections = "1.8" +Test = "1.10" +julia = "1.10" diff --git a/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTSolvers/Project.toml b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTSolvers/Project.toml new file mode 100644 index 0000000..9065356 --- /dev/null +++ b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTSolvers/Project.toml @@ -0,0 +1,107 @@ +name = "CTSolvers" +uuid = "d3e8d392-8e4b-4d9b-8e92-d7d4e3650ef6" +version = "0.6.0" +authors = ["Olivier Cots "] + +[deps] +CTBase = "54762871-cc72-4466-b8e8-f6c8b58076cd" +CTModels = "34c4fa32-2049-4079-8329-de33c2a22e2d" +CommonSolve = "38540f10-b2f7-11e9-35d8-d573e4eb0ff2" +DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" +KernelAbstractions = "63c18a36-062a-441e-b654-da1e3ab1ce7c" +SolverCore = "ff4d7338-4cf1-434d-91df-b86cb86fb843" + +[weakdeps] +ADNLPModels = "54578032-b7ea-4c30-94aa-7cbd1cce6c9a" +CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba" +CUDSS = "45b445bb-4962-46a0-9369-b4df9d0f772e" +DiffEqBase = "2b5f629d-d688-5b77-993f-72d75c75574e" +Enzyme = "7da242da-08ed-463a-9acd-ee780be4f1d9" +ExaModels = "1037b233-b668-4ce9-9b63-f9f681f55dd2" +ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" +MadNCL = "434a0bcb-5a7c-42b2-a9d3-9e3f760e7af0" +MadNLP = "2621e9c9-9eb4-46b1-8089-e8c72242dfb6" +MadNLPGPU = "d72a61cc-809d-412f-99be-fd81f4b8a598" +NLPModels = "a4795742-8479-5a88-8948-cc11e1c8c1a6" +NLPModelsIpopt = "f4238b75-b362-5c4c-b852-0801c9a21d71" +NLPModelsKnitro = "bec4dd0d-7755-52d5-9a02-22f0ffc7efcb" +OrdinaryDiffEqTsit5 = "b1df2697-797e-41e3-8120-5422d3b24e4a" +SciMLBase = "0bca4576-84f4-4d90-8ffe-ffa030f20462" +UnoSolver = "1baa60ac-02f7-4b39-a7a8-2f4f58486b05" +Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f" + +[extensions] +CTSolversADNLPModels = "ADNLPModels" +CTSolversCUDA = "CUDA" +CTSolversEnzyme = "Enzyme" +CTSolversExaModels = ["ExaModels"] +CTSolversForwardDiff = "ForwardDiff" +CTSolversIpopt = ["NLPModels", "NLPModelsIpopt"] +CTSolversKnitro = ["NLPModels", "NLPModelsKnitro"] +CTSolversMadNCL = ["MadNCL", "MadNLP", "NLPModels"] +CTSolversMadNLP = ["MadNLP", "NLPModels"] +CTSolversMadNLPGPU = ["MadNLPGPU", "CUDA", "CUDSS"] +CTSolversOrdinaryDiffEqTsit5 = "OrdinaryDiffEqTsit5" +CTSolversSciMLIntegrator = ["DiffEqBase", "SciMLBase"] +CTSolversUno = ["NLPModels", "UnoSolver"] +CTSolversZygote = "Zygote" + +[compat] +ADNLPModels = "0.8" +Aqua = "0.8" +BenchmarkTools = "1" +CTBase = "0.30" +CTModels = "0.19" +CUDA = "5, 6" +CUDSS = "0.6, 0.7, 0.8" +CommonSolve = "0.2" +DiffEqBase = "6, 7" +DocStringExtensions = "0.9, 0.10" +Enzyme = "0.13" +ExaModels = "0.12" +ForwardDiff = "0.10, 1" +JET = "0.9, 0.11, 0.12" +KernelAbstractions = "0.9" +MadNCL = "0.2" +MadNLP = "0.9, 0.10" +MadNLPGPU = "0.8, 0.10" +NLPModels = "0.21" +NLPModelsIpopt = "0.11" +NLPModelsKnitro = "0.10" +OrderedCollections = "1, 2" +OrdinaryDiffEqTsit5 = "2" +Random = "1" +SciMLBase = "3" +SolverCore = "0.3" +Test = "1" +UnoSolver = "0.3" +Zygote = "0.7" +julia = "1.10" + +[extras] +ADNLPModels = "54578032-b7ea-4c30-94aa-7cbd1cce6c9a" +Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" +BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" +CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba" +CUDSS = "45b445bb-4962-46a0-9369-b4df9d0f772e" +DiffEqBase = "2b5f629d-d688-5b77-993f-72d75c75574e" +Enzyme = "7da242da-08ed-463a-9acd-ee780be4f1d9" +ExaModels = "1037b233-b668-4ce9-9b63-f9f681f55dd2" +ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" +JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" +MadNCL = "434a0bcb-5a7c-42b2-a9d3-9e3f760e7af0" +MadNLP = "2621e9c9-9eb4-46b1-8089-e8c72242dfb6" +MadNLPGPU = "d72a61cc-809d-412f-99be-fd81f4b8a598" +NLPModels = "a4795742-8479-5a88-8948-cc11e1c8c1a6" +NLPModelsIpopt = "f4238b75-b362-5c4c-b852-0801c9a21d71" +NLPModelsKnitro = "bec4dd0d-7755-52d5-9a02-22f0ffc7efcb" +OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" +OrdinaryDiffEqTsit5 = "b1df2697-797e-41e3-8120-5422d3b24e4a" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +SciMLBase = "0bca4576-84f4-4d90-8ffe-ffa030f20462" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +UnoSolver = "1baa60ac-02f7-4b39-a7a8-2f4f58486b05" +Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f" + +[targets] +test = ["ADNLPModels", "Aqua", "BenchmarkTools", "CUDA", "CUDSS", "DiffEqBase", "Enzyme", "ExaModels", "ForwardDiff", "JET", "MadNCL", "MadNLP", "MadNLPGPU", "NLPModels", "NLPModelsIpopt", "OrderedCollections", "OrdinaryDiffEqTsit5", "Random", "SciMLBase", "Test", "UnoSolver", "Zygote"] diff --git a/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTSolvers/docs/Project.toml b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTSolvers/docs/Project.toml new file mode 100644 index 0000000..48e650d --- /dev/null +++ b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/CTSolvers/docs/Project.toml @@ -0,0 +1,59 @@ +[deps] +ADNLPModels = "54578032-b7ea-4c30-94aa-7cbd1cce6c9a" +CTBase = "54762871-cc72-4466-b8e8-f6c8b58076cd" +CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba" +CUDSS = "45b445bb-4962-46a0-9369-b4df9d0f772e" +CommonSolve = "38540f10-b2f7-11e9-35d8-d573e4eb0ff2" +DiffEqBase = "2b5f629d-d688-5b77-993f-72d75c75574e" +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +DocumenterInterLinks = "d12716ef-a0f6-4df4-a9f1-a5a34e75c656" +DocumenterVitepress = "4710194d-e776-4893-9690-8d956a29c365" +Enzyme = "7da242da-08ed-463a-9acd-ee780be4f1d9" +ExaModels = "1037b233-b668-4ce9-9b63-f9f681f55dd2" +ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" +JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" +LiveServer = "16fef848-5104-11e9-1b77-fb7a48bbb589" +Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" +MadNCL = "434a0bcb-5a7c-42b2-a9d3-9e3f760e7af0" +MadNLP = "2621e9c9-9eb4-46b1-8089-e8c72242dfb6" +MadNLPGPU = "d72a61cc-809d-412f-99be-fd81f4b8a598" +Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a" +MarkdownAST = "d0879d2d-cac2-40c8-9cee-1863dc0c7391" +NLPModels = "a4795742-8479-5a88-8948-cc11e1c8c1a6" +NLPModelsIpopt = "f4238b75-b362-5c4c-b852-0801c9a21d71" +NLPModelsKnitro = "bec4dd0d-7755-52d5-9a02-22f0ffc7efcb" +OrdinaryDiffEqTsit5 = "b1df2697-797e-41e3-8120-5422d3b24e4a" +OrdinaryDiffEqVerner = "79d7bb75-1356-48c1-b8c0-6832512096c2" +SciMLBase = "0bca4576-84f4-4d90-8ffe-ffa030f20462" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +UnoSolver = "1baa60ac-02f7-4b39-a7a8-2f4f58486b05" +Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f" + +[compat] +ADNLPModels = "0.8" +CTBase = "0.30.10" +CUDA = "5, 6" +CUDSS = "0.6, 0.7, 0.8" +CommonSolve = "0.2" +DiffEqBase = "6, 7" +Documenter = "1" +DocumenterInterLinks = "1" +DocumenterVitepress = "0.3.5" +Enzyme = "0.13" +ExaModels = "0.12" +ForwardDiff = "0.10, 1" +JET = "0.9, 0.11, 0.12" +LiveServer = "1" +MadNCL = "0.2" +MadNLP = "0.9, 0.10" +MadNLPGPU = "0.8, 0.10" +MarkdownAST = "0.1" +NLPModels = "0.21" +NLPModelsIpopt = "0.11" +NLPModelsKnitro = "0.10" +OrdinaryDiffEqTsit5 = "2" +OrdinaryDiffEqVerner = "2" +SciMLBase = "3" +UnoSolver = "0.3" +Zygote = "0.7" +julia = "1.10" diff --git a/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/OptimalControl/Project.toml b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/OptimalControl/Project.toml new file mode 100644 index 0000000..ad776d9 --- /dev/null +++ b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/OptimalControl/Project.toml @@ -0,0 +1,80 @@ +name = "OptimalControl" +uuid = "5f98b655-cc9a-415a-b60e-744165666948" +version = "2.2.1-beta" +authors = ["Olivier Cots "] + +[deps] +ADNLPModels = "54578032-b7ea-4c30-94aa-7cbd1cce6c9a" +CTBase = "54762871-cc72-4466-b8e8-f6c8b58076cd" +CTDirect = "790bbbee-bee9-49ee-8912-a9de031322d5" +CTFlows = "1c39547c-7794-42f7-af83-d98194f657c2" +CTLie = "6880e05b-3a7d-4cac-887c-30cb52c5fdde" +CTModels = "34c4fa32-2049-4079-8329-de33c2a22e2d" +CTParser = "32681960-a1b1-40db-9bff-a1ca817385d1" +CTSolvers = "d3e8d392-8e4b-4d9b-8e92-d7d4e3650ef6" +CommonSolve = "38540f10-b2f7-11e9-35d8-d573e4eb0ff2" +DifferentiationInterface = "a0c0ee7d-e4b9-4e03-894e-1c5f64a51d63" +DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" +ExaModels = "1037b233-b668-4ce9-9b63-f9f681f55dd2" +ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" +Reexport = "189a3867-3050-52da-a836-e630ba90ab69" + +[compat] +ADNLPModels = "0.8" +BenchmarkTools = "1" +CTBase = "0.30" +CTDirect = "1" +CTFlows = "0.18" +CTLie = "0.2" +CTModels = "0.19" +CTParser = "0.9" +CTSolvers = "0.5" +CUDA = "6" +CUDSS = "0.7" +CommonSolve = "0.2" +DiffEqBase = "7" +DifferentiationInterface = "0.7" +DocStringExtensions = "0.9" +ExaModels = "0.12" +ForwardDiff = "1" +LinearAlgebra = "1" +MadNCL = "0.2" +MadNLP = "0.10" +MadNLPGPU = "0.10" +NLPModelsIpopt = "0.11" +NonlinearSolve = "4" +OrdinaryDiffEq = "7" +OrdinaryDiffEqTsit5 = "2" +Printf = "1" +RecipesBase = "1" +Reexport = "1" +SciMLBase = "3" +SolverCore = "0.3" +SplitApplyCombine = "1" +Test = "1" +UnoSolver = "0.3" +julia = "1.10" + +[extras] +BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" +CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba" +CUDSS = "45b445bb-4962-46a0-9369-b4df9d0f772e" +DiffEqBase = "2b5f629d-d688-5b77-993f-72d75c75574e" +MadNCL = "434a0bcb-5a7c-42b2-a9d3-9e3f760e7af0" +MadNLP = "2621e9c9-9eb4-46b1-8089-e8c72242dfb6" +MadNLPGPU = "d72a61cc-809d-412f-99be-fd81f4b8a598" +NLPModelsIpopt = "f4238b75-b362-5c4c-b852-0801c9a21d71" +NonlinearSolve = "8913a72c-1f9b-4ce2-8d82-65094dcecaec" +OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed" +OrdinaryDiffEqTsit5 = "b1df2697-797e-41e3-8120-5422d3b24e4a" +Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" +SciMLBase = "0bca4576-84f4-4d90-8ffe-ffa030f20462" +SolverCore = "ff4d7338-4cf1-434d-91df-b86cb86fb843" +SplitApplyCombine = "03a91e81-4c3e-53e1-a0a4-9c0c8f19dd66" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +UnoSolver = "1baa60ac-02f7-4b39-a7a8-2f4f58486b05" + +[targets] +test = ["BenchmarkTools", "CUDA", "CUDSS", "DiffEqBase", "MadNCL", "MadNLP", "MadNLPGPU", "NLPModelsIpopt", "NonlinearSolve", "OrdinaryDiffEq", "OrdinaryDiffEqTsit5", "Printf", "SciMLBase", "SolverCore", "SplitApplyCombine", "Test", "UnoSolver"] diff --git a/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/OptimalControl/docs/Project.toml b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/OptimalControl/docs/Project.toml new file mode 100644 index 0000000..30d7aef --- /dev/null +++ b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/default/OptimalControl/docs/Project.toml @@ -0,0 +1,73 @@ +[deps] +ADNLPModels = "54578032-b7ea-4c30-94aa-7cbd1cce6c9a" +CTBase = "54762871-cc72-4466-b8e8-f6c8b58076cd" +CTDirect = "790bbbee-bee9-49ee-8912-a9de031322d5" +CTFlows = "1c39547c-7794-42f7-af83-d98194f657c2" +CTLie = "6880e05b-3a7d-4cac-887c-30cb52c5fdde" +CTModels = "34c4fa32-2049-4079-8329-de33c2a22e2d" +CTParser = "32681960-a1b1-40db-9bff-a1ca817385d1" +CTSolvers = "d3e8d392-8e4b-4d9b-8e92-d7d4e3650ef6" +CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba" +CUDSS = "45b445bb-4962-46a0-9369-b4df9d0f772e" +CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" +Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" +CommonSolve = "38540f10-b2f7-11e9-35d8-d573e4eb0ff2" +DifferentiationInterface = "a0c0ee7d-e4b9-4e03-894e-1c5f64a51d63" +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +DocumenterInterLinks = "d12716ef-a0f6-4df4-a9f1-a5a34e75c656" +DocumenterVitepress = "4710194d-e776-4893-9690-8d956a29c365" +ExaModels = "1037b233-b668-4ce9-9b63-f9f681f55dd2" +ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" +JLD2 = "033835bb-8acc-5ee8-8aae-3f567f8a3819" +JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" +Literate = "98b081ad-f1c9-55d3-8b20-4c87d4299306" +LiveServer = "16fef848-5104-11e9-1b77-fb7a48bbb589" +MadNCL = "434a0bcb-5a7c-42b2-a9d3-9e3f760e7af0" +MadNLP = "2621e9c9-9eb4-46b1-8089-e8c72242dfb6" +MadNLPGPU = "d72a61cc-809d-412f-99be-fd81f4b8a598" +MarkdownAST = "d0879d2d-cac2-40c8-9cee-1863dc0c7391" +NLPModelsIpopt = "f4238b75-b362-5c4c-b852-0801c9a21d71" +NLPModelsKnitro = "bec4dd0d-7755-52d5-9a02-22f0ffc7efcb" +NonlinearSolve = "8913a72c-1f9b-4ce2-8d82-65094dcecaec" +OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed" +OrdinaryDiffEqTsit5 = "b1df2697-797e-41e3-8120-5422d3b24e4a" +OrdinaryDiffEqVerner = "79d7bb75-1356-48c1-b8c0-6832512096c2" +Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" +UnoSolver = "1baa60ac-02f7-4b39-a7a8-2f4f58486b05" + +[compat] +ADNLPModels = "0.8" +CTBase = "0.30" +CTDirect = "1" +CTFlows = "0.18" +CTLie = "0.2" +CTModels = "0.19" +CTParser = "0.9" +CTSolvers = "0.5" +CUDA = "6" +CUDSS = "0.7" +CairoMakie = "0.15" +Colors = "0.13" +CommonSolve = "0.2" +DifferentiationInterface = "0.7" +Documenter = "1" +DocumenterInterLinks = "1" +DocumenterVitepress = "0.3" +ExaModels = "0.12" +ForwardDiff = "1" +JLD2 = "0.6" +JSON3 = "1" +Literate = "2" +LiveServer = "1" +MadNCL = "0.2" +MadNLP = "0.10" +MadNLPGPU = "0.10" +MarkdownAST = "0.1" +NLPModelsIpopt = "0.11" +NLPModelsKnitro = "0.10" +NonlinearSolve = "4" +OrdinaryDiffEq = "7" +OrdinaryDiffEqTsit5 = "2" +Plots = "1" +UnoSolver = "0.3" +julia = "1.10" diff --git a/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/index.toml b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/index.toml new file mode 100644 index 0000000..fe2d3a8 --- /dev/null +++ b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/index.toml @@ -0,0 +1,73 @@ +julia = "1.13.1" + +[packages.CTBase] +repo = "control-toolbox/CTBase.jl" +version = "0.30.10" +default_branch = "main" +files = ["Project.toml", "docs/Project.toml"] +tree = "cafe2bf5cbd697c5b239517aa8a699369ced443c" +default_files = ["Project.toml", "docs/Project.toml"] +uuid = "54762871-cc72-4466-b8e8-f6c8b58076cd" + +[packages.CTModels] +repo = "control-toolbox/CTModels.jl" +version = "0.19.8" +default_branch = "main" +files = ["Project.toml", "docs/Project.toml"] +tree = "25682b75dd2149f0abea4d4264a83e92c6171114" +default_files = ["Project.toml", "docs/Project.toml"] +uuid = "34c4fa32-2049-4079-8329-de33c2a22e2d" + +[packages.CTFlows] +repo = "control-toolbox/CTFlows.jl" +version = "0.18.1" +default_branch = "main" +files = ["Project.toml", "docs/Project.toml"] +tree = "d82116ac4932b8494e7f6f3deb312dce44753969" +default_files = ["Project.toml", "docs/Project.toml"] +uuid = "1c39547c-7794-42f7-af83-d98194f657c2" + +[packages.CTDirect] +repo = "control-toolbox/CTDirect.jl" +version = "1.1.8-beta" +default_branch = "main" +files = ["Project.toml", "docs/Project.toml"] +tree = "38f6fbb168b844b7ceac71678b42c96daa20bba5" +default_files = ["Project.toml", "docs/Project.toml"] +uuid = "790bbbee-bee9-49ee-8912-a9de031322d5" + +[packages.OptimalControl] +repo = "control-toolbox/OptimalControl.jl" +version = "2.2.1-beta" +default_branch = "main" +files = ["Project.toml", "docs/Project.toml"] +tree = "07c2d96cde77525ad4c354547a34a068132f314d" +default_files = ["Project.toml", "docs/Project.toml"] +uuid = "5f98b655-cc9a-415a-b60e-744165666948" + +[packages.CTParser] +repo = "control-toolbox/CTParser.jl" +version = "0.9.5-beta" +default_branch = "main" +files = ["Project.toml", "test/Project.toml", "docs/Project.toml"] +tree = "3f745c73919729ca654d02e1091fce5583facacc" +default_files = ["Project.toml", "test/Project.toml", "docs/Project.toml"] +uuid = "32681960-a1b1-40db-9bff-a1ca817385d1" + +[packages.CTSolvers] +repo = "control-toolbox/CTSolvers.jl" +version = "0.5.8" +default_branch = "main" +files = ["Project.toml", "docs/Project.toml"] +tree = "50e6edf7726eb7d620323fb4cc3c2e87392022c3" +default_files = ["Project.toml", "docs/Project.toml"] +uuid = "d3e8d392-8e4b-4d9b-8e92-d7d4e3650ef6" + +[packages.CTLie] +repo = "control-toolbox/CTLie.jl" +version = "0.2.3" +default_branch = "main" +files = ["Project.toml", "docs/Project.toml"] +tree = "049bbb24dfa8b5d54cff2736352d76ce9e7782ae" +default_files = ["Project.toml", "docs/Project.toml"] +uuid = "6880e05b-3a7d-4cac-887c-30cb52c5fdde" diff --git a/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTBase/Project.toml b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTBase/Project.toml new file mode 100644 index 0000000..aecdb99 --- /dev/null +++ b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTBase/Project.toml @@ -0,0 +1,64 @@ +name = "CTBase" +uuid = "54762871-cc72-4466-b8e8-f6c8b58076cd" +version = "0.30.10" +authors = ["Olivier Cots ", "Jean-Baptiste Caillau "] + +[deps] +ADTypes = "47edcb42-4c32-4615-8424-f2b9edc5f35b" +DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" + +[weakdeps] +Coverage = "a2441757-f6aa-5fb2-8edb-039e3f45d037" +DifferentiationInterface = "a0c0ee7d-e4b9-4e03-894e-1c5f64a51d63" +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" +Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a" +MarkdownAST = "d0879d2d-cac2-40c8-9cee-1863dc0c7391" +Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[extensions] +CTBaseDifferentiationInterface = ["DifferentiationInterface"] +CTBaseMakie = ["Makie"] +CTBasePlots = ["Plots"] +CoveragePostprocessing = ["Coverage"] +DocumenterReference = ["Documenter", "Markdown", "MarkdownAST"] +TestRunner = ["Test"] + +[compat] +ADTypes = "1" +Aqua = "0.8" +BenchmarkTools = "1" +CairoMakie = "0.15" +Coverage = "1" +DifferentiationInterface = "0.7" +DocStringExtensions = "0.9, 0.10" +Documenter = "1" +ForwardDiff = "0.10, 1" +JET = "0.9, 0.11, 0.12" +Logging = "1" +Makie = "0.24" +Markdown = "1" +MarkdownAST = "0.1" +Plots = "1" +Test = "1" +julia = "1.10" + +[extras] +ADTypes = "47edcb42-4c32-4615-8424-f2b9edc5f35b" +Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" +BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" +CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" +Coverage = "a2441757-f6aa-5fb2-8edb-039e3f45d037" +DifferentiationInterface = "a0c0ee7d-e4b9-4e03-894e-1c5f64a51d63" +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" +JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" +Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" +Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a" +MarkdownAST = "d0879d2d-cac2-40c8-9cee-1863dc0c7391" +Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[targets] +test = ["Aqua", "BenchmarkTools", "CairoMakie", "Coverage", "DifferentiationInterface", "Documenter", "ForwardDiff", "JET", "Logging", "Markdown", "MarkdownAST", "Plots", "Test"] diff --git a/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTBase/docs/Project.toml b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTBase/docs/Project.toml new file mode 100644 index 0000000..848cbe1 --- /dev/null +++ b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTBase/docs/Project.toml @@ -0,0 +1,28 @@ +[deps] +ADTypes = "47edcb42-4c32-4615-8424-f2b9edc5f35b" +CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" +Coverage = "a2441757-f6aa-5fb2-8edb-039e3f45d037" +DifferentiationInterface = "a0c0ee7d-e4b9-4e03-894e-1c5f64a51d63" +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +DocumenterInterLinks = "d12716ef-a0f6-4df4-a9f1-a5a34e75c656" +DocumenterVitepress = "4710194d-e776-4893-9690-8d956a29c365" +ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" +JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" +LiveServer = "16fef848-5104-11e9-1b77-fb7a48bbb589" +MarkdownAST = "d0879d2d-cac2-40c8-9cee-1863dc0c7391" +Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" + +[compat] +ADTypes = "1" +CairoMakie = "0.15" +Coverage = "1" +DifferentiationInterface = "0.7" +Documenter = "1" +DocumenterInterLinks = "1" +DocumenterVitepress = "0.3.5" +ForwardDiff = "0.10, 1" +JET = "0.9, 0.11, 0.12" +LiveServer = "1" +MarkdownAST = "0.1" +Plots = "1" +julia = "1.10" diff --git a/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTDirect/Project.toml b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTDirect/Project.toml new file mode 100644 index 0000000..2749473 --- /dev/null +++ b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTDirect/Project.toml @@ -0,0 +1,58 @@ +name = "CTDirect" +uuid = "790bbbee-bee9-49ee-8912-a9de031322d5" +version = "1.1.8-beta" +authors = ["Pierre Martinon "] + +[workspace] +projects = ["test", "docs"] + +[deps] +ADNLPModels = "54578032-b7ea-4c30-94aa-7cbd1cce6c9a" +CTBase = "54762871-cc72-4466-b8e8-f6c8b58076cd" +CTModels = "34c4fa32-2049-4079-8329-de33c2a22e2d" +CTParser = "32681960-a1b1-40db-9bff-a1ca817385d1" +CTSolvers = "d3e8d392-8e4b-4d9b-8e92-d7d4e3650ef6" +DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" +ExaModels = "1037b233-b668-4ce9-9b63-f9f681f55dd2" +SolverCore = "ff4d7338-4cf1-434d-91df-b86cb86fb843" +SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" +SparseConnectivityTracer = "9f842d2f-2579-4b1d-911e-f412cf18a3f5" + +[compat] +ADNLPModels = "0.8" +AMDGPU = "2" +CTBase = "0.29, 0.30" +CTModels = "0.18, 0.19" +CTParser = "0.9" +CTSolvers = "0.5" +CUDA = "6" +CUDSS = "0.8" +CommonSolve = "0.2" +DocStringExtensions = "0.9" +ExaModels = "0.12" +MadNLP = "0.10" +MadNLPGPU = "0.10" +NLPModels = "0.21" +NLPModelsIpopt = "0.11" +SolverCore = "0.3" +SparseArrays = "1" +SparseConnectivityTracer = "1.2.1" +SplitApplyCombine = "1" +Test = "1" +julia = "1.10" + +[extras] +AMDGPU = "21141c5a-9bdb-4563-92ae-f87d6854732e" +CTParser = "32681960-a1b1-40db-9bff-a1ca817385d1" +CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba" +CUDSS = "45b445bb-4962-46a0-9369-b4df9d0f772e" +CommonSolve = "38540f10-b2f7-11e9-35d8-d573e4eb0ff2" +MadNLP = "2621e9c9-9eb4-46b1-8089-e8c72242dfb6" +MadNLPGPU = "d72a61cc-809d-412f-99be-fd81f4b8a598" +NLPModels = "a4795742-8479-5a88-8948-cc11e1c8c1a6" +NLPModelsIpopt = "f4238b75-b362-5c4c-b852-0801c9a21d71" +SplitApplyCombine = "03a91e81-4c3e-53e1-a0a4-9c0c8f19dd66" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[targets] +test = ["AMDGPU", "CTParser", "CUDA", "CUDSS", "CommonSolve", "MadNLP", "MadNLPGPU", "NLPModels", "NLPModelsIpopt", "SplitApplyCombine", "Test"] diff --git a/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTDirect/docs/Project.toml b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTDirect/docs/Project.toml new file mode 100644 index 0000000..50c9b08 --- /dev/null +++ b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTDirect/docs/Project.toml @@ -0,0 +1,13 @@ +[deps] +CTBase = "54762871-cc72-4466-b8e8-f6c8b58076cd" +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +DocumenterMermaid = "a078cd44-4d9c-4618-b545-3ab9d77f9177" +Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a" +MarkdownAST = "d0879d2d-cac2-40c8-9cee-1863dc0c7391" + +[compat] +CTBase = "0.29, 0.30" +Documenter = "1" +DocumenterMermaid = "0.2" +Markdown = "1" +MarkdownAST = "0.1" diff --git a/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTFlows/Project.toml b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTFlows/Project.toml new file mode 100644 index 0000000..b51e704 --- /dev/null +++ b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTFlows/Project.toml @@ -0,0 +1,76 @@ +name = "CTFlows" +uuid = "1c39547c-7794-42f7-af83-d98194f657c2" +version = "0.18.1" +authors = ["Olivier Cots "] + +[deps] +ADTypes = "47edcb42-4c32-4615-8424-f2b9edc5f35b" +CTBase = "54762871-cc72-4466-b8e8-f6c8b58076cd" +CTModels = "34c4fa32-2049-4079-8329-de33c2a22e2d" +CTSolvers = "d3e8d392-8e4b-4d9b-8e92-d7d4e3650ef6" +CommonSolve = "38540f10-b2f7-11e9-35d8-d573e4eb0ff2" +DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" +GPUArraysCore = "46192b85-c4d5-4398-a991-12ede77f4527" +RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" + +[weakdeps] +Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" +Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" +SciMLBase = "0bca4576-84f4-4d90-8ffe-ffa030f20462" +StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" + +[extensions] +CTFlowsMakie = ["Makie"] +CTFlowsPlots = ["Plots"] +CTFlowsSciMLFlows = ["SciMLBase"] +CTFlowsSciMLIntegrator = ["SciMLBase"] +CTFlowsStaticArrays = ["StaticArrays"] + +[compat] +ADTypes = "1" +Aqua = "0.8" +CTBase = "0.30" +CTLie = "0.2" +CTModels = "0.19" +CTSolvers = "0.5" +CUDA = "5, 6" +CairoMakie = "0.15" +CommonSolve = "0.2" +DiffEqBase = "6, 7" +DiffEqGPU = "3" +DifferentiationInterface = "0.7" +DocStringExtensions = "0.9" +ForwardDiff = "0.10, 1" +GPUArraysCore = "0.1, 0.2" +Makie = "0.24" +Mooncake = "0.5" +NonlinearSolve = "4" +OrdinaryDiffEqTsit5 = "2" +Plots = "1" +RecipesBase = "1" +SciMLBase = "3" +StaticArrays = "1" +Test = "1" +Zygote = "0.7" +julia = "1.10" + +[extras] +Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" +CTLie = "6880e05b-3a7d-4cac-887c-30cb52c5fdde" +CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba" +CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" +DiffEqBase = "2b5f629d-d688-5b77-993f-72d75c75574e" +DiffEqGPU = "071ae1c0-96b5-11e9-1965-c90190d839ea" +DifferentiationInterface = "a0c0ee7d-e4b9-4e03-894e-1c5f64a51d63" +ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" +Mooncake = "da2b9cff-9c12-43a0-ae48-6db2b0edb7d6" +NonlinearSolve = "8913a72c-1f9b-4ce2-8d82-65094dcecaec" +OrdinaryDiffEqTsit5 = "b1df2697-797e-41e3-8120-5422d3b24e4a" +Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" +SciMLBase = "0bca4576-84f4-4d90-8ffe-ffa030f20462" +StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f" + +[targets] +test = ["Aqua", "CTLie", "CUDA", "CairoMakie", "DiffEqBase", "DiffEqGPU", "DifferentiationInterface", "ForwardDiff", "Mooncake", "NonlinearSolve", "OrdinaryDiffEqTsit5", "Plots", "SciMLBase", "StaticArrays", "Test", "Zygote"] diff --git a/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTFlows/docs/Project.toml b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTFlows/docs/Project.toml new file mode 100644 index 0000000..f2767df --- /dev/null +++ b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTFlows/docs/Project.toml @@ -0,0 +1,38 @@ +[deps] +CTBase = "54762871-cc72-4466-b8e8-f6c8b58076cd" +CTModels = "34c4fa32-2049-4079-8329-de33c2a22e2d" +CTSolvers = "d3e8d392-8e4b-4d9b-8e92-d7d4e3650ef6" +CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" +DiffEqBase = "2b5f629d-d688-5b77-993f-72d75c75574e" +DifferentiationInterface = "a0c0ee7d-e4b9-4e03-894e-1c5f64a51d63" +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +DocumenterInterLinks = "d12716ef-a0f6-4df4-a9f1-a5a34e75c656" +DocumenterVitepress = "4710194d-e776-4893-9690-8d956a29c365" +ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" +LiveServer = "16fef848-5104-11e9-1b77-fb7a48bbb589" +MarkdownAST = "d0879d2d-cac2-40c8-9cee-1863dc0c7391" +OrdinaryDiffEqRosenbrock = "43230ef6-c299-4910-a778-202eb28ce4ce" +OrdinaryDiffEqTsit5 = "b1df2697-797e-41e3-8120-5422d3b24e4a" +Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" +SciMLBase = "0bca4576-84f4-4d90-8ffe-ffa030f20462" +StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" + +[compat] +CTBase = "0.30" +CTModels = "0.19" +CTSolvers = "0.5" +CairoMakie = "0.15" +DiffEqBase = "7" +DifferentiationInterface = "0.6, 0.7" +Documenter = "1" +DocumenterInterLinks = "1" +DocumenterVitepress = "0.3" +ForwardDiff = "0.10, 1" +LiveServer = "1" +MarkdownAST = "0.1" +OrdinaryDiffEqRosenbrock = "2" +OrdinaryDiffEqTsit5 = "2" +Plots = "1" +SciMLBase = "3" +StaticArrays = "1" +julia = "1.10" diff --git a/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTLie/Project.toml b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTLie/Project.toml new file mode 100644 index 0000000..d2860cd --- /dev/null +++ b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTLie/Project.toml @@ -0,0 +1,38 @@ +name = "CTLie" +uuid = "6880e05b-3a7d-4cac-887c-30cb52c5fdde" +authors = ["Olivier Cots"] +version = "0.2.3" + +[deps] +CTBase = "54762871-cc72-4466-b8e8-f6c8b58076cd" +DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" +MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" + +[compat] +ADTypes = "1" +Aqua = "0.8" +BenchmarkTools = "1" +CTBase = "0.30" +CUDA = "5, 6" +DifferentiationInterface = "0.7" +DocStringExtensions = "0.9" +ForwardDiff = "0.10, 1" +JET = "0.9, 0.11, 0.12" +MacroTools = "0.5" +Mooncake = "0.5" +Test = "1" +julia = "1.10" + +[extras] +ADTypes = "47edcb42-4c32-4615-8424-f2b9edc5f35b" +Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" +BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" +CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba" +DifferentiationInterface = "a0c0ee7d-e4b9-4e03-894e-1c5f64a51d63" +ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" +JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" +Mooncake = "da2b9cff-9c12-43a0-ae48-6db2b0edb7d6" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[targets] +test = ["ADTypes", "Aqua", "BenchmarkTools", "CUDA", "DifferentiationInterface", "ForwardDiff", "JET", "Mooncake", "Test"] \ No newline at end of file diff --git a/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTLie/docs/Project.toml b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTLie/docs/Project.toml new file mode 100644 index 0000000..e1509c8 --- /dev/null +++ b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTLie/docs/Project.toml @@ -0,0 +1,22 @@ +[deps] +ADTypes = "47edcb42-4c32-4615-8424-f2b9edc5f35b" +CTBase = "54762871-cc72-4466-b8e8-f6c8b58076cd" +DifferentiationInterface = "a0c0ee7d-e4b9-4e03-894e-1c5f64a51d63" +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +DocumenterInterLinks = "d12716ef-a0f6-4df4-a9f1-a5a34e75c656" +DocumenterVitepress = "4710194d-e776-4893-9690-8d956a29c365" +ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" +LiveServer = "16fef848-5104-11e9-1b77-fb7a48bbb589" +MarkdownAST = "d0879d2d-cac2-40c8-9cee-1863dc0c7391" + +[compat] +ADTypes = "1" +CTBase = "0.30" +DifferentiationInterface = "0.7" +Documenter = "1" +DocumenterInterLinks = "1" +DocumenterVitepress = "0.3" +ForwardDiff = "0.10, 1" +LiveServer = "1" +MarkdownAST = "0.1" +julia = "1.10" diff --git a/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTModels/Project.toml b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTModels/Project.toml new file mode 100644 index 0000000..a26f0e9 --- /dev/null +++ b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTModels/Project.toml @@ -0,0 +1,61 @@ +name = "CTModels" +uuid = "34c4fa32-2049-4079-8329-de33c2a22e2d" +version = "0.19.8" +authors = ["Olivier Cots "] + +[deps] +CTBase = "54762871-cc72-4466-b8e8-f6c8b58076cd" +DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +MLStyle = "d8e11817-5142-5d16-987a-aa16d5891078" +MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" +OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" +Parameters = "d96e819e-fc66-5662-9728-84c9c7592b0a" +RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" + +[weakdeps] +JLD2 = "033835bb-8acc-5ee8-8aae-3f567f8a3819" +JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" +Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" +Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" + +[extensions] +CTModelsJLD = "JLD2" +CTModelsJSON = "JSON3" +CTModelsMakie = "Makie" +CTModelsPlots = "Plots" + +[compat] +Aqua = "0.8" +BenchmarkTools = "1" +CTBase = "0.30" +CairoMakie = "0.15" +DocStringExtensions = "0.9" +JET = "0.9, 0.11, 0.12" +JLD2 = "0.6" +JSON3 = "1" +LinearAlgebra = "1" +MLStyle = "0.4" +Makie = "0.24" +MacroTools = "0.5" +OrderedCollections = "1, 2" +Parameters = "0.13" +Plots = "1" +Random = "1" +RecipesBase = "1" +Test = "1" +julia = "1.10" + +[extras] +Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" +BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" +CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" +JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" +JLD2 = "033835bb-8acc-5ee8-8aae-3f567f8a3819" +JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" +Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[targets] +test = ["Aqua", "BenchmarkTools", "CairoMakie", "JET", "JLD2", "JSON3", "Plots", "Random", "Test"] diff --git a/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTModels/docs/Project.toml b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTModels/docs/Project.toml new file mode 100644 index 0000000..6a4b442 --- /dev/null +++ b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTModels/docs/Project.toml @@ -0,0 +1,26 @@ +[deps] +CTBase = "54762871-cc72-4466-b8e8-f6c8b58076cd" +CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +DocumenterInterLinks = "d12716ef-a0f6-4df4-a9f1-a5a34e75c656" +DocumenterVitepress = "4710194d-e776-4893-9690-8d956a29c365" +JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" +JLD2 = "033835bb-8acc-5ee8-8aae-3f567f8a3819" +JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" +LiveServer = "16fef848-5104-11e9-1b77-fb7a48bbb589" +MarkdownAST = "d0879d2d-cac2-40c8-9cee-1863dc0c7391" +Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" + +[compat] +CTBase = "0.30" +CairoMakie = "0.15" +Documenter = "1" +DocumenterInterLinks = "1" +DocumenterVitepress = "0.3.5" +JET = "0.9, 0.11, 0.12" +JLD2 = "0.6" +JSON3 = "1" +LiveServer = "1" +MarkdownAST = "0.1" +Plots = "1" +julia = "1.10" diff --git a/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTParser/Project.toml b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTParser/Project.toml new file mode 100644 index 0000000..56fa680 --- /dev/null +++ b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTParser/Project.toml @@ -0,0 +1,30 @@ +name = "CTParser" +uuid = "32681960-a1b1-40db-9bff-a1ca817385d1" +version = "0.9.5-beta" +authors = ["Jean-Baptiste Caillau "] + +[deps] +CTBase = "54762871-cc72-4466-b8e8-f6c8b58076cd" +DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" +MLStyle = "d8e11817-5142-5d16-987a-aa16d5891078" +OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" +Parameters = "d96e819e-fc66-5662-9728-84c9c7592b0a" +Unicode = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" + +[weakdeps] +ExaModels = "1037b233-b668-4ce9-9b63-f9f681f55dd2" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" + +[extensions] +CTParserExaModels = ["ExaModels", "LinearAlgebra"] + +[compat] +CTBase = "0.29, 0.30" +DocStringExtensions = "0.9" +ExaModels = "0.12" +LinearAlgebra = "1" +MLStyle = "0.4" +OrderedCollections = "1, 2" +Parameters = "0.13" +Unicode = "1" +julia = "1.10" diff --git a/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTParser/docs/Project.toml b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTParser/docs/Project.toml new file mode 100644 index 0000000..6b1c7b4 --- /dev/null +++ b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTParser/docs/Project.toml @@ -0,0 +1,12 @@ +[deps] +CTBase = "54762871-cc72-4466-b8e8-f6c8b58076cd" +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a" +MarkdownAST = "d0879d2d-cac2-40c8-9cee-1863dc0c7391" + +[compat] +CTBase = "0.29, 0.30" +Documenter = "1" +Markdown = "1" +MarkdownAST = "0.1" +julia = "1.10" diff --git a/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTParser/test/Project.toml b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTParser/test/Project.toml new file mode 100644 index 0000000..8d16399 --- /dev/null +++ b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTParser/test/Project.toml @@ -0,0 +1,34 @@ +[deps] +Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" +BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" +CTBase = "54762871-cc72-4466-b8e8-f6c8b58076cd" +CTModels = "34c4fa32-2049-4079-8329-de33c2a22e2d" +CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba" +CUDSS = "45b445bb-4962-46a0-9369-b4df9d0f772e" +ExaModels = "1037b233-b668-4ce9-9b63-f9f681f55dd2" +Interpolations = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59" +KernelAbstractions = "63c18a36-062a-441e-b654-da1e3ab1ce7c" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +MadNLP = "2621e9c9-9eb4-46b1-8089-e8c72242dfb6" +MadNLPGPU = "d72a61cc-809d-412f-99be-fd81f4b8a598" +NLPModels = "a4795742-8479-5a88-8948-cc11e1c8c1a6" +OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[compat] +Aqua = "0.8" +BenchmarkTools = "1" +CTBase = "0.29, 0.30" +CTModels = "0.18" +CUDA = "5, 6" +CUDSS = "0.6, 0.7, 0.8" +ExaModels = "0.12" +Interpolations = "0.16" +KernelAbstractions = "0.9" +LinearAlgebra = "1" +MadNLP = "0.9, 0.10" +MadNLPGPU = "0.8, 0.10" +NLPModels = "0.21" +OrderedCollections = "1.8, 2" +Test = "1.10" +julia = "1.10" diff --git a/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTSolvers/Project.toml b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTSolvers/Project.toml new file mode 100644 index 0000000..e18b255 --- /dev/null +++ b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTSolvers/Project.toml @@ -0,0 +1,107 @@ +name = "CTSolvers" +uuid = "d3e8d392-8e4b-4d9b-8e92-d7d4e3650ef6" +version = "0.5.8" +authors = ["Olivier Cots "] + +[deps] +CTBase = "54762871-cc72-4466-b8e8-f6c8b58076cd" +CTModels = "34c4fa32-2049-4079-8329-de33c2a22e2d" +CommonSolve = "38540f10-b2f7-11e9-35d8-d573e4eb0ff2" +DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" +KernelAbstractions = "63c18a36-062a-441e-b654-da1e3ab1ce7c" +SolverCore = "ff4d7338-4cf1-434d-91df-b86cb86fb843" + +[weakdeps] +ADNLPModels = "54578032-b7ea-4c30-94aa-7cbd1cce6c9a" +CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba" +CUDSS = "45b445bb-4962-46a0-9369-b4df9d0f772e" +DiffEqBase = "2b5f629d-d688-5b77-993f-72d75c75574e" +Enzyme = "7da242da-08ed-463a-9acd-ee780be4f1d9" +ExaModels = "1037b233-b668-4ce9-9b63-f9f681f55dd2" +ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" +MadNCL = "434a0bcb-5a7c-42b2-a9d3-9e3f760e7af0" +MadNLP = "2621e9c9-9eb4-46b1-8089-e8c72242dfb6" +MadNLPGPU = "d72a61cc-809d-412f-99be-fd81f4b8a598" +NLPModels = "a4795742-8479-5a88-8948-cc11e1c8c1a6" +NLPModelsIpopt = "f4238b75-b362-5c4c-b852-0801c9a21d71" +NLPModelsKnitro = "bec4dd0d-7755-52d5-9a02-22f0ffc7efcb" +OrdinaryDiffEqTsit5 = "b1df2697-797e-41e3-8120-5422d3b24e4a" +SciMLBase = "0bca4576-84f4-4d90-8ffe-ffa030f20462" +UnoSolver = "1baa60ac-02f7-4b39-a7a8-2f4f58486b05" +Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f" + +[extensions] +CTSolversADNLPModels = "ADNLPModels" +CTSolversCUDA = "CUDA" +CTSolversEnzyme = "Enzyme" +CTSolversExaModels = ["ExaModels"] +CTSolversForwardDiff = "ForwardDiff" +CTSolversIpopt = ["NLPModels", "NLPModelsIpopt"] +CTSolversKnitro = ["NLPModels", "NLPModelsKnitro"] +CTSolversMadNCL = ["MadNCL", "MadNLP", "NLPModels"] +CTSolversMadNLP = ["MadNLP", "NLPModels"] +CTSolversMadNLPGPU = ["MadNLPGPU", "CUDA", "CUDSS"] +CTSolversOrdinaryDiffEqTsit5 = "OrdinaryDiffEqTsit5" +CTSolversSciMLIntegrator = ["DiffEqBase", "SciMLBase"] +CTSolversUno = ["NLPModels", "UnoSolver"] +CTSolversZygote = "Zygote" + +[compat] +ADNLPModels = "0.8" +Aqua = "0.8" +BenchmarkTools = "1" +CTBase = "0.30" +CTModels = "0.19" +CUDA = "5, 6" +CUDSS = "0.6, 0.7, 0.8" +CommonSolve = "0.2" +DiffEqBase = "6, 7" +DocStringExtensions = "0.9" +Enzyme = "0.13" +ExaModels = "0.12" +ForwardDiff = "0.10, 1" +JET = "0.9, 0.11, 0.12" +KernelAbstractions = "0.9" +MadNCL = "0.2" +MadNLP = "0.9, 0.10" +MadNLPGPU = "0.8, 0.10" +NLPModels = "0.21" +NLPModelsIpopt = "0.11" +NLPModelsKnitro = "0.10" +OrderedCollections = "1, 2" +OrdinaryDiffEqTsit5 = "2" +Random = "1" +SciMLBase = "3" +SolverCore = "0.3" +Test = "1" +UnoSolver = "0.3" +Zygote = "0.7" +julia = "1.10" + +[extras] +ADNLPModels = "54578032-b7ea-4c30-94aa-7cbd1cce6c9a" +Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" +BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" +CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba" +CUDSS = "45b445bb-4962-46a0-9369-b4df9d0f772e" +DiffEqBase = "2b5f629d-d688-5b77-993f-72d75c75574e" +Enzyme = "7da242da-08ed-463a-9acd-ee780be4f1d9" +ExaModels = "1037b233-b668-4ce9-9b63-f9f681f55dd2" +ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" +JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" +MadNCL = "434a0bcb-5a7c-42b2-a9d3-9e3f760e7af0" +MadNLP = "2621e9c9-9eb4-46b1-8089-e8c72242dfb6" +MadNLPGPU = "d72a61cc-809d-412f-99be-fd81f4b8a598" +NLPModels = "a4795742-8479-5a88-8948-cc11e1c8c1a6" +NLPModelsIpopt = "f4238b75-b362-5c4c-b852-0801c9a21d71" +NLPModelsKnitro = "bec4dd0d-7755-52d5-9a02-22f0ffc7efcb" +OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" +OrdinaryDiffEqTsit5 = "b1df2697-797e-41e3-8120-5422d3b24e4a" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +SciMLBase = "0bca4576-84f4-4d90-8ffe-ffa030f20462" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +UnoSolver = "1baa60ac-02f7-4b39-a7a8-2f4f58486b05" +Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f" + +[targets] +test = ["ADNLPModels", "Aqua", "BenchmarkTools", "CUDA", "CUDSS", "DiffEqBase", "Enzyme", "ExaModels", "ForwardDiff", "JET", "MadNCL", "MadNLP", "MadNLPGPU", "NLPModels", "NLPModelsIpopt", "OrderedCollections", "OrdinaryDiffEqTsit5", "Random", "SciMLBase", "Test", "UnoSolver", "Zygote"] diff --git a/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTSolvers/docs/Project.toml b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTSolvers/docs/Project.toml new file mode 100644 index 0000000..48ed89a --- /dev/null +++ b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/CTSolvers/docs/Project.toml @@ -0,0 +1,56 @@ +[deps] +ADNLPModels = "54578032-b7ea-4c30-94aa-7cbd1cce6c9a" +CTBase = "54762871-cc72-4466-b8e8-f6c8b58076cd" +CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba" +CUDSS = "45b445bb-4962-46a0-9369-b4df9d0f772e" +CommonSolve = "38540f10-b2f7-11e9-35d8-d573e4eb0ff2" +DiffEqBase = "2b5f629d-d688-5b77-993f-72d75c75574e" +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +DocumenterInterLinks = "d12716ef-a0f6-4df4-a9f1-a5a34e75c656" +DocumenterVitepress = "4710194d-e776-4893-9690-8d956a29c365" +Enzyme = "7da242da-08ed-463a-9acd-ee780be4f1d9" +ExaModels = "1037b233-b668-4ce9-9b63-f9f681f55dd2" +ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" +JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" +LiveServer = "16fef848-5104-11e9-1b77-fb7a48bbb589" +MadNCL = "434a0bcb-5a7c-42b2-a9d3-9e3f760e7af0" +MadNLP = "2621e9c9-9eb4-46b1-8089-e8c72242dfb6" +MadNLPGPU = "d72a61cc-809d-412f-99be-fd81f4b8a598" +MarkdownAST = "d0879d2d-cac2-40c8-9cee-1863dc0c7391" +NLPModels = "a4795742-8479-5a88-8948-cc11e1c8c1a6" +NLPModelsIpopt = "f4238b75-b362-5c4c-b852-0801c9a21d71" +NLPModelsKnitro = "bec4dd0d-7755-52d5-9a02-22f0ffc7efcb" +OrdinaryDiffEqTsit5 = "b1df2697-797e-41e3-8120-5422d3b24e4a" +OrdinaryDiffEqVerner = "79d7bb75-1356-48c1-b8c0-6832512096c2" +SciMLBase = "0bca4576-84f4-4d90-8ffe-ffa030f20462" +UnoSolver = "1baa60ac-02f7-4b39-a7a8-2f4f58486b05" +Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f" + +[compat] +ADNLPModels = "0.8" +CTBase = "0.30" +CUDA = "5, 6" +CUDSS = "0.6, 0.7, 0.8" +CommonSolve = "0.2" +DiffEqBase = "6, 7" +Documenter = "1" +DocumenterInterLinks = "1" +DocumenterVitepress = "0.3" +Enzyme = "0.13" +ExaModels = "0.12" +ForwardDiff = "0.10, 1" +JET = "0.9, 0.11, 0.12" +LiveServer = "1" +MadNCL = "0.2" +MadNLP = "0.9, 0.10" +MadNLPGPU = "0.8, 0.10" +MarkdownAST = "0.1" +NLPModels = "0.21" +NLPModelsIpopt = "0.11" +NLPModelsKnitro = "0.10" +OrdinaryDiffEqTsit5 = "2" +OrdinaryDiffEqVerner = "2" +SciMLBase = "3" +UnoSolver = "0.3" +Zygote = "0.7" +julia = "1.10" diff --git a/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/OptimalControl/Project.toml b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/OptimalControl/Project.toml new file mode 100644 index 0000000..ad776d9 --- /dev/null +++ b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/OptimalControl/Project.toml @@ -0,0 +1,80 @@ +name = "OptimalControl" +uuid = "5f98b655-cc9a-415a-b60e-744165666948" +version = "2.2.1-beta" +authors = ["Olivier Cots "] + +[deps] +ADNLPModels = "54578032-b7ea-4c30-94aa-7cbd1cce6c9a" +CTBase = "54762871-cc72-4466-b8e8-f6c8b58076cd" +CTDirect = "790bbbee-bee9-49ee-8912-a9de031322d5" +CTFlows = "1c39547c-7794-42f7-af83-d98194f657c2" +CTLie = "6880e05b-3a7d-4cac-887c-30cb52c5fdde" +CTModels = "34c4fa32-2049-4079-8329-de33c2a22e2d" +CTParser = "32681960-a1b1-40db-9bff-a1ca817385d1" +CTSolvers = "d3e8d392-8e4b-4d9b-8e92-d7d4e3650ef6" +CommonSolve = "38540f10-b2f7-11e9-35d8-d573e4eb0ff2" +DifferentiationInterface = "a0c0ee7d-e4b9-4e03-894e-1c5f64a51d63" +DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" +ExaModels = "1037b233-b668-4ce9-9b63-f9f681f55dd2" +ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" +Reexport = "189a3867-3050-52da-a836-e630ba90ab69" + +[compat] +ADNLPModels = "0.8" +BenchmarkTools = "1" +CTBase = "0.30" +CTDirect = "1" +CTFlows = "0.18" +CTLie = "0.2" +CTModels = "0.19" +CTParser = "0.9" +CTSolvers = "0.5" +CUDA = "6" +CUDSS = "0.7" +CommonSolve = "0.2" +DiffEqBase = "7" +DifferentiationInterface = "0.7" +DocStringExtensions = "0.9" +ExaModels = "0.12" +ForwardDiff = "1" +LinearAlgebra = "1" +MadNCL = "0.2" +MadNLP = "0.10" +MadNLPGPU = "0.10" +NLPModelsIpopt = "0.11" +NonlinearSolve = "4" +OrdinaryDiffEq = "7" +OrdinaryDiffEqTsit5 = "2" +Printf = "1" +RecipesBase = "1" +Reexport = "1" +SciMLBase = "3" +SolverCore = "0.3" +SplitApplyCombine = "1" +Test = "1" +UnoSolver = "0.3" +julia = "1.10" + +[extras] +BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" +CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba" +CUDSS = "45b445bb-4962-46a0-9369-b4df9d0f772e" +DiffEqBase = "2b5f629d-d688-5b77-993f-72d75c75574e" +MadNCL = "434a0bcb-5a7c-42b2-a9d3-9e3f760e7af0" +MadNLP = "2621e9c9-9eb4-46b1-8089-e8c72242dfb6" +MadNLPGPU = "d72a61cc-809d-412f-99be-fd81f4b8a598" +NLPModelsIpopt = "f4238b75-b362-5c4c-b852-0801c9a21d71" +NonlinearSolve = "8913a72c-1f9b-4ce2-8d82-65094dcecaec" +OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed" +OrdinaryDiffEqTsit5 = "b1df2697-797e-41e3-8120-5422d3b24e4a" +Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" +SciMLBase = "0bca4576-84f4-4d90-8ffe-ffa030f20462" +SolverCore = "ff4d7338-4cf1-434d-91df-b86cb86fb843" +SplitApplyCombine = "03a91e81-4c3e-53e1-a0a4-9c0c8f19dd66" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +UnoSolver = "1baa60ac-02f7-4b39-a7a8-2f4f58486b05" + +[targets] +test = ["BenchmarkTools", "CUDA", "CUDSS", "DiffEqBase", "MadNCL", "MadNLP", "MadNLPGPU", "NLPModelsIpopt", "NonlinearSolve", "OrdinaryDiffEq", "OrdinaryDiffEqTsit5", "Printf", "SciMLBase", "SolverCore", "SplitApplyCombine", "Test", "UnoSolver"] diff --git a/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/OptimalControl/docs/Project.toml b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/OptimalControl/docs/Project.toml new file mode 100644 index 0000000..0a0a850 --- /dev/null +++ b/scripts/compat-prune/test/fixtures/snapshot-2026-10-15/registered/OptimalControl/docs/Project.toml @@ -0,0 +1,72 @@ +[deps] +ADNLPModels = "54578032-b7ea-4c30-94aa-7cbd1cce6c9a" +CTBase = "54762871-cc72-4466-b8e8-f6c8b58076cd" +CTDirect = "790bbbee-bee9-49ee-8912-a9de031322d5" +CTFlows = "1c39547c-7794-42f7-af83-d98194f657c2" +CTLie = "6880e05b-3a7d-4cac-887c-30cb52c5fdde" +CTModels = "34c4fa32-2049-4079-8329-de33c2a22e2d" +CTParser = "32681960-a1b1-40db-9bff-a1ca817385d1" +CTSolvers = "d3e8d392-8e4b-4d9b-8e92-d7d4e3650ef6" +CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba" +CUDSS = "45b445bb-4962-46a0-9369-b4df9d0f772e" +CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" +Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" +CommonSolve = "38540f10-b2f7-11e9-35d8-d573e4eb0ff2" +DifferentiationInterface = "a0c0ee7d-e4b9-4e03-894e-1c5f64a51d63" +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +DocumenterInterLinks = "d12716ef-a0f6-4df4-a9f1-a5a34e75c656" +DocumenterVitepress = "4710194d-e776-4893-9690-8d956a29c365" +ExaModels = "1037b233-b668-4ce9-9b63-f9f681f55dd2" +ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" +JLD2 = "033835bb-8acc-5ee8-8aae-3f567f8a3819" +JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" +Literate = "98b081ad-f1c9-55d3-8b20-4c87d4299306" +LiveServer = "16fef848-5104-11e9-1b77-fb7a48bbb589" +MadNCL = "434a0bcb-5a7c-42b2-a9d3-9e3f760e7af0" +MadNLP = "2621e9c9-9eb4-46b1-8089-e8c72242dfb6" +MadNLPGPU = "d72a61cc-809d-412f-99be-fd81f4b8a598" +MarkdownAST = "d0879d2d-cac2-40c8-9cee-1863dc0c7391" +NLPModelsIpopt = "f4238b75-b362-5c4c-b852-0801c9a21d71" +NLPModelsKnitro = "bec4dd0d-7755-52d5-9a02-22f0ffc7efcb" +NonlinearSolve = "8913a72c-1f9b-4ce2-8d82-65094dcecaec" +OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed" +OrdinaryDiffEqTsit5 = "b1df2697-797e-41e3-8120-5422d3b24e4a" +Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" +UnoSolver = "1baa60ac-02f7-4b39-a7a8-2f4f58486b05" + +[compat] +ADNLPModels = "0.8" +CTBase = "0.30" +CTDirect = "1" +CTFlows = "0.18" +CTLie = "0.2" +CTModels = "0.19" +CTParser = "0.9" +CTSolvers = "0.5" +CUDA = "6" +CUDSS = "0.7" +CairoMakie = "0.15" +Colors = "0.13" +CommonSolve = "0.2" +DifferentiationInterface = "0.7" +Documenter = "1" +DocumenterInterLinks = "1" +DocumenterVitepress = "0.3" +ExaModels = "0.12" +ForwardDiff = "1" +JLD2 = "0.6" +JSON3 = "1" +Literate = "2" +LiveServer = "1" +MadNCL = "0.2" +MadNLP = "0.10" +MadNLPGPU = "0.10" +MarkdownAST = "0.1" +NLPModelsIpopt = "0.11" +NLPModelsKnitro = "0.10" +NonlinearSolve = "4" +OrdinaryDiffEq = "7" +OrdinaryDiffEqTsit5 = "2" +Plots = "1" +UnoSolver = "0.3" +julia = "1.10" diff --git a/scripts/compat-prune/test/golden/classified-2026-10-15.txt b/scripts/compat-prune/test/golden/classified-2026-10-15.txt new file mode 100644 index 0000000..ea1d6f8 --- /dev/null +++ b/scripts/compat-prune/test/golden/classified-2026-10-15.txt @@ -0,0 +1,45 @@ +CTBase | Project.toml | DocStringExtensions | blocked | newest 0.10 +CTBase | Project.toml | ForwardDiff | prune | 1 +CTBase | Project.toml | JET | prune | 0.9, 0.12 +CTBase | docs/Project.toml | ForwardDiff | prune | 1 +CTBase | docs/Project.toml | JET | prune | 0.9, 0.12 +CTDirect | Project.toml | CTBase | prune | 0.30 +CTDirect | Project.toml | CTModels | prune | 0.19 +CTDirect | docs/Project.toml | CTBase | prune | 0.30 +CTFlows | Project.toml | CUDA | prune | 6 +CTFlows | Project.toml | DiffEqBase | prune | 7 +CTFlows | Project.toml | ForwardDiff | prune | 1 +CTFlows | Project.toml | GPUArraysCore | prune | 0.2 +CTFlows | docs/Project.toml | DifferentiationInterface | prune | 0.7 +CTFlows | docs/Project.toml | ForwardDiff | prune | 1 +CTLie | Project.toml | CUDA | prune | 6 +CTLie | Project.toml | ForwardDiff | prune | 1 +CTLie | Project.toml | JET | prune | 0.9, 0.12 +CTLie | docs/Project.toml | ForwardDiff | prune | 1 +CTModels | Project.toml | JET | prune | 0.9, 0.12 +CTModels | Project.toml | OrderedCollections | prune | 2 +CTModels | docs/Project.toml | JET | prune | 0.9, 0.12 +CTParser | Project.toml | CTBase | keep | +CTParser | Project.toml | OrderedCollections | prune | 2 +CTParser | docs/Project.toml | CTBase | prune | 0.30 +CTParser | test/Project.toml | CTBase | blocked | newest 0.30 +CTParser | test/Project.toml | CUDA | prune | 6 +CTParser | test/Project.toml | CUDSS | prune | 0.8 +CTParser | test/Project.toml | MadNLP | prune | 0.10 +CTParser | test/Project.toml | MadNLPGPU | prune | 0.10 +CTParser | test/Project.toml | OrderedCollections | prune | 2 +CTSolvers | Project.toml | CUDA | prune | 6 +CTSolvers | Project.toml | CUDSS | prune | 0.7, 0.8 +CTSolvers | Project.toml | DiffEqBase | prune | 7 +CTSolvers | Project.toml | ForwardDiff | prune | 1 +CTSolvers | Project.toml | JET | prune | 0.9, 0.12 +CTSolvers | Project.toml | MadNLP | prune | 0.10 +CTSolvers | Project.toml | MadNLPGPU | prune | 0.10 +CTSolvers | Project.toml | OrderedCollections | prune | 2 +CTSolvers | docs/Project.toml | CUDA | prune | 6 +CTSolvers | docs/Project.toml | CUDSS | prune | 0.8 +CTSolvers | docs/Project.toml | DiffEqBase | prune | 7 +CTSolvers | docs/Project.toml | ForwardDiff | prune | 1 +CTSolvers | docs/Project.toml | JET | prune | 0.9, 0.12 +CTSolvers | docs/Project.toml | MadNLP | prune | 0.10 +CTSolvers | docs/Project.toml | MadNLPGPU | prune | 0.10 diff --git a/scripts/compat-prune/test/golden/edits-2026-10-15.txt b/scripts/compat-prune/test/golden/edits-2026-10-15.txt new file mode 100644 index 0000000..570e322 --- /dev/null +++ b/scripts/compat-prune/test/golden/edits-2026-10-15.txt @@ -0,0 +1,42 @@ +CTBase | Project.toml | ForwardDiff | edited | +CTBase | Project.toml | JET | edited | +CTBase | docs/Project.toml | ForwardDiff | edited | +CTBase | docs/Project.toml | JET | edited | +CTDirect | Project.toml | CTBase | edited | +CTDirect | Project.toml | CTModels | edited | +CTDirect | docs/Project.toml | CTBase | edited | +CTFlows | Project.toml | CUDA | edited | +CTFlows | Project.toml | DiffEqBase | edited | +CTFlows | Project.toml | ForwardDiff | edited | +CTFlows | Project.toml | GPUArraysCore | edited | +CTFlows | docs/Project.toml | DifferentiationInterface | edited | +CTFlows | docs/Project.toml | ForwardDiff | edited | +CTLie | Project.toml | CUDA | edited | +CTLie | Project.toml | ForwardDiff | edited | +CTLie | Project.toml | JET | edited | +CTLie | docs/Project.toml | ForwardDiff | edited | +CTModels | Project.toml | JET | edited | +CTModels | Project.toml | OrderedCollections | edited | +CTModels | docs/Project.toml | JET | edited | +CTParser | Project.toml | OrderedCollections | drift | `1` on the default branch, `1, 2` audited +CTParser | docs/Project.toml | CTBase | drift | `0.18, 0.27, 0.28` on the default branch, `0.29, 0.30` audited +CTParser | test/Project.toml | CUDA | drift | `5` on the default branch, `5, 6` audited +CTParser | test/Project.toml | CUDSS | drift | no `CUDSS` entry in `[compat]` on the default branch +CTParser | test/Project.toml | MadNLP | drift | `0.9` on the default branch, `0.9, 0.10` audited +CTParser | test/Project.toml | MadNLPGPU | drift | `0.8` on the default branch, `0.8, 0.10` audited +CTParser | test/Project.toml | OrderedCollections | drift | `1.8` on the default branch, `1.8, 2` audited +CTSolvers | Project.toml | CUDA | edited | +CTSolvers | Project.toml | CUDSS | edited | +CTSolvers | Project.toml | DiffEqBase | edited | +CTSolvers | Project.toml | ForwardDiff | edited | +CTSolvers | Project.toml | JET | edited | +CTSolvers | Project.toml | MadNLP | edited | +CTSolvers | Project.toml | MadNLPGPU | edited | +CTSolvers | Project.toml | OrderedCollections | edited | +CTSolvers | docs/Project.toml | CUDA | edited | +CTSolvers | docs/Project.toml | CUDSS | edited | +CTSolvers | docs/Project.toml | DiffEqBase | edited | +CTSolvers | docs/Project.toml | ForwardDiff | edited | +CTSolvers | docs/Project.toml | JET | edited | +CTSolvers | docs/Project.toml | MadNLP | edited | +CTSolvers | docs/Project.toml | MadNLPGPU | edited | diff --git a/scripts/compat-prune/test/golden/envs-2026-10-15.txt b/scripts/compat-prune/test/golden/envs-2026-10-15.txt new file mode 100644 index 0000000..e6dc0ca --- /dev/null +++ b/scripts/compat-prune/test/golden/envs-2026-10-15.txt @@ -0,0 +1,17 @@ +CTBase/docs: deps=ADTypes,CTBase,CairoMakie,Coverage,DifferentiationInterface,Documenter,DocumenterInterLinks,DocumenterVitepress,ForwardDiff,JET,LiveServer,MarkdownAST,Plots pins=CTBase=0.30.10 +CTBase/test: deps=Aqua,BenchmarkTools,CTBase,CairoMakie,Coverage,DifferentiationInterface,Documenter,ForwardDiff,JET,Logging,Markdown,MarkdownAST,Plots,Test pins=CTBase=0.30.10 +CTDirect/docs: deps=CTBase,CTDirect,Documenter,DocumenterMermaid,Markdown,MarkdownAST pins=CTDirect=1.1.8-beta +CTDirect/test: deps=AMDGPU,CTDirect,CTParser,CUDA,CUDSS,CommonSolve,MadNLP,MadNLPGPU,NLPModels,NLPModelsIpopt,SplitApplyCombine,Test pins=CTDirect=1.1.8-beta +CTFlows/docs: deps=CTBase,CTFlows,CTModels,CTSolvers,CairoMakie,DiffEqBase,DifferentiationInterface,Documenter,DocumenterInterLinks,DocumenterVitepress,ForwardDiff,LiveServer,MarkdownAST,OrdinaryDiffEqRosenbrock,OrdinaryDiffEqTsit5,Plots,SciMLBase,StaticArrays pins=CTFlows=0.18.1 +CTFlows/test: deps=Aqua,CTFlows,CTLie,CUDA,CairoMakie,DiffEqBase,DiffEqGPU,DifferentiationInterface,ForwardDiff,Mooncake,NonlinearSolve,OrdinaryDiffEqTsit5,Plots,SciMLBase,StaticArrays,Test,Zygote pins=CTFlows=0.18.1 +CTLie/docs: deps=ADTypes,CTBase,CTLie,DifferentiationInterface,Documenter,DocumenterInterLinks,DocumenterVitepress,ForwardDiff,LiveServer,MarkdownAST pins=CTLie=0.2.3 +CTLie/test: deps=ADTypes,Aqua,BenchmarkTools,CTLie,CUDA,DifferentiationInterface,ForwardDiff,JET,Mooncake,Test pins=CTLie=0.2.3 +CTModels/docs: deps=CTBase,CTModels,CairoMakie,Documenter,DocumenterInterLinks,DocumenterVitepress,JET,JLD2,JSON3,LiveServer,MarkdownAST,Plots pins=CTModels=0.19.8 +CTModels/test: deps=Aqua,BenchmarkTools,CTModels,CairoMakie,JET,JLD2,JSON3,Plots,Random,Test pins=CTModels=0.19.8 +CTParser/docs: deps=CTBase,CTParser,Documenter,Markdown,MarkdownAST pins=CTParser=0.9.5-beta +CTParser/test: deps=Aqua,BenchmarkTools,CTBase,CTModels,CTParser,CUDA,CUDSS,ExaModels,Interpolations,KernelAbstractions,LinearAlgebra,MadNLP,MadNLPGPU,NLPModels,OrderedCollections,Test pins=CTParser=0.9.5-beta +CTSolvers/docs: deps=ADNLPModels,CTBase,CTSolvers,CUDA,CUDSS,CommonSolve,DiffEqBase,Documenter,DocumenterInterLinks,DocumenterVitepress,Enzyme,ExaModels,ForwardDiff,JET,LiveServer,MadNCL,MadNLP,MadNLPGPU,MarkdownAST,NLPModels,NLPModelsIpopt,NLPModelsKnitro,OrdinaryDiffEqTsit5,OrdinaryDiffEqVerner,SciMLBase,UnoSolver,Zygote pins=CTSolvers=0.5.8 +CTSolvers/test: deps=ADNLPModels,Aqua,BenchmarkTools,CTSolvers,CUDA,CUDSS,DiffEqBase,Enzyme,ExaModels,ForwardDiff,JET,MadNCL,MadNLP,MadNLPGPU,NLPModels,NLPModelsIpopt,OrderedCollections,OrdinaryDiffEqTsit5,Random,SciMLBase,Test,UnoSolver,Zygote pins=CTSolvers=0.5.8 +OptimalControl/docs: deps=ADNLPModels,CTBase,CTDirect,CTFlows,CTLie,CTModels,CTParser,CTSolvers,CUDA,CUDSS,CairoMakie,Colors,CommonSolve,DifferentiationInterface,Documenter,DocumenterInterLinks,DocumenterVitepress,ExaModels,ForwardDiff,JLD2,JSON3,Literate,LiveServer,MadNCL,MadNLP,MadNLPGPU,MarkdownAST,NLPModelsIpopt,NLPModelsKnitro,NonlinearSolve,OptimalControl,OrdinaryDiffEq,OrdinaryDiffEqTsit5,Plots,UnoSolver pins=OptimalControl=2.2.1-beta +OptimalControl/test: deps=BenchmarkTools,CUDA,CUDSS,DiffEqBase,MadNCL,MadNLP,MadNLPGPU,NLPModelsIpopt,NonlinearSolve,OptimalControl,OrdinaryDiffEq,OrdinaryDiffEqTsit5,Printf,SciMLBase,SolverCore,SplitApplyCombine,Test,UnoSolver pins=OptimalControl=2.2.1-beta +joint: deps=CTBase,CTDirect,CTFlows,CTLie,CTModels,CTParser,CTSolvers,OptimalControl pins=CTBase=0.30.10,CTDirect=1.1.8-beta,CTFlows=0.18.1,CTLie=0.2.3,CTModels=0.19.8,CTParser=0.9.5-beta,CTSolvers=0.5.8,OptimalControl=2.2.1-beta diff --git a/scripts/compat-prune/test/golden/report.md b/scripts/compat-prune/test/golden/report.md new file mode 100644 index 0000000..c24b53f --- /dev/null +++ b/scripts/compat-prune/test/golden/report.md @@ -0,0 +1,78 @@ + + +Audit of the `[compat]` entries of the control-toolbox packages, by the +[`compat-prune`](https://github.com/control-toolbox/CTActions/blob/main/.github/workflows/compat-prune.yml) workflow on 2026-10-15 19:48 UTC ([run](https://github.com/example-org/Root.jl/actions/runs/1)), for Julia 1.10, 1.12, 1.13. This body is rewritten by every run: do not edit it. + +A prune drops `[compat]` specifiers that no environment selects, on no Julia version, and +leaves every resolved manifest (joint, tests and docs of every package) unchanged. PRs can +therefore be merged in any order, and need no release of their own. + +## Summary + +| | entries | +| --- | --- | +| to prune (open PRs) | 3 | +| merged, awaiting release | 1 | +| drift (not edited) | 1 | +| rejected by validation | 1 | +| withheld | 1 | +| blocked (information) | 2 | + +## To prune + +### Alpha — https://github.com/example-org/Alpha.jl/pull/12 + +| file | dependency | before | after | selected | +| --- | --- | --- | --- | --- | +| `Project.toml` | `Dep1` | `0.5, 0.6, 0.7` | `0.6, 0.7` | 0.7.2 on 1.12; 0.6.4 on 1.10 | +| `test/Project.toml` | `Tool` | `1, 2` | `2` | 2.0.0 on 1.10, 1.12 | + +### Beta + +| file | dependency | before | after | selected | +| --- | --- | --- | --- | --- | +| `Project.toml` | `Dep1` | `0.6, 0.7` | `0.7` | 0.7.2 on 1.12 | + +## Merged, awaiting release + +| entry | after | +| --- | --- | +| Beta `Project.toml` `Tool` | `2` | + +## Drift + +The default branch differs from the registered version: not edited. + +| entry | audited | detail | +| --- | --- | --- | +| Alpha `docs/Project.toml` `Doc` | `0.8, 0.9` | `0.9, 1` on the default branch, `0.8, 0.9` audited | + +## Rejected + +| entry | where | reason | +| --- | --- | --- | +| Beta `Project.toml` `Weak` | Beta/test@1.10 | manifest differs: A 1.0.0 → 1.1.0 \| odd second line | + +## Withheld + +Baseline failed in joint@1.10: lanes used there are unknown. + +| entry | would drop | +| --- | --- | +| Alpha `Project.toml` `Dep2` | `1` | + +## Blocked + +The newest specifier is never selected: information only, nothing is edited. + +| entry | newest | capped by | +| --- | --- | --- | +| Alpha `docs/Project.toml` `Doc` | `1` | Beta, Documenter | +| Beta `Project.toml` `Weak` | `0.2` | resolver preference | + +## Audited versions + +| package | version | repository | +| --- | --- | --- | +| Alpha | 0.3.1 | [example-org/Alpha.jl](https://github.com/example-org/Alpha.jl) | +| Beta | 1.2.0-beta | [example-org/Beta.jl](https://github.com/example-org/Beta.jl) | diff --git a/scripts/compat-prune/test/helpers.jl b/scripts/compat-prune/test/helpers.jl new file mode 100644 index 0000000..781eb44 --- /dev/null +++ b/scripts/compat-prune/test/helpers.jl @@ -0,0 +1,88 @@ +# Shared by every test file: the scripts (their `main` blocks do not run when included), +# fixture paths, and small builders. + +const SCRIPTS = joinpath(@__DIR__, "..") +for f in ("collect.jl", "envs.jl", "resolve.jl", "classify.jl", "validate.jl", "edit.jl", "report.jl") + include(joinpath(SCRIPTS, f)) +end + +const FIXTURES = joinpath(@__DIR__, "fixtures") +const COLLECTED = joinpath(FIXTURES, "collected") +const GOLDEN = joinpath(@__DIR__, "golden") +const UPDATE_GOLDEN = get(ENV, "COMPAT_PRUNE_UPDATE_GOLDEN", "") == "1" + +"A resolved environment result with the given name => version pairs." +ok(versions...) = Dict("ok" => true, "versions" => Dict{String,Any}(versions...)) + +"A failed environment result." +failed(msg="unsatisfiable") = Dict("ok" => false, "error" => msg) + +"A prune record." +prune(owner, file, dep, before, after) = + Dict{String,Any}("owner" => owner, "file" => file, "dep" => dep, "before" => before, "after" => after, + "removed" => setdiff(specifiers(before), specifiers(after)), "status" => "prune", + "evidence" => Dict{String,Any}()) + +"Compare `text` with the golden file `name`, or rewrite it under COMPAT_PRUNE_UPDATE_GOLDEN=1." +function golden(name, text) + path = joinpath(GOLDEN, name) + if UPDATE_GOLDEN || !isfile(path) + mkpath(dirname(path)) + write(path, text) + @info "golden file written: $name" + end + return text == read(path, String) +end + +"Run `f` with `GH` replaced by `fake`, restoring the real runner afterwards." +function with_gh(f, fake) + real = GH[] + GH[] = fake + try + f() + finally + GH[] = real + end +end + +# Captured from real runs (2026-10-15): DocStringExtensions restricted to its newest 0.10. +const CONFLICT_ON_DEP = """ +Unsatisfiable requirements detected for package DocStringExtensions [ffbed154]: + DocStringExtensions [ffbed154] log: + ├─possible versions are: 0.9.3 - 0.10.0 or uninstalled + ├─restricted to versions 0.10 by project [4e53ef5c], leaving only versions: 0.10.0 + │ └─project [4e53ef5c] log: + │ ├─possible versions are: 0.0.0 or uninstalled + │ └─project [4e53ef5c] is fixed to version 0.0.0 + └─restricted by compatibility requirements with SciMLBase [0bca4576] to versions: 0.9.5 — no versions left + └─SciMLBase [0bca4576] log: + ├─possible versions are: 2.77.2 - 3.58.0 or uninstalled + └─restricted by compatibility requirements with Deeper [00000000] to versions: 3 +""" +const CONFLICT_ON_OTHER = """ +Unsatisfiable requirements detected for package DocumenterVitepress [4710194d]: + DocumenterVitepress [4710194d] log: + ├─possible versions are: 0.3.4 - 0.3.8 or uninstalled + └─restricted by compatibility requirements with DocStringExtensions [ffbed154] to versions: uninstalled — no versions left + └─DocStringExtensions [ffbed154] log: + └─restricted to versions 0.10 by project [064cce67], leaving only versions: 0.10.0 +""" + +"Run `f(root)` on a temporary copy of the collected fixture, with its `envs.toml` built." +function with_collected(f) + mktempdir() do dir + root = joinpath(dir, "collected") + cp(COLLECTED, root) + open(io -> TOML.print(io, Dict("env" => build_envs(root))), joinpath(root, "envs.toml"), "w") + f(root) + end +end + +"Baseline results of the collected fixture, keyed `@`." +fixture_results() = Dict{String,Any}( + "joint@1.10" => ok("Alpha" => "0.3.1", "Beta" => "1.2.0-beta", "Dep1" => "0.6.4"), + "joint@1.12" => ok("Alpha" => "0.3.1", "Beta" => "1.2.0-beta", "Dep1" => "0.7.2"), + "Alpha/test@1.12" => ok("Alpha" => "0.3.1", "Beta" => "1.2.0-beta", "Dep1" => "0.7.2", "Tool" => "2.0.0"), + "Alpha/docs@1.12" => ok("Alpha" => "0.3.1", "Beta" => "1.2.0-beta", "Dep1" => "0.7.2", "Doc" => "0.9.3"), + "Beta/test@1.12" => ok("Beta" => "1.2.0-beta", "Dep1" => "0.7.2", "Tool" => "2.1.0"), +) diff --git a/scripts/compat-prune/test/runtests.jl b/scripts/compat-prune/test/runtests.jl index 4b3ddff..4334ce6 100644 --- a/scripts/compat-prune/test/runtests.jl +++ b/scripts/compat-prune/test/runtests.jl @@ -1,144 +1,30 @@ -# Unit tests of the pure logic of compat-prune (no resolution, no network). +# Tests of compat-prune. # # usage: julia scripts/compat-prune/test/runtests.jl +# +# COMPAT_PRUNE_INTEGRATION=1 also run the integration tests (real Pkg resolutions against +# the local General registry, nothing downloaded; seconds) +# COMPAT_PRUNE_UPDATE_GOLDEN=1 rewrite the golden files instead of comparing to them +# +# Unit tests run on fixtures and fakes (no network, no resolution): the `gh` runner (`GH`) +# and the resolver of `simulate` are replaced by functions recording their calls. using Test -include(joinpath(@__DIR__, "..", "classify.jl")) -include(joinpath(@__DIR__, "..", "validate.jl")) - -# Baseline results keyed "@", as classify.jl builds them. -ok(versions...) = Dict("ok" => true, "versions" => Dict(versions...)) - -const AUDITED = Dict("CTSolvers" => "0.5.8", "CTParser" => "0.9.5-beta", "CTBase" => "0.30.9") - -# Captured from real runs (2026-10-15): DocStringExtensions restricted to 0.10. -const CONFLICT_ON_DEP = """ -Unsatisfiable requirements detected for package DocStringExtensions [ffbed154]: - DocStringExtensions [ffbed154] log: - ├─possible versions are: 0.9.3 - 0.10.0 or uninstalled - ├─restricted to versions 0.10 by project [4e53ef5c], leaving only versions: 0.10.0 - │ └─project [4e53ef5c] log: - │ ├─possible versions are: 0.0.0 or uninstalled - │ └─project [4e53ef5c] is fixed to version 0.0.0 - └─restricted by compatibility requirements with SciMLBase [0bca4576] to versions: 0.9.5 — no versions left - └─SciMLBase [0bca4576] log: - ├─possible versions are: 2.77.2 - 3.58.0 or uninstalled - └─restricted by compatibility requirements with Deeper [00000000] to versions: 3 -""" -const CONFLICT_ON_OTHER = """ -Unsatisfiable requirements detected for package DocumenterVitepress [4710194d]: - DocumenterVitepress [4710194d] log: - ├─possible versions are: 0.3.4 - 0.3.8 or uninstalled - └─restricted by compatibility requirements with DocStringExtensions [ffbed154] to versions: uninstalled — no versions left - └─DocStringExtensions [ffbed154] log: - └─restricted to versions 0.10 by project [064cce67], leaving only versions: 0.10.0 -""" +include("helpers.jl") @testset "compat-prune" begin - @testset "specifiers" begin - @test specifiers("0.9, 0.11,0.12") == ["0.9", "0.11", "0.12"] - @test lower("0.7 - 0.8") == v"0.7" - @test lower("^1.2") == v"1.2" - @test lower("=1.1.8") == v"1.1.8" - @test render(spec("0.6, 0.7, 0.8") ∩ spec("0.7, 0.8")) == "0.7 - 0.8" - @test render(spec("0.9, 0.11, 0.12") ∩ spec("0.9, 0.12")) == "0.9, 0.12" - @test isempty(render(spec("0.7") ∩ spec("0.8"))) - end - - @testset "applies" begin - v = Dict("CTSolvers" => "0.5.8") - @test applies("CTSolvers", "Project.toml", "OptimalControl/test", v, AUDITED) - # another version of the owner carries other compat entries - @test !applies("CTSolvers", "Project.toml", "X/test", Dict("CTSolvers" => "0.4.0"), AUDITED) - @test !applies("CTSolvers", "Project.toml", "X/test", Dict{String,String}(), AUDITED) - # test/ and docs/ entries apply in their own environment only - @test applies("CTParser", "test/Project.toml", "CTParser/test", v, AUDITED) - @test !applies("CTParser", "test/Project.toml", "CTParser/docs", v, AUDITED) - @test !applies("CTParser", "docs/Project.toml", "OptimalControl/docs", v, AUDITED) - end - - @testset "owner scoping keeps CTSolvers' CUDSS 0.7" begin - # OptimalControl's tests need CUDSS 0.7, CTDirect's take 0.8: both are CTSolvers users. - results = Dict( - "OptimalControl/test@1.12" => ok("CTSolvers" => "0.5.8", "CUDSS" => "0.7.0"), - "CTDirect/test@1.12" => ok("CTSolvers" => "0.5.8", "CUDSS" => "0.8.1"), - # an older CTSolvers selecting CUDSS 0.6 is not evidence for the audited file - "Old/test@1.12" => ok("CTSolvers" => "0.4.0", "CUDSS" => "0.6.3"), - ) - r = classify_entry("CTSolvers", "Project.toml", "CUDSS", "0.6, 0.7, 0.8", results, AUDITED) - @test r["status"] == "prune" - @test r["removed"] == ["0.6"] - @test r["after"] == "0.7, 0.8" - @test r["evidence"]["0.7"] == ["OptimalControl/test@1.12=0.7.0"] - end - - @testset "a lane used on Julia 1.10 only is kept" begin - results = Dict( - "CTBase/test@1.10" => ok("CTBase" => "0.30.9", "JET" => "0.9.18"), - "CTBase/test@1.12" => ok("CTBase" => "0.30.9", "JET" => "0.12.3"), - ) - r = classify_entry("CTBase", "Project.toml", "JET", "0.9, 0.11, 0.12", results, AUDITED) - @test r["status"] == "prune" - @test r["after"] == "0.9, 0.12" - end - - @testset "blocked, idle, keep" begin - results = Dict("CTParser/test@1.12" => ok("CTParser" => "0.9.5-beta", "CTBase" => "0.29.3", "CUDSS" => "0.7.0")) - r = classify_entry("CTParser", "test/Project.toml", "CTBase", "0.29, 0.30", results, AUDITED) - @test r["status"] == "blocked" - @test r["newest"] == "0.30" - @test r["applies_in"] == ["CTParser/test"] - @test !haskey(r, "prunable") - # blocked, with an obsolete lane below the one in use - r = classify_entry("CTParser", "test/Project.toml", "CUDSS", "0.6, 0.7, 0.8", results, AUDITED) - @test r["status"] == "blocked" - @test r["prunable"] == ["0.6"] - # resolved nowhere the entry applies - r = classify_entry("CTParser", "test/Project.toml", "CUDA", "5, 6", results, AUDITED) - @test r["status"] == "idle" - # every lane in use - results = Dict("CTParser/test@1.10" => ok("CTParser" => "0.9.5-beta", "JET" => "0.9.18"), - "CTParser/test@1.12" => ok("CTParser" => "0.9.5-beta", "JET" => "0.12.3")) - r = classify_entry("CTParser", "test/Project.toml", "JET", "0.9, 0.12", results, AUDITED) - @test r["status"] == "keep" - end - - @testset "a failed baseline withholds every prune" begin - # On its own, the entry would lose 0.9: the 1.10 environment that uses it failed. - results = Dict("CTBase/test@1.10" => Dict("ok" => false, "error" => "unsatisfiable"), - "CTBase/test@1.12" => ok("CTBase" => "0.30.9", "JET" => "0.12.3")) - r = classify_entry("CTBase", "Project.toml", "JET", "0.9, 0.12", results, AUDITED) - @test r["status"] == "prune" - kept, withheld = withhold([r], results) - @test isempty(kept) - @test only(withheld)["status"] == "withheld" - @test occursin("CTBase/test@1.10", only(withheld)["reason"]) - # all baselines resolved: nothing withheld - kept, withheld = withhold([r], Dict("CTBase/test@1.12" => results["CTBase/test@1.12"])) - @test kept == [r] && isempty(withheld) - end - - @testset "narrowings" begin - prunes = [Dict("owner" => "CTSolvers", "file" => "Project.toml", "dep" => "CUDSS", "after" => "0.7, 0.8"), - Dict("owner" => "CTSolvers", "file" => "docs/Project.toml", "dep" => "CUDSS", "after" => "0.8"), - Dict("owner" => "CTBase", "file" => "Project.toml", "dep" => "JET", "after" => "0.9, 0.12")] - v = Dict("CTSolvers" => "0.5.8", "CUDSS" => "0.8.1", "CTBase" => "0.30.9") - # JET absent: nothing to bind; the docs entry intersects with the root one - @test narrowings(prunes, "CTSolvers/docs", v, AUDITED) == Dict("CUDSS" => "0.8") - @test narrowings(prunes, "CTDirect/test", v, AUDITED) == Dict("CUDSS" => "0.7, 0.8") - @test isempty(narrowings(prunes, "CTDirect/test", Dict("CUDSS" => "0.8.1"), AUDITED)) - end - - @testset "validate helpers" begin - @test blockers(CONFLICT_ON_DEP, "DocStringExtensions") == ["SciMLBase"] - @test blockers(CONFLICT_ON_OTHER, "DocStringExtensions") == ["DocumenterVitepress"] - @test isempty(blockers("empty compat intersection for CUDSS", "CUDSS")) - base = Dict("A" => "1.0.0", "B" => "2.0.0") - @test departure(Dict("ok" => true, "versions" => copy(base)), base) === nothing - @test departure(Dict("ok" => true, "versions" => Dict("A" => "1.0.0", "B" => "2.1.0")), base) == - "manifest differs: B 2.0.0 → 2.1.0" - @test departure(Dict("ok" => false, "error" => "empty compat intersection for CUDSS"), base) == - "empty compat intersection for CUDSS" + include("test_common.jl") + include("test_collect.jl") + include("test_envs.jl") + include("test_classify.jl") + include("test_validate.jl") + include("test_edit.jl") + include("test_report.jl") + include("test_regression.jl") + if get(ENV, "COMPAT_PRUNE_INTEGRATION", "") == "1" + include("test_integration.jl") + else + @info "Integration tests skipped (set COMPAT_PRUNE_INTEGRATION=1)" end end diff --git a/scripts/compat-prune/test/test_classify.jl b/scripts/compat-prune/test/test_classify.jl new file mode 100644 index 0000000..03ba483 --- /dev/null +++ b/scripts/compat-prune/test/test_classify.jl @@ -0,0 +1,141 @@ +# classify.jl: which specifiers are used, and what becomes of each entry. + +const AUDITED_C = Dict("CTSolvers" => "0.5.8", "CTParser" => "0.9.5-beta", "CTBase" => "0.30.9") + +@testset "classify" begin + @testset "specifiers and lower bounds" begin + @test specifiers("0.9, 0.11,0.12") == ["0.9", "0.11", "0.12"] + @test specifiers("1") == ["1"] + @test lower("0.7 - 0.8") == v"0.7" + @test lower("^1.2") == v"1.2" + @test lower("~0.3.1") == v"0.3.1" + @test lower("=1.1.8") == v"1.1.8" + @test lower("1") == v"1" + end + + @testset "owner scoping keeps CTSolvers' CUDSS 0.7" begin + # OptimalControl's tests need CUDSS 0.7, CTDirect's take 0.8: both are CTSolvers users. + results = Dict( + "OptimalControl/test@1.12" => ok("CTSolvers" => "0.5.8", "CUDSS" => "0.7.0"), + "CTDirect/test@1.12" => ok("CTSolvers" => "0.5.8", "CUDSS" => "0.8.1"), + # an older CTSolvers selecting CUDSS 0.6 is no evidence for the audited file + "Old/test@1.12" => ok("CTSolvers" => "0.4.0", "CUDSS" => "0.6.3"), + ) + r = classify_entry("CTSolvers", "Project.toml", "CUDSS", "0.6, 0.7, 0.8", results, AUDITED_C) + @test r["status"] == "prune" + @test r["removed"] == ["0.6"] + @test r["after"] == "0.7, 0.8" + @test r["evidence"] == Dict("0.7" => ["OptimalControl/test@1.12=0.7.0"], "0.8" => ["CTDirect/test@1.12=0.8.1"]) + end + + @testset "a lane used on Julia 1.10 only is kept" begin + results = Dict("CTBase/test@1.10" => ok("CTBase" => "0.30.9", "JET" => "0.9.18"), + "CTBase/test@1.12" => ok("CTBase" => "0.30.9", "JET" => "0.12.3")) + r = classify_entry("CTBase", "Project.toml", "JET", "0.9, 0.11, 0.12", results, AUDITED_C) + @test r["status"] == "prune" + @test r["removed"] == ["0.11"] + @test r["after"] == "0.9, 0.12" + end + + @testset "declaration order is kept in `after`" begin + results = Dict("X@1.12" => ok("CTBase" => "0.30.9", "D" => "2.0.0"), + "Y@1.12" => ok("CTBase" => "0.30.9", "D" => "1.5.0")) + r = classify_entry("CTBase", "Project.toml", "D", "1.5, 0.9, 2", results, AUDITED_C) + @test r["after"] == "1.5, 2" + end + + @testset "blocked, idle, keep" begin + results = Dict("CTParser/test@1.12" => ok("CTParser" => "0.9.5-beta", "CTBase" => "0.29.3", "CUDSS" => "0.7.0")) + r = classify_entry("CTParser", "test/Project.toml", "CTBase", "0.29, 0.30", results, AUDITED_C) + @test r["status"] == "blocked" + @test r["newest"] == "0.30" + @test r["applies_in"] == ["CTParser/test"] + @test !haskey(r, "prunable") + # blocked, with an obsolete lane below the one in use + r = classify_entry("CTParser", "test/Project.toml", "CUDSS", "0.6, 0.7, 0.8", results, AUDITED_C) + @test r["status"] == "blocked" + @test r["prunable"] == ["0.6"] + # an unused lane between the used one and the newest is blocked, never pruned + r = classify_entry("CTParser", "test/Project.toml", "CUDSS", "0.7, 0.8, 0.9", results, AUDITED_C) + @test r["status"] == "blocked" && r["newest"] == "0.9" && !haskey(r, "prunable") + # resolved nowhere the entry applies + r = classify_entry("CTParser", "test/Project.toml", "CUDA", "5, 6", results, AUDITED_C) + @test r["status"] == "idle" + # every lane in use + results = Dict("CTParser/test@1.10" => ok("CTParser" => "0.9.5-beta", "JET" => "0.9.18"), + "CTParser/test@1.12" => ok("CTParser" => "0.9.5-beta", "JET" => "0.12.3")) + r = classify_entry("CTParser", "test/Project.toml", "JET", "0.9, 0.12", results, AUDITED_C) + @test r["status"] == "keep" + end + + @testset "a failed environment is no evidence" begin + results = Dict("CTBase/test@1.10" => failed(), + "CTBase/test@1.12" => ok("CTBase" => "0.30.9", "JET" => "0.12.3")) + r = classify_entry("CTBase", "Project.toml", "JET", "0.9, 0.12", results, AUDITED_C) + @test r["status"] == "prune" && r["after"] == "0.12" + end + + @testset "withhold" begin + r = prune("CTBase", "Project.toml", "JET", "0.9, 0.12", "0.12") + results = Dict("CTBase/test@1.10" => failed(), "CTBase/test@1.12" => ok()) + kept, withheld = withhold([r], results) + @test isempty(kept) + @test only(withheld)["status"] == "withheld" + @test occursin("CTBase/test@1.10", only(withheld)["reason"]) + kept, withheld = withhold([r], Dict("CTBase/test@1.12" => ok())) + @test kept == [r] && isempty(withheld) + end + + @testset "classify_all on the fixture" begin + with_collected() do root + prunes, others = classify_all(root, fixture_results()) + got = Dict(entry_key(r) => r for r in vcat(prunes, others)) + # Alpha's Dep1: 0.6 on 1.10 (joint), 0.7 on 1.12 → 0.5 is obsolete + p = got[("Alpha", "Project.toml", "Dep1")] + @test p["status"] == "prune" && p["after"] == "0.6, 0.7" + # test/ entry: Tool 2 only + p = got[("Alpha", "test/Project.toml", "Tool")] + @test p["status"] == "prune" && p["after"] == "2" + # docs/ entry: Doc 1 never selected + b = got[("Alpha", "docs/Project.toml", "Doc")] + @test b["status"] == "blocked" && b["newest"] == "1" && b["applies_in"] == ["Alpha/docs"] + # Beta's Dep1: both lanes in use + @test got[("Beta", "Project.toml", "Dep1")]["status"] == "keep" + # Beta's weak dep: in no resolved environment + @test got[("Beta", "Project.toml", "Weak")]["status"] == "idle" + # single-specifier entries are not classified + @test !haskey(got, ("Beta", "Project.toml", "Tool")) + @test !haskey(got, ("Alpha", "Project.toml", "Beta")) + @test length(prunes) == 2 && length(others) == 3 + end + end + + @testset "classify_all: obsolete lane of a blocked entry, failed baseline" begin + with_collected() do root + results = fixture_results() + # Doc: 0.8 lane obsolete, 1 blocked + path = joinpath(root, "registered", "Alpha", "docs", "Project.toml") + write(path, replace(read(path, String), "Doc = \"0.9, 1\"" => "Doc = \"0.8, 0.9, 1\"")) + prunes, others = classify_all(root, results) + p = only(filter(r -> r["dep"] == "Doc", prunes)) + @test p["after"] == "0.9, 1" && p["removed"] == ["0.8"] + @test any(r -> r["dep"] == "Doc" && r["status"] == "blocked", others) + # a failed baseline withholds every prune + results["Beta/test@1.10"] = failed() + prunes, others = classify_all(root, results) + @test isempty(prunes) + @test count(r -> r["status"] == "withheld", others) == 3 + end + end + + @testset "load_results" begin + mktempdir() do dir + for (j, v) in (("1.10.11", "0.6.4"), ("1.12.6", "0.7.2")) + open(io -> TOML.print(io, Dict("julia" => j, "env" => Dict("joint" => ok("Dep1" => v)))), + joinpath(dir, "b-$j.toml"), "w") + end + r = load_results(joinpath.(dir, ["b-1.10.11.toml", "b-1.12.6.toml"])) + @test r == Dict("joint@1.10" => ok("Dep1" => "0.6.4"), "joint@1.12" => ok("Dep1" => "0.7.2")) + end + end +end diff --git a/scripts/compat-prune/test/test_collect.jl b/scripts/compat-prune/test/test_collect.jl new file mode 100644 index 0000000..32fec21 --- /dev/null +++ b/scripts/compat-prune/test/test_collect.jl @@ -0,0 +1,91 @@ +# collect.jl: files fetched from the registered tree and the default branch (fake `gh`). + +""" +A fake GitHub: `repos` maps "owner/repo" to (default branch, Dict(treeish => Dict(path => text))). +Returns the runner and the vector of the calls it received. +""" +function fake_github(repos) + calls = Vector{Vector{String}}() + blobs = Dict{String,String}() + function runner(args) + push!(calls, args) + @assert args[1] == "api" + path = args[2] + m = match(r"^repos/([^/]+/[^/]+)$", path) + m === nothing || return repos[m[1]][1] * "\n" # --jq .default_branch + m = match(r"^repos/([^/]+/[^/]+)/git/trees/([^?]+)\?recursive=1$", path) + if m !== nothing + files = repos[m[1]][2][m[2]] + lines = String[] + for (p, text) in sort(collect(files)) + sha = string(hash((m[1], m[2], p)); base=16) + blobs[sha] = text + push!(lines, "$p $sha") + end + return join(lines, "\n") * "\n" + end + m = match(r"^repos/[^/]+/[^/]+/git/blobs/(\w+)$", path) + m === nothing || return blobs[m[1]] + error("unexpected gh call: $args") + end + return runner, calls +end + +@testset "collect" begin + repos = Dict( + "org/Alpha.jl" => ("main", Dict( + "t-alpha" => Dict("Project.toml" => "name = \"Alpha\"\n", "test/Project.toml" => "[deps]\n", + "src/Alpha.jl" => "module Alpha end\n", "docs/make.jl" => ""), + "main" => Dict("Project.toml" => "name = \"Alpha\"\nversion = \"0.3.2\"\n"), + )), + "org/Beta.jl" => ("dev", Dict( + "t-beta" => Dict("Project.toml" => "name = \"Beta\"\n", "docs/Project.toml" => "[deps]\n"), + "dev" => Dict("Project.toml" => "name = \"Beta\"\n", "docs/Project.toml" => "[deps]\n"), + )), + ) + runner, calls = fake_github(repos) + mktempdir() do out + index = with_gh(runner) do + collect_files(out, Dict("Alpha" => ("u-a", "org/Alpha.jl"), "Beta" => ("u-b", "org/Beta.jl")), + Dict("Alpha" => "0.3.1", "Beta" => "1.2.0-beta"), + Dict("Alpha" => "t-alpha", "Beta" => "t-beta")) + end + @testset "index" begin + @test index["Alpha"]["version"] == "0.3.1" + @test index["Alpha"]["tree"] == "t-alpha" + @test index["Alpha"]["default_branch"] == "main" + @test index["Alpha"]["files"] == ["Project.toml", "test/Project.toml"] + @test index["Alpha"]["default_files"] == ["Project.toml"] + @test index["Beta"]["files"] == ["Project.toml", "docs/Project.toml"] + @test index["Beta"]["default_branch"] == "dev" + @test TOML.parsefile(joinpath(out, "index.toml"))["packages"] == index + end + @testset "files" begin + # the registered tree and the default branch are kept apart + @test read(joinpath(out, "registered", "Alpha", "Project.toml"), String) == "name = \"Alpha\"\n" + @test read(joinpath(out, "default", "Alpha", "Project.toml"), String) == + "name = \"Alpha\"\nversion = \"0.3.2\"\n" + @test isfile(joinpath(out, "registered", "Alpha", "test", "Project.toml")) + @test !isfile(joinpath(out, "default", "Alpha", "test", "Project.toml")) + @test isfile(joinpath(out, "default", "Beta", "docs", "Project.toml")) + end + @testset "calls" begin + # only the audited files are downloaded; the tree is read by its registered sha + fetched = [c[2] for c in calls if occursin("/git/blobs/", c[2])] + @test length(fetched) == 2 + 1 + 2 + 2 + @test any(c -> c[2] == "repos/org/Alpha.jl/git/trees/t-alpha?recursive=1", calls) + @test any(c -> c[2] == "repos/org/Beta.jl/git/trees/dev?recursive=1", calls) + # blobs are fetched raw + @test all(c -> c[3:4] == ["-H", "Accept: application/vnd.github.raw"], + [c for c in calls if occursin("/git/blobs/", c[2])]) + end + end + + @testset "fetch_files with no audited file" begin + runner, _ = fake_github(Dict("org/Empty.jl" => ("main", Dict("t" => Dict("README.md" => ""))))) + mktempdir() do out + @test with_gh(() -> fetch_files("org/Empty.jl", "t", out), runner) == String[] + @test isempty(readdir(out)) + end + end +end diff --git a/scripts/compat-prune/test/test_common.jl b/scripts/compat-prune/test/test_common.jl new file mode 100644 index 0000000..e5ec7b0 --- /dev/null +++ b/scripts/compat-prune/test/test_common.jl @@ -0,0 +1,104 @@ +# common.jl: specifier arithmetic, where an entry applies, simulation (fake resolver). + +const AUDITED = Dict("CTSolvers" => "0.5.8", "CTParser" => "0.9.5-beta", "CTBase" => "0.30.9") + +@testset "common" begin + @testset "parse_list" begin + @test parse_list("[\"CTBase\",\"CTParser\"]") == ["CTBase", "CTParser"] + @test parse_list("[ \"1.10\" , \"1.12\" ]") == ["1.10", "1.12"] + @test isempty(parse_list("[]")) + end + + @testset "render" begin + @test render(spec("0.6, 0.7, 0.8") ∩ spec("0.7, 0.8")) == "0.7 - 0.8" + @test render(spec("0.9, 0.11, 0.12") ∩ spec("0.9, 0.12")) == "0.9, 0.12" + @test render(spec(">= 0") ∩ spec("1")) == "1" + @test isempty(render(spec("0.7") ∩ spec("0.8"))) + # a rendered spec parses back to the same set + for s in ("0.6, 0.7, 0.8", "1.8, 2", "0.30.8", "5 - 6") + @test spec(render(spec(s))) == spec(s) + end + end + + @testset "entry_key" begin + p = prune("CTSolvers", "Project.toml", "CUDSS", "0.6, 0.7, 0.8", "0.7, 0.8") + @test entry_key(p) == ("CTSolvers", "Project.toml", "CUDSS") + end + + @testset "applies" begin + v = Dict("CTSolvers" => "0.5.8") + @test applies("CTSolvers", "Project.toml", "OptimalControl/test", v, AUDITED) + @test applies("CTSolvers", "Project.toml", "joint", v, AUDITED) + # another version of the owner carries other compat entries + @test !applies("CTSolvers", "Project.toml", "X/test", Dict("CTSolvers" => "0.4.0"), AUDITED) + @test !applies("CTSolvers", "Project.toml", "X/test", Dict{String,String}(), AUDITED) + # test/ and docs/ entries apply in their own environment only + @test applies("CTParser", "test/Project.toml", "CTParser/test", v, AUDITED) + @test applies("CTParser", "docs/Project.toml", "CTParser/docs", v, AUDITED) + @test !applies("CTParser", "test/Project.toml", "CTParser/docs", v, AUDITED) + @test !applies("CTParser", "docs/Project.toml", "OptimalControl/docs", v, AUDITED) + @test !applies("CTParser", "test/Project.toml", "joint", v, AUDITED) + end + + @testset "narrowings" begin + prunes = [prune("CTSolvers", "Project.toml", "CUDSS", "0.6, 0.7, 0.8", "0.7, 0.8"), + prune("CTSolvers", "docs/Project.toml", "CUDSS", "0.6, 0.7, 0.8", "0.8"), + prune("CTBase", "Project.toml", "JET", "0.9, 0.11, 0.12", "0.9, 0.12")] + v = Dict("CTSolvers" => "0.5.8", "CUDSS" => "0.8.1", "CTBase" => "0.30.9") + # JET absent: nothing to bind; the docs entry intersects with the root one + @test narrowings(prunes, "CTSolvers/docs", v, AUDITED) == Dict("CUDSS" => "0.8") + @test narrowings(prunes, "CTDirect/test", v, AUDITED) == Dict("CUDSS" => "0.7, 0.8") + # the owner is absent: the entry does not apply + @test isempty(narrowings(prunes, "CTDirect/test", Dict("CUDSS" => "0.8.1"), AUDITED)) + @test narrowings(prunes, "CTBase/test", Dict("CTBase" => "0.30.9", "JET" => "0.12.3"), AUDITED) == + Dict("JET" => "0.9, 0.12") + @test isempty(narrowings([], "joint", v, AUDITED)) + end + + @testset "simulate (fake resolver)" begin + env = Dict("deps" => Dict("Own" => "u-own", "A" => "u-a"), + "compat" => Dict("Own" => "=1.0.0", "A" => "0.6, 0.7, 0.8"), + "pins" => Dict("Own" => "1.0.0")) + uuids = Dict("A" => "u-a", "T" => "u-t") + calls = [] + resolver(versions) = (deps, compat) -> (push!(calls, (copy(deps), copy(compat))); + Dict("ok" => true, "versions" => Dict{String,Any}(versions), "trees" => Dict())) + # the narrowed compat is intersected with the environment's own + r = simulate("Own/test", env, Dict("A" => "0.7, 0.8"), uuids; resolver=resolver(Dict("Own" => "1.0.0", "A" => "0.8.1"))) + @test r == Dict("ok" => true, "versions" => Dict("Own" => "1.0.0", "A" => "0.8.1")) + @test last(calls)[2]["A"] == "0.7 - 0.8" + @test last(calls)[2]["Own"] == "=1.0.0" + # a transitive dependency is listed to carry its compat entry + simulate("Own/test", env, Dict("T" => "2"), uuids; resolver=resolver(Dict("Own" => "1.0.0", "T" => "2.1.0"))) + @test last(calls)[1]["T"] == "u-t" + @test last(calls)[2]["T"] == "2" + # the environment itself is not modified + @test !haskey(env["deps"], "T") + # an empty intersection fails without calling the resolver + n = length(calls) + r = simulate("Own/test", env, Dict("A" => "0.5"), uuids; resolver=resolver(Dict())) + @test !r["ok"] && occursin("empty compat intersection for A", r["error"]) + @test length(calls) == n + # a pinned package resolved at another version (e.g. a stable release next to the + # audited prerelease) is a failure + r = simulate("Own/test", env, Dict(), uuids; resolver=resolver(Dict("Own" => "1.0.1"))) + @test !r["ok"] && occursin("pinned versions not selected: Own 1.0.1 (audited 1.0.0)", r["error"]) + # a resolver failure is passed through + r = simulate("Own/test", env, Dict(), uuids; resolver=(d, c) -> failed("boom")) + @test r == failed("boom") + end + + @testset "load_collected" begin + mktempdir() do dir + cp(COLLECTED, joinpath(dir, "c")) + root = joinpath(dir, "c") + write(joinpath(root, "envs.toml"), "[env]\n") + envs, audited, uuids = load_collected(root) + @test audited == Dict("Alpha" => "0.3.1", "Beta" => "1.2.0-beta") + # every package named in an audited file, from [deps], [weakdeps] and [extras] + for n in ("Beta", "Dep1", "Tool", "Doc", "Weak", "Unused", "Test") + @test haskey(uuids, n) + end + end + end +end diff --git a/scripts/compat-prune/test/test_edit.jl b/scripts/compat-prune/test/test_edit.jl new file mode 100644 index 0000000..12dff79 --- /dev/null +++ b/scripts/compat-prune/test/test_edit.jl @@ -0,0 +1,153 @@ +# edit.jl: textual edit of one [compat] line; outcome of each prune against a checkout. + +const PROJECT = """ +name = "Pkg" +uuid = "aaaaaaaa-0000-4000-8000-000000000001" +version = "0.6.1" + +[deps] +CUDSS = "45b445bb-4962-46a0-9369-b4df9d0f772e" +JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" +JETLS = "ffffffff-0000-4000-8000-00000000000f" + +[compat] +CUDSS = "0.6, 0.7, 0.8" +JET = "0.9, 0.11, 0.12" +JETLS = "0.9, 0.11" +julia = "1.10" + +[extras] +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +""" + +"`text` with line `old` replaced by `new`, asserting it was there." +function swap(text, old, new) + @assert occursin(old, text) old + replace(text, old => new) +end + +@testset "edit" begin + @testset "edits the value and nothing else" begin + out, status, detail = edit_compat(PROJECT, "JET", "0.9, 0.11, 0.12", "0.9, 0.12") + @test status == "edited" && detail == "" + @test out == swap(PROJECT, "JET = \"0.9, 0.11, 0.12\"", "JET = \"0.9, 0.12\"") + # a key that only starts with the same name is not touched + @test occursin("JETLS = \"0.9, 0.11\"", out) + end + + @testset "layout is preserved" begin + cases = [ + "JET = \"0.9, 0.11, 0.12\" # kept for Julia 1.10" => "JET = \"0.9, 0.12\" # kept for Julia 1.10", + "JET = \"0.9, 0.11, 0.12\"" => "JET = \"0.9, 0.12\"", + " JET = \"0.9, 0.11, 0.12\"" => " JET = \"0.9, 0.12\"", + "JET = '0.9, 0.11, 0.12'" => "JET = '0.9, 0.12'", + "\"JET\" = \"0.9, 0.11, 0.12\"" => "\"JET\" = \"0.9, 0.12\"", + ] + for (old, new) in cases + text = swap(PROJECT, "JET = \"0.9, 0.11, 0.12\"", old) + out, status, _ = edit_compat(text, "JET", "0.9, 0.11, 0.12", "0.9, 0.12") + @test status == "edited" + @test out == swap(text, old, new) + end + end + + @testset "CRLF line endings are kept" begin + text = replace(PROJECT, "\n" => "\r\n") + out, status, _ = edit_compat(text, "CUDSS", "0.6, 0.7, 0.8", "0.7, 0.8") + @test status == "edited" + @test out == replace(swap(PROJECT, "CUDSS = \"0.6, 0.7, 0.8\"", "CUDSS = \"0.7, 0.8\""), "\n" => "\r\n") + end + + @testset "spacing inside the value does not matter" begin + text = swap(PROJECT, "CUDSS = \"0.6, 0.7, 0.8\"", "CUDSS = \"0.6,0.7, 0.8\"") + out, status, _ = edit_compat(text, "CUDSS", "0.6, 0.7, 0.8", "0.7, 0.8") + @test status == "edited" && occursin("CUDSS = \"0.7, 0.8\"", out) + end + + @testset "only the [compat] section is read" begin + # same key in [deps] (a uuid) and in an [extras]-like section after [compat] + text = PROJECT * "\n[sources]\nJET = {path = \"../JET\"}\n" + out, status, _ = edit_compat(text, "JET", "0.9, 0.11, 0.12", "0.9, 0.12") + @test status == "edited" + @test occursin("JET = \"c3a54625-cd67-489e-a8e7-0a5a0ff4e31b\"", out) + @test occursin("JET = {path = \"../JET\"}", out) + # a [compat] entry declared only after a nested table header is not mistaken + text = swap(PROJECT, "[compat]", "[compat.extra]") + @test edit_compat(text, "JET", "0.9, 0.11, 0.12", "0.9, 0.12")[2] == "drift" + end + + @testset "already, drift" begin + text = swap(PROJECT, "JET = \"0.9, 0.11, 0.12\"", "JET = \"0.9, 0.12\"") + out, status, detail = edit_compat(text, "JET", "0.9, 0.11, 0.12", "0.9, 0.12") + @test status == "already" && out == text && occursin("0.9, 0.12", detail) + # the branch moved the entry elsewhere + text = swap(PROJECT, "JET = \"0.9, 0.11, 0.12\"", "JET = \"0.12\"") + out, status, detail = edit_compat(text, "JET", "0.9, 0.11, 0.12", "0.9, 0.12") + @test status == "drift" && out == text + @test detail == "`0.12` on the default branch, `0.9, 0.11, 0.12` audited" + # the entry is gone + text = swap(PROJECT, "JET = \"0.9, 0.11, 0.12\"\n", "") + out, status, detail = edit_compat(text, "JET", "0.9, 0.11, 0.12", "0.9, 0.12") + @test status == "drift" && out == text && occursin("no `JET` entry", detail) + # no [compat] at all + @test edit_compat("name = \"X\"\n", "JET", "0.9", "0.12")[2] == "drift" + end + + @testset "check_edit" begin + edited = swap(PROJECT, "JET = \"0.9, 0.11, 0.12\"", "JET = \"0.9, 0.12\"") + @test check_edit(PROJECT, edited, "JET", "0.9, 0.12") === nothing + @test_throws ErrorException check_edit(PROJECT, swap(edited, "0.6.1", "0.6.0"), "JET", "0.9, 0.12") + @test_throws ErrorException check_edit(PROJECT, edited, "JET", "0.12") + end + + @testset "plan_edits" begin + files = Dict(("Pkg", "Project.toml") => PROJECT, + ("Pkg", "docs/Project.toml") => "[compat]\nJET = \"0.12\"\n") + read_file(owner, file) = get(files, (owner, file), nothing) + audit = Dict("prune" => [ + prune("Pkg", "Project.toml", "JET", "0.9, 0.11, 0.12", "0.9, 0.12"), + prune("Pkg", "Project.toml", "CUDSS", "0.6, 0.7, 0.8", "0.7, 0.8"), # same file, accumulated + prune("Pkg", "docs/Project.toml", "JET", "0.11, 0.12", "0.12"), # already + prune("Pkg", "test/Project.toml", "JET", "0.11, 0.12", "0.12"), # no such file + prune("Pkg", "Project.toml", "JETLS", "0.8, 0.9, 0.11", "0.11"), # drift: the branch has "0.9, 0.11" + ]) + records, texts = plan_edits(audit, read_file) + status = Dict(entry_key(r) => r["status"] for r in records) + @test status[("Pkg", "Project.toml", "JET")] == "edited" + @test status[("Pkg", "Project.toml", "CUDSS")] == "edited" + @test status[("Pkg", "docs/Project.toml", "JET")] == "already" + @test status[("Pkg", "test/Project.toml", "JET")] == "drift" + @test status[("Pkg", "Project.toml", "JETLS")] == "drift" + @test collect(keys(texts)) == [("Pkg", "Project.toml")] + out = texts[("Pkg", "Project.toml")] + @test out == swap(swap(PROJECT, "JET = \"0.9, 0.11, 0.12\"", "JET = \"0.9, 0.12\""), + "CUDSS = \"0.6, 0.7, 0.8\"", "CUDSS = \"0.7, 0.8\"") + # the version line is never touched (regression: copying a collected file reverted it) + @test occursin("version = \"0.6.1\"", out) + end + + @testset "against the fixture's default branches" begin + audit = Dict("prune" => [ + prune("Alpha", "Project.toml", "Dep1", "0.5, 0.6, 0.7", "0.6, 0.7"), + prune("Beta", "Project.toml", "Dep1", "0.6, 0.7", "0.7"), + prune("Alpha", "docs/Project.toml", "Doc", "0.9, 1", "1"), + ]) + records, texts = plan_edits(audit, read_checkout(joinpath(COLLECTED, "default"))) + status = Dict(entry_key(r) => r["status"] for r in records) + # the default branch bumped the version and commented the line: still edited, both kept + @test status[("Alpha", "Project.toml", "Dep1")] == "edited" + out = texts[("Alpha", "Project.toml")] + @test occursin("version = \"0.3.2\"", out) + @test occursin("Dep1 = \"0.6, 0.7\" # lanes kept by CompatHelper", out) + # Beta's default branch already dropped 0.6 + @test status[("Beta", "Project.toml", "Dep1")] == "already" + @test status[("Alpha", "docs/Project.toml", "Doc")] == "edited" + end + + @testset "read_checkout" begin + r = read_checkout(joinpath(COLLECTED, "default")) + @test startswith(r("Alpha", "Project.toml"), "name = \"Alpha\"") + @test r("Alpha", "test/Project.toml") === nothing + @test r("Nope", "Project.toml") === nothing + end +end diff --git a/scripts/compat-prune/test/test_envs.jl b/scripts/compat-prune/test/test_envs.jl new file mode 100644 index 0000000..4e5e65c --- /dev/null +++ b/scripts/compat-prune/test/test_envs.jl @@ -0,0 +1,69 @@ +# envs.jl: environments built from the synthetic collected fixture. + +@testset "envs" begin + envs = build_envs(COLLECTED) + + @testset "names" begin + # Beta has no docs/Project.toml + @test sort(collect(keys(envs))) == ["Alpha/docs", "Alpha/test", "Beta/test", "joint"] + @test envs["Alpha/test"]["owner"] == "Alpha" && envs["Alpha/test"]["kind"] == "test" + @test envs["joint"]["owner"] == "" && envs["joint"]["kind"] == "joint" + end + + @testset "joint" begin + e = envs["joint"] + @test sort(collect(keys(e["deps"]))) == ["Alpha", "Beta"] + # every package is pinned; a prerelease is pinned without its suffix + @test e["compat"] == Dict("Alpha" => "=0.3.1", "Beta" => "=1.2.0") + @test e["pins"] == Dict("Alpha" => "0.3.1", "Beta" => "1.2.0-beta") + end + + @testset "test env from test/Project.toml" begin + e = envs["Alpha/test"] + @test sort(collect(keys(e["deps"]))) == ["Alpha", "Test", "Tool"] + # compat of a dep the file does not list is dropped; only the owner is pinned + @test e["compat"] == Dict("Tool" => "1, 2", "Alpha" => "=0.3.1") + @test e["pins"] == Dict("Alpha" => "0.3.1") + end + + @testset "test env from [targets]" begin + e = envs["Beta/test"] + # [targets] test, not every [extras]: Unused is left out, the weak dep is in + @test sort(collect(keys(e["deps"]))) == ["Beta", "Test", "Tool", "Weak"] + @test e["deps"]["Weak"] == "44444444-0000-4000-8000-000000000004" + # the package's own compat, restricted to those deps, plus julia + @test e["compat"] == Dict("Tool" => "2", "Weak" => "0.1, 0.2", "julia" => "1.10", "Beta" => "=1.2.0") + @test e["pins"] == Dict("Beta" => "1.2.0-beta") + end + + @testset "docs env" begin + e = envs["Alpha/docs"] + # the package itself is added: docs/make.jl develops it + @test sort(collect(keys(e["deps"]))) == ["Alpha", "Doc"] + @test e["compat"] == Dict("Doc" => "0.9, 1", "Alpha" => "=0.3.1") + end + + @testset "test env without [targets]" begin + mktempdir() do dir + write(joinpath(dir, "Project.toml"), """ + name = "Gamma" + [extras] + Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" + [compat] + Aqua = "0.8" + """) + deps, compat = test_env("Gamma", "u-g", dir) + @test sort(collect(keys(deps))) == ["Aqua", "Gamma", "Test"] + @test compat == Dict("Aqua" => "0.8") + end + end + + @testset "envs.toml round trip" begin + mktempdir() do dir + path = joinpath(dir, "envs.toml") + open(io -> TOML.print(io, Dict("env" => envs)), path, "w") + @test TOML.parsefile(path)["env"] == envs + end + end +end diff --git a/scripts/compat-prune/test/test_integration.jl b/scripts/compat-prune/test/test_integration.jl new file mode 100644 index 0000000..eacd2da --- /dev/null +++ b/scripts/compat-prune/test/test_integration.jl @@ -0,0 +1,206 @@ +# Integration: real Pkg resolutions against the local General registry, then the +# scripts run as commands on a one-package ecosystem: Example 0.5.5, whose docs declare +# `OrderedCollections = "1, 2"` (only 2 is selectable with nothing else in the way). +# +# Run with COMPAT_PRUNE_INTEGRATION=1. Needs General installed (any recent copy). + +const EXAMPLE = "7876af07-990d-54b4-ab0e-23690620f79a" +const ORDERED = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" +const JULIA = Base.julia_cmd() + +"Write a collected directory for Example 0.5.5 into `root`; `docs_compat` is the docs compat of OrderedCollections." +function example_collected(root; docs_compat="1, 2") + reg = joinpath(root, "registered", "Example") + mkpath(joinpath(reg, "docs")) + write(joinpath(reg, "Project.toml"), """ + name = "Example" + uuid = "$EXAMPLE" + version = "0.5.5" + + [compat] + julia = "1" + + [extras] + Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + + [targets] + test = ["Test"] + """) + write(joinpath(reg, "docs", "Project.toml"), """ + [deps] + OrderedCollections = "$ORDERED" + + [compat] + OrderedCollections = "$docs_compat" + """) + cp(joinpath(root, "registered"), joinpath(root, "default")) + open(joinpath(root, "index.toml"), "w") do io + TOML.print(io, Dict("julia" => string(VERSION), "packages" => Dict("Example" => Dict( + "uuid" => EXAMPLE, "version" => "0.5.5", "repo" => "JuliaLang/Example.jl", "tree" => "0", + "default_branch" => "master", "files" => ["Project.toml", "docs/Project.toml"], + "default_files" => ["Project.toml", "docs/Project.toml"])))) + end + return root +end + +"Run a script of compat-prune with `args` on the running Julia; return its output." +run_script(script, args...) = read(`$JULIA --startup-file=no $(joinpath(SCRIPTS, script)) $args`, String) + +@testset "integration" begin + @testset "resolve_env" begin + r = resolve_env(Dict("Example" => EXAMPLE), Dict("Example" => "0.5")) + @test r["ok"] + @test startswith(r["versions"]["Example"], "0.5.") + # the registered tree is known, stdlibs have none + @test length(r["trees"]["Example"]) == 40 + r = resolve_env(Dict("Example" => EXAMPLE, "LinearAlgebra" => "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"), Dict()) + @test haskey(r["versions"], "LinearAlgebra") && !haskey(r["trees"], "LinearAlgebra") + # unsatisfiable: a result, not an exception + r = resolve_env(Dict("Example" => EXAMPLE), Dict("Example" => "99")) + @test !r["ok"] && occursin("Example", r["error"]) + # anything else is a bug of the caller: thrown + @test_throws Exception resolve_env(Dict("Example" => EXAMPLE), Dict("Example" => "not a version")) + end + + @testset "registry_lookup" begin + @test registry_lookup("Example") == (EXAMPLE, "JuliaLang/Example.jl") + @test_throws ErrorException registry_lookup("NoSuchPackageForCompatPrune") + end + + @testset "collect.jl as a command (fake gh on PATH)" begin + mktempdir() do dir + # canned answers of `gh api …`, one file per path + answers = joinpath(dir, "answers") + mkpath(answers) + put(path, text) = write(joinpath(answers, replace(path, r"[/?=]" => "_")), text) + project = "name = \"Example\"\nuuid = \"$EXAMPLE\"\nversion = \"0.5.5\"\n" + tree = resolve_env(Dict("Example" => EXAMPLE), Dict("Example" => "=0.5.5"))["trees"]["Example"] + put("repos/JuliaLang/Example.jl", "master\n") + put("repos/JuliaLang/Example.jl/git/trees/$tree?recursive=1", "Project.toml b1\nsrc/Example.jl b2\n") + put("repos/JuliaLang/Example.jl/git/trees/master?recursive=1", "Project.toml b3\n") + put("repos/JuliaLang/Example.jl/git/blobs/b1", project) + put("repos/JuliaLang/Example.jl/git/blobs/b3", project * "# moved on\n") + bin = joinpath(dir, "bin") + mkpath(bin) + write(joinpath(bin, "gh"), """ + #!/bin/sh + exec cat "$answers/\$(printf '%s' "\$2" | tr '/?=' '___')" + """) + chmod(joinpath(bin, "gh"), 0o755) + out = joinpath(dir, "audit") + withenv("PATH" => bin * ":" * ENV["PATH"]) do + run_script("collect.jl", "[\"Example\"]", out) + end + index = TOML.parsefile(joinpath(out, "index.toml"))["packages"]["Example"] + @test index["version"] == "0.5.5" && index["tree"] == tree + @test index["repo"] == "JuliaLang/Example.jl" && index["default_branch"] == "master" + @test read(joinpath(out, "registered", "Example", "Project.toml"), String) == project + @test endswith(read(joinpath(out, "default", "Example", "Project.toml"), String), "# moved on\n") + end + end + + @testset "simulate" begin + env = Dict("deps" => Dict("Example" => EXAMPLE), "compat" => Dict("Example" => "=0.5.5"), + "pins" => Dict("Example" => "0.5.5")) + base = simulate("x", env, Dict(), Dict()) + @test base["ok"] && base["versions"]["Example"] == "0.5.5" + # the pin is checked: =0.5.4 admitted, but the audited version says 0.5.5 + env4 = Dict("deps" => env["deps"], "compat" => Dict("Example" => "=0.5.4"), "pins" => env["pins"]) + r = simulate("x", env4, Dict(), Dict()) + @test !r["ok"] && occursin("pinned versions not selected: Example 0.5.4 (audited 0.5.5)", r["error"]) + # a narrowing that moves a transitive dependency departs from the baseline + free = Dict("deps" => Dict("Example" => EXAMPLE, "OrderedCollections" => ORDERED), "compat" => Dict(), "pins" => Dict()) + b = simulate("x", free, Dict(), Dict()) + @test b["versions"]["OrderedCollections"] == "2.0.3" || VersionNumber(b["versions"]["OrderedCollections"]) ≥ v"2" + r = simulate("x", free, Dict("OrderedCollections" => "1"), Dict("OrderedCollections" => ORDERED)) + @test startswith(departure(r, b["versions"]), "manifest differs: OrderedCollections") + @test departure(simulate("x", free, Dict("OrderedCollections" => "2"), Dict("OrderedCollections" => ORDERED)), b["versions"]) === nothing + end + + @testset "scripts end to end" begin + mktempdir() do dir + root = example_collected(joinpath(dir, "audit")) + out = run_script("envs.jl", root) + @test occursin("3 environments: Example/docs, Example/test, joint", out) + j = join(split(string(VERSION), ".")[1:2], ".") + base = joinpath(root, "baseline-$j.toml") + run_script("resolve.jl", root, base) + b = TOML.parsefile(base) + @test all(r -> r["ok"], values(b["env"])) + @test VersionNumber(b["env"]["Example/docs"]["versions"]["OrderedCollections"]) ≥ v"2" + + classified = joinpath(root, "classified.toml") + out = run_script("classify.jl", root, classified, base) + @test occursin("prune → 2", out) + c = TOML.parsefile(classified) + p = only(c["prune"]) + @test entry_key(p) == ("Example", "docs/Project.toml", "OrderedCollections") && p["after"] == "2" + + # validation accepts it + empty_rejected = joinpath(root, "rejected.toml") + write(empty_rejected, "") + validated = joinpath(root, "validated.toml") + run_script("validate.jl", root, classified, base, validated, empty_rejected, "--blockers") + @test isempty(TOML.parsefile(validated)["rejected"]) + + # resolve.jl with the prunes: same manifests + pruned = joinpath(root, "pruned.toml") + run_script("resolve.jl", root, pruned, classified, base) + @test TOML.parsefile(pruned)["env"] == b["env"] + + # a prune that moves the manifest is rejected + c["prune"][1]["after"] = "1" + bad = joinpath(root, "classified-bad.toml") + open(io -> TOML.print(io, c), bad, "w") + run_script("validate.jl", root, bad, base, validated, empty_rejected) + r = only(TOML.parsefile(validated)["rejected"]) + @test r["where"] == "Example/docs@$j" + @test occursin("OrderedCollections", r["reason"]) + + # edit a checkout, then report + open(joinpath(root, "audit.toml"), "w") do io + TOML.print(io, Dict("date" => "2026-10-15 00:00 UTC", "julia" => [j], "prune" => TOML.parsefile(classified)["prune"], + "rejected" => [], "blocked" => [], "other" => [], + "packages" => TOML.parsefile(joinpath(root, "index.toml"))["packages"])) + end + checkouts = joinpath(dir, "checkouts") + cp(joinpath(root, "default"), checkouts) + out = run_script("edit.jl", root, checkouts) + @test occursin(r"edited\s+1", out) + @test occursin("OrderedCollections = \"2\"", read(joinpath(checkouts, "Example", "docs", "Project.toml"), String)) + @test read(joinpath(checkouts, "Example", "Project.toml"), String) == + read(joinpath(root, "default", "Example", "Project.toml"), String) + # a second run finds it done + out = run_script("edit.jl", root, checkouts) + @test occursin(r"already\s+1", out) + run_script("report.jl", root) + report = read(joinpath(root, "report.md"), String) + @test startswith(report, MARKER) + @test occursin("| merged, awaiting release | 1 |", report) + end + end + + @testset "a blocked entry is probed" begin + mktempdir() do dir + # newest lane 3 does not exist: never selected, the probe cannot resolve it + root = example_collected(joinpath(dir, "audit"); docs_compat="1, 2, 3") + run_script("envs.jl", root) + j = join(split(string(VERSION), ".")[1:2], ".") + base = joinpath(root, "baseline-$j.toml") + run_script("resolve.jl", root, base) + classified = joinpath(root, "classified.toml") + run_script("classify.jl", root, classified, base) + c = TOML.parsefile(classified) + b = only(filter(r -> r["status"] == "blocked", c["other"])) + @test b["newest"] == "3" && b["prunable"] == ["1"] + # its obsolete lane is still a prune + @test only(c["prune"])["after"] == "2, 3" + rejected = joinpath(root, "rejected.toml"); write(rejected, "") + validated = joinpath(root, "validated.toml") + run_script("validate.jl", root, classified, base, validated, rejected, "--blockers") + probe = only(TOML.parsefile(validated)["blocked"]) + @test probe["where"] == "Example/docs@$j" + @test occursin("OrderedCollections", probe["detail"]) + end + end +end diff --git a/scripts/compat-prune/test/test_regression.jl b/scripts/compat-prune/test/test_regression.jl new file mode 100644 index 0000000..6547026 --- /dev/null +++ b/scripts/compat-prune/test/test_regression.jl @@ -0,0 +1,66 @@ +# Non-regression on a real audit (2026-10-15): the collected files of the eight CT packages, +# their default branches, and the baselines of Julia 1.10, 1.12, 1.13 (trimmed to the audited +# packages and the dependencies named in a compat entry, which is all classification reads). +# Environments, classification and edits must stay identical; the golden files are the +# reviewed outputs of that audit. + +const SNAPSHOT = joinpath(FIXTURES, "snapshot-2026-10-15") + +"Lines `owner | file | dep | status | detail`, sorted, for a golden file." +summary_lines(records, detail) = + join(sort(["$(r["owner"]) | $(r["file"]) | $(r["dep"]) | $(r["status"]) | $(detail(r))" for r in records]), "\n") * "\n" + +@testset "regression 2026-10-15" begin + mktempdir() do dir + root = joinpath(dir, "snapshot") + cp(SNAPSHOT, root) + envs = build_envs(root) + open(io -> TOML.print(io, Dict("env" => envs)), joinpath(root, "envs.toml"), "w") + + @testset "environments" begin + @test length(envs) == 17 + text = join(sort(["$name: deps=$(join(sort(collect(keys(e["deps"]))), ",")) " * + "pins=$(join(sort(["$k=$v" for (k, v) in e["pins"]]), ","))" for (name, e) in envs]), "\n") * "\n" + @test golden("envs-2026-10-15.txt", text) + # the docs env of every package contains the package itself + @test all(e -> haskey(e["deps"], e["owner"]), [e for e in values(envs) if e["kind"] == "docs"]) + end + + results = load_results([joinpath(root, "baseline-$j.toml") for j in ("1.10", "1.12", "1.13")]) + prunes, others = classify_all(root, results) + + @testset "classification" begin + detail(r) = r["status"] == "prune" ? r["after"] : r["status"] == "blocked" ? "newest " * r["newest"] : "" + @test golden("classified-2026-10-15.txt", summary_lines(vcat(prunes, others), detail)) + got = Dict(entry_key(r) => r for r in vcat(prunes, others)) + @test length(prunes) == 42 + # CTSolvers keeps CUDSS 0.7: OptimalControl's tests and docs select it + @test got[("CTSolvers", "Project.toml", "CUDSS")]["after"] == "0.7, 0.8" + @test got[("CTSolvers", "docs/Project.toml", "CUDSS")]["after"] == "0.8" + # JET 0.9 is what Julia 1.10 selects + @test got[("CTBase", "Project.toml", "JET")]["after"] == "0.9, 0.12" + # CTParser's own CTBase 0.29 lane is used by its test env (CTModels 0.18) + @test got[("CTParser", "Project.toml", "CTBase")]["status"] == "keep" + @test got[("CTParser", "test/Project.toml", "CTBase")]["status"] == "blocked" + @test got[("CTBase", "Project.toml", "DocStringExtensions")]["status"] == "blocked" + end + + @testset "edits against the default branches" begin + records, texts = plan_edits(Dict("prune" => prunes), read_checkout(joinpath(root, "default"))) + @test golden("edits-2026-10-15.txt", summary_lines(records, r -> r["detail"])) + @test count(r -> r["status"] == "edited", records) == 35 + drift = [r for r in records if r["status"] == "drift"] + # CTParser's default branch is behind its registered version (CTParser#326) + @test length(drift) == 7 && all(r -> r["owner"] == "CTParser", drift) + # every edited file differs from its default branch by compat lines only + for ((owner, file), text) in texts + original = read(joinpath(root, "default", owner, file), String) + a, b = TOML.parse(original), TOML.parse(text) + delete!(a, "compat"); delete!(b, "compat") + @test a == b + changed = [l for l in split(text, '\n') if !(l in split(original, '\n'))] + @test all(l -> occursin(r"^\w+ = \"[^\"]+\"$", l), changed) + end + end + end +end diff --git a/scripts/compat-prune/test/test_report.jl b/scripts/compat-prune/test/test_report.jl new file mode 100644 index 0000000..a28b5c7 --- /dev/null +++ b/scripts/compat-prune/test/test_report.jl @@ -0,0 +1,91 @@ +# report.jl: Markdown body of the tracking issue (golden file + properties). + +"An audit and its edits covering every section of the report." +function report_fixture() + index = TOML.parsefile(joinpath(COLLECTED, "index.toml"))["packages"] + ev(pairs...) = Dict{String,Any}(spec => list for (spec, list) in pairs) + edited = [ + merge(prune("Alpha", "Project.toml", "Dep1", "0.5, 0.6, 0.7", "0.6, 0.7"), Dict( + "evidence" => ev("0.6" => ["joint@1.10=0.6.4"], "0.7" => ["joint@1.12=0.7.2", "Alpha/test@1.12=0.7.2"]), + "status" => "edited", "detail" => "")), + merge(prune("Alpha", "test/Project.toml", "Tool", "1, 2", "2"), Dict( + "evidence" => ev("2" => ["Alpha/test@1.10=2.0.0", "Alpha/test@1.12=2.0.0"]), + "status" => "edited", "detail" => "")), + merge(prune("Beta", "Project.toml", "Dep1", "0.6, 0.7", "0.7"), Dict( + "evidence" => ev("0.7" => ["Beta/test@1.12=0.7.2"]), "status" => "edited", "detail" => "")), + ] + already = [merge(prune("Beta", "Project.toml", "Tool", "1, 2", "2"), Dict("status" => "already", "detail" => "already `2`"))] + drift = [merge(prune("Alpha", "docs/Project.toml", "Doc", "0.8, 0.9", "0.9"), + Dict("status" => "drift", "detail" => "`0.9, 1` on the default branch, `0.8, 0.9` audited"))] + audit = Dict{String,Any}( + "date" => "2026-10-15 19:48 UTC", + "julia" => ["1.10", "1.12", "1.13"], + "packages" => index, + "prune" => vcat(edited, already, drift), + "rejected" => [Dict("owner" => "Beta", "file" => "Project.toml", "dep" => "Weak", "where" => "Beta/test@1.10", + "reason" => "manifest differs: A 1.0.0 → 1.1.0 | odd\nsecond line")], + "blocked" => [ + Dict("owner" => "Alpha", "file" => "docs/Project.toml", "dep" => "Doc", "newest" => "1", + "where" => "Alpha/docs@1.13", "blockers" => ["Documenter"], "detail" => ""), + Dict("owner" => "Alpha", "file" => "docs/Project.toml", "dep" => "Doc", "newest" => "1", + "where" => "joint@1.13", "blockers" => ["Beta", "Documenter"], "detail" => ""), + Dict("owner" => "Beta", "file" => "Project.toml", "dep" => "Weak", "newest" => "0.2", + "where" => "Beta/test@1.13", "blockers" => String[], "detail" => "resolves, but…"), + ], + "other" => [Dict("owner" => "Alpha", "file" => "Project.toml", "dep" => "Dep2", "before" => "1, 2", + "removed" => ["1"], "status" => "withheld", + "reason" => "baseline failed in joint@1.10: lanes used there are unknown")], + ) + return audit, vcat(edited, already, drift) +end + +@testset "report" begin + audit, edits = report_fixture() + prs = Dict("Alpha" => "https://github.com/example-org/Alpha.jl/pull/12") + body = render_report(audit, edits; prs, run_url="https://github.com/example-org/Root.jl/actions/runs/1") + + @testset "golden" begin + @test golden("report.md", body) + end + + @testset "marker and sections" begin + @test startswith(body, MARKER * "\n") + for h in ("## Summary", "## To prune", "### Alpha — https://github.com/example-org/Alpha.jl/pull/12", + "### Beta", "## Merged, awaiting release", "## Drift", "## Rejected", "## Withheld", + "## Blocked", "## Audited versions") + @test occursin("\n" * h * "\n", body) + end + # the reason of a rejection stays on one table row + @test occursin("A 1.0.0 → 1.1.0 \\| odd second line", body) + # blockers aggregated over environments; none found → resolver preference + @test occursin("| Alpha `docs/Project.toml` `Doc` | `1` | Beta, Documenter |", body) + @test occursin("| Beta `Project.toml` `Weak` | `0.2` | resolver preference |", body) + @test occursin("| to prune (open PRs) | 3 |", body) + @test occursin("| blocked (information) | 2 |", body) + end + + @testset "deterministic" begin + shuffled = deepcopy(audit) + reverse!(shuffled["blocked"]) + @test render_report(shuffled, reverse(edits); prs, run_url="https://github.com/example-org/Root.jl/actions/runs/1") == body + end + + @testset "empty sections are left out" begin + quiet = Dict{String,Any}("date" => "2026-11-01 03:00 UTC", "julia" => ["1.10"], "packages" => audit["packages"], + "prune" => [], "rejected" => [], "blocked" => [], "other" => []) + b = render_report(quiet, []) + @test occursin("| to prune (open PRs) | 0 |", b) + for h in ("## To prune", "## Merged", "## Drift", "## Rejected", "## Withheld", "## Blocked") + @test !occursin(h, b) + end + @test !occursin("[run]", b) + @test occursin("## Audited versions", b) + end + + @testset "helpers" begin + @test table(["a", "b"], [["1", "2"]]) == "| a | b |\n| --- | --- |\n| 1 | 2 |" + @test selected(Dict("0.9" => ["CTBase/test@1.10=0.9.18"], "0.12" => ["CTBase/test@1.12=0.12.3", "joint@1.13=0.12.3"])) == + "0.12.3 on 1.12, 1.13; 0.9.18 on 1.10" + @test selected(Dict{String,Any}()) == "" + end +end diff --git a/scripts/compat-prune/test/test_validate.jl b/scripts/compat-prune/test/test_validate.jl new file mode 100644 index 0000000..819fd24 --- /dev/null +++ b/scripts/compat-prune/test/test_validate.jl @@ -0,0 +1,107 @@ +# validate.jl: departures, rejection of failing prunes (fake simulator), blockers. + +""" +A fake simulator: the baseline `versions` of each environment, departed from (`Tool` moves to +9.9.9) when `breaks(name, narrowings)` holds. Records every call. +""" +function fake_sim(baseline; breaks=(name, n) -> false, error_for=Dict()) + calls = [] + sim = function (name, env, n, uuids) + push!(calls, (name, copy(n))) + haskey(error_for, name) && return failed(error_for[name]) + versions = copy(baseline[name]["versions"]) + breaks(name, n) && (versions["Tool"] = "9.9.9") + return Dict("ok" => true, "versions" => versions) + end + return sim, calls +end + +@testset "validate" begin + @testset "departure" begin + base = Dict("A" => "1.0.0", "B" => "2.0.0") + @test departure(ok("A" => "1.0.0", "B" => "2.0.0"), base) === nothing + @test departure(ok("A" => "1.0.0", "B" => "2.1.0"), base) == "manifest differs: B 2.0.0 → 2.1.0" + @test departure(ok("A" => "1.0.0"), base) == "manifest differs: B 2.0.0 → —" + @test departure(ok("A" => "1.0.0", "B" => "2.0.0", "C" => "3.0.0"), base) == "manifest differs: C — → 3.0.0" + @test departure(failed("empty compat intersection for CUDSS"), base) == "empty compat intersection for CUDSS" + many = Dict("P$i" => "1.0.$i" for i in 1:9) + @test endswith(departure(ok(), many), ", …") + end + + @testset "blockers" begin + @test blockers(CONFLICT_ON_DEP, "DocStringExtensions") == ["SciMLBase"] + @test blockers(CONFLICT_ON_OTHER, "DocStringExtensions") == ["DocumenterVitepress"] + @test isempty(blockers("empty compat intersection for CUDSS", "CUDSS")) + end + + with_collected() do root + envs, audited, uuids = load_collected(root) + baseline = Dict(name => r for (k, r) in fixture_results() for name in (first(split(k, "@")),) if endswith(k, "@1.12")) + dep1 = prune("Alpha", "Project.toml", "Dep1", "0.5, 0.6, 0.7", "0.6, 0.7") + tool = prune("Alpha", "test/Project.toml", "Tool", "1, 2", "2") + + @testset "nothing departs" begin + sim, calls = fake_sim(baseline) + @test isempty(validate_prunes(envs, audited, uuids, [dep1, tool], baseline, "1.12"; sim)) + # one combined simulation per environment where a prune applies: Dep1 wherever + # Alpha is (not Beta/test), Tool in Alpha/test only + @test calls == [("Alpha/docs", Dict("Dep1" => "0.6, 0.7")), + ("Alpha/test", Dict("Dep1" => "0.6, 0.7", "Tool" => "2")), + ("joint", Dict("Dep1" => "0.6, 0.7"))] + end + + @testset "a culprit fails alone" begin + sim, calls = fake_sim(baseline; breaks=(name, n) -> get(n, "Tool", "") == "2") + rejected = validate_prunes(envs, audited, uuids, [dep1, tool], baseline, "1.12"; sim) + @test length(rejected) == 1 + r = only(rejected) + @test entry_key(r) == entry_key(tool) + @test r["where"] == "Alpha/test@1.12" + @test r["reason"] == "manifest differs: Tool 2.0.0 → 9.9.9" + end + + @testset "prunes that fail only together are all rejected" begin + sim, _ = fake_sim(baseline; breaks=(name, n) -> haskey(n, "Tool") && haskey(n, "Dep1")) + rejected = validate_prunes(envs, audited, uuids, [dep1, tool], baseline, "1.12"; sim) + @test sort(entry_key.(rejected)) == sort(entry_key.([dep1, tool])) + @test all(r -> startswith(r["reason"], "fails only combined with other prunes"), rejected) + end + + @testset "a rejected prune is not simulated again in later environments" begin + # Dep1 breaks everywhere: rejected in the first environment (sorted: Alpha/docs), + # then absent from the simulations of the next ones. + sim, calls = fake_sim(baseline; breaks=(name, n) -> haskey(n, "Dep1")) + rejected = validate_prunes(envs, audited, uuids, [dep1], baseline, "1.12"; sim) + @test only(rejected)["where"] == "Alpha/docs@1.12" + # combined + alone in Alpha/docs, then nothing left to simulate + @test length(calls) == 2 + end + + @testset "environments that failed at baseline are skipped" begin + b = copy(baseline) + b["joint"] = failed() + sim, calls = fake_sim(baseline) + validate_prunes(envs, audited, uuids, [dep1], b, "1.12"; sim) + @test !("joint" in first.(calls)) + end + + @testset "probe_blockers" begin + blocked = [Dict("owner" => "Alpha", "file" => "docs/Project.toml", "dep" => "Doc", + "newest" => "1", "applies_in" => ["Alpha/docs", "Alpha/test"])] + msg = replace(CONFLICT_ON_OTHER, "DocStringExtensions" => "Doc") + sim, calls = fake_sim(baseline; error_for=Dict("Alpha/docs" => msg)) + found = probe_blockers(envs, uuids, blocked, baseline, "1.12"; sim) + # Alpha/test does not have Doc: not probed + @test calls == [("Alpha/docs", Dict("Doc" => "1"))] + f = only(found) + @test f["where"] == "Alpha/docs@1.12" + @test f["blockers"] == ["DocumenterVitepress"] + @test f["newest"] == "1" + # restricted to the newest, it resolves: the resolver merely prefers other versions + sim, _ = fake_sim(baseline) + f = only(probe_blockers(envs, uuids, blocked, baseline, "1.12"; sim)) + @test isempty(f["blockers"]) + @test f["detail"] == "resolves, but the resolver prefers other versions" + end + end +end diff --git a/scripts/compat-prune/validate.jl b/scripts/compat-prune/validate.jl index 0820752..9bc678d 100644 --- a/scripts/compat-prune/validate.jl +++ b/scripts/compat-prune/validate.jl @@ -1,6 +1,6 @@ # Validate the prunes and identify the blockers, on the running Julia. # -# usage: julia validate.jl [] +# usage: julia validate.jl [--blockers] # # Prunes: all of them (minus those already rejected) are simulated together; an environment # whose manifest differs from the baseline, or that no longer resolves, is a failure. The @@ -8,9 +8,10 @@ # fail alone are rejected; if none does, the failure comes from their combination and all of # them are rejected. `audit.jl` repeats until no Julia version rejects anything. # -# Blockers: for each blocked entry, each environment where it applies is resolved with the -# dependency restricted to the newest specifier; the resolver's explanation names the -# packages that cap it. +# Blockers (`--blockers`, given by `audit.jl` on the newest Julia version only, as they are +# information and cost one resolution per environment): for each blocked entry, each +# environment where it applies is resolved with the dependency restricted to the newest +# specifier; the resolver's explanation names the packages that cap it. include("common.jl") @@ -40,59 +41,79 @@ function blockers(msg, dep) return sort(unique(String[m[1] for m in eachmatch(first_level, msg)])) end -key(p) = (p["owner"], p["file"], p["dep"]) -if abspath(PROGRAM_FILE) == @__FILE__ - root, classified, basefile, outfile = ARGS[1:4] - rejected_before = length(ARGS) ≥ 5 ? Set(key.(get(TOML.parsefile(ARGS[5]), "rejected", []))) : Set() - envs, audited, uuids = load_collected(root) - c = TOML.parsefile(classified) - prunes = [p for p in c["prune"] if !(key(p) in rejected_before)] - baseline = TOML.parsefile(basefile)["env"] - julia = join(split(string(VERSION), ".")[1:2], ".") +""" + validate_prunes(envs, audited, uuids, prunes, baseline, julia; sim=simulate) -> rejected +Simulate `prunes` together in every environment of `envs` that resolved at `baseline`; in an +environment that departs from its baseline, simulate the prunes that apply there one by one +and reject those that fail alone, or all of them when none does. Returns the rejected +records (`owner`, `file`, `dep`, `where`, `reason`). +""" +function validate_prunes(envs, audited, uuids, prunes, baseline, julia; sim=simulate) + prunes = copy(prunes) rejected = Dict{String,Any}[] for name in sort(collect(keys(envs))) base = baseline[name] base["ok"] || continue n = narrowings(prunes, name, base["versions"], audited) isempty(n) && continue - why = departure(simulate(name, envs[name], n, uuids), base["versions"]) + why = departure(sim(name, envs[name], n, uuids), base["versions"]) why === nothing && continue - println(VERSION, " ", rpad(name, 24), "fails together: ", why) + println(julia, " ", rpad(name, 24), "fails together: ", why) suspects = [p for p in prunes if !isempty(narrowings([p], name, base["versions"], audited))] alone = Dict{Any,String}() for p in suspects - w = departure(simulate(name, envs[name], narrowings([p], name, base["versions"], audited), uuids), base["versions"]) - w === nothing || (alone[key(p)] = w) + w = departure(sim(name, envs[name], narrowings([p], name, base["versions"], audited), uuids), base["versions"]) + w === nothing || (alone[entry_key(p)] = w) end - culprits = isempty(alone) ? suspects : [p for p in suspects if haskey(alone, key(p))] + culprits = isempty(alone) ? suspects : [p for p in suspects if haskey(alone, entry_key(p))] for p in culprits - reason = get(alone, key(p), "fails only combined with other prunes: $why") + reason = get(alone, entry_key(p), "fails only combined with other prunes: $why") push!(rejected, Dict("owner" => p["owner"], "file" => p["file"], "dep" => p["dep"], "where" => "$name@$julia", "reason" => reason)) println(" rejected: $(p["owner"]) $(p["file"]) $(p["dep"]) → $(p["after"])") end - rejected_now = Set(key.(culprits)) - filter!(p -> !(key(p) in rejected_now), prunes) + gone = Set(entry_key.(culprits)) + filter!(p -> !(entry_key(p) in gone), prunes) end + return rejected +end + +""" + probe_blockers(envs, uuids, blocked, baseline, julia; sim=simulate) -> probes +For each blocked entry, resolve every environment where it applies with the dependency +restricted to its newest specifier, and read the blockers from the resolver's error. +""" +function probe_blockers(envs, uuids, blocked, baseline, julia; sim=simulate) found = Dict{String,Any}[] - for b in get(c, "other", []) - b["status"] == "blocked" || continue - for name in b["applies_in"] - base = baseline[name] - base["ok"] && haskey(base["versions"], b["dep"]) || continue - r = simulate(name, envs[name], Dict(b["dep"] => b["newest"]), uuids) - push!(found, Dict("owner" => b["owner"], "file" => b["file"], "dep" => b["dep"], - "newest" => b["newest"], "where" => "$name@$julia", - "blockers" => r["ok"] ? String[] : blockers(r["error"], b["dep"]), - "detail" => r["ok"] ? "resolves, but the resolver prefers other versions" : first(r["error"], 2000))) - println(VERSION, " blocked: $(b["owner"]) $(b["file"]) $(b["dep"]) $(b["newest"]) in $name, by ", - r["ok"] ? "nothing (resolver preference)" : join(found[end]["blockers"], ", ")) - end + for b in blocked, name in b["applies_in"] + base = baseline[name] + base["ok"] && haskey(base["versions"], b["dep"]) || continue + r = sim(name, envs[name], Dict(b["dep"] => b["newest"]), uuids) + push!(found, Dict("owner" => b["owner"], "file" => b["file"], "dep" => b["dep"], + "newest" => b["newest"], "where" => "$name@$julia", + "blockers" => r["ok"] ? String[] : blockers(r["error"], b["dep"]), + "detail" => r["ok"] ? "resolves, but the resolver prefers other versions" : first(r["error"], 2000))) + println(julia, " blocked: $(b["owner"]) $(b["file"]) $(b["dep"]) $(b["newest"]) in $name, by ", + r["ok"] ? "nothing (resolver preference)" : join(found[end]["blockers"], ", ")) end + return found +end +if abspath(PROGRAM_FILE) == @__FILE__ + # usage: … [--blockers] + root, classified, basefile, outfile, rejectedfile = ARGS[1:5] + rejected_before = Set(entry_key.(get(TOML.parsefile(rejectedfile), "rejected", []))) + envs, audited, uuids = load_collected(root) + c = TOML.parsefile(classified) + prunes = [p for p in c["prune"] if !(entry_key(p) in rejected_before)] + baseline = TOML.parsefile(basefile)["env"] + julia = join(split(string(VERSION), ".")[1:2], ".") + rejected = validate_prunes(envs, audited, uuids, prunes, baseline, julia) + blocked = [b for b in get(c, "other", []) if b["status"] == "blocked"] + found = "--blockers" in ARGS ? probe_blockers(envs, uuids, blocked, baseline, julia) : Dict{String,Any}[] open(io -> TOML.print(io, Dict("julia" => string(VERSION), "rejected" => rejected, "blocked" => found)), outfile, "w") - println(VERSION, " ", length(rejected), " prune(s) rejected, ", length(found), " blocker probe(s)") + println(julia, " ", length(rejected), " prune(s) rejected, ", length(found), " blocker probe(s)") end From 37cc2991a920c8266a0781b1152d21e151cacca9 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Thu, 15 Oct 2026 23:07:55 +0200 Subject: [PATCH 3/8] feat(compat-prune): reusable workflow (audit job) and its self-test - 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 --- .github/workflows/compat-prune-tests.yml | 51 ++++++++++ .github/workflows/compat-prune.yml | 115 +++++++++++++++++++++++ 2 files changed, 166 insertions(+) create mode 100644 .github/workflows/compat-prune-tests.yml create mode 100644 .github/workflows/compat-prune.yml diff --git a/.github/workflows/compat-prune-tests.yml b/.github/workflows/compat-prune-tests.yml new file mode 100644 index 0000000..e2f434f --- /dev/null +++ b/.github/workflows/compat-prune-tests.yml @@ -0,0 +1,51 @@ +name: Compat prune tests + +# Self-test of compat-prune, in CTActions itself: the test suite of scripts/compat-prune on +# every audited Julia version (unit, non-regression and integration tests), then a real +# dry-run audit of the ecosystem through the reusable workflow. Runs when the scripts or the +# workflows change; nothing is written anywhere (dry run). + +on: + pull_request: + paths: + - 'scripts/compat-prune/**' + - '.github/workflows/compat-prune*.yml' + push: + branches: [main] + paths: + - 'scripts/compat-prune/**' + - '.github/workflows/compat-prune*.yml' + workflow_dispatch: + +concurrency: + group: compat-prune-tests-${{ github.ref }} + cancel-in-progress: true + +jobs: + tests: + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: read + strategy: + fail-fast: false + matrix: + version: ['1.10', '1.12', '1.13'] + steps: + - uses: actions/checkout@v6 + - uses: julia-actions/setup-julia@v2 + with: + version: ${{ matrix.version }} + - name: Add the General registry + run: julia --startup-file=no -e 'using Pkg; Pkg.Registry.add("General")' + - name: Tests (with integration) + env: + COMPAT_PRUNE_INTEGRATION: '1' + run: julia --startup-file=no scripts/compat-prune/test/runtests.jl + + dry-run: + needs: tests + uses: ./.github/workflows/compat-prune.yml + with: + dry_run: true + ctactions_ref: ${{ github.event.pull_request.head.sha || github.sha }} diff --git a/.github/workflows/compat-prune.yml b/.github/workflows/compat-prune.yml new file mode 100644 index 0000000..a88046f --- /dev/null +++ b/.github/workflows/compat-prune.yml @@ -0,0 +1,115 @@ +name: Compat prune + +# Finds the [compat] specifiers that no supported Julia version ever selects across the +# control-toolbox packages, proves that dropping them leaves every resolved manifest +# unchanged, and (unless dry_run) opens one PR per affected repository plus one tracking +# issue in the calling repository, both updated in place by later runs. +# Called monthly by OptimalControl (`CompatPrune.yml`); see docs/WORKFLOWS.md §3.1. +# +# The `audit` job is read-only: it resolves the joint, test and docs environments of every +# package on every Julia version (Pkg's resolver only: nothing is downloaded or installed), +# classifies the multi-specifier entries, validates the prunes, applies them to fresh +# checkouts of the default branches, and uploads the result (`compat-prune` artifact, +# report in the job summary). Scripts and tests: scripts/compat-prune/. + +on: + workflow_call: + inputs: + packages: + description: 'JSON list of the audited packages (registered names)' + required: false + type: string + default: '["CTBase","CTParser","CTModels","CTLie","CTFlows","CTDirect","CTSolvers","OptimalControl"]' + julia_versions: + description: 'JSON list of the Julia versions (juliaup channels); the last one is the newest' + required: false + type: string + default: '["1.10","1.12","1.13"]' + dry_run: + description: 'Only audit and report: no PR, no issue' + required: false + type: boolean + default: true + ctactions_ref: + description: 'Ref of CTActions the scripts are checked out from' + required: false + type: string + default: main + +jobs: + audit: + runs-on: ubuntu-latest + timeout-minutes: 120 + permissions: + contents: read + env: + PACKAGES: ${{ inputs.packages }} + JULIA_VERSIONS: ${{ inputs.julia_versions }} + SCRIPTS: ctactions/scripts/compat-prune + OUT: ${{ github.workspace }}/compat-prune + GH_TOKEN: ${{ github.token }} + steps: + - uses: actions/checkout@v6 + with: + repository: control-toolbox/CTActions + ref: ${{ inputs.ctactions_ref }} + path: ctactions + + - uses: julia-actions/install-juliaup@v3 + with: + channel: release + + - name: Install the audited Julia versions + run: | + for v in $(jq -r '.[]' <<< "$JULIA_VERSIONS"); do juliaup add "$v"; done + juliaup status + + # ct-registry is public: added over HTTPS, no SSH key needed. The registries are + # updated once more by audit.jl, then frozen for the whole audit. + - name: Add the registries + run: | + julia --startup-file=no -e 'using Pkg + Pkg.Registry.add("General") + Pkg.Registry.add(url="https://github.com/control-toolbox/ct-registry.git")' + + - name: Test the scripts + run: julia --startup-file=no "$SCRIPTS/test/runtests.jl" + + - name: Audit + run: julia --startup-file=no "$SCRIPTS/audit.jl" "$PACKAGES" "$JULIA_VERSIONS" "$OUT" + + - name: Apply the prunes to fresh checkouts of the default branches + run: | + mkdir -p checkouts + julia --startup-file=no -e 'using TOML + for (n, p) in TOML.parsefile(ARGS[1])["packages"] + println(n, " ", p["repo"], " ", p["default_branch"]) + end' "$OUT/index.toml" | + while read -r name repo branch; do + git clone --quiet --depth 1 --branch "$branch" "https://github.com/$repo.git" "checkouts/$name" + done + julia --startup-file=no "$SCRIPTS/edit.jl" "$OUT" checkouts + for d in checkouts/*/; do + git -C "$d" diff --src-prefix="a/$(basename "$d")/" --dst-prefix="b/$(basename "$d")/" + done > "$OUT/edits.diff" + + - name: Report + run: | + RUN_URL="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \ + julia --startup-file=no "$SCRIPTS/report.jl" "$OUT" + { + cat "$OUT/report.md" + echo + echo "
Edits on the default branches (diff)" + echo + echo '```diff' + cat "$OUT/edits.diff" + echo '```' + echo "
" + } >> "$GITHUB_STEP_SUMMARY" + + - uses: actions/upload-artifact@v7 + with: + name: compat-prune + path: compat-prune/ + retention-days: 30 From 8808247c5e84587a5e3d33a9a34a0d8284c6f04f Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Thu, 15 Oct 2026 23:20:40 +0200 Subject: [PATCH 4/8] feat(compat-prune): publish job (one PR per repository, one tracking 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 --- .github/workflows/compat-prune.yml | 79 ++++++- scripts/compat-prune/checkout.sh | 18 ++ scripts/compat-prune/publish.jl | 142 ++++++++++++ scripts/compat-prune/report.jl | 22 ++ scripts/compat-prune/test/golden/pr-body.md | 14 ++ scripts/compat-prune/test/runtests.jl | 1 + scripts/compat-prune/test/test_publish.jl | 228 ++++++++++++++++++++ 7 files changed, 496 insertions(+), 8 deletions(-) create mode 100755 scripts/compat-prune/checkout.sh create mode 100644 scripts/compat-prune/publish.jl create mode 100644 scripts/compat-prune/test/golden/pr-body.md create mode 100644 scripts/compat-prune/test/test_publish.jl diff --git a/.github/workflows/compat-prune.yml b/.github/workflows/compat-prune.yml index a88046f..432c1a0 100644 --- a/.github/workflows/compat-prune.yml +++ b/.github/workflows/compat-prune.yml @@ -11,6 +11,12 @@ name: Compat prune # classifies the multi-specifier entries, validates the prunes, applies them to fresh # checkouts of the default branches, and uploads the result (`compat-prune` artifact, # report in the job summary). Scripts and tests: scripts/compat-prune/. +# +# The `publish` job (skipped on dry runs) is the only one that writes, with a token of the +# organization's GitHub App (secrets CT_APP_CLIENT_ID, CT_APP_PRIVATE_KEY; permissions: +# contents, pull requests, issues). It re-applies the prunes to fresh checkouts, then keeps +# one branch `ct/compat-prune` and one PR per repository, and one tracking issue in the +# calling repository: a rerun updates them in place, and closes what has nothing left. on: workflow_call: @@ -35,6 +41,13 @@ on: required: false type: string default: main + secrets: + CT_APP_CLIENT_ID: + description: 'Client ID of the GitHub App that publishes (not needed for a dry run)' + required: false + CT_APP_PRIVATE_KEY: + description: 'Private key of that GitHub App' + required: false jobs: audit: @@ -80,14 +93,7 @@ jobs: - name: Apply the prunes to fresh checkouts of the default branches run: | - mkdir -p checkouts - julia --startup-file=no -e 'using TOML - for (n, p) in TOML.parsefile(ARGS[1])["packages"] - println(n, " ", p["repo"], " ", p["default_branch"]) - end' "$OUT/index.toml" | - while read -r name repo branch; do - git clone --quiet --depth 1 --branch "$branch" "https://github.com/$repo.git" "checkouts/$name" - done + "$SCRIPTS/checkout.sh" "$OUT" checkouts julia --startup-file=no "$SCRIPTS/edit.jl" "$OUT" checkouts for d in checkouts/*/; do git -C "$d" diff --src-prefix="a/$(basename "$d")/" --dst-prefix="b/$(basename "$d")/" @@ -113,3 +119,60 @@ jobs: name: compat-prune path: compat-prune/ retention-days: 30 + + publish: + needs: audit + if: ${{ !inputs.dry_run }} + runs-on: ubuntu-latest + timeout-minutes: 30 + # Two publications at once would race on the same branches and issue. + concurrency: + group: compat-prune-publish + cancel-in-progress: false + permissions: + contents: read + env: + SCRIPTS: ctactions/scripts/compat-prune + OUT: ${{ github.workspace }}/compat-prune + steps: + - name: Check the App credentials + env: + CLIENT_ID: ${{ secrets.CT_APP_CLIENT_ID }} + PRIVATE_KEY: ${{ secrets.CT_APP_PRIVATE_KEY }} + run: | + if [ -z "$CLIENT_ID" ] || [ -z "$PRIVATE_KEY" ]; then + echo "::error::Publishing needs the secrets CT_APP_CLIENT_ID and CT_APP_PRIVATE_KEY (or run with dry_run: true)." + exit 1 + fi + + - uses: actions/checkout@v6 + with: + repository: control-toolbox/CTActions + ref: ${{ inputs.ctactions_ref }} + path: ctactions + + - uses: actions/download-artifact@v8 + with: + name: compat-prune + path: compat-prune + + - uses: julia-actions/install-juliaup@v3 + with: + channel: release + + - uses: actions/create-github-app-token@v3 + id: app + with: + client-id: ${{ secrets.CT_APP_CLIENT_ID }} + private-key: ${{ secrets.CT_APP_PRIVATE_KEY }} + owner: control-toolbox + + - name: Publish + env: + GH_TOKEN: ${{ steps.app.outputs.token }} + run: | + gh auth setup-git + "$SCRIPTS/checkout.sh" "$OUT" checkouts + julia --startup-file=no "$SCRIPTS/edit.jl" "$OUT" checkouts + RUN_URL="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \ + julia --startup-file=no "$SCRIPTS/publish.jl" "$OUT" checkouts "$GITHUB_REPOSITORY" | tee -a "$GITHUB_STEP_SUMMARY" diff --git a/scripts/compat-prune/checkout.sh b/scripts/compat-prune/checkout.sh new file mode 100755 index 0000000..9ba74f9 --- /dev/null +++ b/scripts/compat-prune/checkout.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# Shallow clone of the default branch of every audited package. +# +# usage: checkout.sh +# +# Reads /index.toml; clones https://github.com/ into /. +# Pushing later relies on git's credentials (`gh auth setup-git` in the publish job). +set -euo pipefail +audit=$1 +checkouts=$2 +mkdir -p "$checkouts" +julia --startup-file=no -e 'using TOML + for (n, p) in sort(collect(TOML.parsefile(ARGS[1])["packages"])) + println(n, " ", p["repo"], " ", p["default_branch"]) + end' "$audit/index.toml" | +while read -r name repo branch; do + git clone --quiet --depth 1 --branch "$branch" "https://github.com/$repo.git" "$checkouts/$name" +done diff --git a/scripts/compat-prune/publish.jl b/scripts/compat-prune/publish.jl new file mode 100644 index 0000000..045c051 --- /dev/null +++ b/scripts/compat-prune/publish.jl @@ -0,0 +1,142 @@ +# Publish an audit: one PR per repository with edits, one tracking issue. +# +# usage: julia publish.jl +# +# Run after edit.jl on fresh checkouts (/, cloned with a token allowed +# to push and to open PRs and issues). Idempotent, so that a rerun changes nothing: +# - the tracking issue of is the one whose body carries MARKER, open or +# closed: its body is rewritten, it is reopened when there is something to prune and +# closed (with a comment) when there is nothing left; it is never created twice; +# - each repository has one bot-owned branch, BRANCH, rebuilt from its default branch: it +# is pushed only when its content would change; its open PR is updated, or created; +# when the repository has nothing left to prune, the PR is closed and the branch deleted. +# Every GitHub call goes through `gh` (GH runner, faked in tests); git runs for real. + +include("common.jl") +include("report.jl") + +const BRANCH = "ct/compat-prune" +const ISSUE_TITLE = "[compat] prune report" +const PR_TITLE = "chore(compat): drop [compat] specifiers no supported Julia version selects" +const BOT = ("github-actions[bot]", "41898282+github-actions[bot]@users.noreply.github.com") + +"Run git in `dir` and return its standard output." +git(dir, args...) = read(`git -C $dir $args`, String) + +"Tree of `rev` in `dir`, or `nothing` when it does not exist." +function tree(dir, rev) + t = readchomp(ignorestatus(`git -C $dir rev-parse --verify --quiet $("$rev^{tree}")`)) + return isempty(t) ? nothing : t +end + +""" + prepare_branch(dir, message) -> Bool + +Commit the working-tree changes of checkout `dir` on BRANCH (from its current HEAD) and +tell whether the remote BRANCH differs, i.e. whether a push is needed. +""" +function prepare_branch(dir, message) + git(dir, "checkout", "--quiet", "-B", BRANCH) + git(dir, "add", "--all") + git(dir, "-c", "user.name=$(BOT[1])", "-c", "user.email=$(BOT[2])", "commit", "--quiet", "-m", message) + remote = remote_tree(dir) + return remote != tree(dir, "HEAD") +end + +"Tree of the remote BRANCH of checkout `dir`, or `nothing` when it does not exist." +function remote_tree(dir) + isempty(strip(git(dir, "ls-remote", "--heads", "origin", BRANCH))) && return nothing + git(dir, "fetch", "--quiet", "origin", "$BRANCH:refs/remotes/origin/$BRANCH") + return tree(dir, "refs/remotes/origin/$BRANCH") +end + +"Commit message of the edits of `owner`." +commit_message(owner, edits) = join(vcat("chore(compat): drop [compat] specifiers no supported Julia version selects", "", + sort(["$(e["file"]): $(e["dep"]) = \"$(e["before"])\" → \"$(e["after"])\"" for e in edits if e["owner"] == owner && e["status"] == "edited"])), "\n") + +"Number and URL of the open PR from BRANCH in `repo`, or `nothing`." +function find_pr(repo) + out = strip(gh("pr", "list", "--repo", repo, "--head", BRANCH, "--state", "open", "--json", "number,url", + "--jq", ".[] | \"\\(.number) \\(.url)\"")) + isempty(out) && return nothing + number, url = split(first(split(out, '\n'))) + return (number, url) +end + +"Number and state (`open`/`closed`) of the tracking issue of `repo`, or `nothing`." +function find_issue(repo) + jq = ".[] | select(.pull_request == null) | select((.body // \"\") | contains(\"$MARKER\")) | \"\\(.number) \\(.state)\"" + out = strip(gh("api", "--paginate", "repos/$repo/issues?state=all&per_page=100", "--jq", jq)) + isempty(out) && return nothing + number, state = split(first(split(out, '\n'))) + return (number, state) +end + +""" + publish(root, checkouts, tracking; run_url="") -> Dict + +Publish the audit of `root`, whose edits.toml was produced by edit.jl on `checkouts`. +Returns what was done: `prs` (owner => URL), `pushed`, `closed` (owners), `issue`. +""" +function publish(root, checkouts, tracking; run_url="") + audit = TOML.parsefile(joinpath(root, "audit.toml")) + edits = TOML.parsefile(joinpath(root, "edits.toml"))["edit"] + index = audit["packages"] + done = Dict{String,Any}("prs" => Dict{String,String}(), "pushed" => String[], "closed" => String[]) + with_edits = Set(e["owner"] for e in edits if e["status"] == "edited") + + # The issue first: the PRs link to it. + issue = find_issue(tracking) + if issue === nothing && !isempty(with_edits) + url = strip(gh("issue", "create", "--repo", tracking, "--title", ISSUE_TITLE, + "--body", render_report(audit, edits; run_url))) + issue = (last(split(url, "/")), "open") + end + issue_url = issue === nothing ? "" : "https://github.com/$tracking/issues/$(issue[1])" + + for owner in sort(collect(keys(index))) + repo, base = index[owner]["repo"], index[owner]["default_branch"] + pr = find_pr(repo) + if !(owner in with_edits) + if pr !== nothing + gh("pr", "close", pr[1], "--repo", repo, "--delete-branch", + "--comment", "Nothing left to prune in this repository$(isempty(issue_url) ? "" : " (see $issue_url)").") + push!(done["closed"], owner) + end + continue + end + dir = joinpath(checkouts, owner) + if prepare_branch(dir, commit_message(owner, edits)) + git(dir, "push", "--quiet", "--force", "origin", BRANCH) + push!(done["pushed"], owner) + end + body = render_pr_body(owner, edits; issue_url, run_url) + if pr === nothing + url = strip(gh("pr", "create", "--repo", repo, "--base", base, "--head", BRANCH, "--title", PR_TITLE, "--body", body)) + else + gh("pr", "edit", pr[1], "--repo", repo, "--body", body) + url = pr[2] + end + done["prs"][owner] = url + end + + if issue !== nothing + gh("issue", "edit", issue[1], "--repo", tracking, "--body", render_report(audit, edits; prs=done["prs"], run_url)) + if isempty(with_edits) && issue[2] == "open" + gh("issue", "close", issue[1], "--repo", tracking, "--comment", "Nothing left to prune.") + elseif !isempty(with_edits) && issue[2] == "closed" + gh("issue", "reopen", issue[1], "--repo", tracking) + end + end + done["issue"] = issue_url + return done +end + +if abspath(PROGRAM_FILE) == @__FILE__ + root, checkouts, tracking = ARGS + done = publish(root, checkouts, tracking; run_url=get(ENV, "RUN_URL", "")) + open(io -> TOML.print(io, done), joinpath(root, "published.toml"), "w") + println("issue: ", isempty(done["issue"]) ? "none" : done["issue"]) + for (owner, url) in sort(collect(done["prs"])); println("PR ", rpad(owner, 16), url, owner in done["pushed"] ? " (pushed)" : ""); end + for owner in done["closed"]; println("PR ", rpad(owner, 16), "closed"); end +end diff --git a/scripts/compat-prune/report.jl b/scripts/compat-prune/report.jl index 0b1048b..3496da3 100644 --- a/scripts/compat-prune/report.jl +++ b/scripts/compat-prune/report.jl @@ -97,6 +97,28 @@ function render_report(audit, edits; prs=Dict(), run_url="") return join(out, "\n\n") * "\n" end +""" + render_pr_body(owner, edits; issue_url="", run_url="") -> String + +Body of the PR of `owner`: its edited entries, why they can go, where the audit is. +""" +function render_pr_body(owner, edits; issue_url="", run_url="") + mine = sort([e for e in edits if e["owner"] == owner && e["status"] == "edited"]; by=entry_key) + rows = [[code(e["file"]), code(e["dep"]), code(e["before"]), code(e["after"]), selected(e["evidence"])] for e in mine] + links = String[] + isempty(issue_url) || push!(links, "tracking issue: $issue_url") + isempty(run_url) || push!(links, "[audit run]($run_url)") + return join([MARKER, """ + Drops the `[compat]` specifiers that no supported Julia version selects, in any + environment of the control-toolbox packages (joint, tests and docs of every package). + Without them every audited manifest is unchanged: this PR changes no version that + these environments resolve, needs no release of its own, and can be merged in any + order with the PRs of the other packages.""", + table(["file", "dependency", "before", "after", "selected"], rows), + "Opened by the [`compat-prune`]($WORKFLOW_URL) workflow" * (isempty(links) ? "" : " ($(join(links, ", ")))") * + ". The branch is rewritten by every run: push nothing to it."], "\n\n") * "\n" +end + if abspath(PROGRAM_FILE) == @__FILE__ root = ARGS[1] prs = length(ARGS) ≥ 2 ? TOML.parsefile(ARGS[2]) : Dict() diff --git a/scripts/compat-prune/test/golden/pr-body.md b/scripts/compat-prune/test/golden/pr-body.md new file mode 100644 index 0000000..919b37d --- /dev/null +++ b/scripts/compat-prune/test/golden/pr-body.md @@ -0,0 +1,14 @@ + + +Drops the `[compat]` specifiers that no supported Julia version selects, in any +environment of the control-toolbox packages (joint, tests and docs of every package). +Without them every audited manifest is unchanged: this PR changes no version that +these environments resolve, needs no release of its own, and can be merged in any +order with the PRs of the other packages. + +| file | dependency | before | after | selected | +| --- | --- | --- | --- | --- | +| `Project.toml` | `Dep1` | `0.5, 0.6, 0.7` | `0.6, 0.7` | 0.7.2 on 1.12; 0.6.4 on 1.10 | +| `docs/Project.toml` | `Doc` | `0.9, 1` | `1` | | + +Opened by the [`compat-prune`](https://github.com/control-toolbox/CTActions/blob/main/.github/workflows/compat-prune.yml) workflow (tracking issue: https://github.com/example-org/Root.jl/issues/100, [audit run](https://github.com/example-org/Root.jl/actions/runs/1)). The branch is rewritten by every run: push nothing to it. diff --git a/scripts/compat-prune/test/runtests.jl b/scripts/compat-prune/test/runtests.jl index 4334ce6..fe43782 100644 --- a/scripts/compat-prune/test/runtests.jl +++ b/scripts/compat-prune/test/runtests.jl @@ -21,6 +21,7 @@ include("helpers.jl") include("test_validate.jl") include("test_edit.jl") include("test_report.jl") + include("test_publish.jl") include("test_regression.jl") if get(ENV, "COMPAT_PRUNE_INTEGRATION", "") == "1" include("test_integration.jl") diff --git a/scripts/compat-prune/test/test_publish.jl b/scripts/compat-prune/test/test_publish.jl new file mode 100644 index 0000000..f1eceef --- /dev/null +++ b/scripts/compat-prune/test/test_publish.jl @@ -0,0 +1,228 @@ +# publish.jl: idempotent PRs and tracking issue, against a stateful fake GitHub and real git +# repositories (a local bare repository plays `origin` for each package). + +include(joinpath(SCRIPTS, "publish.jl")) + +""" +A fake GitHub keeping issues and PRs. `origins` maps a repo ("owner/name") to its bare +repository, where `pr close --delete-branch` deletes the branch. +""" +mutable struct FakeGitHub + origins::Dict{String,String} + issues::Vector{Dict{String,Any}} + prs::Vector{Dict{String,Any}} + calls::Vector{Vector{String}} +end +FakeGitHub(origins) = FakeGitHub(origins, Dict{String,Any}[], Dict{String,Any}[], Vector{String}[]) + +opt(args, name) = args[findfirst(==(name), args) + 1] + +function (f::FakeGitHub)(args) + push!(f.calls, args) + cmd = args[1:2] + if cmd == ["api", "--paginate"] + repo = match(r"^repos/([^/]+/[^/]+)/issues", args[3])[1] + hits = [i for i in f.issues if i["repo"] == repo && occursin(MARKER, i["body"])] + return join(["$(i["number"]) $(i["state"])" for i in hits], "\n") * (isempty(hits) ? "" : "\n") + elseif cmd == ["issue", "create"] + repo = opt(args, "--repo") + n = length(f.issues) + 100 + push!(f.issues, Dict("repo" => repo, "number" => n, "state" => "open", "title" => opt(args, "--title"), + "body" => opt(args, "--body"), "comments" => String[])) + return "https://github.com/$repo/issues/$n\n" + elseif cmd[1] == "issue" + i = only(filter(i -> string(i["number"]) == args[3] && i["repo"] == opt(args, "--repo"), f.issues)) + cmd[2] == "edit" && (i["body"] = opt(args, "--body")) + cmd[2] == "close" && (i["state"] = "closed"; push!(i["comments"], opt(args, "--comment"))) + cmd[2] == "reopen" && (i["state"] = "open") + return "" + elseif cmd == ["pr", "list"] + repo, head = opt(args, "--repo"), opt(args, "--head") + hits = [p for p in f.prs if p["repo"] == repo && p["head"] == head && p["state"] == "open"] + return join(["$(p["number"]) $(p["url"])" for p in hits], "\n") + elseif cmd == ["pr", "create"] + repo = opt(args, "--repo") + n = length(f.prs) + 1 + url = "https://github.com/$repo/pull/$n" + push!(f.prs, Dict("repo" => repo, "number" => n, "url" => url, "state" => "open", "head" => opt(args, "--head"), + "base" => opt(args, "--base"), "title" => opt(args, "--title"), "body" => opt(args, "--body"))) + return url * "\n" + elseif cmd[1] == "pr" + p = only(filter(p -> string(p["number"]) == args[3] && p["repo"] == opt(args, "--repo"), f.prs)) + cmd[2] == "edit" && (p["body"] = opt(args, "--body")) + if cmd[2] == "close" + p["state"] = "closed" + "--delete-branch" in args && run(`git -C $(f.origins[p["repo"]]) branch --quiet -D $(p["head"])`) + end + return "" + end + error("unexpected gh call: $args") +end + +const GITC = `git -c user.name=test -c user.email=test@example.org -c init.defaultBranch=main` + +"A bare origin for each fixture package, holding its default-branch files; returns repo => path." +function make_origins(dir) + index = TOML.parsefile(joinpath(COLLECTED, "index.toml"))["packages"] + origins = Dict{String,String}() + for (name, p) in index + bare = joinpath(dir, "origins", name * ".git") + run(`$GITC init --quiet --bare -b $(p["default_branch"]) $bare`) + work = joinpath(dir, "seed", name) + run(`$GITC clone --quiet $bare $work`) + src = joinpath(COLLECTED, "default", name) + foreach(f -> cp(joinpath(src, f), joinpath(work, f)), readdir(src)) + run(`$GITC -C $work checkout --quiet -B $(p["default_branch"])`) + run(`$GITC -C $work add --all`) + run(`$GITC -C $work commit --quiet -m init`) + run(`$GITC -C $work push --quiet origin $(p["default_branch"])`) + origins[p["repo"]] = bare + end + return origins +end + +"Fresh clones of every origin's default branch, then edit.jl's work: writes audit.toml, edits.toml." +function prepare_run(dir, origins, prunes; tag) + index = TOML.parsefile(joinpath(COLLECTED, "index.toml"))["packages"] + root = joinpath(dir, "run-$tag") + checkouts = joinpath(root, "checkouts") + for (name, p) in index + run(`$GITC clone --quiet --branch $(p["default_branch"]) $(origins[p["repo"]]) $(joinpath(checkouts, name))`) + end + audit = Dict{String,Any}("date" => "2026-11-01 04:00 UTC", "julia" => ["1.10", "1.12"], "packages" => index, + "prune" => prunes, "rejected" => [], "blocked" => [], "other" => []) + open(io -> TOML.print(io, audit), joinpath(root, "audit.toml"), "w") + records, texts = plan_edits(audit, read_checkout(checkouts)) + for ((owner, file), text) in texts + write(joinpath(checkouts, owner, file), text) + end + open(io -> TOML.print(io, Dict("edit" => records)), joinpath(root, "edits.toml"), "w") + return root, checkouts +end + +"Commit `text` as `file` on the default branch of `origin` (a maintainer merging the PR)." +function merge_on_origin(dir, origin, branch, file, text) + work = joinpath(dir, "merge-" * string(hash((origin, file, text)); base=16)) + run(`$GITC clone --quiet --branch $branch $origin $work`) + write(joinpath(work, file), text) + run(`$GITC -C $work commit --quiet -am merged`) + run(`$GITC -C $work push --quiet origin $branch`) +end + +branch_exists(origin) = success(`git -C $origin rev-parse --verify --quiet refs/heads/$BRANCH`) + +@testset "publish" begin + @testset "helpers" begin + evidence = Dict{String,Any}("0.6" => ["joint@1.10=0.6.4"], "0.7" => ["joint@1.12=0.7.2"]) + edits = [merge(prune("Alpha", "Project.toml", "Dep1", "0.5, 0.6, 0.7", "0.6, 0.7"), Dict("status" => "edited", "evidence" => evidence)), + merge(prune("Alpha", "docs/Project.toml", "Doc", "0.9, 1", "1"), Dict("status" => "edited")), + merge(prune("Beta", "Project.toml", "Dep1", "0.6, 0.7", "0.7"), Dict("status" => "already"))] + @test commit_message("Alpha", edits) == """ + chore(compat): drop [compat] specifiers no supported Julia version selects + + Project.toml: Dep1 = "0.5, 0.6, 0.7" → "0.6, 0.7" + docs/Project.toml: Doc = "0.9, 1" → "1\"""" + body = render_pr_body("Alpha", edits; issue_url="https://github.com/example-org/Root.jl/issues/100", + run_url="https://github.com/example-org/Root.jl/actions/runs/1") + @test golden("pr-body.md", body) + @test startswith(body, MARKER) + @test !occursin("Beta", body) + end + + mktempdir() do dir + origins = make_origins(dir) + alpha, beta = origins["example-org/Alpha.jl"], origins["example-org/Beta.jl"] + fake = FakeGitHub(origins) + prunes = [prune("Alpha", "Project.toml", "Dep1", "0.5, 0.6, 0.7", "0.6, 0.7"), + prune("Alpha", "docs/Project.toml", "Doc", "0.9, 1", "1"), + prune("Beta", "Project.toml", "Dep1", "0.6, 0.7", "0.7")] # Beta's branch already has it + + @testset "first run: issue, branch, PR" begin + root, checkouts = prepare_run(dir, origins, prunes; tag="1") + done = with_gh(() -> publish(root, checkouts, "example-org/Root.jl"; run_url="https://run/1"), fake) + @test length(fake.issues) == 1 + issue = only(fake.issues) + @test issue["title"] == ISSUE_TITLE && issue["state"] == "open" + @test done["issue"] == "https://github.com/example-org/Root.jl/issues/100" + pr = only(fake.prs) + @test pr["repo"] == "example-org/Alpha.jl" && pr["head"] == BRANCH && pr["base"] == "main" + @test done["prs"] == Dict("Alpha" => pr["url"]) + @test done["pushed"] == ["Alpha"] + # the PR links the issue, the issue links the PR + @test occursin(done["issue"], pr["body"]) + @test occursin("### Alpha — $(pr["url"])", issue["body"]) + # the branch holds the edits on top of the default branch, nothing else + @test branch_exists(alpha) && !branch_exists(beta) + diff = read(`git -C $alpha diff main $BRANCH`, String) + @test occursin("-Dep1 = \"0.5, 0.6, 0.7\" # lanes kept by CompatHelper", diff) + @test occursin("+Dep1 = \"0.6, 0.7\" # lanes kept by CompatHelper", diff) + @test occursin("+Doc = \"1\"", diff) + @test count(l -> startswith(l, "+") && !startswith(l, "+++"), split(diff, '\n')) == 2 + @test strip(read(`git -C $alpha log -1 --format=%an $BRANCH`, String)) == BOT[1] + end + + @testset "second run: nothing changes" begin + before = read(`git -C $alpha rev-parse $BRANCH`, String) + n_calls = length(fake.calls) + root, checkouts = prepare_run(dir, origins, prunes; tag="2") + done = with_gh(() -> publish(root, checkouts, "example-org/Root.jl"; run_url="https://run/2"), fake) + @test length(fake.issues) == 1 && length(fake.prs) == 1 + @test isempty(done["pushed"]) + @test read(`git -C $alpha rev-parse $BRANCH`, String) == before + calls = fake.calls[n_calls+1:end] + @test !any(c -> c[1:2] in (["issue", "create"], ["pr", "create"]), calls) + @test any(c -> c[1:2] == ["pr", "edit"], calls) + # bodies refreshed with the new run + @test occursin("https://run/2", only(fake.issues)["body"]) + end + + @testset "after the merge: PR closed, branch deleted, issue closed" begin + # the maintainer merges: the default branch now carries the pruned values + alpha_now = read(`git -C $alpha show main:Project.toml`, String) + merge_on_origin(dir, alpha, "main", "Project.toml", replace(alpha_now, "Dep1 = \"0.5, 0.6, 0.7\"" => "Dep1 = \"0.6, 0.7\"")) + docs_now = read(`git -C $alpha show main:docs/Project.toml`, String) + merge_on_origin(dir, alpha, "main", "docs/Project.toml", replace(docs_now, "Doc = \"0.9, 1\"" => "Doc = \"1\"")) + root, checkouts = prepare_run(dir, origins, prunes; tag="3") + @test all(e -> e["status"] == "already", TOML.parsefile(joinpath(root, "edits.toml"))["edit"]) + done = with_gh(() -> publish(root, checkouts, "example-org/Root.jl"), fake) + @test done["closed"] == ["Alpha"] + @test only(fake.prs)["state"] == "closed" + @test !branch_exists(alpha) + issue = only(fake.issues) + @test issue["state"] == "closed" && issue["comments"] == ["Nothing left to prune."] + @test occursin("| merged, awaiting release | 3 |", issue["body"]) + end + + @testset "nothing to prune and no issue: nothing is created" begin + f = FakeGitHub(origins) + root, checkouts = prepare_run(dir, origins, prunes; tag="3b") + done = with_gh(() -> publish(root, checkouts, "example-org/Other.jl"), f) + @test isempty(f.issues) && isempty(f.prs) && done["issue"] == "" + end + + @testset "a new prune: the same issue is reopened, a new PR opened" begin + again = [prune("Beta", "Project.toml", "Tool", "1, 2", "2")] + beta_now = read(`git -C $beta show dev:Project.toml`, String) + merge_on_origin(dir, beta, "dev", "Project.toml", replace(beta_now, "Tool = \"2\"" => "Tool = \"1, 2\"")) + root, checkouts = prepare_run(dir, origins, again; tag="4") + done = with_gh(() -> publish(root, checkouts, "example-org/Root.jl"), fake) + @test length(fake.issues) == 1 && only(fake.issues)["state"] == "open" + @test length(fake.prs) == 2 + pr = fake.prs[2] + @test pr["repo"] == "example-org/Beta.jl" && pr["base"] == "dev" && pr["state"] == "open" + @test done["prs"] == Dict("Beta" => pr["url"]) + @test branch_exists(beta) + end + + @testset "find_issue ignores PRs and issues without the marker" begin + f = FakeGitHub(origins) + push!(f.issues, Dict("repo" => "a/b", "number" => 7, "state" => "open", "body" => "no marker", "comments" => String[])) + @test with_gh(() -> find_issue("a/b"), f) === nothing + push!(f.issues, Dict("repo" => "a/b", "number" => 8, "state" => "closed", "body" => MARKER * "\nold", "comments" => String[])) + @test with_gh(() -> find_issue("a/b"), f) == ("8", "closed") + # the jq filter excludes pull requests and empty bodies + jq = f.calls[end][end] + @test occursin("select(.pull_request == null)", jq) && occursin("(.body // \"\")", jq) + end + end +end From 156b8065ce74f7f7de5acf102075dc371946ad59 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Thu, 15 Oct 2026 23:29:29 +0200 Subject: [PATCH 5/8] fix(compat-prune): checkout.sh sorted the index by its dict values too `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 --- scripts/compat-prune/checkout.sh | 7 +++-- scripts/compat-prune/test/test_integration.jl | 28 +++++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/scripts/compat-prune/checkout.sh b/scripts/compat-prune/checkout.sh index 9ba74f9..47e8a3d 100755 --- a/scripts/compat-prune/checkout.sh +++ b/scripts/compat-prune/checkout.sh @@ -3,16 +3,17 @@ # # usage: checkout.sh # -# Reads /index.toml; clones https://github.com/ into /. +# Reads /index.toml; clones $COMPAT_PRUNE_GIT_BASE/.git (default +# https://github.com; tests point it at local repositories) into /. # Pushing later relies on git's credentials (`gh auth setup-git` in the publish job). set -euo pipefail audit=$1 checkouts=$2 mkdir -p "$checkouts" julia --startup-file=no -e 'using TOML - for (n, p) in sort(collect(TOML.parsefile(ARGS[1])["packages"])) + for (n, p) in sort(collect(TOML.parsefile(ARGS[1])["packages"]); by=first) println(n, " ", p["repo"], " ", p["default_branch"]) end' "$audit/index.toml" | while read -r name repo branch; do - git clone --quiet --depth 1 --branch "$branch" "https://github.com/$repo.git" "$checkouts/$name" + git clone --quiet --depth 1 --branch "$branch" "${COMPAT_PRUNE_GIT_BASE:-https://github.com}/$repo.git" "$checkouts/$name" done diff --git a/scripts/compat-prune/test/test_integration.jl b/scripts/compat-prune/test/test_integration.jl index eacd2da..b406b9b 100644 --- a/scripts/compat-prune/test/test_integration.jl +++ b/scripts/compat-prune/test/test_integration.jl @@ -99,6 +99,34 @@ run_script(script, args...) = read(`$JULIA --startup-file=no $(joinpath(SCRIPTS, end end + @testset "checkout.sh (local origins)" begin + mktempdir() do dir + # two packages, one with a dict-valued index entry each (sorting must not compare them) + index = Dict("Beta" => Dict("repo" => "org/Beta.jl", "default_branch" => "dev"), + "Alpha" => Dict("repo" => "org/Alpha.jl", "default_branch" => "main")) + for (name, p) in index + bare = joinpath(dir, "org", "$name.jl.git") + work = joinpath(dir, "seed-$name") + run(`git -c init.defaultBranch=$(p["default_branch"]) init --quiet $work`) + write(joinpath(work, "Project.toml"), "name = \"$name\"\n") + run(`git -C $work add --all`) + run(`git -C $work -c user.name=t -c user.email=t@example.org commit --quiet -m init`) + run(`git clone --quiet --bare $work $bare`) + end + audit = joinpath(dir, "audit") + mkpath(audit) + open(io -> TOML.print(io, Dict("packages" => index)), joinpath(audit, "index.toml"), "w") + withenv("COMPAT_PRUNE_GIT_BASE" => "file://$dir") do + run(`$(joinpath(SCRIPTS, "checkout.sh")) $audit $(joinpath(dir, "checkouts"))`) + end + for (name, p) in index + co = joinpath(dir, "checkouts", name) + @test read(joinpath(co, "Project.toml"), String) == "name = \"$name\"\n" + @test readchomp(`git -C $co rev-parse --abbrev-ref HEAD`) == p["default_branch"] + end + end + end + @testset "simulate" begin env = Dict("deps" => Dict("Example" => EXAMPLE), "compat" => Dict("Example" => "=0.5.5"), "pins" => Dict("Example" => "0.5.5")) From cc32a545a400d916f4f489e57ae09b0148741f99 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Thu, 15 Oct 2026 23:29:29 +0200 Subject: [PATCH 6/8] docs(workflows): compat-prune, its self-test, its secrets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- README.md | 1 + docs/WORKFLOWS.md | 80 +++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 78 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2bdcb1e..ce7fd0c 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ repository and for org-wide updates — is described in | `documentation.yml` | Build and deploy documentation with Documenter.jl | `use_ct_registry` | | `formatter.yml` | Run JuliaFormatter (BlueStyle) and open a PR if needed | — | | `compat-helper.yml` | Keep `[compat]` entries in `Project.toml` up to date | `subdirs` | +| `compat-prune.yml` | Drop the `[compat]` specifiers no supported Julia version selects, across all CT\* packages (one PR per repository, one tracking issue); scripts and tests in `scripts/compat-prune/` | `packages`, `julia_versions`, `dry_run` | | `spell-check.yml` | Spell check with `typos` | `locale`, `extend-identifiers`, `config-path` | | `breakage.yml` | Test downstream packages against a new version; comment results on PR | `pkgname`, `pkgpath`, `pkgversion`, `pkgbreak` | | `add-to-project.yml` | Auto-add issues/PRs to the org GitHub project | `project-url`, `status` | diff --git a/docs/WORKFLOWS.md b/docs/WORKFLOWS.md index 9302bd4..c1ab8d9 100644 --- a/docs/WORKFLOWS.md +++ b/docs/WORKFLOWS.md @@ -223,6 +223,7 @@ few **non-centralized / special** workflows (defined directly in a package). | `formatter.yml` | `Formatter.yml` | Run JuliaFormatter (BlueStyle) and open an auto PR | scheduled (nightly), `workflow_dispatch` | — | — | | `spell-check.yml` | `SpellCheck.yml` | Spell-check with `crate-ci/typos` | PR, `workflow_dispatch` | `locale`, `extend-identifiers`, `config-path` | — | | `compat-helper.yml` | `CompatHelper.yml` | Open PRs bumping `[compat]` bounds | scheduled (daily), `workflow_dispatch` | `subdirs`; `GITHUB_TOKEN`, `DOCUMENTER_KEY` | — | +| `compat-prune.yml` | `CompatPrune.yml` (OptimalControl only) | Drop the `[compat]` specifiers no supported Julia version selects, across all CT\* packages: one PR per repository, one tracking issue | scheduled (monthly), `workflow_dispatch` | `packages`, `julia_versions`, `dry_run`, `ctactions_ref`; `CT_APP_CLIENT_ID`, `CT_APP_PRIVATE_KEY` | — | | `update-readme.yml` | `UpdateReadme.yml` | Regenerate `README.md` from a template + the org's central `ABOUT/INSTALL/CONTRIBUTING.md` + badges | scheduled (weekly), `workflow_dispatch` | `template_file`, `output_file`, `package_name`, `repo_name`, `doc_url`, `citation_badge`, `assignee` | — | | `auto-assign.yml` | `AutoAssign.yml` | Auto-assign new issues/PRs to a maintainer | issue/PR opened | `assignees`, `numOfAssignee` | — | | `add-to-project.yml` | `AddToProject.yml` | Add new issues/PRs to the org project board (set Status) | issue/PR opened | `project-url`, `status`; `project_token` | — | @@ -334,15 +335,87 @@ jobs: uses: control-toolbox/CTActions/.github/workflows/documentation-preview-cleanup.yml@main ``` +**`[compat]` entries are pruned from the top, once a month.** `compat-helper.yml` runs with +`KeepEntry()`: a new major is added next to the old ones (`JET = "0.9, 0.11, 0.12"`), and +`ci.yml` tests the newest on CompatHelper PRs (`force_latest_compatible_version=auto`, +except on Julia 1.10, CTActions#93). Old specifiers therefore pile up, and none can be +dropped from inside one package: `JET 0.9` is still what Julia 1.10 selects, and +CTSolvers' `CUDSS 0.7` is what OptimalControl's tests select. `compat-prune.yml`, called +monthly by OptimalControl (the top of the graph), decides for the whole ecosystem +(control-toolbox/OptimalControl.jl#986): + +- **What is audited.** The version of each package that the joint resolution of all of + them selects, read from its registered tree (not from a branch), and 17 environments: the + joint one, plus the test environment (`test/Project.toml` or `[targets] test`) and the + docs environment of every package, each on every Julia version of `julia_versions`. + Resolution calls Pkg's resolver only: nothing is downloaded or installed, and one + registry snapshot serves the whole audit (about 4 minutes on a runner). +- **What is pruned.** In an entry with several specifiers, those older than the newest one + in use, that no environment selects on any Julia version, counting only the environments + where the entry applies (where its package is at its audited version, or the very `test/` + or `docs/` environment of the file). Every prune is then simulated, by intersecting the + compat of the environments where it applies, and kept only if all manifests stay + identical. A prune therefore changes nothing that is resolved: the PRs can be merged in + any order and need no release. +- **What is reported.** The tracking issue (hidden marker ``) + lists the prunes per repository with their PR, those merged but not yet released, the + *drifts* (the default branch no longer has the audited line: untouched), the prunes + rejected by the simulation, the *withheld* ones (an environment failed to resolve: nothing + is pruned that run), and the *blocked* entries (the newest specifier is never selected, + with the packages that cap it: information only). +- **Idempotence.** One branch `ct/compat-prune` and one PR per repository, one issue: a + rerun rewrites them in place (the branch is pushed only when its content changes), closes + the PR and deletes the branch of a repository with nothing left, and closes the issue + (reopened, never recreated) when nothing is left at all. The edits are re-applied to the + current default branch, one `[compat]` line at a time, never copied from the audit. +- **Dry run.** `dry_run: true` (the default) runs the read-only `audit` job only: report + and diff in the job summary, everything in the `compat-prune` artifact. The `publish` + job writes with a token of the organization's GitHub App (`CT_APP_CLIENT_ID`, + `CT_APP_PRIVATE_KEY`; permissions: contents, pull requests, issues, on the CT\* + repositories) and is the only one that does. + +`ct-registry` is added over HTTPS (it is public), so no `SSH_KEY` is needed. The scripts +live in `scripts/compat-prune/` and run locally the same way (juliaup channels): + +```bash +julia scripts/compat-prune/audit.jl '["CTBase","CTParser","CTModels","CTLie","CTFlows","CTDirect","CTSolvers","OptimalControl"]' '["1.10","1.12","1.13"]' /tmp/audit +COMPAT_PRUNE_INTEGRATION=1 julia scripts/compat-prune/test/runtests.jl # unit, non-regression, integration +``` + +The caller: + +```yaml +# .github/workflows/CompatPrune.yml (OptimalControl) +on: + schedule: + - cron: '0 4 1 * *' + workflow_dispatch: + inputs: + dry_run: { type: boolean, default: true } +jobs: + call: + uses: control-toolbox/CTActions/.github/workflows/compat-prune.yml@main + with: + dry_run: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run }} + secrets: + CT_APP_CLIENT_ID: ${{ secrets.CT_APP_CLIENT_ID }} + CT_APP_PRIVATE_KEY: ${{ secrets.CT_APP_PRIVATE_KEY }} +``` + +Until the GitHub App is set up, the caller keeps every trigger a dry run +(`dry_run: ${{ github.event_name != 'workflow_dispatch' || inputs.dry_run }}`). + ### 3.2 Maintenance workflows that live *in* `CTActions` (not called) -These run on `CTActions` itself, on a schedule, to keep the **self-hosted runners** -healthy. They are not `workflow_call` reusables. +These run on `CTActions` itself: on a schedule, to keep the **self-hosted runners** +healthy, or on its own PRs, to test the scripts behind a reusable workflow. They are not +`workflow_call` reusables. | Workflow (`CTActions`) | Role | Trigger | | --- | --- | --- | | `occidata-runner-maintenance.yml` | Purge Julia compile cache, update TeXLive, rotate logs on the `occidata` self-hosted runner | weekly cron, `workflow_dispatch` | | `occidata-runner-watchdog.yml` | Over SSH, run `scripts/start_runners.sh` (control-toolbox/occidata), which restarts every Occidata runner instance whose SLURM job is neither pending nor running | daily cron, `workflow_dispatch` | +| `compat-prune-tests.yml` | Self-test of `compat-prune`: the test suite of `scripts/compat-prune` on every audited Julia version, then a real dry-run audit through `compat-prune.yml` | PR or push touching `scripts/compat-prune/` or `compat-prune*.yml`, `workflow_dispatch` | `occidata` started as the target of this maintenance workflow only; since CTFlows.jl's `occidata-runner` job (§3.1, added 2026-08-24) it is also a live GPU CI target for every @@ -428,7 +501,7 @@ Two workflows are **conditional**: `Breakage` and `AddToProject`. | CTFlows.jl | ✅ | ✅ | — | split CI (`github-runner`/`occidata-runner`), `CPUProbe`/`GPUProbe` (`run probe`) | | CTDirect.jl | ✅ | ✅ | ✅ | split CI (`github-runner`/`occidata-runner`, replacing the older dedicated `GPU.yml`); `Formatter` disabled — **stale**, not currently kept up to date with the rest of the fleet | | CTSolvers | ✅ | ✅ | ✅ | split CI (`github-runner`/`kkt-runner`/`occidata-runner`); `Documentation` carries a `build-gpu` upgrade pass on `kkt`; `CPUProbe`/`GPUProbe` (`run probe`, GPU on `kkt`) | -| OptimalControl | ✅ | ✅ | ✅ | `JOSS`, split CI (`github-runner`/`occidata-runner`); `Documentation` also carries an optional `build-gpu` upgrade pass on `occidata` (2026-09-02, control-toolbox/OptimalControl.jl#885) | +| OptimalControl | ✅ | ✅ | ✅ | `JOSS`, `CompatPrune` (monthly, for the whole ecosystem), split CI (`github-runner`/`occidata-runner`); `Documentation` also carries an optional `build-gpu` upgrade pass on `occidata` (2026-09-02, control-toolbox/OptimalControl.jl#885) | | OptimalControlProblems | ✅ | — | — | — | | CTDiffFlow.jl | ✅ | — | — | — | | CTBenchmarks.jl | ✅ | — | — | `benchmark-*` | @@ -495,6 +568,7 @@ runner is the one maintained by the scheduled `CTActions` maintenance workflows | `DOCUMENTER_KEY` | Documentation, CompatHelper, TagBot | Deploy docs / sign tags (Documenter deploy key) | | `CODECOV_TOKEN` → `codecov-secret` | Coverage | Upload coverage to Codecov | | `PROJECT_TOKEN` → `project_token` | AddToProject | Write to the org project board | +| `CT_APP_CLIENT_ID`, `CT_APP_PRIVATE_KEY` | CompatPrune (`publish` job) | Token of the organization's GitHub App: push `ct/compat-prune` and open PRs in the CT\* repositories, keep the tracking issue | | `GITHUB_TOKEN` | (built-in) most | Default repo-scoped token | Use `secrets: inherit` (as `UpdateReadme` does) only when the reusable workflow needs From 4b5aa65d1440c517cf9bc0674d53b6256d2808f0 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 16 Oct 2026 09:06:09 +0200 Subject: [PATCH 7/8] feat(compat-prune): monthly run in CTActions, tracking issue here, audit 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 --- .github/workflows/compat-prune-monthly.yml | 32 ++++ .github/workflows/compat-prune.yml | 17 +- scripts/compat-prune/audit.jl | 163 ++++++++++++------ scripts/compat-prune/report.jl | 30 ++++ scripts/compat-prune/test/golden/report.md | 20 +++ scripts/compat-prune/test/helpers.jl | 2 +- scripts/compat-prune/test/runtests.jl | 1 + scripts/compat-prune/test/test_audit.jl | 48 ++++++ scripts/compat-prune/test/test_integration.jl | 7 + scripts/compat-prune/test/test_report.jl | 34 +++- 10 files changed, 293 insertions(+), 61 deletions(-) create mode 100644 .github/workflows/compat-prune-monthly.yml create mode 100644 scripts/compat-prune/test/test_audit.jl diff --git a/.github/workflows/compat-prune-monthly.yml b/.github/workflows/compat-prune-monthly.yml new file mode 100644 index 0000000..4e8c7b4 --- /dev/null +++ b/.github/workflows/compat-prune-monthly.yml @@ -0,0 +1,32 @@ +name: Compat prune (monthly) + +# The organization's monthly [compat] prune: every CT* package audited together, one PR per +# repository with something to drop, one tracking issue here in CTActions. The logic is the +# reusable compat-prune.yml; see docs/WORKFLOWS.md §3.2. + +on: + schedule: + - cron: '0 4 1 * *' + workflow_dispatch: + inputs: + dry_run: + description: 'Only audit and report: no PR, no issue' + type: boolean + default: true + +concurrency: + group: compat-prune-monthly + cancel-in-progress: false + +jobs: + prune: + uses: ./.github/workflows/compat-prune.yml + with: + # Dry run on every trigger until the GitHub App is set up; then the schedule + # publishes: `github.event_name == 'workflow_dispatch' && inputs.dry_run`. + dry_run: ${{ github.event_name != 'workflow_dispatch' || inputs.dry_run }} + # the scripts of the same commit as this workflow + ctactions_ref: ${{ github.sha }} + secrets: + CT_APP_CLIENT_ID: ${{ secrets.CT_APP_CLIENT_ID }} + CT_APP_PRIVATE_KEY: ${{ secrets.CT_APP_PRIVATE_KEY }} diff --git a/.github/workflows/compat-prune.yml b/.github/workflows/compat-prune.yml index 432c1a0..bee0417 100644 --- a/.github/workflows/compat-prune.yml +++ b/.github/workflows/compat-prune.yml @@ -3,8 +3,9 @@ name: Compat prune # Finds the [compat] specifiers that no supported Julia version ever selects across the # control-toolbox packages, proves that dropping them leaves every resolved manifest # unchanged, and (unless dry_run) opens one PR per affected repository plus one tracking -# issue in the calling repository, both updated in place by later runs. -# Called monthly by OptimalControl (`CompatPrune.yml`); see docs/WORKFLOWS.md §3.1. +# issue (in CTActions by default), both updated in place by later runs. The prune is the +# organization's: it is called monthly by `compat-prune-monthly.yml`, here in CTActions, and +# by the self-test `compat-prune-tests.yml`; see docs/WORKFLOWS.md §3.2. # # The `audit` job is read-only: it resolves the joint, test and docs environments of every # package on every Julia version (Pkg's resolver only: nothing is downloaded or installed), @@ -15,8 +16,8 @@ name: Compat prune # The `publish` job (skipped on dry runs) is the only one that writes, with a token of the # organization's GitHub App (secrets CT_APP_CLIENT_ID, CT_APP_PRIVATE_KEY; permissions: # contents, pull requests, issues). It re-applies the prunes to fresh checkouts, then keeps -# one branch `ct/compat-prune` and one PR per repository, and one tracking issue in the -# calling repository: a rerun updates them in place, and closes what has nothing left. +# one branch `ct/compat-prune` and one PR per repository, and one tracking issue in +# `tracking_repo`: a rerun updates them in place, and closes what has nothing left. on: workflow_call: @@ -41,6 +42,11 @@ on: required: false type: string default: main + tracking_repo: + description: 'Repository of the tracking issue' + required: false + type: string + default: control-toolbox/CTActions secrets: CT_APP_CLIENT_ID: description: 'Client ID of the GitHub App that publishes (not needed for a dry run)' @@ -170,9 +176,10 @@ jobs: - name: Publish env: GH_TOKEN: ${{ steps.app.outputs.token }} + TRACKING_REPO: ${{ inputs.tracking_repo }} run: | gh auth setup-git "$SCRIPTS/checkout.sh" "$OUT" checkouts julia --startup-file=no "$SCRIPTS/edit.jl" "$OUT" checkouts RUN_URL="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \ - julia --startup-file=no "$SCRIPTS/publish.jl" "$OUT" checkouts "$GITHUB_REPOSITORY" | tee -a "$GITHUB_STEP_SUMMARY" + julia --startup-file=no "$SCRIPTS/publish.jl" "$OUT" checkouts "$TRACKING_REPO" | tee -a "$GITHUB_STEP_SUMMARY" diff --git a/scripts/compat-prune/audit.jl b/scripts/compat-prune/audit.jl index fc69e77..e79ecab 100644 --- a/scripts/compat-prune/audit.jl +++ b/scripts/compat-prune/audit.jl @@ -9,71 +9,128 @@ # 4. classify.jl, from all baselines # 5. validate.jl on each Julia version, repeated with the rejected prunes removed until no # version rejects anything new (a smaller set of prunes is re-checked as a whole) -# Output: /audit.toml (accepted prunes, rejected prunes, blockers, other entries), -# next to the intermediate files of every step. +# Output: /audit.toml (accepted prunes, rejected prunes, blockers, other entries, and +# the record of the audit itself: registries, coverage, entries), next to the intermediate +# files of every step. using Dates, Pkg, TOML -const HERE = @__DIR__ -packages, versions, out = ARGS[1], ARGS[2], abspath(ARGS[3]) -include(joinpath(HERE, "common.jl")) # for parse_list -jlv = parse_list(versions) +include(joinpath(@__DIR__, "common.jl")) + short(v) = join(split(v, ".")[1:2], ".") +""" + registry_snapshot() -> Vector{Dict} + +Name and state of every reachable registry: the tree hash of a registry installed from a +tarball (General, through the package server), the commit of one cloned with git. +""" +function registry_snapshot() + out = Dict{String,Any}[] + for reg in Pkg.Registry.reachable_registries() + state = if reg.tree_info !== nothing + "tree $(reg.tree_info)" + elseif isdir(joinpath(reg.path, ".git")) + "commit " * readchomp(`git -C $(reg.path) rev-parse HEAD`) + else + "unknown" + end + push!(out, Dict("name" => reg.name, "state" => state)) + end + return sort(out; by=r -> r["name"]) +end + +""" + coverage(baselines) -> Vector{Dict} + +For each Julia version (`julia` => parsed baseline), how many environments resolved, and +which failed. +""" +coverage(baselines) = [Dict("julia" => j, "resolved" => count(r -> r["ok"], values(b["env"])), "total" => length(b["env"]), + "failed" => sort([n for (n, r) in b["env"] if !r["ok"]])) + for (j, b) in sort(collect(baselines); by=first)] + +""" + summarize(; date, julia, index, classified, rejected, blocked, registries, coverage) -> Dict + +The content of audit.toml: the accepted prunes (classified minus rejected), the rejected +ones, the blocker probes, the other entries (keep, idle, withheld), and the record of the +audit: registries, coverage, and how many multi-specifier entries were examined, with one +status each. +""" +function summarize(; date, julia, index, classified, rejected, blocked, registries, coverage) + gone = Set(entry_key.(rejected)) + prunes = [p for p in classified["prune"] if !(entry_key(p) in gone)] + others = [r for r in classified["other"] if r["status"] != "blocked"] + # one status per entry: a blocked entry may also carry a prune of its obsolete lanes + status = Dict{Any,String}() + for p in classified["prune"] + status[entry_key(p)] = entry_key(p) in gone ? "rejected" : "prune" + end + for r in classified["other"]; status[entry_key(r)] = r["status"]; end + entries = Dict{String,Any}(s => count(==(s), values(status)) for s in ("prune", "rejected", "keep", "blocked", "idle", "withheld")) + entries["examined"] = length(status) + return Dict{String,Any}( + "date" => date, "julia" => julia, "packages" => index["packages"], + "prune" => prunes, "rejected" => rejected, "blocked" => blocked, "other" => others, + "record" => Dict{String,Any}("registries" => registries, "coverage" => coverage, "entries" => entries), + ) +end + function run_step(v, script, args...) - cmd = `julia +$v --startup-file=no $(joinpath(HERE, script)) $args` println("\n\$ julia +$v $script ", join(args, " ")) - run(cmd) + run(`julia +$v --startup-file=no $(joinpath(@__DIR__, script)) $args`) end -Pkg.Registry.update() -mkpath(out) +if abspath(PROGRAM_FILE) == @__FILE__ + packages, versions, out = ARGS[1], ARGS[2], abspath(ARGS[3]) + jlv = parse_list(versions) -run_step(last(jlv), "collect.jl", packages, out) -run_step(last(jlv), "envs.jl", out) -for v in jlv - run_step(v, "resolve.jl", out, joinpath(out, "baseline-$(short(v)).toml")) -end -run_step(last(jlv), "classify.jl", out, joinpath(out, "classified.toml"), - (joinpath(out, "baseline-$(short(v)).toml") for v in jlv)...) + Pkg.Registry.update() + registries = registry_snapshot() + mkpath(out) -rejected = Dict{String,Any}[] -blocked = Dict{String,Any}[] -for round in 1:5 - open(io -> TOML.print(io, Dict("rejected" => rejected)), joinpath(out, "rejected.toml"), "w") - new = Dict{String,Any}[] - empty!(blocked) + run_step(last(jlv), "collect.jl", packages, out) + run_step(last(jlv), "envs.jl", out) for v in jlv - res = joinpath(out, "validated-$(short(v)).toml") - blockers = v == last(jlv) ? ("--blockers",) : () - run_step(v, "validate.jl", out, joinpath(out, "classified.toml"), - joinpath(out, "baseline-$(short(v)).toml"), res, joinpath(out, "rejected.toml"), blockers...) - r = TOML.parsefile(res) - append!(new, r["rejected"]) - append!(blocked, r["blocked"]) + run_step(v, "resolve.jl", out, joinpath(out, "baseline-$(short(v)).toml")) end - known = Set(entry_key.(rejected)) - fresh = [p for p in new if !(entry_key(p) in known)] - isempty(fresh) && break - round == 5 && error("validation did not settle after 5 rounds") - # one record per prune, the first reason found - for p in fresh - entry_key(p) in known || (push!(rejected, p); push!(known, entry_key(p))) + run_step(last(jlv), "classify.jl", out, joinpath(out, "classified.toml"), + (joinpath(out, "baseline-$(short(v)).toml") for v in jlv)...) + + rejected = Dict{String,Any}[] + blocked = Dict{String,Any}[] + for round in 1:5 + open(io -> TOML.print(io, Dict("rejected" => rejected)), joinpath(out, "rejected.toml"), "w") + new = Dict{String,Any}[] + empty!(blocked) + for v in jlv + res = joinpath(out, "validated-$(short(v)).toml") + blockers = v == last(jlv) ? ("--blockers",) : () + run_step(v, "validate.jl", out, joinpath(out, "classified.toml"), + joinpath(out, "baseline-$(short(v)).toml"), res, joinpath(out, "rejected.toml"), blockers...) + r = TOML.parsefile(res) + append!(new, r["rejected"]) + append!(blocked, r["blocked"]) + end + known = Set(entry_key.(rejected)) + fresh = [p for p in new if !(entry_key(p) in known)] + isempty(fresh) && break + round == 5 && error("validation did not settle after 5 rounds") + # one record per prune, the first reason found + for p in fresh + entry_key(p) in known || (push!(rejected, p); push!(known, entry_key(p))) + end end -end -c = TOML.parsefile(joinpath(out, "classified.toml")) -gone = Set(entry_key.(rejected)) -index = TOML.parsefile(joinpath(out, "index.toml")) -audit = Dict( - "date" => Dates.format(now(UTC), "yyyy-mm-dd HH:MM") * " UTC", - "julia" => jlv, - "packages" => index["packages"], - "prune" => [p for p in c["prune"] if !(entry_key(p) in gone)], - "rejected" => rejected, - "blocked" => blocked, - "other" => [r for r in c["other"] if r["status"] != "blocked"], -) -open(io -> TOML.print(io, audit), joinpath(out, "audit.toml"), "w") -println("\n", length(audit["prune"]), " prune(s) accepted, ", length(rejected), " rejected, ", - length(unique(entry_key.(blocked))), " blocked entr(y/ies)") + baselines = Dict(short(v) => TOML.parsefile(joinpath(out, "baseline-$(short(v)).toml")) for v in jlv) + audit = summarize(; date=Dates.format(now(UTC), "yyyy-mm-dd HH:MM") * " UTC", julia=jlv, + index=TOML.parsefile(joinpath(out, "index.toml")), + classified=TOML.parsefile(joinpath(out, "classified.toml")), + rejected, blocked, registries, coverage=coverage(baselines)) + open(io -> TOML.print(io, audit), joinpath(out, "audit.toml"), "w") + e = audit["record"]["entries"] + println("\n", e["examined"], " entries examined: ", length(audit["prune"]), " prune(s) accepted, ", + length(rejected), " rejected, ", e["blocked"], " blocked, ", e["keep"], " kept, ", e["idle"], " idle, ", + e["withheld"], " withheld") +end diff --git a/scripts/compat-prune/report.jl b/scripts/compat-prune/report.jl index 3496da3..e85680a 100644 --- a/scripts/compat-prune/report.jl +++ b/scripts/compat-prune/report.jl @@ -29,6 +29,34 @@ end code(s) = "`$s`" entry(r) = "$(r["owner"]) $(code(r["file"])) $(code(r["dep"]))" +"Short form of a registry state: `tree 0123456789ab`." +short_state(s) = (m = match(r"^(\w+) ([0-9a-f]{12})[0-9a-f]*$", s); m === nothing ? s : "$(m[1]) $(m[2])") + +""" + render_record(record, others) -> String + +The `## Audit` section: environments resolved per Julia version, entries examined and their +status, registry snapshot, and (folded) the entries left as they are, with why. +""" +function render_record(record, others) + cov = [[c["julia"], "$(c["resolved"]) / $(c["total"])" * (isempty(c["failed"]) ? "" : " (failed: $(join(c["failed"], ", ")))")] + for c in record["coverage"]] + e = record["entries"] + entries = "$(e["examined"]) multi-specifier `[compat]` entries examined: $(e["prune"]) with lanes to prune, " * + "$(e["rejected"]) rejected, $(e["blocked"]) blocked, $(e["keep"]) kept (every lane in use), " * + "$(e["idle"]) idle (resolved nowhere they apply), $(e["withheld"]) withheld." + regs = "Registries: " * join(["$(r["name"]) ($(short_state(r["state"])))" for r in record["registries"]], ", ") * "." + left = sort([r for r in others if r["status"] in ("keep", "idle")]; by=entry_key) + parts = ["## Audit", table(["Julia", "environments resolved"], cov), entries, regs] + if !isempty(left) + why(r) = r["status"] == "keep" ? "every lane in use: " * selected(r["evidence"]) : "not resolved where the entry applies" + push!(parts, "
Entries left as they are ($(length(left)))\n\n" * + table(["entry", "compat", "status", "why"], [[entry(r), code(r["before"]), r["status"], why(r)] for r in left]) * + "\n\n
") + end + return join(parts, "\n\n") +end + """ render_report(audit, edits; prs=Dict(), run_url="") -> String @@ -53,6 +81,8 @@ function render_report(audit, edits; prs=Dict(), run_url="") leaves every resolved manifest (joint, tests and docs of every package) unchanged. PRs can therefore be merged in any order, and need no release of their own.""") + haskey(audit, "record") && push!(out, render_record(audit["record"], get(audit, "other", []))) + push!(out, "## Summary\n\n" * table(["", "entries"], [ ["to prune (open PRs)", string(length(edited))], ["merged, awaiting release", string(length(already))], diff --git a/scripts/compat-prune/test/golden/report.md b/scripts/compat-prune/test/golden/report.md index c24b53f..a767a9f 100644 --- a/scripts/compat-prune/test/golden/report.md +++ b/scripts/compat-prune/test/golden/report.md @@ -7,6 +7,26 @@ A prune drops `[compat]` specifiers that no environment selects, on no Julia ver leaves every resolved manifest (joint, tests and docs of every package) unchanged. PRs can therefore be merged in any order, and need no release of their own. +## Audit + +| Julia | environments resolved | +| --- | --- | +| 1.10 | 3 / 4 (failed: joint) | +| 1.12 | 4 / 4 | + +9 multi-specifier `[compat]` entries examined: 4 with lanes to prune, 1 rejected, 2 blocked, 1 kept (every lane in use), 1 idle (resolved nowhere they apply), 1 withheld. + +Registries: General (tree aff17d8a1e3e), ct-registry (commit 0123456789ab). + +
Entries left as they are (2) + +| entry | compat | status | why | +| --- | --- | --- | --- | +| Alpha `test/Project.toml` `Gpu` | `5, 6` | idle | not resolved where the entry applies | +| Beta `Project.toml` `Dep3` | `0.9, 1` | keep | every lane in use: 1.2.0 on 1.12; 0.9.4 on 1.10 | + +
+ ## Summary | | entries | diff --git a/scripts/compat-prune/test/helpers.jl b/scripts/compat-prune/test/helpers.jl index 781eb44..cb19db7 100644 --- a/scripts/compat-prune/test/helpers.jl +++ b/scripts/compat-prune/test/helpers.jl @@ -2,7 +2,7 @@ # fixture paths, and small builders. const SCRIPTS = joinpath(@__DIR__, "..") -for f in ("collect.jl", "envs.jl", "resolve.jl", "classify.jl", "validate.jl", "edit.jl", "report.jl") +for f in ("collect.jl", "envs.jl", "resolve.jl", "classify.jl", "validate.jl", "edit.jl", "report.jl", "audit.jl") include(joinpath(SCRIPTS, f)) end diff --git a/scripts/compat-prune/test/runtests.jl b/scripts/compat-prune/test/runtests.jl index fe43782..efe63d3 100644 --- a/scripts/compat-prune/test/runtests.jl +++ b/scripts/compat-prune/test/runtests.jl @@ -19,6 +19,7 @@ include("helpers.jl") include("test_envs.jl") include("test_classify.jl") include("test_validate.jl") + include("test_audit.jl") include("test_edit.jl") include("test_report.jl") include("test_publish.jl") diff --git a/scripts/compat-prune/test/test_audit.jl b/scripts/compat-prune/test/test_audit.jl new file mode 100644 index 0000000..ab52963 --- /dev/null +++ b/scripts/compat-prune/test/test_audit.jl @@ -0,0 +1,48 @@ +# audit.jl: coverage of the baselines, and the record summarizing an audit. + +@testset "audit" begin + @testset "coverage" begin + baselines = Dict( + "1.12" => Dict("env" => Dict("joint" => ok(), "A/test" => ok(), "A/docs" => ok())), + "1.10" => Dict("env" => Dict("joint" => ok(), "A/test" => failed(), "A/docs" => failed())), + ) + c = coverage(baselines) + @test [x["julia"] for x in c] == ["1.10", "1.12"] + @test c[1]["resolved"] == 1 && c[1]["total"] == 3 && c[1]["failed"] == ["A/docs", "A/test"] + @test c[2]["resolved"] == 3 && isempty(c[2]["failed"]) + end + + @testset "summarize" begin + p(owner, dep, status="prune") = merge(prune(owner, "Project.toml", dep, "1, 2", "2"), Dict("status" => status)) + classified = Dict( + "prune" => [p("A", "D1"), p("A", "D2"), p("B", "D3"), + p("B", "D5")], # obsolete lane of a blocked entry + "other" => [merge(p("B", "D4"), Dict("status" => "keep")), + merge(p("B", "D5"), Dict("status" => "blocked", "newest" => "3")), + merge(p("B", "D6"), Dict("status" => "idle")), + merge(p("A", "D7"), Dict("status" => "withheld"))], + ) + rejected = [Dict("owner" => "A", "file" => "Project.toml", "dep" => "D2", "where" => "joint@1.12", "reason" => "x")] + blocked = [Dict("owner" => "B", "file" => "Project.toml", "dep" => "D5", "newest" => "3", "where" => "joint@1.12", + "blockers" => ["Z"], "detail" => "")] + a = summarize(; date="2026-11-01 04:00 UTC", julia=["1.10", "1.12"], index=Dict("packages" => Dict()), + classified, rejected, blocked, registries=[Dict("name" => "General", "state" => "tree abc")], + coverage=[Dict("julia" => "1.12", "resolved" => 2, "total" => 2, "failed" => String[])]) + # accepted prunes: classified minus rejected, the blocked entry's obsolete lane included + @test sort(entry_key.(a["prune"])) == [("A", "Project.toml", "D1"), ("B", "Project.toml", "D3"), ("B", "Project.toml", "D5")] + @test a["rejected"] == rejected && a["blocked"] == blocked + # blocked records live in `blocked` (probes), not in `other` + @test sort([r["status"] for r in a["other"]]) == ["idle", "keep", "withheld"] + # one status per entry + @test a["record"]["entries"] == Dict("examined" => 7, "prune" => 2, "rejected" => 1, "keep" => 1, + "blocked" => 1, "idle" => 1, "withheld" => 1) + @test a["record"]["registries"] == [Dict("name" => "General", "state" => "tree abc")] + @test only(a["record"]["coverage"])["resolved"] == 2 + # the record survives a TOML round trip + mktempdir() do dir + path = joinpath(dir, "audit.toml") + open(io -> TOML.print(io, a), path, "w") + @test TOML.parsefile(path)["record"] == a["record"] + end + end +end diff --git a/scripts/compat-prune/test/test_integration.jl b/scripts/compat-prune/test/test_integration.jl index b406b9b..afa470d 100644 --- a/scripts/compat-prune/test/test_integration.jl +++ b/scripts/compat-prune/test/test_integration.jl @@ -62,6 +62,13 @@ run_script(script, args...) = read(`$JULIA --startup-file=no $(joinpath(SCRIPTS, @test_throws Exception resolve_env(Dict("Example" => EXAMPLE), Dict("Example" => "not a version")) end + @testset "registry_snapshot" begin + snap = registry_snapshot() + @test any(r -> r["name"] == "General", snap) + @test issorted([r["name"] for r in snap]) + @test all(r -> occursin(r"^(tree|commit) [0-9a-f]{40}$|^unknown$", r["state"]), snap) + end + @testset "registry_lookup" begin @test registry_lookup("Example") == (EXAMPLE, "JuliaLang/Example.jl") @test_throws ErrorException registry_lookup("NoSuchPackageForCompatPrune") diff --git a/scripts/compat-prune/test/test_report.jl b/scripts/compat-prune/test/test_report.jl index a28b5c7..0b439ba 100644 --- a/scripts/compat-prune/test/test_report.jl +++ b/scripts/compat-prune/test/test_report.jl @@ -34,7 +34,17 @@ function report_fixture() ], "other" => [Dict("owner" => "Alpha", "file" => "Project.toml", "dep" => "Dep2", "before" => "1, 2", "removed" => ["1"], "status" => "withheld", - "reason" => "baseline failed in joint@1.10: lanes used there are unknown")], + "reason" => "baseline failed in joint@1.10: lanes used there are unknown"), + Dict("owner" => "Beta", "file" => "Project.toml", "dep" => "Dep3", "before" => "0.9, 1", "status" => "keep", + "evidence" => ev("0.9" => ["joint@1.10=0.9.4"], "1" => ["joint@1.12=1.2.0"])), + Dict("owner" => "Alpha", "file" => "test/Project.toml", "dep" => "Gpu", "before" => "5, 6", "status" => "idle", + "evidence" => ev())], + "record" => Dict( + "registries" => [Dict("name" => "General", "state" => "tree aff17d8a1e3ecb7cf9a37f2f40543c57577276fd"), + Dict("name" => "ct-registry", "state" => "commit 0123456789abcdef0123456789abcdef01234567")], + "coverage" => [Dict("julia" => "1.10", "resolved" => 3, "total" => 4, "failed" => ["joint"]), + Dict("julia" => "1.12", "resolved" => 4, "total" => 4, "failed" => String[])], + "entries" => Dict("examined" => 9, "prune" => 4, "rejected" => 1, "keep" => 1, "blocked" => 2, "idle" => 1, "withheld" => 1)), ) return audit, vcat(edited, already, drift) end @@ -50,7 +60,7 @@ end @testset "marker and sections" begin @test startswith(body, MARKER * "\n") - for h in ("## Summary", "## To prune", "### Alpha — https://github.com/example-org/Alpha.jl/pull/12", + for h in ("## Audit", "## Summary", "## To prune", "### Alpha — https://github.com/example-org/Alpha.jl/pull/12", "### Beta", "## Merged, awaiting release", "## Drift", "## Rejected", "## Withheld", "## Blocked", "## Audited versions") @test occursin("\n" * h * "\n", body) @@ -64,6 +74,21 @@ end @test occursin("| blocked (information) | 2 |", body) end + @testset "audit record" begin + @test occursin("| 1.10 | 3 / 4 (failed: joint) |", body) + @test occursin("| 1.12 | 4 / 4 |", body) + @test occursin("9 multi-specifier `[compat]` entries examined: 4 with lanes to prune, 1 rejected, 2 blocked, " * + "1 kept (every lane in use), 1 idle (resolved nowhere they apply), 1 withheld.", body) + @test occursin("Registries: General (tree aff17d8a1e3e), ct-registry (commit 0123456789ab).", body) + @test occursin("Entries left as they are (2)", body) + @test occursin("| Beta `Project.toml` `Dep3` | `0.9, 1` | keep | every lane in use: 1.2.0 on 1.12; 0.9.4 on 1.10 |", body) + @test occursin("| Alpha `test/Project.toml` `Gpu` | `5, 6` | idle | not resolved where the entry applies |", body) + # the section comes before the summary + @test findfirst("## Audit\n", body).start < findfirst("## Summary", body).start + @test short_state("tree aff17d8a1e3ecb7cf9a37f2f40543c57577276fd") == "tree aff17d8a1e3e" + @test short_state("unknown") == "unknown" + end + @testset "deterministic" begin shuffled = deepcopy(audit) reverse!(shuffled["blocked"]) @@ -73,7 +98,12 @@ end @testset "empty sections are left out" begin quiet = Dict{String,Any}("date" => "2026-11-01 03:00 UTC", "julia" => ["1.10"], "packages" => audit["packages"], "prune" => [], "rejected" => [], "blocked" => [], "other" => []) + # without a record (older audit.toml): no Audit section + @test !occursin("## Audit\n", render_report(quiet, [])) + quiet["record"] = Dict("registries" => [], "coverage" => [], "entries" => Dict( + "examined" => 0, "prune" => 0, "rejected" => 0, "keep" => 0, "blocked" => 0, "idle" => 0, "withheld" => 0)) b = render_report(quiet, []) + @test occursin("## Audit\n", b) && !occursin("
", b) @test occursin("| to prune (open PRs) | 0 |", b) for h in ("## To prune", "## Merged", "## Drift", "## Rejected", "## Withheld", "## Blocked") @test !occursin(h, b) From bd46094d7884117862acea806d54b40541d6f53e Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 16 Oct 2026 09:06:09 +0200 Subject: [PATCH 8/8] docs(workflows): compat-prune is the organization's, run from CTActions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- README.md | 2 +- docs/WORKFLOWS.md | 132 ++++++++++++++++++++++++---------------------- 2 files changed, 70 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index ce7fd0c..cf0dd40 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,6 @@ repository and for org-wide updates — is described in | `documentation.yml` | Build and deploy documentation with Documenter.jl | `use_ct_registry` | | `formatter.yml` | Run JuliaFormatter (BlueStyle) and open a PR if needed | — | | `compat-helper.yml` | Keep `[compat]` entries in `Project.toml` up to date | `subdirs` | -| `compat-prune.yml` | Drop the `[compat]` specifiers no supported Julia version selects, across all CT\* packages (one PR per repository, one tracking issue); scripts and tests in `scripts/compat-prune/` | `packages`, `julia_versions`, `dry_run` | | `spell-check.yml` | Spell check with `typos` | `locale`, `extend-identifiers`, `config-path` | | `breakage.yml` | Test downstream packages against a new version; comment results on PR | `pkgname`, `pkgpath`, `pkgversion`, `pkgbreak` | | `add-to-project.yml` | Auto-add issues/PRs to the org GitHub project | `project-url`, `status` | @@ -35,6 +34,7 @@ repository and for org-wide updates — is described in |---|---|---| | `occidata-runner-maintenance.yml` | Weekly (Mon 02:30 UTC) | Purge Julia cache, update TeXLive, rotate logs on self-hosted runner | | `occidata-runner-watchdog.yml` | Daily (03:15 UTC) | Restart, through SSH, the Occidata runner jobs (one per organization) that are neither pending nor running | +| `compat-prune-monthly.yml` | Monthly (1st, 04:00 UTC) | Drop the `[compat]` specifiers no supported Julia version selects, across all CT\* packages: one PR per repository, one tracking issue here (logic: reusable `compat-prune.yml`, scripts and tests in `scripts/compat-prune/`) | The Occidata watchdog requires these CTActions repository or organization secrets: diff --git a/docs/WORKFLOWS.md b/docs/WORKFLOWS.md index c1ab8d9..d04b62e 100644 --- a/docs/WORKFLOWS.md +++ b/docs/WORKFLOWS.md @@ -223,7 +223,6 @@ few **non-centralized / special** workflows (defined directly in a package). | `formatter.yml` | `Formatter.yml` | Run JuliaFormatter (BlueStyle) and open an auto PR | scheduled (nightly), `workflow_dispatch` | — | — | | `spell-check.yml` | `SpellCheck.yml` | Spell-check with `crate-ci/typos` | PR, `workflow_dispatch` | `locale`, `extend-identifiers`, `config-path` | — | | `compat-helper.yml` | `CompatHelper.yml` | Open PRs bumping `[compat]` bounds | scheduled (daily), `workflow_dispatch` | `subdirs`; `GITHUB_TOKEN`, `DOCUMENTER_KEY` | — | -| `compat-prune.yml` | `CompatPrune.yml` (OptimalControl only) | Drop the `[compat]` specifiers no supported Julia version selects, across all CT\* packages: one PR per repository, one tracking issue | scheduled (monthly), `workflow_dispatch` | `packages`, `julia_versions`, `dry_run`, `ctactions_ref`; `CT_APP_CLIENT_ID`, `CT_APP_PRIVATE_KEY` | — | | `update-readme.yml` | `UpdateReadme.yml` | Regenerate `README.md` from a template + the org's central `ABOUT/INSTALL/CONTRIBUTING.md` + badges | scheduled (weekly), `workflow_dispatch` | `template_file`, `output_file`, `package_name`, `repo_name`, `doc_url`, `citation_badge`, `assignee` | — | | `auto-assign.yml` | `AutoAssign.yml` | Auto-assign new issues/PRs to a maintainer | issue/PR opened | `assignees`, `numOfAssignee` | — | | `add-to-project.yml` | `AddToProject.yml` | Add new issues/PRs to the org project board (set Status) | issue/PR opened | `project-url`, `status`; `project_token` | — | @@ -335,14 +334,67 @@ jobs: uses: control-toolbox/CTActions/.github/workflows/documentation-preview-cleanup.yml@main ``` -**`[compat]` entries are pruned from the top, once a month.** `compat-helper.yml` runs with -`KeepEntry()`: a new major is added next to the old ones (`JET = "0.9, 0.11, 0.12"`), and +### 3.2 Maintenance workflows that live *in* `CTActions` (not called) + +These run on `CTActions` itself: on a schedule, to keep the **self-hosted runners** +healthy or to act on the whole organization, or on its own PRs, to test the scripts behind +a workflow. No package calls them. + +| Workflow (`CTActions`) | Role | Trigger | +| --- | --- | --- | +| `occidata-runner-maintenance.yml` | Purge Julia compile cache, update TeXLive, rotate logs on the `occidata` self-hosted runner | weekly cron, `workflow_dispatch` | +| `occidata-runner-watchdog.yml` | Over SSH, run `scripts/start_runners.sh` (control-toolbox/occidata), which restarts every Occidata runner instance whose SLURM job is neither pending nor running | daily cron, `workflow_dispatch` | +| `compat-prune-monthly.yml` | The organization's `[compat]` prune: runs the reusable `compat-prune.yml` on every CT\* package; one PR per repository with something to drop, one tracking issue in CTActions | monthly cron (1st, 04:00 UTC), `workflow_dispatch` (`dry_run`) | +| `compat-prune-tests.yml` | Self-test of `compat-prune`: the test suite of `scripts/compat-prune` on every audited Julia version, then a real dry-run audit through `compat-prune.yml` | PR or push touching `scripts/compat-prune/` or `compat-prune*.yml`, `workflow_dispatch` | + +`occidata` started as the target of the maintenance workflow only; since CTFlows.jl's +`occidata-runner` job (§3.1, added 2026-08-24) it is also a live GPU CI target for every +package's test matrix that needs one, and (2026-09-02, control-toolbox/OptimalControl.jl#885) +a documentation-build target too — not just a runner kept healthy for its own sake. Two +quirks of the node worth knowing for either role: the node's `/tmp` is wiped every hour at +`HH:01` (`ci.yml`'s `TMPDIR` redirect on self-hosted jobs works around this, commit 2f00b67 — +any long-running job, docs builds included, needs the same), and this workflow's own Monday +02:30 UTC purge of the compiled-code cache means the *first* job after it runs cold. A job +landing right after either is slower or needs a retry — not a sign the runner is unhealthy. + +**The `occidata` runner has no GPU of its own: it gets one on demand.** Since 2026-09-28 the +runner idles in a CPU SLURM allocation (`24CPUNodes`) instead of holding a GTX 1080 Ti for +7 days. A step that needs CUDA prefixes its command with `$OCCIDATA_GPU_RUN`, which the runner +exports (control-toolbox/occidata, `scripts/slurm_gpu_run.sh`). That script submits the command +to `GPUNodes` with `sbatch --wait` for the step's duration only. It keeps the step's +environment (including `RUNNER_NAME`), streams its output, propagates its exit status, and +cancels the SLURM job when the workflow is cancelled: + +```yaml +- run: ${OCCIDATA_GPU_RUN:-} julia --project=... script.jl # plain `julia ...` on any other runner +``` + +`ci.yml` (through `julia-runtest`'s `prefix`) and `documentation.yml`'s `build-gpu` already do +this (CTActions#79), so their callers need nothing. **A workflow with its own steps must add +the prefix itself** (like CTFlows.jl's `GPUProbe.yml`). Without it, the step runs on the CPU +node, and the test suites' "`RUNNER_NAME` contains `occidata` ⇒ `CUDA.functional()` is +required" contract fails. Resources can be tuned per step with `OCCIDATA_GPU_PARTITION`, +`OCCIDATA_GPU_GRES`, `OCCIDATA_GPU_CPUS`, `OCCIDATA_GPU_MEM` and `OCCIDATA_GPU_TIME` (a bare +number is read as minutes). The label `gpu` on the runner therefore means "can get a GPU", not +"runs on one". Keep an eye on precompilation: it happens inside the prefixed command, that +is, while the GPU is held. + +Occidata also hosts a second runner instance, `occidata-cpu`, for the `ocourses` +organization. It runs in `cpu` mode: no `$OCCIDATA_GPU_RUN`, so no GPU ever. Instances, +their modes and how to add one are documented in control-toolbox/occidata, +`docs/gha-runner-config.md` § 9. + +**`[compat]` entries are pruned for the whole organization, once a month.** +`compat-helper.yml` runs with `KeepEntry()`: a new major is added next to the old ones +(`JET = "0.9, 0.11, 0.12"`), and `ci.yml` tests the newest on CompatHelper PRs (`force_latest_compatible_version=auto`, except on Julia 1.10, CTActions#93). Old specifiers therefore pile up, and none can be dropped from inside one package: `JET 0.9` is still what Julia 1.10 selects, and -CTSolvers' `CUDSS 0.7` is what OptimalControl's tests select. `compat-prune.yml`, called -monthly by OptimalControl (the top of the graph), decides for the whole ecosystem -(control-toolbox/OptimalControl.jl#986): +CTSolvers' `CUDSS 0.7` is what OptimalControl's tests select. The prune is therefore the +organization's, decided for all CT\* packages at once (control-toolbox/OptimalControl.jl#986), +and lives here: `compat-prune-monthly.yml` runs the reusable `compat-prune.yml` (also run +by the self-test) on the 1st of every month, and keeps its tracking issue in CTActions +(input `tracking_repo`). No package carries a caller. - **What is audited.** The version of each package that the joint resolution of all of them selects, read from its registered tree (not from a branch), and 17 environments: the @@ -358,7 +410,9 @@ monthly by OptimalControl (the top of the graph), decides for the whole ecosyste identical. A prune therefore changes nothing that is resolved: the PRs can be merged in any order and need no release. - **What is reported.** The tracking issue (hidden marker ``) - lists the prunes per repository with their PR, those merged but not yet released, the + opens with the record of the audit (environments resolved per Julia version, entries + examined by status, registry snapshot, and, folded, the entries kept or idle with why), + then lists the prunes per repository with their PR, those merged but not yet released, the *drifts* (the default branch no longer has the audited line: untouched), the prunes rejected by the simulation, the *withheld* ones (an environment failed to resolve: nothing is pruned that run), and the *blocked* entries (the newest specifier is never selected, @@ -382,10 +436,10 @@ julia scripts/compat-prune/audit.jl '["CTBase","CTParser","CTModels","CTLie","CT COMPAT_PRUNE_INTEGRATION=1 julia scripts/compat-prune/test/runtests.jl # unit, non-regression, integration ``` -The caller: +The monthly workflow: ```yaml -# .github/workflows/CompatPrune.yml (OptimalControl) +# .github/workflows/compat-prune-monthly.yml (CTActions) on: schedule: - cron: '0 4 1 * *' @@ -393,67 +447,19 @@ on: inputs: dry_run: { type: boolean, default: true } jobs: - call: - uses: control-toolbox/CTActions/.github/workflows/compat-prune.yml@main + prune: + uses: ./.github/workflows/compat-prune.yml with: dry_run: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run }} + ctactions_ref: ${{ github.sha }} secrets: CT_APP_CLIENT_ID: ${{ secrets.CT_APP_CLIENT_ID }} CT_APP_PRIVATE_KEY: ${{ secrets.CT_APP_PRIVATE_KEY }} ``` -Until the GitHub App is set up, the caller keeps every trigger a dry run +Until the GitHub App is set up, every trigger is a dry run (`dry_run: ${{ github.event_name != 'workflow_dispatch' || inputs.dry_run }}`). -### 3.2 Maintenance workflows that live *in* `CTActions` (not called) - -These run on `CTActions` itself: on a schedule, to keep the **self-hosted runners** -healthy, or on its own PRs, to test the scripts behind a reusable workflow. They are not -`workflow_call` reusables. - -| Workflow (`CTActions`) | Role | Trigger | -| --- | --- | --- | -| `occidata-runner-maintenance.yml` | Purge Julia compile cache, update TeXLive, rotate logs on the `occidata` self-hosted runner | weekly cron, `workflow_dispatch` | -| `occidata-runner-watchdog.yml` | Over SSH, run `scripts/start_runners.sh` (control-toolbox/occidata), which restarts every Occidata runner instance whose SLURM job is neither pending nor running | daily cron, `workflow_dispatch` | -| `compat-prune-tests.yml` | Self-test of `compat-prune`: the test suite of `scripts/compat-prune` on every audited Julia version, then a real dry-run audit through `compat-prune.yml` | PR or push touching `scripts/compat-prune/` or `compat-prune*.yml`, `workflow_dispatch` | - -`occidata` started as the target of this maintenance workflow only; since CTFlows.jl's -`occidata-runner` job (§3.1, added 2026-08-24) it is also a live GPU CI target for every -package's test matrix that needs one, and (2026-09-02, control-toolbox/OptimalControl.jl#885) -a documentation-build target too — not just a runner kept healthy for its own sake. Two -quirks of the node worth knowing for either role: the node's `/tmp` is wiped every hour at -`HH:01` (`ci.yml`'s `TMPDIR` redirect on self-hosted jobs works around this, commit 2f00b67 — -any long-running job, docs builds included, needs the same), and this workflow's own Monday -02:30 UTC purge of the compiled-code cache means the *first* job after it runs cold. A job -landing right after either is slower or needs a retry — not a sign the runner is unhealthy. - -**The `occidata` runner has no GPU of its own: it gets one on demand.** Since 2026-09-28 the -runner idles in a CPU SLURM allocation (`24CPUNodes`) instead of holding a GTX 1080 Ti for -7 days. A step that needs CUDA prefixes its command with `$OCCIDATA_GPU_RUN`, which the runner -exports (control-toolbox/occidata, `scripts/slurm_gpu_run.sh`). That script submits the command -to `GPUNodes` with `sbatch --wait` for the step's duration only. It keeps the step's -environment (including `RUNNER_NAME`), streams its output, propagates its exit status, and -cancels the SLURM job when the workflow is cancelled: - -```yaml -- run: ${OCCIDATA_GPU_RUN:-} julia --project=... script.jl # plain `julia ...` on any other runner -``` - -`ci.yml` (through `julia-runtest`'s `prefix`) and `documentation.yml`'s `build-gpu` already do -this (CTActions#79), so their callers need nothing. **A workflow with its own steps must add -the prefix itself** (like CTFlows.jl's `GPUProbe.yml`). Without it, the step runs on the CPU -node, and the test suites' "`RUNNER_NAME` contains `occidata` ⇒ `CUDA.functional()` is -required" contract fails. Resources can be tuned per step with `OCCIDATA_GPU_PARTITION`, -`OCCIDATA_GPU_GRES`, `OCCIDATA_GPU_CPUS`, `OCCIDATA_GPU_MEM` and `OCCIDATA_GPU_TIME` (a bare -number is read as minutes). The label `gpu` on the runner therefore means "can get a GPU", not -"runs on one". Keep an eye on precompilation: it happens inside the prefixed command, that -is, while the GPU is held. - -Occidata also hosts a second runner instance, `occidata-cpu`, for the `ocourses` -organization. It runs in `cpu` mode: no `$OCCIDATA_GPU_RUN`, so no GPU ever. Instances, -their modes and how to add one are documented in control-toolbox/occidata, -`docs/gha-runner-config.md` § 9. - ### 3.3 Non-centralized / per-repo special workflows | Workflow | Where | Why it isn't centralized | @@ -501,7 +507,7 @@ Two workflows are **conditional**: `Breakage` and `AddToProject`. | CTFlows.jl | ✅ | ✅ | — | split CI (`github-runner`/`occidata-runner`), `CPUProbe`/`GPUProbe` (`run probe`) | | CTDirect.jl | ✅ | ✅ | ✅ | split CI (`github-runner`/`occidata-runner`, replacing the older dedicated `GPU.yml`); `Formatter` disabled — **stale**, not currently kept up to date with the rest of the fleet | | CTSolvers | ✅ | ✅ | ✅ | split CI (`github-runner`/`kkt-runner`/`occidata-runner`); `Documentation` carries a `build-gpu` upgrade pass on `kkt`; `CPUProbe`/`GPUProbe` (`run probe`, GPU on `kkt`) | -| OptimalControl | ✅ | ✅ | ✅ | `JOSS`, `CompatPrune` (monthly, for the whole ecosystem), split CI (`github-runner`/`occidata-runner`); `Documentation` also carries an optional `build-gpu` upgrade pass on `occidata` (2026-09-02, control-toolbox/OptimalControl.jl#885) | +| OptimalControl | ✅ | ✅ | ✅ | `JOSS`, split CI (`github-runner`/`occidata-runner`); `Documentation` also carries an optional `build-gpu` upgrade pass on `occidata` (2026-09-02, control-toolbox/OptimalControl.jl#885) | | OptimalControlProblems | ✅ | — | — | — | | CTDiffFlow.jl | ✅ | — | — | — | | CTBenchmarks.jl | ✅ | — | — | `benchmark-*` | @@ -568,7 +574,7 @@ runner is the one maintained by the scheduled `CTActions` maintenance workflows | `DOCUMENTER_KEY` | Documentation, CompatHelper, TagBot | Deploy docs / sign tags (Documenter deploy key) | | `CODECOV_TOKEN` → `codecov-secret` | Coverage | Upload coverage to Codecov | | `PROJECT_TOKEN` → `project_token` | AddToProject | Write to the org project board | -| `CT_APP_CLIENT_ID`, `CT_APP_PRIVATE_KEY` | CompatPrune (`publish` job) | Token of the organization's GitHub App: push `ct/compat-prune` and open PRs in the CT\* repositories, keep the tracking issue | +| `CT_APP_CLIENT_ID`, `CT_APP_PRIVATE_KEY` | `compat-prune-monthly.yml` in CTActions (`publish` job) | Token of the organization's GitHub App: push `ct/compat-prune` and open PRs in the CT\* repositories, keep the tracking issue | | `GITHUB_TOKEN` | (built-in) most | Default repo-scoped token | Use `secrets: inherit` (as `UpdateReadme` does) only when the reusable workflow needs