231 lines
6.7 KiB
Bash
Executable File
231 lines
6.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# The release flow both publish scripts share: argument parsing, preflight
|
|
# checks, tag preparation and the push.
|
|
#
|
|
# . "$(dirname "$0")/lib/publish-common.sh"
|
|
# . "$(dirname "$0")/lib/publish-release.sh"
|
|
#
|
|
# What differs between the two remotes is only the last step: the GitHub
|
|
# release object (which the release workflow builds the images from) and the
|
|
# Gitea one. Everything before that lives here, so the two scripts cannot
|
|
# drift apart.
|
|
|
|
# Defaults, overridden by the flags in parse_publish_args.
|
|
BRANCH="main"
|
|
VERSION_RAW=""
|
|
TAG=""
|
|
SKIP_CHECKS=0
|
|
HELP=0
|
|
REMAINING_ARGS=()
|
|
NOTES_FILE=""
|
|
TAG_CREATED=0
|
|
|
|
parse_publish_args() {
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
-h | --help) HELP=1; shift ;;
|
|
--yes | -y) ASSUME_YES=1; shift ;;
|
|
--dry-run) DRY_RUN=1; shift ;;
|
|
--branch) BRANCH="${2:?--branch needs a value}"; shift 2 ;;
|
|
--version) VERSION_RAW="${2:?--version needs a value}"; shift 2 ;;
|
|
--skip-checks) SKIP_CHECKS=1; shift ;;
|
|
*)
|
|
REMAINING_ARGS+=("$1")
|
|
shift
|
|
;;
|
|
esac
|
|
done
|
|
|
|
DRY_RUN="${DRY_RUN:-0}"
|
|
ASSUME_YES="${ASSUME_YES:-0}"
|
|
}
|
|
|
|
# Fails when the platform script was handed a flag it does not know.
|
|
reject_unknown_args() {
|
|
local script="$1"
|
|
if [ "${#REMAINING_ARGS[@]}" -eq 0 ]; then
|
|
return 0
|
|
fi
|
|
printf 'Unknown argument: %s\n\n' "${REMAINING_ARGS[*]}" >&2
|
|
"$script" --help >&2
|
|
exit 2
|
|
}
|
|
|
|
# MARK:preflight
|
|
preflight() {
|
|
local remote="$1"
|
|
|
|
require_repo_root
|
|
require_cmd git
|
|
require_cmd "bun" "Install Bun: https://bun.sh"
|
|
|
|
step "Checking the checkout"
|
|
require_clean_worktree
|
|
ok "The working tree has nothing uncommitted."
|
|
require_on_branch "$BRANCH"
|
|
ok "On $BRANCH."
|
|
require_remote "$remote"
|
|
ok "Remote '$remote' is $(git remote get-url "$remote")."
|
|
require_not_behind "$remote" "$BRANCH"
|
|
ok "$remote/$BRANCH has nothing HEAD is missing."
|
|
}
|
|
# END:preflight
|
|
|
|
# MARK:resolve-version
|
|
# Sets TAG (vX.Y.Z) from --version, else from package.json.
|
|
resolve_version() {
|
|
step "Resolving the version"
|
|
if [ -z "$VERSION_RAW" ]; then
|
|
VERSION_RAW="$(source_version)"
|
|
if [ -z "$VERSION_RAW" ]; then
|
|
fail "No version in package.json and none passed with --version."
|
|
fi
|
|
detail "Read from package.json: $VERSION_RAW"
|
|
fi
|
|
validate_version "$VERSION_RAW"
|
|
TAG="$(normalize_version "$VERSION_RAW")"
|
|
ok "Releasing $TAG."
|
|
|
|
if [ "$TAG" != "v$(source_version)" ]; then
|
|
warn "package.json says $(source_version) but the release is $TAG. Bump package.json's version with the release so a dev build reports the same number (RELEASING.md, step 5)."
|
|
fi
|
|
}
|
|
# END:resolve-version
|
|
|
|
# MARK:checks
|
|
# The release gate from RELEASING.md that a machine can check.
|
|
run_release_checks() {
|
|
if [ "$SKIP_CHECKS" = "1" ]; then
|
|
warn "Skipping typecheck, tests and the installer syntax check (--skip-checks)."
|
|
return 0
|
|
fi
|
|
|
|
step "Running the release checks"
|
|
run bun run typecheck
|
|
ok "Type-check passed."
|
|
run bun run test
|
|
ok "Tests passed."
|
|
run bash -n install.sh
|
|
ok "install.sh parses."
|
|
}
|
|
# END:checks
|
|
|
|
# MARK:tag
|
|
# Creates the tag at HEAD, or verifies the existing one is at HEAD.
|
|
prepare_tag() {
|
|
step "Preparing the tag $TAG"
|
|
|
|
if local_tag_exists "$TAG"; then
|
|
local tagged
|
|
tagged="$(git rev-list -n1 "$TAG")"
|
|
if [ "$tagged" != "$(git rev-parse HEAD)" ]; then
|
|
fail "Tag $TAG already exists on ${tagged:0:12}, not on HEAD ($(git rev-parse --short HEAD)). Publishing would ship the code that tag was made from. Move it with: git tag -f -a $TAG -m '$TAG'"
|
|
fi
|
|
ok "Tag $TAG already points at HEAD; reusing it."
|
|
return 0
|
|
fi
|
|
|
|
run git tag -a "$TAG" -m "$TAG"
|
|
if [ "$DRY_RUN" != "1" ]; then
|
|
TAG_CREATED=1
|
|
fi
|
|
ok "Tagged HEAD as $TAG."
|
|
}
|
|
# END:tag
|
|
|
|
# MARK:notes-file
|
|
# Writes release_notes into NOTES_FILE, which the caller cleans up. The
|
|
# compare base is the host's /compare/ path; the tag range is added here,
|
|
# since only this side knows which tag came before (a bare /compare/ opens
|
|
# the host's default comparison rather than this release's).
|
|
build_compare_url() {
|
|
local compare_base="$1"
|
|
local from="$2"
|
|
|
|
if [ -z "$from" ]; then
|
|
printf '%s' "$compare_base"
|
|
return 0
|
|
fi
|
|
printf '%s%s...%s' "$compare_base" "$from" "$TAG"
|
|
}
|
|
|
|
write_notes_file() {
|
|
local compare_base="$1"
|
|
local compare_url=""
|
|
local from=""
|
|
# A dry run makes no tag, so the notes are read up to HEAD instead.
|
|
local to="$TAG"
|
|
|
|
if ! local_tag_exists "$to"; then
|
|
to="HEAD"
|
|
fi
|
|
|
|
from="$(previous_tag "$to")"
|
|
|
|
if [ -n "$from" ]; then
|
|
detail "Changes since $from."
|
|
else
|
|
detail "No earlier tag, so every commit is listed."
|
|
fi
|
|
|
|
compare_url="$(build_compare_url "$compare_base" "$from")"
|
|
|
|
NOTES_FILE="$(mktemp -t cobble-release-notes.XXXXXX)"
|
|
release_notes --from "$from" --to "$to" --compare-url "$compare_url" >"$NOTES_FILE"
|
|
ok "Wrote the release notes to $NOTES_FILE."
|
|
}
|
|
|
|
cleanup_notes() {
|
|
if [ -n "$NOTES_FILE" ] && [ -f "$NOTES_FILE" ]; then
|
|
rm -f "$NOTES_FILE"
|
|
fi
|
|
}
|
|
# END:notes-file
|
|
|
|
# MARK:push
|
|
# Pushes the branch, then the tag. A tag the remote already has is left alone,
|
|
# so a rerun after a failed release object finishes the job instead of
|
|
# failing on the push.
|
|
push_branch_and_tag() {
|
|
local remote="$1"
|
|
|
|
step "Pushing to $remote"
|
|
run git push "$remote" "$BRANCH"
|
|
ok "Pushed $BRANCH."
|
|
|
|
if [ -n "$(remote_tag_exists "$remote" "$TAG")" ]; then
|
|
ok "$remote already has $TAG; not pushing it again."
|
|
return 0
|
|
fi
|
|
run git push "$remote" "refs/tags/$TAG"
|
|
ok "Pushed $TAG."
|
|
}
|
|
# END:push
|
|
|
|
# MARK:reuse-release
|
|
# A release object that already exists is left as it is, so rerunning the
|
|
# script never fails on it and never overwrites a body someone edited.
|
|
release_already_exists() {
|
|
local description="$1"
|
|
warn "$description for $TAG already exists; leaving it untouched."
|
|
warn "Delete it first if it needs to be recreated."
|
|
}
|
|
# END:reuse-release
|
|
|
|
print_release_summary() {
|
|
local remote="$1"
|
|
local branch="$2"
|
|
local release_url="$3"
|
|
|
|
step "Published $TAG"
|
|
printf ' %sremote%s %s\n' "$C_DIM" "$C_RESET" "$remote ($(git remote get-url "$remote"))"
|
|
printf ' %sbranch%s %s\n' "$C_DIM" "$C_RESET" "$branch"
|
|
printf ' %stag%s %s\n' "$C_DIM" "$C_RESET" "$TAG"
|
|
if [ -n "$release_url" ]; then
|
|
printf ' %srelease%s %s\n' "$C_DIM" "$C_RESET" "$release_url"
|
|
fi
|
|
if [ "$DRY_RUN" = "1" ]; then
|
|
printf '\n%sDry run: nothing was tagged, pushed or published.%s\n' "$C_YELLOW" "$C_RESET"
|
|
fi
|
|
}
|