Compare commits
54 Commits
ffe631eeda
...
fix/komga-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98c2fab87f | ||
|
|
cfe948c4d2 | ||
|
|
a80f16fe7d | ||
|
|
b417da3980 | ||
|
|
1133720b5b | ||
|
|
bc2ef6cbec | ||
|
|
9c36f9b480 | ||
|
|
4e043f7b94 | ||
|
|
9e4e38535f | ||
|
|
96380fdf5f | ||
|
|
aff8931eaf | ||
|
|
69a6a1a62f | ||
|
|
9ffe457e5f | ||
|
|
e219d241bc | ||
|
|
1b0969d537 | ||
|
|
a830293736 | ||
|
|
4f7558050b | ||
|
|
6194f26f00 | ||
|
|
39da9cf9b5 | ||
|
|
a04d818479 | ||
|
|
8cee7c3ea5 | ||
|
|
409de579da | ||
|
|
e8f01387d3 | ||
|
|
409a7366ac | ||
|
|
f3d0980b73 | ||
|
|
455853cd43 | ||
|
|
e253d18335 | ||
|
|
a4b100feea | ||
|
|
63d8f6b38e | ||
|
|
e6d8ce0bba | ||
|
|
f96c4903cb | ||
|
|
bb01a71f00 | ||
|
|
a4d3bd10dc | ||
|
|
781091ad35 | ||
|
|
b02ed1c135 | ||
|
|
19f297e05d | ||
|
|
09e77bb359 | ||
|
|
6f5d30bd8a | ||
|
|
74a9f4e0ae | ||
|
|
3cebc286de | ||
|
|
8cc1de502f | ||
|
|
88f2f13da5 | ||
|
|
48f940018d | ||
|
|
ea979365a0 | ||
|
|
38977aa7dd | ||
|
|
b0578b69cb | ||
|
|
672be31026 | ||
|
|
16a8e7cf4a | ||
|
|
b8ffd40bab | ||
|
|
c7fbe3912b | ||
|
|
13b61820bc | ||
|
|
2e714774a0 | ||
|
|
804650fe58 | ||
|
|
3e37648317 |
@@ -25,9 +25,14 @@ Triage first, then one of two paths:
|
|||||||
Escalate mid-flight if a light task grows — touches a default, needs a new script or service, or
|
Escalate mid-flight if a light task grows — touches a default, needs a new script or service, or
|
||||||
reveals a deeper problem.
|
reveals a deeper problem.
|
||||||
|
|
||||||
**Standing rule:** ship the simplest solution that works. Complexity is bought only by a
|
**Standing rule:** ship the simplest solution that works, and build it out of what already
|
||||||
**measurement** showing a concrete, user-visible cost on a real host — never by reasoning about
|
exists — a `.templates/` module, an existing cont-init script, the pattern a sibling add-on
|
||||||
hypothetical performance.
|
already uses for the same problem. 120+ add-ons are maintained by one person: a homogeneous repo
|
||||||
|
where every add-on solves a problem the same way is worth more than a locally nicer bespoke
|
||||||
|
design. Prefer reusing or extending over adding a parallel implementation, and when you must add
|
||||||
|
something new, spell it the way the rest of the repo spells it (naming, option names, script
|
||||||
|
numbering, file layout). Complexity is bought only by a **measurement** showing a concrete,
|
||||||
|
user-visible cost on a real host — never by reasoning about hypothetical performance.
|
||||||
|
|
||||||
**Repo layout.** `alexbelgium/hassio-addons`; each add-on is a top-level directory. This skill is
|
**Repo layout.** `alexbelgium/hassio-addons`; each add-on is a top-level directory. This skill is
|
||||||
checked in at `.claude/skills/hassio-addon-workflow/` (canonical copy). Set the skill root once,
|
checked in at `.claude/skills/hassio-addon-workflow/` (canonical copy). Set the skill root once,
|
||||||
@@ -73,7 +78,14 @@ before it costs a full analysis pass. Measurement methodology, gotchas, and real
|
|||||||
|
|
||||||
## 3. Plan — choose the mechanism level, then Codex reviews it (full loop)
|
## 3. Plan — choose the mechanism level, then Codex reviews it (full loop)
|
||||||
|
|
||||||
Rank mechanisms, pick the lowest (simplest) one that solves it, and state the choice in the plan:
|
Look for prior art first: grep `.templates/` and the other add-ons for something that already
|
||||||
|
solves this (`grep -rl "<knob or pattern>" --exclude-dir=.git .` — search everything, not just
|
||||||
|
`*.sh`: the mechanism may live in a `Dockerfile`'s `ARG MODULES=` or an extensionless s6 `run`
|
||||||
|
file). If an add-on already handles it, the plan is "do what that one does" — say so, and say why
|
||||||
|
the existing mechanism can't be reused if you're not reusing it.
|
||||||
|
|
||||||
|
Then rank mechanisms, pick the lowest (simplest) one that solves it, and state the choice in the
|
||||||
|
plan:
|
||||||
|
|
||||||
1. A config value — an option, a schema constraint, an existing env var.
|
1. A config value — an option, a schema constraint, an existing env var.
|
||||||
2. An existing knob the base image already reads (`MAX_RES`, `DRINODE`, `SELKIES_*`).
|
2. An existing knob the base image already reads (`MAX_RES`, `DRINODE`, `SELKIES_*`).
|
||||||
@@ -110,7 +122,13 @@ not just the happy path.
|
|||||||
|
|
||||||
Before requesting review, check: did the diff stay at the ladder level chosen in step 3? Can this
|
Before requesting review, check: did the diff stay at the ladder level chosen in step 3? Can this
|
||||||
be solved by deleting instead of adding? Is the fix bigger than what it fixes? How does it fail in
|
be solved by deleting instead of adding? Is the fix bigger than what it fixes? How does it fail in
|
||||||
three years? Case studies of what happens when this check is skipped: `references/simplify.md`.
|
three years? And on reuse: does any hunk reimplement something `.templates/`, another script in
|
||||||
|
this add-on, or a sibling add-on already does — and if a future add-on hits this same problem,
|
||||||
|
will it find one way to solve it or two? Fold a near-duplicate into the existing mechanism, or
|
||||||
|
justify the divergence in the PR body — but never at the cost of an isolation rule
|
||||||
|
`references/traps.md` documents: scripts shared by symlink with the webtop add-ons take a new
|
||||||
|
numbered script, not an edit. Case studies of what happens when this check is skipped:
|
||||||
|
`references/simplify.md`.
|
||||||
|
|
||||||
## 6. Codex attacks the code (full loop only)
|
## 6. Codex attacks the code (full loop only)
|
||||||
|
|
||||||
|
|||||||
400
.github/generate_map.py
vendored
@@ -3,17 +3,20 @@
|
|||||||
Generate a static PNG world map colour-coded by the percentage of your
|
Generate a static PNG world map colour-coded by the percentage of your
|
||||||
stargazers that come from each country. The script maintains a CSV
|
stargazers that come from each country. The script maintains a CSV
|
||||||
in ".github/stargazer_countries.csv" cache so that locations are only looked
|
in ".github/stargazer_countries.csv" cache so that locations are only looked
|
||||||
up once (unless the country entry is blank).
|
up once. Blank answers are cached too and retried at most every RECHECK_DAYS,
|
||||||
|
no more than MAX_RECHECKS_PER_RUN re-checks per run.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import csv
|
import csv
|
||||||
|
import datetime
|
||||||
|
import math
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import plotly.express as px
|
import plotly.graph_objects as go
|
||||||
import pycountry
|
import pycountry
|
||||||
import requests
|
import requests
|
||||||
from geopy.geocoders import Nominatim
|
from geopy.geocoders import Nominatim
|
||||||
@@ -25,12 +28,87 @@ GITHUB_TOKEN = os.getenv("GITHUB_TOKEN") # provided by workflow
|
|||||||
CSV_PATH = Path(".github/stargazer_countries.csv")
|
CSV_PATH = Path(".github/stargazer_countries.csv")
|
||||||
PNG_PATH = Path(".github/stargazer_map.png")
|
PNG_PATH = Path(".github/stargazer_map.png")
|
||||||
|
|
||||||
|
# ---- Cache policy -----------------------------------------------------------
|
||||||
|
# Most blank rows are permanent: the user simply has no public "location" on
|
||||||
|
# their profile. Re-asking GitHub and Nominatim for them every week is ~1700
|
||||||
|
# wasted requests per run, so a blank answer is cached too and only refreshed
|
||||||
|
# after RECHECK_DAYS. A row with no "last_checked" (i.e. written before the
|
||||||
|
# column existed) counts as never checked and is looked up once, which
|
||||||
|
# stamps it.
|
||||||
|
RECHECK_DAYS = 90
|
||||||
|
|
||||||
|
# Cap on how many already-checked rows one run may *re*-check, oldest first.
|
||||||
|
# It applies only to rows that carry a real last_checked date and have since
|
||||||
|
# expired: left uncapped, they all fall due on the same day and land as one
|
||||||
|
# spike. Rows that have never been checked -- new stargazers, and every row
|
||||||
|
# migrated from the pre-"last_checked" CSV -- are always looked up in full, so
|
||||||
|
# the first run after this lands still sweeps the whole backlog.
|
||||||
|
MAX_RECHECKS_PER_RUN = 200
|
||||||
|
|
||||||
|
# ---- Rendering theme --------------------------------------------------------
|
||||||
|
# Dark, opaque panel: GitHub does not swap the image between README themes, so
|
||||||
|
# a single background has to work in both. A dark canvas with a bright
|
||||||
|
# sequential ramp stays readable on light and dark pages alike.
|
||||||
|
BG = "#0d1117" # page / ocean
|
||||||
|
LAND = "#2b323c" # countries with zero stargazers (still visible)
|
||||||
|
BORDER = "#0d1117" # country outlines, same as background
|
||||||
|
FG = "#e6edf3" # primary text
|
||||||
|
MUTED = "#8b98a5" # secondary text
|
||||||
|
|
||||||
|
# Viridis truncated at 35 %: even a single stargazer gets a colour that is
|
||||||
|
# clearly distinct from the empty-land grey.
|
||||||
|
SCALE = ["#2c728e", "#21918c", "#35b779", "#90d743", "#fde725"]
|
||||||
|
|
||||||
|
# pycountry names that are too long / too formal for a top-5 list
|
||||||
|
SHORT_NAMES = {
|
||||||
|
"Russian Federation": "Russia",
|
||||||
|
"Korea, Republic of": "South Korea",
|
||||||
|
"Korea, Democratic People's Republic of": "North Korea",
|
||||||
|
"Iran, Islamic Republic of": "Iran",
|
||||||
|
"Taiwan, Province of China": "Taiwan",
|
||||||
|
"Viet Nam": "Vietnam",
|
||||||
|
"Moldova, Republic of": "Moldova",
|
||||||
|
"Bolivia, Plurinational State of": "Bolivia",
|
||||||
|
"Venezuela, Bolivarian Republic of": "Venezuela",
|
||||||
|
"Tanzania, United Republic of": "Tanzania",
|
||||||
|
"Syrian Arab Republic": "Syria",
|
||||||
|
}
|
||||||
|
|
||||||
HEADERS = {
|
HEADERS = {
|
||||||
"Authorization": f"token {GITHUB_TOKEN}",
|
"Authorization": f"token {GITHUB_TOKEN}",
|
||||||
"Accept": "application/vnd.github.v3+json",
|
"Accept": "application/vnd.github.v3+json",
|
||||||
}
|
}
|
||||||
GEOL = Nominatim(user_agent="gh-stargazer-map")
|
GEOL = Nominatim(user_agent="gh-stargazer-map")
|
||||||
|
|
||||||
|
# Non-answers that Nominatim happily resolves to a real place: "Earth" is a
|
||||||
|
# town in Texas, "Remote" is a settlement in Oregon. Matched on the whole
|
||||||
|
# stripped, lowercased string only -- "Earth, TX" is someone's actual address
|
||||||
|
# and must still geocode.
|
||||||
|
JUNK_LOCATIONS = {
|
||||||
|
"127.0.0.1",
|
||||||
|
"/dev/null",
|
||||||
|
"anywhere",
|
||||||
|
"earth",
|
||||||
|
"everywhere",
|
||||||
|
"here",
|
||||||
|
"home",
|
||||||
|
"internet",
|
||||||
|
"localhost",
|
||||||
|
"mars",
|
||||||
|
"moon",
|
||||||
|
"n/a",
|
||||||
|
"none",
|
||||||
|
"nowhere",
|
||||||
|
"null",
|
||||||
|
"planet earth",
|
||||||
|
"remote",
|
||||||
|
"space",
|
||||||
|
"the internet",
|
||||||
|
"unknown",
|
||||||
|
"world",
|
||||||
|
"worldwide",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
@@ -52,20 +130,50 @@ def fetch_stargazer_usernames():
|
|||||||
return [s["login"] for s in github_paginated(url)]
|
return [s["login"] for s in github_paginated(url)]
|
||||||
|
|
||||||
|
|
||||||
|
def _checked_date(value):
|
||||||
|
"""Normalise a last_checked cell: a non-ISO-date value reads as never."""
|
||||||
|
value = (value or "").strip()
|
||||||
|
try:
|
||||||
|
datetime.date.fromisoformat(value)
|
||||||
|
except ValueError:
|
||||||
|
return ""
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
def load_cache():
|
def load_cache():
|
||||||
|
"""Map each username to (country, last_checked). Reads 2- and 3-column CSVs."""
|
||||||
if not CSV_PATH.exists():
|
if not CSV_PATH.exists():
|
||||||
return {}
|
return {}
|
||||||
with CSV_PATH.open(newline="", encoding="utf-8") as f:
|
with CSV_PATH.open(newline="", encoding="utf-8") as f:
|
||||||
return {row["username"]: row["country"] for row in csv.DictReader(f)}
|
return {
|
||||||
|
row["username"]: (
|
||||||
|
row["country"],
|
||||||
|
_checked_date(row.get("last_checked")),
|
||||||
|
)
|
||||||
|
for row in csv.DictReader(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def save_cache(cache):
|
def save_cache(cache):
|
||||||
|
"""Write the cache back as username,country,last_checked."""
|
||||||
CSV_PATH.parent.mkdir(parents=True, exist_ok=True)
|
CSV_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||||
with CSV_PATH.open("w", newline="", encoding="utf-8") as f:
|
with CSV_PATH.open("w", newline="", encoding="utf-8") as f:
|
||||||
w = csv.writer(f)
|
w = csv.writer(f)
|
||||||
w.writerow(["username", "country"])
|
w.writerow(["username", "country", "last_checked"])
|
||||||
for user, country in sorted(cache.items()):
|
for user, (country, last_checked) in sorted(cache.items()):
|
||||||
w.writerow([user, country or ""])
|
w.writerow([user, country or "", last_checked])
|
||||||
|
|
||||||
|
|
||||||
|
def needs_lookup(entry, cutoff):
|
||||||
|
"""True if this entry must be (re)queried. entry is None if absent."""
|
||||||
|
if entry is None:
|
||||||
|
return True # new stargazer
|
||||||
|
country, last_checked = entry
|
||||||
|
if country:
|
||||||
|
return False # a known country never changes here
|
||||||
|
if not last_checked:
|
||||||
|
return True # blank, never checked (pre-"last_checked" row)
|
||||||
|
return last_checked < cutoff # blank, and stale enough to retry
|
||||||
|
|
||||||
|
|
||||||
def username_to_country(login):
|
def username_to_country(login):
|
||||||
@@ -75,47 +183,223 @@ def username_to_country(login):
|
|||||||
loc = (resp.json() or {}).get("location") or ""
|
loc = (resp.json() or {}).get("location") or ""
|
||||||
if not loc.strip():
|
if not loc.strip():
|
||||||
return ""
|
return ""
|
||||||
|
if loc.strip().strip(".!").lower() in JUNK_LOCATIONS:
|
||||||
|
return ""
|
||||||
try:
|
try:
|
||||||
g = GEOL.geocode(loc, language="en", timeout=10)
|
g = GEOL.geocode(loc, language="en", addressdetails=True, timeout=10)
|
||||||
except Exception:
|
except Exception:
|
||||||
return ""
|
return ""
|
||||||
if not g or "display_name" not in g.raw:
|
# Use the ISO code from the structured address: Nominatim's English display
|
||||||
return ""
|
# names ("Russia", "Turkey", "Ivory Coast") do not all match pycountry's ISO
|
||||||
# take the last comma-separated component that matches a country
|
# names ("Russian Federation", "Türkiye", "Côte d'Ivoire").
|
||||||
for part in reversed(g.raw["display_name"].split(",")):
|
code = ((g.raw.get("address") or {}).get("country_code") or "") if g else ""
|
||||||
part = part.strip()
|
country = pycountry.countries.get(alpha_2=code.upper()) if code else None
|
||||||
|
return country.name if country else ""
|
||||||
|
|
||||||
|
|
||||||
|
def count_by_country(cache):
|
||||||
|
"""Counter of country name -> stargazers, ignoring blank locations."""
|
||||||
|
return Counter(country for country, _ in cache.values() if country)
|
||||||
|
|
||||||
|
|
||||||
|
def _log_ticks(lo, hi):
|
||||||
|
"""Colourbar ticks at ... 0.1, 0.3, 1, 3, 10, 30 ... spanning [lo, hi]."""
|
||||||
|
candidates = [m * 10**k for k in range(-3, 3) for m in (1, 3)]
|
||||||
|
ticks = [t for t in candidates if lo / 1.5 <= t <= hi]
|
||||||
|
return ticks or [hi]
|
||||||
|
|
||||||
|
|
||||||
|
def _fmt_pct(value):
|
||||||
|
"""1 -> '1%', 0.3 -> '0.3%' -- no trailing zeros."""
|
||||||
|
return f"{value:.2f}".rstrip("0").rstrip(".") + "%"
|
||||||
|
|
||||||
|
|
||||||
|
def build_figure(counts, total_stargazers):
|
||||||
|
"""Build the choropleth figure from a {country name: stargazers} mapping."""
|
||||||
|
by_iso = {}
|
||||||
|
for name, n in counts.items():
|
||||||
try:
|
try:
|
||||||
country = pycountry.countries.lookup(part).name
|
code = pycountry.countries.lookup(name).alpha_3
|
||||||
return country
|
|
||||||
except LookupError:
|
except LookupError:
|
||||||
pass
|
print("Skip unknown country:", name)
|
||||||
return ""
|
continue
|
||||||
|
# two spellings can resolve to the same ISO code, so accumulate
|
||||||
|
by_iso[code] = by_iso.get(code, 0) + n
|
||||||
|
|
||||||
|
iso = list(by_iso)
|
||||||
|
vals = [by_iso[k] for k in iso]
|
||||||
|
# count only what is actually drawn, so the caption matches the map
|
||||||
|
located = sum(vals) or 1
|
||||||
|
pcts = [v / located * 100 for v in vals]
|
||||||
|
lo, hi = (min(pcts), max(pcts)) if pcts else (1.0, 1.0)
|
||||||
|
|
||||||
def build_choropleth(percent_by_iso):
|
# The distribution is heavily long-tailed (the top country holds ~200x the
|
||||||
iso, vals = zip(*percent_by_iso.items())
|
# share of the tail), so a linear ramp collapses everything but a handful
|
||||||
fig = px.choropleth(
|
# of countries into the first colour step. Colour on log10 of the share.
|
||||||
locations=list(iso),
|
ticks = _log_ticks(lo, hi)
|
||||||
|
fig = go.Figure(
|
||||||
|
go.Choropleth(
|
||||||
|
locations=iso,
|
||||||
locationmode="ISO-3",
|
locationmode="ISO-3",
|
||||||
color=list(vals),
|
z=[math.log10(p) for p in pcts],
|
||||||
color_continuous_scale="Greens",
|
zmin=math.log10(lo) - 0.15, # keep the smallest share off the floor
|
||||||
range_color=(0, max(vals) if vals else 1),
|
zmax=math.log10(hi),
|
||||||
)
|
colorscale=SCALE,
|
||||||
fig.update_layout(
|
marker_line_color=BORDER,
|
||||||
coloraxis_colorbar=dict(
|
marker_line_width=0.5,
|
||||||
title="% stargazers",
|
colorbar=dict(
|
||||||
orientation="h", # <-- échelle horizontale
|
title=dict(
|
||||||
x=0.5, # <-- centré
|
text="share of located stargazers (log scale)",
|
||||||
y=0, # <-- tout en bas
|
font=dict(color=MUTED, size=13),
|
||||||
|
side="top",
|
||||||
|
),
|
||||||
|
orientation="h",
|
||||||
|
x=0.52,
|
||||||
|
y=0.02,
|
||||||
xanchor="center",
|
xanchor="center",
|
||||||
yanchor="bottom",
|
yanchor="bottom",
|
||||||
thickness=15,
|
thickness=12,
|
||||||
len=0.7, # <-- longueur de l'échelle, ajustable
|
len=0.34,
|
||||||
|
outlinewidth=0,
|
||||||
|
tickvals=[math.log10(t) for t in ticks],
|
||||||
|
ticktext=[_fmt_pct(t) for t in ticks],
|
||||||
|
tickfont=dict(color=MUTED, size=12),
|
||||||
),
|
),
|
||||||
margin=dict(l=0, r=0, t=0, b=0),
|
|
||||||
)
|
)
|
||||||
PNG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
)
|
||||||
fig.write_image(str(PNG_PATH), scale=2)
|
|
||||||
|
fig.update_geos(
|
||||||
|
projection_type="natural earth",
|
||||||
|
showframe=False,
|
||||||
|
showcoastlines=False,
|
||||||
|
showland=True,
|
||||||
|
landcolor=LAND,
|
||||||
|
showocean=True,
|
||||||
|
oceancolor=BG,
|
||||||
|
showlakes=False,
|
||||||
|
bgcolor=BG,
|
||||||
|
lataxis_range=[-56, 84], # crop Antarctica, it is always empty
|
||||||
|
lonaxis_range=[-176, 186],
|
||||||
|
domain=dict(x=[0.0, 1.0], y=[0.04, 0.92]),
|
||||||
|
)
|
||||||
|
|
||||||
|
repo = REPO or "this repository"
|
||||||
|
caption = (
|
||||||
|
f"{total_stargazers:,} stargazers"
|
||||||
|
f" | {located:,} mapped to a country"
|
||||||
|
f" | {len(by_iso)} countries"
|
||||||
|
)
|
||||||
|
annotations = [
|
||||||
|
dict(
|
||||||
|
text=f"<b>Stargazers of {repo}</b>",
|
||||||
|
x=0.012,
|
||||||
|
y=0.985,
|
||||||
|
xref="paper",
|
||||||
|
yref="paper",
|
||||||
|
xanchor="left",
|
||||||
|
yanchor="top",
|
||||||
|
showarrow=False,
|
||||||
|
font=dict(color=FG, size=25),
|
||||||
|
),
|
||||||
|
dict(
|
||||||
|
text=caption,
|
||||||
|
x=0.012,
|
||||||
|
y=0.925,
|
||||||
|
xref="paper",
|
||||||
|
yref="paper",
|
||||||
|
xanchor="left",
|
||||||
|
yanchor="top",
|
||||||
|
showarrow=False,
|
||||||
|
font=dict(color=MUTED, size=15),
|
||||||
|
),
|
||||||
|
dict(
|
||||||
|
text="Countries in grey have no located stargazer.<br>"
|
||||||
|
"Location is read from the public GitHub profile,<br>"
|
||||||
|
"so the map covers the located subset only.<br>"
|
||||||
|
"Country lookup by Nominatim geocoding,<br>"
|
||||||
|
"data © OpenStreetMap contributors.",
|
||||||
|
x=0.988,
|
||||||
|
y=0.05,
|
||||||
|
xref="paper",
|
||||||
|
yref="paper",
|
||||||
|
xanchor="right",
|
||||||
|
yanchor="bottom",
|
||||||
|
align="right",
|
||||||
|
showarrow=False,
|
||||||
|
font=dict(color=MUTED, size=12),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Top 5, laid out as two separate annotations (names, share) so each column
|
||||||
|
# stays aligned whatever the country name length -- HTML text in an SVG
|
||||||
|
# annotation collapses padding spaces, so a monospace table would not line
|
||||||
|
# up.
|
||||||
|
top = counts.most_common(5)
|
||||||
|
if top:
|
||||||
|
base_y = 0.40
|
||||||
|
columns = [
|
||||||
|
(
|
||||||
|
0.022,
|
||||||
|
"left",
|
||||||
|
"<br>".join(
|
||||||
|
f"{i}. {SHORT_NAMES.get(name, name)}"
|
||||||
|
for i, (name, _) in enumerate(top, 1)
|
||||||
|
),
|
||||||
|
FG,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
0.215,
|
||||||
|
"right",
|
||||||
|
"<br>".join(f"{n / located * 100:.1f}%" for _, n in top),
|
||||||
|
FG,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
annotations.append(
|
||||||
|
dict(
|
||||||
|
text="<b>TOP COUNTRIES</b>",
|
||||||
|
x=0.022,
|
||||||
|
y=base_y,
|
||||||
|
xref="paper",
|
||||||
|
yref="paper",
|
||||||
|
xanchor="left",
|
||||||
|
yanchor="top",
|
||||||
|
showarrow=False,
|
||||||
|
font=dict(color=MUTED, size=13),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
annotations += [
|
||||||
|
dict(
|
||||||
|
text=text,
|
||||||
|
x=x,
|
||||||
|
y=base_y - 0.055,
|
||||||
|
xref="paper",
|
||||||
|
yref="paper",
|
||||||
|
xanchor=anchor,
|
||||||
|
yanchor="top",
|
||||||
|
align=anchor,
|
||||||
|
showarrow=False,
|
||||||
|
font=dict(color=color, size=15),
|
||||||
|
)
|
||||||
|
for x, anchor, text, color in columns
|
||||||
|
]
|
||||||
|
|
||||||
|
fig.update_layout(
|
||||||
|
width=1240,
|
||||||
|
height=680,
|
||||||
|
paper_bgcolor=BG,
|
||||||
|
plot_bgcolor=BG,
|
||||||
|
margin=dict(l=0, r=0, t=0, b=0),
|
||||||
|
annotations=annotations,
|
||||||
|
)
|
||||||
|
return fig
|
||||||
|
|
||||||
|
|
||||||
|
def build_choropleth(counts, total_stargazers, path=PNG_PATH):
|
||||||
|
fig = build_figure(counts, total_stargazers)
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
# 1.5x of 1240x680 -> 1860x1020, sharp on HiDPI at README width without
|
||||||
|
# committing a multi-megabyte PNG every week.
|
||||||
|
fig.write_image(str(path), scale=1.5)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
@@ -128,40 +412,46 @@ def main():
|
|||||||
|
|
||||||
cache = load_cache()
|
cache = load_cache()
|
||||||
|
|
||||||
# Determine which usernames need a lookup
|
# Determine which usernames need a lookup. Anything never checked -- a new
|
||||||
to_lookup = [u for u in users if cache.get(u, "") == ""]
|
# stargazer, or a row migrated from the pre-"last_checked" CSV -- is looked
|
||||||
print(f"Need geocode for {len(to_lookup)} users")
|
# up in full. Rows that were checked before and have since expired are
|
||||||
|
# rate-limited to MAX_RECHECKS_PER_RUN, oldest first, so the recurring
|
||||||
|
# RECHECK_DAYS wave arrives in slices rather than all at once.
|
||||||
|
now = datetime.date.today()
|
||||||
|
today = now.isoformat()
|
||||||
|
cutoff = (now - datetime.timedelta(days=RECHECK_DAYS)).isoformat()
|
||||||
|
due = [u for u in users if needs_lookup(cache.get(u), cutoff)]
|
||||||
|
never = [u for u in due if not cache.get(u, ("", ""))[1]]
|
||||||
|
expired = sorted(
|
||||||
|
(u for u in due if cache.get(u, ("", ""))[1]),
|
||||||
|
key=lambda u: (cache[u][1], u),
|
||||||
|
)
|
||||||
|
rechecks = expired[:MAX_RECHECKS_PER_RUN]
|
||||||
|
to_lookup = never + rechecks
|
||||||
|
print(
|
||||||
|
f"Need geocode for {len(to_lookup)} users "
|
||||||
|
f"({len(never)} never checked, {len(rechecks)} of {len(expired)} expired)"
|
||||||
|
)
|
||||||
|
|
||||||
for i, login in enumerate(to_lookup, 1):
|
for i, login in enumerate(to_lookup, 1):
|
||||||
country = username_to_country(login)
|
country = username_to_country(login)
|
||||||
cache[login] = country
|
cache[login] = (country, today)
|
||||||
print(f"{i}/{len(to_lookup)}: {login:<20} -> {country}")
|
print(f"{i}/{len(to_lookup)}: {login:<20} -> {country}")
|
||||||
# Nominatim polite usage
|
# Nominatim polite usage
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
# Ensure all stargazers are in cache (even those with blank location)
|
# Ensure all stargazers are in cache (even those with blank location)
|
||||||
for u in users:
|
for u in users:
|
||||||
cache.setdefault(u, "")
|
cache.setdefault(u, ("", today))
|
||||||
|
|
||||||
save_cache(cache)
|
save_cache(cache)
|
||||||
|
|
||||||
# Build stats
|
# The cache is never pruned, so it still holds users who have since
|
||||||
countries = [c for c in cache.values() if c]
|
# unstarred. Keep them for future geocoding, but render only current stars.
|
||||||
counts = Counter(countries)
|
counts = count_by_country({u: cache[u] for u in users})
|
||||||
total = sum(counts.values()) or 1
|
|
||||||
pct_by_country = {c: v / total for c, v in counts.items()}
|
|
||||||
|
|
||||||
# convert to ISO-3 for plotly
|
|
||||||
pct_by_iso = {}
|
|
||||||
for c, pct in pct_by_country.items():
|
|
||||||
try:
|
|
||||||
iso = pycountry.countries.lookup(c).alpha_3
|
|
||||||
pct_by_iso[iso] = pct * 100 # plotly wants numeric
|
|
||||||
except LookupError:
|
|
||||||
print("Skip unknown country:", c)
|
|
||||||
|
|
||||||
print("Rendering PNG map…")
|
print("Rendering PNG map…")
|
||||||
build_choropleth(pct_by_iso)
|
build_choropleth(counts, len(users))
|
||||||
print(
|
print(
|
||||||
"Done – files saved:",
|
"Done – files saved:",
|
||||||
CSV_PATH.relative_to("."),
|
CSV_PATH.relative_to("."),
|
||||||
|
|||||||
18
.github/prompts/issue-classify.md
vendored
@@ -5,8 +5,9 @@ You are triaging a new issue on `alexbelgium/hassio-addons`, a monorepo of
|
|||||||
`run.sh`, s6 services, nginx config, `config.yaml`) around an upstream
|
`run.sh`, s6 services, nginx config, `config.yaml`) around an upstream
|
||||||
application that Alex does not maintain.
|
application that Alex does not maintain.
|
||||||
|
|
||||||
Your entire output is one JSON object written to `/tmp/ai-triage/verdict.json`.
|
Your entire output is one JSON object, returned as the run's structured output
|
||||||
You do not comment, label, or edit anything.
|
and matching the schema below. You have read-only tools by design: you do not
|
||||||
|
comment, label, write files, or edit anything.
|
||||||
|
|
||||||
## Rule 0 — ownership short-circuit
|
## Rule 0 — ownership short-circuit
|
||||||
|
|
||||||
@@ -97,6 +98,13 @@ Never close an issue. Never promise a timeline. Never say a fix is coming.
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`labels` should contain at most two, from the repo's existing set. Do not
|
Only `verdict` and `confidence` are required; omit the rest when they do not
|
||||||
invent new label names; the workflow adds `ai-triage` and `ai:classified`
|
apply.
|
||||||
on its own.
|
|
||||||
|
`labels` is cosmetic and accepts only `bug` or `enhancement`, at most two —
|
||||||
|
the workflow discards anything else, so inventing a label name simply loses
|
||||||
|
it. Control labels are not yours to set: the workflow adds `ai-triage`,
|
||||||
|
`ai:classified`, `ai:needs-info` and `ai:needs-human` on its own.
|
||||||
|
|
||||||
|
`comment` must stay under 4000 characters; a longer one is discarded and the
|
||||||
|
issue is handed to a human instead.
|
||||||
|
|||||||
5
.github/scripts/ai_triage_context.sh
vendored
@@ -77,7 +77,10 @@ if [ -n "$ADDON" ]; then
|
|||||||
{
|
{
|
||||||
echo
|
echo
|
||||||
echo "## Addon files: ${ADDON}/"
|
echo "## Addon files: ${ADDON}/"
|
||||||
if ! git sparse-checkout set --no-cone .github/prompts .github/scripts "$ADDON" 2>&1; then
|
# `set` REPLACES the checkout list, so .templates has to be repeated here
|
||||||
|
# or the workflow's sparse-checkout of it is silently undone at this point
|
||||||
|
# — which is exactly the state that starved #2949 of its turn budget.
|
||||||
|
if ! git sparse-checkout set --no-cone .github/prompts .github/scripts .templates "$ADDON" 2>&1; then
|
||||||
# Swallowing this used to leave ADDON resolved with no files behind it,
|
# Swallowing this used to leave ADDON resolved with no files behind it,
|
||||||
# so the classifier could still reach high confidence off the addon
|
# so the classifier could still reach high confidence off the addon
|
||||||
# name alone. Say so explicitly, in the same word Rule 2 already keys
|
# name alone. Say so explicitly, in the same word Rule 2 already keys
|
||||||
|
|||||||
5298
.github/stargazer_countries.csv
vendored
BIN
.github/stargazer_map.png
vendored
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 404 KiB |
BIN
.github/stats.png
vendored
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 1.9 KiB |
BIN
.github/stats_addons.png
vendored
|
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 4.1 KiB |
2
.github/workflows/daily_ai_fix.yaml
vendored
@@ -125,7 +125,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Analyse and fix
|
- name: Analyse and fix
|
||||||
if: steps.batch.outputs.count != '0'
|
if: steps.batch.outputs.count != '0'
|
||||||
uses: anthropics/claude-code-action@be7b93b1907a4abad570368f3c74b6fe3807510b # v1
|
uses: anthropics/claude-code-action@1623c36729ac1cd5895198cded705a287de7db79 # v1
|
||||||
with:
|
with:
|
||||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||||
# Skip the OIDC -> Claude App token exchange. The scheduled path
|
# Skip the OIDC -> Claude App token exchange. The scheduled path
|
||||||
|
|||||||
2
.github/workflows/on_claude_mention.yml
vendored
@@ -64,7 +64,7 @@ jobs:
|
|||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
- name: Run Claude Code
|
- name: Run Claude Code
|
||||||
uses: anthropics/claude-code-action@be7b93b1907a4abad570368f3c74b6fe3807510b # v1
|
uses: anthropics/claude-code-action@1623c36729ac1cd5895198cded705a287de7db79 # v1
|
||||||
with:
|
with:
|
||||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||||
# AI_PR_TOKEN, not GITHUB_TOKEN, so a PR Claude opens triggers CI.
|
# AI_PR_TOKEN, not GITHUB_TOKEN, so a PR Claude opens triggers CI.
|
||||||
|
|||||||
2
.github/workflows/on_issue_approved.yaml
vendored
@@ -135,7 +135,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Execute the plan
|
- name: Execute the plan
|
||||||
if: steps.bundle.outputs.has_plan == 'true'
|
if: steps.bundle.outputs.has_plan == 'true'
|
||||||
uses: anthropics/claude-code-action@be7b93b1907a4abad570368f3c74b6fe3807510b # v1
|
uses: anthropics/claude-code-action@1623c36729ac1cd5895198cded705a287de7db79 # v1
|
||||||
with:
|
with:
|
||||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||||
# Skip the OIDC -> Claude App token exchange, which 401s whenever
|
# Skip the OIDC -> Claude App token exchange, which 401s whenever
|
||||||
|
|||||||
387
.github/workflows/on_issues_ai_triage.yaml
vendored
@@ -21,6 +21,10 @@
|
|||||||
#
|
#
|
||||||
# Auth: Claude Pro/Max subscription via the CR_PAT GitHub Environment, which
|
# Auth: Claude Pro/Max subscription via the CR_PAT GitHub Environment, which
|
||||||
# holds the CLAUDE_CODE_OAUTH_TOKEN secret (generate with `claude setup-token`).
|
# holds the CLAUDE_CODE_OAUTH_TOKEN secret (generate with `claude setup-token`).
|
||||||
|
# GitHub side is GITHUB_TOKEN throughout — no PAT. The classify job pairs it
|
||||||
|
# with `allowed_non_write_users` so an outside reporter's issue-open event can
|
||||||
|
# get past the action's write-permission gate; the catch-up job pairs it with a
|
||||||
|
# job-level actions:write so it can dispatch. See the comments at each site.
|
||||||
|
|
||||||
name: AI issue triage
|
name: AI issue triage
|
||||||
|
|
||||||
@@ -37,6 +41,16 @@ on:
|
|||||||
issue:
|
issue:
|
||||||
description: "Issue number to (re-)triage manually"
|
description: "Issue number to (re-)triage manually"
|
||||||
required: true
|
required: true
|
||||||
|
source:
|
||||||
|
# Explicit provenance, set only by the catch-up job below. Previously
|
||||||
|
# this was inferred from github.actor, which is brittle: a re-run, a
|
||||||
|
# dispatch via a PAT or App, or another maintainer all change it, and
|
||||||
|
# the dangerous direction is the false negative — an automated retry
|
||||||
|
# that is never recognised as one keeps retrying forever. An input the
|
||||||
|
# scheduler sets explicitly cannot drift with GitHub's actor semantics.
|
||||||
|
description: "Set to 'catchup' by the daily catch-up job; leave blank for a manual re-triage"
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -127,9 +141,17 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
# .templates holds the shared build/runtime scripts (ha_entrypoint.sh,
|
||||||
|
# ha_automodules.sh, the cont-init modules) that nearly every add-on
|
||||||
|
# depends on, so a large share of reports can only be explained by
|
||||||
|
# reading them. Without it the classifier burned 6 of its turns on
|
||||||
|
# #2949 hunting for files that were not checked out, then died on
|
||||||
|
# max_turns. It is a small directory — cheaper to ship than to search
|
||||||
|
# for and not find.
|
||||||
sparse-checkout: |
|
sparse-checkout: |
|
||||||
.github/prompts
|
.github/prompts
|
||||||
.github/scripts
|
.github/scripts
|
||||||
|
.templates
|
||||||
sparse-checkout-cone-mode: false
|
sparse-checkout-cone-mode: false
|
||||||
|
|
||||||
- name: Build context bundle
|
- name: Build context bundle
|
||||||
@@ -144,7 +166,7 @@ jobs:
|
|||||||
id: classify
|
id: classify
|
||||||
if: github.event_name != 'issue_comment' || steps.claim.outputs.go == 'true'
|
if: github.event_name != 'issue_comment' || steps.claim.outputs.go == 'true'
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
uses: anthropics/claude-code-action@be7b93b1907a4abad570368f3c74b6fe3807510b # v1
|
uses: anthropics/claude-code-action@1623c36729ac1cd5895198cded705a287de7db79 # v1
|
||||||
with:
|
with:
|
||||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||||
# Without this the action falls back to the OIDC -> Claude App token
|
# Without this the action falls back to the OIDC -> Claude App token
|
||||||
@@ -153,21 +175,99 @@ jobs:
|
|||||||
# opened the issue or replied to a needs-info request. Same token the
|
# opened the issue or replied to a needs-info request. Same token the
|
||||||
# step already exports as GH_TOKEN; classify only reads.
|
# step already exports as GH_TOKEN; classify only reads.
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
# THE fix for tier 1. `issues` and `issue_comment` are "entity"
|
||||||
|
# contexts in the action (src/github/context.ts), so it runs
|
||||||
|
# checkWritePermissions() against github.actor — which on an
|
||||||
|
# issue-open event is the outside reporter, who never has write.
|
||||||
|
# Every run failed there ("Actor does not have write permissions")
|
||||||
|
# and continue-on-error painted it green. The bypass branch in
|
||||||
|
# src/github/validation/permissions.ts needs BOTH github_token
|
||||||
|
# (above) and a non-empty allowed_non_write_users — hence this.
|
||||||
|
# `schedule` / `workflow_dispatch` are "automation" contexts and skip
|
||||||
|
# the check entirely, which is why the catch-up path below does not
|
||||||
|
# need it.
|
||||||
|
#
|
||||||
|
# This is the case the input exists for (docs/security.md: "designed
|
||||||
|
# for automation workflows where user permissions are already
|
||||||
|
# restricted by the workflow's permission scope"). The scope here is
|
||||||
|
# contents:read + issues:write, the model gets no credentials and no
|
||||||
|
# Bash, and every value it produces is validated in Apply verdict.
|
||||||
|
allowed_non_write_users: "*"
|
||||||
|
# Separate gate from the one above, and it bit the catch-up path in
|
||||||
|
# production: checkHumanActor (src/github/validation/actor.ts)
|
||||||
|
# rejects any actor whose account type is not User. The catch-up
|
||||||
|
# dispatches with GITHUB_TOKEN, so those runs arrive as
|
||||||
|
# github-actions[bot] and died with "Workflow initiated by non-human
|
||||||
|
# actor". allowed_non_write_users does NOT cover this — it is only
|
||||||
|
# consulted for User accounts.
|
||||||
|
# Named rather than "*": only this repo's own workflows can dispatch
|
||||||
|
# as github-actions, whereas "*" would also admit any other App that
|
||||||
|
# can reach a trigger. The matcher lowercases and strips a trailing
|
||||||
|
# [bot], so this entry matches the github-actions[bot] actor.
|
||||||
|
# Scheduled runs are unaffected either way — they arrive as
|
||||||
|
# actor=alexbelgium, a User.
|
||||||
|
allowed_bots: "github-actions"
|
||||||
show_full_output: true
|
show_full_output: true
|
||||||
|
# Stated up front, because a wrong guess about the environment costs
|
||||||
|
# turns the analysis then does not have. On #2949, under the earlier
|
||||||
|
# 12-turn budget, the model spent 3 turns retrying Bash and 6 hunting
|
||||||
|
# files outside the sparse checkout and died before reaching a
|
||||||
|
# verdict. The budget is 25 now, but it is meant to buy analysis, not
|
||||||
|
# more failed probing — keep this in step with --max-turns below.
|
||||||
prompt: |
|
prompt: |
|
||||||
Read /tmp/ai-triage/context.md, then follow the instructions in
|
Read /tmp/ai-triage/context.md, then follow the instructions in
|
||||||
.github/prompts/issue-classify.md exactly.
|
.github/prompts/issue-classify.md exactly.
|
||||||
|
|
||||||
Write your verdict as a single JSON object to
|
Before you start, two facts about this environment. Both are hard
|
||||||
/tmp/ai-triage/verdict.json and write nothing else anywhere.
|
limits, not preferences — working around them is not possible and
|
||||||
Do NOT comment on or label the issue yourself.
|
costs you turns you need for the analysis.
|
||||||
|
|
||||||
|
You have exactly three tools: Read, Glob and Grep. There is no
|
||||||
|
Bash. Do not try to run `find`, `ls`, `cat` or any other command;
|
||||||
|
those calls fail and are not retryable. Use Glob where you would
|
||||||
|
have used `find`, and Grep where you would have used `grep`.
|
||||||
|
|
||||||
|
This is a SPARSE checkout of a 100+ add-on monorepo. Only these
|
||||||
|
paths exist on disk — everything else is absent, and searching for
|
||||||
|
it will find nothing no matter how you phrase the search:
|
||||||
|
* .templates/ shared build and runtime scripts that most
|
||||||
|
add-ons rely on (ha_entrypoint.sh,
|
||||||
|
ha_automodules.sh, the cont-init modules)
|
||||||
|
* .github/prompts/, .github/scripts/
|
||||||
|
* the single add-on directory named in the context bundle, if it
|
||||||
|
was resolved — the bundle says which, or says UNRESOLVED
|
||||||
|
Other add-ons are NOT present. If the bundle says UNRESOLVED, no
|
||||||
|
add-on source is on disk at all: judge from the bundle alone and
|
||||||
|
set confidence accordingly rather than searching for the code.
|
||||||
|
|
||||||
|
You have a budget of 25 turns. The context bundle already contains
|
||||||
|
the issue, its comments, the add-on's config/Dockerfile/docs, its
|
||||||
|
recent commits and candidate duplicates — so read it first and
|
||||||
|
spend turns only on what it does not already answer.
|
||||||
|
|
||||||
|
Return your verdict as structured output. Do NOT comment on or
|
||||||
|
label the issue yourself.
|
||||||
|
# The model gets NO write capability of any kind — not Bash, not
|
||||||
|
# Write, and no GH_TOKEN in this step's env. That matters more here
|
||||||
|
# than usual: allowed_non_write_users above deliberately admits
|
||||||
|
# untrusted reporters, and the issue body it reads is their text.
|
||||||
|
# With a Write tool an injected instruction could drop a script on
|
||||||
|
# disk and append BASH_ENV=<that script> to the runner's $GITHUB_ENV
|
||||||
|
# file command (discoverable under $RUNNER_TEMP with Glob). The
|
||||||
|
# runner applies that between steps, so the next bash step — Apply
|
||||||
|
# verdict, holding an issues:write GH_TOKEN — would source it before
|
||||||
|
# any of the validation below ran. Delivering the verdict through the
|
||||||
|
# action's --json-schema structured output instead of a file removes
|
||||||
|
# the write primitive that chain starts from.
|
||||||
|
# Duplicate lookup is already done too: ai_triage_context.sh ran
|
||||||
|
# `gh search issues` and baked the candidates into context.md, so the
|
||||||
|
# model has nothing left to ask GitHub for either.
|
||||||
claude_args: |
|
claude_args: |
|
||||||
--model claude-sonnet-5
|
--model claude-sonnet-5
|
||||||
--effort low
|
--effort low
|
||||||
--max-turns 12
|
--max-turns 25
|
||||||
--allowedTools "Read,Write,Glob,Grep,Bash(gh issue list:*),Bash(gh search issues:*)"
|
--allowedTools "Read,Glob,Grep"
|
||||||
env:
|
--json-schema '{"type":"object","properties":{"verdict":{"type":"string","enum":["owned","duplicate","needs-info","question","upstream-bug","addon-bug","feature-request"]},"addon":{"type":"string"},"confidence":{"type":"string","enum":["high","medium","low"]},"duplicate_of":{"type":"integer"},"labels":{"type":"array","items":{"type":"string"},"maxItems":2},"root_cause_hint":{"type":"string"},"comment":{"type":"string"}},"required":["verdict","confidence"]}'
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Apply verdict
|
- name: Apply verdict
|
||||||
if: github.event_name != 'issue_comment' || steps.claim.outputs.go == 'true'
|
if: github.event_name != 'issue_comment' || steps.claim.outputs.go == 'true'
|
||||||
@@ -176,31 +276,216 @@ jobs:
|
|||||||
ISSUE: ${{ github.event.issue.number || inputs.issue }}
|
ISSUE: ${{ github.event.issue.number || inputs.issue }}
|
||||||
REPO: ${{ github.repository }}
|
REPO: ${{ github.repository }}
|
||||||
EVENT_NAME: ${{ github.event_name }}
|
EVENT_NAME: ${{ github.event_name }}
|
||||||
|
# Distinguishes the automated catch-up retry from a manual
|
||||||
|
# re-triage — see is_automated_retry below.
|
||||||
|
DISPATCH_SOURCE: ${{ inputs.source }}
|
||||||
|
CLASSIFY_OUTCOME: ${{ steps.classify.outcome }}
|
||||||
|
# Through env, never interpolated into the script body: this string
|
||||||
|
# is model output and "${{ }}" inline would splice it into the shell
|
||||||
|
# source itself.
|
||||||
|
STRUCTURED: ${{ steps.classify.outputs.structured_output }}
|
||||||
|
# Written by the action even when it fails (setExecutionFileOutputIfPresent
|
||||||
|
# runs in its catch block), which is what lets the max-turns check below
|
||||||
|
# work on exactly the runs that need it.
|
||||||
|
EXECUTION_FILE: ${{ steps.classify.outputs.execution_file }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
mkdir -p /tmp/ai-triage
|
||||||
F=/tmp/ai-triage/verdict.json
|
F=/tmp/ai-triage/verdict.json
|
||||||
if [ ! -s "$F" ] || ! jq -e . "$F" >/dev/null 2>&1; then
|
|
||||||
echo "::warning::no usable verdict produced, leaving issue untouched"
|
# A reporter reply consumed ai:needs-info in the claim step above, so
|
||||||
# A reporter reply consumed ai:needs-info in the claim step above.
|
# every early exit below has to restore it or the next reply could
|
||||||
# With no verdict we would otherwise leave the issue with the flag
|
# never re-trigger. Defined once here rather than repeated per exit.
|
||||||
# gone, so the next reply could never re-trigger — restore it.
|
restore_needs_info() {
|
||||||
if [ "${EVENT_NAME:-}" = "issue_comment" ]; then
|
[ "${EVENT_NAME:-}" = "issue_comment" ] || return 0
|
||||||
gh issue edit "$ISSUE" --repo "$REPO" --add-label ai:needs-info >/dev/null 2>&1 || true
|
gh issue edit "$ISSUE" --repo "$REPO" --add-label ai:needs-info >/dev/null 2>&1 || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Is this the automated second look, rather than a first attempt?
|
||||||
|
# EVENT_NAME alone is not enough: workflow_dispatch is BOTH the daily
|
||||||
|
# catch-up retry and the maintainer's manual re-triage, so keying on
|
||||||
|
# it alone escalates a hand-dispatched first attempt immediately.
|
||||||
|
# The catch-up therefore states its provenance explicitly via the
|
||||||
|
# `source` input. Inferring it from github.actor instead was rejected:
|
||||||
|
# a re-run, a PAT- or App-issued dispatch, or a different maintainer
|
||||||
|
# all change the actor, and the failure that matters is the false
|
||||||
|
# NEGATIVE — an automated retry not recognised as one would never
|
||||||
|
# escalate and would retry that issue forever.
|
||||||
|
# Unknown provenance is treated as "not the automated retry", which
|
||||||
|
# is safe here because every non-escalating max-turns path below ends
|
||||||
|
# in a red run rather than a silent green one.
|
||||||
|
is_automated_retry() {
|
||||||
|
[ "${EVENT_NAME:-}" = "workflow_dispatch" ] && [ "${DISPATCH_SOURCE:-}" = "catchup" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Hand the issue to a human and take it out of the retry rotation.
|
||||||
|
# Returns non-zero if the labels did not actually land — callers must
|
||||||
|
# treat that as a failure rather than reporting a hand-off that never
|
||||||
|
# happened, which would leave the issue unlabelled and back in the
|
||||||
|
# retry rotation it was supposed to leave.
|
||||||
|
escalate_to_human() {
|
||||||
|
# Best effort: the label usually exists, and `gh issue edit` fails
|
||||||
|
# on its own below if it does not.
|
||||||
|
gh label create ai:needs-human --repo "$REPO" --color ededed >/dev/null 2>&1 || true
|
||||||
|
# NOT suppressed with `|| true`. ai-triage and ai:needs-info come
|
||||||
|
# off in the same call: leaving ai-triage would keep an issue we
|
||||||
|
# just escalated sitting in tier 2's unattended queue, and leaving
|
||||||
|
# ai:needs-info would let a reporter reply silently re-trigger
|
||||||
|
# classification behind the human's back. Removing a label the
|
||||||
|
# issue does not carry is a no-op, so this cannot fail spuriously.
|
||||||
|
gh issue edit "$ISSUE" --repo "$REPO" \
|
||||||
|
--add-label ai:needs-human \
|
||||||
|
--remove-label ai-triage --remove-label ai:needs-info >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Did the run die on its turn budget rather than on a workflow fault?
|
||||||
|
# The execution file is a JSON array of SDK messages; the terminal
|
||||||
|
# result object carries subtype "error_max_turns".
|
||||||
|
#
|
||||||
|
# This MUST fail closed: a false positive here downgrades a genuine
|
||||||
|
# workflow failure from a red run to a warning, which is the exact
|
||||||
|
# silent-failure class this workflow was rebuilt to remove. Hence the
|
||||||
|
# explicit `type == "array"` root check — without it `.[]?` happily
|
||||||
|
# iterates the VALUES of an object, so if the action ever changed the
|
||||||
|
# file's shape, {"result":{"subtype":"error_max_turns"}} would match
|
||||||
|
# and mask the failure. Anything that is not the array we expect is
|
||||||
|
# treated as "not max turns" and falls through to the loud path.
|
||||||
|
# The `?` and per-element type check keep a non-object element from
|
||||||
|
# aborting the step under set -e.
|
||||||
|
hit_max_turns() {
|
||||||
|
[ -n "${EXECUTION_FILE:-}" ] && [ -s "${EXECUTION_FILE:-}" ] || return 1
|
||||||
|
jq -e '(type == "array") and
|
||||||
|
any(.[]?;
|
||||||
|
(type == "object") and
|
||||||
|
(((.subtype? // "") == "error_max_turns") or
|
||||||
|
((.terminal_reason? // "") == "max_turns")))' \
|
||||||
|
"$EXECUTION_FILE" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
# GATE 1 — did the action itself run? This is checked BEFORE looking
|
||||||
|
# at the payload, because the action can fail *after* having written
|
||||||
|
# a valid structured output: the object would sail through the shape
|
||||||
|
# check below, labels and a comment would be applied, and the step
|
||||||
|
# would exit 0 — a green run on a failed action, which is the exact
|
||||||
|
# silent-failure mode this workflow was rebuilt to eliminate.
|
||||||
|
# A failed action means its output is not trustworthy, full stop.
|
||||||
|
#
|
||||||
|
# This branch is also deliberately label-neutral. An action failure
|
||||||
|
# (auth, config, an outage) is systemic — it hits every issue the
|
||||||
|
# same way — so quarantining here would silently bury a batch a day
|
||||||
|
# while the real fault sits in the workflow. Fail red, add nothing,
|
||||||
|
# let the catch-up retry once it's fixed. Classify carries
|
||||||
|
# continue-on-error so this step still runs at all; without the
|
||||||
|
# explicit exit 1 the job would report success.
|
||||||
|
if [ "${CLASSIFY_OUTCOME:-}" = "failure" ]; then
|
||||||
|
restore_needs_info
|
||||||
|
|
||||||
|
# ...with one exception. Exhausting the turn budget is NOT a
|
||||||
|
# workflow fault: the action ran fine and this particular issue was
|
||||||
|
# just too tangled to finish inside the turn budget. Treating it as systemic
|
||||||
|
# meant #2949 failed red and stayed unlabelled, so the catch-up
|
||||||
|
# re-dispatched it every day forever — and being the newest issue
|
||||||
|
# it took the first of only five daily slots each time.
|
||||||
|
# So it is handled like GATE 2 below instead: one retry, then a
|
||||||
|
# human. Warning rather than error, because a red run per day for a
|
||||||
|
# per-issue condition is alarm fatigue, and the outcome is recorded
|
||||||
|
# durably on the issue itself rather than only in a run log.
|
||||||
|
# A green run is only ever justified once the outcome is recorded
|
||||||
|
# somewhere durable. On the automated second look that is the
|
||||||
|
# ai:needs-human label, and only if it actually landed. On a first
|
||||||
|
# attempt nothing is recorded anywhere but this annotation, so
|
||||||
|
# exiting 0 there would be precisely the "green run, work silently
|
||||||
|
# dead" state that left triage broken for weeks. It costs at most
|
||||||
|
# one red run per problem issue, not one per day, because the
|
||||||
|
# second look ends the retry rotation either way.
|
||||||
|
if hit_max_turns; then
|
||||||
|
if is_automated_retry; then
|
||||||
|
echo "::warning::second attempt for #$ISSUE also ran out of turns, handing it to a human"
|
||||||
|
if ! escalate_to_human; then
|
||||||
|
echo "::error::could not label #$ISSUE ai:needs-human — it is NOT escalated and stays in the retry rotation"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "::error::classification for #$ISSUE ran out of turns; leaving it for the catch-up to retry once, after which it goes to a human"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "::error::the Classify action failed for #$ISSUE — this is usually a workflow-level fault affecting every issue, so the issue is left untouched for a retry. See the Classify step."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# The verdict arrives as the action's schema-validated structured
|
||||||
|
# output rather than a file the model wrote — see the Classify step.
|
||||||
|
printf '%s' "${STRUCTURED:-}" > "$F"
|
||||||
|
|
||||||
|
# GATE 2 — the action ran, but is the payload usable? `jq -e .` alone
|
||||||
|
# accepts any truthy JSON, so a verdict of `[1,2]` or `"hi"` would
|
||||||
|
# pass and then die on `.verdict` below with "Cannot index array with
|
||||||
|
# string", killing the step under set -e before the restore. Require
|
||||||
|
# an object.
|
||||||
|
#
|
||||||
|
# Reaching here means the failure is specific to THIS issue — the
|
||||||
|
# model looked at it and produced nothing usable — so a retry is
|
||||||
|
# worth exactly one attempt. Only a dispatch carrying source=catchup
|
||||||
|
# counts as that second attempt (is_automated_retry above); a manual
|
||||||
|
# workflow_dispatch is a first look and does NOT escalate, leaving
|
||||||
|
# the issue unlabelled so the catch-up still gets its own go. On the
|
||||||
|
# automated retry, hand it to a human rather than re-dispatching the
|
||||||
|
# same issue every day forever; ai:needs-human is in the catch-up
|
||||||
|
# exclusion search, so it drops out of the queue instead of starving
|
||||||
|
# newer issues behind it.
|
||||||
|
if [ ! -s "$F" ] || ! jq -e 'type == "object"' "$F" >/dev/null 2>&1; then
|
||||||
|
restore_needs_info
|
||||||
|
echo "::warning::no usable verdict produced for #$ISSUE"
|
||||||
|
if is_automated_retry; then
|
||||||
|
echo "::warning::second attempt produced no verdict, handing #$ISSUE to a human"
|
||||||
|
if ! escalate_to_human; then
|
||||||
|
echo "::error::could not label #$ISSUE ai:needs-human — it is NOT escalated and stays in the retry rotation"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "--- verdict ---"; jq . "$F"; echo "---------------"
|
echo "--- verdict ---"; jq . "$F"; echo "---------------"
|
||||||
|
|
||||||
|
# Everything below is derived from a file the model wrote after
|
||||||
|
# reading an attacker-controlled issue body, so treat all of it as
|
||||||
|
# untrusted input and validate before it reaches a `gh` call.
|
||||||
VERDICT=$(jq -r '.verdict // "unknown"' "$F")
|
VERDICT=$(jq -r '.verdict // "unknown"' "$F")
|
||||||
CONF=$(jq -r '.confidence // "low"' "$F")
|
CONF=$(jq -r '.confidence // "low"' "$F")
|
||||||
COMMENT=$(jq -r '.comment // ""' "$F")
|
COMMENT=$(jq -r '.comment // ""' "$F")
|
||||||
# Model-supplied labels are cosmetic only (e.g. "bug"). ai-triage /
|
|
||||||
# ai:classified / ai:needs-human are workflow-owned control labels;
|
case "$VERDICT" in
|
||||||
# strip anything in that namespace so a verdict can't self-trigger
|
owned|duplicate|needs-info|question|upstream-bug|addon-bug|feature-request) ;;
|
||||||
# tier 2 (the deterministic add below is the only legitimate source
|
*) echo "::warning::unrecognised verdict '$VERDICT', treating as low confidence"
|
||||||
# of ai-triage).
|
VERDICT="unknown"; CONF="low" ;;
|
||||||
mapfile -t LABELS < <(jq -r '.labels[]? // empty' "$F" | grep -vE '^ai[:-]' || true)
|
esac
|
||||||
|
case "$CONF" in high|medium|low) ;; *) CONF="low" ;; esac
|
||||||
|
|
||||||
|
# A triage comment is a duplicate one-liner or a <=4-item checklist.
|
||||||
|
# Anything longer is a malfunction or an attempt to use the bot's
|
||||||
|
# identity to post a wall of text / mention spam, so cap it.
|
||||||
|
if [ "${#COMMENT}" -gt 4000 ]; then
|
||||||
|
echo "::warning::comment was ${#COMMENT} chars, suppressing it and flagging a human"
|
||||||
|
COMMENT=""; CONF="low"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Model-supplied labels are cosmetic only, so this is an explicit
|
||||||
|
# allowlist rather than "any existing label that isn't ai:*". The repo
|
||||||
|
# carries labels that steer things — automerge, Priority, codex,
|
||||||
|
# wontfix, dependency-update, no-ai — and a crafted issue body must not
|
||||||
|
# be able to reach any of them through the classifier. These two are
|
||||||
|
# the only ones tier 1's verdicts actually map onto (addon-bug /
|
||||||
|
# upstream-bug -> bug, feature-request -> enhancement); both already
|
||||||
|
# exist, so nothing is ever created from model output. Cap at 2, as
|
||||||
|
# issue-classify.md already specifies.
|
||||||
|
mapfile -t LABELS < <(
|
||||||
|
jq -r '.labels[]? // empty' "$F" \
|
||||||
|
| grep -xE 'bug|enhancement' \
|
||||||
|
| head -n 2 || true
|
||||||
|
)
|
||||||
|
|
||||||
# Someone already owns this one: ping_submitter did its job. Best-
|
# Someone already owns this one: ping_submitter did its job. Best-
|
||||||
# effort clear of a manual re-triage's stale control labels (e.g. a
|
# effort clear of a manual re-triage's stale control labels (e.g. a
|
||||||
@@ -238,16 +523,23 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
LABELS+=("ai:classified")
|
LABELS+=("ai:classified")
|
||||||
|
|
||||||
# No --force: an existing label (e.g. a model-supplied cosmetic
|
# Only the workflow-owned control labels are ever created here; the
|
||||||
# "bug") must be left as-is. --force would update it, recoloring
|
# cosmetic ones were already filtered down to labels that exist. No
|
||||||
# every such label to ededed as a side effect of triage. Without it,
|
# --force, so an existing label keeps its colour instead of being
|
||||||
# create fails harmlessly on labels that already exist (|| true),
|
# recoloured to ededed as a side effect of triage.
|
||||||
# and still creates the workflow-owned ones the first time.
|
for l in ai-triage ai:classified ai:needs-human ai:needs-info; do
|
||||||
for l in "${LABELS[@]}"; do
|
|
||||||
gh label create "$l" --repo "$REPO" --color ededed >/dev/null 2>&1 || true
|
gh label create "$l" --repo "$REPO" --color ededed >/dev/null 2>&1 || true
|
||||||
done
|
done
|
||||||
|
# LABELS always picks up ai:classified above, so it cannot be empty
|
||||||
|
# today — but an empty array would expand to zero arguments and make
|
||||||
|
# `gh issue edit` fail with no option supplied, killing the step under
|
||||||
|
# set -e. Guard it so a future branch can't reintroduce that.
|
||||||
|
if [ "${#LABELS[@]}" -gt 0 ]; then
|
||||||
gh issue edit "$ISSUE" --repo "$REPO" \
|
gh issue edit "$ISSUE" --repo "$REPO" \
|
||||||
"${LABELS[@]/#/--add-label=}"
|
"${LABELS[@]/#/--add-label=}"
|
||||||
|
else
|
||||||
|
echo "::warning::no labels selected, skipping the add"
|
||||||
|
fi
|
||||||
|
|
||||||
# Manual re-triage can flip the verdict (e.g. a prior addon-bug
|
# Manual re-triage can flip the verdict (e.g. a prior addon-bug
|
||||||
# re-run now comes back needs-info/upstream-bug): clear whichever
|
# re-run now comes back needs-info/upstream-bug): clear whichever
|
||||||
@@ -265,6 +557,13 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -n "$COMMENT" ]; then
|
if [ -n "$COMMENT" ]; then
|
||||||
|
# The comment body is model prose written after reading an
|
||||||
|
# attacker-controlled issue. Defuse @mentions in it so a crafted
|
||||||
|
# issue can't turn the bot into a notification cannon: the empty
|
||||||
|
# HTML comment stops GitHub linkifying (and notifying) the handle
|
||||||
|
# while still rendering as plain "@name". The footer's own mention
|
||||||
|
# of the maintainer is added below, after this, so it still works.
|
||||||
|
COMMENT=$(printf '%s' "$COMMENT" | sed 's/@\([A-Za-z0-9]\)/@<!-- -->\1/g')
|
||||||
{
|
{
|
||||||
printf '%s\n\n' "$COMMENT"
|
printf '%s\n\n' "$COMMENT"
|
||||||
printf -- '---\n'
|
printf -- '---\n'
|
||||||
@@ -283,13 +582,26 @@ jobs:
|
|||||||
if: ${{ github.event_name == 'schedule' && vars.AI_DISABLED != 'true' }}
|
if: ${{ github.event_name == 'schedule' && vars.AI_DISABLED != 'true' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
environment: CR_PAT
|
# No `environment: CR_PAT` — this job holds no Claude call and now uses
|
||||||
|
# GITHUB_TOKEN, so it needs nothing from that environment's secrets.
|
||||||
|
# Job-level, so only this job gets actions:write — the classify job above
|
||||||
|
# keeps the workflow-level contents:read + issues:write, which is what
|
||||||
|
# allowed_non_write_users is safe under.
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
issues: read
|
||||||
|
actions: write
|
||||||
steps:
|
steps:
|
||||||
- name: Re-dispatch untriaged issues
|
- name: Re-dispatch untriaged issues
|
||||||
env:
|
env:
|
||||||
# AI_PR_TOKEN (repo scope) can dispatch workflows; GITHUB_TOKEN would
|
# Was secrets.AI_PR_TOKEN, which is a fine-grained PAT WITHOUT the
|
||||||
# need actions:write added to the whole workflow.
|
# actions scope: every dispatch returned "HTTP 403: Resource not
|
||||||
GH_TOKEN: ${{ secrets.AI_PR_TOKEN }}
|
# accessible by personal access token" and the `|| echo ::warning::`
|
||||||
|
# below swallowed it, so the safety net never caught anything.
|
||||||
|
# GITHUB_TOKEN + the job-level actions:write above needs no PAT at
|
||||||
|
# all, and workflow_dispatch is explicitly exempt from the rule that
|
||||||
|
# GITHUB_TOKEN-triggered events don't start new runs.
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
REPO: ${{ github.repository }}
|
REPO: ${{ github.repository }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
@@ -313,9 +625,20 @@ jobs:
|
|||||||
|
|
||||||
COUNT=$(grep -c . /tmp/todo.txt || true)
|
COUNT=$(grep -c . /tmp/todo.txt || true)
|
||||||
echo "untriaged issues to re-dispatch: $COUNT"
|
echo "untriaged issues to re-dispatch: $COUNT"
|
||||||
|
FAILED=0
|
||||||
while IFS= read -r n; do
|
while IFS= read -r n; do
|
||||||
[ -n "$n" ] || continue
|
[ -n "$n" ] || continue
|
||||||
echo "re-dispatching tier 1 for #$n"
|
echo "re-dispatching tier 1 for #$n"
|
||||||
gh workflow run "AI issue triage" --repo "$REPO" -f issue="$n" || \
|
gh workflow run "AI issue triage" --repo "$REPO" -f issue="$n" -f source=catchup || {
|
||||||
echo "::warning::could not dispatch classify for #$n"
|
echo "::error::could not dispatch classify for #$n"
|
||||||
|
FAILED=$((FAILED + 1))
|
||||||
|
}
|
||||||
done < /tmp/todo.txt
|
done < /tmp/todo.txt
|
||||||
|
|
||||||
|
# This job IS the safety net. A net that fails silently is worse than
|
||||||
|
# no net — it reported success every day for weeks while dispatching
|
||||||
|
# nothing. Fail the run so the breakage is visible.
|
||||||
|
if [ "$FAILED" -gt 0 ]; then
|
||||||
|
echo "::error::$FAILED of $COUNT catch-up dispatches failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|||||||
13
.github/workflows/on_pr_coderabbit.yml
vendored
@@ -79,7 +79,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Address CodeRabbit comments
|
- name: Address CodeRabbit comments
|
||||||
if: steps.claim.outputs.go == 'true'
|
if: steps.claim.outputs.go == 'true'
|
||||||
uses: anthropics/claude-code-action@be7b93b1907a4abad570368f3c74b6fe3807510b # v1
|
uses: anthropics/claude-code-action@1623c36729ac1cd5895198cded705a287de7db79 # v1
|
||||||
with:
|
with:
|
||||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||||
# Skip the OIDC -> Claude App token exchange, which 401s whenever
|
# Skip the OIDC -> Claude App token exchange, which 401s whenever
|
||||||
@@ -87,6 +87,17 @@ jobs:
|
|||||||
# coderabbitai[bot], the review submitter. AI_PR_TOKEN, not
|
# coderabbitai[bot], the review submitter. AI_PR_TOKEN, not
|
||||||
# GITHUB_TOKEN, so the pushed fixes re-trigger CI on the PR.
|
# GITHUB_TOKEN, so the pushed fixes re-trigger CI on the PR.
|
||||||
github_token: ${{ secrets.AI_PR_TOKEN }}
|
github_token: ${{ secrets.AI_PR_TOKEN }}
|
||||||
|
# Latent until now only because this job has never reached the action:
|
||||||
|
# every run so far skipped on the `ai-fix/*` branch guard. On the
|
||||||
|
# first real firing github.actor is coderabbitai[bot], and
|
||||||
|
# checkHumanActor (src/github/validation/actor.ts) rejects any actor
|
||||||
|
# whose account type is not User — a different gate from the write
|
||||||
|
# check above, which does return early for a [bot] actor. Without
|
||||||
|
# this the whole CodeRabbit follow-up tier would fail on its first
|
||||||
|
# genuine invocation. Named, not "*": the job `if` already requires
|
||||||
|
# the review to come from coderabbitai[bot], so nothing else can get
|
||||||
|
# here anyway, and "*" would only widen it if that guard changed.
|
||||||
|
allowed_bots: "coderabbitai"
|
||||||
prompt: |
|
prompt: |
|
||||||
CodeRabbit has reviewed pull request #${{ github.event.pull_request.number }}
|
CodeRabbit has reviewed pull request #${{ github.event.pull_request.number }}
|
||||||
on ${{ github.repository }}. You are on that PR's branch. Follow
|
on ${{ github.repository }}. You are on that PR's branch. Follow
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<!-- markdownlint-disable MD033 -->
|
<!-- markdownlint-disable MD033 -->
|
||||||
|
|
||||||
## 💖 Support development
|
## Support development
|
||||||
|
|
||||||
I maintain this and other Home Assistant add-ons in my free time: keeping up with upstream changes, HA changes, and testing on real hardware takes a lot of time (and some money). I use around 5-10 of my >110 addons so regularly I install test machines (and purchase some test services such as vpn) that I don't use myself to troubleshoot and improve the addons
|
I maintain this and other Home Assistant add-ons in my free time: keeping up with upstream changes, HA changes, and testing on real hardware takes a lot of time (and some money). I use around 5-10 of my >110 addons so regularly I install test machines (and purchase some test services such as vpn) that I don't use myself to troubleshoot and improve the addons
|
||||||
|
|
||||||
@@ -70,9 +70,15 @@ If you want to do add the repository manually, please follow the procedure highl
|
|||||||
- %%STATS_AMD64%%
|
- %%STATS_AMD64%%
|
||||||
- %%STATS_AARCH64%%
|
- %%STATS_AARCH64%%
|
||||||
|
|
||||||
### Stars evolution
|
### Star History
|
||||||
|
|
||||||
[](https://star-history.com/#alexbelgium/hassio-addons&Date)
|
<a href="https://www.star-history.com/?type=date&repos=alexbelgium%2Fhassio-addons">
|
||||||
|
<picture>
|
||||||
|
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=alexbelgium/hassio-addons&type=date&theme=dark&legend=top-left&sealed_token=Ft6D4rx2V8l-M626J7uFACNWFJexZTQuLZvFi-nQ_FnbQ0KFnkzPBnnQdui7CREsxlWJ5rdTXvx5PVjpFxxQwump2HCc5SDviHt_iZPdJB3ckWEjXp0V3w" />
|
||||||
|
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=alexbelgium/hassio-addons&type=date&legend=top-left&sealed_token=Ft6D4rx2V8l-M626J7uFACNWFJexZTQuLZvFi-nQ_FnbQ0KFnkzPBnnQdui7CREsxlWJ5rdTXvx5PVjpFxxQwump2HCc5SDviHt_iZPdJB3ckWEjXp0V3w" />
|
||||||
|
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=alexbelgium/hassio-addons&type=date&legend=top-left&sealed_token=Ft6D4rx2V8l-M626J7uFACNWFJexZTQuLZvFi-nQ_FnbQ0KFnkzPBnnQdui7CREsxlWJ5rdTXvx5PVjpFxxQwump2HCc5SDviHt_iZPdJB3ckWEjXp0V3w" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
|
||||||
## Add-ons provided by this repository
|
## Add-ons provided by this repository
|
||||||
|
|
||||||
|
|||||||
12
README.md
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<!-- markdownlint-disable MD033 -->
|
<!-- markdownlint-disable MD033 -->
|
||||||
|
|
||||||
## 💖 Support development
|
## Support development
|
||||||
|
|
||||||
I maintain this and other Home Assistant add-ons in my free time: keeping up with upstream changes, HA changes, and testing on real hardware takes a lot of time (and some money). I use around 5-10 of my >110 addons so regularly I install test machines (and purchase some test services such as vpn) that I don't use myself to troubleshoot and improve the addons
|
I maintain this and other Home Assistant add-ons in my free time: keeping up with upstream changes, HA changes, and testing on real hardware takes a lot of time (and some money). I use around 5-10 of my >110 addons so regularly I install test machines (and purchase some test services such as vpn) that I don't use myself to troubleshoot and improve the addons
|
||||||
|
|
||||||
@@ -70,9 +70,15 @@ If you want to do add the repository manually, please follow the procedure highl
|
|||||||
- amd64: 93%
|
- amd64: 93%
|
||||||
- aarch64: 7%
|
- aarch64: 7%
|
||||||
|
|
||||||
### Stars evolution
|
### Star History
|
||||||
|
|
||||||
[](https://star-history.com/#alexbelgium/hassio-addons&Date)
|
<a href="https://www.star-history.com/?type=date&repos=alexbelgium%2Fhassio-addons">
|
||||||
|
<picture>
|
||||||
|
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=alexbelgium/hassio-addons&type=date&theme=dark&legend=top-left&sealed_token=Ft6D4rx2V8l-M626J7uFACNWFJexZTQuLZvFi-nQ_FnbQ0KFnkzPBnnQdui7CREsxlWJ5rdTXvx5PVjpFxxQwump2HCc5SDviHt_iZPdJB3ckWEjXp0V3w" />
|
||||||
|
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=alexbelgium/hassio-addons&type=date&legend=top-left&sealed_token=Ft6D4rx2V8l-M626J7uFACNWFJexZTQuLZvFi-nQ_FnbQ0KFnkzPBnnQdui7CREsxlWJ5rdTXvx5PVjpFxxQwump2HCc5SDviHt_iZPdJB3ckWEjXp0V3w" />
|
||||||
|
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=alexbelgium/hassio-addons&type=date&legend=top-left&sealed_token=Ft6D4rx2V8l-M626J7uFACNWFJexZTQuLZvFi-nQ_FnbQ0KFnkzPBnnQdui7CREsxlWJ5rdTXvx5PVjpFxxQwump2HCc5SDviHt_iZPdJB3ckWEjXp0V3w" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
|
||||||
## Add-ons provided by this repository
|
## Add-ons provided by this repository
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 1.2 KiB |
@@ -1,4 +1,7 @@
|
|||||||
|
|
||||||
|
## 2.1.0 (2026-08-08)
|
||||||
|
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
|
||||||
|
|
||||||
## 2.0.3 (2026-08-01)
|
## 2.0.3 (2026-08-01)
|
||||||
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
|
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
name: Aurral
|
name: Aurral
|
||||||
version: "2.0.3"
|
version: "2.1.0"
|
||||||
slug: aurral
|
slug: aurral
|
||||||
description: >-
|
description: >-
|
||||||
Self-hosted music discovery, request management, flows, and playlist
|
Self-hosted music discovery, request management, flows, and playlist
|
||||||
|
|||||||
BIN
aurral/stats.png
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 1.4 KiB |
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"last_update": "2026-08-01",
|
"last_update": "2026-08-08",
|
||||||
"repository": "alexbelgium/hassio-addons",
|
"repository": "alexbelgium/hassio-addons",
|
||||||
"slug": "aurral",
|
"slug": "aurral",
|
||||||
"source": "github",
|
"source": "github",
|
||||||
"upstream_repo": "lklynet/aurral",
|
"upstream_repo": "lklynet/aurral",
|
||||||
"upstream_version": "2.0.3",
|
"upstream_version": "2.1.0",
|
||||||
"github_beta": false
|
"github_beta": false
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 1.4 KiB |
BIN
baikal/stats.png
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 1.6 KiB |
BIN
bazarr/stats.png
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -1,3 +1,8 @@
|
|||||||
|
## 0.0.101.1 (2026-08-08)
|
||||||
|
- Fix broken builds: upstream retagged `:latest` to the v1.0.0 rewrite on 2026-07-31, so this add-on was building on an image its rootfs does not support. `build_from` is now pinned to `chrisleekr/binance-trading-bot:0.0.101`, the frozen v0 line this add-on targets.
|
||||||
|
- This also resolves the `externally-managed-environment` (PEP 668) pip failure, which was a symptom of the same retag: the v1 image ships a much newer Alpine than the v0 line this add-on is built against.
|
||||||
|
- Upstream tracking is paused: v1.0.0 is a complete rewrite with no in-place upgrade (datastore moved to Postgres + TimescaleDB), so it needs an add-on rewrite rather than a version bump.
|
||||||
|
|
||||||
- Added support for configuring extra environment variables via the `env_vars` add-on option alongside config.yaml. See https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2 for details.
|
- Added support for configuring extra environment variables via the `env_vars` add-on option alongside config.yaml. See https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2 for details.
|
||||||
|
|
||||||
## 0.0.101 (2025-06-13)
|
## 0.0.101 (2025-06-13)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"build_from": {
|
"build_from": {
|
||||||
"aarch64": "chrisleekr/binance-trading-bot:latest",
|
"aarch64": "chrisleekr/binance-trading-bot:0.0.101",
|
||||||
"amd64": "chrisleekr/binance-trading-bot:latest"
|
"amd64": "chrisleekr/binance-trading-bot:0.0.101"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,4 +115,4 @@ schema:
|
|||||||
slug: binance-trading-bot
|
slug: binance-trading-bot
|
||||||
udev: true
|
udev: true
|
||||||
url: https://github.com/alexbelgium/hassio-addons
|
url: https://github.com/alexbelgium/hassio-addons
|
||||||
version: 0.0.101
|
version: 0.0.101.1
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 1.7 KiB |
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"github_beta": "true",
|
"github_beta": "true",
|
||||||
"last_update": "13-06-2025",
|
"last_update": "08-08-2026",
|
||||||
|
"paused": true,
|
||||||
"repository": "alexbelgium/hassio-addons",
|
"repository": "alexbelgium/hassio-addons",
|
||||||
"slug": "binance-trading-bot",
|
"slug": "binance-trading-bot",
|
||||||
"source": "github",
|
"source": "github",
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 1.4 KiB |
@@ -1,4 +1,7 @@
|
|||||||
|
|
||||||
|
## 1.93.134-ls121 (2026-08-08)
|
||||||
|
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
||||||
|
|
||||||
## 1.93.129-ls119 (2026-08-01)
|
## 1.93.129-ls119 (2026-08-01)
|
||||||
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
||||||
|
|
||||||
|
|||||||
@@ -69,5 +69,5 @@ slug: brave
|
|||||||
tmpfs: true
|
tmpfs: true
|
||||||
udev: true
|
udev: true
|
||||||
url: https://github.com/alexbelgium/hassio-addons
|
url: https://github.com/alexbelgium/hassio-addons
|
||||||
version: "1.93.129-ls119"
|
version: "1.93.134-ls121"
|
||||||
video: true
|
video: true
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"github_fulltag": "true",
|
"github_fulltag": "true",
|
||||||
"last_update": "2026-08-01",
|
"last_update": "2026-08-08",
|
||||||
"repository": "alexbelgium/hassio-addons",
|
"repository": "alexbelgium/hassio-addons",
|
||||||
"slug": "brave",
|
"slug": "brave",
|
||||||
"source": "github",
|
"source": "github",
|
||||||
"upstream_repo": "linuxserver/docker-brave",
|
"upstream_repo": "linuxserver/docker-brave",
|
||||||
"upstream_version": "1.93.129-ls119"
|
"upstream_version": "1.93.134-ls121"
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -1,4 +1,7 @@
|
|||||||
|
|
||||||
|
## 2.55.3 (2026-08-08)
|
||||||
|
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
|
||||||
|
|
||||||
## 2.55.2 (2026-08-01)
|
## 2.55.2 (2026-08-01)
|
||||||
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
|
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
|
||||||
|
|
||||||
|
|||||||
@@ -86,5 +86,5 @@ schema:
|
|||||||
slug: browserless_chrome
|
slug: browserless_chrome
|
||||||
udev: true
|
udev: true
|
||||||
url: https://github.com/alexbelgium/hassio-addons/tree/master/browserless_chrome
|
url: https://github.com/alexbelgium/hassio-addons/tree/master/browserless_chrome
|
||||||
version: "2.55.2"
|
version: "2.55.3"
|
||||||
webui: "[PROTO:ssl]://[HOST]:[PORT:3000]/docs"
|
webui: "[PROTO:ssl]://[HOST]:[PORT:3000]/docs"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 1.6 KiB |
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"github_tagfilter": "v",
|
"github_tagfilter": "v",
|
||||||
"last_update": "2026-08-01",
|
"last_update": "2026-08-08",
|
||||||
"repository": "alexbelgium/hassio-addons",
|
"repository": "alexbelgium/hassio-addons",
|
||||||
"slug": "browserless_chrome",
|
"slug": "browserless_chrome",
|
||||||
"source": "github",
|
"source": "github",
|
||||||
"upstream_repo": "browserless/chrome",
|
"upstream_repo": "browserless/chrome",
|
||||||
"upstream_version": "2.55.2"
|
"upstream_version": "2.55.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
|
|
||||||
|
## 9.13.0 (2026-08-08)
|
||||||
|
- Update to latest version from linuxserver/docker-calibre (changelog : https://github.com/linuxserver/docker-calibre/releases)
|
||||||
|
|
||||||
## 9.12.0 (2026-08-01)
|
## 9.12.0 (2026-08-01)
|
||||||
- Update to latest version from linuxserver/docker-calibre (changelog : https://github.com/linuxserver/docker-calibre/releases)
|
- Update to latest version from linuxserver/docker-calibre (changelog : https://github.com/linuxserver/docker-calibre/releases)
|
||||||
|
|
||||||
|
|||||||
@@ -117,5 +117,5 @@ schema:
|
|||||||
slug: calibre
|
slug: calibre
|
||||||
udev: true
|
udev: true
|
||||||
url: https://github.com/alexbelgium/hassio-addons/tree/master/calibre
|
url: https://github.com/alexbelgium/hassio-addons/tree/master/calibre
|
||||||
version: "9.12.0"
|
version: "9.13.0"
|
||||||
video: true
|
video: true
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 1.6 KiB |
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"github_fulltag": "false",
|
"github_fulltag": "false",
|
||||||
"last_update": "2026-08-01",
|
"last_update": "2026-08-08",
|
||||||
"repository": "alexbelgium/hassio-addons",
|
"repository": "alexbelgium/hassio-addons",
|
||||||
"slug": "calibre",
|
"slug": "calibre",
|
||||||
"source": "github",
|
"source": "github",
|
||||||
"upstream_repo": "linuxserver/docker-calibre",
|
"upstream_repo": "linuxserver/docker-calibre",
|
||||||
"upstream_version": "9.12.0"
|
"upstream_version": "9.13.0"
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 1.4 KiB |
@@ -1,3 +1,7 @@
|
|||||||
|
|
||||||
|
## 07308543 (2026-08-08)
|
||||||
|
- Update to latest version from linuxserver/docker-baseimage-selkies (changelog : https://github.com/linuxserver/docker-baseimage-selkies/releases)
|
||||||
|
- Upstream tag : debiantrixie-version-07308543
|
||||||
## 2026.08.04 (04-08-2026)
|
## 2026.08.04 (04-08-2026)
|
||||||
- Fix: reverted the GPU acceleration added in 2026.08.03. It did not just fail to help — it was
|
- Fix: reverted the GPU acceleration added in 2026.08.03. It did not just fail to help — it was
|
||||||
what disabled the GPU. `--use-gl=angle --use-angle=gl-egl` forces Mesa's EGL X11 platform,
|
what disabled the GPU. `--use-gl=angle --use-angle=gl-egl` forces Mesa's EGL X11 platform,
|
||||||
|
|||||||
@@ -136,5 +136,5 @@ schema:
|
|||||||
slug: claude_desktop
|
slug: claude_desktop
|
||||||
udev: true
|
udev: true
|
||||||
url: https://github.com/alexbelgium/hassio-addons
|
url: https://github.com/alexbelgium/hassio-addons
|
||||||
version: "2026.08.04"
|
version: "07308543"
|
||||||
video: true
|
video: true
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 1.4 KiB |
@@ -4,6 +4,6 @@
|
|||||||
"github_fulltag": true,
|
"github_fulltag": true,
|
||||||
"slug": "claude_desktop",
|
"slug": "claude_desktop",
|
||||||
"paused": false,
|
"paused": false,
|
||||||
"upstream_version": "ubunturesolute-version-3a10bef7",
|
"upstream_version": "debiantrixie-version-07308543",
|
||||||
"last_update": "2026-08-01"
|
"last_update": "2026-08-08"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
|
|
||||||
|
## 2.10.3 (2026-08-08)
|
||||||
|
- Update to latest version from Cleanuparr/Cleanuparr (changelog : https://github.com/Cleanuparr/Cleanuparr/releases)
|
||||||
|
|
||||||
## 2.10.2 (2026-08-01)
|
## 2.10.2 (2026-08-01)
|
||||||
- Update to latest version from Cleanuparr/Cleanuparr (changelog : https://github.com/Cleanuparr/Cleanuparr/releases)
|
- Update to latest version from Cleanuparr/Cleanuparr (changelog : https://github.com/Cleanuparr/Cleanuparr/releases)
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
#=== Home Assistant Addon ===#
|
#=== Home Assistant Addon ===#
|
||||||
|
|
||||||
# ARGs used in FROM must be declared before any FROM instruction
|
# ARGs used in FROM must be declared before any FROM instruction
|
||||||
ARG BUILD_UPSTREAM="2.10.2"
|
ARG BUILD_UPSTREAM="2.10.3"
|
||||||
|
|
||||||
#################
|
#################
|
||||||
# 1 Build Image #
|
# 1 Build Image #
|
||||||
|
|||||||
@@ -91,5 +91,5 @@ schema:
|
|||||||
TZ: str?
|
TZ: str?
|
||||||
slug: cleanuparr
|
slug: cleanuparr
|
||||||
url: https://github.com/alexbelgium/hassio-addons/tree/master/cleanuparr
|
url: https://github.com/alexbelgium/hassio-addons/tree/master/cleanuparr
|
||||||
version: "2.10.2"
|
version: "2.10.3"
|
||||||
webui: "[PROTO:ssl]://[HOST]:[PORT:11011]"
|
webui: "[PROTO:ssl]://[HOST]:[PORT:11011]"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 1.4 KiB |
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"last_update": "2026-08-01",
|
"last_update": "2026-08-08",
|
||||||
"repository": "alexbelgium/hassio-addons",
|
"repository": "alexbelgium/hassio-addons",
|
||||||
"slug": "cleanuparr",
|
"slug": "cleanuparr",
|
||||||
"source": "github",
|
"source": "github",
|
||||||
"upstream_repo": "Cleanuparr/Cleanuparr",
|
"upstream_repo": "Cleanuparr/Cleanuparr",
|
||||||
"upstream_version": "2.10.2"
|
"upstream_version": "2.10.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
|
|
||||||
|
## 19.20.0 (2026-08-08)
|
||||||
|
- Update to latest version from coderaiser/cloudcmd (changelog : https://github.com/coderaiser/cloudcmd/releases)
|
||||||
|
|
||||||
## 19.19.1 (2026-06-17)
|
## 19.19.1 (2026-06-17)
|
||||||
- Update to latest version from coderaiser/cloudcmd (changelog : https://github.com/coderaiser/cloudcmd/releases)
|
- Update to latest version from coderaiser/cloudcmd (changelog : https://github.com/coderaiser/cloudcmd/releases)
|
||||||
|
|
||||||
|
|||||||
@@ -104,4 +104,4 @@ schema:
|
|||||||
slug: cloudcommander
|
slug: cloudcommander
|
||||||
udev: true
|
udev: true
|
||||||
url: https://github.com/alexbelgium/hassio-addons/tree/master/cloudcommander
|
url: https://github.com/alexbelgium/hassio-addons/tree/master/cloudcommander
|
||||||
version: "19.19.1"
|
version: "19.20.0"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 1.5 KiB |
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"last_update": "2026-06-17",
|
"last_update": "2026-08-08",
|
||||||
"repository": "alexbelgium/hassio-addons",
|
"repository": "alexbelgium/hassio-addons",
|
||||||
"slug": "cloudcommander",
|
"slug": "cloudcommander",
|
||||||
"source": "github",
|
"source": "github",
|
||||||
"upstream_repo": "coderaiser/cloudcmd",
|
"upstream_repo": "coderaiser/cloudcmd",
|
||||||
"upstream_version": "19.19.1"
|
"upstream_version": "19.20.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
|
|
||||||
|
## 2.2.6 (2026-08-08)
|
||||||
|
- Update to latest version from ajslater/codex (changelog : https://github.com/ajslater/codex/releases)
|
||||||
|
|
||||||
## 2.2.5 (2026-08-02)
|
## 2.2.5 (2026-08-02)
|
||||||
- Update to latest version from ajslater/codex (changelog : https://github.com/ajslater/codex/releases)
|
- Update to latest version from ajslater/codex (changelog : https://github.com/ajslater/codex/releases)
|
||||||
|
|
||||||
|
|||||||
@@ -101,4 +101,4 @@ schema:
|
|||||||
slug: codex
|
slug: codex
|
||||||
udev: true
|
udev: true
|
||||||
url: https://github.com/alexbelgium/hassio-addons
|
url: https://github.com/alexbelgium/hassio-addons
|
||||||
version: "2.2.5"
|
version: "2.2.6"
|
||||||
|
|||||||
BIN
codex/stats.png
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 1.5 KiB |
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"github_beta": "true",
|
"github_beta": "true",
|
||||||
"last_update": "2026-08-02",
|
"last_update": "2026-08-08",
|
||||||
"repository": "alexbelgium/hassio-addons",
|
"repository": "alexbelgium/hassio-addons",
|
||||||
"slug": "codex",
|
"slug": "codex",
|
||||||
"source": "github",
|
"source": "github",
|
||||||
"upstream_repo": "ajslater/codex",
|
"upstream_repo": "ajslater/codex",
|
||||||
"upstream_version": "2.2.5"
|
"upstream_version": "2.2.6"
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 1.6 KiB |
BIN
emby/stats.png
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 1.7 KiB |
@@ -1,3 +1,6 @@
|
|||||||
|
## 4.4.25 (2026-08-08)
|
||||||
|
- Update to latest version from ente/ente (changelog : https://github.com/ente/ente/releases)
|
||||||
|
- Fix build failure: upstream renamed the `ente-io` org to `ente`, so the base image is now `ghcr.io/ente/server` (GHCR does not follow the rename)
|
||||||
|
|
||||||
## 4.4.23 (2026-06-11)
|
## 4.4.23 (2026-06-11)
|
||||||
- Update to latest version from ente-io/ente (changelog : https://github.com/ente-io/ente/releases)
|
- Update to latest version from ente-io/ente (changelog : https://github.com/ente-io/ente/releases)
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ RUN set -eux; \
|
|||||||
|
|
||||||
# Pull the web source
|
# Pull the web source
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
RUN git clone --depth 1 --branch "${ENTE_WEB_TAG}" https://github.com/ente-io/ente.git .
|
RUN git clone --depth 1 --branch "${ENTE_WEB_TAG}" https://github.com/ente/ente.git .
|
||||||
|
|
||||||
# Build web workspace (lives in ./web)
|
# Build web workspace (lives in ./web)
|
||||||
WORKDIR /src/web
|
WORKDIR /src/web
|
||||||
@@ -52,7 +52,7 @@ RUN npm run build:memories
|
|||||||
#################
|
#################
|
||||||
# 1) Base image #
|
# 1) Base image #
|
||||||
#################
|
#################
|
||||||
FROM ghcr.io/ente-io/server:latest
|
FROM ghcr.io/ente/server:latest
|
||||||
|
|
||||||
##################
|
##################
|
||||||
# 2) Tune image #
|
# 2) Tune image #
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ _Thanks to everyone having starred my repo! To star it click on the image below,
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
[Ente](https://github.com/ente-io/ente) is a self-hosted, end-to-end encrypted photo and video storage solution. This addon provides a complete Ente server setup including the museum API server and MinIO S3-compatible storage backend.
|
[Ente](https://github.com/ente/ente) is a self-hosted, end-to-end encrypted photo and video storage solution. This addon provides a complete Ente server setup including the museum API server and MinIO S3-compatible storage backend.
|
||||||
|
|
||||||
Ente offers:
|
Ente offers:
|
||||||
- End-to-end encrypted photo and video backup
|
- End-to-end encrypted photo and video backup
|
||||||
@@ -41,7 +41,7 @@ Ente offers:
|
|||||||
- Album sharing with family and friends
|
- Album sharing with family and friends
|
||||||
- Full control over your data with self-hosting
|
- Full control over your data with self-hosting
|
||||||
|
|
||||||
This addon is based on the official Ente server: https://github.com/ente-io/ente/tree/main/server
|
This addon is based on the official Ente server: https://github.com/ente/ente/tree/main/server
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"build_from": {
|
"build_from": {
|
||||||
"aarch64": "ghcr.io/ente-io/server:927c6a316f181c7901446311f0085593b346b336",
|
"aarch64": "ghcr.io/ente/server:927c6a316f181c7901446311f0085593b346b336",
|
||||||
"amd64": "ghcr.io/ente-io/server:927c6a316f181c7901446311f0085593b346b336"
|
"amd64": "ghcr.io/ente/server:927c6a316f181c7901446311f0085593b346b336"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,6 +131,6 @@ schema:
|
|||||||
slug: ente
|
slug: ente
|
||||||
udev: true
|
udev: true
|
||||||
url: https://github.com/alexbelgium/hassio-addons
|
url: https://github.com/alexbelgium/hassio-addons
|
||||||
version: "4.4.23"
|
version: "4.4.25"
|
||||||
video: true
|
video: true
|
||||||
webui: http://[HOST]:[PORT:3000]
|
webui: http://[HOST]:[PORT:3000]
|
||||||
|
|||||||
BIN
ente/stats.png
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 1.5 KiB |
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"github_beta": "false",
|
"github_beta": "false",
|
||||||
"last_update": "2026-06-11",
|
"last_update": "2026-08-08",
|
||||||
"repository": "alexbelgium/hassio-addons",
|
"repository": "alexbelgium/hassio-addons",
|
||||||
"slug": "ente",
|
"slug": "ente",
|
||||||
"source": "github",
|
"source": "github",
|
||||||
"upstream_repo": "ente-io/ente",
|
"upstream_repo": "ente/ente",
|
||||||
"upstream_version": "4.4.23"
|
"upstream_version": "4.4.25"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
|
|
||||||
|
## 2026.08.04 (2026-08-04)
|
||||||
|
- Update to latest version from charlocharlie/epicgames-freegames
|
||||||
|
- Upstream tag : debian-2026-08-04
|
||||||
|
|
||||||
## 2026.08.01 (2026-08-01)
|
## 2026.08.01 (2026-08-01)
|
||||||
- Update to latest version from charlocharlie/epicgames-freegames
|
- Update to latest version from charlocharlie/epicgames-freegames
|
||||||
- Upstream tag : debian-2026-08-01
|
- Upstream tag : debian-2026-08-01
|
||||||
|
|||||||
@@ -88,5 +88,5 @@ schema:
|
|||||||
slug: epicgamesfree
|
slug: epicgamesfree
|
||||||
udev: true
|
udev: true
|
||||||
url: https://github.com/alexbelgium/hassio-addons
|
url: https://github.com/alexbelgium/hassio-addons
|
||||||
version: "2026.08.01"
|
version: "2026.08.04"
|
||||||
webui: "[PROTO:ssl]://[HOST]:[PORT:3000]"
|
webui: "[PROTO:ssl]://[HOST]:[PORT:3000]"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 1.5 KiB |
@@ -2,10 +2,10 @@
|
|||||||
"dockerhub_by_date": true,
|
"dockerhub_by_date": true,
|
||||||
"dockerhub_list_size": 2,
|
"dockerhub_list_size": 2,
|
||||||
"github_exclude": "-",
|
"github_exclude": "-",
|
||||||
"last_update": "2026-08-01",
|
"last_update": "2026-08-04",
|
||||||
"repository": "alexbelgium/hassio-addons",
|
"repository": "alexbelgium/hassio-addons",
|
||||||
"slug": "epicgamesfree",
|
"slug": "epicgamesfree",
|
||||||
"source": "dockerhub",
|
"source": "dockerhub",
|
||||||
"upstream_repo": "charlocharlie/epicgames-freegames",
|
"upstream_repo": "charlocharlie/epicgames-freegames",
|
||||||
"upstream_version": "debian-2026-08-01"
|
"upstream_version": "debian-2026-08-04"
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 1.6 KiB |
@@ -1,4 +1,7 @@
|
|||||||
|
|
||||||
|
## 1.5.1 (2026-08-08)
|
||||||
|
- Update to latest version from gtsteffaniak/filebrowser (changelog : https://github.com/gtsteffaniak/filebrowser/releases)
|
||||||
|
|
||||||
## 1.5.0 (2026-07-22)
|
## 1.5.0 (2026-07-22)
|
||||||
- Update to latest version from gtsteffaniak/filebrowser (changelog : https://github.com/gtsteffaniak/filebrowser/releases)
|
- Update to latest version from gtsteffaniak/filebrowser (changelog : https://github.com/gtsteffaniak/filebrowser/releases)
|
||||||
## 1.4.0-2 (16-07-2026)
|
## 1.4.0-2 (16-07-2026)
|
||||||
|
|||||||
@@ -114,4 +114,4 @@ schema:
|
|||||||
slug: filebrowser_quantum
|
slug: filebrowser_quantum
|
||||||
udev: true
|
udev: true
|
||||||
url: https://github.com/alexbelgium/hassio-addons
|
url: https://github.com/alexbelgium/hassio-addons
|
||||||
version: "1.5.0"
|
version: "1.5.1"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 1.5 KiB |
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"github_beta": "false",
|
"github_beta": "false",
|
||||||
"last_update": "2026-07-22",
|
"last_update": "2026-08-08",
|
||||||
"paused": false,
|
"paused": false,
|
||||||
"repository": "alexbelgium/hassio-addons",
|
"repository": "alexbelgium/hassio-addons",
|
||||||
"slug": "filebrowser_quantum",
|
"slug": "filebrowser_quantum",
|
||||||
"source": "github",
|
"source": "github",
|
||||||
"upstream_repo": "gtsteffaniak/filebrowser",
|
"upstream_repo": "gtsteffaniak/filebrowser",
|
||||||
"upstream_version": "1.5.0"
|
"upstream_version": "1.5.1"
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -1,4 +1,7 @@
|
|||||||
|
|
||||||
|
## 3.20.2 (2026-08-08)
|
||||||
|
- Update to latest version from wiserain/flexget
|
||||||
|
|
||||||
## 3.19.31 (2026-08-01)
|
## 3.19.31 (2026-08-01)
|
||||||
- Update to latest version from wiserain/flexget
|
- Update to latest version from wiserain/flexget
|
||||||
|
|
||||||
|
|||||||
@@ -95,5 +95,5 @@ schema:
|
|||||||
slug: flexget
|
slug: flexget
|
||||||
udev: true
|
udev: true
|
||||||
url: https://github.com/alexbelgium/hassio-addons
|
url: https://github.com/alexbelgium/hassio-addons
|
||||||
version: "3.19.31"
|
version: "3.20.2"
|
||||||
webui: "[PROTO:ssl]://[HOST]:[PORT:5050]"
|
webui: "[PROTO:ssl]://[HOST]:[PORT:5050]"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"dockerhub_list_size": "10",
|
"dockerhub_list_size": "10",
|
||||||
"last_update": "2026-08-01",
|
"last_update": "2026-08-08",
|
||||||
"repository": "alexbelgium/hassio-addons",
|
"repository": "alexbelgium/hassio-addons",
|
||||||
"slug": "flexget",
|
"slug": "flexget",
|
||||||
"source": "dockerhub",
|
"source": "dockerhub",
|
||||||
"upstream_repo": "wiserain/flexget",
|
"upstream_repo": "wiserain/flexget",
|
||||||
"upstream_version": "3.19.31"
|
"upstream_version": "3.20.2"
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 1.6 KiB |