diff --git a/.github/workflows/sublibrary-downgrade.yml b/.github/workflows/sublibrary-downgrade.yml index 5c5174c..0dec7bf 100644 --- a/.github/workflows/sublibrary-downgrade.yml +++ b/.github/workflows/sublibrary-downgrade.yml @@ -167,6 +167,13 @@ jobs: [ -n "${{ inputs.skip }}" ] && effective="$effective,${{ inputs.skip }}" echo "Effective downgrade skip: $effective" echo "EFFECTIVE_SKIP=$effective" >> "$GITHUB_ENV" + - name: "Save project metadata for QA" + if: "${{ inputs.group-env-value == 'QA' }}" + shell: bash + run: | + save_dir="$RUNNER_TEMP/sciml-downgrade-original-project" + mkdir -p "$save_dir" + cp "${{ matrix.project }}/Project.toml" "$save_dir/Project.toml" - uses: julia-actions/julia-downgrade-compat@v2 with: projects: ${{ matrix.project }} @@ -180,6 +187,19 @@ jobs: - uses: julia-actions/julia-buildpkg@v1 with: project: ${{ matrix.project }} + - name: "Checkout SciML/.github for the downgrade metadata helper" + if: "${{ inputs.group-env-value == 'QA' }}" + uses: actions/checkout@v7 + with: + repository: SciML/.github + path: .sciml-dotgithub + - name: "Restore project metadata before QA" + if: "${{ inputs.group-env-value == 'QA' }}" + shell: bash + run: | + save_dir="$RUNNER_TEMP/sciml-downgrade-original-project" + cp "$save_dir/Project.toml" "${{ matrix.project }}/Project.toml" + julia --startup-file=no .sciml-dotgithub/scripts/restore_downgraded_project.jl "${{ matrix.project }}" - uses: julia-actions/julia-runtest@v1 with: project: ${{ matrix.project }} diff --git a/scripts/restore_downgraded_project.jl b/scripts/restore_downgraded_project.jl new file mode 100644 index 0000000..d17c384 --- /dev/null +++ b/scripts/restore_downgraded_project.jl @@ -0,0 +1,69 @@ +#!/usr/bin/env julia + +using TOML + +function restored_manifest_entry(project::AbstractDict, path::AbstractString) + entry = Dict{String, Any}( + "path" => path, + "uuid" => project["uuid"], + ) + hard_deps = sort!(collect(keys(get(project, "deps", Dict{String, Any}())))) + isempty(hard_deps) || (entry["deps"] = hard_deps) + for key in ("weakdeps", "extensions") + values = get(project, key, nothing) + values isa AbstractDict && !isempty(values) && (entry[key] = deepcopy(values)) + end + haskey(project, "version") && (entry["version"] = project["version"]) + return entry +end + +function local_manifest_entry(entries, manifest_dir::AbstractString, project_dir::AbstractString) + matches = findall(entries) do entry + path = get(entry, "path", nothing) + path isa AbstractString || return false + return realpath(abspath(manifest_dir, path)) == realpath(project_dir) + end + length(matches) == 1 || error( + "expected exactly one local manifest entry for $project_dir, found $(length(matches))", + ) + return only(matches) +end + +""" + restore_downgraded_project(project_dir) + +Repair the local package entry in a downgrade-generated `Manifest.toml` after +restoring the package's original `Project.toml`. Resolved package versions and +test-only manifest entries are retained. +""" +function restore_downgraded_project(project_dir::AbstractString) + project_dir = normpath(abspath(project_dir)) + project_path = joinpath(project_dir, "Project.toml") + manifest_path = joinpath(project_dir, "Manifest.toml") + project = TOML.parsefile(project_path) + manifest = TOML.parsefile(manifest_path) + + package_name = project["name"] + entries = manifest["deps"][package_name] + entry_index = local_manifest_entry(entries, dirname(manifest_path), project_dir) + existing_path = entries[entry_index]["path"] + entries[entry_index] = restored_manifest_entry(project, existing_path) + pop!(manifest, "project_hash", nothing) + + temporary_path, io = mktemp(dirname(manifest_path)) + try + TOML.print(io, manifest; sorted = true) + close(io) + mv(temporary_path, manifest_path; force = true) + catch + close(io) + rm(temporary_path; force = true) + rethrow() + end + return nothing +end + +if abspath(PROGRAM_FILE) == @__FILE__ + length(ARGS) == 1 || error("usage: restore_downgraded_project.jl PROJECT_DIR") + restore_downgraded_project(only(ARGS)) +end diff --git a/test/runtests.jl b/test/runtests.jl index 1a5bc3e..cc520f2 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -8,6 +8,70 @@ include(SCRIPT) const DEVELOP_SCRIPT = joinpath(@__DIR__, "..", "scripts", "develop_sources.jl") include(DEVELOP_SCRIPT) +const RESTORE_DOWNGRADE_SCRIPT = + joinpath(@__DIR__, "..", "scripts", "restore_downgraded_project.jl") +include(RESTORE_DOWNGRADE_SCRIPT) + +@testset "restore downgraded project manifest metadata" begin + root = mktempdir() + write( + joinpath(root, "Project.toml"), """ + name = "Root" + uuid = "00000000-0000-0000-0000-000000000001" + version = "1.2.3" + + [deps] + Runtime = "00000000-0000-0000-0000-000000000002" + + [weakdeps] + Weak = "00000000-0000-0000-0000-000000000003" + + [extensions] + RootWeakExt = "Weak" + """, + ) + write( + joinpath(root, "Manifest.toml"), """ + julia_version = "1.10.0" + manifest_format = "2.0" + project_hash = "stale" + + [[deps.Root]] + deps = ["Runtime", "TestOnly"] + path = "." + uuid = "00000000-0000-0000-0000-000000000001" + version = "1.2.3" + + [[deps.Runtime]] + git-tree-sha1 = "runtime-tree" + uuid = "00000000-0000-0000-0000-000000000002" + version = "2.0.0" + + [[deps.TestOnly]] + git-tree-sha1 = "test-tree" + uuid = "00000000-0000-0000-0000-000000000004" + version = "3.0.0" + """, + ) + + before = TOML.parsefile(joinpath(root, "Manifest.toml")) + restore_downgraded_project(root) + after = TOML.parsefile(joinpath(root, "Manifest.toml")) + + @test !haskey(after, "project_hash") + @test after["deps"]["Runtime"] == before["deps"]["Runtime"] + @test after["deps"]["TestOnly"] == before["deps"]["TestOnly"] + root_entry = only(after["deps"]["Root"]) + @test root_entry["path"] == "." + @test root_entry["uuid"] == "00000000-0000-0000-0000-000000000001" + @test root_entry["version"] == "1.2.3" + @test root_entry["deps"] == ["Runtime"] + @test root_entry["weakdeps"] == Dict( + "Weak" => "00000000-0000-0000-0000-000000000003", + ) + @test root_entry["extensions"] == Dict("RootWeakExt" => "Weak") +end + # Build a fixture monorepo `lib/` tree in a temp dir. # A (base, no internal deps) # B deps A