diff --git a/.github/workflows/ci-guards.yml b/.github/workflows/ci-guards.yml index d639530f5126..965caabf2164 100644 --- a/.github/workflows/ci-guards.yml +++ b/.github/workflows/ci-guards.yml @@ -608,6 +608,10 @@ jobs: if: ${{ matrix.group == 'ci' }} run: python3 tests/test_ci_checkout_network_diagnostics.py + - name: Validate production drift issue updates are sticky + if: ${{ matrix.group == 'ci' }} + run: python3 tests/test_iroh_drift_issue.py + - name: Validate Swift Testing suite timeout guard if: ${{ matrix.group == 'app-host-execution' }} run: python3 tests/test_swift_testing_suite_timeout.py diff --git a/.github/workflows/iroh-v2-production-drift.yml b/.github/workflows/iroh-v2-production-drift.yml index be5b5d384882..2cd20970592a 100644 --- a/.github/workflows/iroh-v2-production-drift.yml +++ b/.github/workflows/iroh-v2-production-drift.yml @@ -78,7 +78,7 @@ jobs: await github.rest.issues.createLabel({ owner, repo, name: label, color: 'b60205', description: 'Production IROH v2 Worker is behind main' }); } if (open.data.length > 0) { - await github.rest.issues.createComment({ owner, repo, issue_number: open.data[0].number, body }); + await github.rest.issues.update({ owner, repo, issue_number: open.data[0].number, body }); } else { await github.rest.issues.create({ owner, repo, title: 'IROH v2 production Worker is behind main', labels: [label], body }); } diff --git a/scripts/ci/workflow_guard_groups.py b/scripts/ci/workflow_guard_groups.py index 1c32c49af696..a0a1c4f9229c 100644 --- a/scripts/ci/workflow_guard_groups.py +++ b/scripts/ci/workflow_guard_groups.py @@ -70,6 +70,8 @@ ".github/workflows/ci-web.yml": frozenset(GROUPS), ".github/workflows/web-complexity.yml": frozenset(("ci",)), ".github/workflows/web-complexity-trusted.yml": frozenset(("ci",)), + ".github/workflows/iroh-v2-production-drift.yml": frozenset(("ci",)), + "tests/test_iroh_drift_issue.py": frozenset(("ci",)), ".github/review-fabric-policy.json": frozenset(("preflight",)), ".github/review-fabric.md": frozenset(("preflight",)), ".github/scripts/review_fabric.py": frozenset(("preflight",)), diff --git a/tests/test-execution.toml b/tests/test-execution.toml index 0e2852a265e5..63012055a413 100644 --- a/tests/test-execution.toml +++ b/tests/test-execution.toml @@ -1483,3 +1483,7 @@ lane = "linux-guard" [[test]] path = "tests/test_ci_package_interface_fingerprint.py" lane = "linux-guard" + +[[test]] +path = "tests/test_iroh_drift_issue.py" +lane = "linux-guard" diff --git a/tests/test_iroh_drift_issue.py b/tests/test_iroh_drift_issue.py new file mode 100644 index 000000000000..a5551afef6cc --- /dev/null +++ b/tests/test_iroh_drift_issue.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +"""Exercise the drift workflow's issue writes with an offline GitHub stub.""" + +import json +from pathlib import Path +import subprocess +import textwrap +import unittest + + +ROOT = Path(__file__).resolve().parents[1] +WORKFLOW = ROOT / ".github/workflows/iroh-v2-production-drift.yml" + + +class DriftIssueTests(unittest.TestCase): + def run_monitor(self, outcomes): + workflow = WORKFLOW.read_text() + script = textwrap.dedent(workflow.split(" script: |\n", 1)[1].split(" - name:", 1)[0]) + harness = """ +const writes = []; +let issue = null; +const fs = {existsSync: () => true, readFileSync: () => 'different rules'}; +const context = {repo: {owner: 'o', repo: 'r'}, serverUrl: 'https://github.com', runId: 1}; +const github = {rest: {issues: { + listForRepo: async () => ({data: issue && issue.state === 'open' ? [issue] : []}), + getLabel: async () => ({}), + create: async args => { writes.push(['create', args]); issue = {...args, number: 9, state: 'open'}; }, + createComment: async args => writes.push(['comment', args]), + update: async args => { writes.push(['update', args]); Object.assign(issue, args); }, +}}}; +const run = async () => { + for (const outcome of OUTCOMES) { + process.env.DRIFT_OUTCOME = outcome; + process.env.HEAD_SHA = String(context.runId).repeat(40); + await (async function(require) { SCRIPT })(name => { + if (name === 'fs') return fs; + throw new Error('unexpected dependency: ' + name); + }); + context.runId++; + } + process.stdout.write(JSON.stringify({writes, issue})); +}; +run().catch(error => { console.error(error); process.exitCode = 1; }); +""".replace("OUTCOMES", json.dumps(outcomes)).replace("SCRIPT", script) + result = subprocess.run(["node", "-e", harness], capture_output=True, text=True, check=True) + return json.loads(result.stdout) + + def test_repeated_drift_updates_the_issue_without_appending_comments(self): + result = self.run_monitor(["failure", "failure", "failure"]) + self.assertEqual([kind for kind, _ in result["writes"]], ["create", "update", "update"]) + self.assertIn("actions/runs/3", result["issue"]["body"]) + + def test_recovery_comments_once_and_closes_the_issue(self): + result = self.run_monitor(["failure", "success", "success"]) + self.assertEqual([kind for kind, _ in result["writes"]], ["create", "comment", "update"]) + self.assertEqual(result["issue"]["state"], "closed") + + def test_success_with_no_open_issue_writes_nothing(self): + self.assertEqual(self.run_monitor(["success"])["writes"], []) + + def test_new_drift_after_recovery_opens_a_new_issue(self): + result = self.run_monitor(["failure", "success", "failure"]) + self.assertEqual([kind for kind, _ in result["writes"]], ["create", "comment", "update", "create"]) + self.assertEqual(result["issue"]["state"], "open") + + +if __name__ == "__main__": + unittest.main()