Skip to content

Commit 110a1a4

Browse files
[Autoloop: python-to-go-migration] Iteration 13: Migrate 13 modules to Go (+3418 lines, 4245 total, 5.92%)
Migrated modules: - utils/content_hash.py -> internal/utils/contenthash - utils/exclude.py -> internal/utils/exclude (with tests) - utils/path_security.py -> internal/utils/pathsecurity (with tests) - utils/version_checker.py -> internal/utils/versionchecker - utils/file_ops.py -> internal/utils/fileops - utils/console.py -> internal/utils/console - utils/diagnostics.py -> internal/utils/diagnostics - utils/install_tui.py -> internal/utils/installtui - utils/github_host.py -> internal/utils/githubhost (with tests) - utils/reflink.py -> internal/utils/reflink (linux+other build tags) - install/errors.py -> internal/install/errors - install/cache_pin.py -> internal/install/cachepin - install/context.py -> internal/install/installctx Metric: 5.92% (previous best: 5.41%, delta: +0.51%) Run: https://github.com/githubnext/apm/actions/runs/25771166584 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 9c40a5f commit 110a1a4

19 files changed

Lines changed: 2329 additions & 6 deletions

File tree

benchmarks/migration-status.json

Lines changed: 97 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"original_python_lines": 71696,
3-
"migrated_python_lines": 827,
3+
"migrated_python_lines": 4245,
44
"migrated_modules": [
55
{
66
"module": "src/apm_cli/constants.py",
@@ -42,7 +42,7 @@
4242
"go_package": "internal/utils/yamlio",
4343
"python_lines": 55,
4444
"status": "migrated",
45-
"notes": "YAML I/O with UTF-8; stdlib-only implementation (gopkg.in/yaml.v3 pending network)"
45+
"notes": "YAML I/O with UTF-8; stdlib-only implementation"
4646
},
4747
{
4848
"module": "src/apm_cli/utils/atomic_io.py",
@@ -63,7 +63,7 @@
6363
"go_package": "internal/utils/guards",
6464
"python_lines": 123,
6565
"status": "migrated",
66-
"notes": "ReadOnlyProjectGuard with snapshot-based mutation detection; stdlib-only"
66+
"notes": "ReadOnlyProjectGuard with snapshot-based mutation detection"
6767
},
6868
{
6969
"module": "src/apm_cli/utils/subprocess_env.py",
@@ -78,8 +78,99 @@
7878
"python_lines": 131,
7979
"status": "migrated",
8080
"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()"
81172
}
82173
],
83-
"last_updated": "2026-05-12T16:29:36Z",
84-
"iteration": 4
85-
}
174+
"last_updated": "2026-05-13T00:52:00Z",
175+
"iteration": 13
176+
}
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+
}

internal/install/errors/errors.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// Package errors provides canonical exception types for the install pipeline.
2+
//
3+
// Centralises typed errors raised by the install machinery so call sites
4+
// can handle a single class hierarchy.
5+
package errors
6+
7+
// DirectDependencyError is raised when one or more direct dependencies fail
8+
// validation or integration.
9+
type DirectDependencyError struct {
10+
Msg string
11+
}
12+
13+
func (e *DirectDependencyError) Error() string { return e.Msg }
14+
15+
// NewDirectDependencyError creates a DirectDependencyError.
16+
func NewDirectDependencyError(msg string) *DirectDependencyError {
17+
return &DirectDependencyError{Msg: msg}
18+
}
19+
20+
// AuthenticationError is raised when a remote host rejects credentials or
21+
// none are available.
22+
type AuthenticationError struct {
23+
Msg string
24+
DiagnosticContext string
25+
}
26+
27+
func (e *AuthenticationError) Error() string { return e.Msg }
28+
29+
// NewAuthenticationError creates an AuthenticationError.
30+
func NewAuthenticationError(msg, diagnosticContext string) *AuthenticationError {
31+
return &AuthenticationError{Msg: msg, DiagnosticContext: diagnosticContext}
32+
}
33+
34+
// FrozenInstallError is raised when apm install --frozen cannot proceed.
35+
// Two trigger conditions:
36+
// - Lockfile (apm.lock.yaml) is missing entirely.
37+
// - Lockfile is structurally out of sync with apm.yml.
38+
type FrozenInstallError struct {
39+
Msg string
40+
Reasons []string
41+
}
42+
43+
func (e *FrozenInstallError) Error() string { return e.Msg }
44+
45+
// NewFrozenInstallError creates a FrozenInstallError.
46+
func NewFrozenInstallError(msg string, reasons []string) *FrozenInstallError {
47+
r := make([]string, len(reasons))
48+
copy(r, reasons)
49+
return &FrozenInstallError{Msg: msg, Reasons: r}
50+
}
51+
52+
// PolicyViolationError is raised when org-policy enforcement halts an install.
53+
type PolicyViolationError struct {
54+
Msg string
55+
PolicySource string
56+
}
57+
58+
func (e *PolicyViolationError) Error() string { return e.Msg }
59+
60+
// NewPolicyViolationError creates a PolicyViolationError.
61+
func NewPolicyViolationError(msg, policySource string) *PolicyViolationError {
62+
return &PolicyViolationError{Msg: msg, PolicySource: policySource}
63+
}
64+
65+
// IsDirect returns true if err is a DirectDependencyError.
66+
func IsDirect(err error) bool {
67+
if err == nil {
68+
return false
69+
}
70+
_, ok := err.(*DirectDependencyError)
71+
return ok
72+
}
73+
74+
// IsAuthentication returns true if err is an AuthenticationError.
75+
func IsAuthentication(err error) bool {
76+
if err == nil {
77+
return false
78+
}
79+
_, ok := err.(*AuthenticationError)
80+
return ok
81+
}
82+
83+
// IsFrozen returns true if err is a FrozenInstallError.
84+
func IsFrozen(err error) bool {
85+
if err == nil {
86+
return false
87+
}
88+
_, ok := err.(*FrozenInstallError)
89+
return ok
90+
}
91+
92+
// IsPolicy returns true if err is a PolicyViolationError.
93+
func IsPolicy(err error) bool {
94+
if err == nil {
95+
return false
96+
}
97+
_, ok := err.(*PolicyViolationError)
98+
return ok
99+
}

0 commit comments

Comments
 (0)