Repository navigation
fix(health): restore Health 68 execution and assert durable-tracker liveness #3189
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
788aaf5
804fe1a
3394369
a670af5
38f4d65
e77b8fc
37adb2b
0f9effa
14fc8c1
251c6bb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,9 @@ on: | |
| workflow_run: | ||
| workflows: [Merge Sync PRs] | ||
| types: [completed] | ||
| schedule: | ||
| # Daily self-healing cadence; workflow_run remains debounced below. | ||
| - cron: '15 6 * * *' | ||
| workflow_dispatch: | ||
| inputs: | ||
| repos: | ||
|
|
@@ -24,10 +27,10 @@ on: | |
|
|
||
| permissions: | ||
| contents: read | ||
| issues: write | ||
| actions: read | ||
|
|
||
| concurrency: | ||
| group: consumer-sync-drift-${{ github.repository }}-${{ github.ref }} | ||
| group: health-68-consumer-sync-drift | ||
| cancel-in-progress: true | ||
|
|
||
| jobs: | ||
|
|
@@ -37,6 +40,8 @@ jobs: | |
| github.event_name != 'workflow_run' || | ||
| (github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'main') | ||
| runs-on: ubuntu-latest | ||
| outputs: | ||
| drift_failed: ${{ steps.compare.outcome == 'failure' }} | ||
| steps: | ||
| - name: Checkout Workflows repo | ||
| uses: actions/checkout@v7 | ||
|
|
@@ -47,18 +52,47 @@ jobs: | |
| secrets: ${{ toJSON(secrets) }} | ||
| github_token: ${{ github.token }} | ||
|
|
||
|
|
||
| - name: Debounce workflow_run fan-out | ||
| id: debounce | ||
| if: github.event_name == 'workflow_run' | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| DEBOUNCE_MINUTES: '30' | ||
| run: | | ||
| set -euo pipefail | ||
| latest_created="$( | ||
| gh api "repos/${GITHUB_REPOSITORY}/actions/workflows/health-68-consumer-sync-drift.yml/runs?per_page=20" \ | ||
| --jq '[.workflow_runs[] | select(.id != '"${GITHUB_RUN_ID}"') | select(.conclusion == "success" or .conclusion == "failure" or .conclusion == "cancelled" or .conclusion == "timed_out")][0].created_at // empty']' | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When two Useful? React with 👍 / 👎. |
||
| ) | ||
| if [ -z "$latest_created" ]; then | ||
| echo "No prior executable Health 68 run; continuing." | ||
| echo "skip=false" >> "$GITHUB_OUTPUT" | ||
| exit 0 | ||
| fi | ||
| latest_epoch="$(date -d "$latest_created" +%s)" | ||
| now_epoch="$(date -u +%s)" | ||
| age_minutes=$(( (now_epoch - latest_epoch) / 60 )) | ||
| echo "Last executable Health 68 run was ${age_minutes} minutes ago at ${latest_created}." | ||
| if [ "$age_minutes" -lt "$DEBOUNCE_MINUTES" ]; then | ||
| echo "Skipping debounced workflow_run fan-out (< ${DEBOUNCE_MINUTES} minutes)." | ||
| echo "skip=true" >> "$GITHUB_OUTPUT" | ||
| exit 0 | ||
| fi | ||
| echo "skip=false" >> "$GITHUB_OUTPUT" | ||
|
|
||
| - name: Setup Python | ||
| if: steps.debounce.outputs.skip != 'true' | ||
| uses: actions/setup-python@v7 | ||
| with: | ||
| python-version: '3.14' | ||
|
|
||
| - name: Install dependencies | ||
| if: steps.debounce.outputs.skip != 'true' | ||
| run: pip install pyyaml requests | ||
|
|
||
| - name: Compare consumer repos to templates | ||
| id: compare | ||
| if: steps.debounce.outputs.skip != 'true' | ||
| env: | ||
| CONSUMER_SYNC_DRIFT_REPORT_JSON: artifacts/consumer-sync-drift-report.json | ||
| INPUT_REPOS: ${{ inputs.repos }} | ||
|
|
@@ -74,16 +108,39 @@ jobs: | |
| --report-json "$CONSUMER_SYNC_DRIFT_REPORT_JSON" | ||
|
|
||
| - name: Upload drift report | ||
| if: ${{ always() }} | ||
| if: ${{ always() && steps.debounce.outputs.skip != 'true' }} | ||
| uses: actions/upload-artifact@v7 | ||
| with: | ||
| name: consumer-sync-drift-report | ||
| path: artifacts/consumer-sync-drift-report.json | ||
| if-no-files-found: warn | ||
| retention-days: 14 | ||
|
|
||
| publish-drift: | ||
| name: Publish actionable drift tracker update | ||
| needs: check-drift | ||
| if: ${{ needs.check-drift.result == 'failure' }} | ||
|
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| contents: read | ||
| issues: write | ||
| steps: | ||
| - name: Checkout Workflows repo | ||
| uses: actions/checkout@v7 | ||
|
github-advanced-security[bot] marked this conversation as resolved.
Fixed
github-advanced-security[bot] marked this conversation as resolved.
Fixed
|
||
|
|
||
| - name: Setup API client | ||
| uses: ./.github/actions/setup-api-client | ||
| with: | ||
| secrets: ${{ toJSON(secrets) }} | ||
|
github-advanced-security[bot] marked this conversation as resolved.
Fixed
|
||
| github_token: ${{ github.token }} | ||
|
|
||
| - name: Download drift report | ||
| uses: actions/download-artifact@v6 | ||
|
github-advanced-security[bot] marked this conversation as resolved.
Fixed
|
||
| with: | ||
| name: consumer-sync-drift-report | ||
| path: artifacts | ||
|
|
||
| - name: Create drift issue | ||
| if: ${{ failure() }} | ||
| uses: actions/github-script@v9 | ||
| with: | ||
| script: | | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| # Workflows monitored by Health 71 durable-tracker liveness assertions. | ||
| # Each entry must match a row in docs/ops/DURABLE_TRACKING_ISSUES.md. | ||
| trackers: | ||
| - workflow: agents-weekly-metrics.yml | ||
| issue: 2211 | ||
| max_age_hours: 192 | ||
| - workflow: maint-82-sync-dependency-campaign.yml | ||
| issue: 1836 | ||
| max_age_hours: 1 | ||
| - workflow: health-83-dependency-sync-efficiency.yml | ||
| issue: 2897 | ||
| max_age_hours: 192 | ||
| - workflow: health-68-consumer-sync-drift.yml | ||
| issue: 2210 | ||
| max_age_hours: 48 | ||
| - workflow: maint-69-sync-integration-repo.yml | ||
| issue: 2470 | ||
| max_age_hours: 168 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with 👍 / 👎. |
||
| - workflow: maint-80-langsmith-metrics-dashboard.yml | ||
| issue: 2415 | ||
| max_age_hours: 192 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,175 @@ | ||
| #!/usr/bin/env python3 | ||
| """Assert durable tracker source workflows executed inside their cadence.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import argparse | ||
| import json | ||
| import os | ||
| import subprocess | ||
| import sys | ||
| from datetime import UTC, datetime | ||
| from pathlib import Path | ||
| from typing import Any | ||
|
|
||
| import yaml | ||
|
|
||
| REPO_ROOT = Path(__file__).resolve().parents[1] | ||
| CONFIG_PATH = REPO_ROOT / "config" / "durable_tracker_liveness.yml" | ||
| TRACKER_DOC = REPO_ROOT / "docs" / "ops" / "DURABLE_TRACKING_ISSUES.md" | ||
| EXECUTABLE_CONCLUSIONS = frozenset({"success", "failure", "cancelled", "timed_out"}) | ||
|
|
||
|
|
||
| def _github_token() -> str: | ||
| token = os.environ.get("GH_TOKEN") or os.environ.get("GITHUB_TOKEN") | ||
| if token: | ||
| return token | ||
| return subprocess.check_output(["gh", "auth", "token"], text=True).strip() | ||
|
|
||
|
|
||
| def _load_config() -> list[dict[str, Any]]: | ||
| data = yaml.safe_load(CONFIG_PATH.read_text(encoding="utf-8")) | ||
| trackers = data.get("trackers") | ||
| if not isinstance(trackers, list) or not trackers: | ||
| raise ValueError(f"{CONFIG_PATH} must define a non-empty trackers list") | ||
| return trackers | ||
|
|
||
|
|
||
| def tracker_doc_workflows() -> set[str]: | ||
| import re | ||
|
|
||
| text = TRACKER_DOC.read_text(encoding="utf-8") | ||
| section_match = re.search( | ||
| r"## Current durable trackers\n\n\| Issue.*?\n\|[-| ]+\n(.*?)(?:\n\n|\n### )", | ||
| text, | ||
| re.DOTALL, | ||
| ) | ||
| assert section_match, "DURABLE_TRACKING_ISSUES.md is missing the tracker table" | ||
| table_body = section_match.group(1) | ||
| workflows = re.findall(r"\[`[^`]+`\]\([^)]*/([^/)]+)\)", table_body) | ||
| return {name for name in workflows if name.endswith(".yml")} | ||
|
|
||
|
|
||
| def _latest_executable_run(repo: str, workflow_file: str, token: str) -> dict[str, Any] | None: | ||
| raw = subprocess.check_output( | ||
| [ | ||
| "gh", | ||
| "api", | ||
| f"repos/{repo}/actions/workflows/{workflow_file}/runs", | ||
| "-f", | ||
| "per_page=100", | ||
|
Comment on lines
+61
to
+62
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
In Health 71's new tracker check, passing Useful? React with 👍 / 👎.
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Verified on exact head 251c6bb: |
||
| "--jq", | ||
| ".workflow_runs", | ||
| ], | ||
| text=True, | ||
| env={**os.environ, "GH_TOKEN": token}, | ||
| ) | ||
|
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
| runs = json.loads(raw) | ||
| if not isinstance(runs, list): | ||
| return None | ||
| for run in runs: | ||
| if not isinstance(run, dict): | ||
| continue | ||
| conclusion = str(run.get("conclusion") or "") | ||
| if conclusion in EXECUTABLE_CONCLUSIONS: | ||
| return run | ||
| return None | ||
|
|
||
|
|
||
| def _hours_since(iso_timestamp: str) -> float: | ||
| created = datetime.fromisoformat(iso_timestamp.replace("Z", "+00:00")) | ||
| return (datetime.now(UTC) - created).total_seconds() / 3600.0 | ||
|
|
||
|
|
||
| def evaluate_trackers(repo: str, token: str | None = None) -> list[dict[str, Any]]: | ||
| auth = token or _github_token() | ||
| results: list[dict[str, Any]] = [] | ||
| for entry in _load_config(): | ||
| workflow = str(entry["workflow"]) | ||
| issue = int(entry["issue"]) | ||
| max_age_hours = float(entry["max_age_hours"]) | ||
| latest = _latest_executable_run(repo, workflow, auth) | ||
| if latest is None: | ||
| results.append( | ||
| { | ||
| "workflow": workflow, | ||
| "issue": issue, | ||
| "healthy": False, | ||
| "reason": "no executable run found (only action_required/skipped)", | ||
| } | ||
| ) | ||
| continue | ||
| hours = _hours_since(str(latest["created_at"])) | ||
| healthy = hours <= max_age_hours | ||
| results.append( | ||
| { | ||
| "workflow": workflow, | ||
| "issue": issue, | ||
| "healthy": healthy, | ||
| "latest_conclusion": latest.get("conclusion"), | ||
| "latest_created_at": latest.get("created_at"), | ||
| "hours_since": round(hours, 2), | ||
| "max_age_hours": max_age_hours, | ||
| "run_url": latest.get("html_url"), | ||
| } | ||
| ) | ||
| return results | ||
|
|
||
|
|
||
| def _comment_on_tracker(repo: str, issue: int, body: str, token: str) -> None: | ||
| subprocess.run( | ||
| ["gh", "issue", "comment", str(issue), "--repo", repo, "--body", body], | ||
| check=True, | ||
| env={**os.environ, "GH_TOKEN": token}, | ||
| ) | ||
|
|
||
|
|
||
| def main(argv: list[str] | None = None) -> int: | ||
| parser = argparse.ArgumentParser() | ||
| parser.add_argument("--repo", default=os.environ.get("GITHUB_REPOSITORY", "stranske/Workflows")) | ||
| parser.add_argument("--comment-on-failure", action="store_true") | ||
| parser.add_argument("--json", action="store_true") | ||
| args = parser.parse_args(argv) | ||
|
|
||
| configured = {str(entry["workflow"]) for entry in _load_config()} | ||
| documented = tracker_doc_workflows() | ||
| missing_from_config = sorted(documented - configured) | ||
| extra_in_config = sorted(configured - documented) | ||
| if missing_from_config or extra_in_config: | ||
| print( | ||
| "Tracker workflow coverage mismatch: " | ||
| f"missing_from_config={missing_from_config} extra_in_config={extra_in_config}", | ||
| file=sys.stderr, | ||
| ) | ||
| return 2 | ||
|
|
||
| token = _github_token() | ||
| results = evaluate_trackers(args.repo, token) | ||
| unhealthy = [item for item in results if not item.get("healthy")] | ||
|
|
||
| if args.json: | ||
| print(json.dumps({"results": results, "unhealthy_count": len(unhealthy)}, indent=2)) | ||
| else: | ||
| for item in results: | ||
| status = "healthy" if item.get("healthy") else "UNHEALTHY" | ||
| print(f"{item['workflow']}\t{status}\t{item}") | ||
|
|
||
| if args.comment_on_failure and unhealthy: | ||
| for item in unhealthy: | ||
| body = ( | ||
| "## Durable tracker liveness alert\n\n" | ||
| f"Source workflow `{item['workflow']}` has no executable run inside its " | ||
| f"{item.get('max_age_hours', '?')}h cadence.\n\n" | ||
| f"- Latest executable run: {item.get('latest_created_at', 'none')}\n" | ||
| f"- Conclusion: {item.get('latest_conclusion', 'n/a')}\n" | ||
| f"- Hours since: {item.get('hours_since', 'n/a')}\n" | ||
| f"- Run URL: {item.get('run_url', 'n/a')}\n\n" | ||
| "Confirm liveness from workflow run history, not tracker comment activity." | ||
| ) | ||
| _comment_on_tracker(args.repo, int(item["issue"]), body, token) | ||
|
|
||
| return 1 if unhealthy else 0 | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| raise SystemExit(main()) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For every qualifying
workflow_runevent, Bash parses this command before invokingghand fails withunexpected EOF while looking for matching \'`because the jq expression ends withempty']'. Consequently the new debounce step fails, the comparison never runs, and this trigger cannot perform the drift check; keep the closing]` inside the quoted jq expression.Useful? React with 👍 / 👎.