mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-28 19:24:58 +08:00
ci: stop daily freshness false alarms for dormant scheduled workflows (#6608)
This commit is contained in:
@@ -7,8 +7,11 @@
|
||||
{ "workflow": ".github/workflows/e2e-s3tests.yml", "max_age_hours": 192 },
|
||||
{ "workflow": ".github/workflows/fuzz.yml", "max_age_hours": 36 },
|
||||
{ "workflow": ".github/workflows/mint.yml", "max_age_hours": 192 },
|
||||
{ "workflow": ".github/workflows/minio-interop.yml", "max_age_hours": 36 },
|
||||
{ "workflow": ".github/workflows/nightly-gnu.yml", "max_age_hours": 36 },
|
||||
{ "workflow": ".github/workflows/performance-ab.yml", "max_age_hours": 36 },
|
||||
{ "workflow": ".github/workflows/runner-hygiene.yml", "max_age_hours": 792 }
|
||||
{
|
||||
"workflow": ".github/workflows/runner-hygiene.yml",
|
||||
"max_age_hours": 792,
|
||||
"never_ran_grace_until": "2026-09-02T06:37:00Z"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -37,6 +37,11 @@
|
||||
# banner. Re-enabling is a UI action; anyone doing so should first check that the
|
||||
# workflow still matches the current CI layout. See rustfs/backlog#1603.
|
||||
#
|
||||
# While disabled, this workflow is deliberately absent from
|
||||
# .github/scheduled-validations.json — a disabled workflow can never satisfy the
|
||||
# freshness check. Whoever re-enables it must re-add the entry in the same
|
||||
# change so the freshness gate covers it again.
|
||||
#
|
||||
name: minio-interop
|
||||
|
||||
on:
|
||||
|
||||
@@ -85,12 +85,12 @@ schedules.
|
||||
| Cadence (UTC unless noted) | Workflow / validation | Budget | Verdict and artifacts | Reproduction |
|
||||
|---|---|---:|---|---|
|
||||
| Daily 02:17 | Fuzz: five nightly corpus targets | 60 min build; 60 min per target | Gate; corpus/crash artifacts, scheduled failure alert | `MAX_TOTAL_TIME=<seconds> ./scripts/fuzz/run.sh` |
|
||||
| Daily 03:17 | MinIO interop (EC + SSE read parity) | 40 min | Gate; scheduled failure alert | Dispatch `minio-interop.yml` or follow its pinned Docker fixture steps |
|
||||
| Dormant (cron 03:17 once re-enabled) | MinIO interop (EC + SSE read parity) | 40 min | Manually disabled in the Actions settings (backlog#1603) and therefore outside the freshness list; re-add it to `.github/scheduled-validations.json` when re-enabling | Follow the pinned Docker fixture steps in `minio-interop.yml` |
|
||||
| Daily 04:29 | Replication / cluster-fault / protocol e2e | 45 / 90 / 90 min | Three independent gates; JUnit, membership, and server logs | `cargo nextest run --profile e2e-repl-nightly -p e2e_test`; `--profile e2e-nightly`; `-j 1 --profile e2e-protocols` |
|
||||
| Daily 06:31 | Warp performance A/B | 180 min | Regression budget gate; A/B summaries and server logs | `bash scripts/run_hotpath_warp_abba.sh --help` |
|
||||
| Daily 00:07 Asia/Shanghai (16:07 UTC previous day) | Nightly GNU build and Vault lanes | 150 / 90 / 60 min | Build, live Vault, and HA failover gates | Use the commands and pinned Vault images in `nightly-gnu.yml` |
|
||||
| Daily 03:23 | Security Audit | 20 / 5 min, plus 30 min on PR dependency review | Cargo Deny and workflow-pin gates; scheduled failure alert | `cargo deny check`; `scripts/security/check_workflow_pins.sh` |
|
||||
| Daily 23:47 | Scheduled Validation Freshness | 10 min | Fails when a critical schedule was never created or is stale | Dispatch `scheduled-validation-freshness.yml` |
|
||||
| Daily 23:47 | Scheduled Validation Freshness | 10 min | Fails when a critical schedule was never created or is stale; an entry may carry `never_ran_grace_until` to cover the window before a newly enabled cron's first slot | Dispatch `scheduled-validation-freshness.yml` |
|
||||
| Sunday 00:11 | Full `Continuous Integration` matrix | Per-job budgets above | Weekly variant coverage, including dormant rio-v2 binary/e2e lanes | Dispatch `ci.yml` |
|
||||
| Sunday 01:13 | Seven-platform build matrix | 150 min per platform | Build/package integrity; scheduled failure alert | Dispatch `build.yml` with an exact platform set |
|
||||
| Sunday 02:19 | Ceph s3-tests full sweep: single and real four-node, four shards each | 180 min per shard | Compatibility gate; report, JUnit, exact node IDs, and server logs | `scripts/s3-tests/run.sh` against an existing single or distributed target |
|
||||
|
||||
@@ -20,12 +20,12 @@ from urllib.request import Request, urlopen
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def load_validations(path: Path) -> list[tuple[str, int]]:
|
||||
def load_validations(path: Path) -> list[tuple[str, int, datetime | None]]:
|
||||
data = json.loads(path.read_text())
|
||||
if not isinstance(data, list) or not data:
|
||||
raise ValueError("scheduled validation config must be a non-empty list")
|
||||
|
||||
validations: list[tuple[str, int]] = []
|
||||
validations: list[tuple[str, int, datetime | None]] = []
|
||||
seen: set[str] = set()
|
||||
for item in data:
|
||||
if not isinstance(item, dict):
|
||||
@@ -44,8 +44,21 @@ def load_validations(path: Path) -> list[tuple[str, int]]:
|
||||
or max_age_hours <= 0
|
||||
):
|
||||
raise ValueError(f"invalid max_age_hours for {workflow}: {max_age_hours!r}")
|
||||
grace_raw = item.get("never_ran_grace_until")
|
||||
grace: datetime | None = None
|
||||
if grace_raw is not None:
|
||||
if not isinstance(grace_raw, str):
|
||||
raise ValueError(
|
||||
f"invalid never_ran_grace_until for {workflow}: {grace_raw!r}"
|
||||
)
|
||||
try:
|
||||
grace = parse_timestamp(grace_raw)
|
||||
except ValueError as error:
|
||||
raise ValueError(
|
||||
f"invalid never_ran_grace_until for {workflow}: {error}"
|
||||
) from error
|
||||
seen.add(workflow)
|
||||
validations.append((workflow, max_age_hours))
|
||||
validations.append((workflow, max_age_hours, grace))
|
||||
return validations
|
||||
|
||||
|
||||
@@ -59,9 +72,18 @@ def parse_timestamp(value: object) -> datetime:
|
||||
|
||||
|
||||
def stale_reason(
|
||||
run: dict[str, object] | None, now: datetime, max_age_hours: int
|
||||
run: dict[str, object] | None,
|
||||
now: datetime,
|
||||
max_age_hours: int,
|
||||
never_ran_grace_until: datetime | None = None,
|
||||
) -> str | None:
|
||||
if run is None:
|
||||
# The grace deadline only covers a workflow whose first scheduled slot
|
||||
# has not arrived yet (for example a monthly cron enabled mid-month).
|
||||
# A recorded-but-old run proves the schedule used to fire and stopped,
|
||||
# so the grace never masks that case.
|
||||
if never_ran_grace_until is not None and now <= never_ran_grace_until:
|
||||
return None
|
||||
return "no scheduled run has been recorded"
|
||||
created_at = parse_timestamp(run.get("created_at"))
|
||||
age = now - created_at
|
||||
@@ -126,10 +148,10 @@ def check_freshness(
|
||||
) -> int:
|
||||
now = datetime.now(timezone.utc)
|
||||
failures: list[tuple[str, int, str, str]] = []
|
||||
for workflow, max_age_hours in load_validations(config):
|
||||
for workflow, max_age_hours, never_ran_grace_until in load_validations(config):
|
||||
try:
|
||||
run = fetch_latest_scheduled_run(repository, workflow, token, api_url)
|
||||
reason = stale_reason(run, now, max_age_hours)
|
||||
reason = stale_reason(run, now, max_age_hours, never_ran_grace_until)
|
||||
if reason is not None:
|
||||
run_url = str(run.get("html_url", "")) if run else ""
|
||||
failures.append((workflow, max_age_hours, reason, run_url))
|
||||
@@ -151,6 +173,15 @@ class SelfTests(unittest.TestCase):
|
||||
self.assertIsNotNone(stale_reason(past_limit, self.NOW, 36))
|
||||
self.assertIsNotNone(stale_reason(None, self.NOW, 36))
|
||||
|
||||
def test_never_ran_grace_only_covers_missing_runs(self) -> None:
|
||||
future_grace = self.NOW + timedelta(hours=1)
|
||||
past_grace = self.NOW - timedelta(seconds=1)
|
||||
self.assertIsNone(stale_reason(None, self.NOW, 36, future_grace))
|
||||
self.assertIsNone(stale_reason(None, self.NOW, 36, self.NOW))
|
||||
self.assertIsNotNone(stale_reason(None, self.NOW, 36, past_grace))
|
||||
stale_run = {"created_at": "2026-08-20T23:59:59Z"}
|
||||
self.assertIsNotNone(stale_reason(stale_run, self.NOW, 36, future_grace))
|
||||
|
||||
def test_config_rejects_duplicate_and_invalid_entries(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = Path(tmp) / "validations.json"
|
||||
@@ -171,6 +202,41 @@ class SelfTests(unittest.TestCase):
|
||||
)
|
||||
with self.assertRaises(ValueError):
|
||||
load_validations(path)
|
||||
for bad_grace in (36, "not-a-timestamp", "2026-09-01T00:00:00"):
|
||||
path.write_text(
|
||||
json.dumps(
|
||||
[
|
||||
{
|
||||
"workflow": ".github/workflows/ci.yml",
|
||||
"max_age_hours": 36,
|
||||
"never_ran_grace_until": bad_grace,
|
||||
}
|
||||
]
|
||||
)
|
||||
)
|
||||
with self.assertRaises(ValueError):
|
||||
load_validations(path)
|
||||
path.write_text(
|
||||
json.dumps(
|
||||
[
|
||||
{
|
||||
"workflow": ".github/workflows/ci.yml",
|
||||
"max_age_hours": 36,
|
||||
"never_ran_grace_until": "2026-09-02T06:37:00Z",
|
||||
}
|
||||
]
|
||||
)
|
||||
)
|
||||
self.assertEqual(
|
||||
load_validations(path),
|
||||
[
|
||||
(
|
||||
".github/workflows/ci.yml",
|
||||
36,
|
||||
datetime(2026, 9, 2, 6, 37, tzinfo=timezone.utc),
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
def test_check_reports_missing_runs(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
|
||||
Reference in New Issue
Block a user