Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions cmd/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -1294,15 +1294,20 @@ func handlePolicyFetchFailure(cmd *StartCmd, configMgr *config.Manager, auditLog
// fail-fast purpose; the intended flow (the watcher fails the job
// on this sentinel, so no build steps run) makes the pre-attach
// window harmless.
//
// ORDER: the downgrade record is written BEFORE the sentinel. The
// action classifies a sentinel as lockdown from this record, so the
// sentinel is the commit signal and is published only once the
// record describing it exists — a reader between the two writes
// would otherwise see a generic startup crash (issue #102). The
// record also tells the dashboard, via the summary push, that the
// run was locked down and why.
writeDowngradeFile(downgradeRecord(datapb.CargoWallDowngradeType_CARGO_WALL_DOWNGRADE_TYPE_LOCKDOWN, fe, reason), logger)
if cmd.FailureFile != "" {
if werr := writeFailureSentinel(cmd.FailureFile, reason); werr != nil {
logger.Warn("Failed to write failure sentinel", "path", cmd.FailureFile, "error", werr)
}
}
// Also recorded as a structured downgrade: if the action lets the
// job proceed (or the post step runs before teardown), the summary
// push tells the dashboard the run was locked down and why.
writeDowngradeFile(downgradeRecord(datapb.CargoWallDowngradeType_CARGO_WALL_DOWNGRADE_TYPE_LOCKDOWN, fe, reason), logger)
// No mode file: lockdown is not a SaaS-resolved posture, and the
// failure sentinel already carries the state the action needs.
return
Expand Down
34 changes: 34 additions & 0 deletions cmd/start_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,40 @@ func TestLoadCIConfig_LockdownSkipsLocalConfig(t *testing.T) {
}
}

// TestLoadCIConfig_LockdownPublishesSentinelLast: consumers poll for the
// failure sentinel and classify lockdown from the downgrade record, so the
// sentinel must be the last write — published before the record, a reader
// between the two sees a lockdown with no record and treats it as a generic
// startup crash (issue #102).
func TestLoadCIConfig_LockdownPublishesSentinelLast(t *testing.T) {
setFastPolicyRetries(t)
redirectStateFiles(t)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
t.Cleanup(srv.Close)

// Both writes atomically replace the same path, so whichever lands last
// is what remains: a deterministic order check with no goroutine racing
// two writes microseconds apart.
sharedPath := filepath.Join(t.TempDir(), "cargowall-state")
downgradeFile = sharedPath // restored by redirectStateFiles' cleanup

cmd := &StartCmd{
GithubAction: true,
ApiUrl: srv.URL,
Token: "test-token",
ApiFailureMode: ApiFailureModeFail,
FailureFile: sharedPath,
}
loadCIConfig(context.Background(), cmd, config.NewConfigManager(), nil, quietLogger())

require.True(t, cmd.policyLockdown)
data, err := os.ReadFile(sharedPath)
require.NoError(t, err)
assert.True(t, strings.HasPrefix(string(data), "pid="), "the failure sentinel must be written after the downgrade record, got: %s", data)
}

// TestLoadCIConfig_CancelledContextSkipsPostureHandling guards the SIGTERM
// unwind path: a fetch killed by shutdown-signal cancellation must not be
// misreported as an outage — no lockdown, no audit downgrade, and no
Expand Down
Loading