diff --git a/tools/release-tools/03-vote-mail.sh b/tools/release-tools/03-vote-mail.sh index 82973f18..4bbf35be 100755 --- a/tools/release-tools/03-vote-mail.sh +++ b/tools/release-tools/03-vote-mail.sh @@ -64,7 +64,10 @@ ${SIGNER_NAME} (${APACHE_ID}) EOF )" -printf '%s\n' "$BODY" > "$body_file" +{ + printf 'Subject: %s\n' "$subject" + printf '\n%s\n' "$BODY" +} > "$body_file" { printf 'To: %s\n' "$VOTE_TO" printf 'Subject: %s\n' "$subject" diff --git a/tools/release-tools/04-release-complete.sh b/tools/release-tools/04-release-complete.sh index 92c8c8cf..82d5b029 100755 --- a/tools/release-tools/04-release-complete.sh +++ b/tools/release-tools/04-release-complete.sh @@ -24,20 +24,32 @@ source "${HERE}/release.env" source "${HERE}/lib/release-common.sh" usage() { - printf 'Usage: %s [--mail-only]\n' "$0" - printf ' --mail-only regenerate announcement drafts without packaging or SVN\n' + printf 'Usage: %s [--mail-only | --github-only]\n' "$0" + printf ' --mail-only regenerate announcement drafts without packaging or SVN\n' + printf ' --github-only publish or refresh the GitHub release only\n' + printf '\n' + printf 'A full run is idempotent: it detects the release files already present\n' + printf 'in release SVN and a GitHub release that already carries these notes,\n' + printf 'and skips those steps instead of failing.\n' } mail_only=0 +github_only=0 while [[ "$#" -gt 0 ]]; do case "$1" in --mail-only) mail_only=1 ;; + --github-only) github_only=1 ;; -h|--help) usage; exit 0 ;; *) usage >&2; die "unknown argument: $1" ;; esac shift done +if [[ "$mail_only" -eq 1 && "$github_only" -eq 1 ]]; then + usage >&2 + die "--mail-only and --github-only are mutually exclusive" +fi + write_announce_email() { local subject body_file eml_file body mkdir -p "$WORK_DIR" @@ -69,7 +81,10 @@ ${SIGNER_NAME} EOF )" - printf '%s\n' "$body" > "$body_file" + { + printf 'Subject: %s\n' "$subject" + printf '\n%s\n' "$body" + } > "$body_file" { printf 'To: %s\n' "$ANNOUNCE_TO" printf 'Subject: %s\n' "$subject" @@ -79,35 +94,156 @@ EOF ok "announcement draft: ${body_file}" ok "mail draft: ${eml_file}" + printf 'Subject: %s\n' "$subject" printf '%s\n' '----------------------------------------------------------------' printf '%s\n' "$body" printf '%s\n' '----------------------------------------------------------------' printf 'Review and send the message manually to %s. No email was sent.\n' "$ANNOUNCE_TO" } +# Writes the GitHub release notes and prints only the file path. +write_github_release_notes() { + local notes_file="${WORK_DIR}/github-release-notes.md" + mkdir -p "$WORK_DIR" + + cat > "$notes_file" </dev/null 2>&1 +} + +github_release_body() { + gh release view "$TAG" --repo "$GITHUB_REPO" --json body --jq '.body' 2>/dev/null | + tr -d '\r' +} + +# Turns the existing Git tag into a GitHub release. Never creates a tag, and +# never rewrites notes that already match the generated ones. +publish_github_release() { + local notes_file title current desired + + gh auth status >/dev/null 2>&1 || + die "gh is not authenticated; run 'gh auth login' or export GH_TOKEN" + + notes_file="$(write_github_release_notes)" + title="Apache Doris Operator ${VERSION}" + ok "GitHub release notes: ${notes_file}" + printf '%s\n' '----------------------------------------------------------------' + cat "$notes_file" + printf '%s\n' '----------------------------------------------------------------' + + if github_release_exists; then + current="$(github_release_body)" + desired="$(cat "$notes_file")" + if [[ "$current" == "$desired" ]]; then + ok "GitHub release ${TAG} is already published: ${GITHUB_TAG_URL}" + return 0 + fi + + warn "GitHub release ${TAG} exists with different notes" + if ! confirm "Replace the notes of GitHub release ${TAG}?"; then + warn "left the existing GitHub release untouched: ${GITHUB_TAG_URL}" + return 0 + fi + gh release edit "$TAG" --repo "$GITHUB_REPO" \ + --title "$title" --notes-file "$notes_file" + ok "updated GitHub release: ${GITHUB_TAG_URL}" + return 0 + fi + + printf 'GitHub release target: %s (from existing tag %s)\n' "$GITHUB_REPO" "$TAG" + if ! confirm "Create the GitHub release for tag ${TAG}?"; then + warn "stopped before creating the GitHub release" + return 0 + fi + gh release create "$TAG" --repo "$GITHUB_REPO" --verify-tag \ + --title "$title" --notes-file "$notes_file" + ok "created GitHub release: ${GITHUB_TAG_URL}" +} + if [[ "$mail_only" -eq 1 ]]; then validate_release_config mail - ok "mail-only mode: skipping tag, package, signing, and SVN operations" + ok "mail-only mode: skipping tag, package, signing, SVN, and GitHub operations" write_announce_email exit 0 fi validate_release_config -require_tools git gpg svn sha512sum gzip || die "install the missing release prerequisites" + +if [[ "$github_only" -eq 1 ]]; then + require_tools git gh || die "install the missing release prerequisites" + ok "github-only mode: skipping package, signing, SVN, and mail operations" + verify_tag_consistency + publish_github_release + exit 0 +fi + +require_tools git gpg svn sha512sum gzip gh || die "install the missing release prerequisites" export GPG_TTY="$(tty 2>/dev/null || true)" -SIGNER="$(resolve_signing_key)" -ok "signer: ${SIGNER}" verify_tag_consistency -prepare_source_artifacts "$SIGNER" - -stage_and_commit_version_dir \ - "$RELEASE_SVN_BASE" \ - "$RELEASE_SVN_DIR" \ - "release-svn" \ - "Release Apache Doris Operator ${VERSION}" \ - "${SOURCE_ARTIFACTS[@]}" -if [[ "$SVN_COMMITTED" -eq 1 ]]; then +release_published=0 +release_state="$( + svn_version_dir_state "$RELEASE_SVN_DIR" \ + "${PKG_BASE}.tar.gz" "${PKG_BASE}.tar.gz.asc" "${PKG_BASE}.tar.gz.sha512" +)" + +case "$release_state" in + complete) + ok "release files already published, skipping packaging and upload: ${RELEASE_SVN_DIR}/" + release_published=1 + ;; + partial) + die "incomplete release directory: ${RELEASE_SVN_DIR}/; inspect and repair it manually" + ;; + *) + SIGNER="$(resolve_signing_key)" + ok "signer: ${SIGNER}" + prepare_source_artifacts "$SIGNER" + + stage_and_commit_version_dir \ + "$RELEASE_SVN_BASE" \ + "$RELEASE_SVN_DIR" \ + "release-svn" \ + "Release Apache Doris Operator ${VERSION}" \ + "${SOURCE_ARTIFACTS[@]}" + release_published="$SVN_COMMITTED" + ;; +esac + +if [[ "$release_published" -eq 1 ]]; then + publish_github_release write_announce_email fi diff --git a/tools/release-tools/README.md b/tools/release-tools/README.md index 9b64a668..9d6982ef 100644 --- a/tools/release-tools/README.md +++ b/tools/release-tools/README.md @@ -20,16 +20,16 @@ under the License. # Doris Operator release tools These scripts package, sign, and publish Apache Doris Operator source releases. -They publish source artifacts only and generate vote and announcement email -drafts. They do not create Git tags or send email. - -The checked-in defaults target version and Git tag `26.0.0`. +They publish source artifacts only, turn the released Git tag into a GitHub +release, and generate vote and announcement email drafts. They do not create +Git tags or send email. ## Prerequisites -Install `git`, `gpg`, `svn`, `svnmucc`, `sha512sum`, `curl`, and `gzip`. The -selected Git tag must already exist locally and on the remote configured by -`GIT_REMOTE`. +Install `git`, `gpg`, `svn`, `svnmucc`, `sha512sum`, `curl`, `gzip`, and `gh`. +The selected Git tag must already exist locally and on the remote configured by +`GIT_REMOTE`. `gh` must be authenticated for `GITHUB_REPO` (`gh auth login`, or +export `GH_TOKEN`); it is used only by step 4. Edit `release.env` before each release. In particular, verify: @@ -37,6 +37,8 @@ Edit `release.env` before each release. In particular, verify: - `APACHE_ID`, `APACHE_EMAIL`, and `SIGNER_NAME`. - `SIGNING_KEY`: required full fingerprint of a locally available secret key. - release notes, verification, download, and mailing-list URLs. +- `GITHUB_REPO`, plus `DOCKER_IMAGE` and `DOCKER_IMAGE_URL` for the operator + image published with this release, for example `apache/doris:operator-26.0.1`. - `WORK_DIR`, which stores generated artifacts, SVN working copies, and drafts. `TAG` must be the final version with no RC suffix. Source artifacts and SVN @@ -112,6 +114,9 @@ asks for confirmation both before staging and before commit. This writes `vote-email.txt` and `vote-email.eml` under `WORK_DIR`. It prints the subject and body, then leaves sending to the release manager. +Both vote and announcement drafts start with a `Subject:` line followed by a +blank line, so the mail title never has to be retyped. + ### 4. Complete a passed release ```bash @@ -129,8 +134,33 @@ https://dist.apache.org/repos/dist/release/doris/doris-operator// It does not inspect, compare, promote, move, or delete anything under dev SVN. It refuses to overwrite an existing release directory and requires two -confirmations. Only after a successful commit does it create -`announce-email.txt` and `announce-email.eml`. +confirmations. + +Once the release files are in release SVN, the script turns the existing Git +tag into a GitHub release on `GITHUB_REPO`. The notes are written to +`github-release-notes.md` under `WORK_DIR`, printed for review, and published +with `gh` after one more confirmation. They link the release-note issue +(`RELEASE_NOTES_URL`), the operator image (`DOCKER_IMAGE`, for example +`apache/doris:operator-26.0.1`, with its `docker pull` command and +`DOCKER_IMAGE_URL`), and the formal source artifacts. `gh release create` runs +with `--verify-tag`, so it publishes the existing tag and never creates one. + +Finally the script writes `announce-email.txt` and `announce-email.eml`. + +The whole run is idempotent, so an interrupted release can simply be repeated: + +- release files already present in release SVN are detected by name, and + packaging, signing, and the SVN commit are skipped; +- a release directory holding only some of the three files is reported as + incomplete and stops the run for manual repair; +- an existing GitHub release whose notes already match is left untouched, and + differing notes are replaced only after an explicit confirmation. + +To publish or refresh only the GitHub release: + +```bash +./04-release-complete.sh --github-only +``` To regenerate only the announcement drafts: @@ -138,16 +168,19 @@ To regenerate only the announcement drafts: ./04-release-complete.sh --mail-only ``` -`--mail-only` skips Git, packaging, GPG, checksums, and SVN. +`--github-only` skips packaging, GPG, checksums, SVN, and the mail drafts. +`--mail-only` skips Git, packaging, GPG, checksums, SVN, and GitHub. ## Safety boundaries -- No script creates, updates, or pushes a Git tag. +- No script creates, updates, or pushes a Git tag; the GitHub release is + published from the existing tag with `gh release create --verify-tag`. - Local and remote tags are compared by peeled commit ID. - Generated signatures and checksums are verified immediately. - Dev and release uploads stop before checkout if the version directory exists. - SVN target URLs and staged files are shown before both confirmations. - SVN uploads contain only the source archive, signature, and checksum. +- Existing GitHub release notes are replaced only after a confirmation. - Public emails are drafts only. - No email was sent by any script; the release manager sends drafts manually. - Formal release packaging is independent from dev SVN. diff --git a/tools/release-tools/lib/release-common.sh b/tools/release-tools/lib/release-common.sh index 6d7ffab2..eabb0aff 100644 --- a/tools/release-tools/lib/release-common.sh +++ b/tools/release-tools/lib/release-common.sh @@ -76,7 +76,7 @@ validate_release_config() { ) if [[ "$mode" != "mail" ]]; then - required+=(REPO_DIR GIT_REMOTE) + required+=(REPO_DIR GIT_REMOTE GITHUB_REPO DOCKER_IMAGE DOCKER_IMAGE_URL) fi for name in "${required[@]}"; do @@ -96,6 +96,10 @@ validate_release_config() { if [[ "$mode" != "mail" ]]; then [[ "$REPO_DIR" == /* ]] || die "release.env: REPO_DIR must be an absolute path" + [[ "$GITHUB_REPO" == */* && "$GITHUB_REPO" != */*/* ]] || + die "release.env: GITHUB_REPO must be /" + [[ "$DOCKER_IMAGE" == *:*"${VERSION}" ]] || + die "release.env: DOCKER_IMAGE must be a : ending with ${VERSION}" fi } @@ -233,6 +237,33 @@ svn_url_exists() { svn info "${SVN_AUTH_ARGS[@]}" "$url" >/dev/null 2>&1 } +# Prints the publication state of an SVN version directory, so a workflow can +# tell an interrupted upload from a finished one and re-run safely: +# missing the directory does not exist yet +# complete the directory exists and holds every expected file name +# partial the directory exists but at least one expected file is absent +svn_version_dir_state() { + local url="$1" + shift + local listing name + [[ "$#" -gt 0 ]] || die "no file names supplied for SVN inspection" + + build_svn_auth_args + if ! svn_url_exists "$url"; then + printf 'missing\n' + return 0 + fi + + listing="$(svn ls "${SVN_AUTH_ARGS[@]}" "$url" 2>/dev/null || true)" + for name in "$@"; do + if ! printf '%s\n' "$listing" | grep -Fxq "$name"; then + printf 'partial\n' + return 0 + fi + done + printf 'complete\n' +} + stage_and_commit_version_dir() { local svn_base="$1" svn_dir="$2" stage_name="$3" commit_message="$4" shift 4 diff --git a/tools/release-tools/release.env b/tools/release-tools/release.env index f5c4b538..e466a498 100644 --- a/tools/release-tools/release.env +++ b/tools/release-tools/release.env @@ -21,7 +21,7 @@ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_DIR="${ROOT}/../.." -VERSION="26.0.0" +VERSION="26.0.1" TAG="${VERSION}" GIT_REMOTE="upstream-apache" @@ -43,11 +43,16 @@ SIGNER_NAME="Mingyu Chen" # Required: full fingerprint of the secret key used to sign release artifacts. SIGNING_KEY="" -GITHUB_TAG_URL="https://github.com/apache/doris-operator/releases/tag/${TAG}" -RELEASE_NOTES_URL="https://github.com/apache/doris-operator/issues/506" +GITHUB_REPO="apache/doris-operator" +GITHUB_TAG_URL="https://github.com/${GITHUB_REPO}/releases/tag/${TAG}" +RELEASE_NOTES_URL="https://github.com/apache/doris-operator/issues/512" VERIFY_GUIDE_URL="https://doris.apache.org/community/release-and-verify/release-verify" DOWNLOAD_PAGE_URL="${GITHUB_TAG_URL}" +# Operator container image published for this release. +DOCKER_IMAGE="apache/doris:operator-${VERSION}" +DOCKER_IMAGE_URL="https://hub.docker.com/r/apache/doris/tags?name=operator-${VERSION}" + VOTE_TO="dev@doris.apache.org" ANNOUNCE_TO="announce@apache.org" diff --git a/tools/release-tools/tests/test-config.sh b/tools/release-tools/tests/test-config.sh index cf00b17f..a2aa5b2b 100755 --- a/tools/release-tools/tests/test-config.sh +++ b/tools/release-tools/tests/test-config.sh @@ -24,16 +24,20 @@ source "${TOOLS_ROOT}/release.env" # shellcheck source=../lib/release-common.sh source "${TOOLS_ROOT}/lib/release-common.sh" -assert_eq "26.0.0" "$VERSION" +[[ -n "$VERSION" ]] || fail "release.env does not set VERSION" +[[ -n "$GIT_REMOTE" ]] || fail "release.env does not set GIT_REMOTE" assert_eq "$VERSION" "$TAG" -assert_eq "upstream-apache" "$GIT_REMOTE" -assert_eq "apache-doris-operator-26.0.0-src" "$PKG_BASE" +assert_eq "apache-doris-operator-${VERSION}-src" "$PKG_BASE" assert_eq "${PKG_BASE}/" "$ARCHIVE_PREFIX" -assert_eq "https://dist.apache.org/repos/dist/dev/doris/doris-operator/26.0.0" "$DEV_SVN_DIR" -assert_eq "https://dist.apache.org/repos/dist/release/doris/doris-operator/26.0.0" "$RELEASE_SVN_DIR" +assert_eq "https://dist.apache.org/repos/dist/dev/doris/doris-operator/${VERSION}" "$DEV_SVN_DIR" +assert_eq "https://dist.apache.org/repos/dist/release/doris/doris-operator/${VERSION}" "$RELEASE_SVN_DIR" assert_eq "https://downloads.apache.org/doris/KEYS" "$KEYS_URL" assert_eq "https://dist.apache.org/repos/dist/dev/doris" "$DEV_KEYS_SVN_BASE" assert_eq "https://dist.apache.org/repos/dist/release/doris" "$RELEASE_KEYS_SVN_BASE" +assert_eq "apache/doris-operator" "$GITHUB_REPO" +assert_eq "https://github.com/${GITHUB_REPO}/releases/tag/${TAG}" "$GITHUB_TAG_URL" +assert_eq "apache/doris:operator-${VERSION}" "$DOCKER_IMAGE" +assert_eq "https://hub.docker.com/r/apache/doris/tags?name=operator-${VERSION}" "$DOCKER_IMAGE_URL" validate_release_config @@ -44,6 +48,16 @@ if (TAG="not-${VERSION}"; validate_release_config) 2>"$error_file"; then fi assert_file_contains "$error_file" "TAG must equal VERSION" +if (GITHUB_REPO="doris-operator"; validate_release_config) 2>"$error_file"; then + fail "configuration validation accepted a GITHUB_REPO without an owner" +fi +assert_file_contains "$error_file" "GITHUB_REPO must be /" + +if (DOCKER_IMAGE="apache/doris:operator-0.0.0"; validate_release_config) 2>"$error_file"; then + fail "configuration validation accepted a DOCKER_IMAGE from another version" +fi +assert_file_contains "$error_file" "DOCKER_IMAGE must be" + if grep -Eq '^[[:space:]]*ASF_(USERNAME|PASSWORD)=' "${TOOLS_ROOT}/release.env"; then fail "release.env stores SVN credentials" fi diff --git a/tools/release-tools/tests/test-mail.sh b/tools/release-tools/tests/test-mail.sh index 366828ec..c58799a0 100755 --- a/tools/release-tools/tests/test-mail.sh +++ b/tools/release-tools/tests/test-mail.sh @@ -45,10 +45,13 @@ APACHE_ID="release-manager" APACHE_EMAIL="release-manager@apache.org" SIGNER_NAME="Release Manager" SIGNING_KEY="0123456789ABCDEF0123456789ABCDEF01234567" +GITHUB_REPO="apache-test/doris-operator" GITHUB_TAG_URL="https://github.example.test/apache/doris-operator/releases/tag/\${TAG}" RELEASE_NOTES_URL="https://github.example.test/apache/doris-operator/releases/notes/\${TAG}" VERIFY_GUIDE_URL="https://doris.example.test/release-verify" DOWNLOAD_PAGE_URL="https://github.example.test/apache/doris-operator/releases/tag/\${TAG}" +DOCKER_IMAGE="apache/doris:operator-\${VERSION}" +DOCKER_IMAGE_URL="https://hub.example.test/r/apache/doris/tags?name=operator-\${VERSION}" VOTE_TO="dev@example.test" ANNOUNCE_TO="announce@example.test" WORK_DIR="${tmp}/work" @@ -74,6 +77,7 @@ vote_eml="${tmp}/work/vote-email.eml" assert_exists "$vote_body" assert_exists "$vote_eml" assert_file_contains "$vote_eml" "Subject: [VOTE] Release Apache Doris Operator 9.9.9" +assert_eq "Subject: [VOTE] Release Apache Doris Operator 9.9.9" "$(head -n 1 "$vote_body")" assert_file_contains "$vote_body" "https://github.example.test/apache/doris-operator/releases/tag/9.9.9" assert_file_contains "$vote_body" "https://dist.example.test/dev/doris/doris-operator/9.9.9/" assert_file_contains "$vote_body" "0123456789ABCDEF0123456789ABCDEF01234567" @@ -91,14 +95,22 @@ assert_exists "$announce_body" assert_exists "$announce_eml" assert_file_contains "$announce_eml" "To: announce@example.test" assert_file_contains "$announce_eml" "Subject: [ANNOUNCE] Apache Doris Operator 9.9.9 release" +assert_eq "Subject: [ANNOUNCE] Apache Doris Operator 9.9.9 release" "$(head -n 1 "$announce_body")" +assert_file_contains "${tmp}/announce-output" "Subject: [ANNOUNCE] Apache Doris Operator 9.9.9 release" assert_file_contains "$announce_body" "automates the deployment and management" assert_file_contains "$announce_body" "https://dist.example.test/release/doris/doris-operator/9.9.9/apache-doris-operator-9.9.9-src.tar.gz" assert_file_contains "$announce_body" "Thank you to everyone" -assert_file_contains "${tmp}/announce-output" "mail-only mode: skipping tag, package, signing, and SVN operations" +assert_file_contains "${tmp}/announce-output" "mail-only mode: skipping tag, package, signing, SVN, and GitHub operations" +assert_not_exists "${tmp}/work/github-release-notes.md" [[ ! -s "$COMMAND_LOG" ]] || fail "--mail-only invoked an external release command" if PATH="${tmp}/fake-bin:${PATH}" "${tool_copy}/04-release-complete.sh" --unknown >/dev/null 2>&1; then fail "04-release-complete.sh accepted an unknown argument" fi +if PATH="${tmp}/fake-bin:${PATH}" "${tool_copy}/04-release-complete.sh" \ + --mail-only --github-only >/dev/null 2>&1; then + fail "04-release-complete.sh accepted --mail-only together with --github-only" +fi + pass diff --git a/tools/release-tools/tests/test-syntax.sh b/tools/release-tools/tests/test-syntax.sh index 34de8c15..d1ef9ac6 100755 --- a/tools/release-tools/tests/test-syntax.sh +++ b/tools/release-tools/tests/test-syntax.sh @@ -37,6 +37,11 @@ for text in \ './03-vote-mail.sh' \ './04-release-complete.sh' \ '--mail-only' \ + '--github-only' \ + '--verify-tag' \ + 'The whole run is idempotent' \ + 'apache/doris:operator-' \ + 'Subject:' \ 'refuses to overwrite' \ 'does not inspect, compare, promote, move, or delete anything under dev SVN' \ 'prints the subject and body' \ diff --git a/tools/release-tools/tests/test-workflows.sh b/tools/release-tools/tests/test-workflows.sh index 34318f53..c9508604 100755 --- a/tools/release-tools/tests/test-workflows.sh +++ b/tools/release-tools/tests/test-workflows.sh @@ -58,10 +58,13 @@ APACHE_ID="release-manager" APACHE_EMAIL="release-manager@apache.org" SIGNER_NAME="Release Manager" SIGNING_KEY="0123456789ABCDEF0123456789ABCDEF01234567" +GITHUB_REPO="apache-test/doris-operator" GITHUB_TAG_URL="https://github.example.test/apache/doris-operator/releases/tag/\${TAG}" -RELEASE_NOTES_URL="https://github.example.test/apache/doris-operator/releases/notes/\${TAG}" +RELEASE_NOTES_URL="https://github.example.test/apache/doris-operator/issues/512" VERIFY_GUIDE_URL="https://doris.example.test/release-verify" DOWNLOAD_PAGE_URL="https://github.example.test/apache/doris-operator/releases/tag/\${TAG}" +DOCKER_IMAGE="apache/doris:operator-\${VERSION}" +DOCKER_IMAGE_URL="https://hub.example.test/r/apache/doris/tags?name=operator-\${VERSION}" VOTE_TO="dev@example.test" ANNOUNCE_TO="announce@example.test" WORK_DIR="${tmp}/work" @@ -69,8 +72,12 @@ EOF export FAKE_GPG_LOG="${tmp}/gpg.log" export FAKE_SVN_LOG="${tmp}/svn.log" +export FAKE_GH_LOG="${tmp}/gh.log" +export FAKE_GH_RELEASE="${tmp}/gh-release-body" : > "$FAKE_GPG_LOG" : > "$FAKE_SVN_LOG" +: > "$FAKE_GH_LOG" +rm -f "$FAKE_GH_RELEASE" cat > "${fake_bin}/gpg" <<'EOF' #!/usr/bin/env bash @@ -112,12 +119,48 @@ case "$command" in destination="${@: -1}" mkdir -p "$destination" ;; + ls) + [[ -n "${FAKE_SVN_LS_FILE:-}" && -f "${FAKE_SVN_LS_FILE:-}" ]] || exit 1 + cat "$FAKE_SVN_LS_FILE" + ;; add|status|commit) exit 0 ;; *) exit 1 ;; esac EOF chmod +x "${fake_bin}/svn" +cat > "${fake_bin}/gh" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +printf '%s\n' "$*" >> "$FAKE_GH_LOG" +all_args=" $* " +[[ "${1:-}" != "auth" ]] || exit 0 +[[ "${1:-}" == "release" ]] || exit 1 +action="${2:-}" +notes="" +while [[ "$#" -gt 0 ]]; do + [[ "$1" != "--notes-file" ]] || notes="$2" + shift +done +case "$action" in + view) + [[ -f "$FAKE_GH_RELEASE" ]] || exit 1 + [[ "$all_args" != *" --json "* ]] || cat "$FAKE_GH_RELEASE" + ;; + create) + [[ "$all_args" == *" --verify-tag "* ]] || exit 1 + [[ -n "$notes" ]] || exit 1 + cp "$notes" "$FAKE_GH_RELEASE" + ;; + edit) + [[ -n "$notes" ]] || exit 1 + cp "$notes" "$FAKE_GH_RELEASE" + ;; + *) exit 1 ;; +esac +EOF +chmod +x "${fake_bin}/gh" + test_path="${fake_bin}:${PATH}" unset FAKE_EXISTING_URL || true if ! printf 'y\ny\n' | PATH="$test_path" "${tool_copy}/02-package-sign-upload.sh" > "${tmp}/dev-output" 2>&1; then @@ -139,7 +182,7 @@ assert_eq "3" "$staged_dev_count" : > "$FAKE_SVN_LOG" : > "$FAKE_GPG_LOG" -if ! printf 'y\ny\n' | PATH="$test_path" "${tool_copy}/04-release-complete.sh" > "${tmp}/release-output" 2>&1; then +if ! printf 'y\ny\ny\n' | PATH="$test_path" "${tool_copy}/04-release-complete.sh" > "${tmp}/release-output" 2>&1; then cat "${tmp}/release-output" >&2 fail "formal release workflow failed" fi @@ -152,6 +195,62 @@ assert_eq "3" "$staged_release_count" assert_exists "${tmp}/work/announce-email.txt" assert_exists "${tmp}/work/announce-email.eml" +notes_file="${tmp}/work/github-release-notes.md" +assert_exists "$notes_file" +assert_file_contains "$notes_file" "https://github.example.test/apache/doris-operator/issues/512" +assert_file_contains "$notes_file" "docker pull apache/doris:operator-9.9.9" +assert_file_contains "$notes_file" "https://hub.example.test/r/apache/doris/tags?name=operator-9.9.9" +assert_file_contains "$notes_file" "https://dist.example.test/release/doris/doris-operator/9.9.9/apache-doris-operator-9.9.9-src.tar.gz" +assert_file_contains "$FAKE_GH_LOG" "release create 9.9.9 --repo apache-test/doris-operator --verify-tag" +assert_exists "$FAKE_GH_RELEASE" + +# A repeated run of a finished release must not repackage, re-upload, or +# rewrite the GitHub release. +: > "$FAKE_SVN_LOG" +: > "$FAKE_GPG_LOG" +: > "$FAKE_GH_LOG" +rm -f "${tmp}/work/announce-email.txt" "${tmp}/work/announce-email.eml" +release_listing="${tmp}/release-listing" +printf '%s\n' \ + 'apache-doris-operator-9.9.9-src.tar.gz' \ + 'apache-doris-operator-9.9.9-src.tar.gz.asc' \ + 'apache-doris-operator-9.9.9-src.tar.gz.sha512' > "$release_listing" +export FAKE_SVN_LS_FILE="$release_listing" +export FAKE_EXISTING_URL="https://dist.example.test/release/doris/doris-operator/9.9.9" +if ! printf 'y\ny\ny\n' | PATH="$test_path" "${tool_copy}/04-release-complete.sh" > "${tmp}/rerun-output" 2>&1; then + cat "${tmp}/rerun-output" >&2 + fail "repeated formal release run was not idempotent" +fi +assert_file_contains "${tmp}/rerun-output" "release files already published" +assert_file_contains "${tmp}/rerun-output" "is already published: https://github.example.test/apache/doris-operator/releases/tag/9.9.9" +assert_file_not_contains "$FAKE_SVN_LOG" "checkout" +assert_file_not_contains "$FAKE_SVN_LOG" "commit" +assert_file_not_contains "$FAKE_GH_LOG" "release create" +assert_file_not_contains "$FAKE_GH_LOG" "release edit" +[[ ! -s "$FAKE_GPG_LOG" ]] || fail "repeated formal release run signed the artifacts again" +assert_exists "${tmp}/work/announce-email.txt" +assert_exists "${tmp}/work/announce-email.eml" + +: > "$FAKE_SVN_LOG" +: > "$FAKE_GH_LOG" +if ! printf 'y\n' | PATH="$test_path" "${tool_copy}/04-release-complete.sh" --github-only > "${tmp}/github-only-output" 2>&1; then + cat "${tmp}/github-only-output" >&2 + fail "github-only mode failed" +fi +assert_file_contains "${tmp}/github-only-output" "github-only mode" +assert_file_contains "$FAKE_GH_LOG" "release view 9.9.9" +[[ ! -s "$FAKE_SVN_LOG" ]] || fail "github-only mode invoked svn" + +: > "$FAKE_SVN_LOG" +printf '%s\n' 'apache-doris-operator-9.9.9-src.tar.gz' > "$release_listing" +if printf 'y\ny\ny\n' | PATH="$test_path" "${tool_copy}/04-release-complete.sh" > "${tmp}/partial-output" 2>&1; then + fail "formal release workflow accepted an incomplete release directory" +fi +assert_file_contains "${tmp}/partial-output" "incomplete release directory" +assert_file_not_contains "$FAKE_SVN_LOG" "checkout" +assert_file_not_contains "$FAKE_SVN_LOG" "commit" +unset FAKE_SVN_LS_FILE + : > "$FAKE_SVN_LOG" export FAKE_EXISTING_URL="https://dist.example.test/dev/doris/doris-operator/9.9.9" if printf 'y\ny\n' | PATH="$test_path" "${tool_copy}/02-package-sign-upload.sh" >/dev/null 2>&1; then