Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/build-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ jobs:
org.opencontainers.image.created=${{ steps.meta.outputs.build_time }}
build-args: |
VERSION=${{ steps.meta.outputs.version }}
COMMIT=${{ steps.meta.outputs.sha_short }}
BRANCH=${{ github.ref_name }}
BUILD_TIME=${{ steps.meta.outputs.build_time }}
BUILD_NUMBER=${{ github.run_number }}
cache-from: type=gha
cache-to: type=gha,mode=max
# Keeps the package listing to the two real platforms instead of
Expand Down
14 changes: 13 additions & 1 deletion .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ jobs:
type=ref,event=branch
type=sha,format=short

# What the status page's build popover shows, alongside the version.
- name: Prepare build metadata
id: build
run: |
echo "sha_short=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
echo "build_time=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> "$GITHUB_OUTPUT"

- name: Build and push
uses: docker/build-push-action@v6
with:
Expand All @@ -49,7 +56,12 @@ jobs:
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: VERSION=${{ steps.meta.outputs.version }}
build-args: |
VERSION=${{ steps.meta.outputs.version }}
COMMIT=${{ steps.build.outputs.sha_short }}
BRANCH=${{ github.ref_name }}
BUILD_TIME=${{ steps.build.outputs.build_time }}
BUILD_NUMBER=${{ github.run_number }}
cache-from: type=gha
cache-to: type=gha,mode=max
# Keeps the package listing to the two real platforms instead of
Expand Down
10 changes: 9 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,19 @@ COPY cmd ./cmd
COPY internal ./internal

ARG VERSION=dev
# Provenance for the status page's build popover. All optional: an unset one
# just leaves that row off the page.
ARG COMMIT=
ARG BRANCH=
ARG BUILD_TIME=
ARG BUILD_NUMBER=
ARG TARGETOS
ARG TARGETARCH
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build \
-trimpath \
-ldflags "-s -w -X main.version=${VERSION}" \
-ldflags "-s -w -X main.version=${VERSION} -X main.commit=${COMMIT} \
-X main.branch=${BRANCH} -X main.buildTime=${BUILD_TIME} \
-X main.buildNumber=${BUILD_NUMBER}" \
-o /out/gh-proxy ./cmd/gh-proxy

# scratch: no shell, no package manager, nothing to pivot to if the proxy is
Expand Down
15 changes: 13 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo dev)
LDFLAGS := -s -w -X main.version=$(VERSION)
# The rest of what the status page's build popover shows. BUILD_NUMBER is a CI
# run number and stays empty locally; the page hides the rows it has no value for.
COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null)
BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD 2>/dev/null)
BUILD_TIME ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
BUILD_NUMBER ?=
LDFLAGS := -s -w -X main.version=$(VERSION) -X main.commit=$(COMMIT) \
-X main.branch=$(BRANCH) -X main.buildTime=$(BUILD_TIME) \
-X main.buildNumber=$(BUILD_NUMBER)

.PHONY: build test race vet lint tidy run docker up down logs token clean

Expand Down Expand Up @@ -32,7 +40,10 @@ run: build
GHP_STATUS_PATH=$$STATUS ./bin/gh-proxy

docker:
docker build --build-arg VERSION=$(VERSION) -t gh-proxy:$(VERSION) -t gh-proxy:latest .
docker build --build-arg VERSION=$(VERSION) --build-arg COMMIT=$(COMMIT) \
--build-arg BRANCH=$(BRANCH) --build-arg BUILD_TIME=$(BUILD_TIME) \
--build-arg BUILD_NUMBER=$(BUILD_NUMBER) \
-t gh-proxy:$(VERSION) -t gh-proxy:latest .

up:
docker compose up -d --build
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,15 @@ GHP_STATUS_PATH=/ghp-status # off unless this is set
`/ghp-status/json` (what the page polls, every 5 s) and `/ghp-status/metrics`
(Prometheus text format) beside it.

The header carries the running version; clicking it opens the build behind it —
branch, commit, build time, CI run number — next to the repository's star count
and a refresh button for when five seconds is too long to wait. The version goes
amber and pulses when a newer release exists. Those two numbers are the only
thing on the page that comes from outside, and the *browser* fetches them
straight from api.github.com, cached for half an hour: the proxy itself makes no
such call, and a viewer who cannot reach GitHub gets the whole page anyway,
without the counts.

The path is a full path from the site root rather than something under
`GHP_PREFIX`, so it is one `location` for the reverse proxy to guard:

Expand Down
8 changes: 8 additions & 0 deletions README.ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,14 @@ GHP_STATUS_PATH=/ghp-status # выключена, пока это не зад
ней — `/ghp-status/json` (её и опрашивает сама страница, раз в 5 с) и
`/ghp-status/metrics` (текстовый формат Prometheus).

В шапке — версия, с которой запущен процесс; по клику раскрывается сборка за ней:
ветка, коммит, время сборки, номер прогона CI. Рядом счётчик звёзд репозитория и
кнопка обновления, когда ждать пять секунд до следующего опроса долго. Версия
становится жёлтой и слегка пульсирует, если вышел релиз новее. Эти два числа —
единственное на странице, что берётся снаружи, и запрашивает их *браузер* прямо
у api.github.com с получасовым кэшем: сам прокси никуда не ходит, а тот, у кого
нет доступа к GitHub, получает страницу целиком, только без счётчиков.

Путь задаётся от корня сайта, а не внутри `GHP_PREFIX`, — чтобы у reverse-proxy
был ровно один `location`, который можно закрыть:

Expand Down
35 changes: 29 additions & 6 deletions cmd/gh-proxy/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,31 @@ import (
"github.com/prettyleaf/gh-proxy/internal/status"
)

// version is overridden at build time with -ldflags "-X main.version=...".
var version = "dev"
// Build metadata, all of it overridden at build time with
// -ldflags "-X main.<name>=...". Everything but the version is empty in a plain
// `go build`, and the status page hides the rows it has no value for.
var (
version = "dev"
commit = ""
branch = ""
buildTime = "" // RFC 3339, UTC
buildNumber = "" // CI run number
// repo is the GitHub repository the status page's header links to, for the
// star count and the latest release. A fork overrides it the same way.
repo = "prettyleaf/gh-proxy"
)

// buildInfo is what the header's version chip and its popover display.
func buildInfo() status.Build {
return status.Build{
Version: version,
Commit: commit,
Branch: branch,
Time: buildTime,
Number: buildNumber,
Repo: repo,
}
}

func main() {
// The scratch-based image has no shell and no curl, so the binary probes
Expand Down Expand Up @@ -97,7 +120,7 @@ func run() error {

public := &http.Server{
Addr: cfg.Listen,
Handler: server.New(cfg, p, m, log),
Handler: server.New(cfg, p, m, buildInfo(), log),
// No WriteTimeout on purpose: it is an absolute deadline on the whole
// response, which would sever long release downloads and idle git
// fetches. ReadHeaderTimeout covers the slowloris case instead.
Expand All @@ -108,7 +131,7 @@ func run() error {

admin := &http.Server{
Addr: cfg.AdminListen,
Handler: adminHandler(m, info),
Handler: adminHandler(m, info, buildInfo()),
ReadHeaderTimeout: 5 * time.Second,
ErrorLog: slog.NewLogLogger(log.Handler(), slog.LevelDebug),
}
Expand Down Expand Up @@ -167,14 +190,14 @@ func serve(s *http.Server, name string, errCh chan<- error) {
// The status page and the metrics live here unconditionally: this listener is
// bound to loopback, so reaching it already means being on the host. The public
// copy under GHP_STATUS_PATH is the one that has to be configured on purpose.
func adminHandler(m *metrics.Metrics, info status.Info) http.Handler {
func adminHandler(m *metrics.Metrics, info status.Info, b status.Build) http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
_, _ = fmt.Fprintf(w, "ok %s\n", version)
})

h := status.New(m, info)
h := status.New(m, info, b)
// Both patterns, so ServeMux answers "/status" itself instead of bouncing
// it to "/status/" with a redirect.
mux.Handle("/status", http.StripPrefix("/status", h))
Expand Down
7 changes: 4 additions & 3 deletions internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@ type Server struct {
}

// New builds the public handler. m may be nil, in which case nothing is
// counted and the status page is not served.
func New(cfg *config.Config, p *proxy.Proxy, m *metrics.Metrics, log *slog.Logger) *Server {
// counted and the status page is not served. b is the build metadata the status
// page's header displays.
func New(cfg *config.Config, p *proxy.Proxy, m *metrics.Metrics, b status.Build, log *slog.Logger) *Server {
if log == nil {
log = slog.Default()
}
Expand All @@ -46,7 +47,7 @@ func New(cfg *config.Config, p *proxy.Proxy, m *metrics.Metrics, log *slog.Logge
if m != nil && cfg.StatusPath != "" {
// Mounted with its own path stripped, so the handler only ever sees
// "", "/json" or "/metrics" and does not need to know where it lives.
s.status = http.StripPrefix(cfg.StatusPath, status.New(m, StatusInfo(cfg)))
s.status = http.StripPrefix(cfg.StatusPath, status.New(m, StatusInfo(cfg), b))
}
return s
}
Expand Down
3 changes: 2 additions & 1 deletion internal/server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/prettyleaf/gh-proxy/internal/metrics"
"github.com/prettyleaf/gh-proxy/internal/proxy"
"github.com/prettyleaf/gh-proxy/internal/server"
"github.com/prettyleaf/gh-proxy/internal/status"
)

const (
Expand Down Expand Up @@ -125,7 +126,7 @@ func newHandler(t *testing.T, f *fakeGitHub, mutate func(*config.Config)) http.H
BaseTransport: f.transport(),
Logger: discardLogger(),
})
return server.New(cfg, p, metrics.New("test"), discardLogger())
return server.New(cfg, p, metrics.New("test"), status.Build{Version: "test"}, discardLogger())
}

// do runs one request through the handler.
Expand Down
Loading