chore(deps): refresh every ecosystem; .NET on the 10.0.x build SDK #33
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| # Single unified release workflow. One `git tag v5.x.y && git push --tags` | |
| # fires this and ships every artifact in the monorepo. | |
| # | |
| # Flow (two phases — all builds must pass before any publish runs): | |
| # | |
| # validate | |
| # │ | |
| # ├─ build-wasm | |
| # ├─ build-ui (needs build-wasm) | |
| # ├─ python-build-wheels (matrix) | |
| # ├─ python-build-sdist | |
| # ├─ python-test | |
| # ├─ go-build-staticlib (matrix) | |
| # └─ node-build-prebuild (matrix) | |
| # ↓ all builds + tests green | |
| # publish-crate ← single chokepoint; nothing publishes if any build above fails | |
| # │ | |
| # ├─ publish-wasm (downloads build-wasm artifact, npm publish — no rebuild) | |
| # ├─ publish-python (downloads wheel/sdist artifacts, PyPI upload) | |
| # ├─ publish-go (downloads go-staticlib artifacts, tag push) | |
| # └─ publish-node (downloads node-prebuild artifacts, napi prepublish + npm publish) | |
| # ↓ publish-wasm done | |
| # publish-ui (downloads build-ui artifact, npm publish) | |
| # | |
| # github-release runs in PARALLEL with publish-crate (gated on the same | |
| # build phase, not on any publish job) so a flake in one registry | |
| # (Maven Central, PyPI, etc.) can't strand the release page with zero | |
| # assets. The C binding has no other distribution channel, so the | |
| # release page must populate independently of registry health. | |
| # | |
| # Why the build-then-publish split: once `publish-crate` succeeds, the | |
| # core is on crates.io and can only be yanked, not deleted. The old | |
| # layout had publish-wasm + publish-ui doing BUILD+PUBLISH in the same | |
| # job, so a build break after publish-crate would leave a partial | |
| # release. Now every binding produces its publishable artifact in a | |
| # pure build job; publish-crate waits on every build to succeed before | |
| # anything ships to a registry, and the publish jobs themselves just | |
| # download + push. | |
| # | |
| # The Go binding ships as a Git tag (`bindings/go/vX.Y.Z`) rather than | |
| # to a package registry — same gate applies. The C ABI matrix is | |
| # reusable: future PHP and JVM bindings will hang off the same | |
| # staticlib/cdylib outputs and slot into the same build-then-publish | |
| # topology. | |
| on: | |
| push: | |
| tags: | |
| - 'v*' | |
| # Manual rerun for when a transient failure (npm hiccup, network blip, | |
| # PyPI maintenance) caused a partial release. Trigger via the Actions | |
| # UI ("Run workflow" → select tag) or: | |
| # gh workflow run release.yml --ref v5.0.0 | |
| # The validate job's tag-match check enforces that the chosen ref is | |
| # actually a `v*` tag whose name matches the core's Cargo.toml version. | |
| workflow_dispatch: | |
| # Cancel-in-progress is deliberately OFF: a half-cancelled release is | |
| # worse than two sequential ones. If two tags land back-to-back, the | |
| # second waits for the first; that's the right tradeoff for a pipeline | |
| # that touches crates.io / npm / PyPI and can't easily reverse partial | |
| # publishes. | |
| concurrency: | |
| group: release-${{ github.ref }} | |
| cancel-in-progress: false | |
| # Read-only by default. This workflow holds publish credentials for nine | |
| # registries, so the blast radius of any one compromised step should stop | |
| # at that step. Only two jobs write to this repo and both opt in for | |
| # themselves: | |
| # | |
| # - publish-go pushes the `bindings/go/vX.Y.Z` module tag | |
| # - github-release creates the release and uploads assets | |
| # | |
| # Everything else (validate, the build matrix, publish-crate, and the | |
| # npm / PyPI / NuGet / Maven / Packagist publishers) authenticates to its | |
| # registry with a dedicated secret and needs no repo write at all. | |
| # publish-php pushes to GoPlasmatic/datalogic-php using a separate | |
| # fine-grained PAT, which the GITHUB_TOKEN's scope never covered anyway. | |
| permissions: | |
| contents: read | |
| jobs: | |
| validate: | |
| name: Validate | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version: ${{ steps.version.outputs.version }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Extract version | |
| id: version | |
| run: | | |
| VERSION=$(grep '^version = ' crates/datalogic-rs/Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/') | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| echo "crates/datalogic-rs/Cargo.toml version: $VERSION" | |
| - name: Validate tag matches version | |
| run: | | |
| TAG_VERSION="${GITHUB_REF_NAME#v}" | |
| VERSION="${{ steps.version.outputs.version }}" | |
| if [ "$TAG_VERSION" != "$VERSION" ]; then | |
| echo "::error::Tag $GITHUB_REF_NAME does not match crates/datalogic-rs/Cargo.toml version $VERSION" | |
| exit 1 | |
| fi | |
| echo "Tag $GITHUB_REF_NAME matches crates/datalogic-rs/Cargo.toml version $VERSION" | |
| # All bindings share the same `v*` tag as core, so their version | |
| # fields must agree. Catch any forgotten bump here, BEFORE core is | |
| # published and the tag is impossible to retract cleanly. Loop | |
| # over every (path, extractor, label) triple in one step rather | |
| # than per-binding to keep the surface single-purpose. | |
| - name: Validate binding versions match core | |
| run: | | |
| VERSION="${{ steps.version.outputs.version }}" | |
| FAIL=0 | |
| check() { | |
| local label="$1" | |
| local actual="$2" | |
| local file="$3" | |
| if [ "$actual" != "$VERSION" ]; then | |
| echo "::error file=${file}::${label} version is '${actual}', expected '${VERSION}' (bump together with core)" | |
| FAIL=1 | |
| else | |
| echo " ${label}: $actual ✓" | |
| fi | |
| } | |
| # Cargo.toml `version = "..."` for each Rust-side binding. | |
| for crate in bindings/python/Cargo.toml \ | |
| bindings/wasm/Cargo.toml \ | |
| bindings/c/Cargo.toml \ | |
| bindings/node/Cargo.toml; do | |
| v=$(grep '^version = ' "$crate" | head -1 | sed 's/version = "\(.*\)"/\1/') | |
| check "$crate" "$v" "$crate" | |
| done | |
| # Python distribution name (pyproject.toml), UI npm package | |
| # (package.json), and Node binding package (package.json) each | |
| # carry their own version field. | |
| PY=$(grep '^version = ' bindings/python/pyproject.toml | head -1 | sed 's/version = "\(.*\)"/\1/') | |
| check "bindings/python/pyproject.toml" "$PY" "bindings/python/pyproject.toml" | |
| UI=$(node -p "require('./ui/package.json').version") | |
| check "ui/package.json" "$UI" "ui/package.json" | |
| NODE_PKG=$(node -p "require('./bindings/node/package.json').version") | |
| check "bindings/node/package.json" "$NODE_PKG" "bindings/node/package.json" | |
| # .NET binding: <Version> in the csproj. | |
| DOTNET=$(grep -oP '(?<=<Version>)[^<]+' bindings/dotnet/src/Datalogic/Datalogic.csproj | head -1) | |
| check "bindings/dotnet/src/Datalogic/Datalogic.csproj" "$DOTNET" "bindings/dotnet/src/Datalogic/Datalogic.csproj" | |
| # JVM binding: top-level <version> in pom.xml (first match — | |
| # parent/dep versions appear later and don't matter here). | |
| JVM=$(grep -oP '(?<=<version>)[^<]+' bindings/jvm/pom.xml | head -1) | |
| check "bindings/jvm/pom.xml" "$JVM" "bindings/jvm/pom.xml" | |
| # PHP binding: composer.json doesn't carry a version field | |
| # (Packagist resolves from tags). Nothing to check. | |
| # Go module: version lives implicitly in the `bindings/go/vX.Y.Z` | |
| # tag pushed by publish-go — nothing in source to drift, so it's | |
| # not checked here. | |
| if [ "$FAIL" != 0 ]; then | |
| echo "::error::One or more binding versions drift from core ($VERSION). Bump them together." | |
| exit 1 | |
| fi | |
| echo "All binding versions match core $VERSION" | |
| # Catch the common "tag pushed without finalising CHANGELOG" mistake. | |
| # The github-release job below extracts notes from CHANGELOG.md by | |
| # section header; a missing or "- TBD" / "- Unreleased" header means | |
| # we'd ship a release with placeholder notes. Fail loudly here, | |
| # before crates.io publication makes the tag impossible to retract. | |
| - name: Validate CHANGELOG.md entry | |
| run: | | |
| VERSION="${{ steps.version.outputs.version }}" | |
| HEADER=$(grep -E "^## \[${VERSION}\]" CHANGELOG.md | head -1 || true) | |
| if [ -z "$HEADER" ]; then | |
| echo "::error file=CHANGELOG.md::No '## [${VERSION}]' section found" | |
| exit 1 | |
| fi | |
| # Match `## [X.Y.Z] - YYYY-MM-DD`. Reject TBD/Unreleased/missing date. | |
| if ! echo "$HEADER" | grep -Eq "^## \[${VERSION}\] - [0-9]{4}-[0-9]{2}-[0-9]{2}\s*$"; then | |
| echo "::error file=CHANGELOG.md::CHANGELOG entry '${HEADER}' is missing a YYYY-MM-DD release date (stamp it before tagging)" | |
| exit 1 | |
| fi | |
| echo "CHANGELOG.md entry: ${HEADER} ✓" | |
| # Same composite as ci.yml's check job — a release must not validate | |
| # against a narrower lint surface than a PR does, and sharing one | |
| # action makes that structural instead of a mirrored block to keep in | |
| # sync. Also sets up the toolchain and cache the test step below uses. | |
| - name: Lint (fmt + clippy, every manifest) | |
| uses: ./.github/actions/rust-lint | |
| - name: Run tests | |
| # `--all-features` is required: most integration tests are gated | |
| # behind `serde_json` and the JSONLogic suite runner needs | |
| # `templating`. Without it, those tests silently skip and the | |
| # step "passes" without exercising them. Matches the | |
| # `cargo test --workspace --all-features` in ci.yml's check job. | |
| run: cargo test --workspace --all-features | |
| publish-crate: | |
| name: Publish core to crates.io | |
| # Gate: every binding's build phase must succeed before we touch | |
| # crates.io. Once this job runs, crates.io has the new version and | |
| # we can't un-publish — so everything that could fail downstream | |
| # must have already passed. Each `build-*` here is a reusable | |
| # workflow that wraps that binding's build/test jobs; if any of | |
| # them fails, this gate never opens and no bindings publish. | |
| needs: | |
| - validate | |
| - build-wasm | |
| - build-ui | |
| - build-python | |
| - build-go | |
| - build-node | |
| - build-c-cdylib | |
| - build-dotnet | |
| - build-jvm | |
| - build-php | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Setup Rust | |
| uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 # stable | |
| - name: Check if version exists on crates.io | |
| id: check | |
| # `cargo search` greps a prefix match, which can return a | |
| # different crate that happens to start with `datalogic-rs`. The | |
| # registry API is an exact lookup: 200 + `versions[].num` array | |
| # gives us the authoritative answer. | |
| run: | | |
| VERSION="${{ needs.validate.outputs.version }}" | |
| if curl -sf "https://crates.io/api/v1/crates/datalogic-rs/${VERSION}" >/dev/null; then | |
| echo "Version $VERSION already published to crates.io, skipping" | |
| echo "skip=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "Publishing version $VERSION to crates.io" | |
| echo "skip=false" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Publish | |
| if: steps.check.outputs.skip != 'true' | |
| env: | |
| CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_IO_TOKEN }} | |
| # The pre-check above can fail to detect an already-published | |
| # version under crates.io API flakiness or shared-runner rate | |
| # limiting (we hit this on v5.0.0). Treat `cargo publish`'s | |
| # "already exists" error as success — it's the authoritative | |
| # source and matches what every other publish job in this file | |
| # does (npm view, --skip-duplicate, skip-existing, etc.). | |
| run: | | |
| set -o pipefail | |
| if cargo publish --manifest-path crates/datalogic-rs/Cargo.toml 2>&1 | tee /tmp/cargo-publish.log; then | |
| exit 0 | |
| fi | |
| if grep -q 'already exists on crates.io index' /tmp/cargo-publish.log; then | |
| echo "::notice::datalogic-rs already on crates.io; treating as success." | |
| exit 0 | |
| fi | |
| exit 1 | |
| # =========================================================== Build phase | |
| # Each `build-*` job is a thin call into a reusable workflow that owns | |
| # that binding's build/test concerns. The reusable workflows live in | |
| # .github/workflows/release-build-*.yml. Artifacts they upload | |
| # (wasm-pkg, ui-package, python-wheels-*, python-sdist, go-staticlib-*) | |
| # are accessible to the publish jobs below — workflow_call artifacts | |
| # are scoped to the calling workflow's run. | |
| # | |
| # All four build jobs run in parallel after validate. publish-crate | |
| # waits on all of them; nothing publishes if any binding's build | |
| # phase fails. | |
| build-wasm: | |
| name: Build WASM | |
| needs: validate | |
| uses: ./.github/workflows/release-build-wasm.yml | |
| publish-wasm: | |
| name: Publish WASM to npm | |
| # publish-crate gates the entire publish phase; this job's actual | |
| # input is the build-wasm artifact. | |
| needs: [validate, publish-crate, build-wasm] | |
| runs-on: ubuntu-latest | |
| # `id-token: write` lets `npm publish --provenance` generate the | |
| # SLSA build-provenance attestation tied to this workflow run. | |
| # `contents: read` is needed because npm provenance includes a | |
| # workflow-source link. | |
| permissions: | |
| id-token: write | |
| contents: read | |
| steps: | |
| - name: Download wasm-pkg artifact | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: wasm-pkg | |
| path: bindings/wasm/pkg/ | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: '22' | |
| registry-url: 'https://registry.npmjs.org' | |
| - name: Check if version exists on npm | |
| id: check | |
| run: | | |
| VERSION="${{ needs.validate.outputs.version }}" | |
| if npm view "@goplasmatic/datalogic-wasm@$VERSION" version 2>/dev/null; then | |
| echo "Version $VERSION already published to npm, skipping" | |
| echo "skip=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "Publishing version $VERSION to npm" | |
| echo "skip=false" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Publish to npm | |
| if: steps.check.outputs.skip != 'true' | |
| run: cd bindings/wasm/pkg && npm publish --access public --provenance | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| build-ui: | |
| name: Build UI | |
| needs: [validate, build-wasm] | |
| uses: ./.github/workflows/release-build-ui.yml | |
| with: | |
| version: ${{ needs.validate.outputs.version }} | |
| publish-ui: | |
| name: Publish UI to npm | |
| # publish-wasm before publish-ui because the published UI's | |
| # dependency on @goplasmatic/datalogic-wasm@$VERSION must be resolvable | |
| # on npm at install time. | |
| needs: [validate, publish-crate, build-ui, publish-wasm] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| id-token: write | |
| contents: read | |
| steps: | |
| - name: Download ui-package artifact | |
| # `actions/upload-artifact@v4+` strips the common parent path | |
| # when every uploaded path shares a prefix (every entry in | |
| # release-build-ui.yml's upload starts with `ui/`, so the | |
| # artifact actually contains `package.json`, `dist/`, etc. at | |
| # root — without the `ui/` wrapper). Download into `ui/` | |
| # explicitly so the subsequent `cd ui && npm publish` works. | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: ui-package | |
| path: ui | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: '22' | |
| registry-url: 'https://registry.npmjs.org' | |
| - name: Check if version exists on npm | |
| id: check | |
| run: | | |
| VERSION="${{ needs.validate.outputs.version }}" | |
| if npm view "@goplasmatic/datalogic-ui@$VERSION" version 2>/dev/null; then | |
| echo "Version $VERSION already published to npm, skipping" | |
| echo "skip=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "Publishing version $VERSION to npm" | |
| echo "skip=false" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Publish to npm | |
| if: steps.check.outputs.skip != 'true' | |
| # `--ignore-scripts` is critical: ui/package.json defines a | |
| # `prepublishOnly` that re-runs `npm run build:lib`, which | |
| # invokes `prebuild:lib` → `sync-wasm` → `cp -R | |
| # ../bindings/wasm/pkg vendor/datalogic`. The publish-ui job | |
| # only downloads the prebuilt `ui-package` artifact (which | |
| # already contains `dist/`); the sibling `bindings/wasm/pkg` | |
| # source tree isn't present, so sync-wasm errors out with | |
| # "cannot stat". Skipping scripts publishes the already-built | |
| # tree as-is, which is what we want. | |
| run: cd ui && npm publish --access public --provenance --ignore-scripts | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| build-python: | |
| name: Build Python | |
| needs: validate | |
| uses: ./.github/workflows/release-build-python.yml | |
| publish-python: | |
| name: Publish Python to PyPI | |
| needs: [validate, publish-crate, build-python] | |
| runs-on: ubuntu-latest | |
| # `pypi` GitHub Environment provides the manual approval gate | |
| # (configure in repo Settings → Environments → pypi → required | |
| # reviewers). A stray tag push won't publish unattended. | |
| environment: | |
| name: pypi | |
| url: https://pypi.org/p/datalogic-py | |
| # OIDC trusted publishing — requires a PyPI publisher (or pending | |
| # publisher) configured for this repo + workflow file + environment. | |
| # No API token used. | |
| permissions: | |
| id-token: write | |
| steps: | |
| - name: Download all Python artifacts | |
| uses: actions/download-artifact@v8 | |
| with: | |
| path: dist | |
| pattern: python-* | |
| merge-multiple: true | |
| - name: Publish to PyPI | |
| uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247 # release/v1 | |
| with: | |
| packages-dir: dist | |
| # `skip-existing` avoids a hard failure if a wheel was already | |
| # uploaded by an earlier (partial) run for the same version. | |
| skip-existing: true | |
| build-go: | |
| name: Build Go staticlibs | |
| needs: validate | |
| uses: ./.github/workflows/release-build-go.yml | |
| build-node: | |
| name: Build Node prebuilds | |
| needs: validate | |
| uses: ./.github/workflows/release-build-node.yml | |
| # Shared C-ABI cdylib matrix. Java, .NET, and PHP all consume these | |
| # artifacts via their respective packaging jobs. Go has its own | |
| # staticlib matrix in build-go above; both run in parallel after | |
| # validate. | |
| build-c-cdylib: | |
| name: Build C ABI cdylib (matrix) | |
| needs: validate | |
| uses: ./.github/workflows/release-build-c-cdylib.yml | |
| build-dotnet: | |
| name: Build .NET NuGet | |
| needs: [validate, build-c-cdylib] | |
| uses: ./.github/workflows/release-build-dotnet.yml | |
| build-jvm: | |
| name: Build JVM JAR | |
| needs: [validate, build-c-cdylib] | |
| uses: ./.github/workflows/release-build-jvm.yml | |
| build-php: | |
| name: Build PHP distribution | |
| needs: [validate, build-c-cdylib] | |
| uses: ./.github/workflows/release-build-php.yml | |
| # ============================================================ Publish phase | |
| # Everything below runs only after publish-crate succeeds — and | |
| # publish-crate runs only when every `build-*` is green. The | |
| # publish-* jobs are pure download + push: no rebuild, so a publish | |
| # failure here can only be a transient registry issue. | |
| publish-go: | |
| name: Publish Go module tag | |
| needs: [validate, publish-crate, build-go] | |
| runs-on: ubuntu-latest | |
| # The Go binding ships as a Git tag rather than to a registry, so this | |
| # is one of only two jobs that needs to write to the repo (see the | |
| # top-level `permissions` note). It pushes `bindings/go/vX.Y.Z` and | |
| # nothing else — the synthetic commit never lands on a branch. | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| # Need the full tag commit to base our synthetic release commit on. | |
| fetch-depth: 0 | |
| - name: Download all staticlibs | |
| uses: actions/download-artifact@v8 | |
| with: | |
| path: artifacts/ | |
| pattern: go-staticlib-* | |
| # merge-multiple: each artifact's tarball roots at <os>_<arch>/, | |
| # so merging puts every platform dir directly under artifacts/. | |
| merge-multiple: true | |
| - name: Stage staticlibs into bindings/go/lib/ | |
| run: | | |
| mkdir -p bindings/go/lib | |
| for dir in artifacts/*/; do | |
| name=$(basename "$dir") | |
| mkdir -p "bindings/go/lib/${name}" | |
| cp "${dir}/libdatalogic_c.a" "bindings/go/lib/${name}/" | |
| done | |
| echo "Staged platform directories:" | |
| ls bindings/go/lib/ | |
| # The cbindgen-generated header is platform-agnostic — it has no | |
| # #ifdef'd content — so we generate it once on this Linux runner | |
| # rather than collecting it from every matrix output. The composite | |
| # action does the cargo build for us as a side effect. | |
| - uses: ./.github/actions/c-abi-host | |
| - name: Stage header into bindings/go/include/ | |
| run: | | |
| mkdir -p bindings/go/include | |
| cp bindings/c/include/datalogic.h bindings/go/include/ | |
| - name: Commit and push Go module tag | |
| run: | | |
| VERSION="${{ needs.validate.outputs.version }}" | |
| TAG="bindings/go/v${VERSION}" | |
| # Skip if the tag already exists on origin (idempotency for | |
| # `gh run rerun` and `workflow_dispatch` against a tag whose | |
| # publish-go cell succeeded in an earlier attempt). Without | |
| # this, `git tag ${TAG}` errors and the job fails even though | |
| # the Go module proxy already has the right artifact set. | |
| if git ls-remote --tags --exit-code origin "refs/tags/${TAG}" >/dev/null 2>&1; then | |
| echo "::notice::Tag ${TAG} already on origin; skipping push." | |
| exit 0 | |
| fi | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| # `-f` because bindings/go/.gitignore excludes these dirs on main. | |
| # The release tag is the only place they ever exist in the tree. | |
| git add -f bindings/go/lib/ bindings/go/include/ | |
| git commit -m "release(go): stage artifacts for ${TAG}" | |
| git tag "${TAG}" | |
| # Push only the tag — the synthetic commit becomes reachable | |
| # through it but never lands on a branch. Main stays binary-free. | |
| git push origin "${TAG}" | |
| # Major-version suffix in the `go get` path is required by Go | |
| # modules for v2+ when a `go.mod` is present (see | |
| # https://go.dev/ref/mod#major-version-suffixes). The on-disk | |
| # `go.mod` declares the module as `.../bindings/go/v5` so the | |
| # suffix here is derived from the major component of VERSION. | |
| GO_MAJOR="v${VERSION%%.*}" | |
| echo "::notice::Go module tag pushed: ${TAG}. End consumers can now run 'go get github.com/GoPlasmatic/datalogic-rs/bindings/go/${GO_MAJOR}@v${VERSION}' (proxy.golang.org indexes the tag within a few minutes)." | |
| publish-node: | |
| name: Publish Node bindings to npm | |
| # publish-crate gates the entire publish phase; this job's actual | |
| # input is the build-node artifacts (one .node per platform). | |
| needs: [validate, publish-crate, build-node] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| id-token: write | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: '22' | |
| registry-url: 'https://registry.npmjs.org' | |
| - name: Install bindings/node deps | |
| working-directory: bindings/node | |
| run: npm ci || npm install | |
| - name: Download all prebuilds | |
| uses: actions/download-artifact@v8 | |
| with: | |
| # Each artifact tarball roots at the .node file directly, so | |
| # merge-multiple drops every prebuild side-by-side into | |
| # bindings/node/. | |
| path: bindings/node/ | |
| pattern: node-prebuild-* | |
| merge-multiple: true | |
| # `napi prepublish -t npm` publishes the per-platform subpackages | |
| # under `bindings/node/npm/<triple>/`, NOT the .node files at root. | |
| # napi-cli creates the npm/ skeleton during `napi build` — but | |
| # only on the same runner that built the .node. Since each .node | |
| # came from a different matrix runner here, we recreate the npm/ | |
| # skeleton on this publish runner (from package.json's | |
| # `napi.targets`) and move each .node into its target subdir. | |
| - name: Create npm subpackage dirs and stage prebuilds | |
| working-directory: bindings/node | |
| run: | | |
| set -euo pipefail | |
| npx napi create-npm-dirs | |
| for f in datalogic-node.*.node; do | |
| [ -f "$f" ] || continue | |
| triple="${f#datalogic-node.}" | |
| triple="${triple%.node}" | |
| if [ -d "npm/${triple}" ]; then | |
| mv "$f" "npm/${triple}/" | |
| echo " staged: $f → npm/${triple}/" | |
| else | |
| echo "::warning::no subpackage dir for triple '${triple}' (not in napi.targets); leaving $f at root" | |
| fi | |
| done | |
| echo "Final npm/ layout:" | |
| ls -la npm/*/ | |
| - name: Check if version exists on npm | |
| id: check | |
| run: | | |
| VERSION="${{ needs.validate.outputs.version }}" | |
| if npm view "@goplasmatic/datalogic-node@$VERSION" version 2>/dev/null; then | |
| echo "Version $VERSION already published to npm, skipping" | |
| echo "skip=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "Publishing version $VERSION to npm" | |
| echo "skip=false" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Publish to npm | |
| if: steps.check.outputs.skip != 'true' | |
| working-directory: bindings/node | |
| # `napi prepublish -t npm` writes per-platform subpackages to | |
| # `npm/<triple>/`, copies the matching .node, and runs | |
| # `npm publish` on each subpackage plus the umbrella package. | |
| # The umbrella declares the subpackages as `optionalDependencies` | |
| # so consumers' npm pulls only the prebuild for their platform. | |
| # | |
| # `NPM_CONFIG_PROVENANCE=true` flips the `--provenance` flag on | |
| # every `npm publish` invocation napi makes (umbrella + each | |
| # `npm/<triple>/` subpackage), so all 9 packages carry a SLSA | |
| # provenance attestation. Needs `id-token: write` (set above). | |
| # | |
| # `NPM_CONFIG_ACCESS=public` is required because the per-platform | |
| # subpackages are first-time publishes; npm 9+ defaults new | |
| # scoped packages (`@goplasmatic/datalogic-node-<triple>`) to | |
| # "restricted" and refuses to generate provenance unless access | |
| # is explicitly set to public: | |
| # npm error EUSAGE Can't generate provenance for new or | |
| # private package, you must set `access` to public. | |
| # napi prepublish has no --access flag; setting NPM_CONFIG_ACCESS | |
| # makes every nested `npm publish` call inherit it. | |
| # | |
| # `--no-gh-release`: the .node binaries ship inside the per-platform | |
| # npm subpackages, so napi's default GitHub-Release asset upload is | |
| # redundant here — and it needs `contents: write`, which this job | |
| # deliberately drops to `contents: read` for npm provenance. Without | |
| # this flag the upload 403s and aborts the whole prepublish before the | |
| # npm publish runs (v5.0.1's node binding silently missed npm as a | |
| # result). | |
| run: npx napi prepublish -t npm --no-gh-release | |
| env: | |
| NPM_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| NPM_CONFIG_PROVENANCE: 'true' | |
| NPM_CONFIG_ACCESS: 'public' | |
| GITHUB_TOKEN: ${{ github.token }} | |
| # `napi prepublish` publishes ONLY the `npm/<triple>/` subpackages; | |
| # the umbrella package itself is published by a plain `npm publish` | |
| # (normally its `prepublishOnly` hook is what runs napi prepublish — | |
| # invoking napi directly, as the step above does, leaves the | |
| # umbrella unpublished). Both 5.0.1 and 5.1.0 shipped their | |
| # umbrella via manual recovery before this step existed. | |
| # `--ignore-scripts` skips the prepublishOnly hook (it just ran); | |
| # napi has already wired the subpackage optionalDependencies into | |
| # package.json in-place. Skip-if-exists keeps reruns idempotent. | |
| - name: Publish umbrella package | |
| working-directory: bindings/node | |
| run: | | |
| VERSION=$(node -p "require('./package.json').version") | |
| if npm view "@goplasmatic/datalogic-node@$VERSION" version >/dev/null 2>&1; then | |
| echo "umbrella $VERSION already on npm, skipping" | |
| else | |
| npm publish --ignore-scripts | |
| fi | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| NPM_CONFIG_PROVENANCE: 'true' | |
| NPM_CONFIG_ACCESS: 'public' | |
| publish-dotnet: | |
| name: Publish .NET to NuGet | |
| needs: [validate, publish-crate, build-dotnet] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/setup-dotnet@v5 | |
| with: | |
| dotnet-version: '10.0.x' | |
| - name: Download .nupkg artifact | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: dotnet-nupkg | |
| path: nupkg/ | |
| - name: Push to NuGet | |
| # `--skip-duplicate` avoids a hard fail if a partial earlier run | |
| # already pushed this version. | |
| run: | | |
| dotnet nuget push 'nupkg/*.nupkg' \ | |
| --api-key "${NUGET_API_KEY}" \ | |
| --source https://api.nuget.org/v3/index.json \ | |
| --skip-duplicate | |
| env: | |
| NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} | |
| publish-jvm: | |
| name: Publish JVM to Maven Central | |
| # `build-jvm` is listed explicitly even though publish-crate already | |
| # transitively depends on it: it makes the dep graph readable at a | |
| # glance ("the JVM smoke must pass before we deploy to Central") and | |
| # prevents a future edit that loosens publish-crate's gates from | |
| # silently dropping the JVM smoke check. | |
| needs: [validate, publish-crate, build-c-cdylib, build-jvm] | |
| runs-on: ubuntu-latest | |
| # `maven-central` GitHub Environment provides a manual-approval | |
| # gate — see Settings → Environments → maven-central → required | |
| # reviewers. Maven Central publishes can't be cleanly retracted, so | |
| # we gate them the same way as PyPI. | |
| environment: | |
| name: maven-central | |
| url: https://central.sonatype.com/artifact/io.github.goplasmatic/datalogic | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-java@v5 | |
| with: | |
| distribution: 'temurin' | |
| java-version: '25' | |
| - name: Check if version exists on Maven Central | |
| id: check | |
| # The Central publishing plugin has no `--skip-duplicate` | |
| # equivalent, and re-deploying an existing coordinate errors out. | |
| # repo1 is the authoritative public mirror; a 200 on the POM means | |
| # the version already published (idempotency for partial-release | |
| # reruns). | |
| run: | | |
| VERSION="${{ needs.validate.outputs.version }}" | |
| if curl -sf "https://repo1.maven.org/maven2/io/github/goplasmatic/datalogic/${VERSION}/datalogic-${VERSION}.pom" >/dev/null; then | |
| echo "Version $VERSION already on Maven Central, skipping" | |
| echo "skip=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "Publishing version $VERSION to Maven Central" | |
| echo "skip=false" >> $GITHUB_OUTPUT | |
| fi | |
| # publish-jvm needs the cdylibs inside the JAR. The build-jvm | |
| # output is an artifact (not a filesystem snapshot), so we | |
| # re-stage from the cdylib matrix outputs here and rebuild before | |
| # `mvn deploy`. This re-runs JAR assembly but it's cheap (no | |
| # cargo) and avoids shipping a fat artifact between jobs. | |
| - name: Download cdylib artifacts | |
| uses: actions/download-artifact@v8 | |
| with: | |
| path: cdylib/ | |
| pattern: c-cdylib-* | |
| merge-multiple: true | |
| # Same classpath-root <os-arch>/ layout the packaged-JAR smoke | |
| # test in release-build-jvm.yml validates — the binding's loader | |
| # does not search META-INF/native/. | |
| - name: Stage native libs under <os-arch>/ (classpath root) | |
| shell: bash | |
| run: bash scripts/stage-jvm-natives.sh cdylib | |
| - name: Configure GPG for signing | |
| env: | |
| GPG_PRIVATE_KEY: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }} | |
| run: | | |
| echo "$GPG_PRIVATE_KEY" | gpg --batch --import | |
| - name: Deploy to Central Portal | |
| if: steps.check.outputs.skip != 'true' | |
| working-directory: bindings/jvm | |
| env: | |
| MAVEN_CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }} | |
| MAVEN_CENTRAL_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} | |
| MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }} | |
| run: | | |
| # Inline server creds into a temporary settings.xml so we | |
| # don't touch the runner's ~/.m2/settings.xml. | |
| cat > settings.xml <<'EOF' | |
| <settings> | |
| <servers> | |
| <server> | |
| <id>central</id> | |
| <username>${env.MAVEN_CENTRAL_USERNAME}</username> | |
| <password>${env.MAVEN_CENTRAL_PASSWORD}</password> | |
| </server> | |
| </servers> | |
| </settings> | |
| EOF | |
| mvn -B -s settings.xml -P release -DskipTests \ | |
| -Dgpg.passphrase="$MAVEN_GPG_PASSPHRASE" \ | |
| deploy | |
| publish-php: | |
| name: Publish PHP (subtree-split + Packagist) | |
| needs: [validate, publish-crate, build-php] | |
| runs-on: ubuntu-latest | |
| # Packagist's public service requires composer.json at the repo | |
| # root, which our monorepo can't provide directly. The fix is the | |
| # standard "subtree split" pattern (same shape Symfony / Doctrine / | |
| # Laravel use): on each v* tag, this job re-stages the built PHP | |
| # tree as the root of a sibling distribution repo | |
| # (GoPlasmatic/datalogic-php), commits, tags, and pushes. Packagist | |
| # watches the distribution repo and picks up the new tag — either | |
| # via webhook ping (if PACKAGIST_TOKEN is configured) or its hourly | |
| # crawler. | |
| steps: | |
| - name: Download PHP distribution | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: php-dist | |
| path: dist/ | |
| - name: Unpack distribution | |
| # build-php's zip contains everything we want at the dist repo's | |
| # root: src/, lib/, composer.json, README.md. Unzip into ./tree. | |
| run: | | |
| mkdir -p tree | |
| unzip -q dist/goplasmatic-datalogic-php.zip -d tree-raw | |
| # The zip wraps everything in a `goplasmatic-datalogic/` dir. | |
| mv tree-raw/goplasmatic-datalogic/* tree/ | |
| ls -la tree/ | |
| - name: Push to GoPlasmatic/datalogic-php as v${{ needs.validate.outputs.version }} | |
| env: | |
| # Fine-grained PAT with Contents:write scoped to the | |
| # GoPlasmatic/datalogic-php repo. The default GITHUB_TOKEN | |
| # can't push across repos, so this is the one secret unique | |
| # to the PHP flow. | |
| PHP_DIST_PUSH_TOKEN: ${{ secrets.PHP_DIST_PUSH_TOKEN }} | |
| VERSION: ${{ needs.validate.outputs.version }} | |
| run: | | |
| set -euo pipefail | |
| if [ -z "${PHP_DIST_PUSH_TOKEN:-}" ]; then | |
| echo "::error::PHP_DIST_PUSH_TOKEN secret is required to push to GoPlasmatic/datalogic-php" | |
| exit 1 | |
| fi | |
| git config --global user.name "github-actions[bot]" | |
| git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| # Clone target with the PAT so push works. Default branch | |
| # assumption: `main`. If empty (just-created repo), git clone | |
| # still works but produces an empty working tree. | |
| git clone --depth=1 \ | |
| "https://x-access-token:${PHP_DIST_PUSH_TOKEN}@github.com/GoPlasmatic/datalogic-php.git" \ | |
| target | |
| cd target | |
| # Replace tree contents wholesale. The dist repo is a derived | |
| # artifact; we don't preserve cross-version diff history at | |
| # file level — one release commit per tag is the contract. | |
| # `git rm -rf .` cleans everything tracked; rsync then drops | |
| # the new tree in place. | |
| git rm -rf . 2>/dev/null || true | |
| # Anything untracked (e.g. .git/) survives `git rm`. Remove | |
| # leftover files except the .git dir to avoid stale content. | |
| find . -mindepth 1 -maxdepth 1 ! -name '.git' -exec rm -rf {} + | |
| cp -r ../tree/. . | |
| git add -A | |
| # Skip empty commits (e.g. a rerun of the same tag). | |
| if git diff --cached --quiet; then | |
| echo "Distribution tree unchanged for v${VERSION}; skipping commit." | |
| else | |
| git commit -m "release: v${VERSION}" | |
| fi | |
| # Tag must be unique; force-update if a partial earlier run | |
| # left a stale tag pointing at an older commit. | |
| git tag -f "v${VERSION}" | |
| git push origin HEAD:main | |
| git push origin "v${VERSION}" --force | |
| echo "::notice::Pushed v${VERSION} to GoPlasmatic/datalogic-php" | |
| - name: Notify Packagist (optional) | |
| # If PACKAGIST_USERNAME / PACKAGIST_TOKEN are configured, ping | |
| # the Packagist API to refresh the package immediately. Without | |
| # them, Packagist's hourly crawler picks up the tag within ~1h. | |
| # | |
| # `continue-on-error: true` because Packagist's API can return | |
| # 404 when the package isn't yet registered on packagist.org | |
| # (first release of a new package), or transiently. The actual | |
| # release work (push to GoPlasmatic/datalogic-php) is done by | |
| # the previous step; Packagist's crawler will sync within ~1h | |
| # regardless. Don't fail the whole job on a "nice-to-have" | |
| # cache-warming ping. | |
| if: ${{ env.PACKAGIST_USERNAME != '' && env.PACKAGIST_TOKEN != '' }} | |
| continue-on-error: true | |
| env: | |
| PACKAGIST_USERNAME: ${{ secrets.PACKAGIST_USERNAME }} | |
| PACKAGIST_TOKEN: ${{ secrets.PACKAGIST_TOKEN }} | |
| run: | | |
| curl -fsSL -XPOST \ | |
| -H 'content-type:application/json' \ | |
| "https://packagist.org/api/update-package?username=${PACKAGIST_USERNAME}&apiToken=${PACKAGIST_TOKEN}" \ | |
| -d '{"repository":{"url":"https://github.com/GoPlasmatic/datalogic-php"}}' | |
| github-release: | |
| name: Create GitHub Release | |
| # Gate on the BUILD phase only, not the publish phase. Once every | |
| # binding has produced its artifact we have enough to attach to the | |
| # release — whether each registry (PyPI, Maven Central, NuGet, npm, | |
| # Packagist, etc.) accepts the upload is independent of whether the | |
| # binaries should be discoverable on the release page. A flake in | |
| # one registry (e.g. Maven Central, which is well-known unreliable) | |
| # used to skip this job entirely and leave the release with no | |
| # assets at all; the C binding has no other distribution channel, | |
| # so an empty release blocks every downstream C/C++/Go consumer. | |
| needs: | |
| - validate | |
| - build-wasm | |
| - build-ui | |
| - build-python | |
| - build-go | |
| - build-node | |
| - build-c-cdylib | |
| - build-dotnet | |
| - build-jvm | |
| - build-php | |
| runs-on: ubuntu-latest | |
| # The workflow-level grant is contents-only; creating the linked | |
| # Discussions announcement additionally needs discussions:write | |
| # (GitHub reports the missing scope as a 404 "Discussion could not | |
| # be created", which is how v5.1.0's first run of this step failed). | |
| permissions: | |
| contents: write | |
| discussions: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| # Extract just this version's section from CHANGELOG.md. Awk reads | |
| # lines between `## [X.Y.Z]` and the next `## [` header (exclusive) | |
| # — the validate job already enforced that the section exists and | |
| # carries a real date, so we can rely on it being present. | |
| - name: Extract release notes from CHANGELOG.md | |
| id: notes | |
| run: | | |
| VERSION="${{ needs.validate.outputs.version }}" | |
| awk -v ver="$VERSION" ' | |
| $0 ~ "^## \\[" ver "\\]" { found=1; next } | |
| found && /^## \[/ { exit } | |
| found { print } | |
| ' CHANGELOG.md > release-notes.md | |
| if [ ! -s release-notes.md ]; then | |
| echo "::error::Failed to extract CHANGELOG section for $VERSION (validate should have caught this)" | |
| exit 1 | |
| fi | |
| # Standard syndication footer (see .github/LAUNCH-PLAYBOOK.md | |
| # "Ongoing"). Conformance stat comes from the script so the | |
| # footer can never drift from the actual suite count. | |
| CONFORMANCE=$(bash scripts/conformance-count.sh) | |
| cat >> release-notes.md <<EOF | |
| --- | |
| Every binding passes the same conformance battery: **${CONFORMANCE}**. | |
| Try rules live in the [playground](https://goplasmatic.github.io/datalogic-rs/playground/). | |
| Running datalogic-rs in production? [Add your project](https://github.com/GoPlasmatic/datalogic-rs/discussions/categories/show-and-tell). | |
| EOF | |
| echo "Release notes preview:" | |
| head -20 release-notes.md | |
| # Download every build-phase artifact uploaded by the build-* jobs | |
| # earlier in the run. We attach these to the GitHub Release as assets | |
| # so air-gapped consumers can grab the .whl / .tgz / .node prebuilds | |
| # without going through PyPI/npm. crates.io tarball isn't attached | |
| # — `cargo publish` is the canonical source there. | |
| - name: Download release artifacts | |
| uses: actions/download-artifact@v8 | |
| with: | |
| path: release-assets | |
| # Skip `python-sdist` and `python-wheels-*` only if you want | |
| # PyPI to be the single source; leaving them in lets users | |
| # pip-install a wheel from the release page without PyPI. | |
| pattern: '*' | |
| - name: Stage assets into a flat dir | |
| run: | | |
| mkdir -p assets | |
| # WASM npm tarball | |
| if [ -d release-assets/wasm-pkg ]; then | |
| cd release-assets/wasm-pkg && npm pack --pack-destination ../../assets && cd ../.. | |
| fi | |
| # UI npm tarball — artifact roots at ui/, repack from there | |
| if [ -d release-assets/ui-package/ui ]; then | |
| cd release-assets/ui-package/ui && npm pack --pack-destination ../../../assets && cd ../../.. | |
| fi | |
| # Python wheels + sdist | |
| find release-assets -type f \( -name '*.whl' -o -name '*.tar.gz' \) \ | |
| -exec cp {} assets/ \; | |
| # Node prebuilds (one .node per platform) | |
| find release-assets -type f -name 'datalogic-node.*.node' \ | |
| -exec cp {} assets/ \; | |
| # Go staticlibs — bundle each platform dir into a tarball. | |
| # The artifact has a single `<os>_<arch>/` subdir; drop the | |
| # cbindgen header alongside the .a so the tarball is | |
| # self-contained for downstream cgo / direct-C consumers. | |
| if [ -d release-assets ]; then | |
| for d in release-assets/go-staticlib-*/; do | |
| [ -d "$d" ] || continue | |
| name=$(basename "$d") | |
| for plat in "$d"*/; do | |
| [ -d "$plat" ] || continue | |
| cp bindings/c/include/datalogic.h "$plat" | |
| done | |
| tar -C "$d" -czf "assets/${name}.tar.gz" . | |
| done | |
| fi | |
| # C ABI cdylibs — same pattern as Go staticlibs above. | |
| # Header bundled per-platform so downstream C/C++/non-Rust | |
| # projects get one self-contained tarball per target. | |
| if [ -d release-assets ]; then | |
| for d in release-assets/c-cdylib-*/; do | |
| [ -d "$d" ] || continue | |
| name=$(basename "$d") | |
| for plat in "$d"*/; do | |
| [ -d "$plat" ] || continue | |
| cp bindings/c/include/datalogic.h "$plat" | |
| done | |
| tar -C "$d" -czf "assets/${name}.tar.gz" . | |
| done | |
| fi | |
| # .NET NuGet packages. | |
| find release-assets -type f -name '*.nupkg' -exec cp {} assets/ \; | |
| # JVM JARs. | |
| find release-assets -type f -name 'datalogic-*.jar' -exec cp {} assets/ \; | |
| # PHP distribution zip. | |
| find release-assets -type f -name '*.zip' -exec cp {} assets/ \; | |
| echo "Final asset set:" | |
| ls -la assets/ | |
| - name: Create or update release | |
| run: | | |
| VERSION="${{ needs.validate.outputs.version }}" | |
| if gh release view "v$VERSION" >/dev/null 2>&1; then | |
| # Release already exists (partial-release rerun): refresh assets | |
| # in place instead of failing on "already exists". | |
| gh release upload "v$VERSION" assets/* --clobber | |
| else | |
| gh release create "v$VERSION" \ | |
| --title "v$VERSION" \ | |
| --notes-file release-notes.md \ | |
| --discussion-category "Announcements" \ | |
| --verify-tag \ | |
| assets/* | |
| fi | |
| env: | |
| GH_TOKEN: ${{ github.token }} |