Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion tools/release-tools/03-vote-mail.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
168 changes: 152 additions & 16 deletions tools/release-tools/04-release-complete.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand All @@ -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" <<EOF
Apache Doris Operator ${VERSION} is released.

## Release note

${RELEASE_NOTES_URL}

## Docker image

\`${DOCKER_IMAGE}\`

\`\`\`shell
docker pull ${DOCKER_IMAGE}
\`\`\`

${DOCKER_IMAGE_URL}

## Source download

The official Apache source release of ${VERSION}:

- ${RELEASE_SVN_DIR}/${PKG_BASE}.tar.gz
- ${RELEASE_SVN_DIR}/${PKG_BASE}.tar.gz.asc
- ${RELEASE_SVN_DIR}/${PKG_BASE}.tar.gz.sha512

KEYS: ${KEYS_URL}

How to verify: ${VERIFY_GUIDE_URL}
EOF

printf '%s\n' "$notes_file"
}

github_release_exists() {
gh release view "$TAG" --repo "$GITHUB_REPO" >/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
55 changes: 44 additions & 11 deletions tools/release-tools/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,25 @@ 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:

- `VERSION`, `TAG`, `GIT_REMOTE`, and all derived artifact/SVN paths.
- `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
Expand Down Expand Up @@ -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
Expand All @@ -129,25 +134,53 @@ https://dist.apache.org/repos/dist/release/doris/doris-operator/<version>/

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:

```bash
./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.
Expand Down
33 changes: 32 additions & 1 deletion tools/release-tools/lib/release-common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 <owner>/<repo>"
[[ "$DOCKER_IMAGE" == *:*"${VERSION}" ]] ||
die "release.env: DOCKER_IMAGE must be a <image>:<tag> ending with ${VERSION}"
fi
}

Expand Down Expand Up @@ -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
Expand Down
Loading
Loading