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
5 changes: 5 additions & 0 deletions .changeset/rust-crates-public-release.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@operatorstack/yield": patch
---

Publish the Rust SDK and platform runtime crates on crates.io with a Rust-first onboarding guide and trusted release controls.
93 changes: 92 additions & 1 deletion .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,23 @@ jobs:
python -m build --wheel --no-isolation --outdir "$GITHUB_WORKSPACE/dist/pypi" "$directory"
done
node packaging/pypi-release.mjs inspect --version "$VERSION" --dist dist/pypi
- name: Build Rust crates
if: needs.resolve.outputs.channel == 'stable'
env:
VERSION: ${{ needs.resolve.outputs.version }}
shell: bash
run: |
set -euo pipefail
node packaging/crates-release.mjs inspect --version "$VERSION" --rust dist/packages/rust
mkdir -p dist/crates
for directory in dist/packages/rust/runtime/*; do
cargo package --manifest-path "$directory/Cargo.toml"
name="$(sed -n 's/^name = "\([^"]*\)"/\1/p' "$directory/Cargo.toml" | head -n 1)"
cp "$directory/target/package/${name}-${VERSION}.crate" dist/crates/
done
(cd dist/packages/rust && cargo package --manifest-path yieldskill/Cargo.toml)
cp "dist/packages/rust/yieldskill/target/package/yieldskill-${VERSION}.crate" dist/crates/
test "$(find dist/crates -maxdepth 1 -name '*.crate' | wc -l | tr -d ' ')" = 7
- name: Inspect npm tarballs
shell: bash
run: |
Expand All @@ -146,7 +163,11 @@ jobs:
mkdir -p dist/release-unit
cp dist/packages/SHA256SUMS.json dist/release-unit/
cp -R dist/packages/npm dist/release-unit/npm
if [[ "$CHANNEL" == stable ]]; then cp -R dist/pypi dist/release-unit/pypi; fi
if [[ "$CHANNEL" == stable ]]; then
cp -R dist/pypi dist/release-unit/pypi
cp -R dist/packages/rust dist/release-unit/rust
cp -R dist/crates dist/release-unit/crates
fi
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: packages-${{ needs.resolve.outputs.version }}-${{ needs.resolve.outputs.source_sha }}
Expand Down Expand Up @@ -261,3 +282,73 @@ jobs:
--dist dist/release-unit/pypi
--attempts 12
--delay-ms 10000

crates:
needs: [resolve, build]
if: needs.resolve.outputs.channel == 'stable'
runs-on: ubuntu-latest
environment: crates-production
permissions:
contents: read
id-token: write
env:
CRATES_BOOTSTRAP_TOKEN: ${{ secrets.CRATES_BOOTSTRAP_TOKEN }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
ref: ${{ needs.resolve.outputs.source_sha }}
- uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
with:
name: packages-${{ needs.resolve.outputs.version }}-${{ needs.resolve.outputs.source_sha }}
path: dist/release-unit
- id: auth
name: Request a short-lived crates.io token
if: env.CRATES_BOOTSTRAP_TOKEN == ''
uses: rust-lang/crates-io-auth-action@c6f97d42243bad5fab37ca0427f495c86d5b1a18 # v1.0.5
- name: Publish complete Rust release unit
env:
CARGO_REGISTRY_TOKEN: ${{ env.CRATES_BOOTSTRAP_TOKEN || steps.auth.outputs.token }}
VERSION: ${{ needs.resolve.outputs.version }}
shell: bash
run: |
set -euo pipefail
publish_if_missing() {
local directory="$1"
local name
name="$(sed -n 's/^name = "\([^"]*\)"/\1/p' "$directory/Cargo.toml" | head -n 1)"
local archive="dist/release-unit/crates/${name}-${VERSION}.crate"
local state
state="$(node packaging/crates-release.mjs status --version "$VERSION" --name "$name" --archive "$archive")"
if [[ "$state" == matched ]]; then
echo "${name}@${VERSION} already exists with the release-unit checksum"
else
test "$state" = missing
(cd "$directory" && cargo publish)
fi
}

for directory in dist/release-unit/rust/runtime/*; do
publish_if_missing "$directory"
done

for attempt in {1..18}; do
missing=0
for directory in dist/release-unit/rust/runtime/*; do
name="$(sed -n 's/^name = "\([^"]*\)"/\1/p' "$directory/Cargo.toml" | head -n 1)"
archive="dist/release-unit/crates/${name}-${VERSION}.crate"
if [[ "$(node packaging/crates-release.mjs status --version "$VERSION" --name "$name" --archive "$archive")" != matched ]]; then
missing=1
fi
done
if [[ "$missing" == 0 ]]; then break; fi
test "$attempt" -lt 18
sleep 10
done

publish_if_missing dist/release-unit/rust/yieldskill
node packaging/crates-release.mjs verify \
--version "$VERSION" \
--archives dist/release-unit/crates \
--attempts 18 \
--delay-ms 10000
5 changes: 5 additions & 0 deletions .github/workflows/release-finalize.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,5 +104,10 @@ jobs:
--dist "$RUNNER_TEMP/release-unit/pypi" \
--attempts 3 \
--delay-ms 10000
node packaging/crates-release.mjs verify \
--version "$version" \
--archives "$RUNNER_TEMP/release-unit/crates" \
--attempts 3 \
--delay-ms 10000
test "$(git rev-list -n 1 "$TAG")" = "$SOURCE_SHA"
gh release edit "$TAG" --repo "$GITHUB_REPOSITORY" --draft=false
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<a href="https://www.npmjs.com/package/@operatorstack/yield"><img alt="npm version" src="https://img.shields.io/npm/v/@operatorstack/yield?style=flat-square" /></a>
<!-- npm-exclude:start -->
<a href="https://pypi.org/project/yieldskill/"><img alt="PyPI version" src="https://img.shields.io/pypi/v/yieldskill?style=flat-square" /></a>
<a href="https://crates.io/crates/yieldskill"><img alt="crates.io version" src="https://img.shields.io/crates/v/yieldskill?style=flat-square" /></a>
<!-- npm-exclude:end -->
<a href="https://github.com/operatorstack/yield/actions/workflows/verify.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/operatorstack/yield/verify.yml?branch=main&amp;style=flat-square&amp;label=build" /></a>
<a href="https://github.com/operatorstack/yield/blob/main/LICENSE"><img alt="MIT license" src="https://img.shields.io/npm/l/@operatorstack/yield?style=flat-square" /></a>
Expand All @@ -27,6 +28,7 @@
<a href="https://www.npmjs.com/package/@operatorstack/yield">npm</a> ·
<!-- npm-exclude:start -->
<a href="https://pypi.org/project/yieldskill/">PyPI</a> ·
<a href="https://crates.io/crates/yieldskill">crates.io</a> ·
<!-- npm-exclude:end -->
<a href="https://github.com/operatorstack/yield">GitHub</a>
</p>
Expand Down
2 changes: 1 addition & 1 deletion cmd/yskill/agents.go
Original file line number Diff line number Diff line change
Expand Up @@ -682,7 +682,7 @@ func localRuntimeInstallCommand(language, expected string) string {
}
return fmt.Sprintf(`mkdir -p .yield/bin && GOBIN="$PWD/.yield/bin" GOPROXY=https://get.operatorstack.systems/go,direct go install github.com/operatorstack/yield/cmd/yskill@v%s`, expected)
case "rust":
return fmt.Sprintf(`cargo install yieldskill@%s --root .yield --index sparse+https://get.operatorstack.systems/cargo/index/ --locked`, expected)
return fmt.Sprintf(`cargo install yieldskill@%s --root .yield --locked`, expected)
default:
return "install the matching Yield package"
}
Expand Down
5 changes: 4 additions & 1 deletion cmd/yskill/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ func TestScaffoldSkillWritesLanguageSpecificEntrypoints(t *testing.T) {
{"typescript", []string{"main.ts", "package.json", "skill.json"}, "npm exec -- yskill run .", `"@operatorstack/yield": "0.1.9"`},
{"python", []string{"main.py", "requirements.txt", "skill.json"}, "python -m yieldskill run .", "yieldskill==0.1.9"},
{"go", []string{"main.go", "go.mod", "skill.json"}, "yskill run .", "github.com/operatorstack/yield v0.1.9"},
{"rust", []string{"src/main.rs", "Cargo.toml", ".cargo/config.toml", "skill.json"}, "yskill run .", `version = "=0.1.9"`},
{"rust", []string{"src/main.rs", "Cargo.toml", "skill.json"}, "yskill run .", `version = "=0.1.9"`},
}
for _, tt := range tests {
t.Run(tt.language, func(t *testing.T) {
Expand Down Expand Up @@ -243,6 +243,9 @@ func TestScaffoldSkillWritesLanguageSpecificEntrypoints(t *testing.T) {
if tt.language == "python" && strings.Contains(manifest, "--index-url") {
t.Fatalf("public Python scaffold contains a private package index:\n%s", manifest)
}
if tt.language == "rust" && strings.Contains(manifest, "registry =") {
t.Fatalf("public Rust scaffold contains a private package registry:\n%s", manifest)
}
})
}
if tidyCalls != 1 {
Expand Down
3 changes: 1 addition & 2 deletions cmd/yskill/scaffold.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,7 @@ func scaffoldFiles(name, language, sdkPath string) map[string]string {
}
case "rust":
return map[string]string{
".cargo/config.toml": "[registries.operatorstack]\nindex = \"sparse+https://get.operatorstack.systems/cargo/index/\"\n",
"Cargo.toml": fmt.Sprintf("[package]\nname = %q\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\nyieldskill = { version = \"=%s\", registry = \"operatorstack\" }\nserde_json = \"1\"\n", name, v),
"Cargo.toml": fmt.Sprintf("[package]\nname = %q\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\nyieldskill = { version = \"=%s\" }\nserde_json = \"1\"\n", name, v),
"src/main.rs": mainRust,
"skill.json": fmt.Sprintf("{\"version\":1,\"language\":\"rust\",\"run\":[\"cargo\",\"run\",\"--quiet\",\"--bin\",%q]}\n", name),
}
Expand Down
4 changes: 2 additions & 2 deletions evals/results/latest.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"schema_version": 2,
"methodology_version": "1.1",
"generated_at": "2026-08-07T21:08:41.456Z",
"source_digest": "e702eb9bc1362b9a4d77fd41c9860bad97405c7aab3b40aba3a6ba3046c43933",
"generated_at": "2026-08-07T21:46:28.142Z",
"source_digest": "e70b0de3a9db5924311609a33e3ce91b539da02a6822acbf57d00471d8f6b63a",
"status": "passed",
"workflow_conformance": {
"passed": 40,
Expand Down
2 changes: 2 additions & 0 deletions examples/data-migration/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// dry-run, show the diff, human approval, apply, verify. The append-only
// run log is the audit trail; every irreversible action has a recorded,
// approved request before it.
// README_EXAMPLE_START
use serde_json::json;
use yieldskill::{define_skill, Context, SkillResult};

Expand Down Expand Up @@ -56,3 +57,4 @@ fn program(ctx: &mut Context) -> SkillResult {
fn main() {
define_skill(program);
}
// README_EXAMPLE_END
9 changes: 7 additions & 2 deletions packaging/assemble.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ async function assemblePython({ version, binaries, output }) {
}

function rustDependency(target, version) {
return `[target.'cfg(all(target_os = "${target.rustOs}", target_arch = "${target.rustArch}"))'.dependencies]\n${rustPackage(target)} = { version = "=${version}", registry = "operatorstack" }\n`;
return `[target.'cfg(all(target_os = "${target.rustOs}", target_arch = "${target.rustArch}"))'.dependencies]\n${rustPackage(target)} = { version = "=${version}" }\n`;
}

async function assembleRust({ version, binaries, output }, records) {
Expand All @@ -115,17 +115,22 @@ async function assembleRust({ version, binaries, output }, records) {
const runtime = target.goos === "windows" ? "yskill.exe" : "yskill";
await mkdir(join(directory, "src"), { recursive: true });
await copyBinary(join(binaries, binaryName(target)), join(directory, "runtime", runtime), false);
await writeFile(join(directory, "Cargo.toml"), `[package]\nname = "${name}"\nversion = "${version}"\nedition = "2021"\nlicense = "MIT"\ndescription = "Internal Yield runtime for ${target.id}"\nrepository = "https://github.com/operatorstack/yield"\ninclude = ["src/lib.rs", "runtime/${runtime}"]\n\n[lib]\npath = "src/lib.rs"\n`);
await cp(join(root, "LICENSE"), join(directory, "LICENSE"));
await writeFile(join(directory, "README.md"), `# ${name}\n\nPlatform runtime support for [Yield](https://crates.io/crates/yieldskill) on ${target.id}.\n\nThis crate is installed automatically by \`yieldskill\`. Do not add it directly.\n`);
await writeFile(join(directory, "Cargo.toml"), `[package]\nname = "${name}"\nversion = "${version}"\nedition = "2021"\nlicense = "MIT"\ndescription = "Yield runtime support for ${target.id}."\nrepository = "https://github.com/operatorstack/yield"\nhomepage = "https://yield.operatorstack.systems/"\nreadme = "README.md"\ninclude = ["src/lib.rs", "runtime/${runtime}", "README.md", "LICENSE"]\n\n[lib]\npath = "src/lib.rs"\n`);
await writeFile(join(directory, "src/lib.rs"), `pub const BYTES: &[u8] = include_bytes!("../runtime/${runtime}");\npub const SHA256: &str = "${runtimeByTarget.get(target.id).sha256}";\n`);
}

const main = join(rust, "yieldskill");
await cp(join(root, "sdk/rust"), main, { recursive: true, filter: (source) => !source.includes("/target") });
await cp(join(root, "LICENSE"), join(main, "LICENSE"));
let cargo = (await readFile(join(main, "Cargo.toml"), "utf8")).replace(/^version = ".*"/m, `version = "${version}"`);
cargo += `\n[[bin]]\nname = "yskill"\npath = "src/bin/yskill.rs"\n\n${targets.map((target) => rustDependency(target, version)).join("\n")}`;
await writeFile(join(main, "Cargo.toml"), cargo);
await mkdir(join(main, "src/bin"), { recursive: true });
await cp(join(root, "packaging/rust-launcher.rs"), join(main, "src/bin/yskill.rs"));
await mkdir(join(rust, ".cargo"), { recursive: true });
await writeFile(join(rust, ".cargo/config.toml"), `[patch.crates-io]\n${targets.map((target) => `${rustPackage(target)} = { path = "runtime/${target.id}" }`).join("\n")}\n`);
}

export async function assemble(options) {
Expand Down
28 changes: 27 additions & 1 deletion packaging/assemble.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { assemble, isPackageVersion } from "./assemble.mjs";
import { binaryName, npmPackage, targets } from "./targets.mjs";
import { binaryName, npmPackage, rustPackage, targets } from "./targets.mjs";

const homepage = "https://yield.operatorstack.systems/";

Expand Down Expand Up @@ -75,5 +75,31 @@ test("assembles one public npm package and six matching npm and Python runtimes"
await readFile(join(pythonRoot, "yieldskill/_runtime", pythonRuntime), "utf8"),
`runtime:${target.id}`,
);

const rustRoot = join(output, `rust/runtime/${target.id}`);
const rustManifest = await readFile(join(rustRoot, "Cargo.toml"), "utf8");
assert.match(rustManifest, new RegExp(`name = "${rustPackage(target)}"`));
assert.match(rustManifest, /version = "1\.2\.3"/);
assert.match(rustManifest, /readme = "README\.md"/);
assert.doesNotMatch(rustManifest, /registry\s*=/);
assert.match(await readFile(join(rustRoot, "README.md"), "utf8"), /installed automatically by `yieldskill`/);
assert.match(await readFile(join(rustRoot, "LICENSE"), "utf8"), /MIT License/);
}

const rustMain = join(output, "rust/yieldskill");
const rustMainManifest = await readFile(join(rustMain, "Cargo.toml"), "utf8");
assert.match(rustMainManifest, /name = "yieldskill"/);
assert.match(rustMainManifest, /version = "1\.2\.3"/);
assert.doesNotMatch(rustMainManifest, /registry\s*=/);
for (const target of targets) {
assert.match(rustMainManifest, new RegExp(`${rustPackage(target)} = \\{ version = "=1\\.2\\.3" \\}`));
}
const rustReadme = await readFile(join(rustMain, "README.md"), "utf8");
assert.match(rustReadme, /crates\.io\/crates\/yieldskill/);
assert.doesNotMatch(rustReadme, /npmjs\.com|pypi\.org/);
assert.match(await readFile(join(rustMain, "LICENSE"), "utf8"), /MIT License/);
const rustPatch = await readFile(join(output, "rust/.cargo/config.toml"), "utf8");
for (const target of targets) {
assert.match(rustPatch, new RegExp(`${rustPackage(target)} = \\{ path = "runtime/${target.id}" \\}`));
}
});
48 changes: 0 additions & 48 deletions packaging/cargo-index.mjs

This file was deleted.

16 changes: 0 additions & 16 deletions packaging/cargo-index.test.mjs

This file was deleted.

Loading