Skip to content

Publish chaincodec

Publish chaincodec #22

Workflow file for this run

name: Publish chaincodec
# Triggered by:
# 1. Pushing a chaincodec version tag:
# git tag chaincodec-v0.1.0 && git push --tags
# 2. Manual via Actions → "Run workflow" (pick any branch):
# dry_run=true → build only, skip publish steps
# ref → optional branch/tag to check out (default: current branch)
on:
push:
tags:
- 'chaincodec-v[0-9]+.[0-9]+.[0-9]+'
workflow_dispatch:
inputs:
dry_run:
description: 'Dry run (build only, skip all publish steps)'
required: true
default: 'true'
type: choice
options: ['true', 'false']
ref:
description: 'Branch or tag to build from (e.g. main, chaincodec-v0.1.0)'
required: false
default: ''
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
jobs:
# ─────────────────────────────────────────────────────────────────────────────
# Job 1: Publish Rust crates to crates.io in dependency order
#
# chaincodec-core
# ↳ chaincodec-registry chaincodec-evm
# ↳ chaincodec-batch chaincodec-stream
# ↳ chaincodec-observability
# ↳ chaincodec-cli
#
# 30-second sleeps let crates.io finish indexing before dependent crates
# resolve the freshly published version.
# ─────────────────────────────────────────────────────────────────────────────
publish-rust:
name: Publish Rust crates to crates.io
runs-on: ubuntu-latest
environment: Prod
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
- name: Cache cargo registry
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
key: ${{ runner.os }}-cargo-publish-${{ hashFiles('chaincodec/Cargo.lock') }}
- name: Publish chaincodec-core
if: github.event.inputs.dry_run != 'true'
working-directory: chaincodec
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: |
if ! cargo publish -p chaincodec-core 2>&1 | tee /tmp/pub_out; then
grep -q "already exists" /tmp/pub_out || { cat /tmp/pub_out; exit 1; }
echo "chaincodec-core already published – skip"
fi
- name: Wait for crates.io to index chaincodec-core
if: github.event.inputs.dry_run != 'true'
run: sleep 30
- name: Publish chaincodec-registry
if: github.event.inputs.dry_run != 'true'
working-directory: chaincodec
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: |
if ! cargo publish -p chaincodec-registry 2>&1 | tee /tmp/pub_out; then
grep -q "already exists" /tmp/pub_out || { cat /tmp/pub_out; exit 1; }
echo "chaincodec-registry already published – skip"
fi
- name: Publish chaincodec-evm
if: github.event.inputs.dry_run != 'true'
working-directory: chaincodec
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: |
if ! cargo publish -p chaincodec-evm 2>&1 | tee /tmp/pub_out; then
grep -q "already exists" /tmp/pub_out || { cat /tmp/pub_out; exit 1; }
echo "chaincodec-evm already published – skip"
fi
- name: Wait for crates.io to index chaincodec-registry + chaincodec-evm
if: github.event.inputs.dry_run != 'true'
run: sleep 30
- name: Publish chaincodec-batch
if: github.event.inputs.dry_run != 'true'
working-directory: chaincodec
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: |
if ! cargo publish -p chaincodec-batch 2>&1 | tee /tmp/pub_out; then
grep -q "already exists" /tmp/pub_out || { cat /tmp/pub_out; exit 1; }
echo "chaincodec-batch already published – skip"
fi
- name: Publish chaincodec-stream
if: github.event.inputs.dry_run != 'true'
working-directory: chaincodec
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: |
if ! cargo publish -p chaincodec-stream 2>&1 | tee /tmp/pub_out; then
grep -q "already exists" /tmp/pub_out || { cat /tmp/pub_out; exit 1; }
echo "chaincodec-stream already published – skip"
fi
- name: Publish chaincodec-observability
if: github.event.inputs.dry_run != 'true'
working-directory: chaincodec
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: |
if ! cargo publish -p chaincodec-observability 2>&1 | tee /tmp/pub_out; then
grep -q "already exists" /tmp/pub_out || { cat /tmp/pub_out; exit 1; }
echo "chaincodec-observability already published – skip"
fi
- name: Wait for crates.io to index batch + stream + observability
if: github.event.inputs.dry_run != 'true'
run: sleep 30
- name: Publish chaincodec-cli
if: github.event.inputs.dry_run != 'true'
working-directory: chaincodec
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: |
if ! cargo publish -p chaincodec-cli 2>&1 | tee /tmp/pub_out; then
grep -q "already exists" /tmp/pub_out || { cat /tmp/pub_out; exit 1; }
echo "chaincodec-cli already published – skip"
fi
# ─────────────────────────────────────────────────────────────────────────────
# Job 2: Build Node.js native .node files for all supported platforms
#
# Matrix targets:
# linux-x64-gnu — Ubuntu / most Linux servers
# linux-x64-musl — Alpine / Docker-minimal images
# linux-arm64-gnu — AWS Graviton, Raspberry Pi 4
# macos-x64 — Intel Macs
# macos-arm64 — Apple Silicon (M1/M2/M3)
# windows-x64 — Windows servers / desktops
# ─────────────────────────────────────────────────────────────────────────────
build-node-bindings:
name: Build Node bindings · ${{ matrix.settings.name }}
runs-on: ${{ matrix.settings.host }}
strategy:
fail-fast: false
matrix:
settings:
- name: linux-x64-gnu
host: ubuntu-latest
target: x86_64-unknown-linux-gnu
build: >
cd chaincodec/bindings/node &&
npx napi build --platform --release --target x86_64-unknown-linux-gnu
- name: linux-x64-musl
host: ubuntu-latest
target: x86_64-unknown-linux-musl
build: |
sudo apt-get install -y musl-tools
rustup target add x86_64-unknown-linux-musl
cd chaincodec/bindings/node
npx napi build --platform --release --target x86_64-unknown-linux-musl
- name: linux-arm64-gnu
host: ubuntu-latest
target: aarch64-unknown-linux-gnu
build: |
sudo apt-get install -y gcc-aarch64-linux-gnu
rustup target add aarch64-unknown-linux-gnu
cd chaincodec/bindings/node
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc \
npx napi build --platform --release --target aarch64-unknown-linux-gnu
- name: macos-x64
host: macos-latest
target: x86_64-apple-darwin
build: |
rustup target add x86_64-apple-darwin
cd chaincodec/bindings/node
npx napi build --platform --release --target x86_64-apple-darwin
- name: macos-arm64
host: macos-latest
target: aarch64-apple-darwin
build: |
rustup target add aarch64-apple-darwin
cd chaincodec/bindings/node
npx napi build --platform --release --target aarch64-apple-darwin
- name: windows-x64
host: windows-latest
target: x86_64-pc-windows-msvc
build: >
cd chaincodec/bindings/node &&
npx napi build --platform --release --target x86_64-pc-windows-msvc
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.settings.target }}
- name: Set up Node.js 20
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: chaincodec/bindings/node/package.json
- name: Install napi-rs CLI and dependencies
working-directory: chaincodec/bindings/node
run: npm install
- name: Build native module
shell: bash
run: ${{ matrix.settings.build }}
- name: Upload .node artifact
uses: actions/upload-artifact@v4
with:
name: bindings-${{ matrix.settings.name }}
path: chaincodec/bindings/node/*.node
if-no-files-found: error
# ─────────────────────────────────────────────────────────────────────────────
# Job 3: Collect all .node files and publish @chainfoundry/chaincodec to npm
# ─────────────────────────────────────────────────────────────────────────────
publish-npm:
name: Publish @chainfoundry/chaincodec to npm
runs-on: ubuntu-latest
needs: [build-node-bindings]
environment: Prod
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Set up Node.js 20
uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
- name: Download all platform .node artifacts
uses: actions/download-artifact@v4
with:
path: chaincodec/bindings/node/artifacts
- name: Move .node files into package root
working-directory: chaincodec/bindings/node
run: |
find artifacts -name '*.node' -exec mv {} . \;
echo "Collected binaries:"
ls -la *.node
- name: Install package dependencies
working-directory: chaincodec/bindings/node
run: npm install
- name: Publish to npm
if: github.event.inputs.dry_run != 'true'
working-directory: chaincodec/bindings/node
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: npm publish --access public
# ─────────────────────────────────────────────────────────────────────────────
# Job 4: Create a GitHub Release with install instructions
# ─────────────────────────────────────────────────────────────────────────────
github-release:
name: Create GitHub Release
runs-on: ubuntu-latest
needs: [publish-rust, publish-npm]
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Extract version from tag
id: version
shell: bash
run: |
if [[ "$GITHUB_REF_NAME" == chaincodec-v* ]]; then
echo "version=${GITHUB_REF_NAME#chaincodec-v}" >> "$GITHUB_OUTPUT"
else
echo "version=0.0.0-manual" >> "$GITHUB_OUTPUT"
fi
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
name: chaincodec v${{ steps.version.outputs.version }}
generate_release_notes: true
draft: false
prerelease: false
body: |
## chaincodec v${{ steps.version.outputs.version }}
Production-grade EVM ABI decoder — universal event log & function call decoding
for Rust, TypeScript/Node.js, Python, and WASM.
### Install
**Rust (Cargo.toml)**
```toml
[dependencies]
chaincodec-core = "${{ steps.version.outputs.version }}"
chaincodec-evm = "${{ steps.version.outputs.version }}"
chaincodec-registry = "${{ steps.version.outputs.version }}"
```
**npm / Node.js**
```bash
npm install @chainfoundry/chaincodec
```
**Python (pip)**
```bash
pip install chaincodec
```
**CLI**
```bash
cargo install chaincodec-cli
```
### Published crates
| Crate | crates.io |
|-------|-----------|
| chaincodec-core | https://crates.io/crates/chaincodec-core |
| chaincodec-evm | https://crates.io/crates/chaincodec-evm |
| chaincodec-registry | https://crates.io/crates/chaincodec-registry |
| chaincodec-batch | https://crates.io/crates/chaincodec-batch |
| chaincodec-stream | https://crates.io/crates/chaincodec-stream |
| chaincodec-observability | https://crates.io/crates/chaincodec-observability |
| chaincodec-cli | https://crates.io/crates/chaincodec-cli |
**npm:** https://www.npmjs.com/package/@chainfoundry/chaincodec
# ─────────────────────────────────────────────────────────────────────────────
# Job 5: Build Python wheels for all supported platforms (maturin)
#
# Produces manylinux2014, musllinux, macOS (x64 + arm64), Windows wheels
# for the chaincodec Python package.
# ─────────────────────────────────────────────────────────────────────────────
build-python-wheels:
name: Build Python wheel · ${{ matrix.platform.name }}
if: startsWith(github.ref, 'refs/tags/chaincodec-v') || github.event_name == 'workflow_dispatch'
runs-on: ${{ matrix.platform.runner }}
strategy:
fail-fast: false
matrix:
platform:
- name: linux-x64
runner: ubuntu-latest
target: x86_64-unknown-linux-gnu
manylinux: auto
- name: linux-arm64
runner: ubuntu-24.04-arm
target: aarch64-unknown-linux-gnu
manylinux: auto
- name: linux-x64-musl
runner: ubuntu-latest
target: x86_64-unknown-linux-musl
manylinux: musllinux_1_1
- name: macos-x64
runner: macos-latest
target: x86_64-apple-darwin
manylinux: ''
- name: macos-arm64
runner: macos-latest
target: aarch64-apple-darwin
manylinux: ''
- name: windows-x64
runner: windows-latest
target: x86_64-pc-windows-msvc
manylinux: ''
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install maturin
run: pip install maturin
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.platform.target }}
- name: Build wheels (Linux manylinux via docker — x86_64 + arm64)
if: runner.os == 'Linux' && matrix.platform.manylinux != 'musllinux_1_1'
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.platform.target }}
manylinux: ${{ matrix.platform.manylinux }}
# working-directory so maturin finds Cargo.toml without --manifest-path
# (avoids Docker cargo metadata resolution issues with workspace path deps)
working-directory: chaincodec/bindings/python
args: --release --out dist
sccache: true
# musl: the musllinux Docker cross-image has no Python interpreters.
# Build directly on the host after installing musl-tools instead.
- name: Build wheels (Linux musl — direct maturin, no Docker)
if: runner.os == 'Linux' && matrix.platform.manylinux == 'musllinux_1_1'
working-directory: chaincodec/bindings/python
run: |
sudo apt-get update -q && sudo apt-get install -y -q musl-tools
rustup target add x86_64-unknown-linux-musl
maturin build --release --out dist --target x86_64-unknown-linux-musl --find-interpreter
- name: Build wheels (macOS / Windows — native)
if: runner.os != 'Linux'
shell: bash
run: |
maturin build --release --out dist \
--manifest-path chaincodec/bindings/python/Cargo.toml \
--target ${{ matrix.platform.target }}
- name: Upload wheel artifact (Linux)
if: runner.os == 'Linux'
uses: actions/upload-artifact@v4
with:
name: python-wheel-${{ matrix.platform.name }}
# maturin-action with working-directory outputs to chaincodec/bindings/python/dist/
# direct maturin build also uses chaincodec/bindings/python/dist/ (same cwd)
path: chaincodec/bindings/python/dist/*.whl
if-no-files-found: error
- name: Upload wheel artifact (macOS / Windows)
if: runner.os != 'Linux'
uses: actions/upload-artifact@v4
with:
name: python-wheel-${{ matrix.platform.name }}
path: dist/*.whl
if-no-files-found: error
# ─────────────────────────────────────────────────────────────────────────────
# Job 6: Publish Python wheels to PyPI (maturin publish)
# ─────────────────────────────────────────────────────────────────────────────
publish-pypi-chaincodec:
name: Publish chaincodec to PyPI
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/chaincodec-v') || github.event_name == 'workflow_dispatch'
needs: [build-python-wheels]
environment:
name: pypi
url: https://pypi.org/p/chaincodec
permissions:
id-token: write # OIDC trusted publishing
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install maturin and twine
run: pip install maturin twine
- name: Download all wheel artifacts
uses: actions/download-artifact@v4
with:
pattern: python-wheel-*
path: dist
merge-multiple: true
- name: List wheels
run: ls -la dist/
- name: Publish to PyPI via OIDC trusted publishing
if: github.event.inputs.dry_run != 'true'
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: dist/
skip-existing: true
# ─────────────────────────────────────────────────────────────────────────────
# Job 7: Build WASM package (wasm-pack) and publish to npm
#
# Produces:
# @chainfoundry/chaincodec-wasm — ESM bundle for browser (target: web)
# @chainfoundry/chaincodec-wasm-node — CJS bundle for Node (target: nodejs)
# @chainfoundry/chaincodec-wasm-bundler — for webpack/vite (target: bundler)
# ─────────────────────────────────────────────────────────────────────────────
build-wasm:
name: Build WASM package
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/chaincodec-v') || github.event_name == 'workflow_dispatch'
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Install Rust stable + wasm32 target
uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown
- name: Cache cargo registry
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
key: ${{ runner.os }}-cargo-wasm-${{ hashFiles('chaincodec/Cargo.lock') }}
- name: Install wasm-pack
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
- name: Install wasm-opt
run: |
npm install -g wasm-opt
wasm-opt --version
- name: Build WASM (web target — ESM + .wasm)
working-directory: chaincodec/bindings/wasm
run: wasm-pack build --release --target web --out-dir pkg-web
- name: Build WASM (nodejs target — CJS)
working-directory: chaincodec/bindings/wasm
run: wasm-pack build --release --target nodejs --out-dir pkg-node
- name: Build WASM (bundler target — for webpack/vite)
working-directory: chaincodec/bindings/wasm
run: wasm-pack build --release --target bundler --out-dir pkg-bundler
- name: Upload WASM artifacts
uses: actions/upload-artifact@v4
with:
name: wasm-packages
path: |
chaincodec/bindings/wasm/pkg-web/
chaincodec/bindings/wasm/pkg-node/
chaincodec/bindings/wasm/pkg-bundler/
publish-wasm-npm:
name: Publish WASM to npm
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/chaincodec-v') || github.event_name == 'workflow_dispatch'
needs: [build-wasm]
environment: Prod
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Set up Node.js 20
uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
- name: Download WASM artifacts
uses: actions/download-artifact@v4
with:
name: wasm-packages
path: wasm-packages
- name: Extract version from tag
id: version
shell: bash
run: |
if [[ "$GITHUB_REF_NAME" == chaincodec-v* ]]; then
echo "version=${GITHUB_REF_NAME#chaincodec-v}" >> "$GITHUB_OUTPUT"
else
echo "version=0.0.0-manual" >> "$GITHUB_OUTPUT"
fi
- name: Patch package name + version (web)
# upload-artifact uses common ancestor as root: chaincodec/bindings/wasm/ is stripped
working-directory: wasm-packages/pkg-web
run: |
node -e "
const pkg = require('./package.json');
pkg.name = '@chainfoundry/chaincodec-wasm';
pkg.version = '${{ steps.version.outputs.version }}';
pkg.description = 'WebAssembly bindings for chaincodec — browser ESM bundle';
require('fs').writeFileSync('package.json', JSON.stringify(pkg, null, 2));
"
- name: Patch package name + version (node)
working-directory: wasm-packages/pkg-node
run: |
node -e "
const pkg = require('./package.json');
pkg.name = '@chainfoundry/chaincodec-wasm-node';
pkg.version = '${{ steps.version.outputs.version }}';
pkg.description = 'WebAssembly bindings for chaincodec — Node.js CJS bundle';
require('fs').writeFileSync('package.json', JSON.stringify(pkg, null, 2));
"
- name: Publish web bundle to npm
if: github.event.inputs.dry_run != 'true'
working-directory: wasm-packages/pkg-web
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: npm publish --access public
- name: Publish node bundle to npm
if: github.event.inputs.dry_run != 'true'
working-directory: wasm-packages/pkg-node
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: npm publish --access public