Files
ghglance/docs/deployment-guide.md
tiennm99 537c87c458 feat(web): show cards inline for quick viewing and accept the visitor's own token
The web UI is for a quick look at a profile, not for hosting embeddable
images. Cards are inlined into /u/<user> as data: images, the per-card
route is gone, and the page no longer offers copy links or Markdown
snippets. The page itself stays shareable.

Visitors can again paste their own token, with a button that opens
GitHub's new-token page with the needed scopes ticked. A pasted token
takes precedence over sign-in, follows the same ownership, privacy and
cooldown rules, is never stored or logged, and is not revoked. The server
still uses no token of its own.
2026-10-07 19:15:15 +07:00

8.6 KiB
Raw Permalink Blame History

Deployment Guide

Three consumption paths: GitHub Action, prebuilt binaries, go install. The same binary also runs as a self-hosted web UI (section 4).

Workflow template

File: .github/workflows/ghglance.yml in your profile repo.

name: ghglance

on:
  schedule:
    - cron: "0 0 * * *"        # daily at 00:00 UTC
  workflow_dispatch:

permissions:
  contents: write              # needed for commit_changes

jobs:
  cards:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: tiennm99/ghglance@v1
        with:
          user: ${{ github.repository_owner }}
          token: ${{ secrets.GHGLANCE_TOKEN }}
          themes: dracula,github_dark,tokyonight
          tz: Asia/Saigon
          # start_of_week: monday   # optional; default sunday — rotates heatmap rows + weekday bars
          include_forks: "false"
          include_private: "false"
          commit_changes: "true"

Required secrets

GHGLANCE_TOKEN: a classic personal access token with at minimum:

Scope Needed for
read:user Basic profile fields, contribution calendar
repo Only if include_private: "true"

Fine-grained PATs and the default ${{ github.token }} lack the introspection scope for contribution calendars in many orgs, so a classic PAT is recommended.

Create one at https://github.com/settings/tokens → "Generate new token (classic)" → select read:user (+ repo if needed) → save as repo secret GHGLANCE_TOKEN.

Embedding in README

![profile](./output/dracula/profile-details.svg)
![repos-per-language](./output/dracula/repos-per-language.svg)
![most-commit-language](./output/dracula/most-commit-language.svg)
![stats](./output/dracula/stats.svg)
![productive-time](./output/dracula/productive-time.svg)
![productive-weekday](./output/dracula/productive-weekday.svg)
![contributions](./output/dracula/contributions.svg)
![contributions-heatmap](./output/dracula/contributions-heatmap.svg)
![top-starred-repos](./output/dracula/top-starred-repos.svg)
![streak](./output/dracula/streak.svg)
![most-commit-language-all-time](./output/dracula/most-commit-language-all-time.svg)
![productive-time-all-time](./output/dracula/productive-time-all-time.svg)
![productive-weekday-all-time](./output/dracula/productive-weekday-all-time.svg)
![contributions-all-time](./output/dracula/contributions-all-time.svg)
![contributions-by-year](./output/dracula/contributions-by-year.svg)

The Action commits SVGs to output/<theme>/ on the default branch. GitHub serves them from the raw URL the README references.

2. Prebuilt binaries

Each tag under v* publishes:

  • Linux amd64, arm64
  • macOS amd64, arm64
  • Windows amd64

Released via .github/workflows/release.yml which matrixes GOOS × GOARCH, strips symbols (-ldflags="-s -w"), and uploads tar.gz / zip to the GitHub Release.

Install:

# Linux x86_64 example
curl -L https://github.com/tiennm99/ghglance/releases/latest/download/ghglance_linux_amd64.tar.gz \
  | tar xz
./ghglance -user YOUR_USERNAME

3. go install

go install github.com/tiennm99/ghglance@latest

Requires Go 1.26+. Puts the binary in $(go env GOPATH)/bin.

4. Web UI (Docker Compose / Coolify)

compose.yml at the repo root builds the Dockerfile, overrides its entrypoint to run ghglance -serve :8080 -data-dir /data, stores cards in the ghglance-data volume, and health-checks /healthz with busybox wget. It publishes no host port; Coolify routes the domain generated for SERVICE_FQDN_GHGLANCE_8080 to container port 8080.

Variable Needed for
GHGLANCE_OAUTH_CLIENT_ID Required. Client ID of the GitHub OAuth App behind "Sign in with GitHub" (-oauth-client-id).
GHGLANCE_OAUTH_CLIENT_SECRET Required. That OAuth App's client secret (-oauth-client-secret). Never logged or printed.
GHGLANCE_PUBLIC_URL Required. The site's external origin, e.g. https://ghglance.sg.miti99.com (-public-url). The OAuth App's callback URL must be exactly <public-url>/auth/callback.

The server holds no GitHub token: every generation runs on the visitor's token, either from Sign in with GitHub (revoked when the job ends) or one they paste into the form (used once, never stored, not revoked). The OAuth App is still required. No token variable exists. The site is for quick viewing only: cards are inlined into /u/<user> and have no URL of their own. compose.yml refuses to start while any of the three variables is empty, and -serve exits with an error naming the missing settings.

Coolify: register an OAuth App (GitHub Settings > Developer settings > OAuth Apps > New OAuth App; homepage URL = the domain, authorization callback URL = <domain>/auth/callback) and generate a client secret. Then create a Docker Compose resource from this repo, compose file /compose.yml, set the three variables, assign the domain, and deploy; the startup log prints the callback URL it uses. Server flags (-cooldown, -retention, -workers, -timeout) are changed by editing command: in compose.yml. Steps for a plain Docker host and the request-handling rules are in the README's "Run the web UI" section.

Rollback: redeploy the previous commit. Card sets on the volume are format-stable, so no data migration is involved.

Docker image

Published to ghcr.io/tiennm99/ghglance:<tag> on each v* release via .github/workflows/release.yml (buildx, multi-tag: exact version, major.minor, major, latest).

The Action itself uses a runner-built image by default (image: Dockerfile in action.yml). To switch to the pre-built image for faster cold starts, edit action.yml:

runs:
  using: docker
  image: docker://ghcr.io/tiennm99/ghglance:v1

Release process

  1. Tag: git tag -a v1.2.0 -m "..." && git push origin v1.2.0.
  2. release.yml runs go vet + go test as a gate before the docker and binaries jobs. If tests fail, no artifacts ship.
  3. On green, GHCR push + cross-platform binary artifacts happen automatically.
  4. The update-major-tag job force-moves the floating major tag (e.g. v1) to this release's commit after test + docker + binaries all pass. Consumers pinned to tiennm99/ghglance@v1 pick up the release on their next Action run without a workflow edit.
  5. Docker base images and third-party actions are SHA-pinned (with version comments) so mutable-tag changes upstream can't rewrite a released image.
  6. Marketplace: the action is listed on the GitHub Marketplace as ghglance; the listing name comes from name: in action.yml. Publishing is a web-UI step — edit the GitHub release and tick "Publish this Action to the GitHub Marketplace" — since action-gh-release cannot publish it.

Rollback

  • Revert the tag: git push --delete origin v1.2.0, delete GitHub release, delete GHCR tag.
  • Users pinned to @v1 keep working because the previous patch is still tagged.

Rate limit considerations

Scenario GraphQL calls per run Notes
Typical user, defaults 15–40 Well under 5000 pts/hr
Active user (8 years, 30+ seed repos) 40–80 Still comfortable
-include-private=true with 100+ work repos 80–200 Fine for daily cron
Adversarial user with 500+ committed repos/year Capped by maxRepositories: 100 per year query Long tail drops silently

No REST calls today. Future -accurate-languages mode will push toward 1000+ REST per run; schedule that mode less frequently (weekly, not daily).

The client auto-handles rate-limit responses: on 429 or 403 with X-RateLimit-Remaining: 0, it sleeps up to 5 minutes (honoring Retry-After / X-RateLimit-Reset) and retries once. A reset window longer than 5 min surfaces as an error so CI can reschedule instead of burning runner time. Use the -timeout flag (default 30m) to cap total fetch duration; SIGINT/SIGTERM cancels in-flight requests cleanly.

Troubleshooting

Symptom Check
"error: fetch profile: graphql: Could not resolve to a User" Username typo
"http 401" Token expired or lacks read:user
"rate limit resets in 42m (>5m0s max wait)" Client refused to sleep through a long window; reschedule the Action
"http 403" on non-rate-limit path PAT scope too narrow
Blank contribution chart User has 0 contributions in their window; expected
Private repo data missing -include-private=true not set, or PAT lacks repo
Nothing committed by the Action Check permissions: contents: write in the workflow