- commit
- cd00d78
- parent
- 10f0f90
- author
- jolheiser
- date
- 2025-04-17 17:07:03 -0400 EDT
single binary Reduce web and ssh to a single binary called git-pr that runs both the SSH and web servers at the same time Signed-off-by: jolheiser <git@jolheiser.com>
13 files changed,
+105,
-170
+1,
-1
1@@ -48,6 +48,6 @@
2 }
3
4 :443 {
5- reverse_proxy git-web:3000
6+ reverse_proxy git-pr:3000
7 encode zstd gzip
8 }
+6,
-29
1@@ -10,7 +10,7 @@ COPY go.* ./
2
3 RUN go mod download
4
5-FROM builder-deps as builder-web
6+FROM builder-deps as builder
7
8 COPY . .
9
10@@ -22,37 +22,14 @@ ENV LDFLAGS="-s -w"
11
12 ENV GOOS=${TARGETOS} GOARCH=${TARGETARCH}
13
14-RUN go build -ldflags "$LDFLAGS" -o /go/bin/git-web ./cmd/git-web
15+RUN go build -ldflags "$LDFLAGS" -o /go/bin/git-pr ./cmd/git-pr
16
17-FROM builder-deps as builder-ssh
18-
19-COPY . .
20-
21-ARG TARGETOS
22-ARG TARGETARCH
23-
24-ENV CGO_ENABLED=0
25-ENV LDFLAGS="-s -w"
26-
27-ENV GOOS=${TARGETOS} GOARCH=${TARGETARCH}
28-
29-RUN go build -ldflags "$LDFLAGS" -o /go/bin/git-ssh ./cmd/git-ssh
30-
31-FROM scratch as release-web
32-
33-WORKDIR /app
34-
35-COPY --from=builder-web /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
36-COPY --from=builder-web /go/bin/git-web ./git-web
37-
38-CMD ["/app/git-web"]
39-
40-FROM scratch as release-ssh
41+FROM scratch as release
42
43 WORKDIR /app
44 ENV TERM="xterm-256color"
45
46-COPY --from=builder-ssh /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
47-COPY --from=builder-ssh /go/bin/git-ssh ./git-ssh
48+COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
49+COPY --from=builder /go/bin/git-pr ./git-pr
50
51-CMD ["/app/git-ssh"]
52+CMD ["/app/git-pr"]
M
Makefile
+3,
-8
1@@ -21,8 +21,7 @@ snapshot:
2 .PHONY: snapshot
3
4 build:
5- go build -o ./build/git-ssh ./cmd/git-ssh
6- go build -o ./build/git-web ./cmd/git-web
7+ go build -o ./build/git-pr ./cmd/git-pr
8 .PHONY: build
9
10 bp-setup:
11@@ -30,12 +29,8 @@ bp-setup:
12 $(DOCKER_CMD) buildx use pico
13 .PHONY: bp-setup
14
15-bp-web: bp-setup
16- $(DOCKER_BUILDX_BUILD) -t "ghcr.io/picosh/pico/git-web:$(DOCKER_TAG)" --target release-web .
17-.PHONY: bp-web
18-
19-bp: bp-web
20- $(DOCKER_BUILDX_BUILD) -t "ghcr.io/picosh/pico/git-ssh:$(DOCKER_TAG)" --target release-ssh .
21+bp: bp-setup
22+ $(DOCKER_BUILDX_BUILD) -t "ghcr.io/picosh/pico/git-pr:$(DOCKER_TAG)" --target release-pr .
23 .PHONY: bp
24
25 deploy: bp-web
+3,
-13
1@@ -159,16 +159,10 @@ vim ./data/git-pr.toml
2
3 ## docker
4
5-Run the ssh app image:
6+Run the app image:
7
8 ```bash
9-docker run -d -v ./data:/app/data ghcr.io/picosh/pico/git-ssh:latest
10-```
11-
12-Run the web app image:
13-
14-```bash
15-docker run -d -v ./data:/app/data ghcr.io/picosh/pico/git-web:latest
16+docker run -d -v ./data:/app/data ghcr.io/picosh/pico/git-pr:latest
17 ```
18
19 ## golang
20@@ -180,11 +174,7 @@ make build
21 ```
22
23 ```bash
24-./build/ssh --config ./data/git-pr.toml
25-```
26-
27-```bash
28-./build/web --config ./data/git-pr.toml
29+./build/git-pr --config ./data/git-pr.toml
30 ```
31
32 ## done!
+57,
-0
1@@ -0,0 +1,57 @@
2+package main
3+
4+import (
5+ "context"
6+ "flag"
7+ "fmt"
8+ "log/slog"
9+ "net/http"
10+ "os"
11+ "os/signal"
12+ "syscall"
13+ "time"
14+
15+ git "github.com/picosh/git-pr"
16+)
17+
18+func main() {
19+ fpath := flag.String("config", "git-pr.toml", "configuration toml file")
20+ flag.Parse()
21+ opts := &slog.HandlerOptions{
22+ AddSource: true,
23+ }
24+ logger := slog.New(
25+ slog.NewTextHandler(os.Stdout, opts),
26+ )
27+ git.LoadConfigFile(*fpath, logger)
28+ cfg := git.NewGitCfg(logger)
29+
30+ // SSH Server
31+ ssh := git.GitSshServer(cfg)
32+ cfg.Logger.Info("starting SSH server", "host", cfg.Host, "port", cfg.SshPort)
33+ go func() {
34+ if err := ssh.ListenAndServe(); err != nil {
35+ cfg.Logger.Error("serve error", "err", err)
36+ }
37+ }()
38+
39+ // Web Server
40+ addr := fmt.Sprintf("%s:%s", cfg.Host, cfg.WebPort)
41+ web := git.GitWebServer(cfg)
42+ cfg.Logger.Info("starting web server", "addr", addr)
43+ go func() {
44+ if err := http.ListenAndServe(addr, web); err != nil {
45+ cfg.Logger.Error("listen", "err", err)
46+ }
47+ }()
48+
49+ done := make(chan os.Signal, 1)
50+ signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
51+ <-done
52+ cfg.Logger.Info("stopping SSH server")
53+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
54+ defer func() { cancel() }()
55+ if err := ssh.Shutdown(ctx); err != nil {
56+ cfg.Logger.Error("shutdown", "err", err)
57+ }
58+}
+0,
-22
1@@ -1,22 +0,0 @@
2-package main
3-
4-import (
5- "flag"
6- "log/slog"
7- "os"
8-
9- git "github.com/picosh/git-pr"
10-)
11-
12-func main() {
13- fpath := flag.String("config", "git-pr.toml", "configuration toml file")
14- flag.Parse()
15- opts := &slog.HandlerOptions{
16- AddSource: true,
17- }
18- logger := slog.New(
19- slog.NewTextHandler(os.Stdout, opts),
20- )
21- git.LoadConfigFile(*fpath, logger)
22- git.GitSshServer(git.NewGitCfg(logger), nil)
23-}
+0,
-22
1@@ -1,22 +0,0 @@
2-package main
3-
4-import (
5- "flag"
6- "log/slog"
7- "os"
8-
9- git "github.com/picosh/git-pr"
10-)
11-
12-func main() {
13- fpath := flag.String("config", "git-pr.toml", "configuration toml file")
14- flag.Parse()
15- opts := &slog.HandlerOptions{
16- AddSource: true,
17- }
18- logger := slog.New(
19- slog.NewTextHandler(os.Stdout, opts),
20- )
21- git.LoadConfigFile(*fpath, logger)
22- git.StartWebServer(git.NewGitCfg(logger))
23-}
+6,
-2
1@@ -4,6 +4,7 @@ import (
2 "flag"
3 "fmt"
4 "log/slog"
5+ "net/http"
6 "os"
7 "os/signal"
8 "syscall"
9@@ -37,9 +38,12 @@ func main() {
10 git.LoadConfigFile(cfgPath, logger)
11 cfg := git.NewGitCfg(logger)
12
13- go git.GitSshServer(cfg, nil)
14+ s := git.GitSshServer(cfg)
15+ go s.ListenAndServe()
16 time.Sleep(time.Millisecond * 100)
17- go git.StartWebServer(cfg)
18+ w := git.GitWebServer(cfg)
19+ addr := fmt.Sprintf("%s:%s", cfg.Host, cfg.WebPort)
20+ go http.ListenAndServe(addr, w)
21
22 // Hack to wait for startup
23 time.Sleep(time.Millisecond * 100)
+2,
-9
1@@ -18,19 +18,12 @@ services:
2 - "${GITPR_HTTP_V4:-80}:80"
3 - "${GITPR_HTTPS_V6:-[::1]:443}:443"
4 - "${GITPR_HTTP_V6:-[::1]:80}:80"
5- web:
6- command: "/app/git-web --config ${GITPR_CONFIG_PATH}"
7+ git-pr:
8+ command: "/app/git-pr --config ${GITPR_CONFIG_PATH}"
9 networks:
10 git:
11 aliases:
12 - web
13- env_file:
14- - .env.prod
15- ssh:
16- command: "/app/git-ssh --config ${GITPR_CONFIG_PATH}"
17- networks:
18- git:
19- aliases:
20 - ssh
21 env_file:
22 - .env.prod
+2,
-7
1@@ -1,11 +1,6 @@
2 services:
3- web:
4- image: ghcr.io/picosh/pico/git-web:latest
5- restart: always
6- volumes:
7- - ./data/git-pr/data:/app/data
8- ssh:
9- image: ghcr.io/picosh/pico/git-ssh:latest
10+ git-pr:
11+ image: ghcr.io/picosh/pico/git-pr:latest
12 restart: always
13 volumes:
14 - ./data/git-pr/data:/app/data
+7,
-6
1@@ -1,6 +1,7 @@
2 package git
3
4 import (
5+ "context"
6 "log/slog"
7 "os"
8 "testing"
9@@ -23,8 +24,8 @@ func testSingleTenantE2E(t *testing.T) {
10 os.RemoveAll(dataDir)
11 }()
12 suite := setupTest(dataDir, cfgSingleTenantTmpl)
13- done := make(chan error)
14- go GitSshServer(suite.cfg, done)
15+ s := GitSshServer(suite.cfg)
16+ go s.ListenAndServe()
17 // Hack to wait for startup
18 time.Sleep(time.Millisecond * 100)
19
20@@ -43,7 +44,7 @@ func testSingleTenantE2E(t *testing.T) {
21 bail(err)
22 snaps.MatchSnapshot(t, actual)
23
24- done <- nil
25+ s.Shutdown(context.Background())
26 }
27
28 func testMultiTenantE2E(t *testing.T) {
29@@ -53,8 +54,8 @@ func testMultiTenantE2E(t *testing.T) {
30 os.RemoveAll(dataDir)
31 }()
32 suite := setupTest(dataDir, cfgMultiTenantTmpl)
33- done := make(chan error)
34- go GitSshServer(suite.cfg, done)
35+ s := GitSshServer(suite.cfg)
36+ go s.ListenAndServe()
37
38 time.Sleep(time.Millisecond * 100)
39
40@@ -120,7 +121,7 @@ func testMultiTenantE2E(t *testing.T) {
41 bail(err)
42 snaps.MatchSnapshot(t, actual)
43
44- done <- nil
45+ s.Shutdown(context.Background())
46 }
47
48 type TestSuite struct {
M
ssh.go
+3,
-30
1@@ -1,13 +1,8 @@
2 package git
3
4 import (
5- "context"
6 "fmt"
7- "os"
8- "os/signal"
9 "path/filepath"
10- "syscall"
11- "time"
12
13 "github.com/charmbracelet/ssh"
14 "github.com/charmbracelet/wish"
15@@ -31,7 +26,7 @@ func authHandler(pr *PrCmd) func(ctx ssh.Context, key ssh.PublicKey) bool {
16 }
17 }
18
19-func GitSshServer(cfg *GitCfg, killCh chan error) {
20+func GitSshServer(cfg *GitCfg) *ssh.Server {
21 dbpath := filepath.Join(cfg.DataDir, "pr.db?_fk=on")
22 dbh, err := SqliteOpen("file:"+dbpath, cfg.Logger)
23 if err != nil {
24@@ -59,31 +54,9 @@ func GitSshServer(cfg *GitCfg, killCh chan error) {
25 GitPatchRequestMiddleware(be, prCmd),
26 ),
27 )
28-
29 if err != nil {
30 cfg.Logger.Error("could not create server", "err", err)
31- return
32- }
33-
34- done := make(chan os.Signal, 1)
35- signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
36- cfg.Logger.Info("starting SSH server", "host", cfg.Host, "port", cfg.SshPort)
37- go func() {
38- if err = s.ListenAndServe(); err != nil {
39- cfg.Logger.Error("serve error", "err", err)
40- // os.Exit(1)
41- }
42- }()
43-
44- select {
45- case <-done:
46- case <-killCh:
47- }
48- cfg.Logger.Info("stopping SSH server")
49- ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
50- defer func() { cancel() }()
51- if err := s.Shutdown(ctx); err != nil {
52- cfg.Logger.Error("shutdown", "err", err)
53- // os.Exit(1)
54+ return nil
55 }
56+ return s
57 }
M
web.go
+15,
-21
1@@ -1086,9 +1086,7 @@ func getEmbedFS(ffs embed.FS, dirName string) (fs.FS, error) {
2 return fsys, nil
3 }
4
5-func StartWebServer(cfg *GitCfg) {
6- addr := fmt.Sprintf("%s:%s", cfg.Host, cfg.WebPort)
7-
8+func GitWebServer(cfg *GitCfg) http.Handler {
9 dbpath := filepath.Join(cfg.DataDir, "pr.db?_fk=on")
10 dbh, err := SqliteOpen("file:"+dbpath, cfg.Logger)
11 if err != nil {
12@@ -1122,28 +1120,24 @@ func StartWebServer(cfg *GitCfg) {
13
14 // ensure legacy router is disabled
15 // GODEBUG=httpmuxgo121=0
16- http.HandleFunc("GET /prs/{id}", ctxMdw(ctx, createPrDetail("pr")))
17- http.HandleFunc("GET /prs/{id}/rss", ctxMdw(ctx, rssHandler))
18- http.HandleFunc("GET /ps/{id}", ctxMdw(ctx, createPrDetail("ps")))
19- http.HandleFunc("GET /rd/{id}", ctxMdw(ctx, createPrDetail("rd")))
20- http.HandleFunc("GET /r/{user}/{repo}/rss", ctxMdw(ctx, rssHandler))
21- http.HandleFunc("GET /r/{user}/{repo}", ctxMdw(ctx, repoDetailHandler))
22- http.HandleFunc("GET /r/{user}", ctxMdw(ctx, userDetailHandler))
23- http.HandleFunc("GET /rss/{user}", ctxMdw(ctx, rssHandler))
24- http.HandleFunc("GET /rss", ctxMdw(ctx, rssHandler))
25- http.HandleFunc("GET /", ctxMdw(ctx, indexHandler))
26- http.HandleFunc("GET /syntax.css", ctxMdw(ctx, chromaStyleHandler))
27+ mux := http.NewServeMux()
28+ mux.HandleFunc("GET /prs/{id}", ctxMdw(ctx, createPrDetail("pr")))
29+ mux.HandleFunc("GET /prs/{id}/rss", ctxMdw(ctx, rssHandler))
30+ mux.HandleFunc("GET /ps/{id}", ctxMdw(ctx, createPrDetail("ps")))
31+ mux.HandleFunc("GET /rd/{id}", ctxMdw(ctx, createPrDetail("rd")))
32+ mux.HandleFunc("GET /r/{user}/{repo}/rss", ctxMdw(ctx, rssHandler))
33+ mux.HandleFunc("GET /r/{user}/{repo}", ctxMdw(ctx, repoDetailHandler))
34+ mux.HandleFunc("GET /r/{user}", ctxMdw(ctx, userDetailHandler))
35+ mux.HandleFunc("GET /rss/{user}", ctxMdw(ctx, rssHandler))
36+ mux.HandleFunc("GET /rss", ctxMdw(ctx, rssHandler))
37+ mux.HandleFunc("GET /", ctxMdw(ctx, indexHandler))
38+ mux.HandleFunc("GET /syntax.css", ctxMdw(ctx, chromaStyleHandler))
39 embedFS, err := getEmbedFS(embedStaticFS, "static")
40 if err != nil {
41 panic(err)
42 }
43 userFS := getUserDefinedFS(cfg.DataDir, "static")
44
45- http.HandleFunc("GET /static/{file}", ctxMdw(ctx, serveFile(userFS, embedFS)))
46-
47- cfg.Logger.Info("starting web server", "addr", addr)
48- err = http.ListenAndServe(addr, nil)
49- if err != nil {
50- cfg.Logger.Error("listen", "err", err)
51- }
52+ mux.HandleFunc("GET /static/{file}", ctxMdw(ctx, serveFile(userFS, embedFS)))
53+ return mux
54 }