"""GitHub Actions provider: latest workflow run of a repository. This file doubles as the reference for writing your own provider plugin. Drop a ``*.py`` file into ``~/.config/everything-green/providers/`` that defines: PROVIDER canonical name used in config ``[[sources]]`` entries (falls back to the file name with ``_`` replaced by ``-``) ALIASES optional extra names the config may use fetch(source, ctx) called once per configured source, in a worker thread. ``source`` is the merged config table for the entry; ``ctx`` offers ``get_json(url, headers=..., insecure=...)``, ``token(source, *default_env_names)``, ``cli_token(argv)``, ``basic_auth(user, secret)``, ``quote(text)``, ``iso_to_epoch(text)``, ``timeout`` and the ``ctx.Fail`` exception for clean, user-facing errors. All network access goes through ``ctx.get_json``. It is where the timeouts, the response size caps, the ETag cache, and the token-free error messages live, so a provider that reaches for urllib itself gives all of that up. ``fetch`` returns a dict with: status required — one of: success, failed, warning, running, pending, canceled, unknown key stable identity for the source (used in ids and as the default label) label optional default display name (config ``label`` wins) detail optional short line, e.g. "CI #142 · push" branch optional — the branch the config asked for. Omit it and the collector fills in the configured branch; do not report the branch of the last run, which is not what the user asked to watch. Nothing renders it today. url optional link opened when the row is clicked finished_at optional epoch seconds of the last finished run Config keys used here: repo required, "owner/name" branch optional, only look at runs on this branch workflow optional, a workflow file name ("ci.yml") or its numeric id ("123456"). Both go to the API verbatim, so a source always costs exactly one request. A display name would need a lookup call first, doubling every tick's requests to save one lookup. base_url optional, for GitHub Enterprise (default https://api.github.com) auth optional, "auto" (default) or "none" to force anonymous access token / token_cmd / token_file / token_env — see README "Tokens" Auth resolution with "auto": explicit token config, then the GITHUB_TOKEN / GH_TOKEN environment variables, then a logged-in ``gh`` CLI (``gh auth token``), then anonymous. """ from urllib.parse import urlparse PROVIDER = "github" ALIASES = ("github-actions", "gha") API_VERSION = "2022-11-28" # GitHub reports a run as status + conclusion; both map into our vocabulary. STATUS_MAP = { "in_progress": "running", "queued": "pending", "waiting": "pending", "requested": "pending", "pending": "pending", } CONCLUSION_MAP = { "success": "success", "failure": "failed", "timed_out": "failed", "startup_failure": "failed", "cancelled": "canceled", "action_required": "warning", "neutral": "unknown", "skipped": "unknown", "stale": "unknown", } def fetch(source, ctx): repo = str(source.get("repo", "")).strip().strip("/") if not repo or "/" not in repo: raise ctx.Fail('needs repo = "owner/name"') base = str(source.get("base_url", "https://api.github.com")).rstrip("/") branch = str(source.get("branch", "")).strip() workflow = str(source.get("workflow", "")).strip() headers = { "Accept": "application/vnd.github+json", "X-GitHub-Api-Version": API_VERSION, } token = None if str(source.get("auth", "auto")).strip().lower() != "none": token = ctx.token(source, "GITHUB_TOKEN", "GH_TOKEN") if not token: hostname = urlparse(base).hostname or "github.com" if hostname == "api.github.com": hostname = "github.com" token = ctx.cli_token(["gh", "auth", "token", "--hostname", hostname]) if token: headers["Authorization"] = f"Bearer {token}" owner, name = repo.split("/", 1) repo_api = f"{base}/repos/{ctx.quote(owner)}/{ctx.quote(name)}" if workflow: runs_url = f"{repo_api}/actions/workflows/{ctx.quote(workflow)}/runs" else: runs_url = f"{repo_api}/actions/runs" query = "?per_page=1" if branch: query += f"&branch={ctx.quote(branch)}" data = ctx.get_json(runs_url + query, headers=headers) runs = data.get("workflow_runs") or [] key = repo + (f"@{branch}" if branch else "") + (f"#{workflow}" if workflow else "") if not runs: return { "status": "unknown", "key": key, "label": repo, "detail": "no runs yet", "branch": branch, "url": f"https://github.com/{repo}/actions", } run = runs[0] conclusion = run.get("conclusion") if run.get("status") == "completed": status = CONCLUSION_MAP.get(conclusion or "", "unknown") else: status = STATUS_MAP.get(run.get("status") or "", "running") detail = str(run.get("name") or "run") if run.get("run_number"): detail += f" #{run['run_number']}" if run.get("event"): detail += f" · {run['event']}" return { "status": status, "key": key, "label": repo, "detail": detail, "branch": branch, "url": run.get("html_url") or f"https://github.com/{repo}/actions", "finished_at": ctx.iso_to_epoch(run.get("updated_at")), }