From c7053606af7e4f0abc3c455900db39542431e584 Mon Sep 17 00:00:00 2001 From: Richard Reeve Date: Thu, 9 Jul 2026 14:48:58 +0100 Subject: [PATCH 1/6] Match Pkg's Project.toml ordering and move RSMD fields into [rsmd]. --- Project.toml | 28 +++++++------- README.md | 19 ++++++---- docs/make.jl | 6 +-- docs/src/index.md | 28 ++++++++++---- src/crosswalk.jl | 59 +++++++++++++++-------------- src/project.jl | 94 +++++++++++++++++++++++++++-------------------- test/runtests.jl | 18 ++++----- 7 files changed, 145 insertions(+), 107 deletions(-) diff --git a/Project.toml b/Project.toml index b7c1051..780f1f7 100644 --- a/Project.toml +++ b/Project.toml @@ -1,12 +1,8 @@ name = "ResearchSoftwareMetadata" uuid = "58378933-4625-47fa-851e-05ee27d397bd" license = "BSD-2-Clause" -description = "Research Software metadata crosswalk between Project.toml, codemeta.json, .zenodo.json and LICENSE to provide consistent metadata" -keywords = ["RSMD", "julia", "metadata", "research software"] -category = "metadata" -development_status = "active" -authors = ["Richard Reeve "] version = "0.2.0" +authors = ["Richard Reeve "] [deps] DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" @@ -28,14 +24,6 @@ TOML = "1" YAML = "0.4" julia = "1.10" -[[author_details]] -name = "Richard Reeve" -orcid = "0000-0003-2589-8091" -email = "richard.reeve@glasgow.ac.uk" - - [[author_details.affiliation]] - ror = "00vtgdb53" - [extras] Git = "d7ba0133-e1db-5d97-8f8c-041e4b3a1eb2" JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" @@ -43,5 +31,19 @@ Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +[rsmd] +keywords = ["RSMD", "julia", "metadata", "research software"] +category = "metadata" +description = "Research Software metadata crosswalk between Project.toml, codemeta.json, .zenodo.json and LICENSE to provide consistent metadata" +development_status = "active" + + [[rsmd.author_details]] + name = "Richard Reeve" + email = "richard.reeve@glasgow.ac.uk" + orcid = "0000-0003-2589-8091" + + [[rsmd.author_details.affiliation]] + ror = "00vtgdb53" + [targets] test = ["Test", "Git", "JSON", "Logging", "TOML"] diff --git a/README.md b/README.md index 897a10a..ff46085 100644 --- a/README.md +++ b/README.md @@ -37,23 +37,26 @@ To capture the license you are using and propagate it throughout the metadata f license = "BSD-2-Clause" ``` -To supplement the metadata on the authors of the package, add the [ORCID](https://orcid.org) for each author and the [ROR](https://ror.org) for the organisation(s) they are affiliated with. You can add as many authors and as much or as little information as you like about each one by adding additional `[[author_details]]` blocks. +All of the other metadata that this package uses lives in a single `[rsmd]` table. To supplement the metadata on the authors of the package, add the [ORCID](https://orcid.org) for each author and the [ROR](https://ror.org) for the organisation(s) they are affiliated with. You can add as many authors and as much or as little information as you like about each one by adding additional `[[rsmd.author_details]]` blocks. ```toml -[[author_details]] -name = "Richard Reeve" -orcid = "0000-0003-2589-8091" +[rsmd] - [[author_details.affiliation]] - ror = "00vtgdb53" + [[rsmd.author_details]] + name = "Richard Reeve" + orcid = "0000-0003-2589-8091" + + [[rsmd.author_details.affiliation]] + ror = "00vtgdb53" ``` -You can also optionally add a `description` of the package, `keywords` associated with it, the software `category` it belongs to, its [repostatus.org](https://www.repostatus.org) `development_status`, and the DOIs of any `publications` associated with the package. All of these are propagated into `codemeta.json` and `.zenodo.json`, and any values already in `codemeta.json` but missing from `Project.toml` will be backfilled into it: +You can also optionally add a `description` of the package, `keywords` associated with it, the software `category` it belongs to, its [repostatus.org](https://www.repostatus.org) `development_status`, and the DOIs of any `publications` associated with the package. All of these are propagated into `codemeta.json` and `.zenodo.json`, and any values already in `codemeta.json` but missing from `Project.toml` will be backfilled into it. Any of these keys found at the top level of `Project.toml` (the old layout) are automatically migrated into `[rsmd]`: ```toml -description = "A package that does things" +[rsmd] keywords = ["julia", "metadata"] category = "metadata" +description = "A package that does things" development_status = "active" publications = ["10.5281/zenodo.12789179"] ``` diff --git a/docs/make.jl b/docs/make.jl index f8296db..2cc0335 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -18,9 +18,9 @@ makedocs(; format = Documenter.HTML(; canonical = "https://boydorr.github.io/ResearchSoftwareMetadata.jl", edit_link = "main", - assets = String[],), - pages = ["Home" => "index.md"],) + assets = String[]), + pages = ["Home" => "index.md"]) deploydocs(; repo = "github.com/boydorr/ResearchSoftwareMetadata.jl", - devbranch = "main",) + devbranch = "main") diff --git a/docs/src/index.md b/docs/src/index.md index c42be62..474d173 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -33,19 +33,31 @@ First you need to add a small amount of additional metadata into your `Project.t To capture the license you are using and propagate it throughout the metadata files and through your julia code, add an [SPDX license identifier](https://spdx.org/licenses/) to the file: ```toml -[license] -SPDX = "BSD-2-Clause" +license = "BSD-2-Clause" ``` -To supplement the metadata on the authors of the package, add the [ORCID](https://orcid.org) for each author and the [ROR](https://ror.org) for the organisation(s) they are affiliated with. You can add as many authors and as much or as little information as you like about each one by adding additional `[[author_details]]` blocks. +All of the other metadata that this package uses lives in a single `[rsmd]` table. To supplement the metadata on the authors of the package, add the [ORCID](https://orcid.org) for each author and the [ROR](https://ror.org) for the organisation(s) they are affiliated with. You can add as many authors and as much or as little information as you like about each one by adding additional `[[rsmd.author_details]]` blocks. ```toml -[[author_details]] -name = "Richard Reeve" -orcid = "0000-0003-2589-8091" +[rsmd] - [[author_details.affiliation]] - ror = "00vtgdb53" + [[rsmd.author_details]] + name = "Richard Reeve" + orcid = "0000-0003-2589-8091" + + [[rsmd.author_details.affiliation]] + ror = "00vtgdb53" +``` + +You can also optionally add a `description` of the package, `keywords` associated with it, the software `category` it belongs to, its [repostatus.org](https://www.repostatus.org) `development_status`, and the DOIs of any `publications` associated with the package. All of these are propagated into `codemeta.json` and `.zenodo.json`, and any values already in `codemeta.json` but missing from `Project.toml` will be backfilled into it. Any of these keys found at the top level of `Project.toml` (the old layout) are automatically migrated into `[rsmd]`: + +```toml +[rsmd] +keywords = ["julia", "metadata"] +category = "metadata" +description = "A package that does things" +development_status = "active" +publications = ["10.5281/zenodo.12789179"] ``` Then, from the root of your package, you can just run a crosswalk: diff --git a/src/crosswalk.jl b/src/crosswalk.jl index a77e416..125fdfc 100644 --- a/src/crosswalk.jl +++ b/src/crosswalk.jl @@ -7,14 +7,15 @@ Runs a crosswalk across `Project.toml`, `LICENSE`, `codemeta.json` and `.zenodo. well as the julia source files to enforce consistency between the different metadata formats. It logs warnings and errors if it identifies inconsistencies while it is editing the files. `Project.toml` is the authoritative source of metadata: as well as its standard fields, the -optional top-level keys `description`, `keywords`, `category`, `development_status` and -`publications` (a vector of DOIs of associated papers) are propagated into `codemeta.json` -and `.zenodo.json`; values found only in `codemeta.json` are backfilled into `Project.toml`. +optional keys `description`, `keywords`, `category`, `development_status` and +`publications` (a vector of DOIs of associated papers) in its `[rsmd]` table are propagated +into `codemeta.json` and `.zenodo.json`; values found only in `codemeta.json` are backfilled +into `Project.toml`. Legacy top-level copies of these keys are migrated into `[rsmd]`. A missing `author_details` section is likewise constructed from `codemeta.json` or `.zenodo.json`, provided the information there is consistent with the definitive `authors` -field. New entries in `authors` are propagated into `author_details` (with a warning to add -their ORCID and ROR affiliation there), `codemeta.json` and `.zenodo.json`, while authors in -`codemeta.json` that are missing from `authors` are removed with an error. +field. New entries in `authors` are propagated into `rsmd.author_details` (with a warning to +add their ORCID and ROR affiliation there), `codemeta.json` and `.zenodo.json`, while authors +in `codemeta.json` that are missing from `authors` are removed with an error. The software category can be set with the `category` argument, likewise the `keywords` argument can contain a vector of keyword strings; both take precedence over and are written back into `Project.toml`. The `build` argument sets the `buildInstructions` RSMD @@ -28,6 +29,7 @@ function crosswalk(git_dir = readchomp(`$(Git.git()) rev-parse --show-toplevel`) category = nothing, keywords = nothing, build = false, update = false) project = read_project(git_dir) + rsmd = get!(project, "rsmd", OrderedDict{String, Any}()) proj_version = VersionNumber(project["version"]) now = string(today()) @@ -55,14 +57,14 @@ function crosswalk(git_dir = readchomp(`$(Git.git()) rev-parse --show-toplevel`) codemeta["@context"] = "https://w3id.org/codemeta/3.0" codemeta["type"] = "SoftwareSourceCode" - isnothing(reconcile!(project, codemeta, "category", "applicationCategory", + isnothing(reconcile!(rsmd, codemeta, "category", "applicationCategory", value = category)) && - @warn "No category metadata, add `category` to Project.toml" + @warn "No category metadata, add `category` to [rsmd] in Project.toml" codemeta["programmingLanguage"] = "julia" - reconcile!(project, codemeta, "development_status", "developmentStatus", + reconcile!(rsmd, codemeta, "development_status", "developmentStatus", default = "active") - isnothing(reconcile!(project, codemeta, "description", "description")) && - @warn "No description metadata, add `description` to Project.toml" + isnothing(reconcile!(rsmd, codemeta, "description", "description")) && + @warn "No description metadata, add `description` to [rsmd] in Project.toml" repo_index = "origin" ∈ remotes ? repo_index = findfirst(==("origin"), remotes) : 1 @@ -184,7 +186,7 @@ function crosswalk(git_dir = readchomp(`$(Git.git()) rev-parse --show-toplevel`) codemeta["downloadUrl"] = urls[repo_index] * "/archive/refs/tags/" * codemeta["version"] * ".tar.gz" - if !haskey(project, "author_details") + if !haskey(rsmd, "author_details") details = nothing source = nothing if !isempty(get(codemeta, "author", [])) @@ -202,7 +204,7 @@ function crosswalk(git_dir = readchomp(`$(Git.git()) rev-parse --show-toplevel`) if author_details_consistent(details, project["authors"]) @info "Reconstructing author_details in Project.toml " * "from $source" - project["author_details"] = details + rsmd["author_details"] = details else @error "Authors in $source are inconsistent with authors " * "in Project.toml, rebuilding author_details from " * @@ -214,8 +216,8 @@ function crosswalk(git_dir = readchomp(`$(Git.git()) rev-parse --show-toplevel`) authors = String[] author_data = [] orcid_people = Dict{String, Any}() - if haskey(project, "author_details") - for author in project["author_details"] + if haskey(rsmd, "author_details") + for author in rsmd["author_details"] person = nothing if haskey(author, "orcid") person = get_person_from_orcid(author["orcid"]) @@ -255,22 +257,22 @@ function crosswalk(git_dir = readchomp(`$(Git.git()) rev-parse --show-toplevel`) isnothing(email) || (detail["email"] = email) push!(author_data, detail) end - project["author_details"] = author_data + rsmd["author_details"] = author_data end # Add authors listed in `authors` but missing from author_details for proj_author in project["authors"] name, email = parse_author(proj_author) covered = any(authors) do author - author == proj_author || - lowercase(author) == lowercase(proj_author) || - occursin(author, proj_author) || - parse_author(author) == (name, email) + return author == proj_author || + lowercase(author) == lowercase(proj_author) || + occursin(author, proj_author) || + parse_author(author) == (name, email) end if !covered detail = OrderedDict{String, Any}("name" => name) isnothing(email) || (detail["email"] = email) - push!(project["author_details"], detail) + push!(rsmd["author_details"], detail) push!(authors, isnothing(email) ? name : name * " <" * email * ">") @warn "Added $name to author_details in Project.toml, please " * "add their ORCID and ROR affiliation there if you can" @@ -366,7 +368,7 @@ function crosswalk(git_dir = readchomp(`$(Git.git()) rev-parse --show-toplevel`) end cm_authors = get(codemeta, "author", OrderedDict{String, Any}[]) - proj_authors = project["author_details"] + proj_authors = rsmd["author_details"] cm_from_proj = OrderedDict{String, Any}[] for author in proj_authors person = haskey(author, "orcid") ? @@ -458,7 +460,8 @@ function crosswalk(git_dir = readchomp(`$(Git.git()) rev-parse --show-toplevel`) "$(codemeta["continuousIntegration"]) ≠ $(codemeta["codemeta:contIntegration"]["id"])" end else - codemeta["codemeta:contIntegration"] = Dict("id" => codemeta["continuousIntegration"]) + codemeta["codemeta:contIntegration"] = Dict("id" => + codemeta["continuousIntegration"]) end else if haskey(codemeta, "codemeta:contIntegration") @@ -467,21 +470,23 @@ function crosswalk(git_dir = readchomp(`$(Git.git()) rev-parse --show-toplevel`) @info "Using .github/workflows/testing.yaml for CI" codemeta["continuousIntegration"] = urls[repo_index] * "/actions/workflows/testing.yaml" - codemeta["codemeta:contIntegration"] = Dict("id" => codemeta["continuousIntegration"]) + codemeta["codemeta:contIntegration"] = Dict("id" => + codemeta["continuousIntegration"]) elseif isfile(".github/workflows/CI.yaml") @info "Using .github/workflows/CI.yaml for CI" codemeta["continuousIntegration"] = urls[repo_index] * "/actions/workflows/CI.yaml" - codemeta["codemeta:contIntegration"] = Dict("id" => codemeta["continuousIntegration"]) + codemeta["codemeta:contIntegration"] = Dict("id" => + codemeta["continuousIntegration"]) elseif isdir(".github/workflows") @warn "CI not found in codemeta.json, but .github/workflows exists" end end - reconcile!(project, codemeta, "keywords", "keywords", + reconcile!(rsmd, codemeta, "keywords", "keywords", value = keywords, default = ["julia"], to_cm = sort) - publications = reconcile!(project, codemeta, "publications", + publications = reconcile!(rsmd, codemeta, "publications", "referencePublication", to_cm = dois -> "https://doi.org/" .* dois, from_cm = refs -> replace.(refs isa Vector ? diff --git a/src/project.jl b/src/project.jl index b581231..d24bb90 100644 --- a/src/project.jl +++ b/src/project.jl @@ -1,61 +1,77 @@ # SPDX-License-Identifier: BSD-2-Clause +# Pkg's canonical Project.toml key order (Pkg/src/project.jl, Julia 1.13) +const PROJECT_KEY_ORDER = ["name", "uuid", "keywords", "license", "desc", + "version", + "readonly", "workspace", "deps", "weakdeps", "sources", + "extensions", "compat"] + +""" + ResearchSoftwareMetadata.project_key_order(key) + +Sort key reproducing Pkg's `Project.toml` ordering: keys Pkg knows about +sort by their position in its canonical list, all other keys sort +alphabetically after them. +""" +function project_key_order(key) + return (something(findfirst(==(key), PROJECT_KEY_ORDER), + length(PROJECT_KEY_ORDER) + 1), key) +end + """ ResearchSoftwareMetadata.order_project(project_d::AbstractDict) -Take a parsed `Project.toml` dictionary and return it in its canonical -order in an OrderedDict. +Take a parsed `Project.toml` dictionary and return it as an OrderedDict +in canonical order — the order Pkg itself writes, applied recursively to +nested tables — so that files written from it are not reordered when Pkg +next edits them. """ function order_project(project_d::AbstractDict) - project_d = copy(project_d) project = OrderedDict{String, Any}() - for key in [ - "name", - "uuid", - "license", - "description", - "keywords", - "category", - "development_status", - "publications", - "authors", - "version", - "deps", - "weakdeps", - "extensions", - "compat", - "author_details", - "extras", - "targets" - ] + for key in sort!(collect(keys(project_d)); by = project_key_order) + project[key] = order_value(project_d[key]) + end + + return project +end + +order_value(val::AbstractDict) = order_project(val) +order_value(val::AbstractVector) = map(order_value, val) +order_value(val) = val + +# The RSMD-specific keys that live in the [rsmd] table of Project.toml +const RSMD_KEYS = ["description", "keywords", "category", "development_status", + "publications", "author_details"] + +""" + ResearchSoftwareMetadata.migrate_rsmd!(project_d::AbstractDict) + +Move any legacy top-level RSMD keys (`description`, `keywords`, +`category`, `development_status`, `publications`, `author_details`) into +the `rsmd` table, where they now live. Existing `rsmd` entries win on +conflict. Returns the dictionary. +""" +function migrate_rsmd!(project_d::AbstractDict) + for key in RSMD_KEYS if haskey(project_d, key) - val = project_d[key] - if val isa AbstractDict - d = OrderedDict{String, Any}() - for k2 in sort(collect(keys(val))) - d[k2] = val[k2] - end - project[key] = d - else - project[key] = val - end + rsmd = get!(project_d, "rsmd", Dict{String, Any}()) + haskey(rsmd, key) || + (rsmd[key] = project_d[key]) delete!(project_d, key) end end - for key in keys(project_d) - project[key] = project_d[key] - end - return project + return project_d end """ ResearchSoftwareMetadata.read_project() -Read a `Project.toml` file in and return it in its canonical order in -an OrderedDict. +Read a `Project.toml` file in, migrate any legacy top-level RSMD keys +into the `rsmd` table, and return it in its canonical order in an +OrderedDict. """ function read_project(git_dir = readchomp(`$(Git.git()) rev-parse --show-toplevel`)) file = joinpath(git_dir, "Project.toml") - return order_project(TOML.parsefile(file)) + return order_project(migrate_rsmd!(TOML.parsefile(file))) end diff --git a/test/runtests.jl b/test/runtests.jl index 3ba0fc5..315edbc 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -168,8 +168,8 @@ end @test isnothing(ResearchSoftwareMetadata.crosswalk(dir)) cd(git_dir) # crosswalk leaves the working directory changed project = TOML.parsefile(joinpath(dir, "Project.toml")) - @test haskey(project, "author_details") - detail = project["author_details"][1] + @test haskey(project["rsmd"], "author_details") + detail = project["rsmd"]["author_details"][1] @test detail["name"] == "Ann B Smith" @test detail["email"] == "ann@example.com" @test detail["affiliation"][1]["name"] == "Example University" @@ -195,7 +195,7 @@ end @test isnothing(ResearchSoftwareMetadata.crosswalk(dir)) cd(git_dir) # crosswalk leaves the working directory changed project = TOML.parsefile(joinpath(dir, "Project.toml")) - detail = project["author_details"][1] + detail = project["rsmd"]["author_details"][1] @test detail["name"] == "Ann B Smith" @test !haskey(detail, "email") @test detail["affiliation"][1]["name"] == "Example University" @@ -220,7 +220,7 @@ end @test isnothing(ResearchSoftwareMetadata.crosswalk(dir)) cd(git_dir) # crosswalk leaves the working directory changed project = TOML.parsefile(joinpath(dir, "Project.toml")) - @test project["author_details"] == + @test project["rsmd"]["author_details"] == [Dict("name" => "Ann B Smith", "email" => "ann@example.com")] # authors is definitive, so the inconsistent codemeta author goes codemeta = JSON.parsefile(joinpath(dir, "codemeta.json")) @@ -246,7 +246,7 @@ end project = TOML.parsefile(toml) @test project["authors"] == ["Ann B Smith ", "Bob Jones "] - details = project["author_details"] + details = project["rsmd"]["author_details"] @test length(details) == 2 @test details[2]["name"] == "Bob Jones" @test details[2]["email"] == "bob@example.com" @@ -277,11 +277,11 @@ end @test isnothing(ResearchSoftwareMetadata.crosswalk(dir)) cd(git_dir) # crosswalk leaves the working directory changed project = TOML.parsefile(joinpath(dir, "Project.toml")) - @test project["description"] == "A fixture package" - @test project["keywords"] == ["fixture", "metadata"] - @test project["category"] == "metadata" + @test project["rsmd"]["description"] == "A fixture package" + @test project["rsmd"]["keywords"] == ["fixture", "metadata"] + @test project["rsmd"]["category"] == "metadata" # Defaults are not backfilled into Project.toml - @test !haskey(project, "development_status") + @test !haskey(project["rsmd"], "development_status") codemeta = JSON.parsefile(joinpath(dir, "codemeta.json")) @test codemeta["developmentStatus"] == "active" end From 90e4473dcac5c6b8f3092e9b9dbee4c8ee7da5db Mon Sep 17 00:00:00 2001 From: Richard Reeve Date: Thu, 9 Jul 2026 15:00:02 +0100 Subject: [PATCH 2/6] Minor version bump and update docs. --- NEWS.md | 4 ++++ Project.toml | 2 +- README.md | 6 +++--- codemeta.json | 4 ++-- docs/src/index.md | 22 +++++++++++----------- src/crosswalk.jl | 3 ++- src/remotequeries.jl | 7 +++++-- 7 files changed, 28 insertions(+), 20 deletions(-) diff --git a/NEWS.md b/NEWS.md index 8e09950..75cafbb 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,9 @@ # NEWS +- v0.3.0 + - Move the RSMD-specific Project.toml keys (description, keywords, category, development_status, publications and author_details) into a single [rsmd] table, migrating legacy top-level keys there automatically + - Write Project.toml in Pkg's canonical key order, applied recursively to nested tables, so Pkg operations like add and rm no longer reorder the file + - Fix errors and stale examples in the README and documentation - v0.2.0 - Include authors without ORCIDs in codemeta.json and .zenodo.json - Handle missing ORCID and ROR records without crashing diff --git a/Project.toml b/Project.toml index 780f1f7..71fe326 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "ResearchSoftwareMetadata" uuid = "58378933-4625-47fa-851e-05ee27d397bd" license = "BSD-2-Clause" -version = "0.2.0" +version = "0.3.0" authors = ["Richard Reeve "] [deps] diff --git a/README.md b/README.md index ff46085..fb7e6ef 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ ## Summary **ResearchSoftwareMetadata** is a [Julia](http://www.julialang.org) package that -provides functionality for to allow a crosswalk between Project.toml, codemeta.json, .zenodo.json and the package LICENSE file to allow a consistent way of providing metadata for research software which allows the Julia General Registry to pick up the same metadata as GitHub and Zenodo while following the Research Software MetaData [guidelines](https://fair-impact.github.io/RSMD-guidelines/). +provides a crosswalk between `Project.toml`, `codemeta.json`, `.zenodo.json` and the package `LICENSE` file, giving a consistent way of providing metadata for research software, so that the Julia General Registry, GitHub and Zenodo all pick up the same metadata, following the Research Software MetaData [guidelines](https://fair-impact.github.io/RSMD-guidelines/). ## Installation @@ -21,7 +21,7 @@ built and installed with `add`. For example: (@v1.12) pkg> add ResearchSoftwareMetadata Resolving package versions... Updating `~/.julia/environments/v1.12/Project.toml` - [aea672f4] + ResearchSoftwareMetadata v0.2.0 + [58378933] + ResearchSoftwareMetadata v0.3.0 Updating `~/.julia/environments/v1.12/Manifest.toml` (@v1.12) pkg> @@ -75,7 +75,7 @@ using ResearchSoftwareMetadata ResearchSoftwareMetadata.crosswalk() ``` -If you want to add in some additional metadata (the `category` of the software, or the `keywords` associated with it, or you want to increase the package version during the crosswalk, this is possible as follows: +If you want to pass in some additional metadata (the `category` of the software, or the `keywords` associated with it, both of which are written back into `[rsmd]` in `Project.toml`), or you want to increase the package version during the crosswalk, you can do that as follows: ```julia # Add in additional metadata diff --git a/codemeta.json b/codemeta.json index db817d6..e52543f 100644 --- a/codemeta.json +++ b/codemeta.json @@ -14,10 +14,10 @@ "Linux", "macOS" ], - "version": "v0.2.0", + "version": "v0.3.0", "dateModified": "2026-07-09", "datePublished": "2024-07-18", - "downloadUrl": "https://github.com/boydorr/ResearchSoftwareMetadata.jl/archive/refs/tags/v0.2.0.tar.gz", + "downloadUrl": "https://github.com/boydorr/ResearchSoftwareMetadata.jl/archive/refs/tags/v0.3.0.tar.gz", "license": "https://spdx.org/licenses/BSD-2-Clause", "author": [ { diff --git a/docs/src/index.md b/docs/src/index.md index 474d173..1dce77d 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -9,7 +9,7 @@ Documentation for [ResearchSoftwareMetadata](https://github.com/boydorr/Research ## Summary **ResearchSoftwareMetadata** is a [Julia](http://www.julialang.org) package that -provides functionality for to allow a crosswalk between Project.toml, codemeta.json, .zenodo.json and the package LICENSE file to allow a consistent way of providing metadata for research software which allows the Julia General Registry to pick up the same metadata as GitHub and Zenodo while following the Research Software MetaData [guidelines](https://fair-impact.github.io/RSMD-guidelines/). +provides a crosswalk between `Project.toml`, `codemeta.json`, `.zenodo.json` and the package `LICENSE` file, giving a consistent way of providing metadata for research software, so that the Julia General Registry, GitHub and Zenodo all pick up the same metadata, following the Research Software MetaData [guidelines](https://fair-impact.github.io/RSMD-guidelines/). ## Installation @@ -17,13 +17,13 @@ The package is registered in the `General` registry so can be built and installed with `add`. For example: ```julia -(@v1.10) pkg> add ResearchSoftwareMetadata +(@v1.12) pkg> add ResearchSoftwareMetadata Resolving package versions... - Updating `~/.julia/environments/v1.10/Project.toml` - [aea672f4] + ResearchSoftwareMetadata v0.1.0 - Updating `~/.julia/environments/v1.10/Manifest.toml` + Updating `~/.julia/environments/v1.12/Project.toml` + [58378933] + ResearchSoftwareMetadata v0.3.0 + Updating `~/.julia/environments/v1.12/Manifest.toml` -(@v1.10) pkg> +(@v1.12) pkg> ``` ## Usage @@ -75,19 +75,19 @@ ResearchSoftwareMetadata.crosswalk() ``` -If you want to add in some additional metadata (the `category` of the software, or the `keywords` associated with it, or you want to increase the package version during the crosswalk, this is possible as follows: +If you want to pass in some additional metadata (the `category` of the software, or the `keywords` associated with it, both of which are written back into `[rsmd]` in `Project.toml`), or you want to increase the package version during the crosswalk, you can do that as follows: ```julia # Add in additional metadata -ResearchSoftwareMetadata.crosswalk(category = "ecology", keywords = ["julia", "metadata", "research software", "RSMD"]) +ResearchSoftwareMetadata.crosswalk(category = "metadata", keywords = ["julia", "metadata", "research software", "RSMD"]) # Increase version number during crosswalk ResearchSoftwareMetadata.increase_patch() # Bump patch version (e.g. 0.4.1 -> 0.4.2) -ResearchSoftwareMetadata.increase_minor() # Bump minor version (e.g. 0.4.1 -> 0.5.0) -ResearchSoftwareMetadata.increase_major() # Bump major version (e.g. 0.4.1 -> 1.0.0) +ResearchSoftwareMetadata.increase_minor() # Bump minor version (e.g. 0.4.2 -> 0.5.0) +ResearchSoftwareMetadata.increase_major() # Bump major version (e.g. 0.5.0 -> 1.0.0) ``` -You might also consider reformatting all of your julia code to a consistent format. A `.JuliaJormatter.toml` file in the package root defines what the formatting standard should be. +You might also consider reformatting all of your julia code to a consistent format. A `.JuliaFormatter.toml` file in the package root defines what the formatting standard should be. ```julia Pkg.add("JuliaFormatter") diff --git a/src/crosswalk.jl b/src/crosswalk.jl index 125fdfc..a7e6787 100644 --- a/src/crosswalk.jl +++ b/src/crosswalk.jl @@ -1,7 +1,8 @@ # SPDX-License-Identifier: BSD-2-Clause """ - crosswalk(; category = nothing, keywords = nothing, build = false) + crosswalk(git_dir; category = nothing, keywords = nothing, + build = false, update = false) Runs a crosswalk across `Project.toml`, `LICENSE`, `codemeta.json` and `.zenodo.json` as well as the julia source files to enforce consistency between the different metadata formats. diff --git a/src/remotequeries.jl b/src/remotequeries.jl index 6ba7e6b..3d595bb 100644 --- a/src/remotequeries.jl +++ b/src/remotequeries.jl @@ -54,12 +54,15 @@ function get_organisation_from_ror(ror::String) if haskey(data, "name") name = data["name"] elseif haskey(data, "names") && length(data["names"]) ≥ 1 - names = [record["value"] for record in data["names"] if "ror_display" in record["types"]] + names = [record["value"] + for record in data["names"] + if "ror_display" in record["types"]] if length(names) ≥ 1 name = names[1] else @warn "No display name found for ROR $ror" - names = [record["value"] for record in data["names"] if record["lang"] == "en"] + names = [record["value"] + for record in data["names"] if record["lang"] == "en"] if length(names) ≥ 1 name = names[1] else From 6654da62a450c7f81904ea5dfee58b04788b5d25 Mon Sep 17 00:00:00 2001 From: Richard Reeve Date: Thu, 9 Jul 2026 15:55:30 +0100 Subject: [PATCH 3/6] Add in demo tests for other packages and explain how to use them. Fix problem with failures when changes are made in Project.toml. --- .github/workflows/testing.yaml | 3 + Project.toml | 4 +- README.md | 4 + docs/make.jl | 3 +- docs/src/index.md | 4 + docs/src/testing.md | 158 +++++++++++++++++++++++++ src/crosswalk.jl | 27 +++-- src/utils.jl | 22 ++-- test/clean_JuliaFormatter.jl | 23 ++++ test/clean_ResearchSoftwareMetadata.jl | 26 ++++ test/runtests.jl | 40 ++++--- 11 files changed, 280 insertions(+), 34 deletions(-) create mode 100644 docs/src/testing.md create mode 100644 test/clean_JuliaFormatter.jl create mode 100644 test/clean_ResearchSoftwareMetadata.jl diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml index 995b7da..d42fa9f 100644 --- a/.github/workflows/testing.yaml +++ b/.github/workflows/testing.yaml @@ -58,6 +58,9 @@ jobs: - uses: julia-actions/cache@v3 - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-runtest@v1 + env: + # Run the crosswalk and formatting hygiene checks on one canonical runner + RSMD_CROSSWALK: ${{ (matrix.os == 'ubuntu-latest' && matrix.julia-version == '1') && 'TRUE' || 'FALSE' }} - uses: julia-actions/julia-processcoverage@v1 - uses: codecov/codecov-action@v7 with: diff --git a/Project.toml b/Project.toml index 71fe326..403cf67 100644 --- a/Project.toml +++ b/Project.toml @@ -19,6 +19,7 @@ Dates = "1.6" Git = "1" HTTP = "1,2" JSON = "1" +JuliaFormatter = "2" Logging = "1" TOML = "1" YAML = "0.4" @@ -27,6 +28,7 @@ julia = "1.10" [extras] Git = "d7ba0133-e1db-5d97-8f8c-041e4b3a1eb2" JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" +JuliaFormatter = "98e50ef6-434e-11e9-1051-2b60c6c9e899" Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" @@ -46,4 +48,4 @@ development_status = "active" ror = "00vtgdb53" [targets] -test = ["Test", "Git", "JSON", "Logging", "TOML"] +test = ["Test", "Git", "JSON", "JuliaFormatter", "Logging", "TOML"] diff --git a/README.md b/README.md index fb7e6ef..7b64796 100644 --- a/README.md +++ b/README.md @@ -96,3 +96,7 @@ using JuliaFormatter using MyPackage format(MyPackage) ``` + +## Automated checking + +You can also make your package's own test suite check automatically that the metadata crosswalk is clean and the code is well formatted, by copying a few files from this package's `test/` directory into your own. See [Automated package checks](https://boydorr.github.io/ResearchSoftwareMetadata.jl/stable/testing/) in the documentation for instructions. diff --git a/docs/make.jl b/docs/make.jl index 2cc0335..c03bdb9 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -19,7 +19,8 @@ makedocs(; canonical = "https://boydorr.github.io/ResearchSoftwareMetadata.jl", edit_link = "main", assets = String[]), - pages = ["Home" => "index.md"]) + pages = ["Home" => "index.md", + "Automated package checks" => "testing.md"]) deploydocs(; repo = "github.com/boydorr/ResearchSoftwareMetadata.jl", diff --git a/docs/src/index.md b/docs/src/index.md index 1dce77d..f51166b 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -97,6 +97,10 @@ using MyPackage format(MyPackage) ``` +## Automated checking + +You can also make your package's own test suite check automatically that the metadata crosswalk is clean and the code is well formatted, by copying a few files from this package's `test/` directory into your own. See [Automated package checks](testing.md) for instructions. + ## Reference guide ```@index diff --git a/docs/src/testing.md b/docs/src/testing.md new file mode 100644 index 0000000..df90ab2 --- /dev/null +++ b/docs/src/testing.md @@ -0,0 +1,158 @@ +# Automated package checks + +As well as running the crosswalk by hand, you can make your package's own test suite +verify that its metadata and formatting stay clean: that running +`ResearchSoftwareMetadata.crosswalk()` on the package regenerates exactly the files that +are committed (so the metadata is consistent and up to date), and that running +[JuliaFormatter](https://github.com/domluna/JuliaFormatter.jl) leaves the source code +unchanged (so the code is well formatted). If either check would change a file, the test +fails until you re-run the crosswalk or formatter and commit the result. + +This is how ResearchSoftwareMetadata tests itself, and the test files are written so that +they can be copied into any other package. + +## Files to copy + +Copy these three files from ResearchSoftwareMetadata's +[`test/` directory](https://github.com/boydorr/ResearchSoftwareMetadata.jl/tree/main/test) +into your own package's `test/` directory: + +- [`GitUtils.jl`](https://github.com/boydorr/ResearchSoftwareMetadata.jl/blob/main/test/GitUtils.jl) + — provides `is_repo_clean`, which the other two files use to check whether the checks + changed anything. It needs no adaptation (though you may want to change the SPDX header + to your own package's licence identifier). +- [`clean_ResearchSoftwareMetadata.jl`](https://github.com/boydorr/ResearchSoftwareMetadata.jl/blob/main/test/clean_ResearchSoftwareMetadata.jl) + — checks the metadata crosswalk is clean. +- [`clean_JuliaFormatter.jl`](https://github.com/boydorr/ResearchSoftwareMetadata.jl/blob/main/test/clean_JuliaFormatter.jl) + — checks the code formatting is clean. + +You can use either or both of the `clean_*.jl` files; the test-suite code below discovers +whichever ones are present. + +In each `clean_*.jl` file, replace `ResearchSoftwareMetadata` in the `using` line with +your own package where it stands for the package under test, and set the SPDX header to +your package's licence (otherwise the crosswalk will rewrite it and the check will fail). +For a package called `MyPackage` with an MIT licence, the two files look like this: + +```julia +# SPDX-License-Identifier: MIT + +module CleanRSMD +using Test +using Git +using Logging +using MyPackage +using ResearchSoftwareMetadata + +include("GitUtils.jl") +using .GitUtils + +# Does not currently work on Windows runners on GitHub due to file writing issues +if !haskey(ENV, "RUNNER_OS") || ENV["RUNNER_OS"] ≠ "Windows" + @testset "RSMD" begin + git_dir = readchomp(`$(Git.git()) rev-parse --show-toplevel`) + @test isnothing(ResearchSoftwareMetadata.crosswalk()) + global_logger(SimpleLogger(stderr, Logging.Warn)) + @test_nowarn ResearchSoftwareMetadata.crosswalk() + global_logger(SimpleLogger(stderr, Logging.Info)) + @test is_repo_clean(git_dir; strict = haskey(ENV, "RUNNER_OS")) + end +else + @test_broken !haskey(ENV, "RUNNER_OS") || ENV["RUNNER_OS"] ≠ "Windows" +end + +end +``` + +```julia +# SPDX-License-Identifier: MIT + +module CleanJuliaFormatter +using Test +using Git +using JuliaFormatter +using MyPackage + +include("GitUtils.jl") +using .GitUtils + +# Does not currently work on Windows runners on GitHub due to file writing issues +if !haskey(ENV, "RUNNER_OS") || ENV["RUNNER_OS"] ≠ "Windows" + @testset "JuliaFormatter" begin + git_dir = readchomp(`$(Git.git()) rev-parse --show-toplevel`) + @test_nowarn format(MyPackage) + @test is_repo_clean(git_dir; strict = haskey(ENV, "RUNNER_OS")) + end +else + @test_broken !haskey(ENV, "RUNNER_OS") || ENV["RUNNER_OS"] ≠ "Windows" +end + +end +``` + +The formatting check assumes a `.JuliaFormatter.toml` file in your package root defining +your formatting standard, as described at the end of the [home page](index.md). + +## Running them from your test suite + +Add this block to the end of your `test/runtests.jl`. It discovers any `clean_*.jl` files +in your `test/` directory and runs each one in its own testset: + +```julia +rsmd = get(ENV, "RSMD_CROSSWALK", "FALSE") +if rsmd == "TRUE" || !haskey(ENV, "RUNNER_OS") # Crosswalk runner or local testing + # Test RSMD crosswalk and other hygiene issues + + # Identify files that are checking package hygiene + cleanbase = map(file -> replace(file, r"clean_(.*).jl$" => s"\1"), + filter(str -> occursin(r"^clean_.*\.jl$", str), + readdir(@__DIR__))) + + if length(cleanbase) > 0 + @info "Crosswalk and clean testing:" + @testset begin + for c in cleanbase + println(" = $c") + end + println() + + @testset for c in cleanbase + fn = "clean_$c.jl" + println(" * Verifying $c.jl ...") + include(fn) + end + end + end +end +``` + +The checks always run locally (when `RUNNER_OS` is not set); on GitHub runners they only +run when the `RSMD_CROSSWALK` environment variable is set to `TRUE` (see below). + +Finally, add the test dependencies to your `Project.toml` — `Git`, `JuliaFormatter`, +`Logging`, `ResearchSoftwareMetadata` and `Test` need to appear in `[extras]` (with their +UUIDs) and in the `test` list in `[targets]`. + +## Running them on CI + +On GitHub Actions, set `RSMD_CROSSWALK: TRUE` on one canonical runner rather than the +whole matrix — formatter output can differ between Julia and JuliaFormatter versions, so +running the checks everywhere invites spurious failures. With a matrix like this +package's, that looks like: + +```yaml + - uses: julia-actions/julia-runtest@v1 + env: + RSMD_CROSSWALK: ${{ (matrix.os == 'ubuntu-latest' && matrix.julia-version == '1') && 'TRUE' || 'FALSE' }} +``` + +Some things to be aware of: + +- The crosswalk queries orcid.org, ror.org, spdx.org and the Julia General registry, so + the runner needs network access, and it interrogates the git history and tags, so the + repository must be checked out in full (`fetch-depth: 0` on `actions/checkout`). +- Windows runners are skipped by both checks (marked as broken) due to file writing + issues. +- On CI the repository must be *strictly* clean — no staged, unstaged or untracked + changes after the checks run. Locally the criterion is relaxed: only unstaged changes + fail the tests, so work you have already staged doesn't stop you running the suite. diff --git a/src/crosswalk.jl b/src/crosswalk.jl index a7e6787..31af1f0 100644 --- a/src/crosswalk.jl +++ b/src/crosswalk.jl @@ -21,8 +21,10 @@ The software category can be set with the `category` argument, likewise the `key argument can contain a vector of keyword strings; both take precedence over and are written back into `Project.toml`. The `build` argument sets the `buildInstructions` RSMD field - `false` leaves the instructions as is, `true` sets it to the same as the README, -and a string sets it to that value. If `update` is true, mismatches between version numbers in -`codemeta.json` are accepted. If any remote metadata query (orcid.org, ror.org, spdx.org, +and a string sets it to that value. If `update` is true, changes to `Project.toml` (e.g. to +the version or the license) are treated as deliberate and propagated to the other metadata +files with `@info` messages instead of being reported as warnings or errors. +If any remote metadata query (orcid.org, ror.org, spdx.org, doi.org or Julia's General registry) cannot be completed, the crosswalk throws an error and all files are left in their original state. """ @@ -59,12 +61,13 @@ function crosswalk(git_dir = readchomp(`$(Git.git()) rev-parse --show-toplevel`) codemeta["@context"] = "https://w3id.org/codemeta/3.0" codemeta["type"] = "SoftwareSourceCode" isnothing(reconcile!(rsmd, codemeta, "category", "applicationCategory", - value = category)) && + value = category, update = update)) && @warn "No category metadata, add `category` to [rsmd] in Project.toml" codemeta["programmingLanguage"] = "julia" reconcile!(rsmd, codemeta, "development_status", "developmentStatus", - default = "active") - isnothing(reconcile!(rsmd, codemeta, "description", "description")) && + default = "active", update = update) + isnothing(reconcile!(rsmd, codemeta, "description", "description", + update = update)) && @warn "No description metadata, add `description` to [rsmd] in Project.toml" repo_index = "origin" ∈ remotes ? @@ -318,6 +321,12 @@ function crosswalk(git_dir = readchomp(`$(Git.git()) rev-parse --show-toplevel`) if codemeta["license"] == cm_license haslicense = true license = proj_license + elseif update + @info "Updating license in codemeta.json to match Project.toml " * + "($(codemeta["license"]) → $cm_license)" + codemeta["license"] = cm_license + haslicense = true + license = proj_license else @error "License mismatch between Project.toml and codemeta.json: " * "$(codemeta["license"]) ≠ $cm_license" @@ -440,7 +449,8 @@ function crosswalk(git_dir = readchomp(`$(Git.git()) rev-parse --show-toplevel`) keys_from_cm = author_key.(cm_authors) for dict in cm_authors if author_key(dict) ∉ keys_from_proj - @error "$dict not in Project.toml, removing from codemeta.json" + msg = "$dict not in Project.toml, removing from codemeta.json" + update ? (@info msg) : (@error msg) end end for dict in cm_from_proj @@ -485,10 +495,11 @@ function crosswalk(git_dir = readchomp(`$(Git.git()) rev-parse --show-toplevel`) end reconcile!(rsmd, codemeta, "keywords", "keywords", - value = keywords, default = ["julia"], to_cm = sort) + value = keywords, default = ["julia"], to_cm = sort, + update = update) publications = reconcile!(rsmd, codemeta, "publications", - "referencePublication", + "referencePublication", update = update, to_cm = dois -> "https://doi.org/" .* dois, from_cm = refs -> replace.(refs isa Vector ? refs : [refs], diff --git a/src/utils.jl b/src/utils.jl index 5de2d7d..dc96252 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -29,22 +29,25 @@ end """ ResearchSoftwareMetadata.reconcile!(project, codemeta, proj_key, cm_key; value = nothing, default = nothing, - to_cm = identity, from_cm = identity) + to_cm = identity, from_cm = identity, + update = false) Reconcile a metadata field between `Project.toml` (authoritative) and `codemeta.json`. An explicit `value` (e.g. from a keyword argument to `crosswalk`) takes precedence and is written into both; otherwise the `Project.toml` entry is used, fixing `codemeta.json` with a warning if it -disagrees. If the field is missing from `Project.toml` but present in -`codemeta.json`, it is backfilled into `Project.toml`. If it is absent -from both, `default` is used for `codemeta.json` (when provided) without -being backfilled. `to_cm` and `from_cm` convert values between the -`Project.toml` and `codemeta.json` representations. Returns the -`Project.toml`-side value, or `nothing` if the field is absent everywhere. +disagrees — or an informational message when `update` is true, meaning +the `Project.toml` change is deliberate and should just propagate. If the +field is missing from `Project.toml` but present in `codemeta.json`, it +is backfilled into `Project.toml`. If it is absent from both, `default` +is used for `codemeta.json` (when provided) without being backfilled. +`to_cm` and `from_cm` convert values between the `Project.toml` and +`codemeta.json` representations. Returns the `Project.toml`-side value, +or `nothing` if the field is absent everywhere. """ function reconcile!(project, codemeta, proj_key, cm_key; value = nothing, default = nothing, - to_cm = identity, from_cm = identity) + to_cm = identity, from_cm = identity, update = false) if !isnothing(value) project[proj_key] = value end @@ -53,8 +56,9 @@ function reconcile!(project, codemeta, proj_key, cm_key; cm_val = to_cm(val) if haskey(codemeta, cm_key) && codemeta[cm_key] ≠ cm_val && isnothing(value) - @warn "Fixing codemeta.json $cm_key to match Project.toml " * + msg = "Fixing codemeta.json $cm_key to match Project.toml " * "($(codemeta[cm_key]) ≠ $cm_val)" + update ? (@info msg) : (@warn msg) end codemeta[cm_key] = cm_val return val diff --git a/test/clean_JuliaFormatter.jl b/test/clean_JuliaFormatter.jl new file mode 100644 index 0000000..b3d4802 --- /dev/null +++ b/test/clean_JuliaFormatter.jl @@ -0,0 +1,23 @@ +# SPDX-License-Identifier: BSD-2-Clause + +module CleanJuliaFormatter +using Test +using Git +using JuliaFormatter +using ResearchSoftwareMetadata + +include("GitUtils.jl") +using .GitUtils + +# Does not currently work on Windows runners on GitHub due to file writing issues +if !haskey(ENV, "RUNNER_OS") || ENV["RUNNER_OS"] ≠ "Windows" + @testset "JuliaFormatter" begin + git_dir = readchomp(`$(Git.git()) rev-parse --show-toplevel`) + @test_nowarn format(ResearchSoftwareMetadata) + @test is_repo_clean(git_dir; strict = haskey(ENV, "RUNNER_OS")) + end +else + @test_broken !haskey(ENV, "RUNNER_OS") || ENV["RUNNER_OS"] ≠ "Windows" +end + +end diff --git a/test/clean_ResearchSoftwareMetadata.jl b/test/clean_ResearchSoftwareMetadata.jl new file mode 100644 index 0000000..b85140e --- /dev/null +++ b/test/clean_ResearchSoftwareMetadata.jl @@ -0,0 +1,26 @@ +# SPDX-License-Identifier: BSD-2-Clause + +module CleanRSMD +using Test +using Git +using Logging +using ResearchSoftwareMetadata + +include("GitUtils.jl") +using .GitUtils + +# Does not currently work on Windows runners on GitHub due to file writing issues +if !haskey(ENV, "RUNNER_OS") || ENV["RUNNER_OS"] ≠ "Windows" + @testset "RSMD" begin + git_dir = readchomp(`$(Git.git()) rev-parse --show-toplevel`) + @test isnothing(ResearchSoftwareMetadata.crosswalk()) + global_logger(SimpleLogger(stderr, Logging.Warn)) + @test_nowarn ResearchSoftwareMetadata.crosswalk() + global_logger(SimpleLogger(stderr, Logging.Info)) + @test is_repo_clean(git_dir; strict = haskey(ENV, "RUNNER_OS")) + end +else + @test_broken !haskey(ENV, "RUNNER_OS") || ENV["RUNNER_OS"] ≠ "Windows" +end + +end diff --git a/test/runtests.jl b/test/runtests.jl index 315edbc..9d37f44 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -10,13 +10,10 @@ using Test include("GitUtils.jl") using .GitUtils -@testset "ResearchSoftwareMetadata.jl" begin +@testset "Version bumping" begin git_dir = readchomp(`$(Git.git()) rev-parse --show-toplevel`) project = ResearchSoftwareMetadata.read_project() - @test isnothing(ResearchSoftwareMetadata.crosswalk()) @test_nowarn global_logger(SimpleLogger(stderr, Logging.Warn)) - @test_nowarn ResearchSoftwareMetadata.crosswalk() - @test is_repo_clean(git_dir) @test_nowarn ResearchSoftwareMetadata.increase_patch() @test_nowarn ResearchSoftwareMetadata.increase_minor() @test_nowarn ResearchSoftwareMetadata.increase_major() @@ -302,18 +299,31 @@ end @test !isfile(joinpath(dir, ".zenodo.json")) @test !isfile(joinpath(dir, "LICENSE")) end +end + +rsmd = get(ENV, "RSMD_CROSSWALK", "FALSE") +if rsmd == "TRUE" || !haskey(ENV, "RUNNER_OS") # Crosswalk runner or local testing + # Test RSMD crosswalk and other hygiene issues + + # Identify files that are checking package hygiene; use @__DIR__ because + # crosswalk() in earlier testsets leaves the working directory changed + cleanbase = map(file -> replace(file, r"clean_(.*).jl$" => s"\1"), + filter(str -> occursin(r"^clean_.*\.jl$", str), + readdir(@__DIR__))) + + if length(cleanbase) > 0 + @info "Crosswalk and clean testing:" + @testset begin + for c in cleanbase + println(" = $c") + end + println() - # Does not currently work on Windows runners on GitHub due to file writing issues - if !haskey(ENV, "RUNNER_OS") || ENV["RUNNER_OS"] ≠ "Windows" - @testset "RSMD" begin - git_dir = readchomp(`$(Git.git()) rev-parse --show-toplevel`) - @test isnothing(ResearchSoftwareMetadata.crosswalk()) - global_logger(SimpleLogger(stderr, Logging.Warn)) - @test_nowarn ResearchSoftwareMetadata.crosswalk() - global_logger(SimpleLogger(stderr, Logging.Info)) - @test is_repo_clean(git_dir; strict = haskey(ENV, "RUNNER_OS")) + @testset for c in cleanbase + fn = "clean_$c.jl" + println(" * Verifying $c.jl ...") + include(fn) + end end - else - @test_broken !haskey(ENV, "RUNNER_OS") || ENV["RUNNER_OS"] ≠ "Windows" end end From dfca66461139c461bcc45f258b89b91853aca28d Mon Sep 17 00:00:00 2001 From: Richard Reeve Date: Thu, 9 Jul 2026 15:55:48 +0100 Subject: [PATCH 4/6] Add in tests --- test/runtests.jl | 49 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/test/runtests.jl b/test/runtests.jl index 9d37f44..39756f0 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -284,6 +284,55 @@ end end end +@testset "Propagate Project.toml changes with update" begin + git_dir = readchomp(`$(Git.git()) rev-parse --show-toplevel`) + extra = """ + description = "A fixture package" + category = "metadata" + """ + # With update = true, deliberate Project.toml changes propagate with @info + mktempdir() do dir + make_fixture(dir, extra = extra) + @test isnothing(ResearchSoftwareMetadata.crosswalk(dir, build = true)) + cd(git_dir) # crosswalk leaves the working directory changed + toml = joinpath(dir, "Project.toml") + project = TOML.parsefile(toml) + project["license"] = "BSD-2-Clause" + project["rsmd"]["description"] = "An updated fixture package" + open(toml, "w") do io + return TOML.print(io, project) + end + @test_nowarn ResearchSoftwareMetadata.crosswalk(dir, update = true) + cd(git_dir) + codemeta = JSON.parsefile(joinpath(dir, "codemeta.json")) + @test codemeta["license"] == "https://spdx.org/licenses/BSD-2-Clause" + @test codemeta["description"] == "An updated fixture package" + zenodo = JSON.parsefile(joinpath(dir, ".zenodo.json")) + @test zenodo["license"] == "BSD-2-Clause" + @test zenodo["description"] == "An updated fixture package" + @test occursin("Redistribution", + read(joinpath(dir, "LICENSE"), String)) + src = readlines(joinpath(dir, "src", "RSMDFixture.jl")) + @test src[1] == "# SPDX-License-Identifier: BSD-2-Clause" + end + # Without update, a license mismatch is an error and is not propagated + mktempdir() do dir + make_fixture(dir, extra = extra) + @test isnothing(ResearchSoftwareMetadata.crosswalk(dir, build = true)) + cd(git_dir) # crosswalk leaves the working directory changed + toml = joinpath(dir, "Project.toml") + project = TOML.parsefile(toml) + project["license"] = "BSD-2-Clause" + open(toml, "w") do io + return TOML.print(io, project) + end + @test_logs (:error, r"License mismatch") match_mode=:any ResearchSoftwareMetadata.crosswalk(dir) + cd(git_dir) + codemeta = JSON.parsefile(joinpath(dir, "codemeta.json")) + @test codemeta["license"] == "https://spdx.org/licenses/MIT" + end +end + @testset "Failed crosswalk leaves files unchanged" begin git_dir = readchomp(`$(Git.git()) rev-parse --show-toplevel`) mktempdir() do dir From c99e3f2a9f487e6eee8d9b4732f12f7ed5b0ca80 Mon Sep 17 00:00:00 2001 From: Richard Reeve Date: Thu, 9 Jul 2026 15:56:48 +0100 Subject: [PATCH 5/6] Change license throughout to MIT to make it easier for people to copy test code out into their own repos. --- .zenodo.json | 2 +- LICENSE | 19 ++++++++++++++----- Project.toml | 2 +- codemeta.json | 2 +- docs/make.jl | 2 +- docs/metadata.jl | 2 +- src/ResearchSoftwareMetadata.jl | 2 +- src/crosswalk.jl | 2 +- src/project.jl | 2 +- src/remotequeries.jl | 2 +- src/utils.jl | 2 +- src/versioning.jl | 2 +- test/GitUtils.jl | 2 +- test/clean_JuliaFormatter.jl | 2 +- test/clean_ResearchSoftwareMetadata.jl | 2 +- test/runtests.jl | 2 +- 16 files changed, 29 insertions(+), 20 deletions(-) diff --git a/.zenodo.json b/.zenodo.json index f724c0f..115f768 100644 --- a/.zenodo.json +++ b/.zenodo.json @@ -11,7 +11,7 @@ } ], "access_right": "open", - "license": "BSD-2-Clause", + "license": "MIT", "related_identifiers": [ { "scheme": "url", diff --git a/LICENSE b/LICENSE index 1c5fcb4..05bad3a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,9 +1,18 @@ -Copyright (c) 2024-2026 Richard Reeve +MIT License -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: +Copyright (c) 2024-2026 Richard Reeve -1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: -2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Project.toml b/Project.toml index 403cf67..d1e5f38 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "ResearchSoftwareMetadata" uuid = "58378933-4625-47fa-851e-05ee27d397bd" -license = "BSD-2-Clause" +license = "MIT" version = "0.3.0" authors = ["Richard Reeve "] diff --git a/codemeta.json b/codemeta.json index e52543f..e2ac351 100644 --- a/codemeta.json +++ b/codemeta.json @@ -18,7 +18,7 @@ "dateModified": "2026-07-09", "datePublished": "2024-07-18", "downloadUrl": "https://github.com/boydorr/ResearchSoftwareMetadata.jl/archive/refs/tags/v0.3.0.tar.gz", - "license": "https://spdx.org/licenses/BSD-2-Clause", + "license": "https://spdx.org/licenses/MIT", "author": [ { "type": "Person", diff --git a/docs/make.jl b/docs/make.jl index c03bdb9..89554a9 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,4 +1,4 @@ -# SPDX-License-Identifier: BSD-2-Clause +# SPDX-License-Identifier: MIT using Pkg "ResearchSoftwareMetadata" ∈ [p.name for p in values(Pkg.dependencies())] && diff --git a/docs/metadata.jl b/docs/metadata.jl index 9b5147a..9277fb5 100644 --- a/docs/metadata.jl +++ b/docs/metadata.jl @@ -1,4 +1,4 @@ -# SPDX-License-Identifier: BSD-2-Clause +# SPDX-License-Identifier: MIT using Pkg diff --git a/src/ResearchSoftwareMetadata.jl b/src/ResearchSoftwareMetadata.jl index 9e4edf0..27d7819 100644 --- a/src/ResearchSoftwareMetadata.jl +++ b/src/ResearchSoftwareMetadata.jl @@ -1,4 +1,4 @@ -# SPDX-License-Identifier: BSD-2-Clause +# SPDX-License-Identifier: MIT """ ResearchSoftwareMetadata diff --git a/src/crosswalk.jl b/src/crosswalk.jl index 31af1f0..5377dec 100644 --- a/src/crosswalk.jl +++ b/src/crosswalk.jl @@ -1,4 +1,4 @@ -# SPDX-License-Identifier: BSD-2-Clause +# SPDX-License-Identifier: MIT """ crosswalk(git_dir; category = nothing, keywords = nothing, diff --git a/src/project.jl b/src/project.jl index d24bb90..e5703b7 100644 --- a/src/project.jl +++ b/src/project.jl @@ -1,4 +1,4 @@ -# SPDX-License-Identifier: BSD-2-Clause +# SPDX-License-Identifier: MIT # Pkg's canonical Project.toml key order (Pkg/src/project.jl, Julia 1.13) const PROJECT_KEY_ORDER = ["name", "uuid", "keywords", "license", "desc", diff --git a/src/remotequeries.jl b/src/remotequeries.jl index 3d595bb..4c1bad1 100644 --- a/src/remotequeries.jl +++ b/src/remotequeries.jl @@ -1,4 +1,4 @@ -# SPDX-License-Identifier: BSD-2-Clause +# SPDX-License-Identifier: MIT """ ResearchSoftwareMetadata.get_person_from_orcid(orcid::String) diff --git a/src/utils.jl b/src/utils.jl index dc96252..ea12c07 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -1,4 +1,4 @@ -# SPDX-License-Identifier: BSD-2-Clause +# SPDX-License-Identifier: MIT """ ResearchSoftwareMetadata.split_name(full_name::AbstractString) diff --git a/src/versioning.jl b/src/versioning.jl index e33e805..ef48350 100644 --- a/src/versioning.jl +++ b/src/versioning.jl @@ -1,4 +1,4 @@ -# SPDX-License-Identifier: BSD-2-Clause +# SPDX-License-Identifier: MIT """ increase_patch() diff --git a/test/GitUtils.jl b/test/GitUtils.jl index 8b1ae3c..b4e88af 100644 --- a/test/GitUtils.jl +++ b/test/GitUtils.jl @@ -1,4 +1,4 @@ -# SPDX-License-Identifier: BSD-2-Clause +# SPDX-License-Identifier: MIT module GitUtils diff --git a/test/clean_JuliaFormatter.jl b/test/clean_JuliaFormatter.jl index b3d4802..b6f70ed 100644 --- a/test/clean_JuliaFormatter.jl +++ b/test/clean_JuliaFormatter.jl @@ -1,4 +1,4 @@ -# SPDX-License-Identifier: BSD-2-Clause +# SPDX-License-Identifier: MIT module CleanJuliaFormatter using Test diff --git a/test/clean_ResearchSoftwareMetadata.jl b/test/clean_ResearchSoftwareMetadata.jl index b85140e..550197e 100644 --- a/test/clean_ResearchSoftwareMetadata.jl +++ b/test/clean_ResearchSoftwareMetadata.jl @@ -1,4 +1,4 @@ -# SPDX-License-Identifier: BSD-2-Clause +# SPDX-License-Identifier: MIT module CleanRSMD using Test diff --git a/test/runtests.jl b/test/runtests.jl index 39756f0..929db6a 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,4 +1,4 @@ -# SPDX-License-Identifier: BSD-2-Clause +# SPDX-License-Identifier: MIT using Git using JSON From 4a9ff6a6d5922aaaafa7eafebd25f77e93a72cc2 Mon Sep 17 00:00:00 2001 From: Richard Reeve Date: Thu, 9 Jul 2026 15:58:22 +0100 Subject: [PATCH 6/6] Update NEWS.md --- NEWS.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index 75cafbb..1cac4bb 100644 --- a/NEWS.md +++ b/NEWS.md @@ -3,7 +3,9 @@ - v0.3.0 - Move the RSMD-specific Project.toml keys (description, keywords, category, development_status, publications and author_details) into a single [rsmd] table, migrating legacy top-level keys there automatically - Write Project.toml in Pkg's canonical key order, applied recursively to nested tables, so Pkg operations like add and rm no longer reorder the file - - Fix errors and stale examples in the README and documentation + - Fix errors and stale examples in the README and documentation, and problem with propagating updates + - Add in demo tests for other packages to adopt and explain how to use them. + - Update license to MIT to make adopting tests in other packages simpler - v0.2.0 - Include authors without ORCIDs in codemeta.json and .zenodo.json - Handle missing ORCID and ROR records without crashing