mirror of
https://github.com/openprose/prose.git
synced 2026-09-19 05:55:05 +08:00
9856ac5b12
* feat(spec): declared ### Skills section with fail-closed compile resolution Implements the spec from issue #60. Components declare required harness skills via a `### Skills` section (colon form, e.g. `document-skills:pdf`). `prose compile` resolves declared skills against ./skills/, ~/.claude/skills/, ~/.codex/skills/, and ~/.agents/skills/, and fails closed with `skill_unresolved` before forwarding to the agent harness when any are missing. - Spec: skills/open-prose/contract-markdown.md gains a ### Skills row in the Canonical Sections table and a ## Skills H2 covering colon naming, search order, the BYO-harness invariant, and fail-closed semantics. - Implementation: tools/cli/src/skills/declared.ts (parser + resolver + directory walker + DeclaredSkillsUnresolvedError). Pure functions; no I/O beyond readFile / readdir / stat. - Wiring: tools/cli/src/commands/compile.ts pre-checks declared skills before forwarding the compile prompt; fails closed with CompileValidationError when any are unresolved. Gated behind the existing skillPreflight option for test parity. - Example: skills/open-prose/examples/declared-skills/ shows the document-skills:pdf canonical pattern. - Tests: 19 new (18 in declared.test.ts covering parser/resolver/walker/ error formatter; 1 in cli.test.ts asserting compile fails closed before the harness is invoked when a declared skill is missing). - BYO harness: OpenProse never installs harness skills; resolution failure is the user's signal to install the named skill themselves. Resolves #60. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(spec): move skill resolution into compiler program; trim spec per review Address review feedback on #62: - contract-markdown.md: drop the "not duplicated in frontmatter" clause — the rejected alternative shouldn't propagate into the spec. - contract-markdown.md: fail-closed clause now mentions only `prose compile`; `prose run` enforcement is deferred per the PR description. - compiler/index.prose.md: add a `skills_resolver` agent that owns the search-path order, scope aggregation, BYO invariant, and fail-closed semantics. Skill resolution is now a compiler/program-level responsibility, not a harness responsibility, so other harnesses running the compiler get the same behavior. - skills/declared.ts: add a header comment pointing at the program-level spec; this module is the harness implementation of `skills_resolver`. - examples/declared-skills/README.md: update wording to reference the compiler agent. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(ci): make smoke artifacts case-specific * fix(ci): use deterministic audit policy --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
388 lines
12 KiB
YAML
388 lines
12 KiB
YAML
name: OpenProse Release
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
version:
|
|
description: "Version to publish. Must match every declared OpenProse version, for example 0.13.0."
|
|
required: true
|
|
type: string
|
|
npm_tag:
|
|
description: "npm dist-tag for @openprose/prose-cli."
|
|
required: true
|
|
default: latest
|
|
type: choice
|
|
options:
|
|
- latest
|
|
- next
|
|
dry_run:
|
|
description: "Build and verify without publishing."
|
|
required: true
|
|
default: true
|
|
type: boolean
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
concurrency:
|
|
group: openprose-release-${{ inputs.version }}
|
|
cancel-in-progress: false
|
|
|
|
env:
|
|
PACKAGE_NAME: "@openprose/prose-cli"
|
|
RELEASE_ACTORS: "irl-dan josemontesdeoca"
|
|
|
|
jobs:
|
|
authorize:
|
|
name: Authorize and preflight release
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 10
|
|
outputs:
|
|
npm_tag: ${{ steps.release.outputs.npm_tag }}
|
|
release_tag: ${{ steps.release.outputs.release_tag }}
|
|
version: ${{ steps.release.outputs.version }}
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Setup Node
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: 22
|
|
|
|
- name: Validate release request
|
|
id: release
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
GITHUB_TRIGGERING_ACTOR: ${{ github.triggering_actor }}
|
|
INPUT_NPM_TAG: ${{ inputs.npm_tag }}
|
|
INPUT_VERSION: ${{ inputs.version }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
fail() {
|
|
printf '%s\n' "$*" >&2
|
|
exit 1
|
|
}
|
|
|
|
actor="${GITHUB_ACTOR}"
|
|
triggering_actor="${GITHUB_TRIGGERING_ACTOR:-$GITHUB_ACTOR}"
|
|
|
|
case " $RELEASE_ACTORS " in
|
|
*" $actor "*) ;;
|
|
*) fail "Unauthorized release actor: $actor" ;;
|
|
esac
|
|
|
|
case " $RELEASE_ACTORS " in
|
|
*" $triggering_actor "*) ;;
|
|
*) fail "Unauthorized triggering actor: $triggering_actor" ;;
|
|
esac
|
|
|
|
version="${INPUT_VERSION#v}"
|
|
./scripts/release-preflight.sh \
|
|
--version "$version" \
|
|
--npm-tag "$INPUT_NPM_TAG" \
|
|
--require-main \
|
|
--check-remote
|
|
|
|
{
|
|
printf 'npm_tag=%s\n' "$INPUT_NPM_TAG"
|
|
printf 'release_tag=v%s\n' "$version"
|
|
printf 'version=%s\n' "$version"
|
|
} >> "$GITHUB_OUTPUT"
|
|
|
|
verify-cli:
|
|
name: Verify CLI package
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 20
|
|
needs: authorize
|
|
defaults:
|
|
run:
|
|
working-directory: tools/cli
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Setup Node
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: 22
|
|
cache: npm
|
|
cache-dependency-path: tools/cli/package-lock.json
|
|
|
|
- name: Install dependencies
|
|
run: npm ci
|
|
|
|
- name: Typecheck
|
|
run: npm run typecheck
|
|
|
|
- name: Test
|
|
run: npm test
|
|
|
|
- name: Build
|
|
run: npm run build
|
|
|
|
- name: Audit production dependencies
|
|
run: npm run audit:policy
|
|
|
|
- name: Dry-run npm publish
|
|
run: npm publish --dry-run
|
|
|
|
verify-plugin:
|
|
name: Verify SKILL and plugin metadata
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 10
|
|
needs: authorize
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Version surfaces in sync
|
|
run: ./scripts/bump-version.sh --check
|
|
|
|
- name: Plugin copy in sync
|
|
run: ./scripts/sync-copy.sh --check
|
|
|
|
- name: Changelog has release notes
|
|
run: ./scripts/extract-changelog.sh "${{ needs.authorize.outputs.version }}"
|
|
|
|
- name: Validate Claude plugin manifest
|
|
run: |
|
|
node -e '
|
|
const fs = require("fs");
|
|
const m = JSON.parse(fs.readFileSync(".claude-plugin/plugin.json","utf8"));
|
|
if (!m.name || !m.version) { console.error("missing name/version"); process.exit(1); }
|
|
const skillsField = String(m.skills || "./skills").replace(/^\.\//,"").replace(/\/$/,"");
|
|
const target = skillsField + "/open-prose/SKILL.md";
|
|
if (!fs.existsSync(target)) { console.error("skills path not found: " + target); process.exit(1); }
|
|
console.log("ok: claude plugin manifest");
|
|
'
|
|
|
|
- name: Validate Codex plugin manifest
|
|
run: |
|
|
node -e '
|
|
const fs = require("fs");
|
|
const m = JSON.parse(fs.readFileSync(".codex-plugin/plugin.json","utf8"));
|
|
if (!m.name || !m.version) { console.error("missing name/version"); process.exit(1); }
|
|
if (!m.interface || !m.interface.displayName) { console.error("missing interface.displayName"); process.exit(1); }
|
|
const skillsField = String(m.skills || "./skills").replace(/^\.\//,"").replace(/\/$/,"");
|
|
const target = skillsField + "/open-prose/SKILL.md";
|
|
if (!fs.existsSync(target)) { console.error("skills path not found: " + target); process.exit(1); }
|
|
const mp = JSON.parse(fs.readFileSync(".agents/plugins/marketplace.json","utf8"));
|
|
if (!Array.isArray(mp.plugins) || mp.plugins[0].name !== "open-prose") { console.error("agents/plugins/marketplace.json mismatch"); process.exit(1); }
|
|
console.log("ok: codex plugin manifest");
|
|
'
|
|
|
|
- name: Plugin assets exist
|
|
run: |
|
|
test -f assets/plugin/logo.png
|
|
test -f assets/plugin/composer-icon.png
|
|
file assets/plugin/logo.png | grep -q 'PNG image data, 512 x 512'
|
|
file assets/plugin/composer-icon.png | grep -q 'PNG image data, 64 x 64'
|
|
|
|
- name: Codex interface has visual assets
|
|
run: |
|
|
jq -e '.interface.logo and .interface.composerIcon and .interface.brandColor and .interface.websiteURL' .codex-plugin/plugin.json
|
|
jq -re '.interface.brandColor' .codex-plugin/plugin.json | grep -E '^#[0-9A-Fa-f]{6}$'
|
|
|
|
release-assets:
|
|
name: Build ${{ matrix.target_os }}-${{ matrix.target_arch }} tarball
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 25
|
|
needs:
|
|
- authorize
|
|
- verify-cli
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- target_os: linux
|
|
target_arch: x64
|
|
- target_os: linux
|
|
target_arch: arm64
|
|
- target_os: darwin
|
|
target_arch: x64
|
|
- target_os: darwin
|
|
target_arch: arm64
|
|
defaults:
|
|
run:
|
|
working-directory: tools/cli
|
|
env:
|
|
TARGET_ARCH: ${{ matrix.target_arch }}
|
|
TARGET_OS: ${{ matrix.target_os }}
|
|
VERSION: ${{ needs.authorize.outputs.version }}
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Setup Node
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: 22
|
|
cache: npm
|
|
cache-dependency-path: tools/cli/package-lock.json
|
|
|
|
- name: Install dependencies
|
|
run: npm ci
|
|
|
|
- name: Build release tarball
|
|
run: |
|
|
set -euo pipefail
|
|
release_dir="$RUNNER_TEMP/prose-release"
|
|
|
|
npm run release:tarball -- \
|
|
--version "$VERSION" \
|
|
--os "$TARGET_OS" \
|
|
--arch "$TARGET_ARCH" \
|
|
--out-dir "$release_dir"
|
|
|
|
cd "$release_dir"
|
|
sha256sum --check "prose-$VERSION-$TARGET_OS-$TARGET_ARCH.tar.gz.sha256"
|
|
tar -tzf "prose-$VERSION-$TARGET_OS-$TARGET_ARCH.tar.gz" >/dev/null
|
|
|
|
- name: Smoke native Linux x64 tarball
|
|
if: ${{ matrix.target_os == 'linux' && matrix.target_arch == 'x64' }}
|
|
run: |
|
|
set -euo pipefail
|
|
release_dir="$RUNNER_TEMP/prose-release"
|
|
install_dir="$RUNNER_TEMP/prose-install"
|
|
bin_dir="$RUNNER_TEMP/prose-bin"
|
|
|
|
env \
|
|
PROSE_VERSION="$VERSION" \
|
|
PROSE_OS="$TARGET_OS" \
|
|
PROSE_ARCH="$TARGET_ARCH" \
|
|
PROSE_BASE_URL="file://$release_dir" \
|
|
PROSE_INSTALL_DIR="$install_dir" \
|
|
PROSE_BIN_DIR="$bin_dir" \
|
|
sh install.sh
|
|
|
|
"$bin_dir/prose" --version
|
|
"$bin_dir/prose" run hello.prose.md --harness mock
|
|
|
|
- name: Upload release tarball
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: cli-release-${{ matrix.target_os }}-${{ matrix.target_arch }}
|
|
path: ${{ runner.temp }}/prose-release/*
|
|
if-no-files-found: error
|
|
|
|
publish:
|
|
name: Publish OpenProse release
|
|
if: ${{ !inputs.dry_run }}
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 25
|
|
needs:
|
|
- authorize
|
|
- verify-cli
|
|
- verify-plugin
|
|
- release-assets
|
|
environment: release
|
|
permissions:
|
|
contents: write
|
|
id-token: write
|
|
defaults:
|
|
run:
|
|
working-directory: tools/cli
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
NPM_CONFIG_PROVENANCE: "true"
|
|
NPM_TAG: ${{ needs.authorize.outputs.npm_tag }}
|
|
RELEASE_TAG: ${{ needs.authorize.outputs.release_tag }}
|
|
VERSION: ${{ needs.authorize.outputs.version }}
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Setup Node for trusted publishing
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: 24
|
|
registry-url: https://registry.npmjs.org
|
|
cache: npm
|
|
cache-dependency-path: tools/cli/package-lock.json
|
|
|
|
- name: Install npm with trusted publishing support
|
|
run: npm install --global npm@latest
|
|
|
|
- name: Install dependencies
|
|
run: npm ci
|
|
|
|
- name: Download release assets
|
|
uses: actions/download-artifact@v4
|
|
with:
|
|
pattern: cli-release-*
|
|
path: ${{ runner.temp }}/prose-release-assets
|
|
merge-multiple: true
|
|
|
|
- name: Validate release assets
|
|
run: |
|
|
set -euo pipefail
|
|
asset_dir="$RUNNER_TEMP/prose-release-assets"
|
|
|
|
expected_assets="
|
|
prose-$VERSION-linux-x64.tar.gz
|
|
prose-$VERSION-linux-arm64.tar.gz
|
|
prose-$VERSION-darwin-x64.tar.gz
|
|
prose-$VERSION-darwin-arm64.tar.gz
|
|
"
|
|
|
|
for asset in $expected_assets; do
|
|
[ -f "$asset_dir/$asset" ] || {
|
|
printf 'Missing release asset: %s\n' "$asset" >&2
|
|
exit 1
|
|
}
|
|
[ -f "$asset_dir/$asset.sha256" ] || {
|
|
printf 'Missing release checksum: %s.sha256\n' "$asset" >&2
|
|
exit 1
|
|
}
|
|
done
|
|
|
|
cd "$asset_dir"
|
|
for checksum in *.sha256; do
|
|
sha256sum --check "$checksum"
|
|
done
|
|
|
|
- name: Create draft GitHub release
|
|
id: create_draft_release
|
|
run: |
|
|
set -euo pipefail
|
|
asset_dir="$RUNNER_TEMP/prose-release-assets"
|
|
notes_path="$RUNNER_TEMP/prose-release-notes.md"
|
|
|
|
../../scripts/extract-changelog.sh "$VERSION" > "$notes_path"
|
|
cat >> "$notes_path" <<EOF
|
|
|
|
## Install
|
|
|
|
Install with npm:
|
|
|
|
npm install --global $PACKAGE_NAME@$VERSION
|
|
|
|
Install with the release tarball:
|
|
|
|
curl -fsSL https://raw.githubusercontent.com/openprose/prose/main/tools/cli/install.sh | sh
|
|
EOF
|
|
|
|
gh release create "$RELEASE_TAG" "$asset_dir"/* \
|
|
--target "$GITHUB_SHA" \
|
|
--title "OpenProse $VERSION" \
|
|
--notes-file "$notes_path" \
|
|
--draft
|
|
|
|
- name: Publish npm package
|
|
id: publish_npm
|
|
run: |
|
|
set -euo pipefail
|
|
node --version
|
|
npm --version
|
|
npm publish --access public --tag "$NPM_TAG"
|
|
|
|
- name: Publish GitHub release
|
|
id: publish_github_release
|
|
run: gh release edit "$RELEASE_TAG" --draft=false
|
|
|
|
- name: Delete draft release after npm publish failure
|
|
if: ${{ failure() && steps.create_draft_release.outcome == 'success' && steps.publish_npm.outcome == 'failure' }}
|
|
run: gh release delete "$RELEASE_TAG" --yes --cleanup-tag || true
|