This is an automated email from the ASF dual-hosted git repository.
mchades pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push:
new 1a28d6ff4c [#10478] feat(release): add gravitino-release skill and fix
state-dir path bug (#10479)
1a28d6ff4c is described below
commit 1a28d6ff4c7c44e0a34c965b2f5c305a092475ef
Author: Jerry Shao <[email protected]>
AuthorDate: Mon Apr 27 10:05:21 2026 +0800
[#10478] feat(release): add gravitino-release skill and fix state-dir path
bug (#10479)
### What changes were proposed in this pull request?
- Adds `dev/release/gravitino-release/SKILL.md`: a Claude Code skill
that acts as an interactive release manager, guiding through all 7
stages (tag, build, docs, publish, docker, finalize, release-note) with
state tracking, mock mode, dry-run support, preflight checks, and
credential collection.
- Adds `dev/release/mock/do-release.sh` and
`dev/release/mock/publish-docker.sh`: mock scripts that simulate all
stages without real operations, for safe end-to-end testing of the
skill.
- Fixes `STATE_DIR` path bug in `do-release.sh`, `mock/do-release.sh`,
and `publish-docker.sh`: the default `.release-state` was a relative
path, causing state files to be written to the caller's working
directory instead of alongside the scripts. Fixed to use
`$SELF/.release-state`.
- Fixes log file path bug in `mock/do-release.sh`: log files also used a
relative path; fixed to use `$SELF/${STEP}.log`.
### Why are the changes needed?
Fix: #10478
The release process is complex and multi-staged. Without structured
tooling, a release manager must remember which stages have run, which
credentials are needed, and how to recover from failures. The skill
automates this orchestration. The state-dir bug caused state files to be
scattered in unpredictable locations depending on where scripts were
invoked from, breaking resume-after-failure behavior.
### Does this PR introduce _any_ user-facing change?
No API or property key changes. The skill and mock scripts are new
developer tooling only.
### How was this patch tested?
Tested end-to-end using mock mode (`dev/release/mock/do-release.sh`)
with all 7 stages, including failure simulation via `MOCK_FAIL_STAGE`,
dry-run mode, and resume-from-partial-state scenarios.
---------
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
---
agent-skills/gravitino-release/README.md | 100 +++++
agent-skills/gravitino-release/SKILL.md | 626 +++++++++++++++++++++++++++++++
dev/release/do-release.sh | 100 ++++-
dev/release/mock/do-release.sh | 240 ++++++++++++
dev/release/mock/publish-docker.sh | 191 ++++++++++
dev/release/publish-docker.sh | 39 ++
6 files changed, 1279 insertions(+), 17 deletions(-)
diff --git a/agent-skills/gravitino-release/README.md
b/agent-skills/gravitino-release/README.md
new file mode 100644
index 0000000000..8c5590628e
--- /dev/null
+++ b/agent-skills/gravitino-release/README.md
@@ -0,0 +1,100 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+# gravitino-release skill
+
+A Claude Code skill that guides you through the full Apache Gravitino release
pipeline, stage by stage.
+
+## What it does
+
+The skill orchestrates the 8-stage release process:
+
+| # | Stage | Description |
+|---|-------|-------------|
+| 1 | **tag** | Creates git tag and bumps version |
+| 2 | **build** | Builds and uploads artifacts to ASF SVN staging |
+| 3 | **docs** | Builds Javadoc and Python Sphinx docs |
+| 4 | **publish** | Publishes Maven artifacts to Apache Nexus |
+| 5 | **docker** | Publishes RC Docker images via GitHub Actions |
+| 6 | **finalize** | Promotes artifacts to release (irreversible) |
+| 7 | **docker-final** | Publishes final Docker images |
+| 8 | **release-note** | Generates a Markdown release note draft |
+
+Long-running stages (build, docs, publish, finalize) run in the background so
you can keep chatting while they complete.
+
+## Installation
+
+Copy the skill directory to Claude Code's skills folder:
+
+```bash
+# Global — available in all projects
+cp -r agent-skills/gravitino-release ~/.claude/skills/
+
+# Project-local — available only inside this repo
+cp -r agent-skills/gravitino-release .claude/skills/
+```
+
+## Prerequisites
+
+- **`gh` CLI** installed and authenticated (`gh auth login`)
+- Apache committer credentials (`ASF_USERNAME`, `ASF_PASSWORD`)
+- GPG key set up for signing (`GPG_KEY`, `GPG_PASSPHRASE`)
+- PyPI API token (`PYPI_API_TOKEN`) for Python package publishing
+- Docker Hub credentials and GitHub token for Docker stages
+
+The skill will prompt you for any missing credentials before each stage runs.
+
+## Usage
+
+Invoke the skill from Claude Code:
+
+```
+/gravitino-release branch-1.2
+```
+
+At startup the skill will:
+1. Verify `gh` is installed and authenticated
+2. Ask for the release branch and RC number, then confirm the derived version
and tag
+3. Sparse-clone `dev/release/` from the release branch (no full local clone
needed)
+4. Show the current release state (which stages are done)
+5. Ask what you want to do next
+
+### Mock mode
+
+Test the full pipeline without making any real changes:
+
+```
+/gravitino-release mock
+```
+
+Mock scripts validate all credentials and simulate each stage, writing real
`.done` state files so you can test resume, retry, and failure scenarios.
+
+Set `MOCK_STAGE_DELAY=<seconds>` to simulate long-running stages (e.g. `export
MOCK_STAGE_DELAY=180`).
+
+### Checking status mid-run
+
+While a long stage is running in the background, just ask:
+
+> "How's the build going?"
+
+Claude will tail the log and report back. You can also use `/loop` to poll
automatically:
+
+```
+/loop 10m check stage build status
+```
+
+The release scripts themselves live in `dev/release/` inside a fresh clone of
`apache/gravitino` — they are downloaded automatically when you start the skill.
diff --git a/agent-skills/gravitino-release/SKILL.md
b/agent-skills/gravitino-release/SKILL.md
new file mode 100644
index 0000000000..d9fd67c12c
--- /dev/null
+++ b/agent-skills/gravitino-release/SKILL.md
@@ -0,0 +1,626 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+---
+name: gravitino-release
+description: Apache Gravitino release manager — guides through the full staged
release pipeline stage by stage
+argument-hint: "[branch] (e.g. branch-1.2 | mock | status)"
+allowed-tools: Bash
+---
+
+# Apache Gravitino Release Manager
+
+You are the Apache Gravitino release manager agent. Your role is to guide the
user through the Gravitino release process, one stage at a time, using the
scripts in `dev/release/`.
+
+## Scripts Reference
+
+| Script | Purpose |
+|--------|---------|
+| `do-release.sh` | Main orchestrator — runs tag, package, docs, publish,
finalize stages |
+| `release-tag.sh` | Creates git tag and bumps version (invoked by
do-release.sh) |
+| `release-build.sh` | Builds, publishes, and finalizes artifacts (invoked by
do-release.sh) |
+| `publish-docker.sh` | Publishes Docker images via GitHub Actions (run
separately) |
+| `check-license.sh` | Verifies all files referenced in LICENSE exist |
+| `release-util.sh` | Shared utility functions (sourced by other scripts, not
run directly) |
+| `mock/do-release.sh` | Mock version of do-release.sh for testing (no real
operations) |
+| `mock/publish-docker.sh` | Mock version of publish-docker.sh for testing (no
real operations) |
+
+## Release Stages (in order)
+
+| # | Stage | `-s` flag | State file |
+|---|-------|-----------|------------|
+| 1 | **tag** | `tag` | `.release-state/{TAG}/tag.done` |
+| 2 | **build** | `build` | `.release-state/{TAG}/build.done` |
+| 3 | **docs** | `docs` | `.release-state/{TAG}/docs.done` |
+| 4 | **publish** | `publish` | `.release-state/{TAG}/publish.done` |
+| 5 | **docker** | _(separate script)_ | `.release-state/{TAG}/docker.done` |
+| 6 | **finalize** | `finalize` | `.release-state/{TAG}/finalize.done` |
+| 7 | **docker-final** | _(separate script)_ |
`.release-state/{TAG}/docker-final.done` |
+| 8 | **release-note** | _(agent-generated)_ | _(no state file — always
re-runnable)_ |
+
+> `{TAG}` is the release candidate tag, e.g. `v1.2.0-rc1`.
+
+## Directory Layout
+
+After setup, the working directory structure is always:
+
+```
+~/gravitino-release-v{VERSION}-rc{RC}/ ← WORK_DIR (sparse clone of
apache/gravitino)
+└── dev/
+ └── release/ ← hardcoded path, never changes
in the repo
+ ├── do-release.sh
+ ├── publish-docker.sh
+ ├── release-build.sh
+ ├── release-tag.sh
+ ├── release-util.sh
+ ├── check-license.sh
+ ├── mock/
+ │ ├── do-release.sh
+ │ └── publish-docker.sh
+ └── .release-state/
+ └── v{VERSION}-rc{RC}/
+ ├── tag.done
+ ├── package.done
+ └── ...
+```
+
+```bash
+WORK_DIR="$HOME/gravitino-release-v${RELEASE_VERSION}-rc${RC_COUNT}"
+RELEASE_SCRIPTS_DIR="$WORK_DIR/dev/release"
+```
+
+`dev/release/` is a fixed path in the Gravitino repository and never changes.
All script invocations use the full path, e.g.
`"$RELEASE_SCRIPTS_DIR/do-release.sh"`.
+
+## State Tracking
+
+State lives in `$RELEASE_SCRIPTS_DIR/.release-state/{RELEASE_TAG}/`.
+
+- **Before a stage**: the script checks for the `.done` file. If found, it
prints the completion info and exits — no re-run.
+- **After success**: the script writes the `.done` file with a timestamp and
verification details.
+- **To re-run a stage**: the user must manually delete the `.done` file.
+- Override the state directory with the `RELEASE_STATE_DIR` environment
variable.
+
+---
+
+## Session Start Protocol
+
+### Step 0 — Check prerequisites
+
+Verify `gh` is installed:
+```bash
+gh --version
+```
+If not found:
+> "`gh` (GitHub CLI) is required. Install it from https://cli.github.com, then
run `gh auth login`."
+> Stop here.
+
+Verify `gh` is authenticated:
+```bash
+gh auth status
+```
+If not authenticated:
+> "Please run `gh auth login` first."
+> Stop here.
+
+---
+
+### Step 1 — Collect release parameters
+
+Ask the user for:
+- **Branch**: e.g. `branch-1.2`
+- **RC number**: e.g. `1`
+
+Auto-detect the release version from the branch:
+```bash
+gh api "repos/apache/gravitino/contents/gradle.properties?ref=${GIT_BRANCH}" \
+ --jq '.content' | base64 -d | grep '^version' | cut -d'=' -f2 | tr -d ' '
+```
+
+Confirm with the user:
+```
+Branch: branch-1.2
+Release version: 1.2.0
+RC number: 1
+Release tag: v1.2.0-rc1
+
+Is this correct? [y/N]
+```
+
+---
+
+### Step 2 — Set up working directory
+
+Derive paths from the confirmed release parameters:
+```bash
+WORK_DIR="$HOME/gravitino-release-v${RELEASE_VERSION}-rc${RC_COUNT}"
+RELEASE_SCRIPTS_DIR="$WORK_DIR/dev/release"
+```
+
+Check if `$WORK_DIR` already exists:
+```bash
+ls "$WORK_DIR" 2>/dev/null
+```
+
+If it exists, ask:
+> "Found existing scripts at `$WORK_DIR`. Use this or re-download?
[use/download]"
+
+If it does not exist (or user chose re-download), sparse-clone just
`dev/release/`:
+```bash
+gh repo clone apache/gravitino "$WORK_DIR" -- \
+ --depth 1 \
+ --branch "${GIT_BRANCH}" \
+ --filter=blob:none \
+ --sparse
+
+cd "$WORK_DIR"
+git sparse-checkout set dev/release
+git checkout
+```
+
+---
+
+### Step 3 — Select mode
+
+Ask before reading state, because mock mode uses a different scripts directory
and therefore a different state directory:
+
+```
+Mock mode? [y/N]
+Dry run mode? [y/N]
+```
+
+If the user selects mock mode, set `RELEASE_SCRIPTS_DIR` to the mock
subdirectory **now**, before any state check:
+```bash
+RELEASE_SCRIPTS_DIR="$WORK_DIR/dev/release/mock"
+```
+
+---
+
+### Step 4 — Read current state
+
+Using the `RELEASE_SCRIPTS_DIR` set in Step 3, check
`.release-state/v${RELEASE_VERSION}-rc${RC_COUNT}/` for `.done` files and show
the user:
+
+```
+Release state for v1.2.0-rc1:
+ ✓ tag (completed 2026-03-19T10:00:00Z)
+ ✓ build (completed 2026-03-19T12:30:00Z)
+ ○ docs (pending)
+ ○ publish (pending)
+ ○ docker (pending)
+ ○ finalize (pending)
+ ○ docker-final (pending)
+ ↻ release-note (always re-runnable — no state file)
+```
+
+`release-note` has no `.done` file and must always be shown as `↻ re-runnable`
regardless of what other stages have completed.
+
+If no state directory exists: "No stages completed yet for v{VERSION}-rc{RC}."
+
+---
+
+### Step 5 — Ask what to do
+
+```
+What would you like to do?
+ 1. Run next pending stage
+ 2. Run a specific stage
+ 3. Run all remaining stages sequentially
+ 4. Show current state only
+```
+
+---
+
+### Step 6 — Collect secrets
+
+Check each required environment variable before running any stage. If unset,
ask the user for the value and `export` it. **Never display, log, or echo
secret values.**
+
+| Variable | Required for stages | Notes |
+|----------|---------------------|-------|
+| `ASF_USERNAME` | tag, package, publish, finalize | Apache committer username
|
+| `ASF_PASSWORD` | tag, package, publish, finalize | Apache committer password
|
+| `GPG_KEY` | package, publish | GPG key ID (typically `[email protected]`) |
+| `GPG_PASSPHRASE` | package, publish | GPG key passphrase |
+| `PYPI_API_TOKEN` | tag, package, finalize | PyPI API token (starts with
`pypi-`) |
+| `GH_TOKEN` | docker, docker-final | GitHub token with `repo`+`workflow`
scope |
+| `DOCKER_USERNAME` | docker, docker-final | Docker Hub username |
+| `PUBLISH_DOCKER_TOKEN` | docker, docker-final | Workflow authorization token
(matched against repo secret) |
+
+Collect only what is needed for the stage(s) about to run. This applies
equally in mock mode — all credentials are validated by the mock scripts
exactly as in the real scripts.
+
+---
+
+## Preflight Checks
+
+Run these checks **before the tag stage only**. Present results clearly, then
ask whether to proceed. All checks are **advisory** — never block automatically.
+
+### 1. Open PRs targeting the release branch
+
+```bash
+gh pr list \
+ --repo apache/gravitino \
+ --base "${GIT_BRANCH}" \
+ --state open \
+ --json number,title,author,url \
+ --jq '.[] | "#\(.number) \(.title) by \(.author.login) \(.url)"'
+```
+
+If any are found:
+> "Found N open PR(s) targeting {GIT_BRANCH}. Any commits merged after tagging
will miss this release."
+> Show the list, then ask: "Proceed anyway? [y/N]"
+
+### 2. Recent CI status on the release branch
+
+```bash
+gh run list \
+ --repo apache/gravitino \
+ --branch "${GIT_BRANCH}" \
+ --limit 5 \
+ --json status,conclusion,name,url \
+ --jq '.[] | "\(.conclusion // .status) \(.name) \(.url)"'
+```
+
+If any recent runs show `failure` or `cancelled`:
+> "Recent CI run(s) on {GIT_BRANCH} did not pass. A green branch is
recommended before tagging."
+> Ask: "Proceed anyway? [y/N]"
+
+### Preflight summary
+
+```
+Preflight results for v1.2.0-rc1 (branch-1.2):
+ Open PRs (base branch-1.2): 1 ⚠
+ Recent CI (branch-1.2): ✓ (last 5 runs passed)
+
+Proceed to tag? [y/N]
+```
+
+---
+
+## Stage Details
+
+### Execution model for long-running stages
+
+Stages **build**, **docs**, **publish**, and **finalize** typically take 30–90
minutes. Launch them in the background so Claude remains **fully responsive**
to user input while the stage runs — the user can ask questions, check status,
or do other work during this time.
+
+#### Launching
+
+```bash
+# Launch in background — stdout+stderr go to the stage log, PID saved to a file
+nohup "$RELEASE_SCRIPTS_DIR/do-release.sh" -s <stage> -y -b "$GIT_BRANCH" -r
"$RC_COUNT" \
+ > "$RELEASE_SCRIPTS_DIR/<stage>.log" 2>&1 &
+echo $! > "$RELEASE_SCRIPTS_DIR/<stage>.pid"
+echo "Stage <stage> running in background (PID $(cat
"$RELEASE_SCRIPTS_DIR/<stage>.pid")). You can keep chatting."
+```
+
+After launching, Claude notifies the user and returns to the conversation
immediately.
+
+#### Checking status (on demand)
+
+Claude runs this whenever the user asks (e.g. "how's the build going?") or
proactively after a natural pause:
+
+```bash
+STAGE_PID=$(cat "$RELEASE_SCRIPTS_DIR/<stage>.pid" 2>/dev/null)
+if kill -0 "$STAGE_PID" 2>/dev/null; then
+ echo "Still running. Recent log:"
+ tail -10 "$RELEASE_SCRIPTS_DIR/<stage>.log"
+elif [ -f "$STATE_DIR/<stage>.done" ]; then
+ echo "Completed successfully:"
+ cat "$STATE_DIR/<stage>.done"
+else
+ echo "Process exited but no .done file — FAILED. Last log:"
+ tail -30 "$RELEASE_SCRIPTS_DIR/<stage>.log"
+fi
+```
+
+#### Auto-polling (optional)
+
+Periodically ask the agent to check status (e.g. "any update on the build?") —
it will run the status check above and report back. In **Claude Code** you can
also use the built-in `/loop` skill to automate this:
+```
+/loop 10m check stage <stage> status
+```
+Use `1m` for mock mode (with `MOCK_STAGE_DELAY=180`), `10m` for real runs.
`/loop` is Claude Code only and not available in other tools.
+
+**Mock mode tip:** Set `MOCK_STAGE_DELAY=<secs>` to simulate a long-running
stage:
+```bash
+export MOCK_STAGE_DELAY=180 # stage sleeps 3 minutes before completing
+```
+
+**Short stages** (tag, docker, docker-final) complete in under 5 minutes and
are run synchronously — no backgrounding needed.
+
+---
+
+### Stage 1: tag
+
+**Command:**
+```bash
+"$RELEASE_SCRIPTS_DIR/do-release.sh" -s tag -y -b "$GIT_BRANCH" -r "$RC_COUNT"
+```
+
+**Required env vars:** `ASF_USERNAME`, `ASF_PASSWORD`, `GPG_KEY`,
`GPG_PASSPHRASE`, `PYPI_API_TOKEN`
+
+**What it does:**
+- Clones the repo from gitbox.apache.org
+- Updates version in: `gradle.properties`, `clients/client-python/setup.py`,
`clients/filesystem-fuse/Cargo.toml`, all three Helm charts (`Chart.yaml` +
`values.yaml`), `mcp-server/pyproject.toml`
+- Commits and creates git tag `v{VERSION}-rc{RC}`
+- Bumps all files to next SNAPSHOT version, commits and pushes both
+
+**Log:** `$RELEASE_SCRIPTS_DIR/tag.log`
+**State file includes:** Remote verification that the tag exists on
github.com/apache/gravitino
+
+**Dry run:** Keeps a local `gravitino-tag/` clone for inspection instead of
pushing.
+
+---
+
+### Stage 2: build
+
+**Command:**
+```bash
+"$RELEASE_SCRIPTS_DIR/do-release.sh" -s build -y -b "$GIT_BRANCH" -r
"$RC_COUNT"
+```
+
+**Required env vars:** `ASF_USERNAME`, `ASF_PASSWORD`, `GPG_KEY`,
`GPG_PASSPHRASE`, `PYPI_API_TOKEN`
+
+**What it does:**
+- Clones from the release tag
+- Builds source tarball (`gravitino-{VERSION}-src.tar.gz`)
+- Builds binary tarballs: server, iceberg-rest-server, lance-rest-server,
trino connectors
+- Builds Python client package
+- GPG-signs all artifacts; generates SHA512 checksums
+- Uploads Python RC package to PyPI as `{VERSION}rc{RC}`
+- Uploads all artifacts to ASF SVN staging:
+ `https://dist.apache.org/repos/dist/dev/gravitino/v{VERSION}-rc{RC}/`
+
+**Log:** `$RELEASE_SCRIPTS_DIR/build.log`
+
+---
+
+### Stage 3: docs
+
+**Command:**
+```bash
+"$RELEASE_SCRIPTS_DIR/do-release.sh" -s docs -y -b "$GIT_BRANCH" -r "$RC_COUNT"
+```
+
+**Required env vars:** _(none — docs build is fully local)_
+
+**What it does:**
+- Builds Javadoc from `clients/client-java` → `gravitino-{VERSION}-javadoc/`
+- Builds Python Sphinx docs from `clients/client-python` →
`gravitino-{VERSION}-pydoc/`
+- Output is local only; nothing is published in this stage
+
+**Log:** `$RELEASE_SCRIPTS_DIR/docs.log`
+
+---
+
+### Stage 4: publish (Maven/Nexus)
+
+**Command:**
+```bash
+"$RELEASE_SCRIPTS_DIR/do-release.sh" -s publish -y -b "$GIT_BRANCH" -r
"$RC_COUNT"
+```
+
+**Required env vars:** `ASF_USERNAME`, `ASF_PASSWORD`, `GPG_KEY`,
`GPG_PASSPHRASE`
+
+**What it does:**
+- Creates an Apache Nexus staging repository
+- Builds Maven artifacts for Scala 2.12 and Scala 2.13
+- Signs each artifact and uploads to Nexus
+- Closes the staging repository (making it available for the vote)
+
+**Log:** `$RELEASE_SCRIPTS_DIR/publish.log`
+
+---
+
+### Stage 5: docker
+
+**Command:**
+```bash
+"$RELEASE_SCRIPTS_DIR/publish-docker.sh" "v${RELEASE_VERSION}-rc${RC_COUNT}" \
+ --docker-version "${RELEASE_VERSION}-rc${RC_COUNT}" \
+ --trino-version "${TRINO_VERSION:-478}"
+```
+
+**Required env vars:** `GH_TOKEN` (or active `gh auth login`),
`DOCKER_USERNAME`, `PUBLISH_DOCKER_TOKEN`
+
+**What it does:** Triggers GitHub Actions `docker-image.yml` workflow for each
image:
+- `apache/gravitino:{VERSION}-rc{RC}`
+- `apache/gravitino-iceberg-rest:{VERSION}-rc{RC}`
+- `apache/gravitino-lance-rest:{VERSION}-rc{RC}`
+- `apache/gravitino-mcp-server:{VERSION}-rc{RC}`
+- `apache/gravitino-playground:trino-{TRINO_VER}-gravitino-{VERSION}-rc{RC}`
+
+**Default Trino version:** `478` — ask the user if they want a different
version before running.
+
+**Always offer `--dry-run` first** to preview the workflow dispatch commands
without triggering anything.
+
+**Monitor progress:**
https://github.com/apache/gravitino/actions/workflows/docker-image.yml
+
+---
+
+### Stage 6: finalize
+
+> ⚠️ **Irreversible.** Always get explicit user confirmation before running.
+
+**Confirmation prompt:**
+```
+finalize will permanently:
+ - Create git tag v{VERSION} (no rc suffix)
+ - Upload final Python package to PyPI (without rc suffix)
+ - Move SVN artifacts: dev/gravitino/v{VERSION}-rc{RC} →
release/gravitino/{VERSION}
+ - Update the public KEYS file
+
+This cannot be undone. Type 'yes' to proceed:
+```
+
+**Command:**
+```bash
+"$RELEASE_SCRIPTS_DIR/do-release.sh" -s finalize -y -b "$GIT_BRANCH" -r
"$RC_COUNT"
+```
+
+**Required env vars:** `ASF_USERNAME`, `ASF_PASSWORD`, `GPG_KEY`,
`GPG_PASSPHRASE`, `PYPI_API_TOKEN`
+
+**Log:** `$RELEASE_SCRIPTS_DIR/finalize.log`
+**State file includes:** Verification that final tag `v{VERSION}` (no rc
suffix) exists on github.com
+
+---
+
+### Stage 7: docker-final
+
+**Command:**
+```bash
+RELEASE_STATE_DIR="$RELEASE_SCRIPTS_DIR/.release-state/$RELEASE_TAG" \
+"$RELEASE_SCRIPTS_DIR/publish-docker.sh" "v${RELEASE_VERSION}" \
+ --docker-version "${RELEASE_VERSION}" \
+ --trino-version "${TRINO_VERSION:-478}" \
+ --state-key "docker-final"
+```
+
+**Required env vars:** `GH_TOKEN` (or active `gh auth login`),
`DOCKER_USERNAME`, `PUBLISH_DOCKER_TOKEN`
+
+**Optional env vars:** `TRINO_VERSION` — Trino version for the playground
image (default: `478`). Ask the user before running if they want a different
version.
+
+**What it does:** Triggers GitHub Actions `docker-image.yml` for each final
(non-RC) image:
+- `apache/gravitino:{VERSION}`
+- `apache/gravitino-iceberg-rest:{VERSION}`
+- `apache/gravitino-lance-rest:{VERSION}`
+- `apache/gravitino-mcp-server:{VERSION}`
+- `apache/gravitino-playground:trino-{TRINO_VER}-gravitino-{VERSION}`
+
+The `--state-key docker-final` flag causes `publish-docker.sh` to write
`docker-final.done` (distinct from the RC `docker.done` written in stage 5).
`RELEASE_STATE_DIR` is set explicitly so both state files land in the same
`.release-state/{RC_TAG}/` directory.
+
+**Always offer `--dry-run` first** before triggering any real workflow
dispatches.
+
+**Monitor progress:**
https://github.com/apache/gravitino/actions/workflows/docker-image.yml
+
+---
+
+### Stage 8: release-note
+
+**Required env vars:** _(none — read-only GitHub API calls via `gh`)_
+
+**What it does:**
+1. Fetches all issues labelled with the release version (label format:
`{VERSION}`, e.g. `1.2.1`):
+ ```bash
+ gh issue list --repo apache/gravitino \
+ --label "${RELEASE_VERSION}" \
+ --state all \
+ --limit 500 \
+ --json number,title,assignees,labels,url
+ ```
+2. Asks the user to describe 3–5 key features/themes for the **Highlights**
section before generating the draft.
+3. Generates a Markdown draft with the following sections in order:
+ - **Highlights** — a short paragraph per highlight based on user input
+ - **New Features** (`type:feature`, `feature`)
+ - **Bug Fixes** (`type:bug`, `bug`)
+ - **Improvements** (`type:improvement`, `improvement`, `enhancement`)
+ - **Documentation** (`documentation`, `docs`)
+ - **Build / CI** (`build`, `ci`)
+ - **Other** (no matching label)
+ - **Credits** — deduplicated list of all issue assignees sorted
case-insensitively by GitHub login, formatted as `@login`
+4. Displays the draft for review, then saves to:
+ `$RELEASE_SCRIPTS_DIR/gravitino-{VERSION}-release-notes.md`
+
+---
+
+## Error Handling
+
+When a script fails:
+1. Read the corresponding `.log` file and show the user the last 30 lines
+2. Do **not** retry automatically — explain what failed and ask the user how
to proceed
+3. The `.done` file will not have been written (only written on success)
+4. After the user fixes the issue, they can re-run the same stage
+
+| Symptom | Likely cause | Action |
+|---------|-------------|--------|
+| `svn: E170013` | Wrong ASF password or expired auth | Re-export
`ASF_PASSWORD` and retry |
+| `gpg: signing failed` | Wrong passphrase or key not in keyring | Check `gpg
--list-secret-keys` |
+| `twine upload` 403 | Expired `PYPI_API_TOKEN` | Regenerate token on pypi.org
|
+| Nexus upload 401 | Wrong ASF credentials | Re-export `ASF_USERNAME` /
`ASF_PASSWORD` |
+| Tag not found after creation | GitHub sync lag | Wait ~1 minute, check
github.com manually |
+| `docker.done` not written | Bad `GH_TOKEN` or `PUBLISH_DOCKER_TOKEN` | Check
`gh auth status` and token scopes |
+
+---
+
+## Testing
+
+To test the skill without any real build, publish, or git operations, use the
mock scripts in `dev/release/mock/`.
+
+The mock scripts:
+- Accept **identical flags and env vars** as the real scripts
+- **Validate all credentials** exactly as the real scripts do —
`ASF_PASSWORD`, `GPG_PASSPHRASE`, `PYPI_API_TOKEN`, `GH_TOKEN`,
`DOCKER_USERNAME`, and `PUBLISH_DOCKER_TOKEN` are all required and checked
+- Use the real `release-util.sh` for state management — guard checks and
`.done` files behave exactly as in production
+- Print what each stage *would* do instead of executing it
+- Support `MOCK_FAIL_STAGE=<stage>` to simulate a failure at any stage and
test error handling
+
+### Invoking mock mode
+
+When the user requests mock mode at Step 4 of the Session Start Protocol, set
`RELEASE_SCRIPTS_DIR` to the `mock/` subdirectory:
+
+```bash
+RELEASE_SCRIPTS_DIR="$WORK_DIR/dev/release/mock"
+```
+
+All stage commands then become e.g. `"$RELEASE_SCRIPTS_DIR/do-release.sh" -s
tag -y ...`
+Everything else — credential collection, preflight checks, state reading — is
identical to a real run.
+
+### Test scenarios
+
+| Scenario | How to trigger |
+|----------|---------------|
+| Full happy path (all 6 stages) | Run all stages in mock mode; verify all
`.done` files are written |
+| Resume from partial state | Pre-create some `.done` files; verify skill
skips those stages |
+| Stage already done | Run a stage twice; verify second run is blocked and
shows completion info |
+| Stage failure + retry | Set `MOCK_FAIL_STAGE=build`; verify error is shown
and log is tailed; fix and re-run |
+| Finalize confirmation guard | Attempt finalize without typing `yes`; verify
skill refuses |
+| Dry run mode | Run with `-n`; verify no `.done` files are written |
+| Missing secret | Unset `ASF_PASSWORD`; verify skill prompts before running |
+| Preflight checks | `gh` calls run against the real repo (read-only — safe in
mock mode) |
+
+---
+
+## Rules
+
+1. **Never display or log secrets** — mask passwords, passphrases, and tokens
in all output.
+2. **Always use `-y` mode** — `do-release.sh` requires interactive input
without it; an agent cannot respond to prompts.
+3. **Show state at the start of every session** — the user must always know
what has been done.
+4. **Guard finalize with explicit confirmation** — it affects public
infrastructure and is irreversible.
+5. **Respect `.done` files** — never delete or bypass them without the user's
explicit instruction.
+6. **Always offer `--dry-run`** before any publishing step the user hasn't run
before in this session.
+7. **Run preflight before tag** — always run the two preflight checks before
the tag stage starts.
+
+---
+
+## Installation
+
+Claude Code, GitHub Copilot CLI, and OpenClaw all manage skills as directories
containing a `SKILL.md` file.
+The skill is packaged as `agent-skills/gravitino-release/` in the Gravitino
repo — copy the whole directory to install.
+
+> Claude Code and GitHub Copilot CLI share the same `.claude/` directory.
+
+```bash
+# Claude Code / GitHub Copilot CLI — global (available in all projects)
+cp -r agent-skills/gravitino-release ~/.claude/skills/
+
+# Claude Code / GitHub Copilot CLI — project-local (available only inside this
repo)
+cp -r agent-skills/gravitino-release .claude/skills/
+
+# OpenClaw — global
+cp -r agent-skills/gravitino-release ~/.openclaw/workspace/skills/
+
+# OpenClaw — project-local
+cp -r agent-skills/gravitino-release .openclaw/workspace/skills/
+```
+
+Invoke with `/gravitino-release`. No local clone of the Gravitino repository
is needed — the skill downloads the release scripts automatically via `gh`.
diff --git a/dev/release/do-release.sh b/dev/release/do-release.sh
index 963513e71a..f75b214a69 100755
--- a/dev/release/do-release.sh
+++ b/dev/release/do-release.sh
@@ -139,44 +139,110 @@ function should_build {
[ -z "$RELEASE_STEP" ] || [ "$WHAT" = "$RELEASE_STEP" ]
}
-if should_build "tag" && [ $SKIP_TAG = 0 ]; then
- run_silent "Creating release tag $RELEASE_TAG..." "tag.log" \
- "$SELF/release-tag.sh"
- echo "It may take some time for the tag to be synchronized to github."
- if is_force; then
- echo "Force mode: skipping wait."
+# ---------------------------------------------------------------------------
+# Stage state tracking
+# Each completed stage writes a marker file to STATE_DIR so that re-runs can
+# detect what has already been done and skip those stages automatically.
+# Override the directory with RELEASE_STATE_DIR env var if needed.
+# ---------------------------------------------------------------------------
+STATE_DIR="${RELEASE_STATE_DIR:-$SELF/.release-state}/${RELEASE_TAG}"
+mkdir -p "$STATE_DIR"
+
+function stage_file { echo "$STATE_DIR/$1.done"; }
+
+function is_stage_done { [ -f "$(stage_file "$1")" ]; }
+
+function mark_stage_done {
+ local STEP="$1" INFO="${2:-}"
+ {
+ echo "completed_at=$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
+ echo "release=$RELEASE_TAG"
+ [ -n "$INFO" ] && echo "info=$INFO"
+ } > "$(stage_file "$STEP")"
+ echo "Stage '$STEP' marked as done. State file: $(stage_file "$STEP")"
+}
+
+# Returns 0 (proceed) if stage is NOT done; returns 1 (skip) and prints a
+# message if the stage is already complete.
+function check_stage_guard {
+ local STEP="$1"
+ if is_stage_done "$STEP"; then
+ echo ""
+ echo "=== Stage '$STEP' is already complete ==="
+ cat "$(stage_file "$STEP")"
+ echo "To re-run this stage, delete: $(stage_file "$STEP")"
+ echo ""
+ return 1
+ fi
+ return 0
+}
+
+if should_build "tag"; then
+ if [ $SKIP_TAG = 1 ]; then
+ echo "Tag $RELEASE_TAG already exists on remote. Skipping tag creation."
+ is_stage_done "tag" || mark_stage_done "tag" "Tag $RELEASE_TAG already
existed on remote"
else
- echo "Press enter when you've verified that the new tag ($RELEASE_TAG) is
available."
- read
+ check_stage_guard "tag" && {
+ run_silent "Creating release tag $RELEASE_TAG..." "tag.log" \
+ "$SELF/release-tag.sh"
+ if is_force; then
+ echo "Force mode: skipping wait for tag sync."
+ else
+ echo "It may take some time for the tag to be synchronized to github."
+ echo "Press enter when you've verified that the new tag ($RELEASE_TAG)
is available."
+ read
+ fi
+ if check_for_tag "$RELEASE_TAG"; then
+ mark_stage_done "tag" "Tag $RELEASE_TAG verified on
github.com/apache/gravitino"
+ else
+ echo "WARNING: Tag $RELEASE_TAG not found on remote after creation.
Stage not marked as done."
+ fi
+ }
fi
else
- echo "Skipping tag creation for $RELEASE_TAG."
+ echo "Skipping tag stage."
fi
if should_build "build"; then
- run_silent "Building Gravitino..." "build.log" \
- "$SELF/release-build.sh" package
+ check_stage_guard "build" && {
+ run_silent "Building Gravitino..." "build.log" \
+ "$SELF/release-build.sh" package
+ mark_stage_done "build" "Artifacts built and uploaded to ASF SVN staging"
+ }
else
echo "Skipping build step."
fi
if should_build "docs"; then
- run_silent "Building documentation..." "docs.log" \
- "$SELF/release-build.sh" docs
+ check_stage_guard "docs" && {
+ run_silent "Building documentation..." "docs.log" \
+ "$SELF/release-build.sh" docs
+ mark_stage_done "docs" "Javadoc and Python docs built locally"
+ }
else
echo "Skipping docs step."
fi
if should_build "publish"; then
- run_silent "Publishing release" "publish.log" \
- "$SELF/release-build.sh" publish-release
+ check_stage_guard "publish" && {
+ run_silent "Publishing release" "publish.log" \
+ "$SELF/release-build.sh" publish-release
+ mark_stage_done "publish" "Maven artifacts uploaded to Apache Nexus
staging"
+ }
else
echo "Skipping publish step."
fi
if [ "$RELEASE_STEP" = "finalize" ]; then
- run_silent "Finalizing release" "finalize.log" \
- "$SELF/release-build.sh" finalize
+ check_stage_guard "finalize" && {
+ run_silent "Finalizing release" "finalize.log" \
+ "$SELF/release-build.sh" finalize
+ if check_for_tag "v$RELEASE_VERSION"; then
+ mark_stage_done "finalize" "Release v$RELEASE_VERSION promoted: PyPI
uploaded, SVN moved to release, KEYS updated"
+ else
+ mark_stage_done "finalize" "Finalize script completed (verify
v$RELEASE_VERSION tag manually)"
+ fi
+ }
fi
echo "Release build and publish completed"
diff --git a/dev/release/mock/do-release.sh b/dev/release/mock/do-release.sh
new file mode 100755
index 0000000000..fc86014156
--- /dev/null
+++ b/dev/release/mock/do-release.sh
@@ -0,0 +1,240 @@
+#!/usr/bin/env bash
+
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# Mock version of do-release.sh for testing the gravitino-release skill.
+#
+# Accepts identical flags and env vars as the real do-release.sh but replaces
+# all build/publish/git operations with printed simulations. State tracking
+# (mark_stage_done / check_stage_guard) runs for real so the skill's
+# orchestration logic is fully exercised.
+#
+# Extra env vars for testing:
+# MOCK_FAIL_STAGE=<stage> Simulate a failure at the given stage name.
+# Valid values: tag, build, docs, publish, finalize
+# MOCK_STAGE_DELAY=<secs> Seconds to sleep per stage (default: 1).
+
+set -euo pipefail
+
+SELF=$(cd "$(dirname "$0")" && pwd)
+REAL_SCRIPTS_DIR="$SELF/.."
+
+# ---------------------------------------------------------------------------
+# Parse identical flags as the real do-release.sh
+# ---------------------------------------------------------------------------
+while getopts ":b:s:p:t:r:nyh" opt; do
+ case $opt in
+ b) GIT_BRANCH=$OPTARG ;;
+ n) DRY_RUN=1 ;;
+ s) RELEASE_STEP=$OPTARG ;;
+ p) GPG_PASSPHRASE=$OPTARG ;;
+ t) ASF_PASSWORD=$OPTARG ;;
+ r) RC_COUNT=$OPTARG ;;
+ y) FORCE=1 ;;
+ h)
+ echo "Usage: $0 [options] (MOCK — no real operations run)"
+ echo ""
+ echo "Accepts identical flags and env vars as do-release.sh."
+ echo "All credentials are validated exactly as in the real script."
+ echo ""
+ echo "Extra env vars for testing:"
+ echo " MOCK_FAIL_STAGE=<stage> Simulate failure at: tag, build, docs,
publish, finalize"
+ echo " MOCK_STAGE_DELAY=<secs> Sleep duration per stage (default: 1)"
+ exit 0
+ ;;
+ :) echo "Option -$OPTARG requires an argument." >&2; exit 1 ;;
+ \?) echo "Invalid option: -$OPTARG" >&2; exit 1 ;;
+ esac
+done
+
+export RUNNING_IN_DOCKER=${RUNNING_IN_DOCKER:-0}
+export DRY_RUN=${DRY_RUN:-0}
+export FORCE=${FORCE:-0}
+export RC_COUNT=${RC_COUNT:-0}
+export RELEASE_STEP=${RELEASE_STEP:-}
+export GIT_BRANCH=${GIT_BRANCH:-}
+export RELEASE_VERSION=${RELEASE_VERSION:-}
+export RELEASE_TAG=${RELEASE_TAG:-}
+export ASF_PASSWORD=${ASF_PASSWORD:-}
+export GPG_PASSPHRASE=${GPG_PASSPHRASE:-}
+
+if [[ "${RC_COUNT}" != "0" ]] && ! [[ "${RC_COUNT}" =~ ^[1-9][0-9]*$ ]]; then
+ echo "Error: RC number must be a positive integer, got: '${RC_COUNT}'" >&2
+ exit 1
+fi
+
+if [ -n "${RELEASE_STEP}" ]; then
+ case "${RELEASE_STEP}" in
+ tag|build|docs|publish|finalize) ;;
+ *) echo "Error: invalid release step '${RELEASE_STEP}'. Valid steps: tag,
build, docs, publish, finalize" >&2; exit 1 ;;
+ esac
+fi
+
+# Source real release-util.sh for get_release_info, is_dry_run, is_force, etc.
+. "$REAL_SCRIPTS_DIR/release-util.sh"
+
+# Validate PYPI_API_TOKEN — same check as the real script.
+if ! is_dry_run; then
+ if [[ -z "${PYPI_API_TOKEN:-}" ]]; then
+ echo 'The environment variable PYPI_API_TOKEN is not set. Exiting.'
+ exit 1
+ fi
+fi
+
+# Collect release parameters the same way as the real script.
+# In -y (force) mode this reads from env vars with no prompts.
+# Credentials (ASF_PASSWORD, GPG_PASSPHRASE) are collected inside
get_release_info.
+SKIP_TAG=0
+get_release_info
+
+function should_build {
+ local WHAT=$1
+ [ -z "$RELEASE_STEP" ] || [ "$WHAT" = "$RELEASE_STEP" ]
+}
+
+# ---------------------------------------------------------------------------
+# State tracking (identical to real do-release.sh)
+# ---------------------------------------------------------------------------
+STATE_DIR="${RELEASE_STATE_DIR:-$SELF/.release-state}/${RELEASE_TAG}"
+mkdir -p "$STATE_DIR"
+
+function stage_file { echo "$STATE_DIR/$1.done"; }
+
+function is_stage_done { [ -f "$(stage_file "$1")" ]; }
+
+function mark_stage_done {
+ local STEP="$1" INFO="${2:-}"
+ {
+ echo "completed_at=$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
+ echo "release=$RELEASE_TAG"
+ echo "mode=MOCK"
+ [ -n "$INFO" ] && echo "info=$INFO"
+ } > "$(stage_file "$STEP")"
+ echo "Stage '$STEP' marked as done (MOCK). State file: $(stage_file "$STEP")"
+}
+
+function check_stage_guard {
+ local STEP="$1"
+ if is_stage_done "$STEP"; then
+ echo ""
+ echo "=== Stage '$STEP' is already complete ==="
+ cat "$(stage_file "$STEP")"
+ echo "To re-run this stage, delete: $(stage_file "$STEP")"
+ echo ""
+ return 1
+ fi
+ return 0
+}
+
+# ---------------------------------------------------------------------------
+# Mock stage runner
+# Prints what the real stage would do, sleeps briefly, then either
+# writes a .done file (success) or exits non-zero (if MOCK_FAIL_STAGE matches).
+# ---------------------------------------------------------------------------
+function mock_stage {
+ local STEP="$1"
+ local LOG_FILE="$SELF/${STEP}.log"
+
+ echo "========================"
+ echo "= [MOCK] Stage: $STEP"
+ echo "= Release: $RELEASE_TAG Branch: $GIT_BRANCH DryRun: $DRY_RUN"
+
+ case "$STEP" in
+ tag)
+ echo "= Would: clone repo, update versions, commit, create tag
$RELEASE_TAG, bump to next SNAPSHOT, push"
+ ;;
+ build)
+ echo "= Would: clone from $RELEASE_TAG, build source+binary tarballs,
sign, checksum,"
+ echo "= upload Python RC to PyPI, upload artifacts to ASF SVN
staging"
+ ;;
+ docs)
+ echo "= Would: build Javadoc and Python Sphinx docs locally"
+ ;;
+ publish)
+ echo "= Would: create Nexus staging repo, build Maven artifacts (Scala
2.12 + 2.13),"
+ echo "= sign, upload to Nexus, close staging repo"
+ ;;
+ finalize)
+ echo "= Would: create tag v$RELEASE_VERSION, upload final Python package
to PyPI,"
+ echo "= move SVN dev → release, update KEYS"
+ ;;
+ esac
+
+ sleep "${MOCK_STAGE_DELAY:-1}"
+
+ if [ "${MOCK_FAIL_STAGE:-}" = "$STEP" ]; then
+ echo "[MOCK] Simulated failure injected for stage '$STEP'" | tee
"$LOG_FILE" >&2
+ echo "Error: MOCK_FAIL_STAGE=$STEP triggered a non-zero exit." >>
"$LOG_FILE"
+ exit 1
+ fi
+
+ echo "[MOCK] Stage '$STEP' completed successfully." | tee "$LOG_FILE"
+}
+
+# ---------------------------------------------------------------------------
+# Stage execution (mirrors real do-release.sh structure exactly)
+# ---------------------------------------------------------------------------
+if should_build "tag"; then
+ if [ $SKIP_TAG = 1 ]; then
+ echo "Tag $RELEASE_TAG already exists on remote. Skipping tag creation."
+ is_stage_done "tag" || mark_stage_done "tag" "MOCK: tag $RELEASE_TAG
already existed on remote"
+ else
+ check_stage_guard "tag" && {
+ mock_stage "tag"
+ mark_stage_done "tag" "MOCK: tag $RELEASE_TAG simulated (not pushed to
remote)"
+ }
+ fi
+else
+ echo "Skipping tag stage."
+fi
+
+if should_build "build"; then
+ check_stage_guard "build" && {
+ mock_stage "build"
+ mark_stage_done "build" "MOCK: artifacts build and SVN staging upload
simulated"
+ }
+else
+ echo "Skipping build step."
+fi
+
+if should_build "docs"; then
+ check_stage_guard "docs" && {
+ mock_stage "docs"
+ mark_stage_done "docs" "MOCK: Javadoc and Python docs build simulated"
+ }
+else
+ echo "Skipping docs step."
+fi
+
+if should_build "publish"; then
+ check_stage_guard "publish" && {
+ mock_stage "publish"
+ mark_stage_done "publish" "MOCK: Maven artifacts upload to Nexus simulated"
+ }
+else
+ echo "Skipping publish step."
+fi
+
+if [ "$RELEASE_STEP" = "finalize" ]; then
+ check_stage_guard "finalize" && {
+ mock_stage "finalize"
+ mark_stage_done "finalize" "MOCK: PyPI upload, SVN promotion, KEYS update
simulated"
+ }
+fi
+
+echo "Mock release completed."
diff --git a/dev/release/mock/publish-docker.sh
b/dev/release/mock/publish-docker.sh
new file mode 100755
index 0000000000..c51a49e0e2
--- /dev/null
+++ b/dev/release/mock/publish-docker.sh
@@ -0,0 +1,191 @@
+#!/usr/bin/env bash
+
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# Mock version of publish-docker.sh for testing the gravitino-release skill.
+#
+# Accepts identical arguments and env vars as the real publish-docker.sh but
+# prints simulated workflow dispatch commands instead of triggering them.
+# State tracking runs for real so the skill's guard logic is exercised.
+#
+# Extra env vars for testing:
+# MOCK_FAIL_STAGE=docker Simulate a failure during docker stage.
+
+set -e
+
+SELF=$(cd "$(dirname "$0")" && pwd)
+
+# Parse arguments — identical to real publish-docker.sh
+DRY_RUN=false
+INPUT_TAG=""
+DOCKER_VERSION=""
+TRINO_VER="478"
+STATE_KEY="docker"
+
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --dry-run)
+ DRY_RUN=true
+ shift
+ ;;
+ --state-key)
+ if [[ -z "${2:-}" || "$2" == -* ]]; then
+ echo "ERROR: --state-key requires a non-empty value." >&2
+ exit 1
+ fi
+ STATE_KEY="$2"
+ shift 2
+ ;;
+ --docker-version)
+ if [[ -z "${2:-}" || "$2" == -* ]]; then
+ echo "ERROR: --docker-version requires a non-empty value." >&2
+ exit 1
+ fi
+ DOCKER_VERSION="$2"
+ shift 2
+ ;;
+ --trino-version)
+ if [[ -z "${2:-}" || "$2" == -* ]]; then
+ echo "ERROR: --trino-version requires a non-empty value." >&2
+ exit 1
+ fi
+ TRINO_VER="$2"
+ shift 2
+ ;;
+ -h|--help)
+ cat << 'EOF'
+Usage: publish-docker.sh <tag|branch> --docker-version <version>
[--trino-version <version>] [--dry-run]
+(MOCK VERSION — no workflows are actually triggered)
+
+Accepts identical arguments as the real publish-docker.sh.
+
+Extra env vars for testing:
+ MOCK_FAIL_STAGE=docker Simulate a failure during the docker stage.
+EOF
+ exit 0
+ ;;
+ *)
+ if [[ -z "$INPUT_TAG" ]]; then
+ INPUT_TAG="$1"
+ else
+ echo "ERROR: Unknown argument: $1" >&2
+ exit 1
+ fi
+ shift
+ ;;
+ esac
+done
+
+if [[ -z "$INPUT_TAG" ]]; then
+ echo "ERROR: Missing tag/branch argument" >&2
+ exit 1
+fi
+
+if [[ -z "$DOCKER_VERSION" ]]; then
+ echo "ERROR: Missing --docker-version argument" >&2
+ exit 1
+fi
+
+# ---------------------------------------------------------------------------
+# State tracking — identical convention as real publish-docker.sh
+# ---------------------------------------------------------------------------
+DOCKER_STATE_DIR="${RELEASE_STATE_DIR:-$SELF/.release-state}/${INPUT_TAG}"
+DOCKER_STATE_FILE="${DOCKER_STATE_DIR}/${STATE_KEY}.done"
+
+if [[ "$DRY_RUN" == "false" ]] && [[ -f "$DOCKER_STATE_FILE" ]]; then
+ echo ""
+ echo "=== Stage '${STATE_KEY}' is already complete ==="
+ cat "$DOCKER_STATE_FILE"
+ echo "To re-run, delete: $DOCKER_STATE_FILE"
+ echo ""
+ exit 0
+fi
+
+TRINO_VERSION="${TRINO_VER}-gravitino-${DOCKER_VERSION}"
+
+echo "[MOCK] Skipping remote tag/branch verification for: $INPUT_TAG"
+
+# Validate credentials — same checks as the real script.
+if [[ "$DRY_RUN" == "false" ]]; then
+ if [[ -z "${GH_TOKEN:-}" ]] && ! gh auth status > /dev/null 2>&1; then
+ echo "ERROR: GH_TOKEN is not set and gh auth is not configured."
+ echo "Either export GH_TOKEN or run: gh auth login"
+ exit 1
+ fi
+ if [[ -z "${DOCKER_USERNAME:-}" ]]; then
+ echo "ERROR: DOCKER_USERNAME environment variable not set"
+ exit 1
+ fi
+ if [[ -z "${PUBLISH_DOCKER_TOKEN:-}" ]]; then
+ echo "ERROR: PUBLISH_DOCKER_TOKEN environment variable not set"
+ exit 1
+ fi
+ echo "Username: ${DOCKER_USERNAME}"
+fi
+echo ""
+
+if [[ "$DRY_RUN" == "true" ]]; then
+ echo "=== [MOCK DRY RUN] Preview Gravitino Docker Image Build ==="
+else
+ echo "=== [MOCK] Simulating Gravitino Docker Image Build ==="
+fi
+echo "Input: ${INPUT_TAG}"
+echo "Docker Version: ${DOCKER_VERSION}"
+echo "Trino Version: ${TRINO_VERSION}"
+echo ""
+
+declare -a images=(
+ "gravitino"
+ "gravitino-iceberg-rest-server"
+ "gravitino-lance-rest-server"
+ "gravitino-mcp-server"
+)
+
+echo "=== [MOCK] Simulated Workflow Dispatches ==="
+
+for img in "${images[@]}"; do
+ echo "[MOCK] gh workflow run docker-image.yml -R apache/gravitino --ref
${INPUT_TAG} -f image=${img} -f version=${DOCKER_VERSION}"
+ sleep 0.2
+done
+
+echo "[MOCK] gh workflow run docker-image.yml -R apache/gravitino --ref
${INPUT_TAG} -f image=gravitino-playground:trino -f version=${TRINO_VERSION}"
+
+echo ""
+
+if [[ "$DRY_RUN" == "true" ]]; then
+ echo "=== [MOCK DRY RUN] Preview Complete ==="
+else
+ if [ "${MOCK_FAIL_STAGE:-}" = "docker" ]; then
+ echo "[MOCK] Simulated failure injected for stage 'docker'" >&2
+ exit 1
+ fi
+ echo "=== [MOCK] All workflows simulated ==="
+fi
+
+# Mark stage as done regardless of dry-run so the skill can track completion.
+mkdir -p "$DOCKER_STATE_DIR"
+{
+ echo "completed_at=$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
+ echo "release=$INPUT_TAG"
+ echo "docker_version=$DOCKER_VERSION"
+ echo "trino_version=$TRINO_VERSION"
+ echo "mode=MOCK"
+ [[ "$DRY_RUN" == "true" ]] && echo "dry_run=true"
+ echo "info=MOCK: all Docker image workflow dispatches simulated"
+} > "$DOCKER_STATE_FILE"
+echo "Stage '${STATE_KEY}' marked as done (MOCK). State file:
$DOCKER_STATE_FILE"
diff --git a/dev/release/publish-docker.sh b/dev/release/publish-docker.sh
index b607bd5fd2..48250f2eca 100755
--- a/dev/release/publish-docker.sh
+++ b/dev/release/publish-docker.sh
@@ -54,6 +54,8 @@
set -e
+SELF=$(cd "$(dirname "$0")" && pwd)
+
# Check required commands
for cmd in git gh; do
if ! command -v "$cmd" > /dev/null 2>&1; then
@@ -67,6 +69,7 @@ DRY_RUN=false
INPUT_TAG=""
DOCKER_VERSION=""
TRINO_VER="478"
+STATE_KEY="docker"
while [[ $# -gt 0 ]]; do
case "$1" in
@@ -74,6 +77,14 @@ while [[ $# -gt 0 ]]; do
DRY_RUN=true
shift
;;
+ --state-key)
+ if [[ -z "${2:-}" || "$2" == -* ]]; then
+ echo "ERROR: --state-key requires a non-empty value." >&2
+ exit 1
+ fi
+ STATE_KEY="$2"
+ shift 2
+ ;;
--docker-version)
if [[ -z "${2:-}" || "$2" == -* ]]; then
echo "ERROR: --docker-version requires a non-empty value." >&2
@@ -153,6 +164,22 @@ fi
echo "Verified: $INPUT_TAG exists"
+# ---------------------------------------------------------------------------
+# Stage state tracking: reuse the same .release-state/{TAG}/ convention as
+# do-release.sh so all stage markers live together.
+# ---------------------------------------------------------------------------
+DOCKER_STATE_DIR="${RELEASE_STATE_DIR:-$SELF/.release-state}/${INPUT_TAG}"
+DOCKER_STATE_FILE="${DOCKER_STATE_DIR}/${STATE_KEY}.done"
+
+if [[ "$DRY_RUN" == "false" ]] && [[ -f "$DOCKER_STATE_FILE" ]]; then
+ echo ""
+ echo "=== Stage '${STATE_KEY}' is already complete ==="
+ cat "$DOCKER_STATE_FILE"
+ echo "To re-run, delete: $DOCKER_STATE_FILE"
+ echo ""
+ exit 0
+fi
+
# Trino special version
TRINO_VERSION="${TRINO_VER}-gravitino-${DOCKER_VERSION}"
@@ -233,3 +260,15 @@ else
echo "=== All workflows triggered ==="
echo "View progress:
https://github.com/apache/gravitino/actions/workflows/docker-image.yml"
fi
+
+# Mark stage as done regardless of dry-run so the skill can track completion.
+mkdir -p "$DOCKER_STATE_DIR"
+{
+ echo "completed_at=$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
+ echo "release=$INPUT_TAG"
+ echo "docker_version=$DOCKER_VERSION"
+ echo "trino_version=$TRINO_VERSION"
+ [[ "$DRY_RUN" == "true" ]] && echo "dry_run=true"
+ echo "info=All Docker image workflows dispatched to apache/gravitino"
+} > "$DOCKER_STATE_FILE"
+echo "Stage '${STATE_KEY}' marked as done. State file: $DOCKER_STATE_FILE"