Skip to content
Merged
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
35 changes: 35 additions & 0 deletions .github/RELEASE_CHECKLIST.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Release checklist

Use this checklist for every tagged release. The Zenodo concept DOI
`10.5281/zenodo.7812066` is the permanent DWave.jl software identifier; do not
create a new concept record for a new version.

## Before tagging

- [ ] Confirm that at least two active JuliaQUBO maintainers have **Can manage**
access to the existing Zenodo deposit. Record the stewards and review
date in the ecosystem tracker, JuliaQUBO/QUBO.jl#66.
- [ ] Update the version in `Project.toml` and the version and release date in
`CITATION.cff`.
- [ ] Keep the concept DOI in `CITATION.cff` and the README badge unchanged.
- [ ] Validate the citation metadata with
`cffconvert --validate --infile CITATION.cff`.
- [ ] Run the package test suite with
`julia --project -e 'using Pkg; Pkg.test()'`.

## After publishing the GitHub release

- [ ] Confirm the **Zenodo release verification** workflow passes. It retries
for six minutes and then fails if the latest record under concept DOI
`10.5281/zenodo.7812066` does not match the GitHub release tag.
- [ ] If Zenodo publication outlasts that bounded retry window, wait for the
archive to appear and rerun the workflow manually with the release tag.
A persistent mismatch means the release integration needs repair; it is
not a reason to mint a new concept DOI.
- [ ] Confirm the record's version, MIT license, repository URL, Julia package
UUID (`4d534982-bf11-4157-9e48-fe3a62208a50`), creators, and archive
contents.
- [ ] Record the new version DOI in the GitHub release notes and update the
README's exact-version example.
- [ ] Download the Zenodo archive and confirm its `Project.toml` version matches
the GitHub tag.
34 changes: 34 additions & 0 deletions .github/workflows/zenodo.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Zenodo release verification

on:
release:
types:
- published
workflow_dispatch:
inputs:
release_tag:
description: "GitHub release tag expected in the latest Zenodo record"
required: true
type: string

permissions:
contents: read

jobs:
verify:
name: Verify Zenodo archive
runs-on: ubuntu-latest
timeout-minutes: 10
env:
EXPECTED_RELEASE_TAG: ${{ github.event.release.tag_name || inputs.release_tag }}
ZENODO_VERIFY_ATTEMPTS: "12"
ZENODO_VERIFY_DELAY_SECONDS: "30"
steps:
- uses: actions/checkout@v7
- uses: julia-actions/setup-julia@v3
with:
version: "1.10"
- name: Instantiate the verification environment
run: julia --project=scripts -e 'using Pkg; Pkg.instantiate()'
- name: Compare the release with Zenodo
run: julia --project=scripts scripts/verify_zenodo_release.jl "${EXPECTED_RELEASE_TAG}"
54 changes: 54 additions & 0 deletions CITATION.cff
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
cff-version: 1.2.0
message: >-
If your work depends directly on DWave.jl, please cite the software using
this metadata. For general discussion of the JuliaQUBO ecosystem, cite the
related QUBO.jl article.
title: "DWave.jl"
type: software
authors:
- family-names: "Bernal Neira"
given-names: "David E."
orcid: "https://orcid.org/0000-0002-8308-5016"
- family-names: "Maciel Xavier"
given-names: "Pedro"
orcid: "https://orcid.org/0000-0002-4678-4942"
- family-names: "Ripper"
given-names: "Pedro"
orcid: "https://orcid.org/0000-0002-2227-3689"
abstract: >-
A Julia interface for D-Wave quantum annealers and the classical samplers
distributed with the D-Wave Ocean SDK.
repository-code: "https://github.com/JuliaQUBO/DWave.jl"
license: MIT
version: "0.7.6"
date-released: 2026-06-30
doi: "10.5281/zenodo.7812066"
keywords:
- Julia
- quantum annealing
- quadratic unconstrained binary optimization
- QUBO
references:
- type: article
authors:
- family-names: "Maciel Xavier"
given-names: "Pedro"
orcid: "https://orcid.org/0000-0002-4678-4942"
- family-names: "Ripper"
given-names: "Pedro"
orcid: "https://orcid.org/0000-0002-2227-3689"
- family-names: "Andrade"
given-names: "Tiago"
- family-names: "Dias Garcia"
given-names: "Joaquim"
orcid: "https://orcid.org/0000-0002-7721-8564"
- family-names: "Maculan"
given-names: "Nelson"
orcid: "https://orcid.org/0000-0002-3897-3356"
- family-names: "Bernal Neira"
given-names: "David E."
orcid: "https://orcid.org/0000-0002-8308-5016"
title: "QUBO.jl: A Julia Ecosystem for Quadratic Unconstrained Binary Optimization"
journal: "Optimization Methods and Software"
year: 2026
doi: "10.1080/10556788.2026.2702926"
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# DWave.jl
[![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)
[![QUBODRIVERS](https://img.shields.io/badge/Powered%20by-QUBODrivers.jl-%20%234063d8)](https://github.com/JuliaQUBO/QUBODrivers.jl)
[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.7812066.svg)](https://doi.org/10.5281/zenodo.7812066)

D-Wave Quantum Annealing Interface for JuMP

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

## Citation

For work that depends directly on this D-Wave integration, cite `DWave.jl`
using [`CITATION.cff`](CITATION.cff) or its
[Zenodo concept DOI](https://doi.org/10.5281/zenodo.7812066). The concept DOI is
the evergreen software identifier and resolves to the latest archived release.

For general discussion of the JuliaQUBO ecosystem, cite the
[QUBO.jl ecosystem article](https://doi.org/10.1080/10556788.2026.2702926).
When exact-release reproducibility matters, cite the corresponding Zenodo
version DOI instead; the archive for `v0.7.6` is
[10.5281/zenodo.21078136](https://doi.org/10.5281/zenodo.21078136).

## Development Checks
To verify that DWave can share a CondaPkg environment with the other
Python-backed JuliaQUBO benchmark drivers, run:
Expand Down
7 changes: 7 additions & 0 deletions scripts/Project.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[deps]
Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6"
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"

[compat]
JSON = "0.21, 1.6"
julia = "1.10"
106 changes: 106 additions & 0 deletions scripts/verify_zenodo_release.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
module ZenodoReleaseVerification

import Downloads
import JSON

const CONCEPT_RECORD_ID = "7812066"
const DEFAULT_API_URL = "https://zenodo.org/api/records/$CONCEPT_RECORD_ID/versions/latest"

normalize_version(value) = replace(strip(string(value)), r"^[vV]" => "")

function check_record(record::AbstractDict, expected_tag::AbstractString)
concept_record_id = string(get(record, "conceptrecid", ""))
if concept_record_id != CONCEPT_RECORD_ID
return (
ok = false,
message = "Zenodo returned concept record '$concept_record_id', expected '$CONCEPT_RECORD_ID'.",
doi = nothing,
)
end

metadata = get(record, "metadata", nothing)
if !(metadata isa AbstractDict) || !haskey(metadata, "version")
return (
ok = false,
message = "Zenodo's latest record does not declare metadata.version.",
doi = nothing,
)
end

archived_version = string(metadata["version"])
if normalize_version(archived_version) != normalize_version(expected_tag)
return (
ok = false,
message = "Zenodo's latest version is '$archived_version', expected '$expected_tag'.",
doi = get(record, "doi", nothing),
)
end

return (
ok = true,
message = "Zenodo record $(get(record, "doi", "without a DOI")) matches release '$expected_tag'.",
doi = get(record, "doi", nothing),
)
end

function fetch_record(url::AbstractString)
output = IOBuffer()
response = Downloads.request(url; output, timeout = 30, throw = false)
hasproperty(response, :status) || error(sprint(showerror, response))
response.status == 200 || error("Zenodo returned HTTP $(response.status).")

return JSON.parse(String(take!(output)))
end

function verify_release(
expected_tag::AbstractString;
api_url::AbstractString = get(ENV, "ZENODO_API_URL", DEFAULT_API_URL),
attempts::Int = parse(Int, get(ENV, "ZENODO_VERIFY_ATTEMPTS", "12")),
delay_seconds::Real = parse(Float64, get(ENV, "ZENODO_VERIFY_DELAY_SECONDS", "30")),
record_fetcher = fetch_record,
)
isempty(strip(expected_tag)) && throw(ArgumentError("The expected release tag is empty."))
attempts > 0 || throw(ArgumentError("ZENODO_VERIFY_ATTEMPTS must be positive."))
delay_seconds >= 0 || throw(ArgumentError("ZENODO_VERIFY_DELAY_SECONDS must be nonnegative."))

last_failure = "No verification attempt ran."

for attempt in 1:attempts
try
result = check_record(record_fetcher(api_url), expected_tag)
result.ok && return result
last_failure = result.message
catch error
last_failure = sprint(showerror, error)
end

if attempt < attempts
println(
stderr,
"Zenodo verification attempt $attempt/$attempts failed: $last_failure Retrying in $(delay_seconds) seconds.",
)
sleep(delay_seconds)
end
end

error("Zenodo release verification failed after $attempts attempts: $last_failure")
end

function main(args)
length(args) == 1 || throw(ArgumentError("Usage: verify_zenodo_release.jl <release-tag>"))
result = verify_release(only(args))
println(result.message)

return nothing
end

end

if abspath(PROGRAM_FILE) == @__FILE__
try
ZenodoReleaseVerification.main(ARGS)
catch error
println(stderr, "ERROR: ", sprint(showerror, error))
exit(1)
end
end
2 changes: 2 additions & 0 deletions test/Project.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
[deps]
Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6"
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
QUBODrivers = "a3f166f7-2cd3-47b6-9e1e-6fbfe0449eb0"
QUBOTools = "60eb5b62-0a39-4ddc-84c5-97d2adff9319"
Expand Down
95 changes: 95 additions & 0 deletions test/citation.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import Test
import TOML

const CITATION_PACKAGE_ROOT = normpath(joinpath(@__DIR__, ".."))

include(joinpath(CITATION_PACKAGE_ROOT, "scripts", "verify_zenodo_release.jl"))

function _zenodo_record(; concept = "7812066", version = "v0.7.6")
return Dict(
"conceptrecid" => concept,
"doi" => "10.5281/zenodo.21078136",
"metadata" => Dict("version" => version),
)
end

Test.@testset "Citation metadata is consistent" begin
read_text(path) = replace(read(path, String), "\r\n" => "\n")

project = TOML.parsefile(joinpath(CITATION_PACKAGE_ROOT, "Project.toml"))
citation = read_text(joinpath(CITATION_PACKAGE_ROOT, "CITATION.cff"))
readme = read_text(joinpath(CITATION_PACKAGE_ROOT, "README.md"))
checklist = read_text(joinpath(CITATION_PACKAGE_ROOT, ".github", "RELEASE_CHECKLIST.md"))
workflow = read_text(joinpath(CITATION_PACKAGE_ROOT, ".github", "workflows", "zenodo.yml"))

concept_doi = "10.5281/zenodo.7812066"
version_doi = "10.5281/zenodo.21078136"
article_doi = "10.1080/10556788.2026.2702926"
version = string(project["version"])

Test.@test occursin("doi: \"$concept_doi\"", citation)
Test.@test occursin("version: \"$version\"", citation)
Test.@test occursin(article_doi, citation)

Test.@test occursin("badge/DOI/$concept_doi.svg", readme)
Test.@test occursin("doi.org/$concept_doi", readme)
Test.@test occursin("doi.org/$version_doi", readme)
Test.@test occursin("doi.org/$article_doi", readme)

Test.@test occursin(concept_doi, checklist)
Test.@test occursin("Can manage", checklist)
Test.@test occursin("Zenodo release verification", checklist)
Test.@test occursin(r"(?m)^\s*release:\s*$", workflow)
Test.@test occursin(r"(?m)^\s*-\s*published\s*$", workflow)
Test.@test occursin("scripts/verify_zenodo_release.jl", workflow)
end

Test.@testset "Zenodo release comparison detects archive drift" begin
current = ZenodoReleaseVerification.check_record(_zenodo_record(), "v0.7.6")
stale = ZenodoReleaseVerification.check_record(
_zenodo_record(; version = "v0.7.5"),
"v0.7.6",
)
wrong_concept = ZenodoReleaseVerification.check_record(
_zenodo_record(; concept = "9999999"),
"v0.7.6",
)

Test.@test current.ok
Test.@test current.doi == "10.5281/zenodo.21078136"
Test.@test !stale.ok
Test.@test occursin("v0.7.5", stale.message)
Test.@test !wrong_concept.ok
Test.@test occursin("9999999", wrong_concept.message)

responses = Any[_zenodo_record(; version = "v0.7.5"), _zenodo_record()]
result = ZenodoReleaseVerification.verify_release(
"v0.7.6";
attempts = 2,
delay_seconds = 0,
record_fetcher = _ -> popfirst!(responses),
)

Test.@test result.ok
Test.@test isempty(responses)
Test.@test_throws ErrorException ZenodoReleaseVerification.verify_release(
"v0.7.6";
attempts = 1,
delay_seconds = 0,
record_fetcher = _ -> _zenodo_record(; version = "v0.7.5"),
)

transport_error = try
ZenodoReleaseVerification.verify_release(
"v0.7.6";
attempts = 1,
delay_seconds = 0,
record_fetcher = _ -> error("network unavailable"),
)
nothing
catch error
sprint(showerror, error)
end

Test.@test occursin("network unavailable", transport_error)
end
1 change: 1 addition & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import DWave
import QUBODrivers

include("compat_metadata.jl")
include("citation.jl")
include("auth.jl")

if DWave.__auth__(; verbose = false)
Expand Down