Skip to content

Commit a0d2bbf

Browse files
authored
Merge pull request #70 from JuliaQUBO/docs/issue-69-zenodo-citation
Add Zenodo citation metadata and release verification
2 parents 60f54b7 + a84dc39 commit a0d2bbf

9 files changed

Lines changed: 348 additions & 0 deletions

File tree

.github/RELEASE_CHECKLIST.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Release checklist
2+
3+
Use this checklist for every tagged release. The Zenodo concept DOI
4+
`10.5281/zenodo.7812066` is the permanent DWave.jl software identifier; do not
5+
create a new concept record for a new version.
6+
7+
## Before tagging
8+
9+
- [ ] Confirm that at least two active JuliaQUBO maintainers have **Can manage**
10+
access to the existing Zenodo deposit. Record the stewards and review
11+
date in the ecosystem tracker, JuliaQUBO/QUBO.jl#66.
12+
- [ ] Update the version in `Project.toml` and the version and release date in
13+
`CITATION.cff`.
14+
- [ ] Keep the concept DOI in `CITATION.cff` and the README badge unchanged.
15+
- [ ] Validate the citation metadata with
16+
`cffconvert --validate --infile CITATION.cff`.
17+
- [ ] Run the package test suite with
18+
`julia --project -e 'using Pkg; Pkg.test()'`.
19+
20+
## After publishing the GitHub release
21+
22+
- [ ] Confirm the **Zenodo release verification** workflow passes. It retries
23+
for six minutes and then fails if the latest record under concept DOI
24+
`10.5281/zenodo.7812066` does not match the GitHub release tag.
25+
- [ ] If Zenodo publication outlasts that bounded retry window, wait for the
26+
archive to appear and rerun the workflow manually with the release tag.
27+
A persistent mismatch means the release integration needs repair; it is
28+
not a reason to mint a new concept DOI.
29+
- [ ] Confirm the record's version, MIT license, repository URL, Julia package
30+
UUID (`4d534982-bf11-4157-9e48-fe3a62208a50`), creators, and archive
31+
contents.
32+
- [ ] Record the new version DOI in the GitHub release notes and update the
33+
README's exact-version example.
34+
- [ ] Download the Zenodo archive and confirm its `Project.toml` version matches
35+
the GitHub tag.

.github/workflows/zenodo.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: Zenodo release verification
2+
3+
on:
4+
release:
5+
types:
6+
- published
7+
workflow_dispatch:
8+
inputs:
9+
release_tag:
10+
description: "GitHub release tag expected in the latest Zenodo record"
11+
required: true
12+
type: string
13+
14+
permissions:
15+
contents: read
16+
17+
jobs:
18+
verify:
19+
name: Verify Zenodo archive
20+
runs-on: ubuntu-latest
21+
timeout-minutes: 10
22+
env:
23+
EXPECTED_RELEASE_TAG: ${{ github.event.release.tag_name || inputs.release_tag }}
24+
ZENODO_VERIFY_ATTEMPTS: "12"
25+
ZENODO_VERIFY_DELAY_SECONDS: "30"
26+
steps:
27+
- uses: actions/checkout@v7
28+
- uses: julia-actions/setup-julia@v3
29+
with:
30+
version: "1.10"
31+
- name: Instantiate the verification environment
32+
run: julia --project=scripts -e 'using Pkg; Pkg.instantiate()'
33+
- name: Compare the release with Zenodo
34+
run: julia --project=scripts scripts/verify_zenodo_release.jl "${EXPECTED_RELEASE_TAG}"

CITATION.cff

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
cff-version: 1.2.0
2+
message: >-
3+
If your work depends directly on DWave.jl, please cite the software using
4+
this metadata. For general discussion of the JuliaQUBO ecosystem, cite the
5+
related QUBO.jl article.
6+
title: "DWave.jl"
7+
type: software
8+
authors:
9+
- family-names: "Bernal Neira"
10+
given-names: "David E."
11+
orcid: "https://orcid.org/0000-0002-8308-5016"
12+
- family-names: "Maciel Xavier"
13+
given-names: "Pedro"
14+
orcid: "https://orcid.org/0000-0002-4678-4942"
15+
- family-names: "Ripper"
16+
given-names: "Pedro"
17+
orcid: "https://orcid.org/0000-0002-2227-3689"
18+
abstract: >-
19+
A Julia interface for D-Wave quantum annealers and the classical samplers
20+
distributed with the D-Wave Ocean SDK.
21+
repository-code: "https://github.com/JuliaQUBO/DWave.jl"
22+
license: MIT
23+
version: "0.7.6"
24+
date-released: 2026-06-30
25+
doi: "10.5281/zenodo.7812066"
26+
keywords:
27+
- Julia
28+
- quantum annealing
29+
- quadratic unconstrained binary optimization
30+
- QUBO
31+
references:
32+
- type: article
33+
authors:
34+
- family-names: "Maciel Xavier"
35+
given-names: "Pedro"
36+
orcid: "https://orcid.org/0000-0002-4678-4942"
37+
- family-names: "Ripper"
38+
given-names: "Pedro"
39+
orcid: "https://orcid.org/0000-0002-2227-3689"
40+
- family-names: "Andrade"
41+
given-names: "Tiago"
42+
- family-names: "Dias Garcia"
43+
given-names: "Joaquim"
44+
orcid: "https://orcid.org/0000-0002-7721-8564"
45+
- family-names: "Maculan"
46+
given-names: "Nelson"
47+
orcid: "https://orcid.org/0000-0002-3897-3356"
48+
- family-names: "Bernal Neira"
49+
given-names: "David E."
50+
orcid: "https://orcid.org/0000-0002-8308-5016"
51+
title: "QUBO.jl: A Julia Ecosystem for Quadratic Unconstrained Binary Optimization"
52+
journal: "Optimization Methods and Software"
53+
year: 2026
54+
doi: "10.1080/10556788.2026.2702926"

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# DWave.jl
22
[![CI](https://github.com/JuliaQUBO/DWave.jl/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/JuliaQUBO/DWave.jl/actions/workflows/ci.yml?query=branch%3Amain)
33
[![QUBODRIVERS](https://img.shields.io/badge/Powered%20by-QUBODrivers.jl-%20%234063d8)](https://github.com/JuliaQUBO/QUBODrivers.jl)
4+
[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.7812066.svg)](https://doi.org/10.5281/zenodo.7812066)
45

56
D-Wave Quantum Annealing Interface for JuMP
67

@@ -144,6 +145,19 @@ Pass `ax = existing_axis` to draw into an existing Matplotlib axis; all other
144145
keyword arguments are forwarded to the corresponding D-Wave NetworkX draw
145146
function.
146147

148+
## Citation
149+
150+
For work that depends directly on this D-Wave integration, cite `DWave.jl`
151+
using [`CITATION.cff`](CITATION.cff) or its
152+
[Zenodo concept DOI](https://doi.org/10.5281/zenodo.7812066). The concept DOI is
153+
the evergreen software identifier and resolves to the latest archived release.
154+
155+
For general discussion of the JuliaQUBO ecosystem, cite the
156+
[QUBO.jl ecosystem article](https://doi.org/10.1080/10556788.2026.2702926).
157+
When exact-release reproducibility matters, cite the corresponding Zenodo
158+
version DOI instead; the archive for `v0.7.6` is
159+
[10.5281/zenodo.21078136](https://doi.org/10.5281/zenodo.21078136).
160+
147161
## Development Checks
148162
To verify that DWave can share a CondaPkg environment with the other
149163
Python-backed JuliaQUBO benchmark drivers, run:

scripts/Project.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[deps]
2+
Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6"
3+
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
4+
5+
[compat]
6+
JSON = "0.21, 1.6"
7+
julia = "1.10"

scripts/verify_zenodo_release.jl

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
module ZenodoReleaseVerification
2+
3+
import Downloads
4+
import JSON
5+
6+
const CONCEPT_RECORD_ID = "7812066"
7+
const DEFAULT_API_URL = "https://zenodo.org/api/records/$CONCEPT_RECORD_ID/versions/latest"
8+
9+
normalize_version(value) = replace(strip(string(value)), r"^[vV]" => "")
10+
11+
function check_record(record::AbstractDict, expected_tag::AbstractString)
12+
concept_record_id = string(get(record, "conceptrecid", ""))
13+
if concept_record_id != CONCEPT_RECORD_ID
14+
return (
15+
ok = false,
16+
message = "Zenodo returned concept record '$concept_record_id', expected '$CONCEPT_RECORD_ID'.",
17+
doi = nothing,
18+
)
19+
end
20+
21+
metadata = get(record, "metadata", nothing)
22+
if !(metadata isa AbstractDict) || !haskey(metadata, "version")
23+
return (
24+
ok = false,
25+
message = "Zenodo's latest record does not declare metadata.version.",
26+
doi = nothing,
27+
)
28+
end
29+
30+
archived_version = string(metadata["version"])
31+
if normalize_version(archived_version) != normalize_version(expected_tag)
32+
return (
33+
ok = false,
34+
message = "Zenodo's latest version is '$archived_version', expected '$expected_tag'.",
35+
doi = get(record, "doi", nothing),
36+
)
37+
end
38+
39+
return (
40+
ok = true,
41+
message = "Zenodo record $(get(record, "doi", "without a DOI")) matches release '$expected_tag'.",
42+
doi = get(record, "doi", nothing),
43+
)
44+
end
45+
46+
function fetch_record(url::AbstractString)
47+
output = IOBuffer()
48+
response = Downloads.request(url; output, timeout = 30, throw = false)
49+
hasproperty(response, :status) || error(sprint(showerror, response))
50+
response.status == 200 || error("Zenodo returned HTTP $(response.status).")
51+
52+
return JSON.parse(String(take!(output)))
53+
end
54+
55+
function verify_release(
56+
expected_tag::AbstractString;
57+
api_url::AbstractString = get(ENV, "ZENODO_API_URL", DEFAULT_API_URL),
58+
attempts::Int = parse(Int, get(ENV, "ZENODO_VERIFY_ATTEMPTS", "12")),
59+
delay_seconds::Real = parse(Float64, get(ENV, "ZENODO_VERIFY_DELAY_SECONDS", "30")),
60+
record_fetcher = fetch_record,
61+
)
62+
isempty(strip(expected_tag)) && throw(ArgumentError("The expected release tag is empty."))
63+
attempts > 0 || throw(ArgumentError("ZENODO_VERIFY_ATTEMPTS must be positive."))
64+
delay_seconds >= 0 || throw(ArgumentError("ZENODO_VERIFY_DELAY_SECONDS must be nonnegative."))
65+
66+
last_failure = "No verification attempt ran."
67+
68+
for attempt in 1:attempts
69+
try
70+
result = check_record(record_fetcher(api_url), expected_tag)
71+
result.ok && return result
72+
last_failure = result.message
73+
catch error
74+
last_failure = sprint(showerror, error)
75+
end
76+
77+
if attempt < attempts
78+
println(
79+
stderr,
80+
"Zenodo verification attempt $attempt/$attempts failed: $last_failure Retrying in $(delay_seconds) seconds.",
81+
)
82+
sleep(delay_seconds)
83+
end
84+
end
85+
86+
error("Zenodo release verification failed after $attempts attempts: $last_failure")
87+
end
88+
89+
function main(args)
90+
length(args) == 1 || throw(ArgumentError("Usage: verify_zenodo_release.jl <release-tag>"))
91+
result = verify_release(only(args))
92+
println(result.message)
93+
94+
return nothing
95+
end
96+
97+
end
98+
99+
if abspath(PROGRAM_FILE) == @__FILE__
100+
try
101+
ZenodoReleaseVerification.main(ARGS)
102+
catch error
103+
println(stderr, "ERROR: ", sprint(showerror, error))
104+
exit(1)
105+
end
106+
end

test/Project.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
[deps]
2+
Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6"
3+
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
24
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
35
QUBODrivers = "a3f166f7-2cd3-47b6-9e1e-6fbfe0449eb0"
46
QUBOTools = "60eb5b62-0a39-4ddc-84c5-97d2adff9319"

test/citation.jl

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import Test
2+
import TOML
3+
4+
const CITATION_PACKAGE_ROOT = normpath(joinpath(@__DIR__, ".."))
5+
6+
include(joinpath(CITATION_PACKAGE_ROOT, "scripts", "verify_zenodo_release.jl"))
7+
8+
function _zenodo_record(; concept = "7812066", version = "v0.7.6")
9+
return Dict(
10+
"conceptrecid" => concept,
11+
"doi" => "10.5281/zenodo.21078136",
12+
"metadata" => Dict("version" => version),
13+
)
14+
end
15+
16+
Test.@testset "Citation metadata is consistent" begin
17+
read_text(path) = replace(read(path, String), "\r\n" => "\n")
18+
19+
project = TOML.parsefile(joinpath(CITATION_PACKAGE_ROOT, "Project.toml"))
20+
citation = read_text(joinpath(CITATION_PACKAGE_ROOT, "CITATION.cff"))
21+
readme = read_text(joinpath(CITATION_PACKAGE_ROOT, "README.md"))
22+
checklist = read_text(joinpath(CITATION_PACKAGE_ROOT, ".github", "RELEASE_CHECKLIST.md"))
23+
workflow = read_text(joinpath(CITATION_PACKAGE_ROOT, ".github", "workflows", "zenodo.yml"))
24+
25+
concept_doi = "10.5281/zenodo.7812066"
26+
version_doi = "10.5281/zenodo.21078136"
27+
article_doi = "10.1080/10556788.2026.2702926"
28+
version = string(project["version"])
29+
30+
Test.@test occursin("doi: \"$concept_doi\"", citation)
31+
Test.@test occursin("version: \"$version\"", citation)
32+
Test.@test occursin(article_doi, citation)
33+
34+
Test.@test occursin("badge/DOI/$concept_doi.svg", readme)
35+
Test.@test occursin("doi.org/$concept_doi", readme)
36+
Test.@test occursin("doi.org/$version_doi", readme)
37+
Test.@test occursin("doi.org/$article_doi", readme)
38+
39+
Test.@test occursin(concept_doi, checklist)
40+
Test.@test occursin("Can manage", checklist)
41+
Test.@test occursin("Zenodo release verification", checklist)
42+
Test.@test occursin(r"(?m)^\s*release:\s*$", workflow)
43+
Test.@test occursin(r"(?m)^\s*-\s*published\s*$", workflow)
44+
Test.@test occursin("scripts/verify_zenodo_release.jl", workflow)
45+
end
46+
47+
Test.@testset "Zenodo release comparison detects archive drift" begin
48+
current = ZenodoReleaseVerification.check_record(_zenodo_record(), "v0.7.6")
49+
stale = ZenodoReleaseVerification.check_record(
50+
_zenodo_record(; version = "v0.7.5"),
51+
"v0.7.6",
52+
)
53+
wrong_concept = ZenodoReleaseVerification.check_record(
54+
_zenodo_record(; concept = "9999999"),
55+
"v0.7.6",
56+
)
57+
58+
Test.@test current.ok
59+
Test.@test current.doi == "10.5281/zenodo.21078136"
60+
Test.@test !stale.ok
61+
Test.@test occursin("v0.7.5", stale.message)
62+
Test.@test !wrong_concept.ok
63+
Test.@test occursin("9999999", wrong_concept.message)
64+
65+
responses = Any[_zenodo_record(; version = "v0.7.5"), _zenodo_record()]
66+
result = ZenodoReleaseVerification.verify_release(
67+
"v0.7.6";
68+
attempts = 2,
69+
delay_seconds = 0,
70+
record_fetcher = _ -> popfirst!(responses),
71+
)
72+
73+
Test.@test result.ok
74+
Test.@test isempty(responses)
75+
Test.@test_throws ErrorException ZenodoReleaseVerification.verify_release(
76+
"v0.7.6";
77+
attempts = 1,
78+
delay_seconds = 0,
79+
record_fetcher = _ -> _zenodo_record(; version = "v0.7.5"),
80+
)
81+
82+
transport_error = try
83+
ZenodoReleaseVerification.verify_release(
84+
"v0.7.6";
85+
attempts = 1,
86+
delay_seconds = 0,
87+
record_fetcher = _ -> error("network unavailable"),
88+
)
89+
nothing
90+
catch error
91+
sprint(showerror, error)
92+
end
93+
94+
Test.@test occursin("network unavailable", transport_error)
95+
end

test/runtests.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import DWave
22
import QUBODrivers
33

44
include("compat_metadata.jl")
5+
include("citation.jl")
56
include("auth.jl")
67

78
if DWave.__auth__(; verbose = false)

0 commit comments

Comments
 (0)