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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/internal-preview-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@ jobs:
- name: Compute internal preview version
id: version
run: >-
uv run --with packaging --no-project python tools/release_version.py
uv run --with packaging --with pyyaml --no-project python
tools/release_version.py
internal-preview
--run-number "${{ steps.provenance.outputs.source_run_number }}"

Expand Down
7 changes: 6 additions & 1 deletion .github/workflows/public-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,16 @@ jobs:
exit 1
fi

# Runs before the build so a stale CITATION.cff stops the release while it
# is still reversible; PyPI publishes are not. PyYAML is explicit because
# `--no-project` skips the project's own dependencies.
- name: Validate release tag
id: version
env:
TAG_NAME: ${{ github.ref_name }}
run: uv run --with packaging --no-project python tools/release_version.py public-release
run: >-
uv run --with packaging --with pyyaml --no-project
python tools/release_version.py public-release

- name: Build distributions
run: uv build --no-sources
Expand Down
28 changes: 28 additions & 0 deletions .zenodo.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"upload_type": "software",
"title": "BenchFlow: framework for RL environments for LLM agents",
"description": "BenchFlow is a framework for building RL environments to evaluate and train LLM agents. Built on the Agent Client Protocol (ACP), it provides Scene-based multi-turn, multi-agent, and multi-model evaluation in shared sandboxes — without Docker Compose or sidecar containers. Supported use cases include interactive user simulation, code-review loops, bring-your-own-skill (BYOS) skill generation, multi-turn iterative refinement, cross-model review (cheap coder + strong reviewer), and stateful service tasks against live mock APIs (Gmail, Calendar, Docs, Drive, Slack). See docs/use-cases.md.",
"creators": [
{
"name": "TODO BEFORE MERGE - replace with the agreed author list (\"Family, Given\" per entry, plus orcid and affiliation where known). Zenodo mints a permanent DOI from this file, so a placeholder that reaches main becomes permanent authorship."
}
],
"license": "Apache-2.0",
"access_right": "open",
"keywords": [
"benchmark",
"llm-agents",
"acp",
"agent-evaluation",
"multi-turn",
"terminal-bench",
"skillsbench"
],
"related_identifiers": [
{
"relation": "isVariantFormOf",
"identifier": "https://pypi.org/project/benchflow/",
"resource_type": "software"
}
]
}
4 changes: 2 additions & 2 deletions CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ authors:
repository-code: "https://github.com/benchflow-ai/benchflow"
url: "https://github.com/benchflow-ai/benchflow"
license: Apache-2.0
version: 0.6.9
date-released: 2026-08-15
version: 0.7.4
date-released: 2026-08-17
keywords:
- benchmark
- llm-agents
Expand Down
51 changes: 46 additions & 5 deletions docs/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,13 +127,17 @@ runs `uv publish`.
Public release:

1. Update `pyproject.toml` from the next `.dev0` version to the final public
version.
version, and set `CITATION.cff` `version` and `date-released` to that same
release.
2. Merge the release PR to `main`.
3. Push a matching release tag.
4. `.github/workflows/public-release.yml` validates the tag, publishes to PyPI,
and creates a GitHub Release. The workflow refuses tags whose commits are
not contained in `origin/main`.
5. Bump `main` to the next `.dev0`.
4. `.github/workflows/public-release.yml` validates the tag against
`pyproject.toml` and `CITATION.cff`, publishes to PyPI, and creates a GitHub
Release. The workflow refuses tags whose commits are not contained in
`origin/main`, and tags whose `CITATION.cff` still names an older release.
5. Bump `main` to the next `.dev0`. Leave `CITATION.cff` on the version just
released: it names the last published release, not the line under
development.

## One-Time PyPI Setup

Expand All @@ -154,3 +158,40 @@ Create matching GitHub environments:

The workflows build with `uv build --no-sources`, check distributions with
`twine check`, and publish with `uv publish`.

## Zenodo Archiving

Each published GitHub Release is archived on Zenodo through the official
GitHub integration, which snapshots the tag's source tree and mints a version
DOI for it. Zenodo also maintains a concept DOI that always resolves to the
newest archived version; cite the concept DOI, not a version DOI, so the
citation never goes stale.

`.zenodo.json` at the repository root supplies the archive metadata. Without it
Zenodo derives authorship from GitHub contributor statistics, which is not the
project's author list. Its `version` and `publication_date` are deliberately
absent: Zenodo takes both from the tag and the GitHub Release, so pinning them
would go stale on every release.

`CITATION.cff` is the human-facing citation record and names the **last
published** release. On `main` it therefore sits one release behind the `.devN`
version in `pyproject.toml`, which is correct, not drift. The tag-driven
`public-release.yml` validation is what keeps it honest: a tag whose
`CITATION.cff` names a different version fails before anything is built or
published.

One-time setup, which requires **admin** rights on the repository:

1. Sign in to <https://zenodo.org> with the GitHub account, and grant Zenodo the
GitHub authorization it asks for.
2. Open Zenodo's GitHub page (<https://zenodo.org/account/settings/github/>) and
flip the switch for `benchflow-ai/benchflow` on. Zenodo installs a release
webhook; it archives releases published *after* the switch is flipped, so the
first archived version is the next release, not the current one.
3. After that release, confirm on Zenodo that the record's authors, title, and
license came from `.zenodo.json` rather than from contributor statistics.
4. Add the concept DOI to `README.md` and to `CITATION.cff` once it exists.

Flip the switch only after a `.zenodo.json` carrying the real author list has
merged to `main`. Zenodo archives the tagged commit, and a published DOI cannot
be withdrawn -- an incomplete author list becomes permanent public metadata.
96 changes: 96 additions & 0 deletions tests/test_citation_metadata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
"""Parity coverage for the citation and Zenodo archiving metadata files."""

from __future__ import annotations

import json
import tomllib
from pathlib import Path

import pytest
import yaml
from packaging.version import Version

REPO_ROOT = Path(__file__).resolve().parent.parent

# Zenodo derives a release's version and publication date from the git tag and
# the GitHub Release, so pinning either here would go stale on every release --
# the exact drift the tag-time CITATION.cff gate exists to prevent.
ZENODO_DERIVED_KEYS = ("version", "publication_date")

CREATORS_PLACEHOLDER = "TODO BEFORE MERGE"


@pytest.fixture(scope="module")
def pyproject() -> dict:
return tomllib.loads((REPO_ROOT / "pyproject.toml").read_text(encoding="utf-8"))


@pytest.fixture(scope="module")
def citation() -> dict:
return yaml.safe_load((REPO_ROOT / "CITATION.cff").read_text(encoding="utf-8"))


@pytest.fixture(scope="module")
def zenodo() -> dict:
return json.loads((REPO_ROOT / ".zenodo.json").read_text(encoding="utf-8"))


def test_zenodo_declares_software_upload(zenodo: dict) -> None:
"""Guards the Zenodo archiving deposit shape."""
assert zenodo["upload_type"] == "software"
assert zenodo["access_right"] == "open"


def test_zenodo_matches_citation_prose(citation: dict, zenodo: dict) -> None:
"""Guards the Zenodo archiving metadata against drifting from CITATION.cff."""
assert zenodo["title"] == citation["title"]
assert zenodo["description"] == citation["abstract"]
assert zenodo["keywords"] == citation["keywords"]


def test_license_is_declared_identically_everywhere(
pyproject: dict, citation: dict, zenodo: dict
) -> None:
"""Guards the Zenodo archiving license claim against a three-way mismatch."""
assert pyproject["project"]["license"]["text"] == "Apache-2.0"
assert citation["license"] == "Apache-2.0"
assert zenodo["license"] == "Apache-2.0"


def test_zenodo_omits_release_derived_fields(zenodo: dict) -> None:
"""Guards the Zenodo archiving metadata against hardcoding a release version."""
for key in ZENODO_DERIVED_KEYS:
assert key not in zenodo


def test_citation_never_names_an_unpublished_version(
pyproject: dict, citation: dict
) -> None:
"""Guards CITATION.cff against naming a version that was never released.

CITATION.cff names the last published release, so on `main` it is either
equal to the staged public version or behind the next `.devN` line. This
catches a citation file that has run ahead of the project; a citation file
that lags several releases behind is caught at tag time instead, by
`tools/release_version.py public-release`.
"""
project_release = Version(pyproject["project"]["version"]).base_version

assert Version(str(citation["version"])) <= Version(project_release)


def test_zenodo_creators_are_resolved(zenodo: dict) -> None:
"""Blocks merging the Zenodo archiving PR with placeholder authorship.

Zenodo mints a permanent DOI from `.zenodo.json`, and a published DOI cannot
be withdrawn. This test is expected to fail until the agreed author list --
names, ORCIDs, affiliations -- replaces the placeholder.
"""
creators = zenodo["creators"]

assert creators, ".zenodo.json must declare at least one creator."
for creator in creators:
assert CREATORS_PLACEHOLDER not in creator["name"], (
"Replace the .zenodo.json creators placeholder with the agreed "
"author list before merging; Zenodo authorship is permanent."
)
Loading
Loading