From 5f39419ef929044a3bf34c9fb86c909935534aad Mon Sep 17 00:00:00 2001 From: Dr Alexander Mikhalev Date: Wed, 14 Oct 2026 14:11:31 +0100 Subject: [PATCH 1/2] fix: bind imported editor assets to release provenance --- vs-code-terraphim-it/CHANGELOG.md | 5 ++ .../README-alternative-control.md | 2 +- vs-code-terraphim-it/package-lock.json | 4 +- vs-code-terraphim-it/package.json | 2 +- .../scripts/released_editor.py | 36 ++++++++++++- .../tests/released_editor_test.py | 53 +++++++++++++++++-- 6 files changed, 94 insertions(+), 8 deletions(-) diff --git a/vs-code-terraphim-it/CHANGELOG.md b/vs-code-terraphim-it/CHANGELOG.md index 5c402c3a5..722328aad 100644 --- a/vs-code-terraphim-it/CHANGELOG.md +++ b/vs-code-terraphim-it/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.1.1 (unreleased) + +- Check the editor release manifest against its pinned source commit before importing any assets. +- Verify and retain the public source repository and Cargo.lock checksum when the editor release supplies them. + ## 0.1.0 (preview) - Extend the existing Terraphim It extension with the shared Alternative Control Markdown editor. diff --git a/vs-code-terraphim-it/README-alternative-control.md b/vs-code-terraphim-it/README-alternative-control.md index 12857ef16..75536e8c3 100644 --- a/vs-code-terraphim-it/README-alternative-control.md +++ b/vs-code-terraphim-it/README-alternative-control.md @@ -1,6 +1,6 @@ # Alternative Control in the existing Terraphim It extension -Version 0.1.0 is an opt-in custom Markdown editor in `applied-knowledge-systems.terraphim-it`. Open a Markdown file with **Terraphim: Open Alternative Control**. Click the word counter for Write_On. Existing server and Atomic commands remain available in trusted workspaces. +This preview is an opt-in custom Markdown editor in `applied-knowledge-systems.terraphim-it`. Open a Markdown file with **Terraphim: Open Alternative Control**. Click the word counter for Write_On. Existing server and Atomic commands remain available in trusted workspaces. The view bundles the released terraphim-editor v0.1.0-rc.1 UI, WASM and source-pinned host bridge. Each view has its own session; VS Code owns document revisions, persistence and undo/redo. Exact source/revision checks protect asynchronous proposals. External edits retain conflicting drafts for recovery. Native export refuses the source itself and dirty output buffers. Browser persistence is disabled in this host. diff --git a/vs-code-terraphim-it/package-lock.json b/vs-code-terraphim-it/package-lock.json index 31b5426c3..f3380dfa5 100644 --- a/vs-code-terraphim-it/package-lock.json +++ b/vs-code-terraphim-it/package-lock.json @@ -1,12 +1,12 @@ { "name": "terraphim-it", - "version": "0.1.0", + "version": "0.1.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "terraphim-it", - "version": "0.1.0", + "version": "0.1.1", "license": "Apache-2.0", "dependencies": { "@tomic/cli": "0.40.0", diff --git a/vs-code-terraphim-it/package.json b/vs-code-terraphim-it/package.json index 041b6e41d..e7c98bae6 100644 --- a/vs-code-terraphim-it/package.json +++ b/vs-code-terraphim-it/package.json @@ -2,7 +2,7 @@ "name": "terraphim-it", "displayName": "Terraphim It", "description": "Intelligent Markdown editor with alternatives, knowledge-graph synonyms and role-based suggestions", - "version": "0.1.0", + "version": "0.1.1", "publisher": "applied-knowledge-systems", "private": true, "license": "Apache-2.0", diff --git a/vs-code-terraphim-it/scripts/released_editor.py b/vs-code-terraphim-it/scripts/released_editor.py index 0f4a878a1..70a69baeb 100644 --- a/vs-code-terraphim-it/scripts/released_editor.py +++ b/vs-code-terraphim-it/scripts/released_editor.py @@ -6,6 +6,7 @@ import io import json from pathlib import Path, PurePosixPath +import re import tarfile import tempfile import urllib.request @@ -13,6 +14,8 @@ from editor_assets import ROOT, import_assets, verify LOCK = ROOT / "editor-release.json" +PUBLIC_REPOSITORY = "https://github.com/terraphim/terraphim-editor" +PROVENANCE_FIELDS = ("sourceRepository", "cargoLockSha256") def checked(data, digest): @@ -21,9 +24,37 @@ def checked(data, digest): return data +def release_manifest(raw, source): + """Bind archive metadata to the reviewed source pin before writing assets.""" + manifest = json.loads(raw) + if not isinstance(manifest, dict) or not isinstance(source, dict): + raise ValueError("Invalid released editor manifest or source pin") + head = source.get("sourceCommit") + assets = manifest.get("assets") + if (type(manifest.get("schema")) is not int or manifest["schema"] != 1 + or not isinstance(head, str) or re.fullmatch(r"[0-9a-f]{40}", head) is None + or manifest.get("sourceHead") != head + or not isinstance(assets, dict) or not assets + or any(not isinstance(digest, str) or re.fullmatch(r"[0-9a-f]{64}", digest) is None + for digest in assets.values())): + raise ValueError("Released editor manifest identity or asset digests mismatch") + # Historical RC1 has no public-dependency receipt. A newer manifest must + # carry both fields and match an explicit source pin; do not silently drop + # the Cargo lock identity or relabel older bytes as a public-crates build. + if any(key in manifest or key in source for key in PROVENANCE_FIELDS): + lock = manifest.get("cargoLockSha256") + if (manifest.get("sourceRepository") != PUBLIC_REPOSITORY + or source.get("sourceRepository") != PUBLIC_REPOSITORY + or not isinstance(lock, str) or re.fullmatch(r"[0-9a-f]{64}", lock) is None + or source.get("cargoLockSha256") != lock): + raise ValueError("Released editor public dependency provenance mismatch") + return manifest + + def materialize(archive, manifest, bridge, destination, source): """Validate the entire archive before writing only regular admitted files.""" - assets = json.loads(manifest)["assets"] + metadata = release_manifest(manifest, source) + assets = metadata["assets"] files = {} total = 0 with tarfile.open(fileobj=io.BytesIO(archive), mode="r:gz") as bundle: @@ -53,6 +84,9 @@ def materialize(archive, manifest, bridge, destination, source): "assets": {name: hashlib.sha256(data).hexdigest() for name, data in sorted(files.items())}, "qualification": "Released editor assets; host acceptance is separate"} + for key in PROVENANCE_FIELDS: + if key in metadata: + lock[key] = metadata[key] (destination / "host-assets.json").write_text(json.dumps(lock, indent=2) + "\n") verify(destination) diff --git a/vs-code-terraphim-it/tests/released_editor_test.py b/vs-code-terraphim-it/tests/released_editor_test.py index 191d27bfa..6083e334c 100644 --- a/vs-code-terraphim-it/tests/released_editor_test.py +++ b/vs-code-terraphim-it/tests/released_editor_test.py @@ -7,12 +7,16 @@ import tarfile import tempfile import unittest +import copy sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts")) from released_editor import checked, materialize class ReleasedEditorTest(unittest.TestCase): + def source(self): + return {"sourceCommit": "a" * 40, "release": "release", "archive": {"sha256": "b" * 64}} + def archive(self, entries): stream = io.BytesIO() with tarfile.open(fileobj=stream, mode="w:gz") as archive: @@ -23,11 +27,13 @@ def archive(self, entries): archive.addfile(entry, io.BytesIO(data)) return stream.getvalue() - def run_import(self, entries, assets): + def run_import(self, entries, assets, metadata=None, source=None): with tempfile.TemporaryDirectory() as directory: target = Path(directory) / "assets" - materialize(self.archive(entries), json.dumps({"assets": assets}), b"bridge", target, - {"sourceCommit": "commit", "release": "release", "archive": {"sha256": "hash"}}) + manifest = {"schema": 1, "sourceHead": "a" * 40, "assets": assets} + manifest.update(metadata or {}) + materialize(self.archive(entries), json.dumps(manifest), b"bridge", target, + source or self.source()) return {p.relative_to(target).as_posix(): p.read_bytes() for p in target.rglob("*") if p.is_file()} @@ -37,6 +43,47 @@ def test_real_archive_preserves_exact_bytes(self): {"vendor/a.js": hashlib.sha256(data).hexdigest()}) self.assertEqual(files["vendor/a.js"], data) self.assertEqual(files["host-session.js"], b"bridge") + lock = json.loads(files["host-assets.json"]) + self.assertEqual(lock["sourceHead"], "a" * 40) + self.assertNotIn("cargoLockSha256", lock) + + def test_public_dependency_provenance_is_bound_and_retained(self): + metadata = {"sourceRepository": "https://github.com/terraphim/terraphim-editor", + "cargoLockSha256": "c" * 64} + source = {**self.source(), **metadata} + files = self.run_import([("a.js", b"a", tarfile.REGTYPE)], + {"a.js": hashlib.sha256(b"a").hexdigest()}, metadata, source) + lock = json.loads(files["host-assets.json"]) + self.assertEqual({key: lock[key] for key in metadata}, metadata) + + def test_manifest_identity_and_public_provenance_refused_before_write(self): + good = {"schema": 1, "sourceHead": "a" * 40, + "sourceRepository": "https://github.com/terraphim/terraphim-editor", + "cargoLockSha256": "c" * 64, + "assets": {"a.js": hashlib.sha256(b"a").hexdigest()}} + source = {**self.source(), "sourceRepository": good["sourceRepository"], + "cargoLockSha256": good["cargoLockSha256"]} + invalid = [None, [], {}, {**good, "schema": 2}, {**good, "sourceHead": "d" * 40}, + {**good, "sourceRepository": "https://example.invalid/editor"}, + {**good, "cargoLockSha256": "e" * 64}, {**good, "cargoLockSha256": None}, + {**good, "assets": []}, {**good, "assets": {}}, + {**good, "assets": {"a.js": "not-a-sha256"}}] + for key in ("sourceRepository", "cargoLockSha256"): + missing = copy.deepcopy(good); del missing[key]; invalid.append(missing) + archive = self.archive([("a.js", b"a", tarfile.REGTYPE)]) + with tempfile.TemporaryDirectory() as directory: + target = Path(directory) / "assets" + for manifest in invalid: + with self.subTest(manifest=manifest), self.assertRaises(ValueError): + materialize(archive, json.dumps(manifest), b"bridge", target, source) + self.assertFalse(target.exists(), "Refused metadata must not write any asset") + + def test_public_metadata_requires_explicit_matching_pin(self): + metadata = {"sourceRepository": "https://github.com/terraphim/terraphim-editor", + "cargoLockSha256": "c" * 64} + with self.assertRaises(ValueError): + self.run_import([("a.js", b"a", tarfile.REGTYPE)], + {"a.js": hashlib.sha256(b"a").hexdigest()}, metadata) def test_wrong_digest_fails(self): with self.assertRaises(ValueError): From 43be743a7dc3b5fa1bf7824b155511d7972054fd Mon Sep 17 00:00:00 2001 From: Dr Alexander Mikhalev Date: Wed, 14 Oct 2026 14:47:45 +0100 Subject: [PATCH 2/2] feat: qualify VS Code preview with public editor RC2 --- .github/workflows/vscode-release.yml | 26 +++- vs-code-terraphim-it/CHANGELOG.md | 4 +- .../README-alternative-control.md | 13 +- vs-code-terraphim-it/editor-release.json | 18 +-- vs-code-terraphim-it/media/editor-build.json | 22 ++-- .../scripts/plan_marketplaces.py | 78 ++++++++++++ .../tests/marketplace_plan_test.py | 115 ++++++++++++++++++ 7 files changed, 254 insertions(+), 22 deletions(-) create mode 100644 vs-code-terraphim-it/scripts/plan_marketplaces.py create mode 100644 vs-code-terraphim-it/tests/marketplace_plan_test.py diff --git a/.github/workflows/vscode-release.yml b/.github/workflows/vscode-release.yml index c10e4e263..5229b6740 100644 --- a/.github/workflows/vscode-release.yml +++ b/.github/workflows/vscode-release.yml @@ -84,12 +84,34 @@ jobs: name: vscode-publication-${{ github.sha }} path: vs-code-terraphim-it/publication.json if-no-files-found: error - marketplace: + marketplace-plan: needs: release + runs-on: ubuntu-24.04 + outputs: + registries: ${{ steps.plan.outputs.registries }} + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + - name: Report disabled targets and plan explicitly enabled attempts + id: plan + env: + VSCODE_PUBLISH_VISUAL_STUDIO_MARKETPLACE: ${{ vars.VSCODE_PUBLISH_VISUAL_STUDIO_MARKETPLACE }} + VSCODE_PUBLISH_OPEN_VSX: ${{ vars.VSCODE_PUBLISH_OPEN_VSX }} + run: python3 vs-code-terraphim-it/scripts/plan_marketplaces.py --receipt marketplace-plan.json + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + if: always() + with: + name: vscode-marketplace-plan-${{ github.sha }} + path: marketplace-plan.json + if-no-files-found: error + marketplace: + needs: [release, marketplace-plan] + if: needs.marketplace-plan.outputs.registries != '[]' && needs.marketplace-plan.outputs.registries != '' strategy: fail-fast: false matrix: - registry: [visual-studio-marketplace, open-vsx] + registry: ${{ fromJSON(needs.marketplace-plan.outputs.registries) }} runs-on: ubuntu-24.04 environment: ${{ matrix.registry }} permissions: diff --git a/vs-code-terraphim-it/CHANGELOG.md b/vs-code-terraphim-it/CHANGELOG.md index 722328aad..dacb14c58 100644 --- a/vs-code-terraphim-it/CHANGELOG.md +++ b/vs-code-terraphim-it/CHANGELOG.md @@ -1,9 +1,11 @@ # Changelog -## 0.1.1 (unreleased) +## 0.1.1 (preview) +- Bundle the verified editor v0.1.0-rc.2 release built with public Terraphim crate dependencies. - Check the editor release manifest against its pinned source commit before importing any assets. - Verify and retain the public source repository and Cargo.lock checksum when the editor release supplies them. +- Publish the qualified GitHub VSIX independently of marketplace setup; disabled marketplace targets are explicitly reported as not published. ## 0.1.0 (preview) diff --git a/vs-code-terraphim-it/README-alternative-control.md b/vs-code-terraphim-it/README-alternative-control.md index 75536e8c3..7ea698011 100644 --- a/vs-code-terraphim-it/README-alternative-control.md +++ b/vs-code-terraphim-it/README-alternative-control.md @@ -2,7 +2,7 @@ This preview is an opt-in custom Markdown editor in `applied-knowledge-systems.terraphim-it`. Open a Markdown file with **Terraphim: Open Alternative Control**. Click the word counter for Write_On. Existing server and Atomic commands remain available in trusted workspaces. -The view bundles the released terraphim-editor v0.1.0-rc.1 UI, WASM and source-pinned host bridge. Each view has its own session; VS Code owns document revisions, persistence and undo/redo. Exact source/revision checks protect asynchronous proposals. External edits retain conflicting drafts for recovery. Native export refuses the source itself and dirty output buffers. Browser persistence is disabled in this host. +The view bundles the released terraphim-editor v0.1.0-rc.2 UI, WASM and source-pinned host bridge. The bundle records its public source repository and Cargo.lock checksum. Each view has its own session; VS Code owns document revisions, persistence and undo/redo. Exact source/revision checks protect asynchronous proposals. External edits retain conflicting drafts for recovery. Native export refuses the source itself and dirty output buffers. Browser persistence is disabled in this host. **Export Document Knowledge Graph** waits for the current edit to be acknowledged, reads that exact source, and uses the shared Rust `synonyms::` parser/types to create the portable graph. It refuses malformed annotations or ambiguous concepts. The document and active role are unchanged. Save, undo, redo and reopen determine which aliases exist in the export. @@ -10,4 +10,15 @@ A fresh checkout runs `npm ci --ignore-scripts`, `npm run editor:fetch`, `npm ru CI builds one VSIX and tests that package in native Linux, macOS and Windows VS Code profiles. Tag release and marketplace workflows promote the same verified package. Publisher identity setup is documented in `docs/plans/design-extension-marketplaces.md` in the repository. +The GitHub preview release does not depend on marketplace account setup. After +its exact package passes qualification and anonymous download verification, +`vscode-release.yml` reads the repository readiness variables +`VSCODE_PUBLISH_VISUAL_STUDIO_MARKETPLACE` and `VSCODE_PUBLISH_OPEN_VSX`. +Unset or `false` skips that marketplace and records `NOT_PUBLISHED` in the run +summary and `vscode-marketplace-plan-*` artifact. Only exact `true` schedules +an attempt; other values refuse all attempts. Set a gate only after its existing +publisher, protected environment and federated identity are ready. Enabled +targets remain `PENDING_PUBLICATION` until the separate registry readback passes. +These gates do not create access, weaken tag policies or provide a PAT fallback. + Acceptance reports distinguish actual saved files, engine tests and visual UI checks. The original 41 visual requirements are not all accepted. This is a scoped preview; stock Zed visual limitations and the separate Zed fork remain outside this VSIX's claims. diff --git a/vs-code-terraphim-it/editor-release.json b/vs-code-terraphim-it/editor-release.json index 1a2342a56..6f5df499d 100644 --- a/vs-code-terraphim-it/editor-release.json +++ b/vs-code-terraphim-it/editor-release.json @@ -1,19 +1,21 @@ { - "release": "https://github.com/terraphim/terraphim-editor/releases/tag/v0.1.0-rc.1", - "sourceCommit": "05d0aa50151c131db4a3b30b56aba67c00add5fc", + "release": "https://github.com/terraphim/terraphim-editor/releases/tag/v0.1.0-rc.2", + "sourceCommit": "25ad3b16a1bebab6edd767a7efd1ace7b25c82ad", "archive": { "name": "editor-candidate.tar.gz", - "url": "https://github.com/terraphim/terraphim-editor/releases/download/v0.1.0-rc.1/editor-candidate.tar.gz", - "sha256": "a514f45276397b93f38d8e14c9e3bde6203bd9c625bc13c81829de31ffa9eb79" + "url": "https://github.com/terraphim/terraphim-editor/releases/download/v0.1.0-rc.2/editor-candidate.tar.gz", + "sha256": "daa104aecdca43df5ca6dcf36b420351be09784a8436ab61bd3fc9acb1504e73" }, "manifest": { "name": "assets-manifest.json", - "url": "https://github.com/terraphim/terraphim-editor/releases/download/v0.1.0-rc.1/assets-manifest.json", - "sha256": "d8d65165263c237f4c3c65d97b69b1083850daaea556fcd9a01241758ec2a4a7" + "url": "https://github.com/terraphim/terraphim-editor/releases/download/v0.1.0-rc.2/assets-manifest.json", + "sha256": "4331adf222fedeeba04eb5986878bb4011f833e051b1a4f484274302ae4465a3" }, "bridge": { "name": "host-session.js", - "url": "https://raw.githubusercontent.com/terraphim/terraphim-editor/05d0aa50151c131db4a3b30b56aba67c00add5fc/public/js/host-session.js", + "url": "https://raw.githubusercontent.com/terraphim/terraphim-editor/25ad3b16a1bebab6edd767a7efd1ace7b25c82ad/public/js/host-session.js", "sha256": "9e5efc4c3f1f2003cd86f05b266b6666e546c68a065eaca67c897c2328a09388" - } + }, + "sourceRepository": "https://github.com/terraphim/terraphim-editor", + "cargoLockSha256": "b485926b2353bd8d045d6ba252247b05dd302c5211fbc8f63602fa0c5b91c24b" } diff --git a/vs-code-terraphim-it/media/editor-build.json b/vs-code-terraphim-it/media/editor-build.json index ce1e10d10..08345eae5 100644 --- a/vs-code-terraphim-it/media/editor-build.json +++ b/vs-code-terraphim-it/media/editor-build.json @@ -1,14 +1,14 @@ { "protocol": 1, - "sourceHead": "05d0aa50151c131db4a3b30b56aba67c00add5fc", - "release": "https://github.com/terraphim/terraphim-editor/releases/tag/v0.1.0-rc.1", - "archiveSha256": "a514f45276397b93f38d8e14c9e3bde6203bd9c625bc13c81829de31ffa9eb79", + "sourceHead": "25ad3b16a1bebab6edd767a7efd1ace7b25c82ad", + "release": "https://github.com/terraphim/terraphim-editor/releases/tag/v0.1.0-rc.2", + "archiveSha256": "daa104aecdca43df5ca6dcf36b420351be09784a8436ab61bd3fc9acb1504e73", "assets": { "alternatives-panel-f7a68a7707d04dfa.css": "a99a407d3f6aa52f48e3013bf865ef4b44bd0b9e4028d9e68c1a6dd32f8a68a8", "blocks-b2480c8f9f3d63b3.css": "383c0e9197f015d651363e3abd31fdf67b56a6b4bde72ebe764a7b1e417c5b4c", "example.html": "fda475be8b87ff7a0789f167b4906357c3d8d7e7d1fcd0348f8c908d65e731d3", "host-session.js": "9e5efc4c3f1f2003cd86f05b266b6666e546c68a065eaca67c897c2328a09388", - "index.html": "3092fd19c610baface73dccee3c70de285b4c11850b2827f947943027e72ae1b", + "index.html": "a9334870599b5656b377655bf78a066bd526aa430d4eefa4e1bdba4c3062e3d7", "js/rolegraph-worker.js": "8240803c0aef05acd66b6ec91eaee16e53a556252aa48c8487a4c3bd9a8924d9", "lab-3fee6e89acf44fa1.css": "1fe0441a83177e9713ac112fac85336ad3315b3ac755ec8a625e008c4ca1f967", "overflow-cc1be2ca3843f438.css": "8fa1a611df91541508b1532d3628addffecec02b2516a3f7a2c98bc385ada218", @@ -16,16 +16,16 @@ "rolegraph/rolegraph-worker.js": "8240803c0aef05acd66b6ec91eaee16e53a556252aa48c8487a4c3bd9a8924d9", "rolegraph/terraphim_rolegraph_browser.d.ts": "95094622b3805fed54e96cdcbb1c732d1244fbecceadb24dd13e0f28c1d8f7e2", "rolegraph/terraphim_rolegraph_browser.js": "0f155f7ebe48ffd9958266c153f965447f98e26724e8fa994d1e18d879a9f4dc", - "rolegraph/terraphim_rolegraph_browser_bg.wasm": "7002fd7e4614ae03acc1593c7225fc82d586641ee96d991780339244ed5eea8a", + "rolegraph/terraphim_rolegraph_browser_bg.wasm": "128bf553be2185e2140e68dcd47ff4127aeed47ea49fcf1dae942b99ac84a0d1", "rolegraph/terraphim_rolegraph_browser_bg.wasm.d.ts": "f6cc0427ac07ca31fc3882c5f9487347717f887a0c9ebba69a1323eda3a822ea", "roles/default.json": "e850422ec72faa06aae0d6c2893a9f1d2e36d60a5aef79bd8f863daa2d71b015", "selection-menu-317cedafb0e3952e.css": "7d0790ed1bf0c6e3955e5305d31ace2bd67f6df5f5f2b7be9692f9c79843891e", - "terraphim-editor-438d353659f8cd7.js": "6f55bfce550053ab0cd8a1f4ffd101436f9de3e2610702269ed53bf0dc2ca7cd", - "terraphim-editor-438d353659f8cd7_bg.wasm": "f167a40fb0934703269a3bfe34adcfbc28994933f939f03bab7bf59425021dc6", + "terraphim-editor-2718c4e298c1b7b6.js": "9ccd7ddee55764ca47067c809d6dbf9881843b6649fd3a8677871df0e5601b24", + "terraphim-editor-2718c4e298c1b7b6_bg.wasm": "93796edac11424d45457a77c3e635d5d59b139c21a08bf09126c88c5ed28ea18", "terraphim-editor.min.css": "147cd5ce172f00873b002ac215b004631c7c17aaadf23fb7c786a90628dfe1f3", "terraphim-editor.min.js": "a650ab15ac36d0868268099175d60da1e228283fc0f2a46b61dd1fa752ef4fba", - "terraphim_editor.js": "6f55bfce550053ab0cd8a1f4ffd101436f9de3e2610702269ed53bf0dc2ca7cd", - "terraphim_editor_bg.wasm": "f167a40fb0934703269a3bfe34adcfbc28994933f939f03bab7bf59425021dc6", + "terraphim_editor.js": "9ccd7ddee55764ca47067c809d6dbf9881843b6649fd3a8677871df0e5601b24", + "terraphim_editor_bg.wasm": "93796edac11424d45457a77c3e635d5d59b139c21a08bf09126c88c5ed28ea18", "tokens-36540e1e0e83eb54.css": "71e359d23d7ec44c9c838f74cf77f6e33aa3c33c57757cfc4cfb96e22a09de55", "trim-a64aceacb4246fba.css": "fe79acb70b9906e404d30e19b7d5e1cf6589bc0aa155f1c1dc93ed18e6dee81d", "vendor/fontawesome/6.5.2/LICENSE.txt": "9b914eae88817d63b576eab5aafde7068c7a1abae125d7cdfb034f1da43a9afc", @@ -138,5 +138,7 @@ "vendor/shoelace/2.12.0/third-party-notices/shoelace-style_localize/LICENSE.md": "ac9ffacbfc877b6f4253a481b3c0edbad0c019ac3506a3b51415b336f4a0c054", "write-on-6a8fd9449457dd74.css": "bd3b7471c8a7ba32c38927d47eefe26e5680ffa6ec96262380f4cf383340674c" }, - "qualification": "Released editor assets; host acceptance is separate" + "qualification": "Released editor assets; host acceptance is separate", + "sourceRepository": "https://github.com/terraphim/terraphim-editor", + "cargoLockSha256": "b485926b2353bd8d045d6ba252247b05dd302c5211fbc8f63602fa0c5b91c24b" } diff --git a/vs-code-terraphim-it/scripts/plan_marketplaces.py b/vs-code-terraphim-it/scripts/plan_marketplaces.py new file mode 100644 index 000000000..d5553793e --- /dev/null +++ b/vs-code-terraphim-it/scripts/plan_marketplaces.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +"""Plan explicit marketplace attempts after the independent GitHub release.""" +import argparse +import json +import os +from pathlib import Path +import re + +from verify_vsix import check_tag + +ROOT = Path(__file__).resolve().parents[1] +TARGETS = ( + ("visual-studio-marketplace", "VSCODE_PUBLISH_VISUAL_STUDIO_MARKETPLACE"), + ("open-vsx", "VSCODE_PUBLISH_OPEN_VSX"), +) + + +def plan(environment): + """Unset gates skip publication; malformed gates refuse every target.""" + report = {"schema": 1, "scope": "Publication routing only; live registry readback is separate", + "registries": [], "targets": [], "configurationValid": True} + for registry, flag in TARGETS: + value = environment.get(flag, "") + valid = value in ("", "false", "true") + enabled = value == "true" + report["configurationValid"] &= valid + report["targets"].append({ + "registry": registry, "readinessVariable": flag, + "status": "PENDING_PUBLICATION" if enabled else "NOT_PUBLISHED", + "reason": "Explicit readiness gate enabled; publication/readback still required" + if enabled else "Readiness gate is unset or disabled", + }) + if enabled: + report["registries"].append(registry) + if not report["configurationValid"]: + report["registries"] = [] + for target in report["targets"]: + target.update(status="NOT_PUBLISHED", reason="Invalid readiness configuration; all attempts refused") + return report + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--receipt", type=Path, required=True) + args = parser.parse_args() + report = plan(os.environ) + source = os.environ.get("GITHUB_SHA", "") + tag = os.environ.get("GITHUB_REF_NAME", "") + try: + check_tag(tag, json.loads((ROOT / "package.json").read_text())["version"]) + if re.fullmatch(r"[0-9a-f]{40}", source) is None: + raise ValueError("Expected exact GitHub source commit") + except ValueError: + report["configurationValid"] = False + report["registries"] = [] + for target in report["targets"]: + target.update(status="NOT_PUBLISHED", reason="Invalid release identity; all attempts refused") + else: + report.update(sourceCommit=source, tag=tag) + # Retain failed/disabled plans as evidence. Never overwrite an earlier plan. + with args.receipt.open("x") as output: + output.write(json.dumps(report, indent=2) + "\n") + if path := os.environ.get("GITHUB_OUTPUT"): + with Path(path).open("a") as output: + output.write("registries=" + json.dumps(report["registries"], separators=(",", ":")) + "\n") + summary = "Marketplace readiness (not publication verification):\n\n" + "\n".join( + f"- {target['registry']}: **{target['status']}** — {target['reason']}" + for target in report["targets"] + ) + "\n" + if path := os.environ.get("GITHUB_STEP_SUMMARY"): + with Path(path).open("a") as output: + output.write(summary) + print(summary, end="") + return 0 if report["configurationValid"] else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/vs-code-terraphim-it/tests/marketplace_plan_test.py b/vs-code-terraphim-it/tests/marketplace_plan_test.py new file mode 100644 index 000000000..f9f2d65d2 --- /dev/null +++ b/vs-code-terraphim-it/tests/marketplace_plan_test.py @@ -0,0 +1,115 @@ +"""Execute the real publication planner with isolated files and no network.""" +import json +import os +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[1] +SCRIPT = ROOT / "scripts/plan_marketplaces.py" +FLAGS = ("VSCODE_PUBLISH_VISUAL_STUDIO_MARKETPLACE", "VSCODE_PUBLISH_OPEN_VSX") + + +class MarketplacePlanTests(unittest.TestCase): + def run_plan(self, flags): + with tempfile.TemporaryDirectory() as directory: + folder = Path(directory) + environment = {key: value for key, value in os.environ.items() if key not in FLAGS} + environment.update({"GITHUB_SHA": "a" * 40, "GITHUB_REF_NAME": "terraphim_vscode-v0.1.1", + "GITHUB_OUTPUT": str(folder / "outputs"), + "GITHUB_STEP_SUMMARY": str(folder / "summary"), **flags}) + result = subprocess.run([sys.executable, str(SCRIPT), "--receipt", str(folder / "receipt.json")], + env=environment, capture_output=True, text=True) + return (result, json.loads((folder / "receipt.json").read_text()), + (folder / "outputs").read_text(), (folder / "summary").read_text()) + + def test_absent_or_false_flags_skip_both_with_explicit_not_published_receipt(self): + for flags in ({}, {FLAGS[0]: "false", FLAGS[1]: ""}): + with self.subTest(flags=flags): + result, receipt, outputs, summary = self.run_plan(flags) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(receipt["registries"], []) + self.assertEqual(outputs, "registries=[]\n") + self.assertTrue(all(target["status"] == "NOT_PUBLISHED" for target in receipt["targets"])) + self.assertEqual(summary.count("NOT_PUBLISHED"), 2) + + def test_each_exact_true_selects_only_its_fixed_registry(self): + for index, expected in enumerate(("visual-studio-marketplace", "open-vsx")): + with self.subTest(expected=expected): + result, receipt, outputs, _ = self.run_plan({FLAGS[index]: "true"}) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(receipt["registries"], [expected]) + self.assertEqual(json.loads(outputs.removeprefix("registries=")), [expected]) + self.assertEqual(receipt["targets"][index]["status"], "PENDING_PUBLICATION") + self.assertEqual(receipt["targets"][1-index]["status"], "NOT_PUBLISHED") + + def test_both_enabled_remain_pending_until_separate_live_readback(self): + result, receipt, _, summary = self.run_plan(dict.fromkeys(FLAGS, "true")) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(receipt["registries"], ["visual-studio-marketplace", "open-vsx"]) + self.assertTrue(all(target["status"] == "PENDING_PUBLICATION" for target in receipt["targets"])) + self.assertNotIn("PUBLISHED_PUBLIC_VERIFIED", summary) + + def test_malformed_flag_refuses_entire_route_without_echoing_value(self): + for value in ("TRUE", "yes", "1", "true\nregistries=foreign"): + with self.subTest(value=value): + result, receipt, outputs, summary = self.run_plan({FLAGS[0]: "true", FLAGS[1]: value}) + self.assertNotEqual(result.returncode, 0) + self.assertEqual(receipt["registries"], []) + self.assertEqual(outputs, "registries=[]\n") + self.assertTrue(all(target["status"] == "NOT_PUBLISHED" for target in receipt["targets"])) + self.assertNotIn(value, result.stdout + result.stderr + summary) + + def test_existing_bootstrap_switch_cannot_enable_marketplace_routing(self): + result, receipt, _, _ = self.run_plan({"OVSX_BOOTSTRAP_FIRST_PUBLISH": "true", + "OVSX_BOOTSTRAP_PAT": "not-a-real-secret"}) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(receipt["registries"], []) + + def test_wrong_tag_or_source_refuses_enabled_targets(self): + for identity in ({"GITHUB_REF_NAME": "main"}, + {"GITHUB_REF_NAME": "terraphim_vscode-v0.1.0"}, + {"GITHUB_SHA": "not-a-commit"}): + with self.subTest(identity=identity): + result, receipt, outputs, _ = self.run_plan({**dict.fromkeys(FLAGS, "true"), **identity}) + self.assertNotEqual(result.returncode, 0) + self.assertEqual(outputs, "registries=[]\n") + self.assertTrue(all(target["status"] == "NOT_PUBLISHED" for target in receipt["targets"])) + + def test_existing_receipt_is_not_overwritten(self): + with tempfile.TemporaryDirectory() as directory: + receipt = Path(directory) / "receipt.json" + receipt.write_text("earlier publication evidence\n") + result = subprocess.run([sys.executable, str(SCRIPT), "--receipt", str(receipt)], + env={**os.environ, "GITHUB_SHA": "a" * 40, + "GITHUB_REF_NAME": "terraphim_vscode-v0.1.1"}, + capture_output=True, text=True) + self.assertNotEqual(result.returncode, 0) + self.assertEqual(receipt.read_text(), "earlier publication evidence\n") + + def test_workflow_keeps_github_release_independent_and_routes_only_planned_targets(self): + sys.path.insert(0, str(ROOT.parent / "tests")) + from workflow_release_recovery_contract_test import has_need, job_block + workflow = (ROOT.parent / ".github/workflows/vscode-release.yml").read_text() + release = job_block(workflow, "release") + plan = job_block(workflow, "marketplace-plan") + marketplace = job_block(workflow, "marketplace") + self.assertTrue(has_need(release, "qualify")) + self.assertNotIn("marketplace", release) + self.assertTrue(has_need(plan, "release")) + self.assertNotIn("id-token: write", plan) + self.assertNotIn("secrets.", plan) + self.assertTrue(has_need(marketplace, "release")) + self.assertTrue(has_need(marketplace, "marketplace-plan")) + self.assertIn("needs.marketplace-plan.outputs.registries != '[]'", marketplace) + self.assertIn("fromJSON(needs.marketplace-plan.outputs.registries)", marketplace) + self.assertIn("--azure-credential", marketplace) + self.assertIn("verify_registry.py", marketplace) + for flag in FLAGS: + self.assertIn("vars." + flag, plan) + + +if __name__ == "__main__": + unittest.main()