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
96 changes: 67 additions & 29 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,46 +1,84 @@
name: SkillSync Release Validation
name: SkillSync GitHub Release

on:
push:
tags: ["v*"]

permissions:
contents: read
id-token: write
actions: read

jobs:
validate:
release:
runs-on: ubuntu-latest
permissions:
contents: write
actions: read
id-token: write
attestations: write
artifact-metadata: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: "24"
package-manager-cache: false
- run: npm ci
- name: Run the offline test suite
run: npm test
- name: Type-check the release candidate
run: npm run type-check
- name: Lint the release candidate
run: npm run lint
- name: Build the release candidate
run: npm run build
- name: Inspect the public package contents
run: npm pack --dry-run
- name: Generate SBOM (CycloneDX)
- name: Bind the immutable tag to an already verified main source
env:
GH_TOKEN: ${{ github.token }}
run: |
npm sbom --sbom-format cyclonedx --sbom-type library > sbom.cyclonedx.json 2>&1 || echo "npm sbom not available on this npm version, falling back to dry-run check"
if [ -f sbom.cyclonedx.json ]; then
echo "SBOM generated at sbom.cyclonedx.json ($(wc -c < sbom.cyclonedx.json) bytes)"
head -20 sbom.cyclonedx.json
set -euo pipefail
[[ "$GITHUB_REF_NAME" =~ ^v[0-9]+[.][0-9]+[.][0-9]+$ ]]
test "$GITHUB_REF_NAME" = "v$(node -p 'require("./package.json").version')"
refs=$(git ls-remote origin "refs/tags/$GITHUB_REF_NAME" "refs/tags/$GITHUB_REF_NAME^{}")
commit=$(printf '%s\n' "$refs" | awk '/\^\{\}$/ {print $1;exit}')
[[ -n "$commit" ]] || commit=$(printf '%s\n' "$refs" | awk 'NR==1 {print $1}')
test "$commit" = "$GITHUB_SHA"
gh api "repos/$GITHUB_REPOSITORY/actions/workflows/skillsync.yml/runs?head_sha=$GITHUB_SHA&branch=main&event=push&status=success&per_page=1" > "$RUNNER_TEMP/source-ci.json"
node --input-type=module - "$RUNNER_TEMP/source-ci.json" <<'JS'
import fs from 'node:fs';
const runs=JSON.parse(fs.readFileSync(process.argv[2],'utf8')).workflow_runs;
if (!runs.some(run=>run.head_sha===process.env.GITHUB_SHA&&run.head_branch==='main'&&run.conclusion==='success')) process.exit(1);
JS
if gh release view "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
echo 'An existing release must not be overwritten' >&2
exit 1
fi
- name: Publish the package with npm provenance
run: npm publish --provenance --access public
- name: Attest build provenance (if publish succeeded)
if: always()
uses: actions/attest-build-provenance@v2
- run: npm ci
- run: npm test
- run: npm run type-check
- run: npm run lint
- run: npm run build
- name: Assemble actual package and validated metadata
run: |
set -euo pipefail
mkdir release
npm pack --json --ignore-scripts --pack-destination release > "$RUNNER_TEMP/package.json"
npm sbom --sbom-format cyclonedx --package-lock-only --omit=dev > release/sbom.cyclonedx.json
node --input-type=module - "$RUNNER_TEMP/package.json" <<'JS'
import fs from 'node:fs';
import crypto from 'node:crypto';
const [pack]=JSON.parse(fs.readFileSync(process.argv[2],'utf8'));
const version=JSON.parse(fs.readFileSync('package.json','utf8')).version;
if(pack.name!=='@chumanic/skillsync'||pack.version!==version||pack.filename!==`chumanic-skillsync-${version}.tgz`) throw new Error('Package identity mismatch');
const allowed=/^(dist\/|runner\/|profiles\/|templates\/|fixtures\/(behavior|product|runner|policy)\/|config\/|docs\/[^/]+[.]md$|README[.]md$|LICENSE$|CHANGELOG[.]md$|package[.]json$)/;
if(!pack.files.length||pack.files.some(file=>!allowed.test(file.path)||file.path.split('/').some(part=>part.startsWith('.')&&part!=='.skillsync'))) throw new Error('Unexpected public package file');
const sbom=JSON.parse(fs.readFileSync('release/sbom.cyclonedx.json','utf8'));
if(sbom.bomFormat!=='CycloneDX'||!Array.isArray(sbom.components)) throw new Error('Invalid SBOM');
const sha256=crypto.createHash('sha256').update(fs.readFileSync(`release/${pack.filename}`)).digest('hex');
fs.writeFileSync('release/release-manifest.json',JSON.stringify({package:pack.name,version,tag:process.env.GITHUB_REF_NAME,source_sha:process.env.GITHUB_SHA,archive:pack.filename,sha256},null,2)+'\n');
JS
(cd release && sha256sum *.tgz sbom.cyclonedx.json release-manifest.json > checksums.txt)
- name: Attest actual package provenance
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4
with:
subject-path: "*.tgz"
continue-on-error: true

subject-path: release/*.tgz
- name: Publish immutable GitHub release
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
gh release create "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" --verify-tag \
--title "SkillSync $GITHUB_REF_NAME" --generate-notes release/*
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
## 0.1.4 - 2026-10-09

- Publish new tagged packages through GitHub Releases with SHA256 checksums,
validated CycloneDX SBOM and required GitHub provenance, without an npm account.
- Keep npm-based CI defaults on the actually public 0.1.0 package; source and
GitHub packages retain artifact/reference/target coverage capabilities.
- Preserve the earlier immutable tags and npm 0.1.0. Source, release artifacts
and real external integration remain separate evidence.

# Changelog

## 0.1.3 - 2026-10-09
Expand Down
27 changes: 24 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

**Verify Agent Skills before you trust them.** SkillSync checks a local Skill's provenance, compatibility, and changes without executing it.

> **Alpha · v0.1.3 · Node.js 20+**
> **Alpha · v0.1.4 · Node.js 20+**
> SkillSync performs offline checks of local Skill content. It does not execute Skill scripts, does not read credentials, and does not enable live provider, remote-worker, or runtime capabilities.

[![Terminal demo](https://raw.githubusercontent.com/Chumaniac/skillsync/main/docs/assets/verify-demo.svg)](https://github.com/Chumaniac/skillsync/blob/main/docs/assets/verify-demo.svg)
Expand All @@ -18,7 +18,7 @@ npm install -g @chumanic/skillsync@0.1.0
skillsync verify --path . --target codex
```

**From source (latest 0.1.3):**
**From source (latest 0.1.4):**

```bash
git clone https://github.com/Chumaniac/skillsync.git
Expand All @@ -28,7 +28,28 @@ npm run build
node dist/cli/index.js verify --path fixtures/product/trust-loop/review --target codex
```

> `0.1.3` is a release candidate prepared for the OIDC provenance workflow (`npm publish --provenance --access public` with `id-token: write`, no long-lived token). Until the Trusted Publisher is verified on npm, use `0.1.0` via `npx` or run `0.1.3` from source. The `skillsync ci init` template pins `0.1.3` by default; override with `--package-version 0.1.0` on npm today.
**From GitHub releases (no npm account required):**

Tagged builds now publish a CLI tarball, checksums, a validated CycloneDX SBOM
and a source manifest on [GitHub Releases](https://github.com/Chumaniac/skillsync/releases).
The current source candidate is 0.1.4; its release is complete only after the
matching immutable tag workflow succeeds. Installation still uses Node.js 20+
and fetches the package's public dependencies; it needs no npm login.

```bash
release_base="https://github.com/Chumaniac/skillsync/releases/download/v0.1.4"
for asset in chumanic-skillsync-0.1.4.tgz checksums.txt sbom.cyclonedx.json release-manifest.json; do
curl --fail --location --output "$asset" "$release_base/$asset"
done
shasum -a 256 -c checksums.txt
npm install --prefix ./skillsync-tools ./chumanic-skillsync-0.1.4.tgz
./skillsync-tools/node_modules/.bin/skillsync --version
```

The public npm registry remains at 0.1.0. Generated npm-based CI templates pin
that existing public version; a source/GitHub installation supplies the newer
artifact and target-coverage commands. Older tags and the existing npm package
remain available independently of this distribution.

The command above verifies the included sample Skill. Replace the fixture path with a directory containing your own `SKILL.md` when you are ready.

Expand Down
15 changes: 8 additions & 7 deletions docs/ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ they can be replaced.

The generated GitHub Action grants `contents: read` and uploads SARIF findings;
it does not execute Skill scripts. The generated consumer command pins the
published SkillSync package version (`@chumanic/skillsync@0.1.3` by default); override it
published SkillSync package version (`@chumanic/skillsync@0.1.0` by default); override it
with `ci init --package-version <version>` when upgrading. Because the current
repository publishes a scoped public package, the generated consumer template can
be used after that package version is available; the repository's own workflow uses
Expand Down Expand Up @@ -69,12 +69,13 @@ placeholders is intentionally not accepted as production evidence.

## Release validation

`.github/workflows/release.yml` runs only for tags matching `v*`. It checks the
test suite, type-check, lint, build, and `npm pack --dry-run`, then publishes
`@chumanic/skillsync` with `npm publish --provenance --access public`. The job
uses GitHub OIDC (`id-token: write`) and no long-lived npm token. npm Trusted
Publisher configuration is an external prerequisite; a tag is not permission
to activate a live runtime capability.
`.github/workflows/release.yml` runs only for tags matching `v*`. It requires the
exact tagged source to have a successful main repository verification, then
checks tests, types, lint, build and the actual public package allowlist. It
publishes a tarball, checksums, validated SBOM and source manifest to GitHub
Releases only after required GitHub provenance succeeds. Existing releases are
not overwritten. This distribution needs no npm account or long-lived token;
the npm registry remains at 0.1.0. A tag does not activate a live runtime.

The operator-facing activation, revocation, rollback, and evidence review
procedure is in [`runtime-operator-runbook.md`](runtime-operator-runbook.md).
2 changes: 1 addition & 1 deletion docs/domain-adaptation.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ change. A `report` with both plan and receipt requires matching plan digests.
These findings are review material, not automatic permission to run a Skill.

The examples are current source additions, not part of the published npm0.1.0
package. Keep npm installation and source0.1.3 capabilities separate. Capability
package. Keep npm installation and source0.1.4 capabilities separate. Capability
profiles are maintained by this project using Agent documentation; a profile is
not vendor certification, and unknown/runtime-dependent features stay explicit.

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@chumanic/skillsync",
"version": "0.1.3",
"version": "0.1.4",
"private": false,
"publishConfig": {
"access": "public"
Expand Down
2 changes: 1 addition & 1 deletion src/cli/commands/ci.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export type CiInitResult = {
};

const DEFAULT_PATHS = [".agents/skills", ".claude/skills", ".cursor/skills"];
const DEFAULT_PACKAGE_VERSION = "0.1.3";
const DEFAULT_PACKAGE_VERSION = "0.1.0";
const PUBLISHED_PACKAGE_NAME = "@chumanic/skillsync";

function validateNodeVersion(value: string): string {
Expand Down
2 changes: 1 addition & 1 deletion src/cli/commands/report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ export async function runReport(options: ReportOptions): Promise<EvidenceReport>
...(resolvedIssueBaseline(beforeReport, before.rootDigest) === undefined
? {}
: { baseline: resolvedIssueBaseline(beforeReport, before.rootDigest) }),
toolVersion: options.toolVersion ?? "0.1.3",
toolVersion: options.toolVersion ?? "0.1.4",
});
}

Expand Down
6 changes: 3 additions & 3 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ import { renderSarif } from "../reporters/sarif.js";
import { renderText } from "../reporters/text.js";
import { parseOutputFormat } from "./output.js";

const VERSION = "0.1.3";
const VERSION = "0.1.4";

const COMMANDS = [
["scan", "Inspect skill files without executing their instructions."],
Expand Down Expand Up @@ -642,15 +642,15 @@ export function createCli(io: CliIO = defaultCliIO): Command {
.command("init")
.option("--target <target>", "Template target: github or pre-commit", "github")
.option("--node-version <version>", "Node.js version for GitHub Actions", "20")
.option("--package-version <version>", "Pinned published SkillSync package version", "0.1.3")
.option("--package-version <version>", "Pinned published npm package version", "0.1.0")
.option("--path <paths...>", "Project Skill paths")
.option("--apply", "Write the generated file")
.option("--force", "Allow replacing an existing generated file")
.action(async (options: CiCliOptions) => {
const result = await runCiInit({
target: options.target ?? "github",
nodeVersion: options.nodeVersion ?? "20",
packageVersion: options.packageVersion ?? "0.1.3",
packageVersion: options.packageVersion ?? "0.1.0",
paths: options.path ?? [],
apply: options.apply,
force: options.force,
Expand Down
2 changes: 1 addition & 1 deletion src/reporters/sarif.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export function renderSarif(report: VerificationReport): string {
tool: {
driver: {
name: "skillsync",
version: "0.1.3",
version: "0.1.4",
rules: [...ruleMap.values()],
},
},
Expand Down
4 changes: 2 additions & 2 deletions templates/github/skillsync.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: "20"
# Requires the published @chumanic/skillsync@0.1.3 package.
- run: npx --yes @chumanic/skillsync@0.1.3 verify --format sarif --path .agents/skills > skillsync.sarif
# Requires the published @chumanic/skillsync@0.1.0 package.
- run: npx --yes @chumanic/skillsync@0.1.0 verify --format sarif --path .agents/skills > skillsync.sarif
- uses: github/codeql-action/upload-sarif@v4
if: always()
with:
Expand Down
2 changes: 1 addition & 1 deletion templates/pre-commit/skillsync.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ repos:
hooks:
- id: skillsync-verify
name: Verify Agent Skills with SkillSync
entry: npx --yes @chumanic/skillsync@0.1.3 verify --format json --path .claude/skills --path .agents/skills
entry: npx --yes @chumanic/skillsync@0.1.0 verify --format json --path .claude/skills --path .agents/skills
language: system
pass_filenames: false
4 changes: 2 additions & 2 deletions tests/cli/ci.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ describe("skillsync ci", () => {
cwd: root,
});
expect(planned.applied).toBe(false);
expect(planned.content).toContain("@chumanic/skillsync@0.1.3 verify --format sarif");
expect(planned.content).toContain("@chumanic/skillsync@0.1.0 verify --format sarif");
await expect(access(join(root, ".github/workflows/skillsync.yml"))).rejects.toThrow();

const applied = await runCiInit({
Expand All @@ -51,7 +51,7 @@ describe("skillsync ci", () => {
apply: true,
});
expect(applied.applied).toBe(true);
expect(await readFile(applied.outputPath, "utf8")).toContain("@chumanic/skillsync@0.1.3 verify --format sarif");
expect(await readFile(applied.outputPath, "utf8")).toContain("@chumanic/skillsync@0.1.0 verify --format sarif");

await expect(
runCiInit({
Expand Down
2 changes: 1 addition & 1 deletion tests/cli/help.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ describe("skillsync CLI", () => {
it("reports the exact public version and lists the verification commands", async () => {
const version = await runCli(["--version"]);
expect(version.exitCode).toBe(0);
expect(version.stdout).toBe("0.1.3\n");
expect(version.stdout).toBe("0.1.4\n");

const result = await runCli(["--help"]);

Expand Down
4 changes: 2 additions & 2 deletions tests/cli/report.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ describe("skillsync report", () => {

const json = await runCli(["report", "--before", beforePath, "--after", afterPath, "--format", "json"]);
expect(JSON.parse(json.stdout).conclusion).toBe("verified");
expect(JSON.parse(json.stdout).toolVersion).toBe("0.1.3");
expect(JSON.parse(json.stdout).toolVersion).toBe("0.1.4");

const sarif = await runCli(["report", "--before", beforePath, "--after", afterPath, "--format", "sarif"]);
const parsedSarif = JSON.parse(sarif.stdout) as {
Expand All @@ -229,7 +229,7 @@ describe("skillsync report", () => {
}>;
};
expect(parsedSarif.version).toBe("2.1.0");
expect(parsedSarif.runs[0]?.tool.driver.version).toBe("0.1.3");
expect(parsedSarif.runs[0]?.tool.driver.version).toBe("0.1.4");
expect(parsedSarif.runs[0]?.results[0]?.properties.issueId).toMatch(/^iss_/);
});

Expand Down
Loading
Loading