Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 113 additions & 2 deletions .github/workflows/downgrade.yml
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,105 @@ jobs:
mode: "${{ inputs.mode != '' && inputs.mode || 'deps' }}"
no_promote: "${{ inputs.no_promote }}"

- name: "Keep resolved test extras reachable during build and test"
env:
DOWNGRADE_TEST_PROJECT: "${{ inputs.project }}"
DOWNGRADE_NO_PROMOTE: "${{ inputs.no_promote }}"
run: |
project_dir="$DOWNGRADE_TEST_PROJECT"
[ "$project_dir" = "@." ] && project_dir="."
case "$project_dir" in
@*) echo "Project must be a directory path or @.: $project_dir" >&2; exit 1 ;;
esac
project_file=""
for filename in Project.toml JuliaProject.toml; do
candidate="$project_dir/$filename"
if [ -f "$candidate" ]; then project_file="$candidate"; break; fi
done
[ -n "$project_file" ] || { echo "No Project.toml or JuliaProject.toml in $project_dir" >&2; exit 1; }
backup="$RUNNER_TEMP/sciml-downgrade-original-project.toml"
cp -- "$project_file" "$backup"
echo "DOWNGRADE_PROJECT_FILE=$project_file" >> "$GITHUB_ENV"
echo "DOWNGRADE_PROJECT_BACKUP=$backup" >> "$GITHUB_ENV"
julia --startup-file=no - "$DOWNGRADE_TEST_PROJECT" "$DOWNGRADE_NO_PROMOTE" <<'JULIA'
using TOML

function project_file(project::AbstractString)
directory = project == "@." ? "." : project
startswith(directory, "@") &&
error("project must be a directory path or @., got: $project")
for filename in ("Project.toml", "JuliaProject.toml")
candidate = joinpath(directory, filename)
isfile(candidate) && return candidate
end
error("no Project.toml or JuliaProject.toml in $directory")
end

function write_project(path::AbstractString, project::AbstractDict)
temporary, io = mktemp(dirname(path))
try
TOML.print(io, project)
close(io)
mv(temporary, path; force = true)
catch
isopen(io) && close(io)
ispath(temporary) && rm(temporary; force = true)
rethrow()
end
return nothing
end

function promote_test_extras(project::AbstractString, no_promote = String[])
path = project_file(project)
parsed = TOML.parsefile(path)
extras = get(parsed, "extras", Dict{String, Any}())
targets = get(parsed, "targets", Dict{String, Any}())
test_target = get(targets, "test", String[])
test_target isa AbstractVector || error("[targets].test must be an array in $path")
excluded = Set(strip.(filter(!isempty, no_promote)))
sources = Set(keys(get(parsed, "sources", Dict{String, Any}())))
names = sort!(unique!(String.(test_target)))
deps = get(parsed, "deps", Dict{String, Any}())
for name in names
if haskey(extras, name) && haskey(deps, name) && deps[name] != extras[name]
error("$name has different UUIDs in [deps] and [extras] in $path")
end
end
targeted_extras = filter(name -> haskey(extras, name), names)
skipped_no_promote = filter(name -> name ∈ excluded, targeted_extras)
skipped_sources = filter(name -> name ∈ sources, targeted_extras)
promoted = filter(names) do name
haskey(extras, name) && !haskey(deps, name) &&
name ∉ excluded && name ∉ sources
end
@info "Selected old-style test extras for the downgraded environment" project = path promoted skipped_no_promote skipped_sources
isempty(promoted) && return promoted

deps = get!(parsed, "deps", Dict{String, Any}())
weakdeps = get(parsed, "weakdeps", nothing)
for name in promoted
uuid = extras[name]
deps[name] = uuid
weakdeps isa AbstractDict && delete!(weakdeps, name)
end
weakdeps isa AbstractDict && isempty(weakdeps) && delete!(parsed, "weakdeps")
write_project(path, parsed)
@info "Promoted resolved test extras into [deps]" project = path promoted
return promoted
end

function main(args)
length(args) == 2 || error("usage: promote_test_extras.jl PROJECT NO_PROMOTE")
no_promote = filter(!isempty, strip.(split(args[2], ',')))
promote_test_extras(args[1], no_promote)
return nothing
end

if PROGRAM_FILE == "-" || abspath(PROGRAM_FILE) == @__FILE__
main(ARGS)
end
JULIA

- name: "Install system packages"
if: "${{ inputs.apt-packages != '' && runner.os == 'Linux' }}"
run: |
Expand All @@ -139,6 +238,12 @@ jobs:
with:
project: "${{ inputs.project }}"

- name: "Export requested test group"
if: "${{ inputs.group != '' }}"
env:
REQUESTED_GROUP: "${{ inputs.group }}"
run: printf 'GROUP=%s\n' "$REQUESTED_GROUP" >> "$GITHUB_ENV"

- uses: julia-actions/julia-runtest@v1
with:
project: "${{ inputs.project }}"
Expand All @@ -148,5 +253,11 @@ jobs:
# and fails resolution (e.g. "empty intersection between X@min and project
# compatibility latest"). Downgrade tests must keep the minimum versions.
force_latest_compatible_version: false
env:
GROUP: "${{ inputs.group }}"

- name: "Restore the original project file"
if: ${{ always() }}
run: |
if [ -n "${DOWNGRADE_PROJECT_BACKUP:-}" ] && [ -f "$DOWNGRADE_PROJECT_BACKUP" ]; then
cp -- "$DOWNGRADE_PROJECT_BACKUP" "$DOWNGRADE_PROJECT_FILE"
rm -- "$DOWNGRADE_PROJECT_BACKUP"
fi
105 changes: 105 additions & 0 deletions .github/workflows/sublibrary-downgrade.yml
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,104 @@ jobs:
projects: ${{ matrix.project }}
skip: ${{ env.EFFECTIVE_SKIP }}
julia_version: ${{ env.JULIA_MM }}
- name: "Keep resolved test extras reachable during build and test"
env:
DOWNGRADE_TEST_PROJECT: ${{ matrix.project }}
DOWNGRADE_NO_PROMOTE: ""
run: |
project_dir="$DOWNGRADE_TEST_PROJECT"
[ "$project_dir" = "@." ] && project_dir="."
case "$project_dir" in
@*) echo "Project must be a directory path or @.: $project_dir" >&2; exit 1 ;;
esac
project_file=""
for filename in Project.toml JuliaProject.toml; do
candidate="$project_dir/$filename"
if [ -f "$candidate" ]; then project_file="$candidate"; break; fi
done
[ -n "$project_file" ] || { echo "No Project.toml or JuliaProject.toml in $project_dir" >&2; exit 1; }
backup="$RUNNER_TEMP/sciml-downgrade-original-project.toml"
cp -- "$project_file" "$backup"
echo "DOWNGRADE_PROJECT_FILE=$project_file" >> "$GITHUB_ENV"
echo "DOWNGRADE_PROJECT_BACKUP=$backup" >> "$GITHUB_ENV"
julia --startup-file=no - "$DOWNGRADE_TEST_PROJECT" "$DOWNGRADE_NO_PROMOTE" <<'JULIA'
using TOML

function project_file(project::AbstractString)
directory = project == "@." ? "." : project
startswith(directory, "@") &&
error("project must be a directory path or @., got: $project")
for filename in ("Project.toml", "JuliaProject.toml")
candidate = joinpath(directory, filename)
isfile(candidate) && return candidate
end
error("no Project.toml or JuliaProject.toml in $directory")
end

function write_project(path::AbstractString, project::AbstractDict)
temporary, io = mktemp(dirname(path))
try
TOML.print(io, project)
close(io)
mv(temporary, path; force = true)
catch
isopen(io) && close(io)
ispath(temporary) && rm(temporary; force = true)
rethrow()
end
return nothing
end

function promote_test_extras(project::AbstractString, no_promote = String[])
path = project_file(project)
parsed = TOML.parsefile(path)
extras = get(parsed, "extras", Dict{String, Any}())
targets = get(parsed, "targets", Dict{String, Any}())
test_target = get(targets, "test", String[])
test_target isa AbstractVector || error("[targets].test must be an array in $path")
excluded = Set(strip.(filter(!isempty, no_promote)))
sources = Set(keys(get(parsed, "sources", Dict{String, Any}())))
names = sort!(unique!(String.(test_target)))
deps = get(parsed, "deps", Dict{String, Any}())
for name in names
if haskey(extras, name) && haskey(deps, name) && deps[name] != extras[name]
error("$name has different UUIDs in [deps] and [extras] in $path")
end
end
targeted_extras = filter(name -> haskey(extras, name), names)
skipped_no_promote = filter(name -> name ∈ excluded, targeted_extras)
skipped_sources = filter(name -> name ∈ sources, targeted_extras)
promoted = filter(names) do name
haskey(extras, name) && !haskey(deps, name) &&
name ∉ excluded && name ∉ sources
end
@info "Selected old-style test extras for the downgraded environment" project = path promoted skipped_no_promote skipped_sources
isempty(promoted) && return promoted

deps = get!(parsed, "deps", Dict{String, Any}())
weakdeps = get(parsed, "weakdeps", nothing)
for name in promoted
uuid = extras[name]
deps[name] = uuid
weakdeps isa AbstractDict && delete!(weakdeps, name)
end
weakdeps isa AbstractDict && isempty(weakdeps) && delete!(parsed, "weakdeps")
write_project(path, parsed)
@info "Promoted resolved test extras into [deps]" project = path promoted
return promoted
end

function main(args)
length(args) == 2 || error("usage: promote_test_extras.jl PROJECT NO_PROMOTE")
no_promote = filter(!isempty, strip.(split(args[2], ',')))
promote_test_extras(args[1], no_promote)
return nothing
end

if PROGRAM_FILE == "-" || abspath(PROGRAM_FILE) == @__FILE__
main(ARGS)
end
JULIA
- name: "Install system packages"
if: "${{ inputs.apt-packages != '' && runner.os == 'Linux' }}"
run: |
Expand All @@ -184,3 +282,10 @@ jobs:
with:
project: ${{ matrix.project }}
allow_reresolve: false
- name: "Restore the original project file"
if: ${{ always() }}
run: |
if [ -n "${DOWNGRADE_PROJECT_BACKUP:-}" ] && [ -f "$DOWNGRADE_PROJECT_BACKUP" ]; then
cp -- "$DOWNGRADE_PROJECT_BACKUP" "$DOWNGRADE_PROJECT_FILE"
rm -- "$DOWNGRADE_PROJECT_BACKUP"
fi
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,8 @@ to catch under-specified `[compat]` lower bounds.
| `skip` | string | `""` | **Additional** deps to skip when downgrading, beyond the auto-included Julia stdlibs and any `[sources]` path/url deps (see note). |
| `projects` | string | `"."` | Comma-separated project dirs to downgrade. |
| `project` | string | `"@."` | `--project` for build/test (a workspace submodule or `lib/X`); default tests the repo root. |
| `mode` | string | `"deps"` | Dependency set minimized by `julia-downgrade-compat`: `deps`, `alldeps`, `weakdeps`, or `forcedeps`. |
| `no_promote` | string | `"Mooncake"` | Comma-separated old-style test extras left out of the joint floor resolve. |
| `self-hosted` / `os` | | `false` / `ubuntu-latest` | Runner selection. |
| `apt-packages` | string | `""` | Space-separated apt packages to install before building/testing (Linux only). |
| `container` | string | `""` | Docker container image to run the job in (e.g. `cmhyett/julia-fenics:latest` for Python-stack packages). Empty = no container. |
Expand All @@ -282,6 +284,14 @@ jobs:
> [SciML/.github #73](https://github.com/SciML/.github/pull/73); strict
> `allow_reresolve: false` is already in effect.)

For old-style `[extras]` / `[targets].test` dependencies, the workflow
temporarily adds each floor-resolved test extra to the active project's
`[deps]` before build and test. This keeps `Pkg.test` from replacing an orphaned
manifest floor with the newest compatible version. `[sources]` and
`no_promote` entries are left untouched, new-style `test/Project.toml`
environments keep their existing handling, and an `always()` cleanup restores
the original project file byte-for-byte.

### `documentation.yml`

Builds and deploys [Documenter](https://documenter.juliadocs.org/)
Expand Down Expand Up @@ -780,6 +790,12 @@ jobs:
> [SciML/.github #73](https://github.com/SciML/.github/pull/73); strict
> `allow_reresolve: false` is already in effect.)

Old-style `[extras]` / `[targets].test` dependencies are temporarily added to
the sublibrary's `[deps]` after floor resolution so `Pkg.build` and `Pkg.test`
retain those exact manifest versions. Source-backed extras are not rewritten,
new-style `test/Project.toml` environments keep their existing handling, and
an `always()` cleanup restores the original project file byte-for-byte.

---

## Recommended repository setup
Expand Down
Loading