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
36 changes: 34 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
workflow_dispatch:

permissions:
contents: read
contents: write

jobs:
package:
Expand Down Expand Up @@ -34,19 +34,51 @@ jobs:
env:
CONFORMANCE_CLEAN_CHECKOUT: "1"
- run: mkdir -p release-artifacts
- name: Record release metadata
run: pnpm release:metadata --output release-artifacts/release-metadata.json
- name: Include conformance record
run: tar -czf release-artifacts/erc-8350-conformance.tar.gz -C artifacts conformance
- run: pnpm --dir packages/core pack --pack-destination ../../release-artifacts
- run: pnpm --dir packages/reference-engine pack --pack-destination ../../release-artifacts
- run: pnpm --dir packages/awareness-adapter pack --pack-destination ../../release-artifacts
- run: pnpm --dir implementations/minimal-ts pack --pack-destination ../../release-artifacts
- name: Package contract sources
run: >-
tar -czf release-artifacts/agent-memory-state-contracts.tar.gz
contracts/src contracts/foundry.toml contracts/remappings.txt
- name: Package normative specification and vectors
run: >-
tar -czf release-artifacts/erc-8350-specification.tar.gz
erc/erc-8350.md erc/assets/erc-8350 SPEC.md
test-vectors/v1.json test-vectors/v2.json LICENSE
- name: Package G4 security evidence
run: >-
tar -czf release-artifacts/erc-8350-g4-security-evidence.tar.gz
audits/g4 SECURITY.md docs/security
- name: Generate checksums
run: cd release-artifacts && sha256sum * > SHA256SUMS
- uses: actions/upload-artifact@v4
with:
name: erc-awar-${{ github.run_id }}
name: erc-8350-${{ github.run_id }}
path: release-artifacts/
if-no-files-found: error
- name: Publish GitHub release
if: github.ref_type == 'tag'
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
prerelease_args=()
if [[ "${GITHUB_REF_NAME#v}" == *-* ]]; then
prerelease_args+=(--prerelease)
fi

if gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1; then
gh release upload "$GITHUB_REF_NAME" release-artifacts/* --clobber
else
gh release create "$GITHUB_REF_NAME" release-artifacts/* \
--verify-tag \
--generate-notes \
--title "ERC-8350 ${GITHUB_REF_NAME#v}" \
"${prerelease_args[@]}"
fi
4 changes: 3 additions & 1 deletion audits/g4/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,9 @@ finding, and zero unresolved Critical or High findings.
6. Map accepted non-Critical/High findings to `residual-risks.json`.
7. Run `pnpm security:rc`, `pnpm check`, and the clean-checkout conformance gate.
8. Obtain maintainer approval of reviewer identity and report quality, then mark
G4 complete and create the RC tag.
G4 complete, update every workspace package to the RC SemVer, and create the
matching `v<version>` tag. The release workflow will re-run the full gate before
publishing the GitHub prerelease.

The script checks evidence integrity. It cannot establish reviewer competence or
independence on its own; those remain explicit human approval duties.
9 changes: 6 additions & 3 deletions docs/release-process.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ the draft an accepted ERC and do not imply that contracts are production ready.
- `1.0.0-beta.x`: v1 encoding is frozen and at least one external implementation
has passed the golden vectors.
- `1.0.0-rc.x`: editor feedback is incorporated, testnet deployments are public,
and an external security review has no unresolved critical findings.
and an external security review has no unresolved Critical or High findings.
- `1.0.0`: the team declares the implementation stable. ERC status remains the
status shown in the official ERC repository.

Expand All @@ -29,8 +29,11 @@ the draft an accepted ERC and do not imply that contracts are production ready.
8. Manually verify reviewer identity, expertise, conflict disclosure, report
quality, and the public attestation. Passing JSON validation alone is not an
audit.
9. Create a signed `v*` tag. The release workflow builds package tarballs,
contract sources, and SHA-256 checksums without publishing automatically.
9. Update every workspace package to the same SemVer, then create a signed tag
whose name is exactly `v<version>`. The release workflow re-runs the gates,
builds package tarballs, contract sources, the normative specification,
vectors, security evidence, and SHA-256 checksums, then creates a GitHub
prerelease for pre-release versions. It does not publish npm packages.
10. Attach audit and testnet deployment references before labeling an artifact
production ready.

Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,14 @@
"security:scope": "node scripts/security-rc-gate.mjs --scope-only && pnpm security:handoff",
"security:test": "node --test scripts/security-rc-gate.test.mjs scripts/security-audit-handoff.test.mjs",
"security:rc": "pnpm security:handoff && node scripts/security-rc-gate.mjs",
"release:metadata": "node scripts/release-metadata-gate.mjs",
"release:test": "node --test scripts/release-metadata-gate.test.mjs",
"test": "pnpm test:ts && pnpm check:vectors && pnpm test:contracts",
"check": "pnpm clean && pnpm build && pnpm -r --if-present typecheck && pnpm -r --if-present test && pnpm check:erc-assets && pnpm check:vectors && pnpm check:private-witness && pnpm evidence:validate && pnpm evidence:test && pnpm security:scope && pnpm security:test && pnpm format:check && pnpm test:contracts",
"check": "pnpm clean && pnpm build && pnpm -r --if-present typecheck && pnpm -r --if-present test && pnpm check:erc-assets && pnpm check:vectors && pnpm check:private-witness && pnpm evidence:validate && pnpm evidence:test && pnpm security:scope && pnpm security:test && pnpm release:test && pnpm format:check && pnpm test:contracts",
"conformance:gate": "node scripts/conformance-gate.mjs",
"conformance:clean": "node scripts/clean-checkout-conformance.mjs",
"audit": "pnpm audit --audit-level=moderate",
"release:check": "pnpm conformance:gate && pnpm security:rc && pnpm audit --audit-level=moderate"
"release:check": "pnpm release:metadata && pnpm release:test && pnpm conformance:gate && pnpm security:rc && pnpm audit --audit-level=moderate"
},
"devDependencies": {
"@noble/curves": "^2.2.0",
Expand Down
155 changes: 155 additions & 0 deletions scripts/release-metadata-gate.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { execFileSync } from "node:child_process";
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";

const scriptDirectory = path.dirname(fileURLToPath(import.meta.url));
const defaultRepositoryRoot = path.resolve(scriptDirectory, "..");
const semverPattern =
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/u;

function isValidSemver(version) {
const match = semverPattern.exec(version);
if (!match) return false;

const prerelease = match[4];
return (
!prerelease ||
prerelease
.split(".")
.every((identifier) => !/^\d+$/u.test(identifier) || !/^0\d+/u.test(identifier))
);
}

function parseArguments(argv) {
const options = {
output: null,
repositoryRoot: defaultRepositoryRoot,
requireTag: false,
tag: null,
};

for (let index = 0; index < argv.length; index += 1) {
const argument = argv[index];
if (argument === "--require-tag") {
options.requireTag = true;
continue;
}
if (argument === "--output" || argument === "--root" || argument === "--tag") {
const value = argv[index + 1];
if (!value) throw new Error(`${argument} requires a value`);
index += 1;
if (argument === "--output") options.output = value;
if (argument === "--root") options.repositoryRoot = path.resolve(value);
if (argument === "--tag") options.tag = value;
continue;
}
throw new Error(`Unknown argument: ${argument}`);
}

return options;
}

async function readJson(filePath) {
return JSON.parse(await readFile(filePath, "utf8"));
}

async function collectPackageManifests(repositoryRoot) {
const manifestPaths = ["package.json"];

for (const parent of ["packages", "implementations"]) {
const entries = await readdir(path.join(repositoryRoot, parent), {
withFileTypes: true,
});
for (const entry of entries) {
if (entry.isDirectory()) {
manifestPaths.push(path.join(parent, entry.name, "package.json"));
}
}
}

const manifests = [];
for (const relativePath of manifestPaths.sort()) {
const manifest = await readJson(path.join(repositoryRoot, relativePath));
if (!manifest.name || !manifest.version) {
throw new Error(`${relativePath} must define name and version`);
}
manifests.push({
name: manifest.name,
path: relativePath,
version: manifest.version,
});
}
return manifests;
}

function inferTag(explicitTag) {
if (explicitTag) return explicitTag;
if (process.env.GITHUB_REF_TYPE === "tag") return process.env.GITHUB_REF_NAME;
if (process.env.GITHUB_REF?.startsWith("refs/tags/")) {
return process.env.GITHUB_REF.slice("refs/tags/".length);
}
return null;
}

function gitValue(repositoryRoot, ...arguments_) {
return execFileSync("git", ["-C", repositoryRoot, ...arguments_], {
encoding: "utf8",
}).trim();
}

async function main() {
const options = parseArguments(process.argv.slice(2));
const manifests = await collectPackageManifests(options.repositoryRoot);
const rootManifest = manifests.find((manifest) => manifest.path === "package.json");
const version = rootManifest.version;

if (!isValidSemver(version)) {
throw new Error(`Root package version is not valid SemVer: ${version}`);
}

const mismatches = manifests.filter((manifest) => manifest.version !== version);
if (mismatches.length > 0) {
throw new Error(
`Workspace package versions differ from ${version}: ${mismatches
.map((manifest) => `${manifest.path}=${manifest.version}`)
.join(", ")}`,
);
}

const tag = inferTag(options.tag);
if (options.requireTag && !tag) {
throw new Error("A release tag is required");
}
if (tag && tag !== `v${version}`) {
throw new Error(`Release tag ${tag} must equal v${version}`);
}

const result = {
schema: "erc-8350/release-metadata/v1",
version,
tag,
prerelease: version.includes("-"),
source: {
commit: gitValue(options.repositoryRoot, "rev-parse", "HEAD"),
tree: gitValue(options.repositoryRoot, "rev-parse", "HEAD^{tree}"),
},
packages: manifests,
};

if (options.output) {
const outputPath = path.resolve(options.repositoryRoot, options.output);
await mkdir(path.dirname(outputPath), { recursive: true });
await writeFile(outputPath, `${JSON.stringify(result, null, 2)}\n`);
}

console.log(`Release metadata: PASS (${manifests.length} package manifests)`);
console.log(`Version: ${version}`);
console.log(`Tag: ${tag ?? "not supplied"}`);
}

main().catch((error) => {
console.error("Release metadata: FAIL");
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
});
Loading
Loading