The release record¶
Every release run uploads one artifact, release-record, holding one file,
release-record.json: what the run set out to publish and what came of it.
It is for a coordinator: a workflow or script that releases a project
split across repositories, starting each repository's release in dependency
order and checking that the results agree.
The template's last job, record, writes it with
release.py record. The job runs whatever happened
before it (if: always()), so a run that failed halfway, or was cancelled,
still says what it published. It needs no permission beyond the workflow's
contents: read.
Getting it¶
# Start a release in another repository and find its run.
gh workflow run release.yml -R org/core -f tag=release-1.4.0
run=$(gh run list -R org/core -w release.yml -b release-1.4.0 -L 1 --json databaseId -q '.[0].databaseId')
gh run watch -R org/core "$run" || true # a failed run still has a record
# Download and read it.
gh run download -R org/core "$run" -n release-record
jq -r .result release-record.json # complete | partial | nothing-published | unknown
gh run download <run> -n release-record writes release-record.json into
the current directory. A token that can read the repository's Actions
artifacts is enough (actions: read on a private repository).
Repositories that installed release-kit 1.x
install writes release.yml only when a repository has none, so an
existing release.yml has no record job. Copy it from
templates/release.yml
to the end of yours, and list in its needs every job of yours that
publishes (record refuses a selected package whose publish job it does
not need).
There is no record when the record job itself did not run: the tag could
not be checked out, or the run was stopped before any job started.
An example¶
A run whose crates.io job published core, then failed on tool, while the
binaries' build failed, so nothing reached GitHub Releases or PyPI:
{
"schema_version": 1,
"repository": "org/project",
"release": "0.2.0",
"tag": "release-0.2.0",
"commit": "3e9a0b1c2d4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b",
"verify_only": false,
"run": {"id": 1234567890, "attempt": 1,
"url": "https://github.com/org/project/actions/runs/1234567890"},
"result": "partial",
"jobs": {"gate": "success", "ci": "success", "build": "failure",
"publish-github": "skipped", "publish-homebrew": "skipped",
"publish-chocolatey": "skipped", "publish-crates": "failure",
"publish-pypi": "skipped", "publish-npm": "skipped", "publish-vscode": "skipped"},
"packages": [
{"name": "core", "registry": "crates.io", "version": "0.1.0", "tag": "core-v0.1.0",
"selected": true, "outcome": "published", "job": "failure",
"url": "https://crates.io/crates/core/0.1.0", "files": []},
{"name": "tool", "registry": "crates.io", "version": "0.2.0", "tag": "tool-v0.2.0",
"selected": true, "outcome": "failed", "job": "failure", "url": null, "files": []},
{"name": "tool-py", "registry": "pypi", "version": "0.2.0", "tag": "tool-py-v0.2.0",
"selected": true, "outcome": "skipped", "job": "skipped", "url": null,
"files": [{"name": "tool_py-0.2.0-py3-none-any.whl",
"sha256": "4c1b…", "size": 18342}]}
]
}
The schema¶
The JSON Schema is release-record.schema.json
(in the repository at docs/release-record.schema.json; release.py record
--schema prints it). release.py record checks every record against it
before writing.
Versioning¶
schema_version is 1. It changes only when a field is removed or renamed,
or changes meaning. New fields, and new values of outcome, result and job
results, may appear without it: ignore fields you do not know, and treat an
unknown outcome as not published.
The run¶
| Field | Value |
|---|---|
schema_version |
1 |
repository |
owner/name |
release |
the release's name (rab.yml's name_from package's version), or null if rab.yml could not be read at the tag |
tag |
the release tag the run was for |
commit |
the tagged commit (40 hex digits), or null if the tag could not be resolved |
verify_only |
whether this was a verify-only run |
run |
id, attempt and url of the workflow run |
result |
the run as a whole: see below |
jobs |
the result of each job the record job needs: success, failure, cancelled or skipped |
packages |
every package of rab.yml at the tagged commit, in its order |
result:
| Value | Meaning |
|---|---|
complete |
every selected package is on its registry, its publish job succeeded (in a verify-only run: was verified), and so did the jobs of the channels due for it (channels; not in a verify-only run, which runs none) |
partial |
this run published some selected packages or channels, but not all, or a published package's job failed afterwards (in verify or consume) |
nothing-published |
this run published none of them: it failed before publishing, its uploads all failed, the gate refused, or a verify-only run failed |
unknown |
nothing is known to be published, and a registry did not answer for at least one package |
Each package¶
| Field | Value |
|---|---|
name, registry, version |
as in rab.yml |
tag |
its record tag |
selected |
whether the gate selected it for this run |
outcome |
see below |
job |
its publish job's result (publish-crates for a crate, and so on), or null if not selected or selected only to finish its channels |
url |
where its registry serves this version, when outcome is published or already-published; else null |
files |
the files this run built for it: name, sha256 and size |
open_vsx_url |
a vscode package with open-vsx: true only: Open VSX's page for this version (https://open-vsx.org/extension/<publisher>/<name>/<version>) when url is set, else null. Such a package counts as published only once both registries have it, so one the Marketplace has but Open VSX lacks is failed (or skipped) with both null |
channels |
a selected github package with homebrew or chocolatey: the results of those jobs, for the channels due for it in this run |
outcome:
| Value | Meaning |
|---|---|
published |
this run put it on its registry |
already-published |
it was on its registry before this run: the gate did not select it because it was published, selected it only to finish the Homebrew or Chocolatey channel made from its release (see the retry policy), or a verify-only run verified it |
failed |
selected, its publish job failed or was cancelled, and its registry does not have it |
skipped |
this run did not publish it or check it: its publish job was skipped (the gate refused, CI or the build failed), or a verify-only run did not select it |
unknown |
its publish job did not succeed, and its registry did not answer whether it has it |
When a selected package's job succeeded, the job's verify step saw the
version on its registry (and consume used it). When the job did not
succeed, the record job asks the registry itself, the way verify does: a
job can fail after uploading, and then the package is published but the
result is not complete.
A run that tag started only to finish a release's channels selects its
github package with channels naming just those: the record shows it
already-published, with job: null, no files, and channels holding
the channel jobs' results, which decide result.
url, per registry:
| Registry | url |
|---|---|
github |
https://github.com/<repo>/releases/tag/<tag> |
crates.io |
https://crates.io/crates/<name>/<version> |
pypi |
https://pypi.org/project/<name>/<version>/ |
npm |
https://www.npmjs.com/package/<name>/v/<version> |
vscode |
https://marketplace.visualstudio.com/items?itemName=<publisher.name> |
files comes from the build artifacts (release--<slug>--*): every file
but the .sha256 ones, with its SHA-256 computed from the file and checked
against its .sha256 where the build wrote one (a github package's). A
ci-artifact package's files are the
promoted members, unchanged. files is empty for crates.io and npm, which
publish from source, and in a verify-only run, which builds nothing. For a
github package it does not list the checksums file, which the publish job
writes from these same files.
Checking a release across repositories¶
A coordinator might require, before starting the next repository's release:
jq -e '.result == "complete" and .commit == $sha' --arg sha "$expected" release-record.json
jq -r '.packages[] | select(.selected) | "\(.name) \(.version) \(.outcome) \(.url)"' release-record.json
and, after a partial or nothing-published record, fix the cause and
re-run that repository's release with release.py tag
(Recovering a failed release): the next run selects only
what is still due, and its record lists the rest as already-published.