Skip to content

Commit 0dbbca9

Browse files
chore: publish generated demo
Source-Commit: 61c5fbf1ec1c928cb0eeb0b47bea3ca7882a6c82
0 parents  commit 0dbbca9

49 files changed

Lines changed: 2795 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
name: Reponomics Dashboard
2+
description: Local wrapper for the generated template's Reponomics Dashboard Action.
3+
4+
inputs:
5+
mode:
6+
description: Runtime mode.
7+
required: true
8+
collection-token:
9+
description: Value of the API token used for repository data collection.
10+
required: false
11+
default: ""
12+
use-github-app:
13+
description: Set true when collection-token is a GitHub App installation token minted in the workflow.
14+
required: false
15+
default: ""
16+
github-token:
17+
description: Token used for GitHub artifact and repository workflow operations.
18+
required: false
19+
default: ""
20+
dashboard-secret:
21+
description: Current dashboard or artifact encryption secret.
22+
required: false
23+
default: ""
24+
dashboard-next-secret:
25+
description: Next dashboard or artifact encryption secret for key rotation or incident reset.
26+
required: false
27+
default: ""
28+
comparison-secret:
29+
description: Optional second dashboard secret for doctor mode.
30+
required: false
31+
default: ""
32+
incident-confirm-mode:
33+
description: Destructive incident-reset confirmation.
34+
required: false
35+
default: ""
36+
incident-confirm-purge:
37+
description: Destructive incident-reset confirmation for old history purge.
38+
required: false
39+
default: ""
40+
incident-confirm-next-secret:
41+
description: Destructive incident-reset confirmation for the replacement dashboard secret.
42+
required: false
43+
default: ""
44+
incident-confirm-irreversible:
45+
description: Destructive incident-reset irreversible-action confirmation.
46+
required: false
47+
default: ""
48+
data-mode:
49+
description: Data storage mode.
50+
required: false
51+
default: ""
52+
retention-days:
53+
description: GitHub Actions artifact retention period.
54+
required: false
55+
default: ""
56+
publish-pages:
57+
description: Set false to render dashboard output without deploying GitHub Pages.
58+
required: false
59+
default: ""
60+
artifact-run-id:
61+
description: Optional workflow run ID whose dashboard-data artifact should be restored.
62+
required: false
63+
default: ""
64+
generate-readme:
65+
description: Generate private-repository README dashboard output.
66+
required: false
67+
default: ""
68+
69+
runs:
70+
using: composite
71+
steps:
72+
- name: Run Reponomics Dashboard Action
73+
id: reponomics
74+
uses: reponomics/reponomics-dashboard-action@v0
75+
with:
76+
mode: ${{ inputs.mode }}
77+
collection-token: ${{ inputs.collection-token }}
78+
use-github-app: ${{ inputs.use-github-app }}
79+
github-token: ${{ inputs.github-token }}
80+
dashboard-secret: ${{ inputs.dashboard-secret }}
81+
dashboard-next-secret: ${{ inputs.dashboard-next-secret }}
82+
comparison-secret: ${{ inputs.comparison-secret }}
83+
incident-confirm-mode: ${{ inputs.incident-confirm-mode }}
84+
incident-confirm-purge: ${{ inputs.incident-confirm-purge }}
85+
incident-confirm-next-secret: ${{ inputs.incident-confirm-next-secret }}
86+
incident-confirm-irreversible: ${{ inputs.incident-confirm-irreversible }}
87+
data-mode: ${{ inputs.data-mode }}
88+
retention-days: ${{ inputs.retention-days }}
89+
publish-pages: ${{ inputs.publish-pages }}
90+
artifact-run-id: ${{ inputs.artifact-run-id }}
91+
generate-readme: ${{ inputs.generate-readme }}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
#!/usr/bin/env python3
2+
"""Resolve and update generated-repository auto-doctor cadence state."""
3+
4+
from __future__ import annotations
5+
6+
import argparse
7+
import json
8+
import os
9+
import sys
10+
from datetime import datetime, timezone
11+
from pathlib import Path
12+
from typing import Any
13+
14+
15+
DEFAULT_MARKER = Path(".reponomics") / "auto-doctor.json"
16+
MIN_INTERVAL_DAYS = 0
17+
MAX_INTERVAL_DAYS = 30
18+
19+
20+
def _utc_now() -> datetime:
21+
return datetime.now(timezone.utc).replace(microsecond=0)
22+
23+
24+
def _parse_interval(raw: str) -> int:
25+
try:
26+
value = int(raw)
27+
except ValueError as exc:
28+
raise ValueError(f"auto_doctor_every_n_days must be an integer, got {raw!r}.") from exc
29+
if value < MIN_INTERVAL_DAYS or value > MAX_INTERVAL_DAYS:
30+
raise ValueError(
31+
"auto_doctor_every_n_days must be between "
32+
+ f"{MIN_INTERVAL_DAYS} and {MAX_INTERVAL_DAYS}."
33+
)
34+
return value
35+
36+
37+
def _parse_timestamp(raw: Any) -> datetime | None:
38+
if not isinstance(raw, str) or not raw.strip():
39+
return None
40+
value = raw.strip()
41+
if value.endswith("Z"):
42+
value = value[:-1] + "+00:00"
43+
try:
44+
parsed = datetime.fromisoformat(value)
45+
except ValueError:
46+
return None
47+
if parsed.tzinfo is None:
48+
parsed = parsed.replace(tzinfo=timezone.utc)
49+
return parsed.astimezone(timezone.utc)
50+
51+
52+
def _load_marker(marker_path: Path) -> dict[str, Any]:
53+
try:
54+
payload = json.loads(marker_path.read_text(encoding="utf-8"))
55+
except (OSError, json.JSONDecodeError):
56+
return {}
57+
return payload if isinstance(payload, dict) else {}
58+
59+
60+
def _write_output(name: str, value: str) -> None:
61+
output_path = os.environ.get("GITHUB_OUTPUT", "").strip()
62+
if output_path:
63+
with Path(output_path).open("a", encoding="utf-8") as output:
64+
output.write(f"{name}={value}\n")
65+
print(f"{name}: {value}")
66+
67+
68+
def _check(interval_days: int, marker_path: Path, now: datetime) -> int:
69+
if interval_days == 0:
70+
due = False
71+
reason = "disabled"
72+
elif not marker_path.exists():
73+
due = True
74+
reason = "no marker found"
75+
else:
76+
marker = _load_marker(marker_path)
77+
last_success = _parse_timestamp(marker.get("last_successful_at"))
78+
if last_success is None:
79+
due = True
80+
reason = "marker was unreadable"
81+
else:
82+
elapsed_days = (now.date() - last_success.date()).days
83+
due = elapsed_days >= interval_days
84+
reason = f"{elapsed_days} day(s) since last successful auto-doctor"
85+
86+
_write_output("due", str(due).lower())
87+
_write_output("reason", reason)
88+
_write_output("checked-at", now.isoformat().replace("+00:00", "Z"))
89+
return 0
90+
91+
92+
def _mark(interval_days: int, marker_path: Path, run_id: str, now: datetime) -> int:
93+
marker_path.parent.mkdir(parents=True, exist_ok=True)
94+
payload = {
95+
"schema_version": 1,
96+
"last_successful_at": now.isoformat().replace("+00:00", "Z"),
97+
"last_successful_run_id": run_id,
98+
"auto_doctor_every_n_days": interval_days,
99+
}
100+
marker_path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
101+
_write_output("marker-path", marker_path.as_posix())
102+
return 0
103+
104+
105+
def main() -> int:
106+
parser = argparse.ArgumentParser()
107+
parser.add_argument("--interval-days", required=True)
108+
parser.add_argument("--marker", default=DEFAULT_MARKER.as_posix())
109+
parser.add_argument("--run-id", default=os.environ.get("GITHUB_RUN_ID", ""))
110+
parser.add_argument("--now", default="")
111+
parser.add_argument("command", choices={"check", "mark"})
112+
args = parser.parse_args()
113+
114+
try:
115+
interval_days = _parse_interval(args.interval_days)
116+
except ValueError as exc:
117+
print(str(exc), file=sys.stderr)
118+
return 1
119+
120+
now = _parse_timestamp(args.now) if args.now else _utc_now()
121+
if now is None:
122+
print(f"--now must be an ISO timestamp, got {args.now!r}.", file=sys.stderr)
123+
return 1
124+
125+
marker_path = Path(args.marker)
126+
if args.command == "check":
127+
return _check(interval_days, marker_path, now)
128+
129+
if not args.run_id.strip():
130+
print("--run-id or GITHUB_RUN_ID is required for mark.", file=sys.stderr)
131+
return 1
132+
return _mark(interval_days, marker_path, args.run_id.strip(), now)
133+
134+
135+
if __name__ == "__main__":
136+
raise SystemExit(main())

0 commit comments

Comments
 (0)