name: Release
on:
push:
tags: ["v*"]
permissions:
actions: read
contents: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
PYTHON_BINDING_FEATURES: >-
parallel
alpha-dft alpha-reaxff alpha-mlff alpha-obara-saika alpha-cga alpha-gsm alpha-sdr
beta-kpm beta-mbh beta-randnla beta-riemannian beta-cpm
WASM_WEB_FEATURES: >-
parallel experimental-gpu
alpha-dft alpha-reaxff alpha-mlff alpha-obara-saika alpha-cga alpha-gsm alpha-sdr
alpha-dynamics-live alpha-imd
beta-kpm beta-mbh beta-randnla beta-riemannian beta-cpm
WASM_NODE_FEATURES: >-
alpha-dft alpha-reaxff alpha-mlff alpha-obara-saika alpha-cga alpha-gsm alpha-sdr
alpha-dynamics-live alpha-imd
beta-kpm beta-mbh beta-randnla beta-riemannian beta-cpm
# ─────────────────────────────────────────────────────────────────────────────
# Release pipeline:
# 1. wait-for-ci — block until CI passes for the same SHA
# 2. build-cli — cross-compile CLI grouped by OS (3 runners, 5 targets)
# 3. build-python — maturin wheels (3 platforms × 3.11)
# 4. build-wasm — wasm-pack bundler + npm package validation
# 5. verify-python — install wheel + smoke test
# 6. verify-node — install npm pkg + smoke test
# 7. verify-cli-linux — run the linux binary + smoke test
# 8. publish-crate — cargo publish to crates.io
# 9. publish-python — twine upload to PyPI
# 10. publish-npm — npm publish
# 11. release — GitHub Release with all artifacts
# ─────────────────────────────────────────────────────────────────────────────
jobs:
# ─── Gate 1: Wait for CI on the same SHA ───────────────────────────────────
wait-for-ci:
name: Wait for CI
runs-on: ubuntu-latest
steps:
- name: Wait for CI workflow to finish successfully
uses: actions/github-script@v7
with:
script: |
const workflowId = 'ci.yml';
const sha = context.sha;
const timeoutMs = 60 * 60 * 1000;
const intervalMs = 30 * 1000;
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const { data } = await github.rest.actions.listWorkflowRuns({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: workflowId,
head_sha: sha,
per_page: 10,
});
const run = data.workflow_runs.find((item) => item.head_sha === sha);
if (!run) {
core.info(`No CI run found yet for ${sha}; waiting...`);
} else if (run.status !== 'completed') {
core.info(`CI run ${run.id} is ${run.status}; waiting...`);
} else if (run.conclusion === 'success') {
core.info(`CI passed for ${sha}: ${run.html_url}`);
return;
} else {
throw new Error(`CI run ${run.id} finished with ${run.conclusion}: ${run.html_url}`);
}
await new Promise((resolve) => setTimeout(resolve, intervalMs));
}
throw new Error(`Timed out waiting for CI to complete successfully for ${sha}`);
# ─── CLI Binaries (grouped by OS to reduce runner startup overhead) ───────
build-cli-linux:
name: CLI (ubuntu-latest)
needs: [wait-for-ci]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-unknown-linux-gnu,aarch64-unknown-linux-gnu
- uses: Swatinem/rust-cache@v2
- name: Install cross-compilation tools (aarch64-linux)
run: |
sudo apt-get update
sudo apt-get install -y gcc-aarch64-linux-gnu
- name: Build Linux x86_64 CLI
run: cargo build --release --package sci-form-cli --target x86_64-unknown-linux-gnu
- name: Build Linux aarch64 CLI
run: cargo build --release --package sci-form-cli --target aarch64-unknown-linux-gnu
env:
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
- name: Stage Linux x86_64 artifact
shell: bash
run: cp target/x86_64-unknown-linux-gnu/release/sci-form sci-form-linux-x86_64
- name: Stage Linux aarch64 artifact
shell: bash
run: cp target/aarch64-unknown-linux-gnu/release/sci-form sci-form-linux-aarch64
- uses: actions/upload-artifact@v6
with:
name: sci-form-linux-x86_64
path: sci-form-linux-x86_64
- uses: actions/upload-artifact@v6
with:
name: sci-form-linux-aarch64
path: sci-form-linux-aarch64
build-cli-macos:
name: CLI (macos-latest)
needs: [wait-for-ci]
runs-on: macos-latest
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-apple-darwin,aarch64-apple-darwin
- uses: Swatinem/rust-cache@v2
- name: Build macOS x86_64 CLI
run: cargo build --release --package sci-form-cli --target x86_64-apple-darwin
- name: Build macOS aarch64 CLI
run: cargo build --release --package sci-form-cli --target aarch64-apple-darwin
- name: Stage macOS x86_64 artifact
shell: bash
run: cp target/x86_64-apple-darwin/release/sci-form sci-form-macos-x86_64
- name: Stage macOS aarch64 artifact
shell: bash
run: cp target/aarch64-apple-darwin/release/sci-form sci-form-macos-aarch64
- uses: actions/upload-artifact@v6
with:
name: sci-form-macos-x86_64
path: sci-form-macos-x86_64
- uses: actions/upload-artifact@v6
with:
name: sci-form-macos-aarch64
path: sci-form-macos-aarch64
build-cli-windows:
name: CLI (windows-latest)
needs: [wait-for-ci]
runs-on: windows-latest
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-pc-windows-msvc
- uses: Swatinem/rust-cache@v2
- name: Build Windows CLI
run: cargo build --release --package sci-form-cli --target x86_64-pc-windows-msvc
- name: Stage Windows artifact
shell: bash
run: cp target/x86_64-pc-windows-msvc/release/sci-form.exe sci-form-windows-x86_64.exe
- uses: actions/upload-artifact@v6
with:
name: sci-form-windows-x86_64.exe
path: sci-form-windows-x86_64.exe
# ─── Python Wheels ─────────────────────────────────────────────────────────
build-python:
name: Python (${{ matrix.os }})
needs: [wait-for-ci]
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: "3.11"
cache: pip
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Install maturin
run: pip install maturin
- name: Build wheel
run: cd crates/python && maturin build --release --features "${PYTHON_BINDING_FEATURES}"
- uses: actions/upload-artifact@v6
with:
name: python-wheel-${{ matrix.os }}
path: target/wheels/*.whl
# ─── WASM / npm ────────────────────────────────────────────────────────────
build-wasm:
name: WASM / npm
needs: [wait-for-ci]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown
- 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 packages (web + nodejs)
run: cd crates/wasm && bash build.sh --web-features "${WASM_WEB_FEATURES}" --node-features "${WASM_NODE_FEATURES}"
- name: Check alpha/beta subpath packaging
run: |
test -f crates/wasm/pkg/alpha/index.js
test -f crates/wasm/pkg/alpha/index.d.ts
test -f crates/wasm/pkg/beta/index.js
test -f crates/wasm/pkg/beta/index.d.ts
- name: Verify Node.js install
run: cd crates/wasm/pkg-node && node -e "const sci = require('./sci_form_wasm.js'); const alpha = require('./alpha'); const beta = require('./beta'); const r = JSON.parse(sci.embed('CCO', 42)); console.assert(r.num_atoms === 9, 'Expected 9 atoms'); console.assert(typeof sci.alpha_compute_dft === 'function', 'missing alpha root export'); console.assert(typeof sci.beta_compute_kpm_dos === 'function', 'missing beta root export'); console.assert(typeof alpha.alpha_modules_info === 'function', 'missing alpha subpath export'); console.assert(typeof beta.beta_modules_info === 'function', 'missing beta subpath export'); console.log('✓ Node.js WASM works with alpha/beta exports');"
- uses: actions/upload-artifact@v6
with:
name: wasm-pkg
path: crates/wasm/pkg/
- uses: actions/upload-artifact@v6
with:
name: wasm-pkg-node
path: crates/wasm/pkg-node/
# ─── Verify: Python wheel installs cleanly ─────────────────────────────────
verify-python:
name: Verify Python install
needs: [build-python]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: "3.11"
- uses: actions/download-artifact@v8
with:
name: python-wheel-ubuntu-latest
path: wheels/
- name: Install wheel
run: pip install wheels/*.whl
- name: Smoke test — embed caffeine
run: |
python3 -c "
import sci_form
result = sci_form.embed('Cn1cnc2c1c(=O)n(c(=O)n2C)C', 42)
assert result.num_atoms > 0, 'embed failed'
assert len(result.coords) == result.num_atoms * 3, 'coords size mismatch'
assert result.error is None, f'error: {result.error}'
print(f'OK: {result.num_atoms} atoms embedded')
"
- name: Smoke test — batch embed
run: |
python3 -c "
import sci_form
smiles = ['CCO', 'c1ccccc1', 'CC(=O)O', 'C1CCCCC1']
results = [sci_form.embed(s, 42) for s in smiles]
failed = [r.smiles for r in results if r.error is not None]
assert not failed, f'Failed: {failed}'
print(f'OK: {len(results)} molecules embedded')
"
- name: Smoke test — experimental Python bindings
run: |
python3 -c "
from sci_form.alpha import dft_calculate, reaxff_gradient, alpha_compute_aevs
from sci_form.beta import kpm_dos, eht_randnla, cpm_charges
import sci_form
result = sci_form.embed('CCO', 42)
assert result.error is None, result.error
dft = dft_calculate(result.elements, result.coords, 'pbe')
reax = reaxff_gradient(result.elements, result.coords)
aevs = alpha_compute_aevs(result.elements, result.coords)
kpm = kpm_dos(result.elements, result.coords, order=64)
randnla = eht_randnla(result.elements, result.coords, sketch_size=8)
cpm = cpm_charges(result.elements, result.coords, potential=0.0)
assert dft.n_basis > 0
assert len(reax.gradient) == len(result.coords)
assert aevs.n_atoms == result.num_atoms
assert len(kpm.energies) > 0
assert len(randnla.orbital_energies) > 0
assert len(cpm.charges) == result.num_atoms
print('OK: experimental Python bindings work')
"
# ─── Verify: WASM/Node package installs cleanly ───────────────────────────
verify-node:
name: Verify Node.js install
needs: [build-wasm]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: 24
- uses: actions/download-artifact@v8
with:
name: wasm-pkg-node
path: pkg-node/
- name: Smoke test — embed caffeine (Node.js CJS)
run: |
node -e "
const sci = require('./pkg-node/sci_form_wasm.js');
const alpha = require('./pkg-node/alpha');
const beta = require('./pkg-node/beta');
const result = JSON.parse(sci.embed('Cn1cnc2c1c(=O)n(c(=O)n2C)C', 42));
if (result.error) throw new Error('embed failed: ' + result.error);
if (result.num_atoms !== 24) throw new Error('expected 24 atoms, got ' + result.num_atoms);
if (typeof sci.alpha_compute_dft !== 'function') throw new Error('missing alpha root export');
if (typeof sci.beta_compute_kpm_dos !== 'function') throw new Error('missing beta root export');
if (typeof alpha.alpha_modules_info !== 'function') throw new Error('missing alpha subpath export');
if (typeof beta.beta_modules_info !== 'function') throw new Error('missing beta subpath export');
console.log('OK: ' + result.num_atoms + ' atoms embedded');
"
# ─── Verify: CLI binary runs ───────────────────────────────────────────────
verify-cli:
name: Verify CLI binary
needs: [build-cli-linux]
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v8
with:
name: sci-form-linux-x86_64
path: .
- name: Make executable
run: chmod +x sci-form-linux-x86_64
- name: Smoke test — version
run: ./sci-form-linux-x86_64 --version
- name: Smoke test — embed caffeine
run: |
./sci-form-linux-x86_64 embed "Cn1cnc2c1c(=O)n(c(=O)n2C)C" \
| grep -E '"num_atoms"' || (echo "CLI embed failed"; exit 1)
- name: Smoke test — parse
run: |
./sci-form-linux-x86_64 parse "CCO" \
| grep -iE 'Atoms:' || (echo "CLI parse failed"; exit 1)
# ─── Publish: crates.io ────────────────────────────────────────────────────
publish-crate:
name: Publish to crates.io
needs: [verify-python, verify-node, verify-cli]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Publish sci-form (lib)
run: cargo publish --allow-dirty --token ${{ secrets.CARGO_REGISTRY_TOKEN }}
- name: Wait for crates.io propagation
run: sleep 30
- name: Publish sci-form-cli
run: cd crates/cli && cargo publish --allow-dirty --token ${{ secrets.CARGO_REGISTRY_TOKEN }}
# ─── Publish: PyPI ─────────────────────────────────────────────────────────
publish-python:
name: Publish to PyPI
needs: [verify-python]
runs-on: ubuntu-latest
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v8
with:
pattern: python-wheel-*
path: dist/
merge-multiple: true
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_API_TOKEN }}
skip-existing: true
verbose: true
# ─── Publish: npm ──────────────────────────────────────────────────────────
# NOTE: NPM_TOKEN must be a Granular Access Token with Automation type.
# Classic tokens require OTP even in CI. To create one:
# npmjs.com → Avatar → Access Tokens → Generate New Token
# → Granular Access Token → Expiration → Packages: sci-form-wasm (Read+Write)
# → Token type: Automation → Generate Token
# Then update the NPM_TOKEN repository secret with the new token.
publish-npm:
name: Publish to npm
needs: [verify-node]
runs-on: ubuntu-latest
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v8
with:
name: wasm-pkg
path: pkg/
- uses: actions/setup-node@v6
with:
node-version: 24
registry-url: https://registry.npmjs.org
- name: Publish
run: cd pkg && npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
# ─── GitHub Release ────────────────────────────────────────────────────────
release:
name: Create GitHub Release
needs: [publish-crate, publish-python, publish-npm]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/download-artifact@v8
with:
path: artifacts/
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
files: |
artifacts/sci-form-linux-x86_64/*
artifacts/sci-form-linux-aarch64/*
artifacts/sci-form-macos-x86_64/*
artifacts/sci-form-macos-aarch64/*
artifacts/sci-form-windows-x86_64.exe/*
artifacts/python-wheel-*/*.whl
generate_release_notes: true
body: |
## Installation
### Python
```bash
pip install sciforma
```
### Node.js / TypeScript
```bash
npm install sci-form-wasm
```
### Rust
```toml
[dependencies]
sci-form = "0.1"
```
### CLI
Download the binary for your platform from the assets below.
- **Linux x86_64**: `sci-form-linux-x86_64`
- **Linux aarch64**: `sci-form-linux-aarch64`
- **macOS x86_64**: `sci-form-macos-x86_64`
- **macOS Apple Silicon**: `sci-form-macos-aarch64`
- **Windows x86_64**: `sci-form-windows-x86_64.exe`