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
72 changes: 53 additions & 19 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -1,25 +1,30 @@
{
"name": "Validate Design Patterns release",
"name": "Publish Design Patterns release",
"on": {
"release": {
"types": ["published"]
},
"workflow_dispatch": {}
},
"permissions": {
"contents": "read",
"id-token": "write"
"push": {
"tags": ["v*"]
}
},
"permissions": {},
"jobs": {
"publish": {
"validate": {
"runs-on": "ubuntu-latest",
"environment": "marketplace-production",
"permissions": {
"contents": "read"
},
"outputs": {
"version": "${{ steps.release.outputs.version }}",
"release_root": "${{ steps.release.outputs.release_root }}"
},
"steps": [
{
"uses": "actions/checkout@v4"
"uses": "actions/checkout@11d5960a326750d5838078e36cf38b85af677262",
"with": {
"persist-credentials": false
}
},
{
"uses": "actions/setup-python@v5",
"uses": "actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065",
"with": {
"python-version": "3.12"
}
Expand All @@ -33,18 +38,47 @@
"run": "set -euo pipefail\nmkdir -p dist\npython3 scripts/release_inventory.py > dist/release-inventory.json\npython3 scripts/release_inventory.py > \"$RUNNER_TEMP/release-inventory-second.json\"\ncmp dist/release-inventory.json \"$RUNNER_TEMP/release-inventory-second.json\"\npython3 scripts/build_release.py --output \"$RUNNER_TEMP/design-patterns-first\"\npython3 scripts/build_release.py --output \"$RUNNER_TEMP/design-patterns-second\"\ndiff -qr \"$RUNNER_TEMP/design-patterns-first\" \"$RUNNER_TEMP/design-patterns-second\"\npython3 scripts/build_release.py\n"
},
{
"uses": "actions/upload-artifact@v4",
"name": "Install the immutable audited publisher",
"uses": "ByteDeskAI/marketplace-publisher/.github/actions/setup-bdm@43fa98d57a0d7a52f6c79b8f72409d09a424a541",
"with": {
"name": "design-patterns-release-candidate",
"path": "bytedesk-package.yaml\ndist/design-patterns\ndist/release-inventory.json\n",
"if-no-files-found": "error"
"expected-sha256": "e807b92ee362c7fe146ae0913a0caa306a9f2d6ea7798cd1c44d0bd03b785e3d"
}
},
{
"name": "Fail closed until bdm supports trusted-publisher OIDC",
"run": "echo \"::error title=Marketplace publication blocked::ByteDesk marketplace publication is blocked because bdm does not yet acquire GitHub Actions OIDC credentials.\"\necho \"Do not substitute a PAT, token, raw Actions JWT, or another secret.\"\nexit 1\n"
"id": "release",
"name": "Validate the exact publication candidate",
"env": {
"EXPECTED_PACKAGE": "@bytedesk/design-patterns",
"RESULT_PATH": "${{ runner.temp }}/bdm-validation.json"
},
"run": "set -euo pipefail\numask 077\n\"$RUNNER_TEMP/bytedesk-publisher/bdm\" validate bytedesk-package.yaml > \"$RESULT_PATH\"\npython3 - <<'PY'\nimport json\nimport os\nimport re\nfrom pathlib import Path\n\ndef closed(pairs):\n result = {}\n for key, value in pairs:\n if key in result:\n raise ValueError('duplicate JSON member')\n result[key] = value\n return result\n\nraw = Path(os.environ['RESULT_PATH']).read_bytes()\nif len(raw) > (1 << 20) or not raw.endswith(b'\\n'):\n raise SystemExit('invalid validation output framing')\ndocument = json.loads(raw, object_pairs_hook=closed)\nrequired = {'schemaVersion', 'package', 'version', 'manifestDigest', 'releaseRootDigest', 'validatorRevision', 'variants'}\nif not isinstance(document, dict) or set(document) != required or document['schemaVersion'] != 1:\n raise SystemExit('unexpected validation output contract')\nversion = document['version']\nroot = document['releaseRootDigest']\nif document['package'] != os.environ['EXPECTED_PACKAGE'] or os.environ['GITHUB_REF_NAME'] != 'v' + version:\n raise SystemExit('release identity does not match tag')\nif not isinstance(version, str) or re.fullmatch(r'(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z.-]+)?(?:\\+[0-9A-Za-z.-]+)?', version) is None:\n raise SystemExit('release version is invalid')\nif not isinstance(root, str) or re.fullmatch(r'sha256:[a-f0-9]{64}', root) is None:\n raise SystemExit('release root is invalid')\nwith open(os.environ['GITHUB_OUTPUT'], 'a', encoding='utf-8') as output:\n output.write(f'version={version}\\nrelease_root={root}\\n')\nPY\n"
},
{
"uses": "actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02",
"with": {
"name": "bdm-publication-source",
"path": "bytedesk-package.yaml\ndist/design-patterns\n",
"include-hidden-files": true,
"if-no-files-found": "error",
"retention-days": 1
}
}
]
},
"publish": {
"needs": "validate",
"permissions": {
"actions": "read",
"contents": "read",
"id-token": "write"
},
"uses": "ByteDeskAI/marketplace-publisher/.github/workflows/publish-v1.yml@5c37d14b790fcb43f5f4dd1fb141950e3b1c6b07",
"with": {
"package": "@bytedesk/design-patterns",
"version": "${{ needs.validate.outputs.version }}",
"source-commit": "${{ github.sha }}",
"release-root": "${{ needs.validate.outputs.release_root }}"
}
}
}
}
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,22 @@ claude plugin install design-patterns@bytedesk-design-patterns
codex plugin marketplace add .
```

## Marketplace Publication

Pushing an immutable `v<version>` tag runs the repository validation and
deterministic staging checks, validates `bytedesk-package.yaml` with the audited
`bdm` client, and uploads the fixed `bdm-publication-source` candidate. The
workflow then calls the centrally governed ByteDesk publisher by full commit
SHA. That publisher independently compares the candidate manifest with the
tagged source, recomputes the release root, and exchanges GitHub OIDC for a
short-lived publisher identity. No PAT, repository secret, direct CLI publish,
or mutable workflow reference is used.

The tag must equal `v` plus `metadata.version`; mismatched identities or release
roots fail before publication. The marketplace trusted-publisher binding must
authorize this repository, `.github/workflows/publish.yml`, and the exact
central reusable-workflow SHA.

## What Is Included

- A Claude Code marketplace manifest at `.claude-plugin/marketplace.json`.
Expand Down
85 changes: 74 additions & 11 deletions tests/test_release_contract.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import json
import os
import subprocess
import tempfile
import unittest
Expand Down Expand Up @@ -68,25 +69,49 @@ def test_codex_mcp_uses_artifact_relative_command_without_root_cwd(self):
# and invalid under codex-plugin-v1 validation.
self.assertNotIn("cwd", server)

def test_publish_workflow_is_oidc_only_and_fails_closed_after_candidate_upload(self):
def test_publish_workflow_uses_immutable_validated_candidate(self):
workflow_path = ROOT / ".github" / "workflows" / "publish.yml"
self.assertTrue(workflow_path.is_file())
workflow = load_json(workflow_path)
self.assertEqual(workflow["permissions"], {"contents": "read", "id-token": "write"})
publish = workflow["jobs"]["publish"]
self.assertEqual(publish["environment"], "marketplace-production")
commands = "\n".join(step.get("run", "") for step in publish["steps"])
self.assertEqual(workflow["on"], {"push": {"tags": ["v*"]}})
self.assertEqual(workflow["permissions"], {})
self.assertEqual(set(workflow["jobs"]), {"validate", "publish"})
validate = workflow["jobs"]["validate"]
self.assertEqual(validate["permissions"], {"contents": "read"})
self.assertEqual(set(validate["outputs"]), {"version", "release_root"})
commands = "\n".join(step.get("run", "") for step in validate["steps"])
self.assertIn("python3 scripts/release_inventory.py", commands)
self.assertIn("python3 scripts/build_release.py", commands)
self.assertIn("python3 scripts/validate_catalog.py", commands)
self.assertIn("python3 plugins/design-patterns/scripts/validate_catalog.py", commands)
self.assertIn("python3 -m unittest tests.test_release_contract", commands)
self.assertIn("exit 1", commands)
upload = next(step for step in publish["steps"] if step.get("uses") == "actions/upload-artifact@v4")
self.assertEqual(upload["with"]["name"], "design-patterns-release-candidate")
self.assertIn("bytedesk-package.yaml", upload["with"]["path"])
self.assertIn("dist/design-patterns", upload["with"]["path"])
self.assertIn("dist/release-inventory.json", upload["with"]["path"])
self.assertIn('"$RUNNER_TEMP/bytedesk-publisher/bdm" validate bytedesk-package.yaml', commands)
setup = next(step for step in validate["steps"] if step.get("name") == "Install the immutable audited publisher")
self.assertEqual(
setup["uses"],
"ByteDeskAI/marketplace-publisher/.github/actions/setup-bdm@43fa98d57a0d7a52f6c79b8f72409d09a424a541",
)
self.assertEqual(setup["with"]["expected-sha256"], "e807b92ee362c7fe146ae0913a0caa306a9f2d6ea7798cd1c44d0bd03b785e3d")
upload = next(step for step in validate["steps"] if str(step.get("uses", "")).startswith("actions/upload-artifact@"))
self.assertEqual(upload["uses"], "actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02")
self.assertEqual(upload["with"]["name"], "bdm-publication-source")
self.assertEqual(upload["with"]["path"], "bytedesk-package.yaml\ndist/design-patterns\n")
self.assertIs(upload["with"]["include-hidden-files"], True)
self.assertEqual(upload["with"]["retention-days"], 1)

publish = workflow["jobs"]["publish"]
self.assertEqual(publish["needs"], "validate")
self.assertEqual(publish["permissions"], {"actions": "read", "contents": "read", "id-token": "write"})
self.assertEqual(
publish["uses"],
"ByteDeskAI/marketplace-publisher/.github/workflows/publish-v1.yml@5c37d14b790fcb43f5f4dd1fb141950e3b1c6b07",
)
self.assertEqual(publish["with"], {
"package": "@bytedesk/design-patterns",
"version": "${{ needs.validate.outputs.version }}",
"source-commit": "${{ github.sha }}",
"release-root": "${{ needs.validate.outputs.release_root }}",
})
raw = workflow_path.read_text(encoding="utf-8")
for forbidden in [
"secrets.",
Expand All @@ -95,9 +120,47 @@ def test_publish_workflow_is_oidc_only_and_fails_closed_after_candidate_upload(s
"go install",
"curl ",
"ACTIONS_ID_TOKEN_REQUEST",
"workflow_dispatch",
'"release":',
"exit 1",
]:
self.assertNotIn(forbidden, raw)

def test_publish_validation_output_parser_binds_tag_package_and_root(self):
workflow = load_json(ROOT / ".github" / "workflows" / "publish.yml")
release = next(step for step in workflow["jobs"]["validate"]["steps"] if step.get("id") == "release")
validator = release["run"].split("python3 - <<'PY'\n", 1)[1].rsplit("\nPY\n", 1)[0]
payload = {
"schemaVersion": 1,
"package": "@bytedesk/design-patterns",
"version": VERSION,
"manifestDigest": "sha256:" + "b" * 64,
"releaseRootDigest": "sha256:" + "a" * 64,
"validatorRevision": "providers-v1",
"variants": [{"id": "claude-code"}],
}
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
result_path = root / "validation.json"
output_path = root / "github-output"
result_path.write_text(json.dumps(payload, separators=(",", ":")) + "\n", encoding="utf-8")
environment = os.environ.copy()
environment.update(
RESULT_PATH=str(result_path), GITHUB_OUTPUT=str(output_path),
GITHUB_REF_NAME="v" + VERSION, EXPECTED_PACKAGE="@bytedesk/design-patterns",
)
result = subprocess.run(["python3", "-c", validator], env=environment, text=True, capture_output=True)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(
output_path.read_text(encoding="utf-8"),
f"version={VERSION}\nrelease_root=sha256:{'a' * 64}\n",
)
payload["releaseRootDigest"] = "not-a-digest"
result_path.write_text(json.dumps(payload) + "\n", encoding="utf-8")
rejected = subprocess.run(["python3", "-c", validator], env=environment, text=True, capture_output=True)
self.assertNotEqual(rejected.returncode, 0)
self.assertIn("release root is invalid", rejected.stderr)

def test_source_tree_recipe_has_a_stable_inventory(self):
recipe = load_json(ROOT / "packaging" / "source-tree-v1.json")
self.assertEqual(recipe["id"], "source-tree-v1")
Expand Down