name: release
on:
push:
tags: ["v*.*.*"]
permissions:
contents: read
defaults:
run:
shell: bash
jobs:
guard:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.v.outputs.version }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 - id: v
run: |
set -eu
tag=${GITHUB_REF#refs/tags/v}
manifest=$(awk -F'"' '/^version/ {print $2; exit}' Cargo.toml)
if [ "$tag" != "$manifest" ]; then
echo "tag v$tag but Cargo.toml says $manifest" >&2
exit 1
fi
pinned=$(awk '$1 == "version" {print $2; exit}' RELEASE)
if [ "$tag" != "$pinned" ]; then
echo "tag v$tag but RELEASE pins $pinned: run prepare-release and commit its RELEASE" >&2
exit 1
fi
echo "version=$tag" >> "$GITHUB_OUTPUT"
- run: make check
draft:
needs: guard
runs-on: ubuntu-latest
permissions:
contents: write
outputs:
id: ${{ steps.check.outputs.id }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 - id: check
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ needs.guard.outputs.version }}
run: |
set -eu
ids=$(gh api "repos/$GITHUB_REPOSITORY/releases?per_page=100" \
--jq ".[] | select(.draft and .tag_name == \"v$VERSION\") | .id")
[ "$(printf '%s\n' "$ids" | grep -c .)" = 1 ] || {
echo "::error::want one draft release for v$VERSION (run prepare-release), found: ${ids:-none}"; exit 1; }
mkdir art
gh api "repos/$GITHUB_REPOSITORY/releases/$ids/assets?per_page=100" \
--jq '.[] | "\(.id) \(.name)"' > assets
while read -r aid name; do
gh api -H 'Accept: application/octet-stream' \
"repos/$GITHUB_REPOSITORY/releases/assets/$aid" > "art/$name"
done < assets
# Every pinned archive is there with its pinned digest, and no
# archive is there that RELEASE does not pin.
rc=0
awk '$1 != "version" && $1 !~ /^#/ && NF { print $1, $2 }' RELEASE > pinned
while read -r target want; do
f=art/git-attest-$VERSION-$target.tar.gz
if [ ! -f "$f" ]; then echo "::error::the draft has no $f"; rc=1; continue; fi
got=$(sha256sum < "$f" | awk '{print $1}')
if [ "$got" = "$want" ]; then echo "ok $target $got"
else echo "::error::$target: the draft's archive is $got, RELEASE pins $want"; rc=1; fi
done < pinned
for f in art/*.tar.gz; do
t=${f#art/git-attest-"$VERSION"-}; t=${t%.tar.gz}
awk -v t="$t" '$1 == t { found = 1 } END { exit !found }' pinned \
|| { echo "::error::the draft carries $f, which RELEASE does not pin"; rc=1; }
done
[ "$rc" = 0 ]
echo "id=$ids" >> "$GITHUB_OUTPUT"
publish:
needs: [guard, draft, publish-crates]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: publish the checked draft
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ needs.guard.outputs.version }}
ID: ${{ needs.draft.outputs.id }}
run: |
set -eu
notes=$(gh api -X POST "repos/$GITHUB_REPOSITORY/releases/generate-notes" \
-f tag_name="v$VERSION" --jq .body)
# The commit, so a consumer can pin the step that decides whether
# tests run to something that cannot move.
body=$(printf 'Pin by commit: `uses: fredericrous/attest@%s # v%s`\n\n%s\n' "$GITHUB_SHA" "$VERSION" "$notes")
gh api -X PATCH "repos/$GITHUB_REPOSITORY/releases/$ID" \
-f tag_name="v$VERSION" -F draft=false -f make_latest=true -f body="$body" > /dev/null
got=$(gh api "repos/$GITHUB_REPOSITORY/releases/tags/v$VERSION" --jq '.draft')
[ "$got" = false ] || { echo "::error::v$VERSION is still a draft"; exit 1; }
publish-crates:
needs: [guard, draft]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 - name: CARGO_REGISTRY_TOKEN is set
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: |
set -eu
if [ -z "${CARGO_REGISTRY_TOKEN:-}" ]; then
echo "::error::no CARGO_REGISTRY_TOKEN; the library must reach crates.io"
exit 1
fi
- run: cargo publish --locked
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
- name: the version resolves on crates.io
env:
VERSION: ${{ needs.guard.outputs.version }}
run: |
set -eu
for i in 1 2 3 4 5 6 7 8 9 10; do
if cargo info "git-attest@$VERSION" > /dev/null 2>&1; then
echo "git-attest@$VERSION resolves"
exit 0
fi
sleep $((i * 6))
done
echo "::error::git-attest@$VERSION does not resolve on crates.io"
exit 1