|
| 1 | +#!/usr/bin/env bash |
| 2 | + |
| 3 | +set -euo pipefail |
| 4 | + |
| 5 | +usage() { |
| 6 | + cat >&2 <<'USAGE' |
| 7 | +usage: VERSION=X.Y.Z [REPO=dc-tec/openbao-operator] hack/ci/verify-post-release.sh |
| 8 | +
|
| 9 | +Verifies the post-release invariants that should hold after the Release workflow |
| 10 | +has published a stable release. |
| 11 | +
|
| 12 | +Environment: |
| 13 | + VERSION Required release version, for example 0.3.0. |
| 14 | + REPO GitHub repository. Default: dc-tec/openbao-operator. |
| 15 | + GIT_REMOTE Git remote used for branch/tag checks. Default: https://github.com/${REPO}.git. |
| 16 | + ALLOW_DRAFT Set to 1 to allow a draft GitHub Release. Default: 0. |
| 17 | +USAGE |
| 18 | +} |
| 19 | + |
| 20 | +fail() { |
| 21 | + echo "error: $*" >&2 |
| 22 | + exit 1 |
| 23 | +} |
| 24 | + |
| 25 | +info() { |
| 26 | + echo "==> $*" |
| 27 | +} |
| 28 | + |
| 29 | +require_cmd() { |
| 30 | + local cmd="$1" |
| 31 | + command -v "${cmd}" >/dev/null 2>&1 || fail "required command not found: ${cmd}" |
| 32 | +} |
| 33 | + |
| 34 | +VERSION="${VERSION:-${1:-}}" |
| 35 | +REPO="${REPO:-dc-tec/openbao-operator}" |
| 36 | +OWNER="${REPO%%/*}" |
| 37 | +GIT_REMOTE="${GIT_REMOTE:-https://github.com/${REPO}.git}" |
| 38 | +ALLOW_DRAFT="${ALLOW_DRAFT:-0}" |
| 39 | + |
| 40 | +if [[ "${VERSION}" == "-h" || "${VERSION}" == "--help" ]]; then |
| 41 | + usage |
| 42 | + exit 0 |
| 43 | +fi |
| 44 | + |
| 45 | +if [[ -z "${VERSION}" ]]; then |
| 46 | + usage |
| 47 | + exit 2 |
| 48 | +fi |
| 49 | + |
| 50 | +if ! [[ "${VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+([-.+][0-9A-Za-z.-]+)?$ ]]; then |
| 51 | + fail "VERSION must be SemVer, got '${VERSION}'" |
| 52 | +fi |
| 53 | + |
| 54 | +for cmd in gh jq git docker cosign; do |
| 55 | + require_cmd "${cmd}" |
| 56 | +done |
| 57 | + |
| 58 | +required_assets=( |
| 59 | + install.yaml |
| 60 | + crds.yaml |
| 61 | + checksums.txt |
| 62 | + checksums.txt.bundle |
| 63 | + checksums.txt.sigstore.json |
| 64 | + checksums.intoto.jsonl |
| 65 | + sbom-openbao-operator.spdx.json |
| 66 | + sbom-openbao-init.spdx.json |
| 67 | + sbom-openbao-backup.spdx.json |
| 68 | + sbom-openbao-upgrade.spdx.json |
| 69 | + provenance-index.json |
| 70 | +) |
| 71 | + |
| 72 | +info "checking remote tag ${VERSION}" |
| 73 | +git ls-remote --exit-code --tags "${GIT_REMOTE}" "refs/tags/${VERSION}" >/dev/null || |
| 74 | + fail "release tag ${VERSION} was not found on ${GIT_REMOTE}" |
| 75 | + |
| 76 | +info "checking GitHub Release ${VERSION}" |
| 77 | +release_json="$( |
| 78 | + gh release view "${VERSION}" \ |
| 79 | + --repo "${REPO}" \ |
| 80 | + --json assets,isDraft,isPrerelease,tagName,url |
| 81 | +)" |
| 82 | + |
| 83 | +release_tag="$(jq -r '.tagName' <<<"${release_json}")" |
| 84 | +if [[ "${release_tag}" != "${VERSION}" ]]; then |
| 85 | + fail "GitHub Release tagName is '${release_tag}', expected '${VERSION}'" |
| 86 | +fi |
| 87 | + |
| 88 | +is_draft="$(jq -r '.isDraft' <<<"${release_json}")" |
| 89 | +if [[ "${is_draft}" == "true" && "${ALLOW_DRAFT}" != "1" ]]; then |
| 90 | + fail "GitHub Release ${VERSION} is still a draft" |
| 91 | +fi |
| 92 | + |
| 93 | +mapfile -t asset_names < <(jq -r '.assets[].name' <<<"${release_json}" | LC_ALL=C sort) |
| 94 | +missing_assets=() |
| 95 | +for asset in "${required_assets[@]}"; do |
| 96 | + if ! printf '%s\n' "${asset_names[@]}" | grep -Fxq "${asset}"; then |
| 97 | + missing_assets+=("${asset}") |
| 98 | + fi |
| 99 | +done |
| 100 | +if (( ${#missing_assets[@]} > 0 )); then |
| 101 | + printf 'missing release assets:\n' >&2 |
| 102 | + printf ' - %s\n' "${missing_assets[@]}" >&2 |
| 103 | + exit 1 |
| 104 | +fi |
| 105 | + |
| 106 | +release_url="$(jq -r '.url' <<<"${release_json}")" |
| 107 | +info "release assets present: ${release_url}" |
| 108 | + |
| 109 | +tmpdir="$(mktemp -d)" |
| 110 | +trap 'rm -rf "${tmpdir}"' EXIT |
| 111 | + |
| 112 | +info "downloading signature evidence" |
| 113 | +gh release download "${VERSION}" \ |
| 114 | + --repo "${REPO}" \ |
| 115 | + --dir "${tmpdir}" \ |
| 116 | + --clobber \ |
| 117 | + --pattern checksums.txt \ |
| 118 | + --pattern checksums.txt.bundle \ |
| 119 | + --pattern provenance-index.json |
| 120 | + |
| 121 | +identity="https://github.com/${REPO}/.github/workflows/release.yml@refs/tags/${VERSION}" |
| 122 | + |
| 123 | +info "verifying checksums signature" |
| 124 | +cosign verify-blob \ |
| 125 | + --new-bundle-format=true \ |
| 126 | + --bundle "${tmpdir}/checksums.txt.bundle" \ |
| 127 | + --certificate-identity "${identity}" \ |
| 128 | + --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \ |
| 129 | + "${tmpdir}/checksums.txt" >/dev/null |
| 130 | + |
| 131 | +provenance_tag="$(jq -r '.release.tag' "${tmpdir}/provenance-index.json")" |
| 132 | +if [[ "${provenance_tag}" != "${VERSION}" ]]; then |
| 133 | + fail "provenance-index.json release tag is '${provenance_tag}', expected '${VERSION}'" |
| 134 | +fi |
| 135 | + |
| 136 | +chart_ref="ghcr.io/${OWNER}/charts/openbao-operator:${VERSION}" |
| 137 | +info "checking Helm chart publication: ${chart_ref}" |
| 138 | +chart_digest="$( |
| 139 | + docker buildx imagetools inspect "${chart_ref}" --format '{{json .Manifest.Digest}}' | tr -d '"' |
| 140 | +)" |
| 141 | +if [[ -z "${chart_digest}" || "${chart_digest}" == "null" ]]; then |
| 142 | + fail "could not resolve chart digest for ${chart_ref}" |
| 143 | +fi |
| 144 | + |
| 145 | +info "verifying Helm chart signature" |
| 146 | +cosign verify \ |
| 147 | + --new-bundle-format=true \ |
| 148 | + --certificate-identity "${identity}" \ |
| 149 | + --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \ |
| 150 | + "ghcr.io/${OWNER}/charts/openbao-operator@${chart_digest}" >/dev/null |
| 151 | + |
| 152 | +info "checking for open release-please PRs" |
| 153 | +open_release_prs="$( |
| 154 | + gh pr list \ |
| 155 | + --repo "${REPO}" \ |
| 156 | + --state open \ |
| 157 | + --json number,title,headRefName,url \ |
| 158 | + --jq '.[] | select(.headRefName | startswith("release-please--branches--")) | "#\(.number) \(.title) [\(.headRefName)] \(.url)"' |
| 159 | +)" |
| 160 | +if [[ -n "${open_release_prs}" ]]; then |
| 161 | + echo "${open_release_prs}" >&2 |
| 162 | + fail "unexpected open release-please PRs remain" |
| 163 | +fi |
| 164 | + |
| 165 | +info "checking for stale release-please branches" |
| 166 | +stale_branches="$(git ls-remote --heads "${GIT_REMOTE}" 'release-please--branches--*')" |
| 167 | +if [[ -n "${stale_branches}" ]]; then |
| 168 | + echo "${stale_branches}" >&2 |
| 169 | + fail "stale release-please branches remain" |
| 170 | +fi |
| 171 | + |
| 172 | +info "post-release verification passed for ${VERSION}" |
0 commit comments