DEV Community

Cover image for GitHub Release to Zenodo DOI
Dmytro Nasyrov
Dmytro Nasyrov

Posted on

GitHub Release to Zenodo DOI

Cover illustration: evolving source becomes one preserved release.

A GitHub release can exist before its Zenodo archive is ready. A DOI can resolve while the metadata still misrepresents the files. A discovery service can list the record without having checked whether the software works. Treating those events as one success state makes a release difficult to reproduce and surprisingly easy to describe incorrectly.

This guide follows a real software release through those boundaries. The example is version 1.1.0 of an LLM-as-a-judge cost calculator, published on August 16, 2026. Its GitHub release, archived files, DOI resolution, DataCite metadata, OAI-PMH response and OpenAIRE entry were checked again on September 17. The useful deliverable is a small verification receipt that tells the next developer exactly what was preserved and what each check established.

Start with the object someone will cite

The calculator release is a browser application with calculation code, tests, methodology and dated pricing registries. That combination gives another developer something to inspect and reuse. A repository containing only an announcement would offer much less reason to preserve a particular version.

The release work behind Pharos Production software development supplies the concrete example here: a cost model packaged with explicit assumptions and separate terms for code and data. The engineering problem is retaining the inputs behind an estimate when the application and vendor prices later change. Archiving a named release preserves those inputs; it does not keep them current.

The identities checked for this example are:

Object Observed identifier What it identifies
GitHub release v1.1.0 The named release and its notes
Git commit 67b6d1d9d741214a986ea4d7ce7111a99605bbf3 The source revision resolved from that tag
Zenodo record 21963489 The published archive and its metadata
Version DOI 10.5281/zenodo.21963489 This particular archived version
Concept DOI 10.5281/zenodo.21963488 The work across its versions

On the verification date, the public Zenodo record contained one ZIP file of 133,258 bytes. Its advertised MD5 checksum matched the downloaded archive. The four citation and license files inside that ZIP also matched their counterparts at the pinned Git commit byte for byte. This is a bounded file comparison, not a claim that every executable path was retested for this article.

Record the commit separately from the tag. In this case GitHub's release response reported immutable: false; the existence of a release URL alone therefore did not establish GitHub release immutability. The version DOI and retained archive checksum provide different evidence from a mutable repository page.

Complete preflight before enabling automatic deposits

Choose the smallest package that supports independent use. Include source, installation or execution instructions, dependency information, representative tests and the data needed for the documented example. If a benchmark depends on a private dataset that cannot be shared, say what remains unavailable and how that limits reproduction.

Check the actual archive contents, particularly when a project uses generated binaries, submodules, large files or separately hosted assets. A successful source-code archive does not by itself show that every resource mentioned in the README was deposited. The calculator example has no uploaded GitHub release assets; its archived ZIP is the object inspected here.

For reproduction, preserve the invocation as well as the program. A calculator result needs its workload values; a benchmark needs its configuration and measurement procedure. A DOI pointing to the right code does not tell a reader which options produced a reported number. Put those inputs in a small example or run manifest, with expected outputs and tolerances where relevant. If an external service is required, document that dependency and the resulting limits. A source archive can preserve an algorithm while leaving the environment needed to execute it only partly reproducible.

Confirm that publishing every included file is intended. Remove secrets, private configuration, customer records and accidental build outputs before tagging. Decide authorship from contributions rather than copying whoever happens to operate the release account. Verify the creator's name, affiliation and ORCID against the intended citation, and retain that decision with the release review.

GitHub's official guidance states:

“Make sure to include a license in your repository so readers know how they can reuse your work.”

GitHub, Referencing and citing content.

That instruction belongs in preflight because an archive cannot repair missing permission by assigning an identifier. A useful review asks which files are covered by each license, where the applicable terms are stored and whether third-party materials have separate conditions. A repository-level badge is too coarse for a package containing differently licensed components.

For the integration itself, GitHub documents public-repository access and notes that an organization owner may need to approve the Zenodo application. Review that access deliberately. Then use Zenodo's GitHub settings to synchronize the repository list and enable the intended repository. Do not interpret a successful account connection as evidence that the correct repository is enabled. The enable-repository guide describes these as separate steps.

Give citation metadata one clear owner

Keep CITATION.cff in the repository root when you want GitHub to offer a citation through its interface. It describes the software for people and citation tools. The example's archived file includes the title, version, author, ORCID, repository URL and a description of its two license scopes.

Zenodo also accepts .zenodo.json. Its current metadata documentation makes the precedence explicit: when both files exist, the GitHub archiving integration uses .zenodo.json and ignores CITATION.cff. It does not merge them. A corrected author in CFF cannot compensate for a stale creator in the JSON file.

Choose an owner for each repeated field. For example, a release review can require title, version and creators to agree across the two files, while reserving Zenodo-specific related identifiers for JSON. This is a proposed consistency check, not a claim that either service enforces your repository's policy automatically.

A short excerpt from the actual archived JSON shows why reviewing fields individually matters:

{
  "version": "1.1.0",
  "upload_type": "software",
  "access_right": "open",
  "license": "other-open",
  "language": "eng"
}
Enter fullscreen mode Exit fullscreen mode

This is an excerpt, not a complete upload template. In particular, other-open describes the historical release input; it is not a recommendation to replace specific license declarations with a generic value. The full file contains creators, description, keywords, related identifiers and notes defining the code/data boundary.

Validate the exact files contained in the tag. Checking only the default branch after publication can inspect a later correction that never entered the archive. JSON syntax validation catches malformed JSON, but it does not prove that an identifier, relationship or license choice is appropriate. Review the supported fields and then inspect the resulting record.

The same distinction applies to CFF validation. Schema validation and correct citation intent are separate checks. A well-formed file can still point to the wrong software version or credit an unrelated paper. Keep the intended citation in the review so a mechanically valid result can be compared with it.

Choose a version DOI for reproducible claims

Zenodo's DOI versioning explanation distinguishes a specific version from the concept representing all versions. The first publication creates both identifiers; later versions receive their own version identifiers. The relationships belong in metadata rather than in an invented suffix appended to the DOI.

Use the version DOI when a result depends on the exact software or data you used. A cost estimate based on the calculator's August pricing registry should identify that release, its workload inputs and the registry's verification date. A general project description may instead refer to the concept DOI because its subject is the evolving work.

Both identifiers resolved to record 21963489 during this check. That shared destination does not make their meanings interchangeable. It only describes their resolution at the time of observation. A receipt should store both identifiers with explicit field names, rather than a single ambiguous doi field copied from whichever badge was easiest to find.

Likewise, keep release version, publication date and data-verification date separate. Version 1.1.0 was published on August 16; its pricing sources were described as verified on August 13. Neither date makes those prices current in September. This article verifies the preserved object and its metadata, not today's commercial pricing.

The LLM observability cost methodology explains the problem the calculator addresses: different billing entities require explicit workload assumptions. Keeping that methodological context beside the versioned artifact helps a reader understand an estimate. The company article remains contextual documentation, not independent validation of the software.

Preserve mixed licenses across representations

The example assigns MIT to source code and the static application. Its DATA-LICENSE.md assigns CC BY 4.0 to normalized pricing records in data/ and docs/data/. Those paths matter: two license names without file scope could be misread as offering a choice of terms for every file.

Zenodo's license guidance permits declaring multiple licenses for mixed uploads. Keep the file-level explanation inside the archive as well as in record metadata. A downloaded ZIP should remain understandable without requiring the reader to reconstruct an earlier state of its landing page.

The September inspection exposed three different representations of the same release. The archived JSON retained other-open. The current legacy Zenodo Records API exposed a single MIT license object plus descriptive notes. DataCite's rightsList and the OAI-PMH response each included MIT and CC BY 4.0. The public record's metadata had therefore evolved beyond the original release input.

This is why reading only metadata.license.id would have produced an incomplete account. A consumer using that field alone could miss the data license even though it was present in other exports and in the archive. Compare the rights arrays, human-readable notes and actual license files before declaring that the metadata lost or preserved every condition.

Do not generalize this observation into a promise that the integration automatically reconstructs mixed licensing. The evidence establishes the archived input and the current published outputs. It does not establish that every intermediate transformation was automatic. Keep any post-publication metadata correction in the release history so later maintainers can explain the difference.

A practical review assigns a row to each materially different file group: paths, copyright holder or attribution, license file and exported license identifier. If a component's rights are unclear, resolve that uncertainty before depositing it. Adding a second identifier to metadata is not a substitute for permission to distribute a file.

Publish once, then reconcile the integration result

Prepare the release from the reviewed commit, confirm the intended tag and publish through the normal GitHub release workflow. Enabling the repository is preparation; creating the GitHub release triggers the integration. A tag pushed by itself should not be treated as evidence that this documented release workflow completed.

Zenodo's archiving guide explicitly includes a processing interval. Wait for the integration result, open the resulting record and compare it with the intended release. The existence of a GitHub release page is only the first observation in this sequence.

Use separate operational states: release published, ingestion pending, record published, DOI resolved and metadata verified. Store a timestamp and evidence URL for each completed state. These are suggested states for your own release process; they are not a list of status names returned by every service.

When ingestion reports a metadata error, inspect the error for that exact release. Correct the responsible metadata before deciding on a new release. When the result is merely unknown because a request timed out, first look for an existing matching record. Repeating publication immediately can create another object without resolving the uncertainty about the first attempt.

Keep a retry decision tied to repository, tag and resolved commit. A background worker should never interpret an empty response as unconditional permission to make a fresh deposit. Record the unresolved state and the next read-only reconciliation step. This also helps a human maintainer resume after an interrupted browser session without guessing which actions occurred.

For CI, put preflight checks before release creation and verification after it. A read-only verification job can fail without creating a second release. Keep publication credentials out of the verifier; reading the public evidence below does not require them. That separation gives reviewers a useful tool without handing every verification run authority to publish.

Separate evidence checks connect a pinned Git commit to a Zenodo archive and version DOI, then branch to DataCite, OAI-PMH and OpenAIRE. Software Heritage remains independently unconfirmed.

Each box requires its own observation. A successful archive does not certify software correctness or completion in every downstream service.

Verify the archive and the exported metadata

The following Python 3 script makes a bounded set of public, read-only requests. Run it in an empty directory: it saves four response snapshots and the archive for inspection. It does not execute downloaded software, create a deposit, modify permissions or require a token. Any failed request or assertion stops the check.

import hashlib
import json
from pathlib import Path
from urllib.request import urlopen
import xml.etree.ElementTree as ET

DOI = "10.5281/zenodo.21963489"
RECORD = "21963489"

def read(url, filename):
    with urlopen(url, timeout=30) as response:
        payload = response.read()
    Path(filename).write_bytes(payload)
    return payload

record = json.loads(read(
    f"https://zenodo.org/api/records/{RECORD}", "record.json"))
assert record["doi"] == DOI
assert record["metadata"]["version"] == "1.1.0"
assert record["status"] == "published"

files = record["files"]
assert len(files) == 1  # Expected for this particular release.
archive = read(files[0]["links"]["self"], "release.zip")
assert len(archive) == files[0]["size"]
assert "md5:" + hashlib.md5(archive).hexdigest() == files[0]["checksum"]

attributes = json.loads(read(
    f"https://api.datacite.org/dois/{DOI}", "datacite.json"
))["data"]["attributes"]
assert attributes["doi"] == DOI
assert attributes["state"] == "findable"
assert attributes["version"] == "1.1.0"
rights = {x.get("rightsIdentifier") for x in attributes["rightsList"]}
assert {"mit", "cc-by-4.0"} <= rights

oai = ET.fromstring(read(
    "https://zenodo.org/oai2d?verb=GetRecord&metadataPrefix=oai_dc"
    f"&identifier=oai:zenodo.org:{RECORD}", "oai.xml"))
ns = {"o": "http://www.openarchives.org/OAI/2.0/"}
assert oai.find("o:error", ns) is None
assert oai.findtext(".//o:header/o:identifier", namespaces=ns) == (
    f"oai:zenodo.org:{RECORD}")

openaire = json.loads(read(
    f"https://api.openaire.eu/search/software?doi={DOI}&format=json",
    "openaire.json"))
assert int(openaire["response"]["header"]["total"]["$"]) == 1
print("Archive SHA-256:", hashlib.sha256(archive).hexdigest())
print("Bounded checks passed; inspect the saved metadata and match identity.")
Enter fullscreen mode Exit fullscreen mode

The script's expected record, version, file count and two rights identifiers are deliberately specific. Change those expectations only after reviewing a different release. Weakening the assertions until an unrelated record passes would remove the reason to run the check.

The observed archive SHA-256 was 3e72a12ee4ef08095bd57e35c92ef061fe995b153cd15251bfedcb870ae30655. Keep that digest with the timestamp and original download URL. The service-provided MD5 is useful for checking a completed download against its record; the separately retained SHA-256 supplies a stronger content fingerprint. Neither digest establishes who authored the code or whether it is correct.

Read each verification result at its actual scope

DataCite's public API exposes findable DOI metadata. For this release it returned the expected creator, version, both rights declarations and an IsVersionOf relationship to the concept DOI. Those checks establish registered, discoverable metadata with the expected fields. They do not establish peer review or successful software execution.

OAI-PMH answers another question: can a harvester retrieve this repository record in the requested metadata format? The saved GetRecord response contained the expected OAI identifier, title, DOI and both rights declarations. Inspect the XML body as well as HTTP status, because a protocol-level error can still arrive in a successful HTTP response.

The OpenAIRE software query returned one matching result. Its title, creator, original OAI identifier and landing-page URL matched the example. Its returned status included UNDER_CURATION; the article therefore claims observed discovery, not completed curation. A result count alone would be insufficient without those identity comparisons.

The script checks that count but leaves the richer OpenAIRE identity comparison visible in the saved response. In a maintained verifier, make the expected identifier and landing URL explicit assertions too. Avoid a title-only comparison: titles can change, be reused or appear on related records.

Software Heritage is a further boundary. The Zenodo API response contained an empty swh object during this inspection, so completed Software Heritage preservation was not established. Keep that result unconfirmed rather than promoting it to success because the DOI resolves. A later verified archival identifier can update that particular observation.

These services provide useful persistence and discovery infrastructure. Their presence in a release record should not be presented as endorsement by CERN, a security audit, a license-compatibility opinion or a guarantee of search traffic. State exactly which response was inspected and what matched.

Make a failed check actionable

A verifier should distinguish a content mismatch from an unavailable observation. If the downloaded archive has the wrong checksum, stop treating that download as the expected object. Preserve the response details and compare the advertised file identity before investigating further. Do not replace the expected checksum with the newly observed value simply to restore a passing result.

If a public API returns an access error, timeout or rate limit, record the affected service and leave its observation unresolved. That result does not prove the record was removed. It also does not establish that the previous successful response still describes current metadata. Retain the last verified snapshot with its original timestamp and keep the fresh failure separate.

If DataCite resolves the expected DOI but shows an unexpected creator or rights list, classify the problem as a metadata mismatch. Inspect the published Zenodo metadata and the corresponding export before editing repository files. Changing the current branch cannot retroactively change which bytes were archived in the existing ZIP, so it may be the wrong repair for the observed defect.

If OpenAIRE returns no matching result while the record and DOI are available, report that particular discovery check as unconfirmed. Choose a bounded later recheck appropriate to the release process. Do not claim a guaranteed indexing delay or create another deposit to make a search result appear. Discovery latency and archive identity require different responses.

These branches deserve a simple responsibility map. A release maintainer owns the tag and package contents. A metadata reviewer owns creator, relationship and rights corrections. A verifier records observations and can block acceptance, but should not invent an alternative publication when a downstream service is unavailable. A small project can assign all three responsibilities to one person while still keeping their decisions distinct.

Preserve enough context for another operator to diagnose a failure: request URL, UTC time, response status, expected identity and the comparison that failed. Avoid putting tokens or private account responses into a public receipt. The example uses public endpoints, so its relevant evidence can be shared without exposing publication credentials.

Finally, a successful rerun should append a new observation. It should not erase the failed attempt or turn its timestamp into the time of eventual success. That history explains whether the release was initially wrong, temporarily unavailable or waiting for another service. It also prevents an incident review from confusing a later repair with what readers could access at publication time.

Correct metadata without disguising a new release

A misspelled creator or incomplete description is a metadata correction. Zenodo's record-management guidance permits published metadata to be edited without changing the DOI. Preserve the previous and corrected values, the reason and the verification timestamp, especially when downstream exports are part of the acceptance criteria.

Changes to calculation logic or pricing files create a different reproducibility object. Use the version-management workflow for substantive file changes, then cite the new version when reporting results produced with it. Keep the old version identifiable for people who used its earlier behavior.

Avoid an absolute claim that published files can never change. The current file-management documentation describes a limited correction window: minor file edits can be initiated within 30 days, with the draft published within 45 days of the original publication. Outside that window, justified exceptional cases go through support. That exception makes a retained archive digest worthwhile even when a DOI is stable.

For this calculator, a new vendor price or formula changes the inputs behind an estimate and should be reviewed as a new version, not silently described as the old reproducible result. Updating an explanatory sentence about license scope is a different operation. Classify the change by what a reader would need to reproduce, then preserve the corresponding history.

Finish with a release receipt someone else can use

Keep the final receipt small: repository, tag, commit, record URL, version DOI, concept DOI, archive filename and digest, expected creators, license scopes, check timestamps and unresolved observations. Link each conclusion to the response or file that supports it. A green job badge without these identities is weak evidence for a later investigation.

Accept the release when the intended archive is retrievable, its metadata describes the right work and the version citation resolves correctly. Report downstream discovery separately, including pending or unconfirmed services. This leaves the next maintainer with a precise starting point: a preserved object, a readable citation and a documented boundary around what was actually verified.

More insights to read

About the author

Portrait of Dmytro Nasyrov wearing a dark suit and light blue shirt against a dark background.

Dmytro Nasyrov. Photo supplied by the author.

Written by Dmytro Nasyrov PhD, software architect with 24 years of production experience. Dmytro is the founder and CTO of Pharos Production. He works on production software architecture for FinTech, AI, Web3 and blockchain systems.

Top comments (0)