From b2ed330475a97c4cdd6631512db83ece9e3203b2 Mon Sep 17 00:00:00 2001 From: claude-ceo-assistant Date: Thu, 7 May 2026 22:23:51 +0000 Subject: [PATCH] fix(post-suspension): vanity import path go.moleculesai.app/plugin/gh-identity (closes molecule-ai/internal#71 phase 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migrates go.mod + the single Go import + a stale go.mod replace-comment + README require example off the dead github.com/Molecule-AI/ identity and onto the vanity host go.moleculesai.app, owned by us. Adds a structural lint test that walks every *.go / *.mod / Dockerfile / *.md and rejects future re-introduction of either the literal github.com/Molecule-AI/ string or the historical Molecule-AI/molecule-monorepo path. Why a vanity host and not git.moleculesai.app/molecule-ai/...: the latter just relocates the lock-in. 2026-05-06 didn't teach us 'Gitea > GitHub'; it taught us 'vendor URLs in source code are a future incident'. With go.moleculesai.app, the next SCM migration edits one config (the responder) instead of every import statement. See internal#71 for the full rationale + the alternatives rejected. Smoke-validation for this migration sweep — chosen as the first PR because it has the smallest blast radius (1 import line). Following PRs: molecule-cli, molecule-controlplane, molecule-core (parallel after this lands). Test plan: - go build ./... — clean - go test ./... — internal/ghidentity green - TestNoLegacyGitHubImportPaths — passes on clean tree, FAILS on injected canary string (mutation-tested before commit) Open dependency: go.moleculesai.app responder must be deployed before external 'go install go.moleculesai.app/plugin/gh-identity@latest' works. Internal builds are unaffected (self-referential module path, no external fetch needed). Responder code prepared in worker.js; deploy tracked separately. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 2 +- go.mod | 4 +- pluginloader/import_path_lint_test.go | 145 ++++++++++++++++++++++++++ pluginloader/pluginloader.go | 2 +- 4 files changed, 149 insertions(+), 4 deletions(-) create mode 100644 pluginloader/import_path_lint_test.go diff --git a/README.md b/README.md index db2e2ae..c001c51 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ manifest rename: Monorepo side: ``` manifest.json:plugins += {name: "gh-identity", repo: "Molecule-AI/molecule-ai-plugin-gh-identity", ref: "main"} -workspace-server/go.mod: require github.com/Molecule-AI/molecule-ai-plugin-gh-identity +workspace-server/go.mod: require go.moleculesai.app/plugin/gh-identity workspace-server/cmd/server/main.go: pluginloader.BuildRegistry() ``` diff --git a/go.mod b/go.mod index 72d124a..9ea48c2 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/Molecule-AI/molecule-ai-plugin-gh-identity +module go.moleculesai.app/plugin/gh-identity go 1.25.0 @@ -9,7 +9,7 @@ require gopkg.in/yaml.v3 v3.0.1 // is needed. If we ever need to reference exported types from // molecule-monorepo/platform, uncomment: // -// replace github.com/Molecule-AI/molecule-monorepo/platform => ../molecule-monorepo/workspace-server +// replace go.moleculesai.app/core/platform => ../molecule-core/workspace-server // // Keeping this out of the require list lets the plugin build standalone in CI // without checking out the monorepo. diff --git a/pluginloader/import_path_lint_test.go b/pluginloader/import_path_lint_test.go new file mode 100644 index 0000000..f047db7 --- /dev/null +++ b/pluginloader/import_path_lint_test.go @@ -0,0 +1,145 @@ +// Issue molecule-ai/internal#71 lint gate. +// +// Walks every *.go file in the module + the go.mod declaration + any +// Dockerfile in the repo, and rejects any reference to the dead +// github.com/Molecule-AI/* identity (or the historical +// Molecule-AI/molecule-monorepo path). +// +// We had a 374+131+30+1-line "github.com/Molecule-AI/" footprint across +// the org pre-migration. The class of bug this gate prevents: +// +// - copy-pastes from old branches re-introducing the dead path +// - Dockerfile -ldflags strings drifting back to github.com on a +// refactor (the path has to match the module declaration to inject +// buildinfo correctly; if they disagree the binary builds but +// reports a wrong / stale GitSHA) +// - new modules added to the repo with the wrong import root because +// someone copied an old go.mod without thinking +// +// Why not just a CI shell grep: a Go test runs everywhere `go test ./...` +// runs, including local pre-push hooks and contributor IDEs. The gate +// fires immediately, with a per-file message that points at the line — +// CI shell grep failures are silent until the runner picks them up. + +package pluginloader + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +// forbiddenSubstrings is the literal-match list. Each string MUST NOT +// appear anywhere under the module root. Entries are checked with +// substring matching, not regex — keep the patterns specific enough +// that a false-positive needs an explicit allowlist entry. +var forbiddenSubstrings = []string{ + "github.com/Molecule-AI/", + "Molecule-AI/molecule-monorepo", +} + +// allowlistedFiles is the per-file escape hatch. Empty by default — +// add an entry only when there is a documented reason a forbidden +// string MUST appear (e.g. a regression-test fixture that asserts +// the lint gate itself rejects the string). Each entry MUST be +// accompanied by a comment explaining why. +var allowlistedFiles = map[string]bool{ + // (intentionally empty — add only with justification) +} + +func TestNoLegacyGitHubImportPaths(t *testing.T) { + moduleRoot, err := findModuleRoot() + if err != nil { + t.Fatalf("findModuleRoot: %v", err) + } + + checkExt := map[string]bool{ + ".go": true, + ".mod": true, + ".sum": false, // go.sum is auto-generated, refs flow from go.mod + ".sh": true, + ".yml": true, + ".yaml": true, + ".toml": true, + ".md": true, + } + checkBasename := map[string]bool{ + "Dockerfile": true, + "Dockerfile.tenant": true, + } + + violations := 0 + walkErr := filepath.Walk(moduleRoot, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + // Skip vendor + .git + node_modules — not our code. + base := info.Name() + if base == "vendor" || base == ".git" || base == "node_modules" || base == "testdata" { + return filepath.SkipDir + } + return nil + } + ext := filepath.Ext(path) + base := filepath.Base(path) + if !checkExt[ext] && !checkBasename[base] { + return nil + } + rel, _ := filepath.Rel(moduleRoot, path) + if allowlistedFiles[rel] { + return nil + } + // Skip the lint test itself — it legitimately names the forbidden + // strings as match patterns. + if strings.HasSuffix(rel, "import_path_lint_test.go") { + return nil + } + data, err := os.ReadFile(path) + if err != nil { + return err + } + text := string(data) + for _, bad := range forbiddenSubstrings { + if strings.Contains(text, bad) { + // Find the line number for a useful error message. + for lineNo, line := range strings.Split(text, "\n") { + if strings.Contains(line, bad) { + t.Errorf("%s:%d — forbidden substring %q (use go.moleculesai.app//... per molecule-ai/internal#71)", rel, lineNo+1, bad) + violations++ + break + } + } + } + } + return nil + }) + if walkErr != nil { + t.Fatalf("walk: %v", walkErr) + } + if violations > 0 { + t.Logf("Total violations: %d. Add to allowlistedFiles ONLY with a documented justification.", violations) + } +} + +// findModuleRoot walks up from the test's CWD to find go.mod. The Go +// test harness sets CWD to the package directory; the module root may +// be one or more parents up. +func findModuleRoot() (string, error) { + cwd, err := os.Getwd() + if err != nil { + return "", err + } + dir := cwd + for { + if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { + return dir, nil + } + parent := filepath.Dir(dir) + if parent == dir { + return "", os.ErrNotExist + } + dir = parent + } +} diff --git a/pluginloader/pluginloader.go b/pluginloader/pluginloader.go index 0bf711a..a057617 100644 --- a/pluginloader/pluginloader.go +++ b/pluginloader/pluginloader.go @@ -19,7 +19,7 @@ import ( "fmt" "os" - "github.com/Molecule-AI/molecule-ai-plugin-gh-identity/internal/ghidentity" + "go.moleculesai.app/plugin/gh-identity/internal/ghidentity" ) // Result bundles what BuildRegistry returns — a single mutator plus