From 49da5b10c39c6dbf1c0cb96eb2d7892a491d8f71 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 8 Feb 2026 18:30:29 +0000 Subject: [PATCH] feat: add Go vanity import server and BugSETI CI pipeline Add dappco.re vanity import handler (cmd/vanity-import/) that serves go-import meta tags, enabling `go get dappco.re/core` to resolve to forge.lthn.ai/host-uk/core. Deployed as a Docker container behind Traefik on snider-linux. Add Woodpecker CI pipeline (.woodpecker/bugseti.yml) for BugSETI cross-platform builds. Phase 1: Linux amd64 with CGO, triggered on bugseti-v* tags and main branch pushes to cmd/bugseti/. Closes #3, closes #9 Co-Authored-By: Claude Opus 4.6 --- .woodpecker/bugseti.yml | 52 ++++++++++++++++++ cmd/vanity-import/Dockerfile | 9 +++ cmd/vanity-import/go.mod | 3 + cmd/vanity-import/main.go | 104 +++++++++++++++++++++++++++++++++++ 4 files changed, 168 insertions(+) create mode 100644 .woodpecker/bugseti.yml create mode 100644 cmd/vanity-import/Dockerfile create mode 100644 cmd/vanity-import/go.mod create mode 100644 cmd/vanity-import/main.go diff --git a/.woodpecker/bugseti.yml b/.woodpecker/bugseti.yml new file mode 100644 index 00000000..5e9387c9 --- /dev/null +++ b/.woodpecker/bugseti.yml @@ -0,0 +1,52 @@ +when: + - event: tag + ref: "refs/tags/bugseti-v*" + - event: push + branch: main + path: "cmd/bugseti/**" + +steps: + - name: frontend + image: node:22-bookworm + commands: + - cd cmd/bugseti/frontend + - npm ci --prefer-offline + - npm run build + + - name: build-linux + image: golang:1.25-bookworm + environment: + CGO_ENABLED: "1" + GOOS: linux + GOARCH: amd64 + commands: + - apt-get update -qq && apt-get install -y -qq libgtk-3-dev libwebkit2gtk-4.1-dev > /dev/null 2>&1 + - cd cmd/bugseti + - go build -tags production -trimpath -buildvcs=false -ldflags="-w -s" -o ../../bin/bugseti + depends_on: [frontend] + + - name: package + image: alpine:3.21 + commands: + - cd bin + - tar czf bugseti-linux-amd64.tar.gz bugseti + - sha256sum bugseti-linux-amd64.tar.gz > bugseti-linux-amd64.tar.gz.sha256 + - echo "=== Package ===" + - ls -lh bugseti-linux-amd64.* + - cat bugseti-linux-amd64.tar.gz.sha256 + depends_on: [build-linux] + + - name: release + image: plugins/gitea-release + settings: + api_key: + from_secret: forgejo_token + base_url: https://forge.lthn.ai + files: + - bin/bugseti-linux-amd64.tar.gz + - bin/bugseti-linux-amd64.tar.gz.sha256 + title: ${CI_COMMIT_TAG} + note: "BugSETI ${CI_COMMIT_TAG} — Linux amd64 build" + when: + - event: tag + depends_on: [package] diff --git a/cmd/vanity-import/Dockerfile b/cmd/vanity-import/Dockerfile new file mode 100644 index 00000000..163c42e8 --- /dev/null +++ b/cmd/vanity-import/Dockerfile @@ -0,0 +1,9 @@ +FROM golang:1.25-alpine AS build +WORKDIR /src +COPY go.mod main.go ./ +RUN go build -trimpath -ldflags="-w -s" -o /vanity-import . + +FROM alpine:3.21 +COPY --from=build /vanity-import /vanity-import +EXPOSE 8080 +ENTRYPOINT ["/vanity-import"] diff --git a/cmd/vanity-import/go.mod b/cmd/vanity-import/go.mod new file mode 100644 index 00000000..e046ca81 --- /dev/null +++ b/cmd/vanity-import/go.mod @@ -0,0 +1,3 @@ +module dappco.re/vanity-import + +go 1.25.6 diff --git a/cmd/vanity-import/main.go b/cmd/vanity-import/main.go new file mode 100644 index 00000000..c6e40220 --- /dev/null +++ b/cmd/vanity-import/main.go @@ -0,0 +1,104 @@ +// Package main provides a Go vanity import server for dappco.re. +// +// When a Go tool requests ?go-get=1, this server responds with HTML +// containing tags that map dappco.re module +// paths to their Git repositories on forge.lthn.ai. +// +// For browser requests (no ?go-get=1), it redirects to the Forgejo +// repository web UI. +package main + +import ( + "fmt" + "log" + "net/http" + "os" + "strings" +) + +var modules = map[string]string{ + "core": "host-uk/core", + "build": "host-uk/build", +} + +const ( + forgeBase = "https://forge.lthn.ai" + vanityHost = "dappco.re" + defaultAddr = ":8080" +) + +func main() { + addr := os.Getenv("ADDR") + if addr == "" { + addr = defaultAddr + } + + // Allow overriding forge base URL + forge := os.Getenv("FORGE_URL") + if forge == "" { + forge = forgeBase + } + + // Parse additional modules from VANITY_MODULES env (format: "mod1=owner/repo,mod2=owner/repo") + if extra := os.Getenv("VANITY_MODULES"); extra != "" { + for _, entry := range strings.Split(extra, ",") { + parts := strings.SplitN(strings.TrimSpace(entry), "=", 2) + if len(parts) == 2 { + modules[parts[0]] = parts[1] + } + } + } + + http.HandleFunc("/", handler(forge)) + + log.Printf("vanity-import listening on %s (%d modules)", addr, len(modules)) + for mod, repo := range modules { + log.Printf(" %s/%s → %s/%s.git", vanityHost, mod, forge, repo) + } + log.Fatal(http.ListenAndServe(addr, nil)) +} + +func handler(forge string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Extract the first path segment as the module name + path := strings.TrimPrefix(r.URL.Path, "/") + if path == "" { + // Root request — redirect to forge org page + http.Redirect(w, r, forge+"/host-uk", http.StatusFound) + return + } + + // Module is the first path segment (e.g., "core" from "/core/pkg/mcp") + mod := strings.SplitN(path, "/", 2)[0] + + repo, ok := modules[mod] + if !ok { + http.NotFound(w, r) + return + } + + // If go-get=1, serve the vanity import HTML + if r.URL.Query().Get("go-get") == "1" { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + fmt.Fprintf(w, ` + + + + + + + +Redirecting to %s/%s... + + +`, vanityHost, mod, forge, repo, + vanityHost, mod, forge, repo, forge, repo, forge, repo, + forge, repo, + forge, repo, forge, repo) + return + } + + // Browser request — redirect to Forgejo + http.Redirect(w, r, forge+"/"+repo, http.StatusFound) + } +}