diff --git a/.github/workflows/gate.yml b/.github/workflows/gate.yml index fa7b934..0a946f9 100644 --- a/.github/workflows/gate.yml +++ b/.github/workflows/gate.yml @@ -17,6 +17,11 @@ on: type: string required: false default: "cargo xtask ci" + setup-command: + description: "Optional setup script, run from the repository root before the gate (for example, fetch SHA-verified corpora)" + type: string + required: false + default: "" windows-command: description: "The gate on Windows legs; empty runs `command`" type: string @@ -274,6 +279,16 @@ jobs: save-if: ${{ github.event_name != 'pull_request' && (github.ref_name == github.event.repository.default_branch || (inputs.cache-save-on-dispatch && github.event_name == 'workflow_dispatch')) }} cache-on-failure: true + - name: Caller setup + if: inputs.setup-command != '' + shell: bash + env: + SETUP: ${{ inputs.setup-command }} + run: | + set -euo pipefail + printf '%s\n' "$SETUP" > "$RUNNER_TEMP/gate-setup.sh" + bash -euo pipefail "$RUNNER_TEMP/gate-setup.sh" + - name: Gate shell: bash env: diff --git a/README.md b/README.md index bb1b00a..8dee6e3 100644 --- a/README.md +++ b/README.md @@ -513,6 +513,8 @@ The quality gate for an app's pull requests and pushes to main. It runs `command - the app and craft-fonts (at the suite's pin, with `CRAFT_FONTS_REQUIRED=1`) checked out without persisted credentials, and craft-i18n when the app has `craft-i18n.rev`; - `packages` installed through `actions/install-apt-packages` on Linux legs (none when empty); +- an optional `setup-command` run from the repository root before the gate, for fetching pinned + test data such as licensed corpora; its environment changes can be written to `$GITHUB_ENV`; - the channel from `rust-toolchain.toml`, or `toolchain`, with clippy, rustfmt and `targets`; - prebuilt cargo tools from `tools` (by default `cargo-nextest@0.9.150`); - `CARGO_PROFILE_DEV_DEBUG=line-tables-only` (set `dev-debug: ""` to keep the repository's @@ -541,6 +543,11 @@ check, can be a second call with its own `command`, `os`, `tools` and `targets`, command: packaging/web/package.sh ``` +`setup-command` is also passed through the environment, then run with Bash from the checkout root. +Use it for caller-specific test inputs whose contents are pinned and verified by the caller's +fetcher. For example, a corpus-enabled workspace can fetch its locked corpus before running the +gate and export `CRAFT_PSD_CORPUS` through `$GITHUB_ENV`; a failed fetch stops the job before tests. + #### Caller Example: ```yaml name: CI diff --git a/tests/test_gate.py b/tests/test_gate.py index 9b2d74a..7f57120 100644 --- a/tests/test_gate.py +++ b/tests/test_gate.py @@ -135,6 +135,33 @@ class CallerEnvironmentTests(unittest.TestCase): self.assertEqual(self.run_step(ALL=f'GOOD=1\n{line}')[0], 1) +class CallerSetupTests(unittest.TestCase): + STEP = step('Caller setup') + + def run_step(self, setup): + 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, 'SETUP': setup, 'RUNNER_TEMP': directory, + 'GITHUB_ENV': str(github_env)}) + return result.returncode, github_env.read_text().splitlines(), Path(directory, 'setup-ran').exists() + + def test_setup_input_is_passed_through_env_before_the_gate(self): + self.assertEqual(set(self.STEP['env']), {'SETUP'}) + self.assertLess(STEPS.index(self.STEP), STEPS.index(step('Gate'))) + self.assertNotIn('${{', self.STEP['run']) + + def test_setup_can_provision_data_and_export_its_path(self): + setup = 'touch setup-ran\necho "CRAFT_PSD_CORPUS=/corpus" >> "$GITHUB_ENV"' + self.assertEqual(self.run_step(setup), (0, ['CRAFT_PSD_CORPUS=/corpus'], True)) + + def test_setup_failure_stops_the_gate_job(self): + self.assertEqual(self.run_step('exit 17')[0], 17) + + class ProfileCacheTests(unittest.TestCase): def key(self, manifest): with tempfile.TemporaryDirectory() as directory: