diff --git a/.github/workflows/track-ghcr-downloads.yml b/.github/workflows/track-ghcr-downloads.yml
new file mode 100644
index 0000000..972a2b0
--- /dev/null
+++ b/.github/workflows/track-ghcr-downloads.yml
@@ -0,0 +1,147 @@
+name: Track GHCR downloads
+
+on:
+ schedule:
+ - cron: "17 2 * * *"
+ workflow_dispatch:
+
+permissions:
+ contents: write
+ packages: read
+
+concurrency:
+ group: ghcr-download-tracker
+ cancel-in-progress: false
+
+jobs:
+ track:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Read package download count
+ id: downloads
+ env:
+ GH_TOKEN: ${{ github.token }}
+ shell: bash
+ run: |
+ set -euo pipefail
+ total="$(
+ gh api --paginate "/users/${{ github.repository_owner }}/packages/container/copilot_here/versions?per_page=100" --jq '.[].download_count // 0' |
+ awk '{ total += $1 } END { print total + 0 }'
+ )"
+ echo "total=$total" >> "$GITHUB_OUTPUT"
+ echo "Current GHCR downloads: $total"
+
+ - name: Update history and chart
+ env:
+ TOTAL_DOWNLOADS: ${{ steps.downloads.outputs.total }}
+ run: |
+ python3 <<'PY'
+ import csv
+ import html
+ import os
+ from datetime import date, timedelta
+ from pathlib import Path
+
+ data_path = Path("docs/ghcr-downloads.csv")
+ chart_path = Path("docs/ghcr-downloads.svg")
+ data_path.parent.mkdir(parents=True, exist_ok=True)
+
+ today = date.today()
+ total = int(os.environ["TOTAL_DOWNLOADS"])
+ rows = {}
+
+ if data_path.exists():
+ with data_path.open(newline="", encoding="utf-8") as f:
+ for row in csv.DictReader(f):
+ rows[date.fromisoformat(row["date"])] = int(row["downloads"])
+
+ rows[today] = total
+ cutoff = today - timedelta(days=89)
+ rows = {d: count for d, count in rows.items() if d >= cutoff}
+ points = sorted(rows.items())
+
+ with data_path.open("w", newline="", encoding="utf-8") as f:
+ writer = csv.writer(f)
+ writer.writerow(["date", "downloads"])
+ for d, count in points:
+ writer.writerow([d.isoformat(), count])
+
+ width, height = 900, 360
+ left, right, top, bottom = 72, 24, 54, 54
+ plot_w = width - left - right
+ plot_h = height - top - bottom
+ counts = [count for _, count in points]
+ min_count = min(counts)
+ max_count = max(counts)
+ span = max(max_count - min_count, 1)
+ y_min = max(0, min_count - max(1, span // 10))
+ y_span = max(max_count - y_min, 1)
+
+ def x(i):
+ return left + (plot_w * i / max(len(points) - 1, 1))
+
+ def y(value):
+ return top + plot_h - ((value - y_min) / y_span * plot_h)
+
+ polyline = " ".join(f"{x(i):.1f},{y(count):.1f}" for i, (_, count) in enumerate(points))
+ start_date = points[0][0].isoformat()
+ end_date = points[-1][0].isoformat()
+ latest = points[-1][1]
+
+ grid = []
+ labels = []
+ for i in range(5):
+ value = y_min + y_span * i / 4
+ yy = y(value)
+ grid.append(f'')
+ labels.append(f'{int(value):,}')
+
+ svg = f""""""
+ chart_path.write_text(svg, encoding="utf-8")
+ PY
+
+ - name: Commit updated statistics
+ shell: bash
+ run: |
+ set -euo pipefail
+ if git diff --quiet -- docs/ghcr-downloads.csv docs/ghcr-downloads.svg; then
+ echo "No statistics changes to commit."
+ exit 0
+ fi
+
+ git config user.name "github-actions[bot]"
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
+ git add docs/ghcr-downloads.csv docs/ghcr-downloads.svg
+ git commit -m "chore: update GHCR download statistics"
+ git push
diff --git a/docs/ghcr-downloads.md b/docs/ghcr-downloads.md
new file mode 100644
index 0000000..fafdb92
--- /dev/null
+++ b/docs/ghcr-downloads.md
@@ -0,0 +1,9 @@
+# GHCR download statistics
+
+
+
+This chart tracks the cumulative download count for `ghcr.io/gordonbeeming/copilot_here`.
+
+The [raw history](ghcr-downloads.csv) is updated daily by the [Track GHCR downloads workflow](../.github/workflows/track-ghcr-downloads.yml). The repository keeps a rolling 90-day history, so daily changes can also be derived from consecutive rows.
+
+> The history starts when the tracker first runs; GitHub does not provide historical daily pull data retroactively.