diff --git a/.github/workflows/pages.yaml b/.github/workflows/pages.yaml new file mode 100644 index 0000000..00808e1 --- /dev/null +++ b/.github/workflows/pages.yaml @@ -0,0 +1,33 @@ +name: Deploy the product page to Pages + +on: + push: + branches: ['main'] + paths: + - 'docs/**' + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +# One deployment at a time. Do not cancel a deployment that is running. +concurrency: + group: pages + cancel-in-progress: false + +jobs: + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/configure-pages@v5 + - uses: actions/upload-pages-artifact@v3 + with: + path: docs/ + - id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index cc0463f..39de51f 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -7,6 +7,8 @@ on: - main paths-ignore: - '**.md' + - 'docs/**' + - '.github/workflows/pages.yaml' - '.github/workflows/ci.yaml' permissions: diff --git a/docs/CNAME b/docs/CNAME new file mode 100644 index 0000000..b963563 --- /dev/null +++ b/docs/CNAME @@ -0,0 +1 @@ +ghtracker.raczylo.com \ No newline at end of file diff --git a/docs/ghtracker-hero-dark.png b/docs/ghtracker-hero-dark.png new file mode 100644 index 0000000..88bd5fd Binary files /dev/null and b/docs/ghtracker-hero-dark.png differ diff --git a/docs/ghtracker-hero-light.png b/docs/ghtracker-hero-light.png new file mode 100644 index 0000000..1631d2d Binary files /dev/null and b/docs/ghtracker-hero-light.png differ diff --git a/docs/ghtracker-logo-dark.svg b/docs/ghtracker-logo-dark.svg new file mode 100644 index 0000000..bc62b12 --- /dev/null +++ b/docs/ghtracker-logo-dark.svg @@ -0,0 +1,4 @@ + + + ghtracker + diff --git a/docs/ghtracker-logo-light.svg b/docs/ghtracker-logo-light.svg new file mode 100644 index 0000000..e74a6e7 --- /dev/null +++ b/docs/ghtracker-logo-light.svg @@ -0,0 +1,4 @@ + + + ghtracker + diff --git a/docs/ghtracker-screenshot-dark.png b/docs/ghtracker-screenshot-dark.png new file mode 100644 index 0000000..375d474 Binary files /dev/null and b/docs/ghtracker-screenshot-dark.png differ diff --git a/docs/ghtracker-screenshot-light.png b/docs/ghtracker-screenshot-light.png new file mode 100644 index 0000000..9de5b15 Binary files /dev/null and b/docs/ghtracker-screenshot-light.png differ diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..8fbca9f --- /dev/null +++ b/docs/index.html @@ -0,0 +1,903 @@ + + + + + +ghtracker: see which GitHub repositories need attention + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+
+

GitHub repository dashboard

+

See which of your GitHub repositories need attention

+

ghtracker scans the repositories you list through a GitHub App. It shows only what is wrong: failing workflows, pull requests that wait too long and stale issues. One binary serves the dashboard and Prometheus metrics.

+
Docker
docker run --rm -p 8080:8080 \
+  -v "$PWD/config.yaml:/config/config.yaml:ro" \
+  -e GHTRACKER_PRIVATE_KEY="$(cat app.pem)" \
+  ghcr.io/lukaszraczylo/ghtracker:latest
+

Binaries, Kubernetes and building from source: Install. You need a GitHub App first: Set up.

+ +
+
Licence
MIT
+
Language
Go and Vue
+
Platforms
Linux, macOS, Windows
+
Container image
amd64, arm64
+
+
+
+
+ + + ghtracker dashboard: 10 critical alerts and 1 warning across 20 projects, a health strip with one segment per project, and failing workflows and pull requests grouped by repository + +
+

Example data: 20 public repositories of the author, scanned in October 2026.

+
+
+
+ + +
+
+
+

Dashboard

+

Healthy repositories stay out of the way

+

A long list of green items hides the red ones. ghtracker lists only items that need attention, grouped by repository, and keeps one line per repository in a side panel for everything else.

+
+
+ + + ghtracker dashboard: 10 critical alerts and 1 warning across 20 projects, a health strip with one segment per project, and failing workflows and pull requests grouped by repository + +
The same page at desktop width, with 20 public repositories of the author.
+
+
    +
  1. Headline. The count of critical items and warnings, and how many projects they come from.
  2. +
  3. Health strip. One segment per project, red, amber or green. Select a segment to focus that project. Segments that do not match the current search or filters dim.
  4. +
  5. Five figures. Workflows failing, pull requests waiting, pull requests with failing checks, stale issues and data problems.
  6. +
  7. Needs attention. One block per repository with its latest release. Each row links to the workflow run, pull request or issue, and shows its age. A meter beside a waiting pull request fills towards the critical age.
  8. +
  9. Projects. Every project with its latest release and age, and counts of workflows, pull requests and issues, each linked to GitHub.
  10. +
+

The page uses the full width, works on phones and follows the colour scheme of the system. While the first scan runs it shows how many repositories are done.

+
+
+ + +
+
+
+

Features

+

What ghtracker does

+

Every snippet below comes from the repository: the source, config.example.yaml or the Kubernetes example.

+
+ +
+
+

Workflows

+

Failing workflows, including background ones

+

For every active workflow ghtracker takes the newest run on the default branch. A failed run is critical. Scheduled, dynamic and manually started workflows carry a background mark, because nobody is watching them run.

+

Pull request runs appear as checks on the pull request instead. A workflow that has not run for 30 days is muted, so an old failure does not raise an alert for ever.

+
+
+
Example rows
critical  lukaszraczylo/git-velocity
+Git Velocity Analysis (background)
+failure on main (schedule)             4d
+
+critical  lukaszraczylo/gohoarder
+Release
+failure on main (push)                29h
+
+
+ +
+
+

Pull requests

+

Pull requests that wait too long

+

A non-draft pull request is a warning after 48 hours and critical after 7 days. Drafts are ignored. A pull request with failing checks is a warning, and its check names appear in the row.

+

Set the limits with thresholds. A negative value turns a check off.

+
+
+
config.yaml
thresholds:
+  pr_warn_after: 48h
+  pr_crit_after: 168h
+  issue_stale_after: 720h
+  workflow_stuck_after: 6h
+  workflow_dormant_after: 720h
+
+
+ +
+
+

Search and filters

+

Search, and filters that show their own counts

+

Type to match projects, versions, titles and details. Every word must match. Press / to jump to the search box and Esc to clear it.

+

Filter by severity, kind (workflows, pull requests, issues, data) and owner. Each chip shows how many items it would leave given the other filters, and a chip that would match nothing is disabled. The address holds the filters, so you can share a view.

+
+
+
Address of a filtered view
https://ghtracker.example.com/?q=bump&kind=pr&owner=lukaszraczylo&sev=crit
+

Search for "bump", pull requests only, one owner, critical only.

+
+
+ +
+
+

Releases

+

Latest release of every project, one click away

+

Each project shows its newest GitHub release with a link and its age. A project without a release shows its newest tag. The same data is available as the metric ghtracker_release_info.

+
+
+
Example series
ghtracker_release_info{repo="lukaszraczylo/kportal",
+  version="v0.11.2",
+  url="https://github.com/lukaszraczylo/kportal/releases/tag/v0.11.2"} 1
+ghtracker_release_published_timestamp_seconds{repo="lukaszraczylo/kportal"} 1.790906487e+09
+
+
+ +
+
+

Metrics

+

Prometheus metrics for alerting

+

Metrics are computed when you scrape, so a pull request age keeps growing between scans. Per-repository gauges cover health, open pull requests and issues, workflow state and the age of the oldest pull request.

+

The Kubernetes example ships alert rules for failing workflows, pull requests waiting over 3 days, repositories that fail to scan, stale scans and a low GitHub API budget.

+
+
+
Prometheus rules
- alert: GhtrackerWorkflowFailing
+  expr: ghtracker_workflow_failing == 1
+  for: 10m
+- alert: GhtrackerPRWaiting
+  expr: ghtracker_pr_age_seconds > 3 * 86400
+- alert: GhtrackerStale
+  expr: time() - ghtracker_last_refresh_timestamp_seconds > 13 * 3600
+
+
+ +
+
+

GitHub API

+

Careful with your rate limit

+
    +
  • Conditional requests with ETag. A 304 reply does not count against the primary limit.
  • +
  • Requests stop while fewer than 100 remain, until the window resets.
  • +
  • On a primary or secondary limit response ghtracker waits for Retry-After or the reset time, stops the scan and keeps the last good data.
  • +
  • Two repositories at a time by default. Pagination stops at max_items.
  • +
+
+
+
Cost of one scan
requests per scan =
+  6 per repository
++ 2 per open pull request
++ 1 per active workflow
+
+63 repositories take about 100 seconds
+and a few hundred requests
+

Measured on 63 repositories with the default settings. The installation limit of a GitHub App is at least 5,000 requests per hour.

+
+
+ +
+
+

Accuracy

+

A failure is confirmed before it alerts

+

GitHub sometimes answers a workflow run listing from a lagging replica. One answer held 135 runs ending three weeks ago while the current listing held 414. A client that reuses its connection can keep hitting that replica.

+

ghtracker asks again on a new connection, without its response cache, for every failing workflow. It keeps the union of both answers, so the newest run decides.

+
+
+
Example from a real repository
stale answer     runs: 135   newest: 09-11  failure
+current answer   runs: 414   newest: 10-02  success
+
+shown after the check:  success
+
+
+ +
+
+

On demand

+

Rescan with one button

+

The Rescan repositories button starts a new scan and follows it, so the new data appears as soon as the scan ends. One rescan every 5 minutes is allowed, and the server refuses requests that come from other sites. Set manual_refresh: false to turn the button off.

+
+
+
Same thing from a shell
curl -X POST http://localhost:8080/refresh
+

Answers 202. A second call within 5 minutes answers 429.

+
+
+ +
+
+

One binary

+

The dashboard is inside the binary

+

The Vue app is built into the Go binary, so there is nothing else to host. The container image is a static binary on a distroless base that runs as a non-root user. Release checksums and image manifests are signed with cosign.

+
+
+
Command line
ghtracker -version
+ghtracker -config config.yaml
+
+
+ +
+
+

Kubernetes

+

Example manifests for a cluster

+

The repository has a Kustomize base with a Deployment, a Service, a ConfigMap, an External Secrets resource for the private key, a Traefik route, a ServiceMonitor and a PrometheusRule. Keep one replica, because each replica scans GitHub on its own.

+
+
+
Review before you apply
kustomize build deploy/kubernetes | kubectl diff -f -
+

The dashboard has no login of its own. Put it behind your ingress authentication.

+
+
+
+
+ + +
+
+
+

Alerts

+

How ghtracker decides what is wrong

+

Alerts are evaluated every time you read the page or scrape the metrics, so ages keep advancing between scans. Draft pull requests and archived repositories raise no work alerts.

+
+
+ + + + + + + + + + + + + +
ConditionSeveritySetting
The newest completed default-branch run of a workflow failedcriticalthresholds.workflow_dormant_after mutes workflows idle longer than this
A scan of a repository failed. The dashboard keeps the last good data.critical
A non-draft pull request is older than the critical agecriticalthresholds.pr_crit_after, default 168h
A non-draft pull request is older than the warning agewarningthresholds.pr_warn_after, default 48h
The checks of a pull request failwarning
A workflow is queued or running for too longwarningthresholds.workflow_stuck_after, default 6h
Open issues without activity. One alert for each repository.warningthresholds.issue_stale_after, default 720h
A section failed to load, for example a missing App permissionwarning
The data of a repository is older than expectedwarningthresholds.data_stale_after, default twice the interval plus 30 minutes
+
+

Durations use Go syntax such as 48h. There is no d unit.

+
+
+ + +
+
+
+

Install

+

Install ghtracker

+

Pick one method. Release archives cover Linux, macOS and Windows on amd64 and arm64.

+
+ +
+
+

Docker

+
docker run --rm -p 8080:8080 \
+  -v "$PWD/config.yaml:/config/config.yaml:ro" \
+  -e GHTRACKER_PRIVATE_KEY="$(cat app.pem)" \
+  ghcr.io/lukaszraczylo/ghtracker:latest
+

The config must set listen: ":8080" so the port is reachable from outside the container. Tags are the release version, for example 0.2.3, and latest.

+
+ +
+

Release archive

+

The releases page has one archive per platform, named ghtracker-<os>-<arch>.tar.gz (.zip on Windows). Each holds the binary, the licence, the README and config.example.yaml.

+
tar -xzf ghtracker-linux-amd64.tar.gz
+./ghtracker -config config.yaml
+
+ +
+

Kubernetes

+

Copy deploy/kubernetes/ from the repository, set your App ID, repositories, secret store and host, then review the result before you apply it.

+
git clone https://github.com/lukaszraczylo/ghtracker.git
+kustomize build ghtracker/deploy/kubernetes | kubectl diff -f -
+

If your cluster uses GitOps, commit the manifests and let your controller apply them.

+
+ +
+

Build from source

+
git clone https://github.com/lukaszraczylo/ghtracker.git
+cd ghtracker
+make build
+

The build needs Go 1.25 or later, Node 22.13 or later and pnpm 11. make build installs the UI packages, builds the UI into dist/ and then builds the Go binary, which embeds dist/. Without the UI build the server answers 503 at /.

+
+ +
+

Verify a release signature

+

Release checksums are signed with cosign using keyless signing. Download the checksums file and its sigstore bundle from the release, then run:

+
cosign verify-blob \
+  --certificate-identity-regexp "^https://github\.com/lukaszraczylo/shared-actions/\.github/workflows/go-release\.yaml@refs/heads/main$" \
+  --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
+  --bundle ghtracker-checksums.txt.sigstore.json \
+  ghtracker-checksums.txt
+

Then check the archive against the verified checksums file with shasum -a 256 or sha256sum.

+
+
+
+
+ + +
+
+
+

Set up

+

From install to the first dashboard

+

ghtracker signs in as a GitHub App. An App can only read what you grant it and you can install it on a user account or an organisation.

+
+
    +
  1. Create a GitHub App with read-only repository permissions: Metadata, Contents, Issues, Pull requests, Actions, Checks and Commit statuses. It needs no webhook and no write access.
  2. +
  3. Install the App on every account or organisation whose repositories you list. Choose all repositories, or select the ones you want.
  4. +
  5. Generate a private key for the App and keep the .pem file safe.
  6. +
  7. Copy config.example.yaml to config.yaml, set github.app_id and list your repositories.
  8. +
  9. Give ghtracker the key through github.private_key_file or the environment variable GHTRACKER_PRIVATE_KEY, then start it and open http://localhost:8080.
  10. +
+
config.yaml
listen: ":8080"
+refresh_interval: 6h
+github:
+  app_id: 123456
+  private_key_file: /secrets/ghtracker.pem
+  installation_id: 0        # 0 = find the installation for each repository
+repos:
+  - lukaszraczylo/kubemirror
+  - lukaszraczylo/semver-generator
+
+

Public and private repositories. GitHub lets any installation read public data, so public repositories work even with fewer permissions. Private repositories need every permission above, and each installation must accept the update after you add one.

+
+
+
+ + +
+
+
+

Metrics

+

Prometheus metrics

+

All metrics use the ghtracker_ prefix and are computed at scrape time. A scrape interval of one minute is enough. Reading the metrics costs no GitHub requests.

+
+
+ + + + + + + + + + + + + + + + + +
MetricLabelsMeaning
repo_up, repo_healthrepoLast scan succeeded. Worst alert: 0 ok, 1 warning, 2 critical.
repo_open_issues, repo_open_prsrepoOpen issues and open pull requests, including drafts.
repo_open_prs_failing_checks, repo_stale_issuesrepoPull requests with failing checks. Issues without recent activity.
repo_oldest_pr_age_secondsrepoAge of the oldest non-draft pull request.
pr_age_seconds, pr_checks_failingrepo, numberOne series per non-draft pull request.
workflow_failing, workflow_runningrepo, workflow, backgroundThe newest completed run failed. A run is in progress.
workflow_last_run_timestamp_secondsrepo, workflow, backgroundWhen the newest default-branch run started.
release_inforepo, version, urlLatest release or tag. The value is always 1.
release_published_timestamp_secondsrepoWhen the latest release was published.
alertsseverityActive alerts of each severity.
refreshes_total, repo_refresh_errors_totalCompleted scans. Scan failures of single repositories.
last_refresh_timestamp_seconds, last_refresh_duration_secondsWhen the last scan started and how long it took.
github_rate_limit_remainingRequests left in the current GitHub window.
+
+
+
+ + +
+
+
+

Reference

+

Reference

+
+ +

Command line and environment

+
+ + + + + + + +
NameEffect
-config <path>Path of the YAML config file. Default config.yaml.
-versionPrint the version and exit.
GHTRACKER_PRIVATE_KEYThe App private key as PEM text. It takes precedence over github.private_key_file.
+
+ +

Configuration keys

+
+ + + + + + + + + + + + + + + +
KeyDefaultMeaning
listen:8080Address of the HTTP server.
refresh_interval6hTime between scans. The minimum is 1m.
manual_refreshtrueThe rescan button and POST /refresh.
github.app_idRequired. The numeric ID of the GitHub App.
github.private_key_filePath of the private key. Required unless GHTRACKER_PRIVATE_KEY is set.
github.installation_id0Use one installation for every repository, or 0 to find the installation of each repository.
github.api_urlhttps://api.github.comAPI address. GitHub Enterprise Server has not been tested.
thresholds.*see AlertsAges and durations that raise alerts. A negative value disables a check.
max_items100Most issues and pull requests read for each repository.
concurrency2Repositories scanned at the same time.
reposRequired. A list of owner/name.
+
+ +

HTTP endpoints

+
+ + + + + + + + + +
PathAnswer
/The dashboard.
/api/stateThe state as JSON: repositories, alerts, thresholds and scan progress.
/healthz200 after the first scan finished, 503 before.
/metricsPrometheus metrics.
POST /refreshStarts a scan. Allowed once every 5 minutes. Same-site requests only.
+
+

The dashboard keeps its data in memory. After a restart it scans again, and the page shows the progress.

+
+
+ + +
+
+
+

Troubleshooting

+

Common failures

+
+
+
+

HTTP 403: Resource not accessible by integration

+

The row says Incomplete data for a private repository. The App lacks a permission. Grant read access to Issues, Actions, Checks and Commit statuses as well as the others in Set up, then accept the update on every installation. Restart ghtracker, because installation tokens last an hour.

+

github rate limit reached

+

ghtracker stopped the scan on purpose and keeps the last good data. It tries again at the next scan. Lower the number of repositories, raise refresh_interval or lower concurrency. The metric ghtracker_github_rate_limit_remaining shows the budget.

+
+
+

The page says "Scanning repositories" for a long time

+

The first scan has to read every repository. Sixty repositories take about two minutes. The page shows how many are done and the container is ready before the scan ends.

+

UI is not built

+

The server answers 503 at / when you built the binary without the UI. Run make build, or pnpm build before go build.

+

Config errors at start

+

ghtracker names the problem and exits, for example repos must list at least one owner/name, github.app_id is required or refresh_interval 30s is below the 1m0s minimum.

+
+
+
+
+ + +
+
+
+

FAQ

+

Questions and answers

+
+
Does ghtracker change anything on GitHub?

No. The client sends only GET requests, apart from the request that exchanges the App key for an installation token. Give the App read-only permissions.

+
Does it need webhooks?

No. It polls on a schedule, so it works behind a firewall and needs no public address.

+
Why every six hours?

It is the default and keeps the cost of a scan low. Set refresh_interval down to one minute, or use the rescan button when you need fresh data.

+
Can I run more than one replica?

It is not useful. Each replica scans GitHub on its own and spends API budget, and the data lives in memory.

+
Is there a login?

No. Put the dashboard behind a reverse proxy or an identity provider.

+
Which repositories can it read?

Any repository that an installation of your App can access, public or private. A repository outside every installation fails its scan and shows as critical.

+
What is the licence?

MIT.

+
+
+ +
+ + + + +