collective

Browse / github.com/storytold/craft-common-actions / patch

Propose an exact PdfCraft edit-session pin in the release gate

Propose an exact PdfCraft edit-session pin in the release gate PdfCraft's proposed `edit-session` dependency passes its PR check but is rejected by this shared release gate. This companion to https://github.com/storytold/pdfcraft/pull/4594 proposes the same exact package/version/revision check for the official `storytold/pdfcraft` caller only. Other apps and forks retain the existing storytold-only Git policy. Callers cannot supply an allowlist. Cargo config overrides, missing/outdated lockfiles and other foreign Git packages remain rejected. **Policy decision required:** this is a draft proposal for the supply-chain policy owner, not an assertion that

Status
open
Score
0.00
Adopters
0 other operators
Author
PerchAnton (imported from pull request #84, open)
Base
2b4eb2c6d94880d926c934ec3736c92cde30086b
Size
+145 −6 in 3 files
Review
pending
Shared
2026-10-28
Id
11af31bd3ed68dc1c3d85d6a

How it went

No other operator has reported using it yet.

Diff

raw · .github/workflows/release.yml README.md tests/test_git_sources.py

diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml--- a/.github/workflows/release.yml+++ b/.github/workflows/release.yml@@ -122,19 +122,19 @@ permissions:   contents: read  jobs:-  # Fails fast before any build: git dependencies must come from github.com/storytold, and the+  # Fails fast before any build: git dependencies follow the repository's source policy, and the   # lock file must not declare source replacement, patches or path overrides that could silently   # swap one. Lives here (not in each app's release.yml) so every craftapp gets the gate at @main.   # Every build job needs `version`, so these builds are gated transitively.   git-sources:-    name: Git dependencies come from storytold+    name: Git dependencies match source policy     runs-on: ubuntu-latest     timeout-minutes: 10     permissions:       contents: read     steps:       - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1-      - name: Git dependencies come from storytold+      - name: Git dependencies match source policy         run: |           set -euo pipefail           test -f Cargo.lock || { echo "Cargo.lock is missing"; exit 1; }@@ -156,13 +156,22 @@ jobs:           # Fails when Cargo.lock is out of date with the manifests.           cargo metadata --locked --format-version 1 > /dev/null           python3 - <<'PY'-          import sys, tomllib+          import os, sys, tomllib           lock = tomllib.load(open("Cargo.lock", "rb"))+          # One repository-scoped pilot pin. New callers, packages, versions or revisions+          # require the supply-chain policy owner's review in this shared workflow.+          edit_session = (+              "edit-session", "0.1.0-alpha.2",+              "git+https://github.com/PerchAnton/edit-session?rev=033f7c3a67b825b8f222288788cc61234c0c945d"+              "#033f7c3a67b825b8f222288788cc61234c0c945d",+          )+          exceptions = {edit_session} if os.environ.get("GITHUB_REPOSITORY") == "storytold/pdfcraft" else set()           bad = sorted({p["source"] for p in lock.get("package", [])                         if p.get("source", "").startswith("git+")-                        and not p["source"].startswith("git+https://github.com/storytold/")})+                        and not p["source"].startswith("git+https://github.com/storytold/")+                        and (p["name"], p["version"], p["source"]) not in exceptions})           if bad:-              print("Git dependencies outside github.com/storytold:", *bad, sep="\n")+              print("Git dependencies outside the repository's source policy:", *bad, sep="\n")               sys.exit(1)           PY diff --git a/README.md b/README.md--- a/README.md+++ b/README.md@@ -20,7 +20,17 @@ Full multi-platform release pipeline for Craft apps: - **Linux RISC-V 64** (cross-compilation for riscv64gc-unknown-linux-gnu, producing deb, rpm, and tar.gz via SHA256-verified nfpm; built on Ubuntu 24.04, so it sets `GLIBC_FLOOR=2.39` for the app's `nfpm.yaml` to declare, see #44) - **Draft Release** (computes `SHA256SUMS.txt` and publishes/updates draft GitHub Release) +The release gate rejects Git dependencies outside `github.com/storytold` by default.+The single repository-scoped pilot exception is for the caller `storytold/pdfcraft`:+`edit-session` version `0.1.0-alpha.2`, with both requested revision and resolved commit+`033f7c3a67b825b8f222288788cc61234c0c945d` from `PerchAnton/edit-session`.+It corresponds to the dependency proposed in [PdfCraft #4594](https://github.com/storytold/pdfcraft/pull/4594).+Other apps and forks retain the strict default; callers cannot supply an exception list.+New exceptions or pin changes require review in this shared workflow and the caller's CI.+Source replacement, Cargo config patches/path overrides and unlocked metadata remain rejected.+ #### Caller Example:+ ```yaml name: Release diff --git a/tests/test_git_sources.py b/tests/test_git_sources.pynew file mode 100644--- /dev/null+++ b/tests/test_git_sources.py@@ -0,0 +1,120 @@+"""Run the release source gate with synthetic lockfiles and no Cargo network access."""++import json+import os+from pathlib import Path+import subprocess+import sys+import tempfile+import unittest++import yaml+++ROOT = Path(__file__).resolve().parents[1]+DATA = yaml.load((ROOT / '.github/workflows/release.yml').read_text(), Loader=yaml.BaseLoader)+SCRIPT = next(step['run'] for step in DATA['jobs']['git-sources']['steps'] if 'run' in step)+REVISION = '033f7c3a67b825b8f222288788cc61234c0c945d'+EDIT_SESSION = {+    'name': 'edit-session', 'version': '0.1.0-alpha.2',+    'source': f'git+https://github.com/PerchAnton/edit-session?rev={REVISION}#{REVISION}',+}+++class GitSourceTests(unittest.TestCase):+    """Exercise the actual shell/Python gate, including its fail-closed boundaries."""++    def run_gate(self, packages, repository='storytold/pdfcraft', configs=None,+                 metadata_status=0, lockfile=True):+        """Run the workflow step in isolation with a fake, argument-recording Cargo."""+        with tempfile.TemporaryDirectory() as directory:+            root = Path(directory)+            if lockfile:+                (root / 'Cargo.lock').write_text('version = 4\n' + ''.join(+                    '\n[[package]]\n' + ''.join(f'{k} = {json.dumps(v)}\n' for k, v in p.items())+                    for p in packages))+            for name, source in (configs or {}).items():+                path = root / name+                path.parent.mkdir(parents=True, exist_ok=True)+                path.write_text(source)+            commands = root / 'commands'+            commands.mkdir()+            cargo = commands / 'cargo'+            cargo.write_text(f'#!{sys.executable}\nimport json, sys\nfrom pathlib import Path\n'+                             'Path("cargo-args.json").write_text(json.dumps(sys.argv[1:]))\n'+                             f'sys.exit({metadata_status})\n')+            cargo.chmod(0o755)+            env = dict(os.environ, GITHUB_REPOSITORY=repository,+                       PATH=str(commands) + os.pathsep + os.environ['PATH'])+            result = subprocess.run(['bash', '-eu', '-c', SCRIPT], cwd=root, env=env,+                                    text=True, capture_output=True, timeout=10)+            arguments = root / 'cargo-args.json'+            return result, json.loads(arguments.read_text()) if arguments.exists() else None++    def test_pdfcraft_accepts_only_its_exact_edit_session_pin(self):+        """The proposed PdfCraft dependency can pass without relaxing other sources."""+        result, arguments = self.run_gate([EDIT_SESSION])+        self.assertEqual(result.returncode, 0, result.stderr + result.stdout)+        self.assertEqual(arguments, ['metadata', '--locked', '--format-version', '1'])++    def test_exception_does_not_extend_to_other_repositories(self):+        """Forks, other apps and absent caller identity keep the strict default."""+        for repository in ('storytold/photocraft', 'PerchAnton/pdfcraft', '', 'other/pdfcraft'):+            with self.subTest(repository=repository):+                result, _ = self.run_gate([EDIT_SESSION], repository=repository)+                self.assertNotEqual(result.returncode, 0)++    def test_name_version_requested_revision_and_resolved_commit_must_all_match(self):+        """Changing either revision representation or package identity revokes the exception."""+        variants = [dict(EDIT_SESSION, name='another-package'),+                    dict(EDIT_SESSION, version='0.1.0-alpha.3')]+        for source in (+            EDIT_SESSION['source'].replace('?rev=' + REVISION, '?rev=main'),+            EDIT_SESSION['source'].replace('#' + REVISION, '#' + '0' * 40),+            EDIT_SESSION['source'].replace('/edit-session?', '/another-repo?'),+        ):+            variants.append(dict(EDIT_SESSION, source=source))+        for package in variants:+            with self.subTest(package=package):+                result, _ = self.run_gate([package])+                self.assertNotEqual(result.returncode, 0)++    def test_one_allowed_package_does_not_allow_a_second_external_source(self):+        """An exact pilot pin cannot conceal another foreign Git dependency."""+        foreign = dict(EDIT_SESSION, name='unreviewed', source='git+https://github.com/other/code#123')+        result, _ = self.run_gate([EDIT_SESSION, foreign])+        self.assertNotEqual(result.returncode, 0)++    def test_existing_storytold_registry_and_local_packages_still_pass(self):+        """The default source policy remains unchanged for every other caller."""+        packages = [+            {'name': 'local', 'version': '1.0.0'},+            {'name': 'registry', 'version': '1.0.0', 'source': 'registry+https://github.com/rust-lang/crates.io-index'},+            {'name': 'craft-ui', 'version': '1.0.0', 'source': 'git+https://github.com/storytold/craft-libs#123'},+        ]+        for repository in ('storytold/pdfcraft', 'storytold/photocraft', ''):+            with self.subTest(repository=repository):+                result, _ = self.run_gate(packages, repository=repository)+                self.assertEqual(result.returncode, 0, result.stderr + result.stdout)++    def test_source_replacement_patch_and_path_overrides_are_still_refused(self):+        """Both Cargo config spellings reject substitutions before invoking Cargo."""+        for filename in ('.cargo/config', '.cargo/config.toml'):+            for config in ('[source.crates-io]\nreplace-with="other"\n',+                           '[patch.crates-io]\n', 'paths=["../other"]\n'):+                with self.subTest(filename=filename, config=config):+                    result, arguments = self.run_gate([EDIT_SESSION], configs={filename: config})+                    self.assertNotEqual(result.returncode, 0)+                    self.assertIsNone(arguments)++    def test_missing_lockfile_and_metadata_failure_still_fail(self):+        """The exception cannot bypass lockfile presence or Cargo's locked resolution."""+        result, arguments = self.run_gate([], lockfile=False)+        self.assertNotEqual(result.returncode, 0)+        self.assertIsNone(arguments)+        result, _ = self.run_gate([EDIT_SESSION], metadata_status=23)+        self.assertEqual(result.returncode, 23)+++if __name__ == '__main__':+    unittest.main()