Skip to content

Commit 5b4817b

Browse files
[Crane: crane-migration-python-to-go-full-apm-cli-rewrite] Iteration 13: Port install/cache_pin.go + sources.go types
Run: https://github.com/githubnext/apm/actions/runs/26481040888 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 0c54f42 commit 5b4817b

3 files changed

Lines changed: 440 additions & 0 deletions

File tree

internal/install/cache_pin.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// Package install: cache-pin marker for drift-replay correctness.
2+
//
3+
// When apm install populates apm_modules/<owner>/<repo>/ from a specific
4+
// lockfile pin, it drops a small JSON marker (.apm-pin) at the package root
5+
// recording the resolved_commit that produced the cache contents.
6+
//
7+
// apm audit drift-replay verifies the marker matches the lockfile's
8+
// resolved_commit before diffing. This catches shared CI runner hazards and
9+
// lockfile-bumps without re-running apm install.
10+
//
11+
// Schema (v1):
12+
//
13+
// {"schema_version": 1, "resolved_commit": "<git-sha-or-similar>"}
14+
package install
15+
16+
import (
17+
"encoding/json"
18+
"fmt"
19+
"os"
20+
"path/filepath"
21+
)
22+
23+
// MarkerFilename is the name of the cache-pin file dropped inside each
24+
// package install directory. Mirrors MARKER_FILENAME in cache_pin.py.
25+
const MarkerFilename = ".apm-pin"
26+
27+
// CachePinSchemaVersion is the current marker schema version.
28+
const CachePinSchemaVersion = 1
29+
30+
// CachePinError is returned when the cache pin is missing, malformed, or stale.
31+
// The orchestrator (drift replay) catches this and translates it to a
32+
// CacheMissError with the same message.
33+
type CachePinError struct {
34+
msg string
35+
}
36+
37+
func (e *CachePinError) Error() string { return e.msg }
38+
39+
// newCachePinError creates a CachePinError with the given message.
40+
func newCachePinError(format string, args ...any) *CachePinError {
41+
return &CachePinError{msg: fmt.Sprintf(format, args...)}
42+
}
43+
44+
// cachePinMarker is the JSON payload written to MarkerFilename.
45+
type cachePinMarker struct {
46+
SchemaVersion int `json:"schema_version"`
47+
ResolvedCommit string `json:"resolved_commit"`
48+
}
49+
50+
// WriteMarker writes the cache-pin marker file to installPath.
51+
//
52+
// Idempotent: overwrites any prior marker. Silent on filesystem errors because
53+
// a missing marker is non-fatal for apm install -- it is detected at
54+
// drift-replay verify time.
55+
func WriteMarker(installPath, resolvedCommit string) {
56+
info, err := os.Stat(installPath)
57+
if err != nil || !info.IsDir() {
58+
return
59+
}
60+
payload := cachePinMarker{
61+
SchemaVersion: CachePinSchemaVersion,
62+
ResolvedCommit: resolvedCommit,
63+
}
64+
data, err := json.Marshal(payload)
65+
if err != nil {
66+
return
67+
}
68+
marker := filepath.Join(installPath, MarkerFilename)
69+
_ = os.WriteFile(marker, data, 0o644)
70+
}
71+
72+
// VerifyMarker verifies that the marker at installPath matches expectedCommit.
73+
//
74+
// Returns a *CachePinError on any of: marker absent, unreadable, malformed JSON,
75+
// unsupported schema_version, missing resolved_commit, or commit mismatch.
76+
func VerifyMarker(installPath, expectedCommit string) error {
77+
marker := filepath.Join(installPath, MarkerFilename)
78+
if _, err := os.Stat(marker); os.IsNotExist(err) {
79+
return newCachePinError(
80+
"cache pin marker missing at %s -- cache pre-dates supply-chain hardening",
81+
marker,
82+
)
83+
}
84+
raw, err := os.ReadFile(marker)
85+
if err != nil {
86+
return newCachePinError("cache pin marker unreadable at %s: %v", marker, err)
87+
}
88+
var payload cachePinMarker
89+
if err := json.Unmarshal(raw, &payload); err != nil {
90+
return newCachePinError("cache pin marker at %s is not valid JSON: %v", marker, err)
91+
}
92+
if payload.SchemaVersion != CachePinSchemaVersion {
93+
return newCachePinError(
94+
"cache pin marker at %s has unsupported schema_version %d; expected %d",
95+
marker, payload.SchemaVersion, CachePinSchemaVersion,
96+
)
97+
}
98+
if payload.ResolvedCommit == "" {
99+
return newCachePinError("cache pin marker at %s is missing resolved_commit", marker)
100+
}
101+
if payload.ResolvedCommit != expectedCommit {
102+
return newCachePinError(
103+
"cache pin mismatch at %s: marker says %q, lockfile expects %q",
104+
marker, payload.ResolvedCommit, expectedCommit,
105+
)
106+
}
107+
return nil
108+
}

internal/install/install_test.go

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package install
22

33
import (
4+
"os"
5+
"path/filepath"
46
"strings"
57
"testing"
68
)
@@ -500,3 +502,247 @@ func TestParityInstallRequest(t *testing.T) {
500502
}
501503
})
502504
}
505+
506+
// ---------------------------------------------------------------------------
507+
// TestParityCachePin -- mirrors cache_pin.py
508+
// ---------------------------------------------------------------------------
509+
510+
// TestParityCachePinConstants verifies the marker filename and schema version
511+
// match the Python constants.
512+
func TestParityCachePinConstants(t *testing.T) {
513+
if MarkerFilename != ".apm-pin" {
514+
t.Fatalf("MarkerFilename: want .apm-pin, got %q", MarkerFilename)
515+
}
516+
if CachePinSchemaVersion != 1 {
517+
t.Fatalf("CachePinSchemaVersion: want 1, got %d", CachePinSchemaVersion)
518+
}
519+
}
520+
521+
// TestParityCachePinWriteAndVerify exercises the round-trip write -> verify.
522+
func TestParityCachePinWriteAndVerify(t *testing.T) {
523+
dir := t.TempDir()
524+
WriteMarker(dir, "abc123")
525+
526+
marker := filepath.Join(dir, MarkerFilename)
527+
if _, err := os.Stat(marker); err != nil {
528+
t.Fatalf("marker file not created: %v", err)
529+
}
530+
531+
if err := VerifyMarker(dir, "abc123"); err != nil {
532+
t.Fatalf("unexpected error: %v", err)
533+
}
534+
}
535+
536+
// TestParityCachePinWriteNoopOnMissingDir verifies WriteMarker is silent when
537+
// the path does not exist -- mirroring the Python try/except OSError.
538+
func TestParityCachePinWriteNoopOnMissingDir(t *testing.T) {
539+
WriteMarker("/nonexistent/path/that/does/not/exist", "abc123")
540+
// no panic, no error
541+
}
542+
543+
// TestParityCachePinWriteNoopOnFile verifies WriteMarker is a no-op when the
544+
// path is a file, not a directory.
545+
func TestParityCachePinWriteNoopOnFile(t *testing.T) {
546+
f, err := os.CreateTemp(t.TempDir(), "not-a-dir")
547+
if err != nil {
548+
t.Fatal(err)
549+
}
550+
f.Close()
551+
WriteMarker(f.Name(), "abc123")
552+
// no panic
553+
}
554+
555+
// TestParityCachePinMissingMarker verifies VerifyMarker returns CachePinError
556+
// when the marker file is absent.
557+
func TestParityCachePinMissingMarker(t *testing.T) {
558+
dir := t.TempDir()
559+
err := VerifyMarker(dir, "abc123")
560+
if err == nil {
561+
t.Fatal("expected error for missing marker")
562+
}
563+
ce, ok := err.(*CachePinError)
564+
if !ok {
565+
t.Fatalf("expected *CachePinError, got %T", err)
566+
}
567+
if !strings.Contains(ce.Error(), "cache pin marker missing") {
568+
t.Fatalf("unexpected message: %q", ce.Error())
569+
}
570+
if !strings.Contains(ce.Error(), "supply-chain hardening") {
571+
t.Fatalf("missing supply-chain phrase: %q", ce.Error())
572+
}
573+
}
574+
575+
// TestParityCachePinMalformedJSON verifies VerifyMarker returns CachePinError
576+
// for invalid JSON.
577+
func TestParityCachePinMalformedJSON(t *testing.T) {
578+
dir := t.TempDir()
579+
marker := filepath.Join(dir, MarkerFilename)
580+
if err := os.WriteFile(marker, []byte("not json at all"), 0o644); err != nil {
581+
t.Fatal(err)
582+
}
583+
err := VerifyMarker(dir, "abc123")
584+
if err == nil {
585+
t.Fatal("expected error for malformed JSON")
586+
}
587+
ce, ok := err.(*CachePinError)
588+
if !ok {
589+
t.Fatalf("expected *CachePinError, got %T", err)
590+
}
591+
if !strings.Contains(ce.Error(), "not valid JSON") {
592+
t.Fatalf("unexpected message: %q", ce.Error())
593+
}
594+
}
595+
596+
// TestParityCachePinWrongSchema verifies VerifyMarker returns CachePinError
597+
// for an unsupported schema_version -- mirrors the Python check.
598+
func TestParityCachePinWrongSchema(t *testing.T) {
599+
dir := t.TempDir()
600+
marker := filepath.Join(dir, MarkerFilename)
601+
payload := `{"schema_version": 99, "resolved_commit": "abc"}`
602+
if err := os.WriteFile(marker, []byte(payload), 0o644); err != nil {
603+
t.Fatal(err)
604+
}
605+
err := VerifyMarker(dir, "abc")
606+
if err == nil {
607+
t.Fatal("expected error for wrong schema")
608+
}
609+
ce, ok := err.(*CachePinError)
610+
if !ok {
611+
t.Fatalf("expected *CachePinError, got %T", err)
612+
}
613+
if !strings.Contains(ce.Error(), "unsupported schema_version") {
614+
t.Fatalf("unexpected message: %q", ce.Error())
615+
}
616+
if !strings.Contains(ce.Error(), "99") {
617+
t.Fatalf("schema version 99 not mentioned: %q", ce.Error())
618+
}
619+
}
620+
621+
// TestParityCachePinMissingCommitField verifies VerifyMarker returns
622+
// CachePinError when the marker has no resolved_commit field.
623+
func TestParityCachePinMissingCommitField(t *testing.T) {
624+
dir := t.TempDir()
625+
marker := filepath.Join(dir, MarkerFilename)
626+
payload := `{"schema_version": 1}`
627+
if err := os.WriteFile(marker, []byte(payload), 0o644); err != nil {
628+
t.Fatal(err)
629+
}
630+
err := VerifyMarker(dir, "abc123")
631+
if err == nil {
632+
t.Fatal("expected error for missing resolved_commit")
633+
}
634+
ce, ok := err.(*CachePinError)
635+
if !ok {
636+
t.Fatalf("expected *CachePinError, got %T", err)
637+
}
638+
if !strings.Contains(ce.Error(), "missing resolved_commit") {
639+
t.Fatalf("unexpected message: %q", ce.Error())
640+
}
641+
}
642+
643+
// TestParityCachePinMismatch verifies VerifyMarker returns CachePinError when
644+
// the marker commit differs from the expected commit.
645+
func TestParityCachePinMismatch(t *testing.T) {
646+
dir := t.TempDir()
647+
WriteMarker(dir, "aaa111")
648+
err := VerifyMarker(dir, "bbb222")
649+
if err == nil {
650+
t.Fatal("expected error for commit mismatch")
651+
}
652+
ce, ok := err.(*CachePinError)
653+
if !ok {
654+
t.Fatalf("expected *CachePinError, got %T", err)
655+
}
656+
if !strings.Contains(ce.Error(), "cache pin mismatch") {
657+
t.Fatalf("unexpected message: %q", ce.Error())
658+
}
659+
if !strings.Contains(ce.Error(), "aaa111") {
660+
t.Fatalf("marker commit not in error: %q", ce.Error())
661+
}
662+
if !strings.Contains(ce.Error(), "bbb222") {
663+
t.Fatalf("expected commit not in error: %q", ce.Error())
664+
}
665+
}
666+
667+
// TestParityCachePinIdempotent verifies WriteMarker overwrites prior markers.
668+
func TestParityCachePinIdempotent(t *testing.T) {
669+
dir := t.TempDir()
670+
WriteMarker(dir, "first")
671+
WriteMarker(dir, "second")
672+
673+
if err := VerifyMarker(dir, "second"); err != nil {
674+
t.Fatalf("expected second marker: %v", err)
675+
}
676+
if err := VerifyMarker(dir, "first"); err == nil {
677+
t.Fatal("expected mismatch after overwrite")
678+
}
679+
}
680+
681+
// ---------------------------------------------------------------------------
682+
// TestParitySources -- mirrors sources.py
683+
// ---------------------------------------------------------------------------
684+
685+
// TestParityMaterializationDefaults verifies NewMaterialization sets default
686+
// Deltas with installed:1.
687+
func TestParityMaterializationDefaults(t *testing.T) {
688+
m := NewMaterialization(nil, "/tmp/some-pkg", "owner/repo")
689+
if m.InstallPath != "/tmp/some-pkg" {
690+
t.Fatalf("InstallPath: want /tmp/some-pkg, got %q", m.InstallPath)
691+
}
692+
if m.DepKey != "owner/repo" {
693+
t.Fatalf("DepKey: want owner/repo, got %q", m.DepKey)
694+
}
695+
if m.Deltas == nil {
696+
t.Fatal("Deltas should not be nil")
697+
}
698+
if m.Deltas["installed"] != 1 {
699+
t.Fatalf("Deltas[installed]: want 1, got %d", m.Deltas["installed"])
700+
}
701+
}
702+
703+
// TestParityMaterializationNilPackageInfo verifies Materialization can hold a
704+
// nil PackageInfo (skip-integration signal).
705+
func TestParityMaterializationNilPackageInfo(t *testing.T) {
706+
m := NewMaterialization(nil, "/tmp/x", "k")
707+
if m.PackageInfo != nil {
708+
t.Fatal("PackageInfo should be nil")
709+
}
710+
}
711+
712+
// TestParityMaterializationUnpinnedDelta verifies that callers can add an
713+
// "unpinned" delta alongside "installed" -- matching the Python pattern.
714+
func TestParityMaterializationUnpinnedDelta(t *testing.T) {
715+
m := NewMaterialization(nil, "/tmp/x", "k")
716+
m.Deltas["unpinned"] = 1
717+
if m.Deltas["unpinned"] != 1 {
718+
t.Fatalf("Deltas[unpinned]: want 1, got %d", m.Deltas["unpinned"])
719+
}
720+
}
721+
722+
// TestParitySourceConstants verifies the integrate-error-prefix constants
723+
// match the Python class attributes.
724+
func TestParitySourceConstants(t *testing.T) {
725+
if IntegrateErrorPrefix != "Failed to integrate primitives" {
726+
t.Fatalf("IntegrateErrorPrefix: got %q", IntegrateErrorPrefix)
727+
}
728+
if IntegrateErrorPrefixLocal != "Failed to integrate primitives from local package" {
729+
t.Fatalf("IntegrateErrorPrefixLocal: got %q", IntegrateErrorPrefixLocal)
730+
}
731+
if IntegrateErrorPrefixCached != "Failed to integrate primitives from cached package" {
732+
t.Fatalf("IntegrateErrorPrefixCached: got %q", IntegrateErrorPrefixCached)
733+
}
734+
}
735+
736+
// TestParitySourceKindValues verifies the SourceKind constants are distinct
737+
// and match the expected ordering.
738+
func TestParitySourceKindValues(t *testing.T) {
739+
if SourceKindLocal == SourceKindCached {
740+
t.Fatal("Local and Cached should differ")
741+
}
742+
if SourceKindLocal == SourceKindFresh {
743+
t.Fatal("Local and Fresh should differ")
744+
}
745+
if SourceKindCached == SourceKindFresh {
746+
t.Fatal("Cached and Fresh should differ")
747+
}
748+
}

0 commit comments

Comments
 (0)