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
276 changes: 276 additions & 0 deletions .github/workflows/preview-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
name: preview release

# Publishes a preview as GitHub Release assets, one zip per platform, because `org.chdb` is
# not on Maven Central yet and a native package is too large to commit.
#
# Same four runners, same build-native.sh and same integration tests as release.yml; only the
# last step differs. The version lives in git here too: no `versions:set` in CI, so the POMs
# at the tagged commit already carry the tag's version. The first attempt at v1.0.0-preview.1
# broke that rule and shipped a side branch 67 commits behind main, which is what every check
# in `preflight` is for. It was withdrawn, so the tag name is in use again here.
on:
push:
tags: ["v*-preview.*"]
workflow_dispatch:
inputs:
tag:
description: An existing preview tag to build and publish
required: true
type: string

concurrency:
group: preview-release-${{ inputs.tag || github.ref_name }}
cancel-in-progress: false

permissions:
contents: read
actions: read

env:
MAVEN_ARGS: "--batch-mode --no-transfer-progress"

jobs:
preflight:
name: preflight
runs-on: ubuntu-latest
outputs:
tag: ${{ steps.resolve.outputs.tag }}
version: ${{ steps.resolve.outputs.version }}
sha: ${{ steps.resolve.outputs.sha }}
steps:
- uses: actions/checkout@v4
with:
# On a dispatch `github.sha` is the branch, not what gets built.
ref: ${{ inputs.tag || github.ref }}

- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "11"
cache: maven

- name: Resolve the tag and check it against the POMs
id: resolve
env:
TAG: ${{ inputs.tag || github.ref_name }}
run: |
set -euo pipefail

case "$TAG" in
v*-preview.*) ;;
*)
echo "::error::expected a tag like v1.0.0-preview.1, got $TAG"
exit 1
;;
esac
TAG_VERSION="${TAG#v}"

# From Maven, so an inherited or property-substituted version reads correctly.
VERSION=$(mvn $MAVEN_ARGS -q -DforceStdout help:evaluate -Dexpression=project.version)
if [ "$TAG_VERSION" != "$VERSION" ]; then
echo "::error::tag $TAG implies version $TAG_VERSION but the POMs say $VERSION. Commit the preview version, then tag that commit."
exit 1
fi
case "$VERSION" in
*-SNAPSHOT)
echo "::error::the POMs are at $VERSION. A published preview needs a non-SNAPSHOT version committed and tagged."
exit 1
;;
esac

SHA=$(git rev-parse HEAD)
echo "tag $TAG, version $VERSION, commit $SHA"
{
echo "tag=$TAG"
echo "version=$VERSION"
echo "sha=$SHA"
} >> "$GITHUB_OUTPUT"

- name: The tag must point at the commit being built
# release.yml's script, for the same guarantee: what is published is what a commit says.
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
scripts/check-release-tag.sh \
"${{ github.repository }}" \
"${{ steps.resolve.outputs.version }}" \
"${{ steps.resolve.outputs.sha }}" \
| tee -a "$GITHUB_STEP_SUMMARY"

- name: The tagged commit must be on main
# Green on a branch says the tree works, not that it is the tree main has.
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
if ! gh api "repos/${{ github.repository }}/compare/main...${{ steps.resolve.outputs.sha }}" \
--jq '.status' | grep -qx 'identical\|behind'; then
echo "::error::${{ steps.resolve.outputs.sha }} is not an ancestor of main. Merge the release commit to main, then tag it there."
exit 1
fi

- name: The `build` workflow must have passed for this commit
# `build` filters on branches, so a tag push does not run it.
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
CONCLUSION=$(gh api \
"repos/${{ github.repository }}/actions/runs?head_sha=${{ steps.resolve.outputs.sha }}&per_page=100" \
--jq '[.workflow_runs[] | select(.name == "build")] | first | .conclusion // "none"')
echo "build workflow for ${{ steps.resolve.outputs.sha }}: $CONCLUSION"
if [ "$CONCLUSION" != "success" ]; then
echo "::error::the build workflow for this commit concluded '$CONCLUSION'. Let the matrix go green on main, then tag that commit."
exit 1
fi

- name: The engine version this preview carries
run: |
set -euo pipefail
ENGINE=$(sed -n 's/^engine.version=//p' scripts/engine.properties | head -1)
echo "engine $ENGINE, pinned by SHA-256 in scripts/engine.properties" >> "$GITHUB_STEP_SUMMARY"

# One job per platform: build-native.sh refuses to cross-build.
bundle:
name: bundle ${{ matrix.platform }}
needs: preflight
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- platform: linux-x86_64-gnu
runner: ubuntu-22.04
- platform: linux-aarch64-gnu
runner: ubuntu-22.04-arm
- platform: macos-aarch64
runner: macos-15
# macos-15-intel, not macos-15: the plain label is Apple Silicon.
- platform: macos-x86_64
runner: macos-15-intel
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.preflight.outputs.sha }}

- uses: actions/setup-java@v4
with:
# The floor, deliberately: it is what makes maven.compiler.release=11 a fact.
distribution: temurin
java-version: "11"
cache: maven

- name: Cache the pinned engine download
uses: actions/cache@v4
with:
path: target/engine/download
key: chdb-engine-${{ matrix.platform }}-${{ hashFiles('scripts/engine.properties') }}

- name: Compile the Java side and run its unit tests
run: mvn $MAVEN_ARGS -pl chdb-jdbc -am test

- name: Fetch the engine, build the shim, stage the platform package
run: |
case "${{ matrix.platform }}" in
linux-*) scripts/build-native-in-container.sh ${{ matrix.platform }} ;;
*) scripts/build-native.sh ${{ matrix.platform }} ;;
esac

- name: Integration tests against the staged package
# The bytes about to be zipped are new bytes, whatever `build` concluded for the commit.
run: |
set -eu
# stdin closed: chDB reads a non-TTY stdin with bytes on it as external data for an
# INSERT. See the same step in build.yml.
exec </dev/null
case "${{ matrix.platform }}" in
macos-*) OS=macos ;;
*) OS=linux ;;
esac
case "${{ matrix.platform }}" in
*aarch64*) ARCH=aarch64 ;;
*) ARCH=x86_64 ;;
esac
LIBS="$PWD/chdb-native-${{ matrix.platform }}/target/native/META-INF/chdb/native/$OS/$ARCH"
mvn $MAVEN_ARGS -pl chdb-integration-tests -am verify \
-Dchdb.it.platform=${{ matrix.platform }} \
-Dchdb.it.library.path="$LIBS"

- name: Package the JARs and the preview bundle
run: |
set -euo pipefail
mvn $MAVEN_ARGS -pl chdb-jdbc,chdb-native-${{ matrix.platform }} package -DskipTests
scripts/package-preview.sh "${{ needs.preflight.outputs.version }}" \
'${{ matrix.platform }}' dist

- name: Consume the bundle from a project outside this checkout
# Resolution, not just execution: the POMs are what install-file can break.
run: |
scripts/verify-preview-bundle.sh \
"dist/chdb-java-${{ needs.preflight.outputs.version }}-${{ matrix.platform }}.zip"

- name: Upload the platform bundle
uses: actions/upload-artifact@v4
with:
name: preview-${{ matrix.platform }}
path: dist/*.zip
if-no-files-found: error
retention-days: 14

publish:
name: publish
needs: [preflight, bundle]
runs-on: ubuntu-latest
permissions:
contents: write
actions: read
steps:
# Every gh call below passes --repo, but `gh release create` with neither that nor a
# checkout is how the first preview publish failed: `fatal: not a git repository`.
- uses: actions/checkout@v4
with:
ref: ${{ needs.preflight.outputs.sha }}
fetch-depth: 0

- name: Download every platform bundle
uses: actions/download-artifact@v4
with:
pattern: preview-*
path: release-assets
merge-multiple: true

- name: Checksum what is about to be uploaded
working-directory: release-assets
run: |
set -euo pipefail
test "$(ls -1 -- *.zip | wc -l)" -eq 4 || { echo "::error::expected four platform bundles"; exit 1; }
sha256sum -- *.zip | tee SHA256SUMS >> "$GITHUB_STEP_SUMMARY"

- name: Publish the release
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ needs.preflight.outputs.tag }}
EXPECTED_SHA: ${{ needs.preflight.outputs.sha }}
run: |
set -euo pipefail
Comment thread
macroscopeapp[bot] marked this conversation as resolved.

# Re-resolved here, not just in preflight: staging takes tens of minutes and a tag
# can be moved or deleted inside that window, which would upload these bundles under
# a tag naming a different commit. --verify-tag only checks that the tag exists.
REMOTE_SHA=$(gh api "repos/${{ github.repository }}/commits/$TAG" --jq '.sha')
if [ "$REMOTE_SHA" != "$EXPECTED_SHA" ]; then
echo "::error::$TAG now points at $REMOTE_SHA, not the $EXPECTED_SHA these bundles were built from"
exit 1
fi

# --prerelease so a preview never becomes "Latest release"; --draft=false because an
# existing draft would otherwise take the uploads and stay invisible.
if gh release view "$TAG" --repo "${{ github.repository }}" >/dev/null 2>&1; then
gh release edit "$TAG" --repo "${{ github.repository }}" --prerelease --draft=false
else
gh release create "$TAG" --repo "${{ github.repository }}" \
--title "chdb-java $TAG" --prerelease --verify-tag --generate-notes
fi
gh release upload "$TAG" --repo "${{ github.repository }}" \
release-assets/*.zip release-assets/SHA256SUMS --clobber
7 changes: 6 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,12 @@ on:
# A release is a tag on a commit whose POMs already carry the release version. `build` does
# not run on tags (it filters on branches), which is why preflight checks that `build`
# succeeded for this exact commit rather than assuming a tag implies a tested tree.
tags: ["v*"]
tags:
- "v*"
# Never a preview: `v1.0.0-preview.1` matches `v*` and preflight would accept it, so
# the Central path would stage, sign and offer a preview in the portal, where a release
# cannot be unpublished. preview-release.yml publishes those.
- "!v*-preview.*"
workflow_dispatch:
inputs:
channel:
Expand Down
54 changes: 26 additions & 28 deletions CHDB_JAVA_V1_WORK_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,36 +201,34 @@ META-INF/sbom/

### 4.3 Versioning rules

The scheme is "full engine version plus binding revision":

```text
<engine-version>.<binding-revision>
```

- `engine-version` keeps the chDB Core release version verbatim, `rc` qualifier included.
- `binding-revision` is the trailing positive integer covering Java, JNI, loader and platform packaging revisions. It restarts at `1` for every new engine version.
- This is an engine-aligned scheme. Do not read it as Java SemVer.

Examples:

| Case | Maven version | Meaning |
|---|---|---|
| First binding against stable engine 26.7.0 | `26.7.0.1` | engine=`26.7.0`, binding revision=`1` |
| Java/JNI/loader fix only, same stable engine | `26.7.0.2` | engine unchanged, binding revision incremented |
| First binding against engine 26.7.2-rc.2 | `26.7.2-rc.2.1` | engine=`26.7.2-rc.2`, binding revision=`1` |
| Re-release on the same rc.2 after an addon change | `26.7.2-rc.2.2` | engine unchanged, binding revision incremented |
| Engine moves to rc.3 | `26.7.2-rc.3.1` | new engine, binding revision back to `1` |
| Engine reaches stable 26.7.2 | `26.7.2.1` | first binding release on that stable engine |
The binding is versioned on its own, in SemVer — `MAJOR.MINOR.PATCH`, first release `1.0.0`.
The number describes the Java API, which is the question a consumer asks it.

The engine version is not in it. It is recorded in each native package's
`manifest.properties`, pinned with its SHA-256 in `scripts/engine.properties`, and named in
the release notes; the ABI check refuses any other engine build, so the pairing is enforced
rather than spelled.

This replaces `<engine-version>.<binding-revision>`, which read the wrong way round in both
directions: `26.7.2-rc.2.1` → `26.7.3.1` looked major and was not the binding's doing, while
a break in the Java API could ship as a trailing `.2`.

| Case | Version |
|---|---|
| First release | `1.0.0` |
| Fix in the driver, loader or JNI shim | `1.0.1` |
| New engine baseline, no Java API change | `1.1.0` |
| Breaking change to the Java API | `2.0.0` |
| Preview of `1.0.0` | `1.0.0-preview.1` |

Release rules:

- A release artifact in a Maven repository is immutable. Never overwrite `26.7.2-rc.2.1`. Any addon, JNI, Java, POM, loader, checksum or single-platform fix ships as `.2`.
- Within one binding release, `chdb-jdbc`, the four platform packages and `chdb-bom` carry exactly the same version. They ship together even when some platform content did not change, so the BOM and the platform packages never end up on mixed versions.
- Moving the engine from one RC to another, or from an RC to a stable release, counts as a new engine version, and the binding revision restarts at `1`.
- A Java artifact built on an engine RC is itself a preview and cannot be the engine dependency of V1 GA. V1 GA has to bind a stable chDB Core release.
- Development builds may use `26.7.2-rc.2.2-SNAPSHOT`, but `SNAPSHOT` never enters a Maven Central release.
- If the Java binding on a stable engine needs its own release candidates, use `26.7.0.1-rc.1`, `26.7.0.1-rc.2`, with `26.7.0.1` as the final GA. A candidate and the GA never reuse the same immutable artifact.
- Any engine change produces a new Maven version and a full platform test run.
- A published artifact is immutable. Never overwrite `1.0.0`; any fix ships as `1.0.1`.
- `chdb-jdbc`, the four platform packages and `chdb-bom` always carry the same version.
- An engine change is a `MINOR` bump when it changes what the driver can do and a `PATCH` when it does not. It is never invisible: the manifest and the release notes name the engine.
- A binding built on an engine RC is a preview and cannot be V1 GA.
- `-SNAPSHOT` is for development and never reaches Maven Central.
- **`-preview.<n>` sorts *above* the release it previews.** Maven's `ComparableVersion` orders unknown qualifiers after the final release, so `1.0.0-preview.1` compares newer than `1.0.0` — measured, not assumed. It costs nothing here because previews are installed by hand into a local repository and never published beside a GA, and consumers name exact versions. If a preview ever has to live in a shared repository, use `-rc.<n>`, which Maven does order below the release.

To remove string-parsing ambiguity, every artifact manifest records these separately:

Expand Down Expand Up @@ -524,7 +522,7 @@ Exit condition: a Java user who knows nothing about the implementation can insta
- [ ] Freeze the public Java API and the JNI ABI.
- [ ] Write the release notes and the known limitations.
- [ ] Publish an RC and hold a soak and external validation window of at least one week.
- [ ] Once every V1 release gate passes, publish the first release aligned to a stable engine, for example `26.7.0.1`.
- [ ] Once every V1 release gate passes, publish the first release, `1.0.0`, built on a stable engine.

## 6. Milestones

Expand Down
Loading
Loading