Compare commits
6 Commits
8b1ae4ac60
...
83fbf1f992
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83fbf1f992 | ||
|
|
9b5334fee7 | ||
|
|
f71a5aab39 | ||
|
|
f219437282 | ||
|
|
b15584d33a | ||
|
|
0a57d96f2e |
@@ -83,8 +83,17 @@ def test_warn_mode_never_turns_red(monkeypatch):
|
|||||||
assert state == "success" and desc.startswith("⚠ WARN (not blocking): 1 CI hygiene issue in CI/Dockerfiles")
|
assert state == "success" and desc.startswith("⚠ WARN (not blocking): 1 CI hygiene issue in CI/Dockerfiles")
|
||||||
|
|
||||||
|
|
||||||
def test_allow_file_loads_and_is_empty_today():
|
def test_allow_file_is_line_scoped_exceptions_only():
|
||||||
assert hy.cg.load_allow(hy.ALLOW_FILE) == []
|
"""Every exception is line-scoped (`matches`), so an allowed file can't hide a
|
||||||
|
NEW floating install or docker step. Today: windy-pro's if:false deploy job."""
|
||||||
|
allow = hy.cg.load_allow(hy.ALLOW_FILE)
|
||||||
|
assert [(e["repo"], e["paths"]) for e in allow] == [("windy-pro", [".github/workflows/ci.yml"])]
|
||||||
|
assert all(e.get("matches") for e in allow)
|
||||||
|
ok = " run: docker build -f account-server/Dockerfile -t windy-pro:${{ github.sha }} ."
|
||||||
|
new = " run: docker build -t windy-pro-api ."
|
||||||
|
assert hy.cg.allowed("windy-pro", WF, allow, ok)
|
||||||
|
assert not hy.cg.allowed("windy-pro", WF, allow, new)
|
||||||
|
assert not hy.cg.allowed("windy-chat", WF, allow, ok)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("path, text, want", [
|
@pytest.mark.parametrize("path, text, want", [
|
||||||
@@ -116,3 +125,40 @@ def test_optional_lock_globs_are_flagged(text):
|
|||||||
])
|
])
|
||||||
def test_pinned_images_and_real_locks_pass(path, text):
|
def test_pinned_images_and_real_locks_pass(path, text):
|
||||||
assert hy.scan_line(path, text) == []
|
assert hy.scan_line(path, text) == []
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("text", [
|
||||||
|
" - run: docker compose -f docker-compose.yml -f docker-compose.ci.yml build", # eternitas ci/build
|
||||||
|
" run: docker build -t windy-mail .",
|
||||||
|
" - run: docker-compose up -d",
|
||||||
|
" run: docker buildx build --load .",
|
||||||
|
" - uses: docker/build-push-action@v6",
|
||||||
|
])
|
||||||
|
def test_docker_in_ci_is_flagged_with_the_fix(text):
|
||||||
|
hits = hy.scan_line(WF, text)
|
||||||
|
assert [k for k, _ in hits] == ["needs docker"]
|
||||||
|
assert "no-Docker smoke test" in hits[0][1]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("path, text", [
|
||||||
|
("Dockerfile", "RUN docker build ."), # not a workflow
|
||||||
|
(WF, " run: ssh host 'docker compose up -d'"), # remote host has a daemon
|
||||||
|
(WF, " # docker compose build"), # comment
|
||||||
|
(WF, " run: echo docker build"),
|
||||||
|
])
|
||||||
|
def test_docker_not_flagged_outside_ci_steps(path, text):
|
||||||
|
assert [k for k, _ in hy.scan_line(path, text) if k == "needs docker"] == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_needs_docker_skips_workflows_disabled_on_windy_git(monkeypatch):
|
||||||
|
"""deploy.yml runs on the target host (a real daemon); Gitea has it disabled here."""
|
||||||
|
F = hy.cg.Finding
|
||||||
|
monkeypatch.setattr(hy, "_DISABLED", {"eternitas": {"deploy.yml"}})
|
||||||
|
got = hy._runs_here("Eternitas", [
|
||||||
|
F(".github/workflows/deploy.yml", 70, "needs docker", "docker compose in CI"),
|
||||||
|
F(".github/workflows/ci.yml", 176, "needs docker", "docker compose in CI"),
|
||||||
|
F(".github/workflows/deploy.yml", 12, "floating install", "npm install"),
|
||||||
|
])
|
||||||
|
assert [(f.path.rsplit("/", 1)[1], f.kind) for f in got] == [
|
||||||
|
("ci.yml", "needs docker"), ("deploy.yml", "floating install")]
|
||||||
|
assert hy._runs_here("eternitas", None) is None
|
||||||
|
|||||||
@@ -57,7 +57,8 @@ def test_render_splits_lane_and_grant_counts():
|
|||||||
md = gr.render(res)
|
md = gr.render(res)
|
||||||
assert "| ci-hygiene (house rule 6) | 1 | 1 | ❌ not yet |" in md
|
assert "| ci-hygiene (house rule 6) | 1 | 1 | ❌ not yet |" in md
|
||||||
assert "| compute-guard (Mind is the only door) | 0 | 0 | ✅ YES |" in md
|
assert "| compute-guard (Mind is the only door) | 0 | 0 | ✅ YES |" in md
|
||||||
assert "| windy-git | bbbbbbb | 0 | 0 | clean ✅ |" in md
|
assert "| windy-git | Windy Git | bbbbbbb | 0 | 0 | clean ✅ |" in md
|
||||||
|
assert "| windy-pro | Windy Hub | aaaaaaa |" in md # owner = session to message
|
||||||
assert "(job reality-check)" in md
|
assert "(job reality-check)" in md
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -411,3 +411,19 @@ def test_unchanged_base_leaves_the_mirror_alone(fake):
|
|||||||
f = fake(gh_prs=gh, wg_prs=[{"number": 3, "title": "[GH#5] t", "base": {"ref": "main"}}])
|
f = fake(gh_prs=gh, wg_prs=[{"number": 3, "title": "[GH#5] t", "base": {"ref": "main"}}])
|
||||||
bridge.sync_prs("windy-chat")
|
bridge.sync_prs("windy-chat")
|
||||||
assert f.closed == [] and f.opened == []
|
assert f.closed == [] and f.opened == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_named_no_daemon_job_is_not_posted_for_that_repo_only(fake, monkeypatch):
|
||||||
|
"""eternitas ci/build needs Docker but its name doesn't say so (option A, 09-23)."""
|
||||||
|
monkeypatch.setattr(bridge, "NO_DAEMON_NAMED", {"eternitas": {"ci/build"}})
|
||||||
|
runs = [_run(1, "ci.yml", "build", "failure"), _run(2, "ci.yml", "test", "success")]
|
||||||
|
f = fake(runs=runs)
|
||||||
|
bridge.post_statuses("eternitas", SHA)
|
||||||
|
assert [p["context"] for p in f.posted] == ["windy-git/ci/test"]
|
||||||
|
f2 = fake(runs=runs)
|
||||||
|
bridge.post_statuses("windy-chat", SHA)
|
||||||
|
assert sorted(p["context"] for p in f2.posted) == ["windy-git/ci/build", "windy-git/ci/test"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_default_no_daemon_named_is_eternitas_build():
|
||||||
|
assert bridge.NO_DAEMON_NAMED.get("eternitas") == {"ci/build"}
|
||||||
|
|||||||
@@ -2,4 +2,12 @@
|
|||||||
# a host port. House rule 6 (09-23): installs come from a lockfile. Every entry
|
# a host port. House rule 6 (09-23): installs come from a lockfile. Every entry
|
||||||
# is an exception and MUST say why. Paths are fnmatch globs from the repo root.
|
# is an exception and MUST say why. Paths are fnmatch globs from the repo root.
|
||||||
# Owner: Windy Git lane (13); changes go through the orchestrator.
|
# Owner: Windy Git lane (13); changes go through the orchestrator.
|
||||||
allow: []
|
allow:
|
||||||
|
- repo: windy-pro
|
||||||
|
paths: [".github/workflows/ci.yml"]
|
||||||
|
# ONLY the old deploy job's two docker lines. That job is `if: false`
|
||||||
|
# (CD boundary, 2026-07), and the compose line runs ON windyword.ai inside
|
||||||
|
# the ssh string. Any other docker step in ci.yml still flags.
|
||||||
|
matches: ['docker build -f account-server/Dockerfile -t windy-pro:', 'docker compose down && docker compose up -d --build']
|
||||||
|
reason: "needs-docker false positive: the deploy job is if: false and its compose runs on the remote host over ssh. Added with the needs-docker rule (orchestrator option A, 09-23)."
|
||||||
|
|
||||||
|
|||||||
@@ -110,7 +110,12 @@ their CI permanently, not a stopgap:
|
|||||||
- **Image-build jobs** (name matches `docker`) post nothing: job containers
|
- **Image-build jobs** (name matches `docker`) post nothing: job containers
|
||||||
have no Docker daemon by design (I-5), so they are red on every commit. A
|
have no Docker daemon by design (I-5), so they are red on every commit. A
|
||||||
rootless builder (BuildKit rootless / buildx in the capped dind) is the open
|
rootless builder (BuildKit rootless / buildx in the capped dind) is the open
|
||||||
decision that would bring them back.
|
decision that would bring them back. Jobs that need Docker but are named otherwise go in
|
||||||
|
`BRIDGE_NO_DAEMON` (default `eternitas:ci/build`). DECIDED 09-23 (orchestrator,
|
||||||
|
option A): lanes convert these jobs to no-Docker smoke tests (job `services:` +
|
||||||
|
start the app + curl /health); the real image build is the deploy step on the
|
||||||
|
target host. ci-hygiene flags docker build/compose/run in workflows ("needs docker").
|
||||||
|
No host Docker socket for CI without a separate decision.
|
||||||
|
|
||||||
**Onboarding another private repo** — the promotion steps below, then:
|
**Onboarding another private repo** — the promotion steps below, then:
|
||||||
|
|
||||||
|
|||||||
@@ -28,9 +28,11 @@ Exceptions: ci/ci-hygiene-allow.yml, one reason per entry.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import shlex
|
import shlex
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -45,9 +47,11 @@ MODE = os.environ.get("CI_HYGIENE_MODE", "warn")
|
|||||||
INCLUDE = re.compile(r"(^|/)\.(github|gitea)/workflows/[^/]+\.ya?ml$|(^|/)(Dockerfile[^/]*|[^/]+\.Dockerfile)$")
|
INCLUDE = re.compile(r"(^|/)\.(github|gitea)/workflows/[^/]+\.ya?ml$|(^|/)(Dockerfile[^/]*|[^/]+\.Dockerfile)$")
|
||||||
NEVER = re.compile(r"(^|/)(node_modules|vendor|third_party)/")
|
NEVER = re.compile(r"(^|/)(node_modules|vendor|third_party)/")
|
||||||
PREFILTER = (r"pip3? install|pip install|uv sync|npm (install|i )|yarn install|pnpm install"
|
PREFILTER = (r"pip3? install|pip install|uv sync|npm (install|i )|yarn install|pnpm install"
|
||||||
r"|^\s*-\s*['\"]?[0-9]+:[0-9]+|:latest|lock[^ ]*\*")
|
r"|^\s*-\s*['\"]?[0-9]+:[0-9]+|:latest|lock[^ ]*\*"
|
||||||
|
r"|docker[ -]compose|docker (build|buildx|run)|docker/build-push-action")
|
||||||
|
|
||||||
TOOLING = {"pip", "setuptools", "wheel"}
|
TOOLING = {"pip", "setuptools", "wheel"}
|
||||||
|
NO_DOCKER_FIX = "use job services: + a no-Docker smoke test; the image builds at deploy"
|
||||||
DOCKER_FILE = re.compile(r"(^|/)(Dockerfile[^/]*|[^/]+\.Dockerfile)$")
|
DOCKER_FILE = re.compile(r"(^|/)(Dockerfile[^/]*|[^/]+\.Dockerfile)$")
|
||||||
LATEST = re.compile(r"(?:^\s*FROM\s+(?:--platform=\S+\s+)?|--from=|image:\s*['\"]?|docker://)([\w./-]+):latest\b", re.I)
|
LATEST = re.compile(r"(?:^\s*FROM\s+(?:--platform=\S+\s+)?|--from=|image:\s*['\"]?|docker://)([\w./-]+):latest\b", re.I)
|
||||||
LOCKNAME = re.compile(r"(uv\.lock|poetry\.lock|package-lock\.json|pnpm-lock\.yaml|yarn\.lock|requirements[^ ]*\.(txt|lock))", re.I)
|
LOCKNAME = re.compile(r"(uv\.lock|poetry\.lock|package-lock\.json|pnpm-lock\.yaml|yarn\.lock|requirements[^ ]*\.(txt|lock))", re.I)
|
||||||
@@ -121,8 +125,19 @@ def scan_line(path: str, text: str) -> list[tuple[str, str]]:
|
|||||||
globbed = [t for t in text.split() if "*" in t and LOCKNAME.search(t)]
|
globbed = [t for t in text.split() if "*" in t and LOCKNAME.search(t)]
|
||||||
if globbed:
|
if globbed:
|
||||||
hits.append(("optional lock", f"COPY {globbed[0]} (must fail if the lock is missing)"))
|
hits.append(("optional lock", f"COPY {globbed[0]} (must fail if the lock is missing)"))
|
||||||
|
# Windy Git jobs get NO Docker daemon (I-5), so a docker build/compose/run
|
||||||
|
# step in CI can never pass here (orchestrator 09-23, option A). The real
|
||||||
|
# image build is the deploy step on the target host.
|
||||||
|
if "/workflows/" in path and re.search(r"uses:\s*['\"]?docker/build-push-action", text):
|
||||||
|
hits.append(("needs docker", "docker/build-push-action in CI (no Docker daemon on Windy Git; " + NO_DOCKER_FIX + ")"))
|
||||||
for toks in _commands(text):
|
for toks in _commands(text):
|
||||||
low = [t.lower() for t in toks]
|
low = [t.lower() for t in toks]
|
||||||
|
if "/workflows/" in path and (
|
||||||
|
low[:2] in (["docker", "build"], ["docker", "buildx"], ["docker", "run"], ["docker", "compose"])
|
||||||
|
or low[:1] == ["docker-compose"]
|
||||||
|
):
|
||||||
|
hits.append(("needs docker", f"{' '.join(low[:2])} in CI (no Docker daemon on Windy Git; " + NO_DOCKER_FIX + ")"))
|
||||||
|
continue
|
||||||
# pip install / python -m pip install / uv pip install
|
# pip install / python -m pip install / uv pip install
|
||||||
for i in range(len(low) - 1):
|
for i in range(len(low) - 1):
|
||||||
if os.path.basename(low[i]) in ("pip", "pip3") and low[i + 1] == "install":
|
if os.path.basename(low[i]) in ("pip", "pip3") and low[i + 1] == "install":
|
||||||
@@ -143,7 +158,49 @@ def scan_line(path: str, text: str) -> list[tuple[str, str]]:
|
|||||||
return hits
|
return hits
|
||||||
|
|
||||||
|
|
||||||
|
_DISABLED: dict[str, set[str]] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def disabled_workflows() -> dict[str, set[str]]:
|
||||||
|
"""Workflow file names Gitea has DISABLED per repo (lowercased repo name).
|
||||||
|
|
||||||
|
Deploy/release workflows are disabled on Windy Git: they run on the target
|
||||||
|
host, where a Docker daemon really exists, so "needs docker" must not flag
|
||||||
|
them. One bounded query per process; on any failure nothing is excused
|
||||||
|
(flag rather than hide).
|
||||||
|
"""
|
||||||
|
global _DISABLED
|
||||||
|
if _DISABLED is not None:
|
||||||
|
return _DISABLED
|
||||||
|
_DISABLED = {}
|
||||||
|
query = ("select coalesce(json_object_agg(r.lower_name, u.config::json->'DisabledWorkflows'), '{}'::json)"
|
||||||
|
" from repo_unit u join repository r on r.id = u.repo_id"
|
||||||
|
" where u.type = 10 and u.config like '%DisabledWorkflows%';")
|
||||||
|
try:
|
||||||
|
out = subprocess.run(
|
||||||
|
["docker", "exec", "-i", "windy-git-db-1", "sh", "-c",
|
||||||
|
'psql -U "$POSTGRES_USER" -d gitea -At -v ON_ERROR_STOP=1'],
|
||||||
|
input=query, capture_output=True, text=True, check=True, timeout=30,
|
||||||
|
).stdout.strip()
|
||||||
|
_DISABLED = {k: set(v or []) for k, v in json.loads(out or "{}").items()}
|
||||||
|
except (subprocess.SubprocessError, OSError, ValueError):
|
||||||
|
pass
|
||||||
|
return _DISABLED
|
||||||
|
|
||||||
|
|
||||||
|
def _runs_here(repo: str, findings):
|
||||||
|
"""Drop "needs docker" hits in workflows that never run on Windy Git."""
|
||||||
|
if findings is None:
|
||||||
|
return None
|
||||||
|
off = disabled_workflows().get(repo.lower(), set())
|
||||||
|
return [f for f in findings if not (f.kind == "needs docker" and Path(f.path).name in off)]
|
||||||
|
|
||||||
|
|
||||||
def check(repo: str, sha: str, default_branch: str, is_default_head: bool):
|
def check(repo: str, sha: str, default_branch: str, is_default_head: bool):
|
||||||
|
return _runs_here(repo, _check(repo, sha, default_branch, is_default_head))
|
||||||
|
|
||||||
|
|
||||||
|
def _check(repo: str, sha: str, default_branch: str, is_default_head: bool):
|
||||||
bare = cg.WORK / f"{repo}.git"
|
bare = cg.WORK / f"{repo}.git"
|
||||||
if not bare.is_dir() or not cg.fetched(bare, sha): # pushed after the fetch: next cycle
|
if not bare.is_dir() or not cg.fetched(bare, sha): # pushed after the fetch: next cycle
|
||||||
return None
|
return None
|
||||||
@@ -179,7 +236,7 @@ def report(repos: list[str]) -> int:
|
|||||||
continue
|
continue
|
||||||
head = cg._git(bare, "symbolic-ref", "--short", "HEAD").strip()
|
head = cg._git(bare, "symbolic-ref", "--short", "HEAD").strip()
|
||||||
sha = cg._git(bare, "rev-parse", head).strip()
|
sha = cg._git(bare, "rev-parse", head).strip()
|
||||||
fs = cg.scan_tree(repo, bare, sha, allow, line_fn=scan_line, path_ok=path_ok, prefilter=PREFILTER)
|
fs = _runs_here(repo, cg.scan_tree(repo, bare, sha, allow, line_fn=scan_line, path_ok=path_ok, prefilter=PREFILTER))
|
||||||
total += len(fs)
|
total += len(fs)
|
||||||
print(f"## {repo} ({head} {sha[:7]}): {len(fs)} issue(s)")
|
print(f"## {repo} ({head} {sha[:7]}): {len(fs)} issue(s)")
|
||||||
for f in fs:
|
for f in fs:
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ REPOS = os.environ.get("BRIDGE_REPOS", "").split() or [
|
|||||||
# windy-admin/telemetry -> "Windy Admin"). windy-pro here = its server/web side; the desktop
|
# windy-admin/telemetry -> "Windy Admin"). windy-pro here = its server/web side; the desktop
|
||||||
# app is Grant-owned and listed in its own section below.
|
# app is Grant-owned and listed in its own section below.
|
||||||
OWNERS = {
|
OWNERS = {
|
||||||
"windy-chat": "grantwhitmer-ca", "windy-mail": "Windy Mail", "windy-calendar": "Windy Calender",
|
"windy-chat": "Windy Chat", "windy-mail": "Windy Mail", "windy-calendar": "Windy Calender",
|
||||||
"Windy-Clone": "Windy Clone", "WindyCloud": "Windy Cloud", "windy-cloud-sites": "Windy Cloud",
|
"Windy-Clone": "Windy Clone", "WindyCloud": "Windy Cloud", "windy-cloud-sites": "Windy Cloud",
|
||||||
"windy-cloud-domains": "Windy Cloud", "windy-cloud-vps": "Windy Cloud", "windy-search": "Windy Search",
|
"windy-cloud-domains": "Windy Cloud", "windy-cloud-vps": "Windy Cloud", "windy-search": "Windy Search",
|
||||||
"windy-connect": "Windy Connect", "windy-drops": "Windy Drops", "windy-registry": "Windy Drops",
|
"windy-connect": "Windy Connect", "windy-drops": "Windy Drops", "windy-registry": "Windy Drops",
|
||||||
|
|||||||
@@ -75,6 +75,19 @@ MIRROR_TAG = "[GH#"
|
|||||||
# always red trains everyone to ignore red. Not posted until a rootless builder
|
# always red trains everyone to ignore red. Not posted until a rootless builder
|
||||||
# exists; that is a decision, recorded in docs/CUTOVER.md, not a failure.
|
# exists; that is a decision, recorded in docs/CUTOVER.md, not a failure.
|
||||||
NO_DAEMON_JOB = re.compile(r"docker", re.IGNORECASE)
|
NO_DAEMON_JOB = re.compile(r"docker", re.IGNORECASE)
|
||||||
|
# Image-build jobs whose NAME doesn't say docker (orchestrator 09-23, option A:
|
||||||
|
# each lane converts the job to a no-Docker smoke test; until then it is not
|
||||||
|
# posted). Format: "repo:workflow/job,...;repo2:...".
|
||||||
|
NO_DAEMON_NAMED: dict[str, set[str]] = {}
|
||||||
|
for _entry in os.environ.get("BRIDGE_NO_DAEMON", "eternitas:ci/build").split(";"):
|
||||||
|
if ":" in _entry:
|
||||||
|
_repo, _jobs = _entry.split(":", 1)
|
||||||
|
NO_DAEMON_NAMED[_repo.strip()] = {j.strip() for j in _jobs.split(",") if j.strip()}
|
||||||
|
|
||||||
|
|
||||||
|
def needs_daemon(repo: str, wf: str, job: str) -> bool:
|
||||||
|
"""True for image-build jobs, which cannot run here (no Docker daemon, I-5)."""
|
||||||
|
return bool(NO_DAEMON_JOB.search(job)) or f"{wf}/{job}" in NO_DAEMON_NAMED.get(repo, ())
|
||||||
|
|
||||||
# Jobs Grant ruled NON-BLOCKING (GRANT_DECISIONS_2026-09-23): still run on
|
# Jobs Grant ruled NON-BLOCKING (GRANT_DECISIONS_2026-09-23): still run on
|
||||||
# Windy Git and visible there, but not posted to GitHub, so they cannot turn a
|
# Windy Git and visible there, but not posted to GitHub, so they cannot turn a
|
||||||
@@ -294,7 +307,7 @@ def post_statuses(repo: str, sha: str) -> None:
|
|||||||
break
|
break
|
||||||
latest: dict[str, dict] = {}
|
latest: dict[str, dict] = {}
|
||||||
for r in runs:
|
for r in runs:
|
||||||
if r["head_sha"] != sha or NO_DAEMON_JOB.search(r["name"]):
|
if r["head_sha"] != sha or needs_daemon(repo, r["workflow_id"].removesuffix(".yml"), r["name"]):
|
||||||
continue
|
continue
|
||||||
if f"{r['workflow_id'].removesuffix('.yml')}/{r['name']}" in NON_BLOCKING.get(repo, ()):
|
if f"{r['workflow_id'].removesuffix('.yml')}/{r['name']}" in NON_BLOCKING.get(repo, ()):
|
||||||
continue
|
continue
|
||||||
@@ -304,9 +317,9 @@ def post_statuses(repo: str, sha: str) -> None:
|
|||||||
# Queued jobs: `pending` where nothing newer has been picked up. A re-run
|
# Queued jobs: `pending` where nothing newer has been picked up. A re-run
|
||||||
# queued behind an old failure must read pending, not the stale red.
|
# queued behind an old failure must read pending, not the stale red.
|
||||||
for q in queued_jobs(repo, sha):
|
for q in queued_jobs(repo, sha):
|
||||||
if NO_DAEMON_JOB.search(q["name"]):
|
|
||||||
continue
|
|
||||||
wf = q["workflow_id"].removesuffix(".yml")
|
wf = q["workflow_id"].removesuffix(".yml")
|
||||||
|
if needs_daemon(repo, wf, q["name"]):
|
||||||
|
continue
|
||||||
if f"{wf}/{q['name']}" in NON_BLOCKING.get(repo, ()):
|
if f"{wf}/{q['name']}" in NON_BLOCKING.get(repo, ()):
|
||||||
continue
|
continue
|
||||||
ctx = f"windy-git/{wf}/{q['name']}"
|
ctx = f"windy-git/{wf}/{q['name']}"
|
||||||
|
|||||||
Reference in New Issue
Block a user