| 1 | #!/usr/bin/env bash |
| 2 | set -euo pipefail |
| 3 | |
| 4 | script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
| 5 | repo_root="$(cd "${script_dir}/../.." && pwd)" |
| 6 | |
| 7 | usage() { |
| 8 | cat <<'EOF' |
| 9 | usage: scripts/release/verify-release-assets.sh [--allow-npm-binary-mismatch] [VERSION] |
| 10 | |
| 11 | Proves the public GitHub Release assets for VERSION were built from the same |
| 12 | tag commit that will be published to Cargo/npm. |
| 13 | |
| 14 | Checks: |
| 15 | - local tag vVERSION exists |
| 16 | - remote tag vVERSION resolves to the same commit SHA |
| 17 | - GitHub Release vVERSION exists |
| 18 | - a successful Release workflow run used that SHA |
| 19 | - npm/codewhale release:check sees the fresh binary/archive/installer matrix |
| 20 | and both required checksum manifests |
| 21 | |
| 22 | Set GH_BIN=/path/to/gh to choose a GitHub CLI binary. Set |
| 23 | CODEWHALE_GITHUB_REPO=owner/repo or CODEWHALE_RELEASE_REMOTE=remote to override |
| 24 | the default Hmbown/CodeWhale origin check. |
| 25 | EOF |
| 26 | } |
| 27 | |
| 28 | allow_npm_binary_mismatch=0 |
| 29 | version="" |
| 30 | |
| 31 | while (($# > 0)); do |
| 32 | case "$1" in |
| 33 | --allow-npm-binary-mismatch) |
| 34 | allow_npm_binary_mismatch=1 |
| 35 | ;; |
| 36 | -h|--help) |
| 37 | usage |
| 38 | exit 0 |
| 39 | ;; |
| 40 | *) |
| 41 | if [[ -n "${version}" ]]; then |
| 42 | usage >&2 |
| 43 | exit 2 |
| 44 | fi |
| 45 | version="$1" |
| 46 | ;; |
| 47 | esac |
| 48 | shift |
| 49 | done |
| 50 | |
| 51 | cd "${repo_root}" |
| 52 | |
| 53 | if [[ -z "${version}" ]]; then |
| 54 | version="$(grep -E '^version = "' Cargo.toml | head -n1 | sed -E 's/^version = "([^"]+)".*/\1/')" |
| 55 | fi |
| 56 | version="${version#v}" |
| 57 | tag="v${version}" |
| 58 | |
| 59 | if [[ -z "${version}" ]]; then |
| 60 | echo "Could not determine release version." >&2 |
| 61 | exit 1 |
| 62 | fi |
| 63 | |
| 64 | repo="${CODEWHALE_GITHUB_REPO:-Hmbown/CodeWhale}" |
| 65 | remote="${CODEWHALE_RELEASE_REMOTE:-origin}" |
| 66 | gh_bin="${GH_BIN:-gh}" |
| 67 | |
| 68 | if ! command -v "${gh_bin}" >/dev/null 2>&1; then |
| 69 | echo "GitHub CLI not found: ${gh_bin}" >&2 |
| 70 | echo "Install gh or set GH_BIN=/path/to/gh." >&2 |
| 71 | exit 1 |
| 72 | fi |
| 73 | |
| 74 | local_sha="$(git rev-list -n 1 "${tag}" 2>/dev/null || true)" |
| 75 | if [[ -z "${local_sha}" ]]; then |
| 76 | echo "Local tag ${tag} does not exist." >&2 |
| 77 | exit 1 |
| 78 | fi |
| 79 | |
| 80 | remote_sha="$(git ls-remote --tags "${remote}" "refs/tags/${tag}^{}" | awk 'NR == 1 {print $1}')" |
| 81 | if [[ -z "${remote_sha}" ]]; then |
| 82 | remote_sha="$(git ls-remote --tags "${remote}" "refs/tags/${tag}" | awk 'NR == 1 {print $1}')" |
| 83 | fi |
| 84 | if [[ -z "${remote_sha}" ]]; then |
| 85 | echo "Remote tag ${tag} does not exist on ${remote}." >&2 |
| 86 | exit 1 |
| 87 | fi |
| 88 | if [[ "${local_sha}" != "${remote_sha}" ]]; then |
| 89 | echo "Tag SHA mismatch for ${tag}:" >&2 |
| 90 | echo " local : ${local_sha}" >&2 |
| 91 | echo " remote: ${remote_sha}" >&2 |
| 92 | exit 1 |
| 93 | fi |
| 94 | echo "Tag check OK: ${tag} -> ${local_sha}" |
| 95 | |
| 96 | release_url="$("${gh_bin}" release view "${tag}" --repo "${repo}" --json url --jq '.url')" |
| 97 | if [[ -z "${release_url}" ]]; then |
| 98 | echo "GitHub Release ${tag} was not found in ${repo}." >&2 |
| 99 | exit 1 |
| 100 | fi |
| 101 | echo "GitHub Release OK: ${release_url}" |
| 102 | |
| 103 | run_summary="$( |
| 104 | TAG_SHA="${local_sha}" "${gh_bin}" run list \ |
| 105 | --repo "${repo}" \ |
| 106 | --workflow "Release" \ |
| 107 | --limit 100 \ |
| 108 | --json databaseId,headSha,headBranch,event,conclusion,status,createdAt,updatedAt,url \ |
| 109 | --jq 'map(select(.headSha == env.TAG_SHA and .conclusion == "success" and (.event == "push" or .event == "workflow_dispatch"))) | sort_by(.updatedAt) | last | if . == null then empty else "\(.databaseId)\t\(.headBranch)\t\(.event)\t\(.url)" end' |
| 110 | )" |
| 111 | if [[ -z "${run_summary}" ]]; then |
| 112 | echo "No successful Release workflow run found in the last 100 Release runs for ${tag} at ${local_sha}." >&2 |
| 113 | echo "Rerun the Release workflow before publishing Cargo/npm." >&2 |
| 114 | exit 1 |
| 115 | fi |
| 116 | printf 'Release workflow OK: %s\n' "${run_summary}" |
| 117 | |
| 118 | npm_package_version="$(node -p "require('./npm/codewhale/package.json').version")" |
| 119 | npm_binary_version="$( |
| 120 | node -p "const p=require('./npm/codewhale/package.json'); p.codewhaleBinaryVersion || p.deepseekBinaryVersion || p.version" |
| 121 | )" |
| 122 | if [[ "${npm_package_version}" != "${version}" ]]; then |
| 123 | echo "npm/codewhale package version ${npm_package_version} does not match ${version}." >&2 |
| 124 | exit 1 |
| 125 | fi |
| 126 | if [[ "${npm_binary_version}" != "${version}" && "${allow_npm_binary_mismatch}" != "1" ]]; then |
| 127 | echo "npm/codewhale codewhaleBinaryVersion ${npm_binary_version} does not match ${version}." >&2 |
| 128 | echo "Use --allow-npm-binary-mismatch only for an intentional packaging-only npm release." >&2 |
| 129 | exit 1 |
| 130 | fi |
| 131 | |
| 132 | ( |
| 133 | cd npm/codewhale |
| 134 | env \ |
| 135 | -u CODEWHALE_RELEASE_BASE_URL \ |
| 136 | -u DEEPSEEK_TUI_RELEASE_BASE_URL \ |
| 137 | -u DEEPSEEK_RELEASE_BASE_URL \ |
| 138 | -u CODEWHALE_USE_CNB_MIRROR \ |
| 139 | DEEPSEEK_TUI_VERSION="${version}" \ |
| 140 | DEEPSEEK_TUI_GITHUB_REPO="${repo}" \ |
| 141 | CODEWHALE_ALLOW_NPM_BINARY_MISMATCH="${allow_npm_binary_mismatch}" \ |
| 142 | npm run release:check |
| 143 | ) |
| 144 | |
| 145 | echo "Release asset gate OK: ${tag} assets match ${local_sha} and npm/codewhale is ready for publish." |
| 146 |