ci: validate contracts early and report real cache restores

This commit is contained in:
kuddev committed 2026-09-23 19:25:26 +08:00
1 parent 7c8298d817
commit 0133873c46
5 files changed
+183 -1

No files matched your search

+54
View File
@@ -22,6 +22,15 @@ outputs:
cache-hit:
description: Whether the compiled target cache matched exactly
value: ${{ steps.target.outputs.cache-hit }}
cache-matched-key:
description: The compiled target cache key actually restored by actions/cache
value: ${{ steps.target.outputs.cache-matched-key }}
legacy-fallback-invoked:
description: Whether the optional legacy cache fallback action was invoked
value: ${{ steps.cache-summary.outputs.legacy-fallback-invoked }}
legacy-fallback-outcome:
description: Outcome of the optional legacy cache fallback action
value: ${{ steps.cache-summary.outputs.legacy-fallback-outcome }}
runs:
using: composite
@@ -96,6 +105,7 @@ runs:
${{ steps.identity.outputs.restore-key }}
${{ steps.identity.outputs.migration-key }}
- name: Recover an existing combined cache during migration
id: legacy-fallback
if: steps.target.outputs.cache-matched-key == '' && inputs.legacy-key != ''
uses: Swatinem/rust-cache@v2
with:
@@ -104,6 +114,50 @@ runs:
cache-bin: false
cache-on-failure: true
save-if: false
- name: Summarize compiled target cache
id: cache-summary
if: always()
shell: bash
env:
TARGET_CACHE_HIT: ${{ steps.target.outputs.cache-hit }}
TARGET_CACHE_MATCHED_KEY: ${{ steps.target.outputs.cache-matched-key }}
SAVE_KEY: ${{ steps.identity.outputs.key }}
LEGACY_CONFIGURED: ${{ inputs.legacy-key != '' && 'true' || 'false' }}
LEGACY_OUTCOME: ${{ steps.legacy-fallback.outcome }}
CACHE_WORKLOAD: ${{ inputs.key }}
RUNNER_OS_VALUE: ${{ runner.os }}
RUNNER_ARCH_VALUE: ${{ runner.arch }}
run: |
set -euo pipefail
target_cache_hit="${TARGET_CACHE_HIT:-false}"
target_cache_matched_key="${TARGET_CACHE_MATCHED_KEY:-<none>}"
save_key="${SAVE_KEY:-<none>}"
legacy_configured="${LEGACY_CONFIGURED:-false}"
legacy_outcome="${LEGACY_OUTCOME:-skipped}"
legacy_invoked=false
if [[ "$legacy_configured" == 'true' && "$legacy_outcome" != 'skipped' && -n "$legacy_outcome" ]]; then
legacy_invoked=true
fi
{
echo '### Rust compiled target cache'
echo
echo "- Workload: \`$CACHE_WORKLOAD\`"
echo "- Runner: \`$RUNNER_OS_VALUE / $RUNNER_ARCH_VALUE\`"
echo "- Target cache exact hit: \`$target_cache_hit\`"
echo "- Target cache matched key: \`$target_cache_matched_key\`"
echo "- Save key: \`$save_key\`"
echo "- Legacy fallback configured: \`$legacy_configured\`"
echo "- Legacy fallback invoked: \`$legacy_invoked\`"
echo "- Legacy fallback outcome: \`$legacy_outcome\`"
echo '- Legacy fallback status is separate and is not counted as target exact hit'
} >> "$GITHUB_STEP_SUMMARY"
{
echo "legacy-fallback-invoked=$legacy_invoked"
echo "legacy-fallback-outcome=$legacy_outcome"
} >> "$GITHUB_OUTPUT"
- name: Populate complete downloads before sharing the cache
if: steps.downloads.outputs.cache-hit != 'true'
shell: bash
+7
View File
@@ -54,6 +54,13 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Test CI contracts before matrix planning
run: |
python3 -m unittest \
scripts.tests.test_ci_plan \
scripts.tests.test_ci_native_tests \
scripts.tests.test_stable_release \
scripts.tests.test_ci_cache -v
- name: Plan native validation
id: matrix
shell: bash
@@ -60,6 +60,15 @@ The existing Windows diagnostic dispatch continues to use its separate workflow.
## Validation
The required `lint` job now runs the lightweight planner/native-workflow/
stable-release and cache contract tests after `cargo fmt --all -- --check` and before
`ci_plan.py` writes either matrix. The count is 7 planner tests, 10 native
workflow tests, 17 stable-release tests and 8 cache contract tests. A final local run of the exact
command completed 42 tests (41 passed, one Windows-only case skipped) in 3.341 seconds on 2026-09-23; this is a local
measurement, not a general speed guarantee. The workflow contract test checks
that this step precedes planning and that both dynamic matrices depend on the
required lint job.
Planner tests cover draft and ready PRs, all full-validation caller events,
malformed input and real command-line output. Workflow contract tests require the
existing lint check to own planning, both matrices to depend on it, draft step
+97
View File
@@ -3,6 +3,8 @@
import os
from pathlib import Path
import re
import shutil
import subprocess
import tempfile
import textwrap
import unittest
@@ -13,6 +15,10 @@ ACTION = Path(__file__).resolve().parents[2] / ".github/actions/rust-cache/actio
class CacheIdentityTests(unittest.TestCase):
@staticmethod
def action_text() -> str:
return ACTION.read_text(encoding="utf-8")
def identity(self, *, compiler="rustc 1.97.1", sdk="15.0", **changes):
source = ACTION.read_text(encoding="utf-8")
body = re.search(r"python - <<'PY'\n(.*?)\n PY", source, re.S)
@@ -67,6 +73,97 @@ class CacheIdentityTests(unittest.TestCase):
self.assertEqual(before["restore-key"], after["restore-key"])
self.assertEqual(before["key"], self.identity(CACHE_REVISION="one", PREVIEW_ID="third")["key"])
def test_summary_exposes_real_target_cache_outputs(self):
source = self.action_text()
self.assertIn(
"value: ${{ steps.target.outputs.cache-matched-key }}",
source,
)
self.assertIn(
"value: ${{ steps.cache-summary.outputs.legacy-fallback-invoked }}",
source,
)
self.assertIn(
"value: ${{ steps.cache-summary.outputs.legacy-fallback-outcome }}",
source,
)
self.assertIn(
"TARGET_CACHE_HIT: ${{ steps.target.outputs.cache-hit }}",
source,
)
self.assertIn(
"TARGET_CACHE_MATCHED_KEY: ${{ steps.target.outputs.cache-matched-key }}",
source,
)
self.assertIn("SAVE_KEY: ${{ steps.identity.outputs.key }}", source)
self.assertIn("CACHE_WORKLOAD: ${{ inputs.key }}", source)
self.assertIn("RUNNER_OS_VALUE: ${{ runner.os }}", source)
self.assertIn("RUNNER_ARCH_VALUE: ${{ runner.arch }}", source)
summary = source.split("- name: Summarize compiled target cache", 1)[1]
summary = summary.split("\n - name:", 1)[0]
run_body = summary.split(" run: |", 1)[1]
self.assertNotIn("${{", run_body)
for field in (
"Target cache exact hit",
"Target cache matched key",
"Save key",
"Legacy fallback invoked",
"Legacy fallback outcome",
):
self.assertIn(field, run_body)
def test_legacy_fallback_is_distinct_from_target_cache_hit(self):
source = self.action_text()
legacy = source.split("- name: Recover an existing combined cache during migration", 1)[1]
self.assertIn("id: legacy-fallback", legacy)
self.assertIn(
"if: steps.target.outputs.cache-matched-key == '' && inputs.legacy-key != ''",
legacy,
)
self.assertIn("LEGACY_OUTCOME: ${{ steps.legacy-fallback.outcome }}", source)
self.assertIn("legacy-fallback-invoked=", source)
self.assertIn("Legacy fallback status is separate and is not counted as target exact hit", source)
self.assertNotIn("LEGACY_KEY:", source)
self.assertNotIn("ACTIONS_RUNTIME_TOKEN", source)
self.assertNotIn("GITHUB_TOKEN", source)
@unittest.skipUnless(os.name != "nt" and shutil.which("bash"), "POSIX summary shell")
def test_summary_executes_exact_partial_and_legacy_cache_cases(self):
source = self.action_text().split("- name: Summarize compiled target cache", 1)[1]
script = textwrap.dedent(source.split(" run: |\n", 1)[1].split("\n - name:", 1)[0])
cases = (
("true", "current-key", "false", "skipped", "false"),
("false", "older-compatible-key", "true", "skipped", "false"),
("", "", "true", "success", "true"),
("", "", "true", "failure", "true"),
)
for hit, matched, configured, outcome, invoked in cases:
with self.subTest(hit=hit, matched=matched, outcome=outcome), tempfile.TemporaryDirectory() as directory:
root = Path(directory)
marker = root / "must-not-be-created"
workload = f"$(touch {marker})"
environment = {
"PATH": os.environ["PATH"],
"GITHUB_STEP_SUMMARY": str(root / "summary"),
"GITHUB_OUTPUT": str(root / "outputs"),
"TARGET_CACHE_HIT": hit, "TARGET_CACHE_MATCHED_KEY": matched,
"SAVE_KEY": "new-save-key", "LEGACY_CONFIGURED": configured,
"LEGACY_OUTCOME": outcome, "CACHE_WORKLOAD": workload,
"RUNNER_OS_VALUE": "Linux", "RUNNER_ARCH_VALUE": "X64",
}
result = subprocess.run([shutil.which("bash"), "-c", script], env=environment,
text=True, capture_output=True, check=False)
self.assertEqual(result.returncode, 0, result.stderr)
summary = (root / "summary").read_text()
outputs = (root / "outputs").read_text()
self.assertIn(f"Target cache exact hit: `{hit or 'false'}`", summary)
self.assertIn(f"Target cache matched key: `{matched or '<none>'}`", summary)
self.assertIn(f"legacy-fallback-invoked={invoked}\n", outputs)
self.assertIn(f"legacy-fallback-outcome={outcome}\n", outputs)
self.assertIn(workload, summary)
self.assertFalse(marker.exists(), "summary values must never execute as shell code")
@unittest.skipUnless(os.name == "nt", "Windows cache migration")
def test_windows_migration_preserves_compiler_and_workload_boundaries(self):
before = self.identity()
+16 -1
View File
@@ -16,9 +16,24 @@ class NativeSuiteTests(unittest.TestCase):
workflow = (root / ".github/workflows/linux-lua.yml").read_text()
lint = workflow.split("\n lint:\n", 1)[1].split("\n native-tests:\n", 1)[0]
self.assertIn("name: lint", lint)
self.assertIn("Test CI contracts before matrix planning", lint)
for contract in (
"scripts.tests.test_ci_plan",
"scripts.tests.test_ci_native_tests",
"scripts.tests.test_stable_release",
"scripts.tests.test_ci_cache",
):
self.assertIn(contract, lint)
self.assertIn("python scripts/ci_plan.py", lint)
self.assertIn('--event-path "$GITHUB_EVENT_PATH"', lint)
self.assertLess(lint.index("cargo fmt"), lint.index("python scripts/ci_plan.py"))
self.assertLess(
lint.index("cargo fmt"),
lint.index("Test CI contracts before matrix planning"),
)
self.assertLess(
lint.index("Test CI contracts before matrix planning"),
lint.index("python scripts/ci_plan.py"),
)
for job, output in (("native-tests", "native_matrix"),
("macos-release-check", "release_matrix")):
body = workflow.split(f"\n {job}:\n", 1)[1]