diff --git a/.github/scripts/backend-tests-workflow-contract_test.py b/.github/scripts/backend-tests-workflow-contract_test.py
new file mode 100644
index 0000000000..7e2163122a
--- /dev/null
+++ b/.github/scripts/backend-tests-workflow-contract_test.py
@@ -0,0 +1,94 @@
+#!/usr/bin/env python3
+"""Contract tests for bounded backend test summaries and retained diagnostics."""
+
+from pathlib import Path
+import unittest
+
+
+REPO_ROOT = Path(__file__).resolve().parents[2]
+BACKEND_WORKFLOW = REPO_ROOT / ".github" / "workflows" / "backend-tests.yml"
+LINT_WORKFLOW = REPO_ROOT / ".github" / "workflows" / "lint-action-pinning.yml"
+
+
+def step_block(workflow: str, name: str) -> str:
+ marker = f" - name: {name}\n"
+ _, separator, remainder = workflow.partition(marker)
+ if not separator:
+ raise AssertionError(f"backend-tests.yml has no {name!r} step")
+ return remainder.partition("\n - name: ")[0]
+
+
+class BackendTestsWorkflowContractTest(unittest.TestCase):
+ def test_generated_report_is_redirected_then_published_with_a_bound(self) -> None:
+ workflow = BACKEND_WORKFLOW.read_text(encoding="utf-8")
+ checkout = step_block(workflow, "Checkout Go test reporter")
+ generate = step_block(workflow, "Generate test report")
+ publish = step_block(workflow, "Publish bounded test report summary")
+
+ temporary_summary = "$RUNNER_TEMP/backend-test-summary-${{ matrix.shard }}.md"
+ self.assertIn(
+ "uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10",
+ checkout,
+ )
+ self.assertIn("repository: robherley/go-test-action", checkout)
+ self.assertIn(
+ "ref: 2f859e0c8769d755d3174eecb9af8f64660827f3",
+ checkout,
+ )
+ self.assertIn("path: .github/vendor/go-test-action", checkout)
+ self.assertIn(f'report_summary="{temporary_summary}"', generate)
+ self.assertNotIn("${{ runner.temp }}", generate)
+ self.assertIn(': > "$report_summary"', generate)
+ self.assertIn('GITHUB_STEP_SUMMARY="$report_summary"', generate)
+ self.assertIn(
+ 'INPUT_FROMJSONFILE="$GITHUB_WORKSPACE/apps/backend/'
+ 'test-results-${{ matrix.shard }}.json"',
+ generate,
+ )
+ self.assertIn(
+ 'INPUT_MODULEDIRECTORY="$GITHUB_WORKSPACE/apps/backend"',
+ generate,
+ )
+ self.assertIn('INPUT_OMIT="successful"', generate)
+ self.assertIn(
+ 'node ".github/vendor/go-test-action/dist/index.js"',
+ generate,
+ )
+ self.assertNotIn("uses: robherley/go-test-action", generate)
+ self.assertIn("if: always()", publish)
+ self.assertIn("python3 .github/scripts/bounded-step-summary.py", publish)
+ self.assertIn(f'--input "{temporary_summary}"', publish)
+ self.assertIn('--output "$GITHUB_STEP_SUMMARY"', publish)
+ self.assertIn("--diagnostics-label", publish)
+ self.assertIn("backend-test-results-${{ matrix.shard }}", publish)
+ self.assertIn("${{ github.run_id }}#artifacts", publish)
+
+ def test_full_json_diagnostics_remain_in_the_named_artifact(self) -> None:
+ workflow = BACKEND_WORKFLOW.read_text(encoding="utf-8")
+ upload = step_block(workflow, "Upload test artifacts")
+
+ self.assertIn("if: always()", upload)
+ self.assertIn("name: backend-test-results-${{ matrix.shard }}", upload)
+ self.assertIn("apps/backend/test-results-${{ matrix.shard }}.json", upload)
+
+ def test_summary_writer_changes_run_the_backend_workflow(self) -> None:
+ workflow = BACKEND_WORKFLOW.read_text(encoding="utf-8")
+ detect = step_block(workflow, "Detect relevant changes")
+
+ self.assertIn(".github/scripts/bounded-step-summary.py", detect)
+
+ def test_bounded_summary_contract_runs_in_ci(self) -> None:
+ workflow = LINT_WORKFLOW.read_text(encoding="utf-8")
+
+ self.assertIn(
+ "python3 .github/scripts/bounded-step-summary_test.py",
+ workflow,
+ )
+ self.assertIn(
+ "python3 .github/scripts/backend-tests-workflow-contract_test.py",
+ workflow,
+ )
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/.github/scripts/bounded-step-summary.py b/.github/scripts/bounded-step-summary.py
new file mode 100644
index 0000000000..fb92e7d52f
--- /dev/null
+++ b/.github/scripts/bounded-step-summary.py
@@ -0,0 +1,77 @@
+#!/usr/bin/env python3
+"""Write generated Markdown within GitHub Actions' step-summary size limit."""
+
+import argparse
+import re
+from pathlib import Path
+
+
+FAILURE_IDENTITY = re.compile(r"
š“([^<]*)")
+
+
+def parse_args() -> argparse.Namespace:
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--input", type=Path, required=True)
+ parser.add_argument("--output", type=Path, required=True)
+ parser.add_argument("--diagnostics-label", required=True)
+ parser.add_argument("--diagnostics-url", required=True)
+ parser.add_argument("--max-bytes", type=int, default=983_040)
+ return parser.parse_args()
+
+
+def bounded_summary(
+ source: bytes,
+ *,
+ diagnostics_label: str,
+ diagnostics_url: str,
+ max_bytes: int,
+) -> bytes:
+ source_text = source.decode("utf-8")
+ if len(source) <= max_bytes:
+ return source
+
+ notice = (
+ "> [!WARNING]\n"
+ "> This step summary was truncated to stay below GitHub Actions' "
+ "1 MiB limit. Full diagnostics are available in the "
+ f"[{diagnostics_label}]({diagnostics_url}) artifact.\n\n"
+ ).encode("utf-8")
+ if len(notice) > max_bytes:
+ raise ValueError("max bytes is too small for the truncation notice")
+
+ prefix_budget = max_bytes - len(notice)
+ failure_identities = FAILURE_IDENTITY.findall(source_text)
+ identity_section = b""
+ if failure_identities:
+ identity_section = (
+ "## Failed test identities\n\n"
+ + "\n".join(f"- `{identity}`" for identity in failure_identities)
+ + "\n\n"
+ ).encode("utf-8")
+ identity_section = (
+ identity_section[: max(0, prefix_budget)]
+ .decode("utf-8", errors="ignore")
+ .encode("utf-8")
+ )
+
+ prefix_budget -= len(identity_section)
+ # The full source is valid UTF-8, so ignoring errors can only discard a
+ # partial code point at the byte boundary.
+ prefix = source[:prefix_budget].decode("utf-8", errors="ignore").encode("utf-8")
+ return notice + identity_section + prefix
+
+
+def main() -> int:
+ args = parse_args()
+ output = bounded_summary(
+ args.input.read_bytes(),
+ diagnostics_label=args.diagnostics_label,
+ diagnostics_url=args.diagnostics_url,
+ max_bytes=args.max_bytes,
+ )
+ args.output.write_bytes(output)
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/.github/scripts/bounded-step-summary_test.py b/.github/scripts/bounded-step-summary_test.py
new file mode 100644
index 0000000000..4339008839
--- /dev/null
+++ b/.github/scripts/bounded-step-summary_test.py
@@ -0,0 +1,155 @@
+#!/usr/bin/env python3
+"""Behavior tests for the bounded GitHub Actions step-summary writer."""
+
+from pathlib import Path
+import subprocess
+import tempfile
+import unittest
+
+
+REPO_ROOT = Path(__file__).resolve().parents[2]
+SCRIPT = REPO_ROOT / ".github" / "scripts" / "bounded-step-summary.py"
+
+
+class BoundedStepSummaryTest(unittest.TestCase):
+ def run_writer(self, summary: str, *, max_bytes: int = 983_040) -> bytes:
+ with tempfile.TemporaryDirectory() as tmp:
+ tmp_path = Path(tmp)
+ source = tmp_path / "generated-summary.md"
+ output = tmp_path / "github-step-summary.md"
+ source.write_text(summary, encoding="utf-8")
+
+ result = subprocess.run(
+ [
+ "python3",
+ str(SCRIPT),
+ "--input",
+ str(source),
+ "--output",
+ str(output),
+ "--diagnostics-label",
+ "backend-test-results-1",
+ "--diagnostics-url",
+ "https://github.example.test/kdlbs/kandev/actions/runs/123#artifacts",
+ "--max-bytes",
+ str(max_bytes),
+ ],
+ capture_output=True,
+ text=True,
+ check=False,
+ )
+
+ self.assertEqual(result.returncode, 0, result.stderr)
+ return output.read_bytes()
+
+ def test_normal_summary_is_unchanged(self) -> None:
+ summary = "## š Test results\n\nš“ `TestWorkspaceFailure` failed\n"
+
+ self.assertEqual(self.run_writer(summary), summary.encode("utf-8"))
+
+ def test_oversized_summary_is_bounded_explicit_and_utf8_safe(self) -> None:
+ summary = (
+ "## š Test results\n\n"
+ "š“ `TestWorkspaceFailure` failed\n\n"
+ "boundary-padding-12"
+ + "diagnostic=å¤±ęš„\n" * 60_000
+ )
+ source_bytes = summary.encode("utf-8")
+ self.assertGreater(len(source_bytes), 1_048_576)
+
+ first = self.run_writer(summary)
+ second = self.run_writer(summary)
+ rendered = first.decode("utf-8")
+
+ self.assertLessEqual(len(first), 983_040)
+ self.assertEqual(first, second)
+ self.assertIn("TestWorkspaceFailure", rendered)
+ self.assertIn("truncated", rendered.lower())
+ self.assertIn("backend-test-results-1", rendered)
+ self.assertIn(
+ "https://github.example.test/kdlbs/kandev/actions/runs/123#artifacts",
+ rendered,
+ )
+
+ def test_oversized_summary_retains_later_failure_identity(self) -> None:
+ summary = (
+ "š Test results
\n"
+ ""
+ "- š“
TestEarlyFailure "
+ " |
\n"
+ + "diagnostic=å¤±ęš„\n" * 200_000
+ + ""
+ "- š“
TestLaterFailure "
+ " |
\n"
+ )
+
+ rendered = self.run_writer(summary).decode("utf-8")
+
+ self.assertIn("TestEarlyFailure", rendered)
+ self.assertIn("TestLaterFailure", rendered)
+
+ def test_invalid_utf8_input_is_rejected(self) -> None:
+ # Contract coverage for the writer's existing UTF-8 validation.
+ with tempfile.TemporaryDirectory() as tmp:
+ tmp_path = Path(tmp)
+ source = tmp_path / "generated-summary.md"
+ output = tmp_path / "github-step-summary.md"
+ source.write_bytes(b"valid prefix\xff")
+
+ result = subprocess.run(
+ [
+ "python3",
+ str(SCRIPT),
+ "--input",
+ str(source),
+ "--output",
+ str(output),
+ "--diagnostics-label",
+ "backend-test-results-1",
+ "--diagnostics-url",
+ "https://github.example.test/kdlbs/kandev/actions/runs/123#artifacts",
+ ],
+ capture_output=True,
+ text=True,
+ check=False,
+ )
+
+ self.assertNotEqual(result.returncode, 0)
+ self.assertIn("UnicodeDecodeError", result.stderr)
+
+ def test_impossibly_small_max_bytes_is_rejected(self) -> None:
+ with tempfile.TemporaryDirectory() as tmp:
+ tmp_path = Path(tmp)
+ source = tmp_path / "generated-summary.md"
+ output = tmp_path / "github-step-summary.md"
+ source.write_text("oversized summary", encoding="utf-8")
+
+ result = subprocess.run(
+ [
+ "python3",
+ str(SCRIPT),
+ "--input",
+ str(source),
+ "--output",
+ str(output),
+ "--diagnostics-label",
+ "backend-test-results-1",
+ "--diagnostics-url",
+ "https://github.example.test/kdlbs/kandev/actions/runs/123#artifacts",
+ "--max-bytes",
+ "1",
+ ],
+ capture_output=True,
+ text=True,
+ check=False,
+ )
+
+ self.assertNotEqual(result.returncode, 0)
+ self.assertIn(
+ "max bytes is too small for the truncation notice",
+ result.stderr,
+ )
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/.github/workflows/backend-tests.yml b/.github/workflows/backend-tests.yml
index 2879c169b8..c969f677f3 100644
--- a/.github/workflows/backend-tests.yml
+++ b/.github/workflows/backend-tests.yml
@@ -63,6 +63,7 @@ jobs:
scripts/check-make-shells
.github/workflows/backend-tests.yml
.github/scripts/changed-paths.py
+ .github/scripts/bounded-step-summary.py
!**/*.md
!**/*.mdx
!**/*.markdown
@@ -317,12 +318,41 @@ jobs:
end
'
- - name: Generate test report
- uses: robherley/go-test-action@2f859e0c8769d755d3174eecb9af8f64660827f3 # v1.1.0
+ # Run the pinned reporter as a child process so its summary target can be
+ # redirected. A `uses:` step cannot override GITHUB_STEP_SUMMARY because
+ # the runner reapplies its own file-command environment to JS actions.
+ - name: Checkout Go test reporter
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
if: always()
with:
- fromJSONFile: apps/backend/test-results-${{ matrix.shard }}.json
- omit: successful
+ repository: robherley/go-test-action
+ ref: 2f859e0c8769d755d3174eecb9af8f64660827f3 # v1.1.0
+ path: .github/vendor/go-test-action
+ persist-credentials: false
+
+ - name: Generate test report
+ if: always()
+ shell: bash
+ working-directory: ${{ github.workspace }}
+ run: |
+ report_summary="$RUNNER_TEMP/backend-test-summary-${{ matrix.shard }}.md"
+ : > "$report_summary"
+ GITHUB_STEP_SUMMARY="$report_summary" \
+ INPUT_FROMJSONFILE="$GITHUB_WORKSPACE/apps/backend/test-results-${{ matrix.shard }}.json" \
+ INPUT_MODULEDIRECTORY="$GITHUB_WORKSPACE/apps/backend" \
+ INPUT_OMIT="successful" \
+ node ".github/vendor/go-test-action/dist/index.js"
+
+ - name: Publish bounded test report summary
+ if: always()
+ shell: bash
+ working-directory: ${{ github.workspace }}
+ run: |
+ python3 .github/scripts/bounded-step-summary.py \
+ --input "$RUNNER_TEMP/backend-test-summary-${{ matrix.shard }}.md" \
+ --output "$GITHUB_STEP_SUMMARY" \
+ --diagnostics-label "backend-test-results-${{ matrix.shard }}" \
+ --diagnostics-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}#artifacts"
- name: Upload test artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
diff --git a/.github/workflows/lint-action-pinning.yml b/.github/workflows/lint-action-pinning.yml
index ba03d137bf..d219fc4a8f 100644
--- a/.github/workflows/lint-action-pinning.yml
+++ b/.github/workflows/lint-action-pinning.yml
@@ -73,6 +73,12 @@ jobs:
- name: Test E2E workflow contract
run: python3 .github/scripts/e2e-tests-workflow-contract_test.py
+ - name: Test bounded step summary
+ run: python3 .github/scripts/bounded-step-summary_test.py
+
+ - name: Test backend tests workflow contract
+ run: python3 .github/scripts/backend-tests-workflow-contract_test.py
+
- name: Test stale merge approval revocation workflow contract
run: python3 .github/scripts/revoke-ready-to-merge-workflow-contract_test.py
diff --git a/apps/backend/internal/auth/hostnames/resolver_test.go b/apps/backend/internal/auth/hostnames/resolver_test.go
index 5e80fe1c4d..a8023ab425 100644
--- a/apps/backend/internal/auth/hostnames/resolver_test.go
+++ b/apps/backend/internal/auth/hostnames/resolver_test.go
@@ -438,8 +438,12 @@ func TestResolverWakeQueueMatchesLookupConcurrency(t *testing.T) {
func TestResolverChecksEachUserOncePerResolve(t *testing.T) {
cache := &memoryCache{entries: map[string]CacheEntry{}, set: make(chan struct{}, 1)}
var settingsReads atomic.Int32
+ thirdSettingsRead := make(chan struct{})
+ var thirdSettingsReadOnce sync.Once
resolver := NewResolver(cache, func(context.Context, string) (bool, error) {
- settingsReads.Add(1)
+ if settingsReads.Add(1) == 3 {
+ thirdSettingsReadOnce.Do(func() { close(thirdSettingsRead) })
+ }
return true, nil
}, nil, nil, nil)
resolver.lookupAddr = func(context.Context, string) ([]string, error) {
@@ -454,9 +458,9 @@ func TestResolverChecksEachUserOncePerResolve(t *testing.T) {
[]string{"192.0.2.10", "::ffff:192.0.2.10"},
)
select {
- case <-cache.set:
+ case <-thirdSettingsRead:
case <-time.After(time.Second):
- t.Fatal("hostname lookup did not finish")
+ t.Fatal("resolver did not perform the publication settings read")
}
if got := settingsReads.Load(); got != 3 {
t.Fatalf("settings reads = %d, want admission, lookup, and publication reads", got)