Skip to content
Merged
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
7 changes: 7 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,13 @@ jobs:
- name: The macOS installer verifies the macOS download
run: scripts/check-macos-installer.sh ${{ github.repository }} .

# The release's publish job refuses to lift the draft until this says every
# artifact is up. Its self-test covers a missing file, an upload that never
# finished, an empty file and another version's files, and checks its list
# of architectures against release.yml's matrix.
- name: The release check refuses an incomplete release
run: scripts/check-release-assets.sh --self-test

# The GUI (Native SDK): macOS only, via the native CLI.
gui:
runs-on: macos-latest
Expand Down
37 changes: 33 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,22 @@ jobs:
# succeeded, and a re-run is what somebody reaches for when a publish
# fails halfway. The tests above still gate it, so nothing arrives here
# that has not passed them on the tag's own source.
#
# An existing release goes back to being a draft FIRST, so both branches
# leave it invisible until `publish` has checked every artifact. Uploading
# straight into a published release offered a partial set for as long as
# the other jobs took. While it is a draft the release page and the latest
# link fall back to the newest other release. `publish` lifts it without
# claiming Latest for it, so re-running an older tag cannot take Latest
# away from a newer one.
- name: Publish the release
env:
GH_TOKEN: ${{ github.token }}
run: |
asset="Notary-${GITHUB_REF_NAME}-macos.zip"
if gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1; then
echo "release exists; replacing its artifact with the one built here"
echo "release exists; hiding it and replacing its artifact with the one built here"
gh release edit "$GITHUB_REF_NAME" --draft=true
gh release upload "$GITHUB_REF_NAME" "$asset" --clobber
else
# A DRAFT. The Linux tarballs are built by another job that takes
Expand Down Expand Up @@ -161,16 +170,36 @@ jobs:
# Lifts the draft, once and only once every artifact is up. Until this runs
# the release is invisible, so there is no window in which the page offers a
# Linux install that is not there yet.
#
# "Every artifact is up" is CHECKED, not inferred from the jobs above having
# exited 0: each expected file has to be listed by name, finished uploading,
# and not empty. The check is a script so it can be run by hand against any
# release, and ci.yml runs its self-test on every change.
publish:
needs: [macos-app, linux-tarball]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Every artifact is up
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release view "$GITHUB_REF_NAME" --json assets \
--jq '.assets[] | "\(.name)\t\(.state)\t\(.size)"' \
| scripts/check-release-assets.sh "$GITHUB_REF_NAME"

# `make_latest=legacy`, not gh's `--draft=false`. Publishing a draft
# defaults to making it Latest, which is right for a new tag and wrong for
# an older one re-run through the branch above: it would move
# /releases/latest, where both installers look, back a version. `legacy`
# lets GitHub pick Latest by version and date, so a new release still
# becomes Latest and a re-published old one does not. gh cannot send
# `legacy`, hence the API call.
- name: Publish
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release edit "$GITHUB_REF_NAME" --draft=false
echo "published $GITHUB_REF_NAME with:"
gh release view "$GITHUB_REF_NAME" --json assets --jq '.assets[].name'
id="$(gh release view "$GITHUB_REF_NAME" --json databaseId --jq .databaseId)"
gh api -X PATCH "repos/$GITHUB_REPOSITORY/releases/$id" \
-F draft=false -f make_latest=legacy --jq '"published \(.tag_name)"'
105 changes: 105 additions & 0 deletions scripts/check-release-assets.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
#!/usr/bin/env bash
# Whether a release carries every artifact it is supposed to, before anybody
# can see it.
#
# gh release view <tag> --json assets \
# --jq '.assets[] | "\(.name)\t\(.state)\t\(.size)"' \
# | scripts/check-release-assets.sh <tag>
# scripts/check-release-assets.sh --self-test
#
# Reads one asset per line (name, state, size, tab separated) and exits 1,
# naming each one that is missing, when the release lacks any artifact a
# release of <tag> should carry. An asset counts only when GitHub says it
# finished uploading and it is not empty: an upload that dies halfway leaves an
# entry with the right name and nothing behind it.
#
# The release workflow runs this before it lifts the draft, which is the check
# its `publish` job used to claim and did not make.
set -euo pipefail

# The Linux architectures release.yml builds. The self-test compares this with
# that workflow's matrix, so the two cannot drift apart unnoticed.
LINUX_ARCHES="x86_64 aarch64"

# Every file a release of $1 should carry, one per line.
expected() {
local tag="$1" ver="${1#v}" arch
printf '%s\n' "Notary-$tag-macos.zip"
for arch in $LINUX_ARCHES; do
printf '%s\n' "notary-$ver-linux-$arch.tar.gz" "notary-$ver-linux-$arch.tar.gz.sha256"
done
}

# Checks the asset lines on stdin against what $1 should carry.
check() {
local tag="$1" have name missing=0
have="$(awk -F '\t' '$2 == "uploaded" && $3 > 0 { print $1 }')"
while IFS= read -r name; do
# A here-string, not a pipe: `grep -q` stops reading at the first match,
# and under pipefail the writer's SIGPIPE would turn a match into a miss.
if ! grep -qxF -- "$name" <<<"$have"; then
echo "missing: $name" >&2
missing=1
fi
done < <(expected "$tag")
if [ "$missing" != 0 ]; then
echo "$tag is not ready to publish. It has:" >&2
printf '%s\n' "${have:- (nothing uploaded)}" >&2
return 1
fi
echo "$tag carries all $(expected "$tag" | wc -l | tr -d ' ') artifacts"
}

# Asserts that the asset lines in $4 are refused, and refused for the right
# reason: exactly $3 files reported missing, $2 among them. Checking only that
# the check failed would pass on a fixture that broke the wrong line.
refuses() {
local what="$1" name="$2" count="$3" err
if err="$(check v9.9.9 <<<"$4" 2>&1 >/dev/null)"; then
echo "$what was accepted" >&2; exit 1
fi
if ! grep -qxF -- "missing: $name" <<<"$err" ||
[ "$(grep -c '^missing: ' <<<"$err")" != "$count" ]; then
echo "$what was refused, but not for the reason expected:" >&2
printf '%s\n' "$err" >&2
exit 1
fi
echo "ok: $what is refused"
}

self_test() {
local here full
here="$(cd "$(dirname "$0")/.." && pwd)"
full="$(expected v9.9.9 | awk '{ printf "%s\tuploaded\t1000\n", $0 }')"

check v9.9.9 <<<"$full" >/dev/null ||
{ echo "a complete release was refused" >&2; exit 1; }
echo "ok: a complete release passes"

local tab=$'\t'
refuses "a missing file" "notary-9.9.9-linux-aarch64.tar.gz.sha256" 1 \
"$(grep -v 'aarch64.tar.gz.sha256' <<<"$full")"
# The pattern and replacement live in variables: quotes written inside
# ${var/pattern/replacement} are kept as literal characters, which renamed the
# file in this fixture and made it fail for the wrong reason.
local from to
from="Notary-v9.9.9-macos.zip${tab}uploaded" to="Notary-v9.9.9-macos.zip${tab}starter"
refuses "an unfinished upload" "Notary-v9.9.9-macos.zip" 1 "${full/$from/$to}"
from="x86_64.tar.gz${tab}uploaded${tab}1000" to="x86_64.tar.gz${tab}uploaded${tab}0"
refuses "an empty file" "notary-9.9.9-linux-x86_64.tar.gz" 1 "${full/$from/$to}"
refuses "a release built for another version" "Notary-v9.9.9-macos.zip" 5 \
"${full//9.9.9/9.9.8}"

local matrix
matrix="$(grep -o 'arch: [a-z0-9_]*' "$here/.github/workflows/release.yml" | cut -d' ' -f2 | sort | tr '\n' ' ')"
if [ "$matrix" != "$(tr ' ' '\n' <<<"$LINUX_ARCHES" | grep . | sort | tr '\n' ' ')" ]; then
echo "release.yml builds [$matrix] but this checks [$LINUX_ARCHES]" >&2; exit 1
fi
echo "ok: the architectures match release.yml's matrix"
}

case "${1:-}" in
--self-test) self_test ;;
"") echo "usage: $0 <tag> < assets, or $0 --self-test" >&2; exit 2 ;;
*) check "$1" ;;
esac
Loading