Files
craft-common-actions/tests/test_check_git_sources.py
T
nnaannooandKristian Myram 4d1066685f Add git-sources.yml: the shared "Git dependencies come from storytold" check (#147)
Eight apps paste the same 25-line job into ci.yml, nix.yml or release.yml (ten
copies): Cargo.lock exists and is up to date, no .cargo/config(.toml)
replaces, patches or overrides a source, and every git dependency comes from
github.com/storytold. Apps' workflows are meant to only call workflows here.

actions/check-git-sources holds the check as a tested script, and
git-sources.yml runs it on a credential-less checkout, so an app swaps its
job for one `uses:` line and keeps `needs: git-sources`. It reports every
problem rather than the first, as GitHub error annotations, and treats
unreadable TOML as a failure. On all 18 apps' current Cargo.lock and Cargo
config it agrees with the inline copy.

Co-authored-by: Kristian Myram <kmy@oresundsbron.com>
2026-11-05 09:13:01 -05:00

179 lines
8.6 KiB
Python

"""The git-sources check, run on small Cargo workspaces with a stand-in cargo."""
from pathlib import Path
import os
import stat
import subprocess
import sys
import tempfile
import textwrap
import unittest
import yaml
ROOT = Path(__file__).resolve().parents[1]
ACTION_DIR = ROOT / "actions/check-git-sources"
SCRIPT = ACTION_DIR / "check_git_sources.py"
ACTION = yaml.load((ACTION_DIR / "action.yml").read_text(), Loader=yaml.BaseLoader)
WORKFLOW = yaml.load((ROOT / ".github/workflows/git-sources.yml").read_text(), Loader=yaml.BaseLoader)
STORYTOLD = "git+https://github.com/storytold/craft-libs?branch=main#0123456789abcdef0123456789abcdef01234567"
REGISTRY = "registry+https://github.com/rust-lang/crates.io-index"
def lock(*sources):
packages = "".join(f'\n[[package]]\nname = "dep{i}"\nversion = "1.0.0"\nsource = "{source}"\n'
for i, source in enumerate(sources))
return f'version = 4\n\n[[package]]\nname = "app"\nversion = "0.1.0"\n{packages}'
class CheckGitSourcesTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.root = Path(self.temp.name)
self.workspace = self.root / "app"
self.workspace.mkdir()
self.calls = self.root / "cargo-calls.log"
self.calls.touch()
self.cargo = self.stand_in_cargo()
def tearDown(self):
self.temp.cleanup()
def stand_in_cargo(self):
# Records its arguments; exits with $FAKE_CARGO_EXIT, printing an error the way cargo does.
body = textwrap.dedent(f"""\
import os, sys
with open({str(self.calls)!r}, "a") as log:
log.write(" ".join(sys.argv[1:]) + "\\n")
code = int(os.environ.get("FAKE_CARGO_EXIT", "0"))
if code:
print("error: the lock file needs to be updated but --locked was passed", file=sys.stderr)
sys.exit(code)
""")
stub = self.root / "cargo_stub.py"
stub.write_text(body)
if os.name == "nt":
cargo = self.root / "cargo.bat"
cargo.write_text(f'@"{sys.executable}" "{stub}" %*\r\n')
else:
cargo = self.root / "cargo"
cargo.write_text(f'#!/bin/sh\nexec "{sys.executable}" "{stub}" "$@"\n')
cargo.chmod(cargo.stat().st_mode | stat.S_IXUSR)
return cargo
def write(self, name, text):
path = self.workspace / name
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(text)
def check(self, *args, **env):
environment = {key: value for key, value in os.environ.items() if key != "GITHUB_ACTIONS"}
environment.update(CARGO=str(self.cargo), **env)
return subprocess.run([sys.executable, str(SCRIPT), "--directory", str(self.workspace), *args],
env=environment, capture_output=True, text=True)
def assertFails(self, result, message):
self.assertEqual(result.returncode, 1, result.stdout + result.stderr)
self.assertIn(message, result.stdout)
def test_registry_and_storytold_git_dependencies_pass_and_the_lock_is_checked(self):
self.write("Cargo.lock", lock(REGISTRY, STORYTOLD))
result = self.check()
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertIn("Git dependencies all come from https://github.com/storytold/", result.stdout)
self.assertEqual(self.calls.read_text().splitlines(), ["metadata --locked --format-version 1"])
def test_a_missing_lock_file_fails(self):
self.assertFails(self.check(), "Cargo.lock is missing")
self.assertEqual(self.calls.read_text(), "", "cargo must not run without a lock file")
def test_a_git_dependency_from_elsewhere_fails_and_names_every_one(self):
others = ["git+https://github.com/someone/fork?rev=abc#abc", "git+https://gitlab.com/x/y#def"]
self.write("Cargo.lock", lock(STORYTOLD, *others))
result = self.check()
for source in others:
self.assertFails(result, f"git dependency outside https://github.com/storytold/: {source}")
self.assertNotIn("craft-libs", result.stdout)
def test_a_look_alike_owner_is_not_storytold(self):
for source in ("git+https://github.com/storytold-mirror/craft-libs#abc",
"git+https://github.com/storytold.evil/craft-libs#abc",
"git+http://github.com/storytold/craft-libs#abc"):
with self.subTest(source=source):
self.write("Cargo.lock", lock(source))
self.assertFails(self.check(), source)
def test_cargo_config_that_replaces_patches_or_overrides_a_source_fails(self):
configs = {
'[source.crates-io]\nreplace-with = "vendored"\n\n[source.vendored]\ndirectory = "vendor"\n':
"replaces the source crates-io",
"[source]\ncrates-io = { replace-with = 'mirror' }\nmirror = { registry = 'https://example.com/index' }\n":
"replaces the source crates-io",
'[patch.crates-io]\nserde = { git = "https://github.com/someone/serde" }\n': "patches dependencies",
'paths = ["../local-serde"]\n': "overrides dependencies with local paths",
}
self.write("Cargo.lock", lock(REGISTRY))
for name in (".cargo/config.toml", ".cargo/config"):
for text, message in configs.items():
with self.subTest(name=name, config=text):
self.write(name, text)
self.assertFails(self.check(), f"{name} {message}")
(self.workspace / name).unlink()
def test_harmless_cargo_config_passes(self):
self.write("Cargo.lock", lock(REGISTRY))
self.write(".cargo/config.toml", '[alias]\nxtask = "run --package xtask --"\n\n[source.crates-io]\nprotocol = "sparse"\n')
result = self.check()
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
def test_an_out_of_date_lock_file_fails(self):
self.write("Cargo.lock", lock(REGISTRY))
result = self.check(FAKE_CARGO_EXIT="101")
self.assertFails(result, "Cargo.lock is out of date with the manifests")
self.assertIn("the lock file needs to be updated", result.stdout)
def test_unreadable_toml_fails_instead_of_passing(self):
self.write("Cargo.lock", "this is = = not toml")
self.assertFails(self.check(), "Cargo.lock is not valid TOML")
self.write("Cargo.lock", lock(REGISTRY))
self.write(".cargo/config.toml", "[patch\n")
self.assertFails(self.check(), ".cargo/config.toml is not valid TOML")
def test_another_allowed_prefix(self):
self.write("Cargo.lock", lock("git+https://git.example.com/team/lib#abc"))
self.assertEqual(self.check("--allowed-prefix", "git+https://git.example.com/team/").returncode, 0)
self.assertFails(self.check(), "git dependency outside")
result = self.check("--allowed-prefix", "https://git.example.com/")
self.assertEqual(result.returncode, 2)
self.assertIn("must be one line starting with git+", result.stderr)
def test_in_github_actions_each_problem_is_an_error_annotation(self):
self.write("Cargo.lock", lock("git+https://github.com/someone/a#1", "git+https://github.com/someone/b#2"))
result = self.check(GITHUB_ACTIONS="true")
errors = [line for line in result.stdout.splitlines() if line.startswith("::error::")]
self.assertEqual(len(errors), 2, result.stdout)
class CheckGitSourcesWiringTests(unittest.TestCase):
def test_the_action_passes_its_inputs_through_the_environment(self):
step = ACTION["runs"]["steps"][0]
# Inputs reach the shell as variables, never pasted into the script.
self.assertNotIn("${{", step["run"])
self.assertEqual(step["env"]["WORKSPACE_DIR"], "${{ inputs.working-directory }}")
self.assertEqual(step["env"]["ALLOWED_PREFIX"], "${{ inputs.allowed-prefix }}")
self.assertEqual(ACTION["inputs"]["allowed-prefix"]["default"], "git+https://github.com/storytold/")
def test_the_workflow_checks_out_without_credentials_and_runs_the_action(self):
job = WORKFLOW["jobs"]["git-sources"]
self.assertEqual(WORKFLOW["permissions"], {"contents": "read"})
self.assertIn("timeout-minutes", job)
checkout, check = job["steps"]
self.assertTrue(checkout["uses"].startswith("actions/checkout@"))
self.assertEqual(checkout["with"]["persist-credentials"], "false")
self.assertEqual(check["uses"], "storytold/craft-common-actions/actions/check-git-sources@main")
self.assertEqual(check["with"]["working-directory"], "${{ inputs.working-directory }}")
if __name__ == "__main__":
unittest.main()