feat(ci): core-side platformPaths drift gate (#820) #2924

Closed
agent-dev-a wants to merge 2 commits from feat/820-platform-paths-drift-gate into main
2 changed files with 241 additions and 0 deletions
@@ -0,0 +1,46 @@
# cf-tunnel-platform-paths-drift
#
# Prod-safety gate (molecule-controlplane#820): every top-level route in
# molecule-core/workspace-server/internal/router/router.go must be covered by a
# regex in molecule-controlplane/internal/cloudflareapi/tunnel.go's
# platformPaths list. If a core PR adds a tenant route without a matching
# platformPaths regex, production tenants will misroute it to canvas:3000.
#
# This workflow checks out molecule-controlplane (public repo) and runs the
# core-side drift test, catching the drift at the source rather than relying on
# the reactive controlplane gate.
name: cf-tunnel-platform-paths-drift
on:
push:
branches: [main, staging]
pull_request:
branches: [main, staging]
env:
GITHUB_SERVER_URL: https://git.moleculesai.app
jobs:
core-platform-paths-drift:
name: Core routes covered by platformPaths
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: checkout molecule-controlplane
# Manual git clone because actions/checkout@v6 fails on cross-repo
# public clones when the auto-injected GITEA_TOKEN is scoped to this
# repo only. molecule-controlplane is public — no token needed.
run: |
git clone --depth 1 https://git.moleculesai.app/molecule-ai/molecule-controlplane.git _molecule-controlplane
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: '1.25'
cache: false
- name: drift gate
env:
MOLECULE_CP_DIR: ${{ github.workspace }}/_molecule-controlplane
run: go test -count=1 -v -run TestCoreRoutesCoveredByPlatformPaths ./workspace-server/internal/router/
@@ -0,0 +1,195 @@
package router
import (
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"testing"
)
// TestCoreRoutesCoveredByPlatformPaths is the molecule-core side of the
// cross-repo drift gate for cf-tunnel platformPaths (molecule-controlplane#820).
// It ensures that every top-level route registered in this router is matched by
// at least one regex in the controlplane's platformPaths list. Drift is caught
// in the dedicated workflow (which clones molecule-controlplane); in the
// standard CI Go job the sibling checkout is absent, so the test skips.
//
// The hard-fail only fires when the operator explicitly configured
// MOLECULE_CP_DIR but the file is unreadable — that signals a broken checkout
// and must not pass silently.
func TestCoreRoutesCoveredByPlatformPaths(t *testing.T) {
cpTunnelPath := findControlPlaneTunnel(t)
if cpTunnelPath == "" {
if os.Getenv("MOLECULE_CP_DIR") != "" {
t.Fatal("MOLECULE_CP_DIR is set but molecule-controlplane tunnel.go is not readable; " +
"the drift gate checkout is broken")
}
t.Skip("molecule-controlplane tunnel.go not found; " +
"set MOLECULE_CP_DIR to enable cross-repo drift gate")
}
t.Logf("loading controlplane platformPaths from %s", cpTunnelPath)
cpSrc, err := os.ReadFile(cpTunnelPath)
if err != nil {
t.Fatalf("read %s: %v", cpTunnelPath, err)
}
platformPaths := extractPlatformPaths(string(cpSrc))
if len(platformPaths) == 0 {
t.Fatalf("no platformPaths extracted from %s — extractor is broken", cpTunnelPath)
}
t.Logf("extracted %d platformPaths regex(es)", len(platformPaths))
platformRegexes := make([]*regexp.Regexp, 0, len(platformPaths))
for _, p := range platformPaths {
re, err := regexp.Compile(p)
if err != nil {
t.Fatalf("platformPaths regex %q failed to compile: %v", p, err)
}
platformRegexes = append(platformRegexes, re)
}
routerPath := "router.go"
if _, err := os.Stat(routerPath); err != nil {
t.Fatalf("cannot read own router.go at %s: %v", routerPath, err)
}
routerSrc, err := os.ReadFile(routerPath)
if err != nil {
t.Fatalf("read %s: %v", routerPath, err)
}
routes := extractGinRoutes(string(routerSrc))
if len(routes) == 0 {
t.Fatalf("no routes extracted from %s — extractor is broken", routerPath)
}
t.Logf("extracted %d route(s) from molecule-core router", len(routes))
var unmatched []string
for _, route := range routes {
if !routeMatchesAny(route, platformRegexes) {
unmatched = append(unmatched, route)
}
}
if len(unmatched) > 0 {
sort.Strings(unmatched)
t.Errorf("\n=== platformPaths drift detected ===\n"+
"%d molecule-core route(s) are NOT matched by any controlplane platformPaths regex.\n"+
"They will silently misroute to canvas:3000 in production tenants.\n\n"+
"Unmatched routes:\n - %s\n\n"+
"Fix: add a matching regex to platformPaths in molecule-controlplane/internal/cloudflareapi/tunnel.go,\n"+
"then add the same regex to the wantPlatformPaths lockstep test in tunnel_test.go.\n",
len(unmatched),
strings.Join(unmatched, "\n - "))
}
}
// findControlPlaneTunnel resolves the path to molecule-controlplane's
// internal/cloudflareapi/tunnel.go using the search order documented on the
// test. Returns "" if no candidate exists.
func findControlPlaneTunnel(t *testing.T) string {
t.Helper()
rel := filepath.Join("internal", "cloudflareapi", "tunnel.go")
candidates := []string{}
if env := os.Getenv("MOLECULE_CP_DIR"); env != "" {
candidates = append(candidates, filepath.Join(env, rel))
}
if home, err := os.UserHomeDir(); err == nil {
candidates = append(candidates, filepath.Join(home, "molecule-controlplane", rel))
}
candidates = append(candidates, filepath.Join("..", "..", "molecule-controlplane", rel))
candidates = append(candidates, filepath.Join("..", "molecule-controlplane", rel))
for _, c := range candidates {
if _, err := os.Stat(c); err == nil {
return c
}
}
return ""
}
// extractPlatformPaths parses tunnel.go source and returns the platformPaths
// regex slice as it would be pushed to cloudflared.
func extractPlatformPaths(src string) []string {
startMarker := "platformPaths := []string{"
start := strings.Index(src, startMarker)
if start < 0 {
return nil
}
start += len(startMarker)
end := strings.Index(src[start:], "}")
if end < 0 {
return nil
}
block := src[start : start+end]
// Each entry is a backtick-quoted Go raw string.
re := regexp.MustCompile("`([^`]*)`")
var out []string
for _, m := range re.FindAllStringSubmatch(block, -1) {
out = append(out, m[1])
}
return out
}
// extractGinRoutes parses router.go source and returns every concrete
// top-level path registered on the gin router, with group prefixes resolved.
//
// Adapted from molecule-controlplane/internal/cloudflareapi/cross_repo_drift_test.go
// which performs the inverse check. The extractor is regex-based and sufficient
// for molecule-core's structure where Groups don't nest and prefixes are static
// literals.
func extractGinRoutes(src string) []string {
groupRe := regexp.MustCompile(`(\w+)\s*:=\s*r\.Group\(\s*"([^"]*)"`)
groupMap := make(map[string]string) // varname → prefix
for _, m := range groupRe.FindAllStringSubmatch(src, -1) {
groupMap[m[1]] = m[2]
}
methodRe := regexp.MustCompile(
`(\w+)\.(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS|Handle)\(\s*` +
`(?:"[^"]*"\s*,\s*)?` + // Handle("METHOD", "/path") form
`"([^"]*)"`)
seen := make(map[string]struct{})
for _, m := range methodRe.FindAllStringSubmatch(src, -1) {
recv := m[1]
path := m[3]
full := ""
switch {
case recv == "r":
full = path
default:
if prefix, ok := groupMap[recv]; ok {
full = prefix + path
} else {
continue
}
}
if full == "" {
continue
}
if i := strings.IndexAny(full, "?#"); i >= 0 {
full = full[:i]
}
seen[full] = struct{}{}
}
out := make([]string, 0, len(seen))
for p := range seen {
out = append(out, p)
}
sort.Strings(out)
return out
}
func routeMatchesAny(route string, patterns []*regexp.Regexp) bool {
for _, re := range patterns {
if re.MatchString(route) {
return true
}
}
return false
}