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 <noreply@anthropic.com>
This commit is contained in:
Claude 2026-02-08 18:30:29 +00:00
parent d2916db640
commit 4b179b2c94
No known key found for this signature in database
GPG key ID: AF404715446AEB41
4 changed files with 168 additions and 0 deletions

52
.woodpecker/bugseti.yml Normal file
View file

@ -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]

View file

@ -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"]

3
cmd/vanity-import/go.mod Normal file
View file

@ -0,0 +1,3 @@
module dappco.re/vanity-import
go 1.25.6

104
cmd/vanity-import/main.go Normal file
View file

@ -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 <meta name="go-import"> 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, `<!DOCTYPE html>
<html>
<head>
<meta name="go-import" content="%s/%s git %s/%s.git">
<meta name="go-source" content="%s/%s %s/%s %s/%s/src/branch/main{/dir} %s/%s/src/branch/main{/dir}/{file}#L{line}">
<meta http-equiv="refresh" content="0; url=%s/%s">
</head>
<body>
Redirecting to <a href="%s/%s">%s/%s</a>...
</body>
</html>
`, 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)
}
}