Skip to content

Commit bb0f9db

Browse files
authored
Merge pull request #17 from githubnext/autoloop/python-to-go-migration
[Autoloop: python-to-go-migration]
2 parents 37d17e4 + a78e47d commit bb0f9db

35 files changed

Lines changed: 2959 additions & 1 deletion

Makefile

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,19 @@
11
# Minimal Makefile -- DX shortcut for the NOTICE-file generator.
22
# Add other targets here as the project grows.
33

4-
.PHONY: notice notice-check
4+
.PHONY: notice notice-check go-build go-test go-vet
5+
6+
# Build the Go binary.
7+
go-build:
8+
go build -o bin/apm-go ./cmd/apm
9+
10+
# Run Go tests.
11+
go-test:
12+
go test ./...
13+
14+
# Run Go vet.
15+
go-vet:
16+
go vet ./...
517

618
# Regenerate NOTICE from pyproject.toml + scripts/notice-metadata.yaml.
719
# Run this whenever you add / remove / bump a runtime dependency.

benchmarks/migration-status.json

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
{
2+
"original_python_lines": 71696,
3+
"migrated_python_lines": 4245,
4+
"migrated_modules": [
5+
{
6+
"module": "src/apm_cli/constants.py",
7+
"go_package": "internal/constants",
8+
"python_lines": 55,
9+
"status": "migrated",
10+
"notes": "Pure constants and enum - no external dependencies"
11+
},
12+
{
13+
"module": "src/apm_cli/version.py",
14+
"go_package": "internal/version",
15+
"python_lines": 101,
16+
"status": "migrated",
17+
"notes": "Version resolution from build constants or pyproject.toml"
18+
},
19+
{
20+
"module": "src/apm_cli/utils/short_sha.py",
21+
"go_package": "internal/utils/sha",
22+
"python_lines": 45,
23+
"status": "migrated",
24+
"notes": "Short SHA formatter with sentinel and hex validation"
25+
},
26+
{
27+
"module": "src/apm_cli/utils/paths.py",
28+
"go_package": "internal/utils/paths",
29+
"python_lines": 27,
30+
"status": "migrated",
31+
"notes": "Cross-platform relative path utility"
32+
},
33+
{
34+
"module": "src/apm_cli/utils/normalization.py",
35+
"go_package": "internal/utils/normalization",
36+
"python_lines": 57,
37+
"status": "migrated",
38+
"notes": "Content normalization: BOM, CRLF, build-ID header stripping"
39+
},
40+
{
41+
"module": "src/apm_cli/utils/yaml_io.py",
42+
"go_package": "internal/utils/yamlio",
43+
"python_lines": 55,
44+
"status": "migrated",
45+
"notes": "YAML I/O with UTF-8; stdlib-only implementation"
46+
},
47+
{
48+
"module": "src/apm_cli/utils/atomic_io.py",
49+
"go_package": "internal/utils/atomicio",
50+
"python_lines": 52,
51+
"status": "migrated",
52+
"notes": "Atomic file write via temp+rename, same-filesystem rename"
53+
},
54+
{
55+
"module": "src/apm_cli/utils/git_env.py",
56+
"go_package": "internal/utils/gitenv",
57+
"python_lines": 97,
58+
"status": "migrated",
59+
"notes": "Cached git lookup and subprocess env sanitization"
60+
},
61+
{
62+
"module": "src/apm_cli/utils/guards.py",
63+
"go_package": "internal/utils/guards",
64+
"python_lines": 123,
65+
"status": "migrated",
66+
"notes": "ReadOnlyProjectGuard with snapshot-based mutation detection"
67+
},
68+
{
69+
"module": "src/apm_cli/utils/subprocess_env.py",
70+
"go_package": "internal/utils/subprocenv",
71+
"python_lines": 84,
72+
"status": "migrated",
73+
"notes": "PyInstaller env restoration; stdlib-only; MapToSlice helper"
74+
},
75+
{
76+
"module": "src/apm_cli/utils/helpers.py",
77+
"go_package": "internal/utils/helpers",
78+
"python_lines": 131,
79+
"status": "migrated",
80+
"notes": "IsToolAvailable, GetAvailablePackageManagers, DetectPlatform, FindPluginJSON"
81+
},
82+
{
83+
"module": "src/apm_cli/utils/content_hash.py",
84+
"go_package": "internal/utils/contenthash",
85+
"python_lines": 108,
86+
"status": "migrated",
87+
"notes": "Deterministic SHA-256 tree hashing; excludes .apm-pin marker and .git/__pycache__"
88+
},
89+
{
90+
"module": "src/apm_cli/utils/exclude.py",
91+
"go_package": "internal/utils/exclude",
92+
"python_lines": 169,
93+
"status": "migrated",
94+
"notes": "Glob pattern matching with ** support; bounded recursion; safety limit on ** count"
95+
},
96+
{
97+
"module": "src/apm_cli/utils/path_security.py",
98+
"go_package": "internal/utils/pathsecurity",
99+
"python_lines": 130,
100+
"status": "migrated",
101+
"notes": "Path traversal guards; iterative percent-decode; EnsurePathWithin; SafeRmtree"
102+
},
103+
{
104+
"module": "src/apm_cli/utils/version_checker.py",
105+
"go_package": "internal/utils/versionchecker",
106+
"python_lines": 193,
107+
"status": "migrated",
108+
"notes": "GitHub API version check; parse_version; is_newer_version; once-per-day cache"
109+
},
110+
{
111+
"module": "src/apm_cli/utils/file_ops.py",
112+
"go_package": "internal/utils/fileops",
113+
"python_lines": 326,
114+
"status": "migrated",
115+
"notes": "Retry-aware rmtree/copytree/copy2; exponential backoff; Windows AV-lock detection"
116+
},
117+
{
118+
"module": "src/apm_cli/utils/console.py",
119+
"go_package": "internal/utils/console",
120+
"python_lines": 224,
121+
"status": "migrated",
122+
"notes": "STATUS_SYMBOLS; RichEcho/Success/Error/Warning/Info; ANSI colour with NO_COLOR guard"
123+
},
124+
{
125+
"module": "src/apm_cli/utils/diagnostics.py",
126+
"go_package": "internal/utils/diagnostics",
127+
"python_lines": 486,
128+
"status": "migrated",
129+
"notes": "DiagnosticCollector; thread-safe; grouped RenderSummary; all category constants"
130+
},
131+
{
132+
"module": "src/apm_cli/utils/install_tui.py",
133+
"go_package": "internal/utils/installtui",
134+
"python_lines": 365,
135+
"status": "migrated",
136+
"notes": "InstallTui; deferred spinner (250ms); ShouldAnimate TTY check; phase/task tracking"
137+
},
138+
{
139+
"module": "src/apm_cli/utils/github_host.py",
140+
"go_package": "internal/utils/githubhost",
141+
"python_lines": 624,
142+
"status": "migrated",
143+
"notes": "Host classification (github/ghes/ghe_com/gitlab/ado/artifactory); GHES precedence; FQDN validation"
144+
},
145+
{
146+
"module": "src/apm_cli/utils/reflink.py",
147+
"go_package": "internal/utils/reflink",
148+
"python_lines": 281,
149+
"status": "migrated",
150+
"notes": "CoW reflink via FICLONE ioctl (Linux); device capability cache; regularCopy fallback"
151+
},
152+
{
153+
"module": "src/apm_cli/install/errors.py",
154+
"go_package": "internal/install/errors",
155+
"python_lines": 113,
156+
"status": "migrated",
157+
"notes": "DirectDependencyError, AuthenticationError, FrozenInstallError, PolicyViolationError"
158+
},
159+
{
160+
"module": "src/apm_cli/install/cache_pin.py",
161+
"go_package": "internal/install/cachepin",
162+
"python_lines": 233,
163+
"status": "migrated",
164+
"notes": "WriteMarker (silent on failures); VerifyMarker (typed CachePinError); schema v1"
165+
},
166+
{
167+
"module": "src/apm_cli/install/context.py",
168+
"go_package": "internal/install/installctx",
169+
"python_lines": 166,
170+
"status": "migrated",
171+
"notes": "InstallContext dataclass -> Go struct; all maps/slices initialised in New()"
172+
}
173+
],
174+
"last_updated": "2026-05-13T00:52:00Z",
175+
"iteration": 13
176+
}

bin/apm-go

2.63 MB
Binary file not shown.

cmd/apm/main.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Package main is the APM CLI Go entry point.
2+
// This is a stub that will grow as more Python modules are migrated.
3+
package main
4+
5+
import (
6+
"fmt"
7+
"os"
8+
9+
"github.com/githubnext/apm/internal/version"
10+
)
11+
12+
func main() {
13+
if len(os.Args) > 1 && os.Args[1] == "version" {
14+
fmt.Println(version.GetVersion())
15+
return
16+
}
17+
fmt.Fprintln(os.Stderr, "apm-go: stub binary (migration in progress)")
18+
os.Exit(1)
19+
}

internal/constants/constants.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Package constants defines shared constants for the APM CLI.
2+
// Migrated from src/apm_cli/constants.py
3+
package constants
4+
5+
// InstallMode controls which dependency types are installed.
6+
type InstallMode string
7+
8+
const (
9+
InstallModeAll InstallMode = "all"
10+
InstallModeAPM InstallMode = "apm"
11+
InstallModeMCP InstallMode = "mcp"
12+
)
13+
14+
// File and directory names.
15+
const (
16+
APMYMLFilename = "apm.yml"
17+
APMLockFilename = "apm.lock"
18+
APMModulesDir = "apm_modules"
19+
APMDir = ".apm"
20+
SkillMDFilename = "SKILL.md"
21+
AgentsMDFilename = "AGENTS.md"
22+
ClaudeMDFilename = "CLAUDE.md"
23+
GitHubDir = ".github"
24+
ClaudeDir = ".claude"
25+
GitignoreFilename = ".gitignore"
26+
APMModulesGitignorePattern = "apm_modules/"
27+
)
28+
29+
// DefaultSkipDirs lists directory names unconditionally skipped during
30+
// primitive-file discovery. These never contain APM primitives and can
31+
// be very large (e.g. node_modules, .git objects).
32+
var DefaultSkipDirs = map[string]struct{}{
33+
".git": {},
34+
"node_modules": {},
35+
"__pycache__": {},
36+
".pytest_cache": {},
37+
".venv": {},
38+
"venv": {},
39+
".tox": {},
40+
"build": {},
41+
"dist": {},
42+
".mypy_cache": {},
43+
"apm_modules": {},
44+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// Package cachepin provides cache-pin marker functionality for drift-replay correctness.
2+
//
3+
// When apm install populates apm_modules/<owner>/<repo>/ from a specific lockfile
4+
// pin, it drops a small JSON marker (.apm-pin) at the package root recording the
5+
// resolved_commit that produced the cache contents.
6+
//
7+
// apm audit drift-replay verifies the marker matches the lockfile's resolved_commit
8+
// BEFORE diffing.
9+
//
10+
// Schema (v1):
11+
//
12+
// {"schema_version": 1, "resolved_commit": "<git-sha-or-similar>"}
13+
package cachepin
14+
15+
import (
16+
"encoding/json"
17+
"errors"
18+
"fmt"
19+
"os"
20+
"path/filepath"
21+
)
22+
23+
// MarkerFilename is the name of the cache-pin marker file.
24+
const MarkerFilename = ".apm-pin"
25+
26+
// SchemaVersion is the current schema version.
27+
const SchemaVersion = 1
28+
29+
// CachePinError is raised when the cache pin is missing, malformed, or stale.
30+
type CachePinError struct {
31+
Msg string
32+
}
33+
34+
func (e *CachePinError) Error() string { return e.Msg }
35+
36+
// IsCachePinError reports whether err is a CachePinError.
37+
func IsCachePinError(err error) bool {
38+
var t *CachePinError
39+
return errors.As(err, &t)
40+
}
41+
42+
type markerPayload struct {
43+
SchemaVersion int `json:"schema_version"`
44+
ResolvedCommit string `json:"resolved_commit"`
45+
}
46+
47+
// WriteMarker writes the cache-pin marker file to installPath.
48+
//
49+
// Idempotent: overwrites any prior marker. Failures are silent because
50+
// they are non-fatal for apm install itself.
51+
func WriteMarker(installPath, resolvedCommit string) {
52+
info, err := os.Stat(installPath)
53+
if err != nil || !info.IsDir() {
54+
return
55+
}
56+
payload := markerPayload{SchemaVersion: SchemaVersion, ResolvedCommit: resolvedCommit}
57+
data, err := json.Marshal(payload)
58+
if err != nil {
59+
return
60+
}
61+
markerPath := filepath.Join(installPath, MarkerFilename)
62+
_ = os.WriteFile(markerPath, data, 0o644)
63+
}
64+
65+
// VerifyMarker verifies the marker at installPath matches expectedCommit.
66+
//
67+
// Returns CachePinError on any of: marker file absent, unreadable, malformed
68+
// JSON, unsupported schema_version, missing resolved_commit field, or commit
69+
// mismatch.
70+
func VerifyMarker(installPath, expectedCommit string) error {
71+
markerPath := filepath.Join(installPath, MarkerFilename)
72+
data, err := os.ReadFile(markerPath)
73+
if err != nil {
74+
if os.IsNotExist(err) {
75+
return &CachePinError{Msg: fmt.Sprintf("cache-pin marker missing at %s (run apm install to refresh)", installPath)}
76+
}
77+
return &CachePinError{Msg: fmt.Sprintf("cannot read cache-pin marker at %s: %v", markerPath, err)}
78+
}
79+
80+
var payload markerPayload
81+
if err := json.Unmarshal(data, &payload); err != nil {
82+
return &CachePinError{Msg: fmt.Sprintf("cache-pin marker at %s is malformed JSON: %v", markerPath, err)}
83+
}
84+
85+
if payload.SchemaVersion != SchemaVersion {
86+
return &CachePinError{Msg: fmt.Sprintf("cache-pin marker at %s has unsupported schema_version %d (expected %d)", markerPath, payload.SchemaVersion, SchemaVersion)}
87+
}
88+
89+
if payload.ResolvedCommit == "" {
90+
return &CachePinError{Msg: fmt.Sprintf("cache-pin marker at %s is missing resolved_commit field", markerPath)}
91+
}
92+
93+
if payload.ResolvedCommit != expectedCommit {
94+
return &CachePinError{Msg: fmt.Sprintf("cache-pin marker mismatch at %s: marker=%s expected=%s (run apm install to refresh)", markerPath, payload.ResolvedCommit, expectedCommit)}
95+
}
96+
97+
return nil
98+
}

0 commit comments

Comments
 (0)