Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions .github/workflows/publish-to-crates-io.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Publishes the scry pure-library crates to crates.io on every `v*` tag push,
# in parallel with release.yml (signed wasm component + SBOMs + MC/DC evidence).
#
# Publishes only the reusable libraries — scry-interval, scry-taint,
# scry-octagon, scry-provenance, scry-analyze-core. The Wasm-component crates
# (wasm-lattice, scry-analyzer) and the test / MC-DC harnesses are
# `publish = false` and ship as signed `.wasm` release assets instead (the
# witness precedent: publish the libs, ship the component).
#
# Trust model: an org-wide CRATES_IO_TOKEN secret (set at the pulseengine
# GitHub organization, inherited by this repo); cargo reads it from
# CARGO_REGISTRY_TOKEN. Mirrors pulseengine/synth's publish-to-crates-io.yml
# plus scripts/publish.rs, which walks the dependency order with a 10-attempt /
# 40s retry loop to ride out crates.io index propagation between deps.

name: Publish to crates.io

concurrency:
group: publish-${{ github.ref }}
cancel-in-progress: false

on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
tag:
description: "Existing tag whose workspace state should be (re)published (e.g. v1.10.0)"
required: true
type: string

permissions:
contents: read

env:
CARGO_TERM_COLOR: always

jobs:
publish:
name: Publish workspace libraries
if: github.repository == 'pulseengine/scry'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
with:
ref: ${{ inputs.tag || github.ref }}

- uses: dtolnay/rust-toolchain@stable

- uses: Swatinem/rust-cache@v2
with:
key: publish-crates-io

- name: Verify tag matches workspace version
env:
INPUT_TAG: ${{ inputs.tag }}
run: |
set -euo pipefail
VERSION="${INPUT_TAG:-${GITHUB_REF#refs/tags/}}"
EXPECTED="${VERSION#v}"
ACTUAL=$(
awk '
/^\[workspace.package\]/ { in_pkg = 1; next }
/^\[/ { in_pkg = 0 }
in_pkg && /^version *=/ {
gsub(/[ \t"]/, "", $0)
sub(/version=/, "", $0)
print
exit
}
' Cargo.toml
)
if [ "$EXPECTED" != "$ACTUAL" ]; then
echo "::error::tag $VERSION expects workspace version $EXPECTED but Cargo.toml has $ACTUAL"
exit 1
fi
echo "::notice::tag $VERSION matches workspace version $ACTUAL"

- name: Build publish helper
run: rustc --edition 2024 scripts/publish.rs -o publish

# NOTE: there is intentionally NO pre-flight `./publish verify` step in CI.
# Both `cargo publish --dry-run` AND `cargo package` resolve the path-dep
# version requirements against the crates.io index when preparing the
# upload, so the dependent (scry-analyze-core) fails with "failed to
# select a version for scry-interval = ^1.9.x" on the FIRST publish of any
# new version (the deps aren't on the registry yet). Compile errors are
# already caught by the CI test/build job (path deps, no chicken-and-egg);
# `./publish publish` validates each crate's metadata as cargo's own
# pre-upload check, with the 10-attempt / 40s retry loop that rides out
# registry index propagation between deps. `./publish verify` remains
# available for local pre-flight on the leaf crates.
- name: Publish workspace libraries to crates.io
run: ./publish publish
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_IO_TOKEN }}
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,36 @@ Versioning: [SemVer 2.0](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added — crates.io publishing

- **The reusable library crates are now publishable to crates.io**, mirroring
`pulseengine/synth`: a `Publish to crates.io` workflow
(`.github/workflows/publish-to-crates-io.yml`) fires on every `v*` tag
alongside `release.yml`, driven by `scripts/publish.rs` — a leaf-first
dependency-ordered publisher with a 10-attempt / 40s retry loop to ride out
crates.io index propagation, using the org-wide `CRATES_IO_TOKEN`.
- **Published set** (the genuine reusable work body): `scry-interval`,
`scry-taint`, `scry-octagon`, `scry-provenance` (leaves) → `scry-analyze-core`.
Internal path deps now carry `version` so `cargo publish` rewrites them to the
crates.io coordinate.
- **Not published** (`publish = false`): the Wasm-component crates
`wasm-lattice` and `scry-analyzer` — they are `cdylib` components whose WIT
bindings are generated by the Bazel `rust_wasm_component_bindgen` rule (no
cargo source) and only build for `wasm32-wasip2`, so a host `cargo publish`
cannot build them and a `cargo add` consumer could not use a cdylib component.
They continue to ship as signed `.wasm` release assets (the witness
precedent: publish the libraries, ship the component). The `scry-host-tests`
and `scry-mcdc` harnesses remain unpublished as before.

### Changed

- **`workspace.package.version` aligned to the release-tag line: `0.1.0` →
`1.9.0`** so the crate version on crates.io matches the `v1.x` release
artifacts and `SCRY_VERSION`. A future release bump must move the tag, this
version, and the internal path-dep `version` fields in lockstep (the
crates.io publish workflow asserts tag == workspace version). The first
crates.io publish therefore lands on the next release tag.

## [1.9.0] — 2026-06-14

Headline: **the octagon gains Miné strong (tight) closure — pure algebra,
Expand Down
16 changes: 8 additions & 8 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 8 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,18 @@ default-members = [
]

[workspace.package]
version = "0.1.0"
# Aligned with the release-tag line (v1.x = SCRY_VERSION) so the crate version
# on crates.io matches the release artifacts. The crates.io publish workflow
# asserts the pushed `v*` tag equals this version, so a release bump must move
# both in lockstep (and the internal path-dep `version = "..."` fields below).
version = "1.9.0"
edition = "2024"
license = "MIT OR Apache-2.0"
repository = "https://github.com/pulseengine/scry"
homepage = "https://pulseengine.eu"
authors = ["Ralf Anton Beier and scry contributors"]
keywords = ["wasm", "abstract-interpretation", "verification", "static-analysis"]
categories = ["development-tools", "wasm", "no-std"]

[workspace.dependencies]
bitflags = "2"
Expand Down
11 changes: 7 additions & 4 deletions crates/scry-analyze-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ path = "src/lib.rs"
# Interval + Region are field-identical to the WIT `interval` /
# `region-pointer-payload`, so the core reuses the pure types directly
# (the analyzer's region transfer ops then need no conversion).
scry-interval = { path = "../scry-interval" }
# Path deps carry `version` so `cargo publish` rewrites them to the crates.io
# coordinate (crates.io rejects path-only deps). The version equals the
# workspace version and must be bumped in lockstep with it.
scry-interval = { path = "../scry-interval", version = "1.9.0" }

# Step 2 (DD-012): the analyze body + helpers moved here. wasmparser parses
# the input Wasm Core Model module; sha2 digests the module bytes for
Expand All @@ -39,14 +42,14 @@ sha2 = { workspace = true }

# Security-label (taint) lattice for the noninterference analysis (FEAT-009)
# and the pure meld<->scry provenance boundary crate (FEAT-002 / DD-002).
scry-taint = { path = "../scry-taint" }
scry-provenance = { path = "../scry-provenance" }
scry-taint = { path = "../scry-taint", version = "1.9.0" }
scry-provenance = { path = "../scry-provenance", version = "1.9.0" }

# Octagon relational domain (FEAT-016 slice-2b-ii): carried alongside the
# intervals through the structured-CFG fixpoint so a loop counter bounded by a
# VARIABLE relation (`i < n`) stays bounded where the interval domain alone
# widens it to ⊤. Same pure `#![no_std]` dual-compile crate as scry-interval.
scry-octagon = { path = "../scry-octagon" }
scry-octagon = { path = "../scry-octagon", version = "1.9.0" }

[dev-dependencies]
# Test-only (the crate is otherwise dep-light + no_std): assemble the .wat
Expand Down
7 changes: 7 additions & 0 deletions crates/scry-analyzer/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ edition.workspace = true
license.workspace = true
repository.workspace = true
authors.workspace = true
# Not published to crates.io: this is the `cdylib` Wasm-component GUEST. It
# `export!`s `scry_analyzer_component_bindings`, generated by the Bazel
# `rust_wasm_component_bindgen` rule (no cargo source), and only builds for
# wasm32-wasip2 — a host `cargo publish` cannot build it. The reusable Rust API
# is the pure, bindgen-free `scry-analyze-core` crate (published); this
# component ships as a signed `.wasm` release asset (the witness precedent).
publish = false

[lib]
crate-type = ["cdylib"]
Expand Down
10 changes: 5 additions & 5 deletions crates/scry-mcdc/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions crates/wasm-lattice/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ edition.workspace = true
license.workspace = true
repository.workspace = true
authors.workspace = true
# Not published to crates.io: this is a `cdylib` Wasm component whose WIT
# bindings are generated by the Bazel `rust_wasm_component_bindgen` rule (no
# cargo source) and which only builds for wasm32-wasip2 — a host `cargo publish`
# cannot build it, and a `cargo add` consumer cannot use a cdylib component. The
# reusable algebra is published as the pure `scry-octagon` / `scry-taint`
# crates; the component ships as a signed `.wasm` release asset (the witness
# precedent: publish the libs, ship the component).
publish = false

[lib]
crate-type = ["cdylib"]
Expand Down
Loading
Loading