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
19 changes: 19 additions & 0 deletions .agents/skills/release-yield/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
name: release-yield
description: "Release Yield through its protected GitHub workflows and verify every public registry."
---

<!-- generated-by: yskill; source: skills/release-yield; digest: sha256:28f0b916efb30d4a8be4d1b217c475e31b567942c0a7cf1d8c6d912de479477a; version: 0.1.38 -->

This adapter exposes the canonical Yield workflow at `skills/release-yield`.
Read its SKILL.md, then run from the repository root:

npm exec -- yskill run 'skills/release-yield'

Follow each returned operation exactly. Answer each operation directly:

npm exec -- yskill respond <run-id> --value <answer> --skill 'skills/release-yield'

For structured agent results, use --result-json instead of --value.

Do not skip an operation or invent its response.
5 changes: 5 additions & 0 deletions .changeset/self-host-release.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@operatorstack/yield": patch
---

Preserve executable platform runtimes in npm packages and add the repository's protected self-hosted release workflow.
19 changes: 19 additions & 0 deletions .claude/skills/release-yield/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
name: release-yield
description: "Release Yield through its protected GitHub workflows and verify every public registry."
---

<!-- generated-by: yskill; source: skills/release-yield; digest: sha256:28f0b916efb30d4a8be4d1b217c475e31b567942c0a7cf1d8c6d912de479477a; version: 0.1.38 -->

This adapter exposes the canonical Yield workflow at `skills/release-yield`.
Read its SKILL.md, then run from the repository root:

npm exec -- yskill run 'skills/release-yield'

Follow each returned operation exactly. Answer each operation directly:

npm exec -- yskill respond <run-id> --value <answer> --skill 'skills/release-yield'

For structured agent results, use --result-json instead of --value.

Do not skip an operation or invent its response.
19 changes: 19 additions & 0 deletions .cursor/skills/release-yield/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
name: release-yield
description: "Release Yield through its protected GitHub workflows and verify every public registry."
---

<!-- generated-by: yskill; source: skills/release-yield; digest: sha256:28f0b916efb30d4a8be4d1b217c475e31b567942c0a7cf1d8c6d912de479477a; version: 0.1.38 -->

This adapter exposes the canonical Yield workflow at `skills/release-yield`.
Read its SKILL.md, then run from the repository root:

npm exec -- yskill run 'skills/release-yield'

Follow each returned operation exactly. Answer each operation directly:

npm exec -- yskill respond <run-id> --value <answer> --skill 'skills/release-yield'

For structured agent results, use --result-json instead of --value.

Do not skip an operation or invent its response.
33 changes: 33 additions & 0 deletions .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -353,3 +353,36 @@ jobs:
name: crates-${{ needs.resolve.outputs.version }}-${{ needs.resolve.outputs.source_sha }}
path: dist/crates-receipt/
if-no-files-found: error

selfhost-canary:
name: Release skill against exact canary
needs: [resolve, npm]
if: needs.resolve.outputs.channel == 'canary'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
ref: ${{ needs.resolve.outputs.source_sha }}
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: "24"
package-manager-cache: false
- name: Install the exact published canary without changing repository state
env:
VERSION: ${{ needs.resolve.outputs.version }}
run: |
npm ci --ignore-scripts
npm install --no-save --package-lock=false --ignore-scripts "@operatorstack/yield@${VERSION}"
test "$(npm exec -- yskill version | awk '{print $2}')" = "$VERSION"
- name: Run the release skill contract against the canary SDK
run: |
npm run test:selfhost
journal="$RUNNER_TEMP/release-yield-journal.json"
output="$RUNNER_TEMP/release-yield-output.json"
printf '%s\n' '{"run_id":"canary-smoke","skill":{"name":"release-yield","digest":"sha256:canary"}}' > "$journal"
YIELD_JOURNAL="$journal" node skills/release-yield/main.ts > "$output"
node -e '
const output = require(process.argv[1]);
if (output.type !== "request" || output.envelope.request.id !== "select-bump") process.exit(1);
' "$output"
16 changes: 15 additions & 1 deletion .github/workflows/verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,20 @@ jobs:
- run: npm run test:release
- run: node scripts/check-release-control.mjs

selfhost:
name: Published SDK self-hosting
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: "24"
cache: npm
- run: npm ci --ignore-scripts
- run: npm run prepare:selfhost
- run: npm run test:selfhost
- run: npm exec -- yskill doctor skills/release-yield --root . --agent codex,cursor,claude-code

typescript:
name: TypeScript SDK and npm package
runs-on: ubuntu-latest
Expand Down Expand Up @@ -147,7 +161,7 @@ jobs:
validate:
name: Release authority and full validation
if: ${{ always() }}
needs: [go, release, typescript, python, rust, conformance, examples]
needs: [go, release, selfhost, typescript, python, rust, conformance, examples]
runs-on: ubuntu-latest
steps:
- name: Require every validation job
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,20 @@ Replace them with the test, publish, and registry commands for your project.
The complete tested source is in
[`examples/release-checklist`](https://github.com/operatorstack/yield/tree/main/examples/release-checklist/).

<!-- npm-exclude:start -->
## Yield releases Yield

This repository uses its own exact published SDK for stable releases. The
canonical [`release-yield`](https://github.com/operatorstack/yield/tree/main/skills/release-yield)
workflow dispatches the protected GitHub release controller, records the human
authorization, waits through the npm, PyPI, and crates.io environments, and
verifies the Go module and final GitHub release. It never publishes from the
developer's computer.

Every newly published canary runs the same contract tests in an isolated CI
lane. Stable release execution remains pinned to an exact public version.
<!-- npm-exclude:end -->

## Use Yield in five steps

### 1. Install Yield
Expand Down
120 changes: 105 additions & 15 deletions package-lock.json

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

7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
{
"name": "@operatorstack/yield-repository",
"private": true,
"type": "module",
"scripts": {
"changeset": "changeset",
"postinstall": "node scripts/prepare-selfhost.mjs",
"prepare:selfhost": "node scripts/prepare-selfhost.mjs",
"release:plan": "node scripts/release-plan.mjs",
"test:release": "node --test scripts/*.test.mjs packaging/*.test.mjs"
"test:release": "node --test scripts/*.test.mjs packaging/*.test.mjs",
"test:selfhost": "node --test skills/release-yield/*.test.mjs"
},
"devDependencies": {
"@operatorstack/yield": "0.1.38",
"@changesets/cli": "2.31.1",
"yaml": "2.9.0"
}
Expand Down
1 change: 1 addition & 0 deletions packaging/assemble.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ async function assembleNpm({ version, binaries, output }) {
await writeFile(join(directory, "package.json"), `${JSON.stringify({
name: npmPackage(target), version, description: `Yield runtime for ${target.id}`,
license: "MIT", os: [target.nodeOs], cpu: [target.nodeCpu], main: `./${runtime}`,
bin: { "yskill-runtime": `./${runtime}` },
files: [runtime, "LICENSE"], repository: { type: "git", url: "git+https://github.com/operatorstack/yield.git" },
homepage: "https://yield.operatorstack.systems/",
bugs: { url: "https://github.com/operatorstack/yield/issues" },
Expand Down
11 changes: 11 additions & 0 deletions packaging/assemble.test.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import test from "node:test";
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { access, mkdtemp, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
Expand Down Expand Up @@ -55,6 +56,10 @@ test("assembles one public npm package and six matching npm and Python runtimes"
assert.ok(main.files.includes("assets"));
assert.match(assembledReadme, /<h1 align="center">Yield<\/h1>/);
assert.match(await readFile(join(output, "npm/yield/LICENSE"), "utf8"), /MIT License/);
await assert.rejects(access(join(output, "npm/yield/skills/release-yield")), { code: "ENOENT" });
await assert.rejects(access(join(output, "npm/yield/.agents")), { code: "ENOENT" });
await assert.rejects(access(join(output, "npm/yield/.cursor")), { code: "ENOENT" });
await assert.rejects(access(join(output, "npm/yield/.claude")), { code: "ENOENT" });

for (const target of targets) {
const runtime = await readJson(join(output, `npm/${target.id}/package.json`));
Expand All @@ -63,7 +68,13 @@ test("assembles one public npm package and six matching npm and Python runtimes"
assert.equal(runtime.homepage, homepage);
assert.deepEqual(runtime.os, [target.nodeOs]);
assert.deepEqual(runtime.cpu, [target.nodeCpu]);
assert.deepEqual(runtime.bin, { "yskill-runtime": `./${target.goos === "windows" ? "yskill.exe" : "yskill"}` });
assert.equal(runtime.publishConfig.provenance, true);
const packed = JSON.parse(execFileSync("npm", ["pack", "--dry-run", "--json"], {
cwd: join(output, `npm/${target.id}`),
encoding: "utf8",
}));
assert.equal(packed[0].files.find((file) => file.path === (target.goos === "windows" ? "yskill.exe" : "yskill")).mode, 0o755);
assert.match(await readFile(join(output, `npm/${target.id}/LICENSE`), "utf8"), /MIT License/);

const pythonRoot = join(output, `python/${target.id}`);
Expand Down
5 changes: 4 additions & 1 deletion scripts/check-release-control.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export async function checkReleaseControl(root = resolve(import.meta.dirname, ".
expect(!names.includes("sync-upstream.yml"), "projection sync workflow must be removed after graduation");

const verify = workflows["verify.yml"];
const validationJobs = ["go", "release", "typescript", "python", "rust", "conformance", "examples"];
const validationJobs = ["go", "release", "selfhost", "typescript", "python", "rust", "conformance", "examples"];
expect(verify, "verify.yml is required");
expect(verify.on?.pull_request !== undefined, "verification must run on every pull request");
expect(validationJobs.every((name) => verify.jobs?.[name]), "verification must expose every SDK and package boundary");
Expand Down Expand Up @@ -75,6 +75,9 @@ export async function checkReleaseControl(root = resolve(import.meta.dirname, ".
expect(publisher.jobs?.crates?.permissions?.contents === "read" && publisher.jobs?.crates?.permissions?.["id-token"] === "write", "crates.io publisher must use read-only source plus OIDC");
expect(publisher.jobs?.pypi?.environment === "pypi-production", "stable PyPI publishing must use the protected pypi-production environment");
expect(publisher.jobs?.crates?.environment === "crates-production", "stable crates.io publishing must use the protected crates-production environment");
expect(publisher.jobs?.["selfhost-canary"]?.needs?.includes("npm"), "canary self-hosting must follow successful npm publication");
expect(raw["npm-publish.yml"].includes('"@operatorstack/yield@${VERSION}"'), "canary self-hosting must install the exact resolved version");
expect(!raw["npm-publish.yml"].includes("@operatorstack/yield@canary"), "canary self-hosting must not execute a floating dist-tag");
const pythonWheelStep = publisher.jobs?.build?.steps?.find((step) => step.name === "Build Python wheels");
expect(pythonWheelStep?.if === "needs.resolve.outputs.channel == 'stable'", "PyPI wheels must be built only for stable PEP 440 versions");
expect(raw["npm-publish.yml"].indexOf("Publish platform runtimes") < raw["npm-publish.yml"].indexOf("Publish SDK and CLI"), "runtime packages must publish before the SDK package");
Expand Down
Loading