Repository navigation
feat: harden native plugin publishing lifecycle #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 3 commits
Commits
Show all changes
44 commits
Select commit
Hold shift + click to select a range
9d06f01
fix: bootstrap the initial plugin release
nathanvale 83fd820
Address PR review feedback (#11)
nathanvale a1e3dab
fix: enforce native plugin release metadata
nathanvale 246f1e3
Address PR review feedback (#11)
nathanvale 6930f18
feat(init): reset recipient release lineage to 0.1.0
nathanvale 96aad7e
feat(payload): enforce one safe regular-file-only payload inventory
nathanvale 5396fd3
feat(metadata): own native package rules with two named profiles
nathanvale e6451fe
feat(release): bind proof, tag, release, and repair to one commit
nathanvale 3127a5b
feat(ci): gate installable payload changes on releasable PR title
nathanvale 5f1b06f
feat(readiness): verify human-owned publication safeguards read-only
nathanvale 57cd1d3
feat(proof): prove native install, cache, trust, update, and rollback
nathanvale d1e859f
feat(canary): qualify publishing-system changes with hosted Git canaries
nathanvale 048faae
docs: align operator guidance with the hardened release contract
nathanvale d00e0fb
docs(plan): add reviewed plugin publishing hardening plan
nathanvale 4c4dd44
fix(ci): isolate packaging fixture from GitHub SHA
nathanvale 04fb28a
Address PR review feedback (#11)
nathanvale 79cd078
fix: close release and canary review gaps
nathanvale 64b60ea
fix: exercise centralized release impact policy
nathanvale c55082c
fix: bind final release validation to candidate
nathanvale 95c4fb2
fix: close release repair review gaps
nathanvale d32f637
fix: fetch fork canary heads from source
nathanvale 4c95f28
fix: make hosted and repair gates fail closed
nathanvale ebbc03e
fix: bind release gates across rename and trust boundaries
nathanvale f3f8d03
fix: execute release gates from trusted code
nathanvale 232ec8e
fix: isolate release impact in trusted workflow
nathanvale 9c6954c
fix: preserve canary generator and transport boundaries
nathanvale aafeec5
fix: close release review gaps (#11)
nathanvale 4cf8727
docs: bind release automation identity (#11)
nathanvale 7180e3f
fix: bind package source to checkout (#11)
nathanvale 7832ac9
fix: resolve publication review findings (#11)
nathanvale 5cfdcdc
fix: pin bootstrap release to v0.1.0 (#11)
nathanvale fc0c99d
docs: clarify release automation credentials (#11)
nathanvale fef9097
fix: require release automation token (#11)
nathanvale 2a9efcf
fix: harden release and canary admission (#11)
nathanvale 6b9b3b6
fix: close release review gaps
nathanvale 8660009
fix: fail closed on workflow policy gaps
nathanvale 24034b5
fix: sanitize public canary publication
nathanvale 9096fa6
fix: prove sanitized public canaries
nathanvale 7036418
fix: preserve unusual archive filenames
nathanvale 7ea726a
fix: detect list-item workflow actions
nathanvale ed5a995
fix: resolve release qualification review findings (#11)
nathanvale fac5e40
fix: close remaining release review gaps
nathanvale cb182a3
fix: check main linear history policy
nathanvale bf01654
fix: verify hosted canary credentials (#11)
nathanvale File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1 @@ | ||
| { | ||
| ".": "0.1.0" | ||
| } | ||
| {} | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +0,0 @@ | ||
| # Changelog | ||
|
|
||
| Release Please maintains this file from Conventional Commit titles when the generated release pull request is merged. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| import { existsSync, readFileSync } from "node:fs" | ||
| import { join, resolve } from "node:path" | ||
|
|
||
| import { expect, test } from "bun:test" | ||
|
|
||
| import { renderGeneratedFiles, type PluginConfig } from "./plugin-config" | ||
|
|
||
| const root = resolve(import.meta.dir, "..") | ||
| const pluginRoot = join(root, "plugin") | ||
| const config = JSON.parse(readFileSync(join(root, "plugin.config.json"), "utf8")) as PluginConfig | ||
| const claudeManifest = JSON.parse( | ||
| readFileSync(join(pluginRoot, ".claude-plugin", "plugin.json"), "utf8"), | ||
| ) | ||
| const codexManifest = JSON.parse( | ||
| readFileSync(join(pluginRoot, ".codex-plugin", "plugin.json"), "utf8"), | ||
| ) | ||
|
|
||
| const strictSemver = | ||
| /^(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-]+)*)?$/ | ||
|
|
||
| test("checked-in native manifests satisfy the published harness contracts", () => { | ||
| for (const field of [ | ||
| "name", | ||
| "version", | ||
| "description", | ||
| "author", | ||
| "repository", | ||
| "license", | ||
| "keywords", | ||
| "skills", | ||
| "hooks", | ||
| ]) { | ||
| expect(claudeManifest[field], `Claude manifest field ${field}`).toBeDefined() | ||
| expect(codexManifest[field], `Codex manifest field ${field}`).toBeDefined() | ||
| } | ||
|
|
||
| expect(claudeManifest).toMatchObject({ | ||
| name: config.name, | ||
| displayName: config.displayName, | ||
| version: config.version, | ||
| description: config.description, | ||
| author: config.author, | ||
| repository: config.repository, | ||
| license: config.license, | ||
| keywords: config.keywords, | ||
| }) | ||
| expect(codexManifest).toMatchObject({ | ||
| name: config.name, | ||
| version: config.version, | ||
| description: config.description, | ||
| author: config.author, | ||
| repository: config.repository, | ||
| license: config.license, | ||
| keywords: config.keywords, | ||
| }) | ||
|
|
||
| expect(config.version).toMatch(strictSemver) | ||
| expect(config.displayName.length).toBeLessThanOrEqual(30) | ||
| expect(codexManifest.interface).toMatchObject({ | ||
| displayName: config.displayName, | ||
| shortDescription: config.shortDescription, | ||
| longDescription: config.longDescription, | ||
| developerName: config.author.name, | ||
| category: config.category, | ||
| capabilities: config.capabilities, | ||
| defaultPrompt: config.defaultPrompts, | ||
| }) | ||
| expect(config.shortDescription).not.toContain("\n") | ||
| expect(config.shortDescription.length).toBeLessThanOrEqual(30) | ||
| expect(config.longDescription.length).toBeLessThanOrEqual(4_000) | ||
| expect(config.capabilities.length).toBeLessThanOrEqual(20) | ||
| expect(config.defaultPrompts.length).toBeLessThanOrEqual(3) | ||
| expect(config.defaultPrompts.every((prompt: string) => prompt.length <= 128)).toBe(true) | ||
|
|
||
| for (const manifest of [claudeManifest, codexManifest]) { | ||
| for (const field of ["skills", "hooks"]) { | ||
| const path = manifest[field] | ||
| expect(path.startsWith("./"), `${field} must be plugin-relative`).toBe(true) | ||
| expect(existsSync(join(pluginRoot, path)), `${field} target must exist`).toBe(true) | ||
| } | ||
| } | ||
| }) | ||
|
|
||
| test.each([ | ||
| ["name", (value: PluginConfig) => (value.name = "Not Valid"), "plugin name"], | ||
| ["semantic version", (value: PluginConfig) => (value.version = "01.0.0"), "semantic versioning"], | ||
| ["required display name", (value: PluginConfig) => (value.displayName = ""), "are required"], | ||
| ["display name", (value: PluginConfig) => (value.displayName = "x".repeat(31)), "displayName"], | ||
| ["developer name", (value: PluginConfig) => (value.author.name = "x".repeat(81)), "author.name"], | ||
| ["empty short description", (value: PluginConfig) => (value.shortDescription = ""), "shortDescription"], | ||
| ["short description line", (value: PluginConfig) => (value.shortDescription = "two\nlines"), "shortDescription"], | ||
| ["short description length", (value: PluginConfig) => (value.shortDescription = "x".repeat(31)), "shortDescription"], | ||
| ["long description", (value: PluginConfig) => (value.longDescription = ""), "longDescription"], | ||
| ["long description length", (value: PluginConfig) => (value.longDescription = "x".repeat(4_001)), "longDescription"], | ||
| ["category", (value: PluginConfig) => (value.category = "Unknown"), "category"], | ||
| ["capability count", (value: PluginConfig) => (value.capabilities = Array(21).fill("Read")), "capabilities"], | ||
| ["empty capability", (value: PluginConfig) => (value.capabilities = [""]), "capabilities"], | ||
| ["capability length", (value: PluginConfig) => (value.capabilities = ["x".repeat(121)]), "capabilities"], | ||
| ["repository", (value: PluginConfig) => (value.repository = "git@example.com:plugin.git"), "repository"], | ||
| ["canary owner", (value: PluginConfig) => (value.canary.owner = "not_valid"), "canary.owner"], | ||
| ["prompt count", (value: PluginConfig) => (value.defaultPrompts = Array(4).fill("Run")), "defaultPrompts"], | ||
| ["prompt length", (value: PluginConfig) => (value.defaultPrompts = ["x".repeat(129)]), "defaultPrompts"], | ||
| ] as const)("rejects invalid published %s metadata", (_name, mutate, message) => { | ||
| const invalid = structuredClone(config) | ||
| mutate(invalid) | ||
| expect(() => renderGeneratedFiles(invalid)).toThrow(message) | ||
| }) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.