Files
craft-common-actions/tests/test_gate.py
T
aiongg 9587bf235d Shared quality-gate workflow for the craft apps (gate.yml) (#69)
* ci: add a shared CI workflow for the craft apps

A reusable workflow that runs each app's quality gate (by default
`cargo xtask ci`) on one or more runners, with the suite's shared setup:
cached apt packages, the repository's toolchain, prebuilt cargo tools,
the craft-fonts and craft-i18n checkouts, and a dependency cache that only
the default branch writes.

The rust-cache key includes the root Cargo.toml: rust-cache hashes only
the workspace members' manifests, so a [profile] change in a virtual root
manifest would otherwise keep restoring the old cache.

Part of #60.

* tests: cover the shared CI workflow

test_ci.py checks that third-party actions are pinned to commits, that
only non-PR runs on the default branch save the cache, that caller input
reaches run blocks only through env, and runs the caller-environment
script under bash to check it writes valid KEY=VALUE lines and rejects
others. The craft-fonts test also holds ci.yml to the suite's pin, and the
test workflow runs on changes to ci.yml.

* docs: document the shared CI workflow

Describes what ci.yml sets up and runs, with a plain caller and one that
adds a Windows leg on main, manual runs and full-ci pull requests.

* actions: don't persist credentials in the craft-fonts and craft-i18n checkouts

Both repositories are public and nothing later in a job runs git in them,
so the token has no use there.

* ci: name the shared workflow gate.yml

The repository's own CI proposed in #21 takes .github/workflows/ci.yml.
Callers use .github/workflows/gate.yml@main; the job still reports as
gate (<os>).
2026-10-23 12:15:49 -04:00

76 lines
3.1 KiB
Python

"""The shared gate workflow: pinned actions, a cache only the default branch writes, and caller
input that reaches the shell only through the environment."""
from pathlib import Path
import os
import re
import subprocess
import tempfile
import unittest
import yaml
ROOT = Path(__file__).resolve().parents[1]
WORKFLOW = yaml.load((ROOT / '.github/workflows/gate.yml').read_text(), Loader=yaml.BaseLoader)
STEPS = WORKFLOW['jobs']['gate']['steps']
OWN_ACTION = re.compile(r'^storytold/craft-common-actions/actions/[a-z0-9-]+@main$')
def step(name):
return next(s for s in STEPS if s.get('name') == name)
class GateWorkflowTests(unittest.TestCase):
def test_third_party_actions_are_pinned_to_a_commit(self):
for s in STEPS:
if 'uses' in s:
with self.subTest(uses=s['uses']):
if not OWN_ACTION.match(s['uses']):
self.assertRegex(s['uses'], r'@[0-9a-f]{40}$')
def test_only_pushes_to_the_default_branch_save_the_cache(self):
cache = next(s for s in STEPS if 'rust-cache' in s.get('uses', ''))
condition = re.sub(r'\s+', ' ', cache['with']['save-if'])
self.assertIn("github.event_name != 'pull_request'", condition)
self.assertIn('github.ref_name == github.event.repository.default_branch', condition)
self.assertNotIn('||', condition)
def test_inputs_reach_scripts_only_through_env(self):
for s in STEPS:
if 'run' in s:
with self.subTest(step=s.get('name')):
self.assertNotIn('${{', s['run'])
class CallerEnvironmentTests(unittest.TestCase):
STEP = step('Caller environment')
def run_step(self, runner_os='Linux', **inputs):
env = {'ALL': '', 'LINUX': '', 'WINDOWS': '', 'DEV_DEBUG': '', **inputs}
with tempfile.TemporaryDirectory() as directory:
github_env = Path(directory, 'github_env')
github_env.touch()
result = subprocess.run(
['bash', '--noprofile', '--norc', '-eo', 'pipefail', '-c', self.STEP['run']],
cwd=directory, capture_output=True, text=True,
env={**os.environ, **env, 'RUNNER_OS': runner_os, 'GITHUB_ENV': str(github_env)})
return result.returncode, github_env.read_text().splitlines()
def test_the_step_reads_its_inputs_from_env(self):
self.assertEqual(set(self.STEP['env']), {'ALL', 'LINUX', 'WINDOWS', 'DEV_DEBUG'})
def test_writes_shared_and_per_os_lines(self):
inputs = {'ALL': 'A=1\n\nB=two words', 'LINUX': 'ONLY_LINUX=1', 'WINDOWS': 'ONLY_WINDOWS=1',
'DEV_DEBUG': 'line-tables-only'}
self.assertEqual(self.run_step('Linux', **inputs), (0, [
'CARGO_PROFILE_DEV_DEBUG=line-tables-only', 'A=1', 'B=two words', 'ONLY_LINUX=1']))
self.assertEqual(self.run_step('Windows', **inputs)[1][-1], 'ONLY_WINDOWS=1')
def test_rejects_a_line_that_is_not_key_value(self):
for line in ('NOT AN ASSIGNMENT', '1X=1', 'X<<EOF'):
with self.subTest(line=line):
self.assertEqual(self.run_step(ALL=f'GOOD=1\n{line}')[0], 1)
if __name__ == '__main__':
unittest.main()