Skip to content

Release

Release #25

Workflow file for this run

---
name: Release
on:
workflow_dispatch:
inputs:
version:
description: >-
Release version (e.g. 0.3.1). Triggers full release: builds Swift
XCFramework, updates Package.swift, creates tag, publishes all
registries. Leave empty to re-trigger individual registries.
type: string
default: ''
crates-io:
description: Publish iscc-lib to crates.io
type: boolean
default: false
pypi:
description: Publish iscc-lib to PyPI
type: boolean
default: false
npm:
description: Publish @iscc/lib and @iscc/wasm to npm
type: boolean
default: false
maven:
description: Publish iscc-lib to Maven Central
type: boolean
default: false
ffi:
description: Build and publish FFI tarballs to GitHub Releases
type: boolean
default: false
rubygems:
description: Publish iscc-lib to RubyGems
type: boolean
default: false
nuget:
description: Publish Iscc.Lib to NuGet
type: boolean
default: false
maven-kotlin:
description: Publish iscc-lib-kotlin to Maven Central
type: boolean
default: false
permissions:
contents: read
concurrency:
group: release
cancel-in-progress: false # Never cancel a release in progress
jobs:
# Build Swift XCFramework, update Package.swift checksum, commit to main,
# create release tag and GitHub Release. Runs only for full releases (version
# input provided). All other publish jobs wait for this to complete so the tag
# exists before they start.
prepare-release:
name: Prepare release
if: inputs.version != ''
runs-on: macos-14
permissions:
contents: write
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: main
- name: Validate version
id: version
run: |
CARGO_VERSION=$(grep -m1 '^version' Cargo.toml | sed 's/.*"\(.*\)"/\1/')
if [ "$CARGO_VERSION" != "${{ inputs.version }}" ]; then
echo "::error::Cargo.toml version ($CARGO_VERSION) != input version (${{ inputs.version }})"
exit 1
fi
echo "version=${{ inputs.version }}" >> "$GITHUB_OUTPUT"
- uses: dtolnay/rust-toolchain@stable
with:
targets: >-
aarch64-apple-darwin,x86_64-apple-darwin,
aarch64-apple-ios,aarch64-apple-ios-sim,x86_64-apple-ios
- uses: Swatinem/rust-cache@v2
- uses: actions/cache@v4
id: xcf-cache
with:
path: target/ios/IsccLib.xcframework.zip
key: xcf-${{ hashFiles( 'crates/iscc-*/src/**', 'crates/iscc-*/Cargo.toml',
'Cargo.lock', 'Cargo.toml', 'scripts/build_xcframework.sh', 'packages/swift/Sources/iscc_uniffiFFI/**'
) }}
- name: Build XCFramework
if: steps.xcf-cache.outputs.cache-hit != 'true'
run: ./scripts/build_xcframework.sh --release
- name: Update Package.swift checksum
run: |
VERSION="${{ inputs.version }}"
CHECKSUM=$(swift package compute-checksum target/ios/IsccLib.xcframework.zip)
sed -E -i '' \
"s/(let releaseTag = \")[^\"]+/\1$VERSION/" Package.swift
sed -E -i '' \
"s/(let releaseChecksum = \")[^\"]+/\1$CHECKSUM/" Package.swift
- name: Commit Package.swift
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add Package.swift
if git diff --cached --quiet; then
echo "Package.swift unchanged, skipping commit"
else
git commit -m "chore: update Swift XCFramework checksum for v${{ inputs.version }}"
git push origin main
fi
- name: Create tags
run: |
git tag "v${{ inputs.version }}" 2>/dev/null || echo "Tag v${{ inputs.version }} already exists"
git tag "packages/go/v${{ inputs.version }}" 2>/dev/null || echo "Tag packages/go/v${{ inputs.version }} already exists"
git push origin "v${{ inputs.version }}" "packages/go/v${{ inputs.version }}" --force
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ inputs.version }}
files: target/ios/IsccLib.xcframework.zip
generate_release_notes: true
# Publish Rust core crate to crates.io via OIDC trusted publishing
publish-crates-io:
name: Publish to crates.io
needs: [prepare-release]
if: ${{ !cancelled() && !failure() && (inputs.version != '' || inputs.crates-io) }}
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Get workspace version
id: version
run: |
VERSION=$(grep -m1 '^version' Cargo.toml | sed 's/.*"\(.*\)"/\1/')
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Check version on registry
id: check
run: |
VERSION="${{ steps.version.outputs.version }}"
if cargo info iscc-lib 2>/dev/null | grep -q "version: $VERSION"; then
echo "Version $VERSION already published to crates.io, skipping"
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
- name: Run tests
if: steps.check.outputs.skip != 'true'
run: cargo test --workspace --exclude iscc-rb
- name: Authenticate with crates.io
if: steps.check.outputs.skip != 'true'
id: crates-auth
uses: rust-lang/crates-io-auth-action@v1
- name: Publish iscc-lib
if: steps.check.outputs.skip != 'true'
run: cargo publish -p iscc-lib
env:
CARGO_REGISTRY_TOKEN: ${{ steps.crates-auth.outputs.token }}
# Build cross-platform Python wheels using maturin
build-wheels:
name: Build wheels (${{ matrix.target }})
needs: [prepare-release]
if: ${{ !cancelled() && !failure() && (inputs.version != '' || inputs.pypi) }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
include:
- os: ubuntu-latest
target: x86_64
interpreter: python3.10
- os: macos-14
target: universal2-apple-darwin
interpreter: python3.10
- os: windows-latest
target: x64
interpreter: python
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Build wheel
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.target }}
args: --release --out dist --manifest-path crates/iscc-py/Cargo.toml -i
${{ matrix.interpreter }}
manylinux: auto
before-script-linux: |
# Put Python 3.10 first in PATH so maturin uses it for abi3 wheel tagging.
# Without this, the manylinux container defaults to Python 3.8 which is below
# the abi3-py310 minimum and produces a version-specific cp38 wheel instead.
export PATH="/opt/python/cp310-cp310/bin:$PATH"
- name: Upload wheels
uses: actions/upload-artifact@v4
with:
name: wheels-${{ matrix.os }}-${{ matrix.target }}
path: dist
# Build source distribution
build-sdist:
name: Build sdist
needs: [prepare-release]
if: ${{ !cancelled() && !failure() && (inputs.version != '' || inputs.pypi) }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build sdist
uses: PyO3/maturin-action@v1
with:
command: sdist
args: --out dist --manifest-path crates/iscc-py/Cargo.toml
- name: Upload sdist
uses: actions/upload-artifact@v4
with:
name: wheels-sdist
path: dist
# Smoke test Python wheel on Linux before publishing
test-wheels:
name: Test Python wheel
needs: [build-wheels]
if: inputs.version != '' || inputs.pypi
runs-on: ubuntu-latest
steps:
- uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Download wheel
uses: actions/download-artifact@v4
with:
name: wheels-ubuntu-latest-x86_64
path: dist
- name: Install and test
run: |
pip install dist/*.whl
python -c "from iscc_lib import conformance_selftest; assert conformance_selftest()"
# Publish all wheels and sdist to PyPI via OIDC trusted publishing
publish-pypi:
name: Publish to PyPI
needs: [build-wheels, build-sdist, test-wheels]
if: inputs.version != '' || inputs.pypi
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
- name: Get workspace version
id: version
run: |
VERSION=$(grep -m1 '^version' Cargo.toml | sed 's/.*"\(.*\)"/\1/')
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Check version on registry
id: check
run: |
VERSION="${{ steps.version.outputs.version }}"
if curl -sf "https://pypi.org/pypi/iscc-lib/$VERSION/json" > /dev/null 2>&1; then
echo "Version $VERSION already published to PyPI, skipping"
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
- name: Download all artifacts
if: steps.check.outputs.skip != 'true'
uses: actions/download-artifact@v4
with:
pattern: wheels-*
path: dist
merge-multiple: true
- name: Publish to PyPI
if: steps.check.outputs.skip != 'true'
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: dist
# Build napi-rs native addons for cross-platform npm publishing
build-napi:
name: Build napi (${{ matrix.target }})
needs: [prepare-release]
if: ${{ !cancelled() && !failure() && (inputs.version != '' || inputs.npm) }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
- os: ubuntu-latest
target: aarch64-unknown-linux-gnu
- os: macos-14
target: aarch64-apple-darwin
- os: macos-14
target: x86_64-apple-darwin
- os: windows-latest
target: x86_64-pc-windows-msvc
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
- name: Install npm dependencies
run: npm install
working-directory: crates/iscc-napi
- name: Install cross-compiler
if: matrix.target == 'aarch64-unknown-linux-gnu'
run: |
sudo apt-get update
sudo apt-get install -y gcc-aarch64-linux-gnu
- name: Build native addon
run: npx napi build --platform --release --target ${{ matrix.target }}
working-directory: crates/iscc-napi
env:
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: napi-${{ matrix.target }}
path: |
crates/iscc-napi/*.node
crates/iscc-napi/index.js
crates/iscc-napi/index.d.ts
# Smoke test NAPI native addon on Linux before publishing
test-napi:
name: Test NAPI addon
needs: [build-napi]
if: inputs.version != '' || inputs.npm
runs-on: ubuntu-latest
steps:
- uses: actions/setup-node@v4
with:
node-version: '22'
- name: Download addon
uses: actions/download-artifact@v4
with:
name: napi-x86_64-unknown-linux-gnu
path: addon
- name: Test native addon
run: node -e "const m = require('./addon/iscc-lib.linux-x64-gnu.node'); if
(!m.conformance_selftest()) { throw new Error('conformance failed'); }"
# Publish @iscc/lib to npm
publish-npm-lib:
name: Publish @iscc/lib to npm
needs: [build-napi, test-napi]
if: inputs.version != '' || inputs.npm
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
registry-url: https://registry.npmjs.org
- name: Install npm dependencies
run: npm install
working-directory: crates/iscc-napi
- name: Download all napi artifacts
uses: actions/download-artifact@v4
with:
pattern: napi-*
path: crates/iscc-napi
merge-multiple: true
- name: Prepare npm packages
run: npx napi prepublish -t npm
working-directory: crates/iscc-napi
- name: Get workspace version
id: version
run: |
VERSION=$(grep -m1 '^version' Cargo.toml | sed 's/.*"\(.*\)"/\1/')
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Check version on registry
id: check
run: |
VERSION="${{ steps.version.outputs.version }}"
if npm view "@iscc/lib@$VERSION" version 2>/dev/null; then
echo "Version $VERSION already published to npm, skipping"
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
- name: Publish to npm
if: steps.check.outputs.skip != 'true'
run: npm publish --provenance --access public
working-directory: crates/iscc-napi
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
# Build WASM package for npm publishing
build-wasm:
name: Build WASM package
needs: [prepare-release]
if: ${{ !cancelled() && !failure() && (inputs.version != '' || inputs.npm) }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Install wasm-pack
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
- name: Build WASM package
run: wasm-pack build --target web --release crates/iscc-wasm --features conformance
- name: Set npm package name and version
run: |
node <<'SCRIPT'
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('crates/iscc-wasm/pkg/package.json', 'utf8'));
pkg.name = '@iscc/wasm';
pkg.repository = { type: 'git', url: 'https://github.com/iscc/iscc-lib' };
const toml = fs.readFileSync('Cargo.toml', 'utf8');
const m = toml.match(/^version\s*=\s*"(.+?)"/m);
if (m) pkg.version = m[1];
fs.writeFileSync('crates/iscc-wasm/pkg/package.json', JSON.stringify(pkg, null, 2) + '\n');
SCRIPT
- name: Upload WASM package
uses: actions/upload-artifact@v4
with:
name: wasm-pkg
path: crates/iscc-wasm/pkg/
# Smoke test WASM package on Linux before publishing
test-wasm:
name: Test WASM package
needs: [build-wasm]
if: inputs.version != '' || inputs.npm
runs-on: ubuntu-latest
steps:
- uses: actions/setup-node@v4
with:
node-version: '22'
- name: Download WASM package
uses: actions/download-artifact@v4
with:
name: wasm-pkg
path: pkg
- name: Test WASM module
run: |
cat > smoke.mjs << 'SCRIPT'
import { readFileSync } from 'fs';
import init, { conformance_selftest } from './pkg/iscc_wasm.js';
const wasmBytes = readFileSync('./pkg/iscc_wasm_bg.wasm');
await init(wasmBytes);
if (!conformance_selftest()) { throw new Error('conformance failed'); }
SCRIPT
node smoke.mjs
# Publish @iscc/wasm to npm
publish-npm-wasm:
name: Publish @iscc/wasm to npm
needs: [build-wasm, test-wasm]
if: inputs.version != '' || inputs.npm
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/setup-node@v4
with:
node-version: '22'
registry-url: https://registry.npmjs.org
- name: Download WASM package
uses: actions/download-artifact@v4
with:
name: wasm-pkg
path: pkg
- name: Get package version
id: version
run: |
VERSION=$(node -p "require('./pkg/package.json').version")
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Check version on registry
id: check
run: |
VERSION="${{ steps.version.outputs.version }}"
if npm view "@iscc/wasm@$VERSION" version 2>/dev/null; then
echo "Version $VERSION already published to npm, skipping"
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
- name: Publish to npm
if: steps.check.outputs.skip != 'true'
run: npm publish --provenance --access public
working-directory: pkg
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
# Build JNI native libraries for cross-platform JAR packaging
build-jni:
name: Build JNI (${{ matrix.native-dir }})
needs: [prepare-release]
if: ${{ !cancelled() && !failure() && (inputs.version != '' || inputs.maven) }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
native-dir: linux-x86_64
lib-name: libiscc_jni.so
- os: ubuntu-latest
target: aarch64-unknown-linux-gnu
native-dir: linux-aarch64
lib-name: libiscc_jni.so
- os: macos-14
target: aarch64-apple-darwin
native-dir: macos-aarch64
lib-name: libiscc_jni.dylib
- os: macos-14
target: x86_64-apple-darwin
native-dir: macos-x86_64
lib-name: libiscc_jni.dylib
- os: windows-latest
target: x86_64-pc-windows-msvc
native-dir: windows-x86_64
lib-name: iscc_jni.dll
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
- name: Install cross-compiler
if: matrix.target == 'aarch64-unknown-linux-gnu'
run: |
sudo apt-get update
sudo apt-get install -y gcc-aarch64-linux-gnu
- name: Build JNI library
run: cargo build -p iscc-jni --release --target ${{ matrix.target }}
env:
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: jni-${{ matrix.native-dir }}
path: target/${{ matrix.target }}/release/${{ matrix.lib-name }}
# Assemble cross-platform JAR with bundled native libraries
assemble-jar:
name: Assemble JAR
needs: [build-jni]
if: inputs.version != '' || inputs.maven
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
- name: Download JNI artifacts
uses: actions/download-artifact@v4
with:
pattern: jni-*
path: jni-staging
- name: Copy native libraries to resources
run: |
for dir in jni-staging/jni-*/; do
native_dir=$(basename "$dir" | sed 's/^jni-//')
dest="crates/iscc-jni/java/src/main/resources/META-INF/native/$native_dir"
mkdir -p "$dest"
cp "$dir"* "$dest/"
done
- name: Build JAR
run: mvn package -DskipTests -f crates/iscc-jni/java/pom.xml
- name: Upload JAR
uses: actions/upload-artifact@v4
with:
name: iscc-lib-jar
path: crates/iscc-jni/java/target/*.jar
# Smoke test JNI native libraries on Linux before publishing
test-jni:
name: Test JNI libraries
needs: [build-jni]
if: inputs.version != '' || inputs.maven
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
- name: Download JNI artifacts
uses: actions/download-artifact@v4
with:
pattern: jni-*
path: jni-staging
- name: Copy native libraries to resources
run: |
for dir in jni-staging/jni-*/; do
native_dir=$(basename "$dir" | sed 's/^jni-//')
dest="crates/iscc-jni/java/src/main/resources/META-INF/native/$native_dir"
mkdir -p "$dest"
cp "$dir"* "$dest/"
done
- name: Run tests
run: mvn test -f crates/iscc-jni/java/pom.xml
# Publish Java JAR to Maven Central via Sonatype Central Portal
publish-maven:
name: Publish to Maven Central
needs: [build-jni, test-jni]
if: inputs.version != '' || inputs.maven
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
server-id: central
server-username: MAVEN_USERNAME
server-password: MAVEN_PASSWORD
- name: Get workspace version
id: version
run: |
VERSION=$(grep -m1 '^version' Cargo.toml | sed 's/.*"\(.*\)"/\1/')
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Check version on Maven Central
id: check
run: |
VERSION="${{ steps.version.outputs.version }}"
if curl -sf "https://search.maven.org/solrsearch/select?q=g:io.iscc+AND+a:iscc-lib+AND+v:$VERSION&rows=1&wt=json" | grep -q '"numFound":1'; then
echo "Version $VERSION already published to Maven Central, skipping"
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
- name: Download JNI artifacts
if: steps.check.outputs.skip != 'true'
uses: actions/download-artifact@v4
with:
pattern: jni-*
path: jni-staging
- name: Copy native libraries to resources
if: steps.check.outputs.skip != 'true'
run: |
for dir in jni-staging/jni-*/; do
native_dir=$(basename "$dir" | sed 's/^jni-//')
dest="crates/iscc-jni/java/src/main/resources/META-INF/native/$native_dir"
mkdir -p "$dest"
cp "$dir"* "$dest/"
done
- name: Import GPG key
if: steps.check.outputs.skip != 'true'
run: |
echo "${{ secrets.MAVEN_GPG_PRIVATE_KEY }}" | gpg --batch --import || true
gpg --list-secret-keys --keyid-format LONG | grep -q "sec" || { echo "GPG key import failed"; exit 1; }
- name: Publish to Maven Central
if: steps.check.outputs.skip != 'true'
run: mvn deploy -Prelease -DskipTests -f crates/iscc-jni/java/pom.xml
env:
MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }}
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }}
# Build FFI shared/static libraries for C/C++ consumers
build-ffi:
name: Build FFI (${{ matrix.target }})
needs: [prepare-release]
if: ${{ !cancelled() && !failure() && (inputs.version != '' || inputs.ffi || inputs.nuget) }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
lib-shared: libiscc_ffi.so
lib-static: libiscc_ffi.a
- os: ubuntu-latest
target: aarch64-unknown-linux-gnu
lib-shared: libiscc_ffi.so
lib-static: libiscc_ffi.a
- os: macos-14
target: aarch64-apple-darwin
lib-shared: libiscc_ffi.dylib
lib-static: libiscc_ffi.a
- os: macos-14
target: x86_64-apple-darwin
lib-shared: libiscc_ffi.dylib
lib-static: libiscc_ffi.a
- os: windows-latest
target: x86_64-pc-windows-msvc
lib-shared: iscc_ffi.dll
lib-static: iscc_ffi.lib
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
- name: Install cross-compiler
if: matrix.target == 'aarch64-unknown-linux-gnu'
run: |
sudo apt-get update
sudo apt-get install -y gcc-aarch64-linux-gnu
- name: Build FFI library
run: cargo build -p iscc-ffi --release --target ${{ matrix.target }}
env:
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
- name: Get workspace version
id: version
shell: bash
run: |
VERSION=$(grep -m1 '^version' Cargo.toml | sed 's/.*"\(.*\)"/\1/')
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Stage artifacts (Unix)
if: runner.os != 'Windows'
run: |
VERSION="${{ steps.version.outputs.version }}"
TARGET="${{ matrix.target }}"
DIR="iscc-ffi-v${VERSION}-${TARGET}"
mkdir -p "$DIR"
cp "target/${TARGET}/release/${{ matrix.lib-shared }}" "$DIR/"
cp "target/${TARGET}/release/${{ matrix.lib-static }}" "$DIR/"
cp crates/iscc-ffi/include/iscc.h "$DIR/"
cp packages/cpp/include/iscc/iscc.hpp "$DIR/"
cp LICENSE "$DIR/"
tar czf "${DIR}.tar.gz" "$DIR"
- name: Stage artifacts (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
$VERSION = "${{ steps.version.outputs.version }}"
$TARGET = "${{ matrix.target }}"
$DIR = "iscc-ffi-v${VERSION}-${TARGET}"
New-Item -ItemType Directory -Path $DIR -Force
Copy-Item "target/${TARGET}/release/iscc_ffi.dll" "$DIR/"
Copy-Item "target/${TARGET}/release/iscc_ffi.dll.lib" "$DIR/"
Copy-Item "target/${TARGET}/release/iscc_ffi.lib" "$DIR/"
Copy-Item "crates/iscc-ffi/include/iscc.h" "$DIR/"
Copy-Item "packages/cpp/include/iscc/iscc.hpp" "$DIR/"
Copy-Item "LICENSE" "$DIR/"
Compress-Archive -Path "$DIR" -DestinationPath "${DIR}.zip"
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ffi-${{ matrix.target }}
path: iscc-ffi-v*.*
# Smoke test FFI shared library on Linux before publishing
test-ffi:
name: Test FFI library
needs: [build-ffi]
if: inputs.version != '' || inputs.ffi
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Download FFI artifact
uses: actions/download-artifact@v4
with:
name: ffi-x86_64-unknown-linux-gnu
path: ffi-staging
- name: Extract and test
run: |
tar xzf ffi-staging/*.tar.gz
LIB_DIR=$(find . -maxdepth 1 -name 'iscc-ffi-v*' -type d)
cc -o test_iscc crates/iscc-ffi/tests/test_iscc.c \
-I"$LIB_DIR" -L"$LIB_DIR" -liscc_ffi -lpthread -ldl -lm
LD_LIBRARY_PATH="$LIB_DIR" ./test_iscc
# Publish FFI tarballs to GitHub Releases
publish-ffi:
name: Publish FFI to GitHub Releases
needs: [build-ffi, test-ffi]
if: inputs.version != '' || inputs.ffi
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- name: Get workspace version
id: version
run: |
VERSION=$(grep -m1 '^version' Cargo.toml | sed 's/.*"\(.*\)"/\1/')
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Download all FFI artifacts
uses: actions/download-artifact@v4
with:
pattern: ffi-*
path: ffi-staging
merge-multiple: true
- name: List artifacts
run: ls -lR ffi-staging/
- name: Upload release assets
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ steps.version.outputs.version }}
files: ffi-staging/iscc-ffi-v*.*
# Build cross-platform precompiled Ruby gems via rb-sys-dock
build-gem:
name: Build gem (${{ matrix.platform }})
needs: [prepare-release]
if: ${{ !cancelled() && !failure() && (inputs.version != '' || inputs.rubygems) }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
platform:
- x86_64-linux
- aarch64-linux
- x86_64-darwin
- arm64-darwin
- x64-mingw-ucrt
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3'
bundler-cache: true
working-directory: crates/iscc-rb
- name: Link Gemfile.lock for rb_sys version detection
# The cross-gem action's configure step runs `grep rb_sys Gemfile.lock`
# in the repo root (not working-directory). Without this symlink it
# falls back to `gem info rb_sys --remote` -> 0.9.124, which installs
# rake-compiler-dock 1.11.0 (maps "3.3" -> "3.3.10" -- missing from
# the 0.9.123 Docker image that only has rbconfig for 3.3.9).
run: ln -sf crates/iscc-rb/Gemfile.lock Gemfile.lock
- name: Cross-compile gem
uses: oxidize-rb/actions/cross-gem@v1
with:
platform: ${{ matrix.platform }}
ruby-versions: 3.1, 3.2, 3.3
working-directory: crates/iscc-rb
# Pin to 0.9.123 Docker image (Ruby 3.4 build tool, rbconfigs for
# 3.1.7/3.2.9/3.3.9). The 0.9.124 image has Ruby 4.0 which breaks
# RbSys::ExtensionTask. Must match rb_sys in Gemfile.lock.
tag: 0.9.123
- name: Upload gem
uses: actions/upload-artifact@v4
with:
name: gem-${{ matrix.platform }}
path: crates/iscc-rb/pkg/*-${{ matrix.platform }}.gem
# Smoke test Ruby gem on Linux before publishing
test-gem:
name: Test Ruby gem
needs: [build-gem]
if: inputs.version != '' || inputs.rubygems
runs-on: ubuntu-latest
steps:
- uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3'
- name: Download gem
uses: actions/download-artifact@v4
with:
name: gem-x86_64-linux
path: gems
- name: Install and test
run: |
gem install gems/*.gem
ruby -e "require 'iscc_lib'; raise 'Conformance failed' unless IsccLib.conformance_selftest"
# Publish precompiled Ruby gems and source gem to RubyGems.org
publish-rubygems:
name: Publish to RubyGems
needs: [build-gem, test-gem]
if: inputs.version != '' || inputs.rubygems
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3'
- name: Configure RubyGems credentials (OIDC trusted publishing)
uses: rubygems/configure-rubygems-credentials@main
- name: Get workspace version
id: version
run: |
VERSION=$(grep -m1 '^version' Cargo.toml | sed 's/.*"\(.*\)"/\1/')
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Check version on RubyGems
id: check
run: |
VERSION="${{ steps.version.outputs.version }}"
if curl -sf "https://rubygems.org/api/v1/versions/iscc-lib.json" | grep -q "\"number\":\"$VERSION\""; then
echo "Version $VERSION already published to RubyGems, skipping"
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
- name: Download gem artifacts
if: steps.check.outputs.skip != 'true'
uses: actions/download-artifact@v4
with:
pattern: gem-*
path: gems
merge-multiple: true
- name: Build source gem
if: steps.check.outputs.skip != 'true'
run: gem build iscc-lib.gemspec
working-directory: crates/iscc-rb
- name: Publish gems
if: steps.check.outputs.skip != 'true'
run: |-
# Publish precompiled platform gems
for gem in gems/*.gem; do
echo "Publishing $gem"
gem push "$gem"
done
# Publish source gem (fallback for platforms without precompiled gems)
for gem in crates/iscc-rb/*.gem; do
echo "Publishing $gem"
gem push "$gem"
done
# Pack NuGet package with bundled native libraries for 5 platforms
pack-nuget:
name: Pack NuGet package
needs: [build-ffi]
if: inputs.version != '' || inputs.nuget
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0'
- name: Download FFI artifacts
uses: actions/download-artifact@v4
with:
pattern: ffi-*
path: ffi-staging
- name: Extract and map native libraries to NuGet RIDs
run: |
PROJ_DIR="packages/dotnet/Iscc.Lib"
declare -A TARGET_RID=(
["x86_64-unknown-linux-gnu"]="linux-x64"
["aarch64-unknown-linux-gnu"]="linux-arm64"
["aarch64-apple-darwin"]="osx-arm64"
["x86_64-apple-darwin"]="osx-x64"
["x86_64-pc-windows-msvc"]="win-x64"
)
declare -A TARGET_LIB=(
["x86_64-unknown-linux-gnu"]="libiscc_ffi.so"
["aarch64-unknown-linux-gnu"]="libiscc_ffi.so"
["aarch64-apple-darwin"]="libiscc_ffi.dylib"
["x86_64-apple-darwin"]="libiscc_ffi.dylib"
["x86_64-pc-windows-msvc"]="iscc_ffi.dll"
)
for target in "${!TARGET_RID[@]}"; do
rid="${TARGET_RID[$target]}"
lib="${TARGET_LIB[$target]}"
dest="$PROJ_DIR/runtimes/$rid/native"
mkdir -p "$dest"
# Extract archive (tar.gz for Unix, zip for Windows)
artifact_dir="ffi-staging/ffi-$target"
if ls "$artifact_dir"/*.tar.gz 1>/dev/null 2>&1; then
tar xzf "$artifact_dir"/*.tar.gz
elif ls "$artifact_dir"/*.zip 1>/dev/null 2>&1; then
unzip -o "$artifact_dir"/*.zip
fi
# Find and copy the shared library (scope by target to avoid cross-arch matches)
find . -maxdepth 2 -name "$lib" -path "*-${target}/*" -exec cp {} "$dest/" \;
echo "Copied $lib -> $dest/"
done
echo "Runtime layout:"
find "$PROJ_DIR/runtimes" -type f
- name: Pack NuGet package
run: dotnet pack -c Release -o nupkg
working-directory: packages/dotnet/Iscc.Lib
- name: Upload NuGet package
uses: actions/upload-artifact@v4
with:
name: nuget-package
path: packages/dotnet/Iscc.Lib/nupkg/*.nupkg
# Smoke test NuGet package on all platforms before publishing
test-nuget:
name: Test NuGet (${{ matrix.os }})
needs: [pack-nuget]
if: inputs.version != '' || inputs.nuget
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0'
- name: Download NuGet package
uses: actions/download-artifact@v4
with:
name: nuget-package
path: nupkg
- name: Create and run smoke test (Unix)
if: runner.os != 'Windows'
run: |
dotnet new console -o smoke
cd smoke
dotnet nuget add source "$PWD/../nupkg" -n local
dotnet add package Iscc.Lib
cat > Program.cs << 'EOF'
using Iscc.Lib;
if (!IsccLib.ConformanceSelftest())
throw new Exception("Conformance selftest failed");
Console.WriteLine("NuGet smoke test passed");
EOF
dotnet run
- name: Create and run smoke test (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
dotnet new console -o smoke
cd smoke
dotnet nuget add source "$PWD\..\nupkg" -n local
dotnet add package Iscc.Lib
@"
using Iscc.Lib;
if (!IsccLib.ConformanceSelftest())
throw new Exception("Conformance selftest failed");
Console.WriteLine("NuGet smoke test passed");
"@ | Set-Content Program.cs
dotnet run
# Publish Iscc.Lib to NuGet.org
publish-nuget:
name: Publish to NuGet
needs: [pack-nuget, test-nuget]
if: inputs.version != '' || inputs.nuget
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0'
- name: Get workspace version
id: version
run: |
VERSION=$(grep -m1 '^version' Cargo.toml | sed 's/.*"\(.*\)"/\1/')
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Check version on NuGet
id: check
run: |
VERSION="${{ steps.version.outputs.version }}"
if curl -sf "https://api.nuget.org/v3-flatcontainer/iscc.lib/$VERSION/iscc.lib.nuspec" > /dev/null 2>&1; then
echo "Version $VERSION already published to NuGet, skipping"
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
- name: Download NuGet package
if: steps.check.outputs.skip != 'true'
uses: actions/download-artifact@v4
with:
name: nuget-package
path: nupkg
- name: Publish to NuGet
if: steps.check.outputs.skip != 'true'
run: dotnet nuget push nupkg/*.nupkg --api-key "$NUGET_API_KEY" --source https://api.nuget.org/v3/index.json
env:
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
# Build UniFFI native libraries for Kotlin JVM JAR packaging
build-kotlin-native:
name: Build Kotlin native (${{ matrix.native-dir }})
needs: [prepare-release]
if: ${{ !cancelled() && !failure() && (inputs.version != '' || inputs.maven-kotlin) }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
native-dir: linux-x86-64
lib-name: libiscc_uniffi.so
- os: ubuntu-latest
target: aarch64-unknown-linux-gnu
native-dir: linux-aarch64
lib-name: libiscc_uniffi.so
- os: macos-14
target: aarch64-apple-darwin
native-dir: darwin-aarch64
lib-name: libiscc_uniffi.dylib
- os: macos-14
target: x86_64-apple-darwin
native-dir: darwin-x86-64
lib-name: libiscc_uniffi.dylib
- os: windows-latest
target: x86_64-pc-windows-msvc
native-dir: win32-x86-64
lib-name: iscc_uniffi.dll
- os: ubuntu-latest
target: aarch64-linux-android
native-dir: android-aarch64
lib-name: libiscc_uniffi.so
android-abi: arm64-v8a
- os: ubuntu-latest
target: armv7-linux-androideabi
native-dir: android-arm
lib-name: libiscc_uniffi.so
android-abi: armeabi-v7a
- os: ubuntu-latest
target: x86_64-linux-android
native-dir: android-x86-64
lib-name: libiscc_uniffi.so
android-abi: x86_64
- os: ubuntu-latest
target: i686-linux-android
native-dir: android-x86
lib-name: libiscc_uniffi.so
android-abi: x86
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
- name: Install cross-compiler
if: matrix.target == 'aarch64-unknown-linux-gnu'
run: |
sudo apt-get update
sudo apt-get install -y gcc-aarch64-linux-gnu
- name: Setup Android NDK
if: contains(matrix.target, 'android')
uses: nttld/setup-ndk@v1
id: setup-ndk
with:
ndk-version: r27c
add-to-path: false
- name: Install cargo-ndk
if: contains(matrix.target, 'android')
run: cargo install cargo-ndk
- name: Build UniFFI library
if: "!contains(matrix.target, 'android')"
run: cargo build -p iscc-uniffi --release --target ${{ matrix.target }}
env:
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
- name: Build UniFFI library (Android)
if: contains(matrix.target, 'android')
run: cargo ndk --target ${{ matrix.android-abi }} build -p iscc-uniffi --release
env:
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: kotlin-native-${{ matrix.native-dir }}
path: target/${{ matrix.target }}/release/${{ matrix.lib-name }}
# Assemble Kotlin JAR with bundled native libraries for JNA
assemble-kotlin:
name: Assemble Kotlin JAR
needs: [build-kotlin-native]
if: inputs.version != '' || inputs.maven-kotlin
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
- name: Download native artifacts
uses: actions/download-artifact@v4
with:
pattern: kotlin-native-*
path: kotlin-staging
- name: Copy native libraries to JNA resource paths
run: |
for dir in kotlin-staging/kotlin-native-*/; do
native_dir=$(basename "$dir" | sed 's/^kotlin-native-//')
dest="packages/kotlin/src/main/resources/$native_dir"
mkdir -p "$dest"
cp "$dir"* "$dest/"
done
echo "JNA resource layout:"
find packages/kotlin/src/main/resources -type f
- name: Build JAR
run: ./gradlew build
working-directory: packages/kotlin
- name: Upload JAR
uses: actions/upload-artifact@v4
with:
name: kotlin-jar
path: packages/kotlin/build/libs/*.jar
# Smoke test Kotlin native libraries on Linux before publishing
test-kotlin-release:
name: Test Kotlin libraries
needs: [build-kotlin-native, assemble-kotlin]
if: inputs.version != '' || inputs.maven-kotlin
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
- name: Download linux-x86-64 native artifact
uses: actions/download-artifact@v4
with:
name: kotlin-native-linux-x86-64
path: native-lib
- name: Copy native library to JNA resource path
run: |
mkdir -p packages/kotlin/src/main/resources/linux-x86-64
cp native-lib/* packages/kotlin/src/main/resources/linux-x86-64/
- name: Run tests
run: ./gradlew test
working-directory: packages/kotlin
- name: Download assembled JAR
uses: actions/download-artifact@v4
with:
name: kotlin-jar
path: kotlin-jar
- name: Validate JAR contains all native libraries
run: |
# Select the runtime JAR (exclude -sources and -javadoc classifier JARs)
JAR=$(ls kotlin-jar/*.jar | grep -v '\-sources\.jar$\|\-javadoc\.jar$' | head -1)
echo "Inspecting JAR: $JAR"
JAR_CONTENTS=$(jar tf "$JAR")
EXPECTED_PATHS=(
"linux-x86-64/libiscc_uniffi.so"
"linux-aarch64/libiscc_uniffi.so"
"darwin-aarch64/libiscc_uniffi.dylib"
"darwin-x86-64/libiscc_uniffi.dylib"
"win32-x86-64/iscc_uniffi.dll"
"android-aarch64/libiscc_uniffi.so"
"android-arm/libiscc_uniffi.so"
"android-x86-64/libiscc_uniffi.so"
"android-x86/libiscc_uniffi.so"
)
MISSING=()
for path in "${EXPECTED_PATHS[@]}"; do
if echo "$JAR_CONTENTS" | grep -q "$path"; then
echo "OK: $path"
else
echo "MISSING: $path"
MISSING+=("$path")
fi
done
if [ ${#MISSING[@]} -ne 0 ]; then
echo ""
echo "ERROR: JAR is missing ${#MISSING[@]} native library path(s):"
for m in "${MISSING[@]}"; do
echo " - $m"
done
exit 1
fi
echo ""
echo "All 9 native library resource paths present in JAR"
# Publish Kotlin JAR to Maven Central via Sonatype Central Portal
publish-maven-kotlin:
name: Publish Kotlin to Maven Central
needs: [assemble-kotlin, test-kotlin-release]
if: inputs.version != '' || inputs.maven-kotlin
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
- name: Get workspace version
id: version
run: |
VERSION=$(grep -m1 '^version' Cargo.toml | sed 's/.*"\(.*\)"/\1/')
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Check version on Maven Central
id: check
run: |
VERSION="${{ steps.version.outputs.version }}"
if curl -sf "https://search.maven.org/solrsearch/select?q=g:io.iscc+AND+a:iscc-lib-kotlin+AND+v:$VERSION&rows=1&wt=json" | grep -q '"numFound":1'; then
echo "Version $VERSION already published to Maven Central, skipping"
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
- name: Download native artifacts
if: steps.check.outputs.skip != 'true'
uses: actions/download-artifact@v4
with:
pattern: kotlin-native-*
path: kotlin-staging
- name: Copy native libraries to JNA resource paths
if: steps.check.outputs.skip != 'true'
run: |
for dir in kotlin-staging/kotlin-native-*/; do
native_dir=$(basename "$dir" | sed 's/^kotlin-native-//')
dest="packages/kotlin/src/main/resources/$native_dir"
mkdir -p "$dest"
cp "$dir"* "$dest/"
done
- name: Publish to staging with signatures
if: steps.check.outputs.skip != 'true'
run: ./gradlew publishMavenPublicationToStagingRepository
working-directory: packages/kotlin
env:
SIGNING_KEY: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }}
SIGNING_PASSWORD: ${{ secrets.MAVEN_GPG_PASSPHRASE }}
- name: Upload bundle to Central Portal
if: steps.check.outputs.skip != 'true'
run: |
cd packages/kotlin/build/staging-deploy
zip -r "$GITHUB_WORKSPACE/bundle.zip" .
cd "$GITHUB_WORKSPACE"
HTTP_CODE=$(curl -s -o response.txt -w "%{http_code}" \
-X POST "https://central.sonatype.com/api/v1/publisher/upload?publishingType=AUTOMATIC" \
-u "$MAVEN_USERNAME:$MAVEN_PASSWORD" \
-F "bundle=@bundle.zip")
if [ "$HTTP_CODE" -ne 201 ]; then
echo "Upload failed with HTTP $HTTP_CODE"
cat response.txt
exit 1
fi
echo "Upload successful: $(cat response.txt)"
env:
MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }}
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}