mirror of
https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli.git
synced 2026-09-28 13:14:08 +08:00
494 lines
19 KiB
Bash
Executable File
494 lines
19 KiB
Bash
Executable File
#!/bin/sh
|
|
set -eu
|
|
|
|
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
|
|
. "$SCRIPT_DIR/release-lib.sh"
|
|
ROOT="$(CDPATH= cd -- "$SCRIPT_DIR/../.." && pwd)"
|
|
CLOUD_RELEASE_URL="https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/actions/workflows/release.yml"
|
|
|
|
CHANNEL="${1:-}"
|
|
VERSION="${2:-}"
|
|
REMOTE=""
|
|
BRANCH="main"
|
|
FROM_BETA=""
|
|
PUBLISH=0
|
|
YES=0
|
|
OFFICIAL_TAGS_URL="${DWS_RELEASE_OFFICIAL_TAGS_URL:-https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli.git}"
|
|
|
|
usage() {
|
|
cat >&2 <<'EOF'
|
|
usage: release.sh <prerelease|stable> <version> [options]
|
|
|
|
Runs the full test, authoritative CLI/Schema compatibility, package, and install preflight.
|
|
The default is validation only. Official publication is cloud-only.
|
|
|
|
Options:
|
|
--remote <name> Required tag destination
|
|
--from-beta <tag> Required for stable releases
|
|
--publish Create a tag only for a non-official test or fork remote
|
|
--yes Skip the interactive version confirmation
|
|
EOF
|
|
}
|
|
|
|
[ -n "$CHANNEL" ] && [ -n "$VERSION" ] || { usage; exit 2; }
|
|
shift 2
|
|
while [ "$#" -gt 0 ]; do
|
|
case "$1" in
|
|
--remote) [ "$#" -ge 2 ] || { usage; exit 2; }; REMOTE="$2"; shift 2 ;;
|
|
--from-beta) [ "$#" -ge 2 ] || { usage; exit 2; }; FROM_BETA="$2"; shift 2 ;;
|
|
--publish) PUBLISH=1; shift ;;
|
|
--yes) YES=1; shift ;;
|
|
-h|--help) usage; exit 0 ;;
|
|
*) printf 'unknown argument: %s\n' "$1" >&2; usage; exit 2 ;;
|
|
esac
|
|
done
|
|
|
|
release_validate_version_channel "$CHANNEL" "$VERSION"
|
|
[ -n "$REMOTE" ] || { printf '%s\n' '--remote is required' >&2; exit 2; }
|
|
cd "$ROOT"
|
|
fetch_url="$(git remote get-url "$REMOTE" 2>/dev/null)" || { printf 'unknown release remote: %s\n' "$REMOTE" >&2; exit 1; }
|
|
push_urls="$(git remote get-url --push --all "$REMOTE" 2>/dev/null)" || {
|
|
printf 'could not resolve push URL for release remote: %s\n' "$REMOTE" >&2
|
|
exit 1
|
|
}
|
|
[ "$(printf '%s\n' "$push_urls" | sed '/^$/d' | wc -l | tr -d '[:space:]')" -eq 1 ] || {
|
|
printf 'release remote must have exactly one push URL: %s\n' "$REMOTE" >&2
|
|
exit 1
|
|
}
|
|
push_url="$(printf '%s\n' "$push_urls" | sed -n '1p')"
|
|
|
|
repository_identity() {
|
|
identity_url="$1"
|
|
case "$identity_url" in
|
|
https://github.com/*) identity_path="${identity_url#https://github.com/}" ;;
|
|
http://github.com/*) identity_path="${identity_url#http://github.com/}" ;;
|
|
git://github.com/*) identity_path="${identity_url#git://github.com/}" ;;
|
|
git@github.com:*) identity_path="${identity_url#git@github.com:}" ;;
|
|
ssh://git@github.com/*) identity_path="${identity_url#ssh://git@github.com/}" ;;
|
|
*) printf '%s\n' "$identity_url"; return 0 ;;
|
|
esac
|
|
identity_path="${identity_path%/}"
|
|
identity_path="${identity_path%.git}"
|
|
printf 'github.com/%s\n' "$identity_path"
|
|
}
|
|
|
|
fetch_identity="$(repository_identity "$fetch_url")"
|
|
push_identity="$(repository_identity "$push_url")"
|
|
normalized_push_identity="$(
|
|
printf '%s\n' "$push_identity" | tr '[:upper:]' '[:lower:]'
|
|
)"
|
|
[ "$fetch_identity" = "$push_identity" ] || {
|
|
printf 'release remote fetch and push URLs target different repositories:\n fetch: %s\n push: %s\n' "$fetch_url" "$push_url" >&2
|
|
exit 1
|
|
}
|
|
if [ "$PUBLISH" -eq 1 ] &&
|
|
[ "$normalized_push_identity" = "github.com/dingtalk-real-ai/dingtalk-workspace-cli" ]; then
|
|
printf '%s\n' 'Direct tag publication to the official repository is disabled.' >&2
|
|
printf 'Use Actions -> Release -> Run workflow: %s\n' "$CLOUD_RELEASE_URL" >&2
|
|
exit 1
|
|
fi
|
|
printf 'Release target: %s\n fetch: %s\n push: %s\n' "$REMOTE" "$fetch_url" "$push_url"
|
|
|
|
github_repository_from_url() {
|
|
github_identity="$(repository_identity "$1")"
|
|
case "$github_identity" in
|
|
github.com/*) printf '%s\n' "${github_identity#github.com/}" ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
require_delivered_previous_stable() {
|
|
github_repository="$(github_repository_from_url "$push_url" 2>/dev/null || true)"
|
|
if [ -z "$github_repository" ]; then
|
|
[ "${DWS_RELEASE_ALLOW_NON_GITHUB_REMOTE:-0}" = "1" ] || {
|
|
printf 'release validation requires a github.com remote to prove the stable baseline was delivered\n' >&2
|
|
return 1
|
|
}
|
|
return 0
|
|
fi
|
|
[ -n "$previous_stable" ] || {
|
|
printf 'a delivered previous stable baseline is required\n' >&2
|
|
return 1
|
|
}
|
|
stable_commit="$(git rev-parse "$previous_stable^{commit}")"
|
|
DWS_RELEASE_OFFICIAL_REPOSITORY="${DWS_RELEASE_OFFICIAL_REPOSITORY:-DingTalk-Real-AI/dingtalk-workspace-cli}" \
|
|
"$SCRIPT_DIR/verify-delivered-stable.sh" "$previous_stable" "$stable_commit"
|
|
}
|
|
|
|
require_github_publication_authority() {
|
|
github_repository="$(github_repository_from_url "$push_url" 2>/dev/null || true)"
|
|
if [ -z "$github_repository" ]; then
|
|
[ "${DWS_RELEASE_ALLOW_NON_GITHUB_REMOTE:-0}" = "1" ] || {
|
|
printf 'publishing requires a github.com remote so CI and delivery authority can be verified\n' >&2
|
|
return 1
|
|
}
|
|
printf 'warning: GitHub publication checks explicitly disabled for non-GitHub test remote\n' >&2
|
|
return 0
|
|
fi
|
|
command -v gh >/dev/null 2>&1 || {
|
|
printf 'gh is required to verify CI and release authority before publishing\n' >&2
|
|
return 1
|
|
}
|
|
|
|
active_runs="$(
|
|
gh api -H 'Accept: application/vnd.github+json' \
|
|
"repos/$github_repository/actions/workflows/release.yml/runs?per_page=100" \
|
|
--jq '.workflow_runs[] | select(.status != "completed") | [.id, .event, .status, .head_branch] | @tsv'
|
|
)" || {
|
|
printf 'could not query active Release workflow runs for %s\n' "$github_repository" >&2
|
|
return 1
|
|
}
|
|
[ -z "$active_runs" ] || {
|
|
printf 'another Release workflow is still active; wait for it before allocating a new tag:\n%s\n' "$active_runs" >&2
|
|
return 1
|
|
}
|
|
|
|
sealed_commit="$(git rev-parse HEAD)"
|
|
admission_check_runs="$(
|
|
gh api -H 'Accept: application/vnd.github+json' \
|
|
"repos/$github_repository/commits/$sealed_commit/check-runs?filter=latest&per_page=100" \
|
|
--jq '.check_runs | group_by(.name) | map(max_by(.id)) | .[] | [.name, (.conclusion // .status // "unknown")] | @tsv'
|
|
)" || {
|
|
printf 'could not query Code Admission contexts for %s\n' "$sealed_commit" >&2
|
|
return 1
|
|
}
|
|
|
|
missing_contexts=""
|
|
non_success_contexts=""
|
|
while IFS= read -r required_context; do
|
|
context_state="$(
|
|
printf '%s\n' "$admission_check_runs" |
|
|
awk -F '\t' -v required="$required_context" '$1 == required { state = $2 } END { print state }'
|
|
)"
|
|
if [ -z "$context_state" ]; then
|
|
missing_contexts="${missing_contexts}${missing_contexts:+, }$required_context"
|
|
elif [ "$context_state" != "success" ]; then
|
|
non_success_contexts="${non_success_contexts}${non_success_contexts:+, }$required_context=$context_state"
|
|
fi
|
|
done <<'ADMISSION_CONTEXTS'
|
|
Lint
|
|
Test
|
|
Coverage
|
|
Policy
|
|
Edition
|
|
Interface Integrity
|
|
AI Behavior
|
|
CLI Smoke
|
|
Mock MCP
|
|
ADMISSION_CONTEXTS
|
|
[ -z "$missing_contexts" ] && [ -z "$non_success_contexts" ] || {
|
|
printf 'Code Admission contexts are not all successful for sealed commit %s in %s; missing: %s; non-success: %s\n' \
|
|
"$sealed_commit" \
|
|
"$github_repository" \
|
|
"${missing_contexts:-none}" \
|
|
"${non_success_contexts:-none}" >&2
|
|
return 1
|
|
}
|
|
|
|
if [ "$CHANNEL" = "stable" ]; then
|
|
beta_state="$(
|
|
gh api -H 'Accept: application/vnd.github+json' \
|
|
"repos/$github_repository/releases/tags/$FROM_BETA" \
|
|
--jq '[.tag_name, .draft, .prerelease, .immutable] | @tsv'
|
|
)" || {
|
|
printf 'could not query beta release %s in %s\n' "$FROM_BETA" "$github_repository" >&2
|
|
return 1
|
|
}
|
|
[ "$beta_state" = "$(printf '%s\tfalse\ttrue\ttrue' "$FROM_BETA")" ] || {
|
|
printf '%s must be a public immutable prerelease before stable tag allocation (got: %s)\n' "$FROM_BETA" "$beta_state" >&2
|
|
return 1
|
|
}
|
|
beta_assets="$(
|
|
gh api -H 'Accept: application/vnd.github+json' \
|
|
"repos/$github_repository/releases/tags/$FROM_BETA" \
|
|
--jq '.assets[].name'
|
|
)" || return 1
|
|
[ "$(printf '%s\n' "$beta_assets" | sed '/^$/d' | wc -l | tr -d '[:space:]')" -eq 8 ] || {
|
|
printf 'beta release %s must contain exactly eight supported assets\n' "$FROM_BETA" >&2
|
|
return 1
|
|
}
|
|
for beta_asset in \
|
|
dws-darwin-amd64.tar.gz dws-darwin-arm64.tar.gz \
|
|
dws-linux-amd64.tar.gz dws-linux-arm64.tar.gz \
|
|
dws-windows-amd64.zip dws-windows-arm64.zip \
|
|
dws-skills.zip checksums.txt; do
|
|
[ "$(printf '%s\n' "$beta_assets" | grep -Fxc "$beta_asset")" -eq 1 ] || {
|
|
printf 'beta release %s must contain %s exactly once\n' "$FROM_BETA" "$beta_asset" >&2
|
|
return 1
|
|
}
|
|
done
|
|
beta_commit="$(git rev-parse "$FROM_BETA^{commit}")"
|
|
DWS_RELEASE_OFFICIAL_REPOSITORY="$github_repository" \
|
|
"$SCRIPT_DIR/verify-release-workflow-delivery.sh" "$FROM_BETA" "$beta_commit" || return 1
|
|
fi
|
|
}
|
|
|
|
require_release_governance_ci() {
|
|
governance_repository="$(github_repository_from_url "$push_url" 2>/dev/null || true)"
|
|
if [ -z "$governance_repository" ]; then
|
|
[ "${DWS_RELEASE_ALLOW_NON_GITHUB_REMOTE:-0}" = "1" ] || {
|
|
printf 'release governance preflight requires a github.com publication remote\n' >&2
|
|
return 1
|
|
}
|
|
printf 'warning: Release governance CI preflight explicitly disabled for non-GitHub test remote\n' >&2
|
|
return 0
|
|
fi
|
|
"$SCRIPT_DIR/verify-release-governance-ci.sh" \
|
|
"$governance_repository" \
|
|
"$(git rev-parse HEAD)"
|
|
}
|
|
|
|
preflight_proof_path() {
|
|
proof_dir="$(git rev-parse --git-path dws-release-preflight)"
|
|
printf '%s/%s.proof\n' "$proof_dir" "$VERSION"
|
|
}
|
|
|
|
preflight_proof_is_reusable() {
|
|
proof_file="$1"
|
|
[ -f "$proof_file" ] || return 1
|
|
[ "$(wc -l < "$proof_file" | tr -d '[:space:]')" -eq 9 ] || return 1
|
|
[ "$(sed -n 's/^format=//p' "$proof_file")" = "1" ] || return 1
|
|
[ "$(sed -n 's/^channel=//p' "$proof_file")" = "$CHANNEL" ] || return 1
|
|
[ "$(sed -n 's/^version=//p' "$proof_file")" = "$VERSION" ] || return 1
|
|
[ "$(sed -n 's/^commit=//p' "$proof_file")" = "$(git rev-parse HEAD)" ] || return 1
|
|
[ "$(sed -n 's/^repository=//p' "$proof_file")" = "$push_identity" ] || return 1
|
|
[ "$(sed -n 's/^branch=//p' "$proof_file")" = "$BRANCH" ] || return 1
|
|
[ "$(sed -n 's/^from_beta=//p' "$proof_file")" = "$FROM_BETA" ] || return 1
|
|
[ "$(sed -n 's/^previous_stable=//p' "$proof_file")" = "$previous_stable" ] || return 1
|
|
|
|
proof_created_at="$(sed -n 's/^created_at=//p' "$proof_file")"
|
|
printf '%s\n' "$proof_created_at" | grep -Eq '^[0-9]+$' || return 1
|
|
proof_now="$(date +%s)"
|
|
[ "$proof_created_at" -le "$proof_now" ] || return 1
|
|
[ $((proof_now - proof_created_at)) -le 21600 ] || return 1
|
|
}
|
|
|
|
write_preflight_proof() {
|
|
proof_file="$1"
|
|
proof_dir="$(dirname "$proof_file")"
|
|
umask 077
|
|
mkdir -p "$proof_dir"
|
|
proof_tmp="${proof_file}.tmp.$$"
|
|
{
|
|
printf 'format=1\n'
|
|
printf 'channel=%s\n' "$CHANNEL"
|
|
printf 'version=%s\n' "$VERSION"
|
|
printf 'commit=%s\n' "$(git rev-parse HEAD)"
|
|
printf 'repository=%s\n' "$push_identity"
|
|
printf 'branch=%s\n' "$BRANCH"
|
|
printf 'from_beta=%s\n' "$FROM_BETA"
|
|
printf 'previous_stable=%s\n' "$previous_stable"
|
|
printf 'created_at=%s\n' "$(date +%s)"
|
|
} > "$proof_tmp"
|
|
mv "$proof_tmp" "$proof_file"
|
|
}
|
|
|
|
build_policy_binary() {
|
|
policy_tmp="$ROOT/tmp/go-build"
|
|
mkdir -p "$policy_tmp"
|
|
GOTMPDIR="$policy_tmp" make build
|
|
if [ "$(uname -s)" = "Darwin" ] && [ "${DWS_RELEASE_ALLOW_NON_GITHUB_REMOTE:-0}" != "1" ]; then
|
|
command -v codesign >/dev/null 2>&1 || {
|
|
printf '%s\n' 'codesign is required to prepare the macOS policy binary' >&2
|
|
return 1
|
|
}
|
|
codesign --force --sign - "$ROOT/dws"
|
|
codesign --verify --strict "$ROOT/dws"
|
|
fi
|
|
}
|
|
|
|
fetch_release_tags() {
|
|
git fetch --force "$REMOTE" \
|
|
'+refs/tags/v*:refs/tags/v*' \
|
|
'+refs/tags/withdrawn/v*:refs/tags/withdrawn/v*'
|
|
git fetch --force --no-tags "$OFFICIAL_TAGS_URL" \
|
|
'+refs/tags/v*:refs/tags/v*' \
|
|
'+refs/tags/withdrawn/v*:refs/tags/withdrawn/v*'
|
|
}
|
|
|
|
printf '==> Refreshing %s/%s and release tags\n' "$REMOTE" "$BRANCH"
|
|
git fetch --force "$REMOTE" "+refs/heads/$BRANCH:refs/remotes/$REMOTE/$BRANCH"
|
|
fetch_release_tags
|
|
|
|
metadata="$(mktemp "${TMPDIR:-/tmp}/dws-release-metadata.XXXXXX")"
|
|
final_metadata="$(mktemp "${TMPDIR:-/tmp}/dws-release-final-metadata.XXXXXX")"
|
|
cleanup() { rm -f "$metadata" "$final_metadata"; }
|
|
trap cleanup EXIT HUP INT TERM
|
|
|
|
run_contract() {
|
|
if [ -n "$FROM_BETA" ]; then
|
|
"$SCRIPT_DIR/release-contract.sh" \
|
|
--channel "$CHANNEL" \
|
|
--version "$VERSION" \
|
|
--context local \
|
|
--remote "$REMOTE" \
|
|
--branch "$BRANCH" \
|
|
--from-beta "$FROM_BETA" \
|
|
"$@"
|
|
else
|
|
"$SCRIPT_DIR/release-contract.sh" \
|
|
--channel "$CHANNEL" \
|
|
--version "$VERSION" \
|
|
--context local \
|
|
--remote "$REMOTE" \
|
|
--branch "$BRANCH" \
|
|
"$@"
|
|
fi
|
|
}
|
|
|
|
run_contract --metadata-output "$metadata"
|
|
previous_stable="$(sed -n 's/^previous_stable=//p' "$metadata" | tail -1)"
|
|
|
|
printf '==> Verifying delivered stable baseline %s\n' "${previous_stable:-none}"
|
|
require_delivered_previous_stable
|
|
|
|
printf '==> Verifying GitHub publication authority before local gates\n'
|
|
require_github_publication_authority
|
|
|
|
proof_path="$(preflight_proof_path)"
|
|
ran_expensive_gates=0
|
|
if [ "$PUBLISH" -eq 1 ] && preflight_proof_is_reusable "$proof_path"; then
|
|
printf '==> Reusing exact preflight proof for %s at %s\n' "$VERSION" "$(git rev-parse HEAD)"
|
|
else
|
|
rm -f "$proof_path"
|
|
printf '==> Running repository test, build, and policy gates\n'
|
|
make test
|
|
# Installer tests intentionally exercise source builds. Rebuild the release
|
|
# binary before policy so policy never depends on a pre-existing ./dws.
|
|
build_policy_binary
|
|
make policy
|
|
|
|
if [ -n "$previous_stable" ]; then
|
|
printf '==> Comparing authoritative CLI and Schema with %s\n' "$previous_stable"
|
|
"$SCRIPT_DIR/check-release-compatibility.sh" \
|
|
--repo-root "$ROOT" \
|
|
--base-ref HEAD \
|
|
--stable-ref "$previous_stable" \
|
|
--candidate-ref HEAD
|
|
fi
|
|
|
|
printf '==> Building local release artifacts for %s\n' "$VERSION"
|
|
make package VERSION="$VERSION"
|
|
|
|
printf '==> Verifying release artifact set and checksums\n'
|
|
"$SCRIPT_DIR/verify-release-artifacts.sh" "$VERSION"
|
|
|
|
printf '==> Verifying npm package installation\n'
|
|
"$SCRIPT_DIR/verify-package-managers.sh" --npm-only --expected-version "$VERSION"
|
|
if command -v brew >/dev/null 2>&1; then
|
|
printf '==> Verifying Homebrew package installation\n'
|
|
"$SCRIPT_DIR/verify-package-managers.sh" --brew-only --expected-version "$VERSION"
|
|
fi
|
|
ran_expensive_gates=1
|
|
fi
|
|
|
|
# Collect human confirmation before the final remote validations.
|
|
if [ "$PUBLISH" -eq 1 ] && [ "$YES" -ne 1 ]; then
|
|
if [ ! -t 0 ]; then
|
|
printf 'interactive confirmation is unavailable; pass --yes after reviewing the preflight\n' >&2
|
|
exit 1
|
|
fi
|
|
printf 'Type %s to create and push the release tag: ' "$VERSION"
|
|
IFS= read -r confirmation
|
|
[ "$confirmation" = "$VERSION" ] || { printf 'release cancelled\n' >&2; exit 1; }
|
|
fi
|
|
|
|
printf '==> Verifying the Release workflow publication identity\n'
|
|
require_release_governance_ci
|
|
|
|
# Refresh after every local gate and the remote governance check. dist/ is
|
|
# ignored and therefore does not make the tree dirty.
|
|
printf '==> Refreshing %s/%s before sealing the tag\n' "$REMOTE" "$BRANCH"
|
|
git fetch --force "$REMOTE" "+refs/heads/$BRANCH:refs/remotes/$REMOTE/$BRANCH"
|
|
fetch_release_tags
|
|
run_contract --metadata-output "$final_metadata"
|
|
final_previous_stable="$(sed -n 's/^previous_stable=//p' "$final_metadata" | tail -1)"
|
|
[ -n "$final_previous_stable" ] || {
|
|
printf 'latest stable authority unexpectedly became empty\n' >&2
|
|
exit 1
|
|
}
|
|
previous_stable_before_refresh="$previous_stable"
|
|
previous_stable="$final_previous_stable"
|
|
|
|
printf '==> Revalidating delivered stable baseline %s\n' "$previous_stable"
|
|
require_delivered_previous_stable
|
|
|
|
if [ "$previous_stable" != "$previous_stable_before_refresh" ]; then
|
|
printf '==> Stable authority advanced from %s to %s; rechecking authoritative CLI and Schema compatibility\n' \
|
|
"${previous_stable_before_refresh:-none}" "$previous_stable"
|
|
"$SCRIPT_DIR/check-release-compatibility.sh" \
|
|
--repo-root "$ROOT" \
|
|
--base-ref HEAD \
|
|
--stable-ref "$previous_stable" \
|
|
--candidate-ref HEAD
|
|
fi
|
|
|
|
if [ "$PUBLISH" -eq 1 ]; then
|
|
printf '==> Reconfirming GitHub publication authority on the settled baseline\n'
|
|
require_github_publication_authority
|
|
fi
|
|
|
|
# Delivery, compatibility, and publication checks above may take long enough
|
|
# for main or stable authority to move. This last refresh must be followed only
|
|
# by local proof/tag creation. Only the tag is pushed: the sealed commit is
|
|
# already contained in protected main history, so a later main advance never
|
|
# invalidates the release.
|
|
printf '==> Settling final %s/%s and stable authority\n' "$REMOTE" "$BRANCH"
|
|
git fetch --force "$REMOTE" "+refs/heads/$BRANCH:refs/remotes/$REMOTE/$BRANCH"
|
|
fetch_release_tags
|
|
run_contract --metadata-output "$final_metadata"
|
|
settled_previous_stable="$(sed -n 's/^previous_stable=//p' "$final_metadata" | tail -1)"
|
|
[ "$settled_previous_stable" = "$previous_stable" ] || {
|
|
printf 'stable authority moved from %s to %s during final validation; rerun the release command\n' \
|
|
"$previous_stable" "$settled_previous_stable" >&2
|
|
exit 1
|
|
}
|
|
|
|
if [ "$ran_expensive_gates" -eq 1 ] &&
|
|
[ "$normalized_push_identity" != "github.com/dingtalk-real-ai/dingtalk-workspace-cli" ]; then
|
|
write_preflight_proof "$proof_path"
|
|
fi
|
|
|
|
if [ "$PUBLISH" -ne 1 ]; then
|
|
printf '\nPreflight passed. No tag was created.\n'
|
|
if [ "$normalized_push_identity" = "github.com/dingtalk-real-ai/dingtalk-workspace-cli" ]; then
|
|
printf 'Publish from Actions -> Release -> Run workflow: %s\n' "$CLOUD_RELEASE_URL"
|
|
else
|
|
printf 'Publish within six hours with the same command plus --publish to reuse this exact proof.\n'
|
|
fi
|
|
exit 0
|
|
fi
|
|
|
|
printf '==> Creating annotated tag %s\n' "$VERSION"
|
|
if [ "$CHANNEL" = "stable" ]; then
|
|
git tag -a "$VERSION" -m "Release $VERSION" -m 'Channel: stable' -m "From-Beta: $FROM_BETA"
|
|
else
|
|
git tag -a "$VERSION" -m "Release $VERSION" -m 'Channel: prerelease'
|
|
fi
|
|
|
|
if ! git push "$push_url" "refs/tags/$VERSION"; then
|
|
set +e
|
|
remote_refs="$(git ls-remote --tags "$push_url" "refs/tags/$VERSION" "refs/tags/$VERSION^{}")"
|
|
query_status=$?
|
|
set -e
|
|
if [ "$query_status" -ne 0 ]; then
|
|
printf 'tag push reported failure and remote state could not be verified; keeping local tag %s for investigation\n' "$VERSION" >&2
|
|
exit 1
|
|
fi
|
|
remote_object="$(printf '%s\n' "$remote_refs" | awk '$2 !~ /\^\{\}$/ { print $1; exit }')"
|
|
local_object="$(git rev-parse "refs/tags/$VERSION")"
|
|
if [ -z "$remote_object" ]; then
|
|
git tag -d "$VERSION" >/dev/null 2>&1 || true
|
|
printf 'tag push failed; remote has no tag and the new local tag was removed: %s\n' "$VERSION" >&2
|
|
exit 1
|
|
fi
|
|
if [ "$remote_object" != "$local_object" ]; then
|
|
printf 'tag push failed and remote %s points elsewhere; keeping local tag for investigation\n' "$VERSION" >&2
|
|
exit 1
|
|
fi
|
|
printf 'warning: push reported failure, but push target %s has the exact sealed tag; treating it as published\n' "$push_url" >&2
|
|
fi
|
|
|
|
printf 'Release tag pushed: %s -> %s. CI/CD now owns artifact publication.\n' "$VERSION" "$push_url"
|
|
rm -f "$proof_path"
|