1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
name: Publish
# Publish `dtg-credentials` to crates.io on a `vX.Y.Z` tag push.
#
# This repository had no release path at all: every version on crates.io got
# there by someone running `cargo publish` from a laptop. That is why 0.6.0 -
# the release carrying the VAC and VDC, which the data-rooms work in
# `verifiable-trust-infrastructure` depends on - sat merged and unpublished
# while the consumer could not compile against it. A tag is now the release.
#
# The trigger is the tag push rather than `release: published` on purpose: a
# Release created with the default GITHUB_TOKEN does not cascade-trigger other
# workflows, so a `release:` handler would silently never fire.
#
# Auth is crates.io Trusted Publishing (OIDC), so there is no long-lived token
# in this repository. ONE-TIME SETUP, on crates.io under the crate's Settings ->
# Trusted Publishing: add owner `OpenVTC`, repository `dtg-credentials`,
# workflow `publish.yml`. Until that exists the job fails at the auth step with
# a message naming exactly this, which is the right failure - better than a
# token sitting in the repo for the one day a year it is used.
#
# The run is idempotent: a tag re-pushed after a failed release job finds the
# crate already on crates.io at that version and skips rather than dying on
# "crate version already uploaded", which would otherwise force a version bump
# nothing needed.
on:
push:
tags:
workflow_dispatch:
permissions:
id-token: write # OIDC token for crates.io Trusted Publishing
contents: read
jobs:
publish:
name: Publish to crates.io
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
# The tag says one version and Cargo.toml says another exactly once - the
# time someone tags before the bump lands - and the result is a release
# whose number means nothing. Cheap to check, unrecoverable to fix.
- name: Tag must match the crate version
if: startsWith(github.ref, 'refs/tags/v')
run: |
set -euo pipefail
tag="${GITHUB_REF_NAME#v}"
crate=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].version')
if [ "$tag" != "$crate" ]; then
echo "::error::tag v${tag} does not match Cargo.toml version ${crate}"
exit 1
fi
echo "publishing dtg-credentials ${crate}"
# Before authenticating, not after. The skip is what makes a re-pushed tag
# recoverable, and it cannot do that job from behind the auth step: the
# v0.6.0 tag push found 0.6.0 already on crates.io (published by hand
# minutes earlier) and still failed, because it never got past
# "No Trusted Publishing config found" to reach the check that would have
# said there was nothing to do. A step that decides whether to act should
# not sit downstream of acquiring the means to act.
- name: Skip if this version is already published
id: check
run: |
set -euo pipefail
version=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].version')
echo "version=${version}" >> "$GITHUB_OUTPUT"
# The sparse index is newline-delimited JSON, one object per version.
# A 404 (name never published) is a clean "not there".
if curl -sSf "https://index.crates.io/dt/g-/dtg-credentials" 2>/dev/null \
| jq -se --arg v "$version" 'any(.[]; .vers == $v)' >/dev/null; then
echo "::notice::dtg-credentials ${version} is already on crates.io — nothing to do"
echo "published=true" >> "$GITHUB_OUTPUT"
else
echo "published=false" >> "$GITHUB_OUTPUT"
fi
- name: Authenticate to crates.io
if: steps.check.outputs.published == 'false'
uses: rust-lang/crates-io-auth-action@v1
id: auth
- name: Publish
if: steps.check.outputs.published == 'false'
env:
CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}
run: cargo publish --locked