From da7b738c0af0cb571a8a925b75ed3cbb5b7d0d8f Mon Sep 17 00:00:00 2001 From: Brendan Ryan <1572504+brendanjryan@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:23:01 -0700 Subject: [PATCH 1/6] feat: generate plugin skills from a registry of source repositories --- .github/sts/skill-sync.sts.yaml | 8 +++ .github/workflows/sync-skills.yml | 101 ++++++++++++++++++++++++++++++ .github/workflows/validate.yml | 4 ++ CONTRIBUTING.md | 11 +++- README.md | 6 +- scripts/bump-plugin-version.ts | 21 +++++++ skills.json | 32 ++++++++++ skills.lock.json | 25 ++++++++ test/bump-plugin-version.test.ts | 22 +++++++ test/repository.test.ts | 17 ++++- 10 files changed, 241 insertions(+), 6 deletions(-) create mode 100644 .github/sts/skill-sync.sts.yaml create mode 100644 .github/workflows/sync-skills.yml create mode 100644 scripts/bump-plugin-version.ts create mode 100644 skills.json create mode 100644 skills.lock.json create mode 100644 test/bump-plugin-version.test.ts diff --git a/.github/sts/skill-sync.sts.yaml b/.github/sts/skill-sync.sts.yaml new file mode 100644 index 0000000..ce93b2c --- /dev/null +++ b/.github/sts/skill-sync.sts.yaml @@ -0,0 +1,8 @@ +# Allow only the main-branch skill sync workflow to open its generated +# update pull requests in this repository. +subject: repo:tempoxyz@211589300/plugins@1356140289:ref:refs/heads/main +claim_pattern: + job_workflow_ref: tempoxyz/plugins/\.github/workflows/sync-skills\.yml@refs/heads/main +permissions: + contents: write + pull_requests: write diff --git a/.github/workflows/sync-skills.yml b/.github/workflows/sync-skills.yml new file mode 100644 index 0000000..5c6b20e --- /dev/null +++ b/.github/workflows/sync-skills.yml @@ -0,0 +1,101 @@ +name: Sync skills + +# Each plugin's skills are copied from the repositories listed in skills.json. +# One pull request per plugin keeps a failing source from blocking the rest; +# generated-only pull requests merge once validation passes. +on: + schedule: + - cron: "*/30 * * * *" + workflow_dispatch: + repository_dispatch: + types: [skill-source-updated] + +concurrency: + group: sync-skills + cancel-in-progress: false + +permissions: + contents: read + +jobs: + plugins: + name: List plugins + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + id-token: write + outputs: + plugins: ${{ steps.list.outputs.plugins }} + steps: + - name: Secure runner + uses: tempoxyz/gh-actions/actions/secure-runner@55fe3b63d9612623cc51e6c62d9d62fbcb175fb0 # 2026-09-29T01-25-15Z-55fe3b63 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - id: list + run: echo "plugins=$(jq -c '[.skills[] | .plugin // .name] | unique' skills.json)" >> "$GITHUB_OUTPUT" + + sync: + name: Sync ${{ matrix.plugin }} + needs: plugins + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + id-token: write + strategy: + fail-fast: false + matrix: + plugin: ${{ fromJSON(needs.plugins.outputs.plugins) }} + steps: + - name: Secure runner + uses: tempoxyz/gh-actions/actions/secure-runner@55fe3b63d9612623cc51e6c62d9d62fbcb175fb0 # 2026-09-29T01-25-15Z-55fe3b63 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Sync skills from their sources + id: sync + uses: tempoxyz/gh-actions/actions/skill-sync@060c0d9179e6a74462c1fcd0c9840c5416222029 # brendanjryan/skill-sync + with: + only: ${{ matrix.plugin }} + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + if: steps.sync.outputs.changed == 'true' + with: + node-version: 24 + cache: npm + - name: Bump the plugin version and regenerate manifests + if: steps.sync.outputs.changed == 'true' + env: + PLUGIN: ${{ matrix.plugin }} + run: | + npm ci --ignore-scripts + npx tsx scripts/bump-plugin-version.ts "$PLUGIN" + npm run build + npm run update:goldens + - name: Fetch GitHub token via STS + if: steps.sync.outputs.changed == 'true' + id: sts + uses: tempoxyz/gh-actions/actions/github-sts@55fe3b63d9612623cc51e6c62d9d62fbcb175fb0 # 2026-09-29T01-25-15Z-55fe3b63 + with: + policy: skill-sync + ttl: 10m + - name: Open or update the sync pull request + if: steps.sync.outputs.changed == 'true' + uses: tempoxyz/gh-actions/actions/create-pull-request@3554f9435b9ff4da58e8931acc517c1a4814a7e2 # brendanjryan/create-pr-keep-open-prs + with: + token: ${{ steps.sts.outputs.token }} + branch: automation/sync-skills/${{ matrix.plugin }} + commit-message: "chore(${{ matrix.plugin }}): sync skills" + title: "chore(${{ matrix.plugin }}): sync skills" + body: | + Generated from `skills.json` by `.github/workflows/sync-skills.yml`. Edit the source, not these copies. + + ${{ steps.sync.outputs.summary }} + add-paths: | + plugins/${{ matrix.plugin }} + registry/${{ matrix.plugin }} + skills.lock.json + manifest.config.ts + test/fixtures/golden + auto-merge: squash diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index e3e5820..a43fffb 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -29,6 +29,10 @@ jobs: with: node-version: 24 cache: npm + - name: Check generated skills + uses: tempoxyz/gh-actions/actions/skill-sync@060c0d9179e6a74462c1fcd0c9840c5416222029 # brendanjryan/skill-sync + with: + mode: check - run: npm ci --ignore-scripts - run: npm test - name: Validate Claude marketplace diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 454b794..c542acd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -43,7 +43,14 @@ the independently versioned plugin releases. - Production MCP endpoints are smoke-tested daily. - All changes must pass validation and use signed commits. - Use the canonical Tempo and Mercator brand assets. -- The Mercator skill is generated upstream. Do not edit its generated copy. -- Upstream Mercator changes arrive through signed synchronization pull requests. +- Skills under `plugins/*/skills/` are generated from the repositories listed + in [`skills.json`](./skills.json). Edit the source repository, not the copy; + CI rejects copies that no longer match `skills.lock.json`. +- `.github/workflows/sync-skills.yml` opens one signed pull request per plugin + when a source changes, bumps that plugin's patch version, and merges once + validation passes. +- To distribute a new skill, add an entry to `skills.json`. A private source + repository also needs a `.github/sts/skill-sync-read.sts.yaml` read policy; + see the [skill-sync action](https://github.com/tempoxyz/gh-actions/tree/main/actions/skill-sync). - Report marketplace compatibility changes through an issue and update the corresponding manifest, test, and submission notes together. diff --git a/README.md b/README.md index 4c0582b..5de038a 100644 --- a/README.md +++ b/README.md @@ -59,9 +59,9 @@ npm run test:mcp # Smoke-test production MCP endpoints ``` Edit [`manifest.config.ts`](./manifest.config.ts), not generated JSON manifests. -Repository tooling is TypeScript and tested with Vitest. The Mercator skill is -generated upstream; changes arrive here through signed synchronization pull -requests. +Repository tooling is TypeScript and tested with Vitest. Skills are generated +from the source repositories listed in [`skills.json`](./skills.json) and arrive +through signed synchronization pull requests; edit them upstream. ## Contributing diff --git a/scripts/bump-plugin-version.ts b/scripts/bump-plugin-version.ts new file mode 100644 index 0000000..58b415a --- /dev/null +++ b/scripts/bump-plugin-version.ts @@ -0,0 +1,21 @@ +import { readFileSync, writeFileSync } from 'node:fs' +import { resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +const configPath = fileURLToPath(new URL('../manifest.config.ts', import.meta.url)) + +/** Bump one plugin's patch version so installed clients pick up synced skill changes. */ +export const bumpPluginVersion = (config: string, name: string): string => { + const pattern = new RegExp(`(\\n name: '${name}',\\n version: ')(\\d+)\\.(\\d+)\\.(\\d+)(')`) + const match = pattern.exec(config) + if (!match) throw new Error(`manifest.config.ts has no version for plugin "${name}"`) + const [, prefix, major, minor, patch, suffix] = match + return config.replace(pattern, `${prefix}${major}.${minor}.${Number(patch) + 1}${suffix}`) +} + +const isMain = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url) +if (isMain) { + const name = process.argv[2] + if (!name) throw new Error('usage: bump-plugin-version.ts ') + writeFileSync(configPath, bumpPluginVersion(readFileSync(configPath, 'utf8'), name)) +} diff --git a/skills.json b/skills.json new file mode 100644 index 0000000..37799ef --- /dev/null +++ b/skills.json @@ -0,0 +1,32 @@ +{ + "skills": [ + { + "name": "docs", + "plugin": "docs", + "repo": "tempoxyz/docs", + "path": "SKILL.md", + "ref": "main" + }, + { + "name": "wallet", + "plugin": "wallet", + "repo": "tempoxyz/wallet-cli", + "path": "SKILL.md", + "ref": "release" + }, + { + "name": "mercator", + "plugin": "mercator", + "repo": "tempoxyz/mercator", + "path": "skills/mercator", + "ref": "main" + }, + { + "name": "mercator-setup", + "plugin": "mercator", + "repo": "tempoxyz/mercator", + "path": "skills/mercator-setup", + "ref": "main" + } + ] +} diff --git a/skills.lock.json b/skills.lock.json new file mode 100644 index 0000000..744835e --- /dev/null +++ b/skills.lock.json @@ -0,0 +1,25 @@ +{ + "version": 1, + "skills": [ + { + "name": "mercator", + "plugin": "mercator", + "repo": "tempoxyz/mercator", + "path": "skills/mercator", + "ref": "main", + "commit": "598f247026fbe99d87e10ccf1cef914918eb5389", + "digest": "sha256:2832e9166e61033bc8c16d7ecedb807c14f5e9dc8d95eef6f44c11fd478bc22e", + "dest": "plugins/mercator/skills/mercator" + }, + { + "name": "mercator-setup", + "plugin": "mercator", + "repo": "tempoxyz/mercator", + "path": "skills/mercator-setup", + "ref": "main", + "commit": "598f247026fbe99d87e10ccf1cef914918eb5389", + "digest": "sha256:d7c7f672a8327800a09086604f508253b3a452689b338e7b337e5ec674a1d8d4", + "dest": "plugins/mercator/skills/mercator-setup" + } + ] +} diff --git a/test/bump-plugin-version.test.ts b/test/bump-plugin-version.test.ts new file mode 100644 index 0000000..9b03245 --- /dev/null +++ b/test/bump-plugin-version.test.ts @@ -0,0 +1,22 @@ +import { readFileSync } from 'node:fs' +import { describe, expect, it } from 'vitest' +import { bumpPluginVersion } from '../scripts/bump-plugin-version.ts' + +const config = readFileSync(new URL('../manifest.config.ts', import.meta.url), 'utf8') +const version = (source: string, name: string): string | undefined => + new RegExp(`\\n name: '${name}',\\n version: '([^']+)'`).exec(source)?.[1] + +describe('bumpPluginVersion', () => { + it.each(['docs', 'wallet', 'mercator'])('bumps only the %s patch version', (name) => { + const bumped = bumpPluginVersion(config, name) + const [major, minor, patch] = version(config, name)!.split('.') + expect(version(bumped, name)).toBe(`${major}.${minor}.${Number(patch) + 1}`) + for (const other of ['docs', 'wallet', 'mercator'].filter((plugin) => plugin !== name)) { + expect(version(bumped, other)).toBe(version(config, other)) + } + }) + + it('rejects an unknown plugin', () => { + expect(() => bumpPluginVersion(config, 'missing')).toThrow(/no version for plugin "missing"/) + }) +}) diff --git a/test/repository.test.ts b/test/repository.test.ts index 9f8ec25..c2f44f3 100644 --- a/test/repository.test.ts +++ b/test/repository.test.ts @@ -62,7 +62,7 @@ describe('repository validation', () => { test('Tempo Docs skill stays read-only', () => { const skill = readFileSync(join(root, 'plugins/docs/skills/docs/SKILL.md'), 'utf8') - expect(skill).toMatch(/This plugin is read-only/) + expect(skill).toMatch(/This (plugin|skill) is read-only/) expect(skill).toMatch(/must not install software/) }) @@ -141,6 +141,21 @@ describe('repository validation', () => { .toMatch(/value: "mercator"/) }) + test('every synced skill ships in a cataloged plugin', () => { + const registry = readJson<{ skills: Array<{ name: string; plugin?: string }> }>('skills.json') + for (const skill of registry.skills) { + expect(pluginNames, skill.name).toContain(skill.plugin ?? skill.name) + } + }) + + test('skill sync trust is limited to its main-branch workflow', () => { + const policy = readFileSync(join(root, '.github/sts/skill-sync.sts.yaml'), 'utf8') + expect(policy).toMatch(/plugins@1356140289:ref:refs\/heads\/main/) + expect(policy).toMatch(/sync-skills\\\.yml@refs\/heads\/main/) + expect(policy).toMatch(/contents: write/) + expect(policy).toMatch(/pull_requests: write/) + }) + test('Mercator sync trust is limited to its main-branch workflow', () => { const policy = readFileSync( join(root, '.github/sts/mercator-skill-sync.sts.yaml'), From f3d46bc39c99fe4477b537cd746ca61bbf773ceb Mon Sep 17 00:00:00 2001 From: Brendan Ryan <1572504+brendanjryan@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:05:52 -0700 Subject: [PATCH 2/6] ci: pin create-pull-request to the merged release --- .github/workflows/sync-skills.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sync-skills.yml b/.github/workflows/sync-skills.yml index 5c6b20e..1bce60f 100644 --- a/.github/workflows/sync-skills.yml +++ b/.github/workflows/sync-skills.yml @@ -82,7 +82,7 @@ jobs: ttl: 10m - name: Open or update the sync pull request if: steps.sync.outputs.changed == 'true' - uses: tempoxyz/gh-actions/actions/create-pull-request@3554f9435b9ff4da58e8931acc517c1a4814a7e2 # brendanjryan/create-pr-keep-open-prs + uses: tempoxyz/gh-actions/actions/create-pull-request@a4fa1d51aa4014d86628f43b1dff8ba88219e04d # 2026-10-15T00-03-55Z-a4fa1d51 with: token: ${{ steps.sts.outputs.token }} branch: automation/sync-skills/${{ matrix.plugin }} From c59991a91f2356a1ee8f6cb021b310d9e274a4a2 Mon Sep 17 00:00:00 2001 From: Brendan Ryan <1572504+brendanjryan@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:09:07 -0700 Subject: [PATCH 3/6] fix: stage the registry directory so plugins without an MCP record can sync --- .github/workflows/sync-skills.yml | 2 +- test/repository.test.ts | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/sync-skills.yml b/.github/workflows/sync-skills.yml index 1bce60f..8794513 100644 --- a/.github/workflows/sync-skills.yml +++ b/.github/workflows/sync-skills.yml @@ -94,7 +94,7 @@ jobs: ${{ steps.sync.outputs.summary }} add-paths: | plugins/${{ matrix.plugin }} - registry/${{ matrix.plugin }} + registry skills.lock.json manifest.config.ts test/fixtures/golden diff --git a/test/repository.test.ts b/test/repository.test.ts index c2f44f3..a80e314 100644 --- a/test/repository.test.ts +++ b/test/repository.test.ts @@ -1,5 +1,5 @@ import { createHash } from 'node:crypto' -import { readFileSync, readdirSync } from 'node:fs' +import { existsSync, readFileSync, readdirSync } from 'node:fs' import { join } from 'node:path' import { fileURLToPath } from 'node:url' import { describe, expect, it, test } from 'vitest' @@ -148,6 +148,16 @@ describe('repository validation', () => { } }) + test.each(pluginNames)('skill sync stages only paths that exist for %s', (name) => { + const workflow = readFileSync(join(root, '.github/workflows/sync-skills.yml'), 'utf8') + const addPaths = /add-paths: \|\n((?: {12}.+\n)+)/.exec(workflow)?.[1] + expect(addPaths).toBeDefined() + for (const pathspec of addPaths!.trim().split('\n')) { + const path = pathspec.trim().replace('${{ matrix.plugin }}', name) + expect(existsSync(join(root, path)), path).toBe(true) + } + }) + test('skill sync trust is limited to its main-branch workflow', () => { const policy = readFileSync(join(root, '.github/sts/skill-sync.sts.yaml'), 'utf8') expect(policy).toMatch(/plugins@1356140289:ref:refs\/heads\/main/) From 2d7081ac2abffdf6960ffd6603538f904c031460 Mon Sep 17 00:00:00 2001 From: Brendan Ryan <1572504+brendanjryan@users.noreply.github.com> Date: Thu, 15 Oct 2026 10:05:48 -0700 Subject: [PATCH 4/6] ci: pin skill-sync to the merged release --- .github/workflows/sync-skills.yml | 2 +- .github/workflows/validate.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/sync-skills.yml b/.github/workflows/sync-skills.yml index 8794513..af2865e 100644 --- a/.github/workflows/sync-skills.yml +++ b/.github/workflows/sync-skills.yml @@ -56,7 +56,7 @@ jobs: persist-credentials: false - name: Sync skills from their sources id: sync - uses: tempoxyz/gh-actions/actions/skill-sync@060c0d9179e6a74462c1fcd0c9840c5416222029 # brendanjryan/skill-sync + uses: tempoxyz/gh-actions/actions/skill-sync@caf7c59ea3a8359618ca5640d123a161a2a87721 # 2026-10-15T17-04-41Z-caf7c59e with: only: ${{ matrix.plugin }} - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index a43fffb..13e6ab5 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -30,7 +30,7 @@ jobs: node-version: 24 cache: npm - name: Check generated skills - uses: tempoxyz/gh-actions/actions/skill-sync@060c0d9179e6a74462c1fcd0c9840c5416222029 # brendanjryan/skill-sync + uses: tempoxyz/gh-actions/actions/skill-sync@caf7c59ea3a8359618ca5640d123a161a2a87721 # 2026-10-15T17-04-41Z-caf7c59e with: mode: check - run: npm ci --ignore-scripts From df6e2e39ec6ab14349d8bc277135d31b8737cfaa Mon Sep 17 00:00:00 2001 From: Brendan Ryan <1572504+brendanjryan@users.noreply.github.com> Date: Thu, 15 Oct 2026 10:06:11 -0700 Subject: [PATCH 5/6] ci: align sync workflow pins with main --- .github/workflows/sync-skills.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/sync-skills.yml b/.github/workflows/sync-skills.yml index af2865e..6ffbc42 100644 --- a/.github/workflows/sync-skills.yml +++ b/.github/workflows/sync-skills.yml @@ -29,7 +29,7 @@ jobs: plugins: ${{ steps.list.outputs.plugins }} steps: - name: Secure runner - uses: tempoxyz/gh-actions/actions/secure-runner@55fe3b63d9612623cc51e6c62d9d62fbcb175fb0 # 2026-09-29T01-25-15Z-55fe3b63 + uses: tempoxyz/gh-actions/actions/secure-runner@947d9a3ccb2425a9c94d563fd721cc0e33c89e26 # 2026-10-15T00-15-06Z-947d9a3c - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false @@ -50,7 +50,7 @@ jobs: plugin: ${{ fromJSON(needs.plugins.outputs.plugins) }} steps: - name: Secure runner - uses: tempoxyz/gh-actions/actions/secure-runner@55fe3b63d9612623cc51e6c62d9d62fbcb175fb0 # 2026-09-29T01-25-15Z-55fe3b63 + uses: tempoxyz/gh-actions/actions/secure-runner@947d9a3ccb2425a9c94d563fd721cc0e33c89e26 # 2026-10-15T00-15-06Z-947d9a3c - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false @@ -76,7 +76,7 @@ jobs: - name: Fetch GitHub token via STS if: steps.sync.outputs.changed == 'true' id: sts - uses: tempoxyz/gh-actions/actions/github-sts@55fe3b63d9612623cc51e6c62d9d62fbcb175fb0 # 2026-09-29T01-25-15Z-55fe3b63 + uses: tempoxyz/gh-actions/actions/github-sts@947d9a3ccb2425a9c94d563fd721cc0e33c89e26 # 2026-10-15T00-15-06Z-947d9a3c with: policy: skill-sync ttl: 10m From df75b19ba608da01168563e29a8ee19fc16bd447 Mon Sep 17 00:00:00 2001 From: Brendan Ryan <1572504+brendanjryan@users.noreply.github.com> Date: Thu, 15 Oct 2026 10:09:33 -0700 Subject: [PATCH 6/6] fix: read the docs skill from public/SKILL.md, which SKILL.md now links to --- skills.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skills.json b/skills.json index 37799ef..c4fe1ec 100644 --- a/skills.json +++ b/skills.json @@ -4,7 +4,7 @@ "name": "docs", "plugin": "docs", "repo": "tempoxyz/docs", - "path": "SKILL.md", + "path": "public/SKILL.md", "ref": "main" }, {