mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
feat: populate CI invocation context on cloud events (#304)
* feat: add CloudSinkEnvResolver interface with default implementation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add GitHub Actions environment resolver for cloud sink Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: populate invocation context with CI environment on cloud events Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address lint errors in cloud sink tests Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: use getter-based CloudSinkCIResolver with nil-when-no-CI Rename to CloudSinkCIResolver with focused CI concern. Factory returns nil when no CI is detected, removing the need for IsCI() and a default resolver. Leaves room for a separate agent resolver in the future. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add CI metadata support using updated API SDK Update SDK to include SetMetadata on EndpointCIContext. Add Metadata() to CloudSinkCIResolver interface and GitHub Actions implementation (workflow, job, run_attempt, server_url). Wire metadata into buildInvocationContext. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: address review comments on CI resolver - Inject CloudSinkCIResolver as dependency into newCloudSink for testability - Check both GITHUB_ACTIONS and GITHUB_RUN_ID for GHA environment detection - Make factory and constructor package-private (newCloudSinkCIResolver, newGithubActionsCIResolver) - Attach invocation context only to session complete events, not every event Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: fail fast on os.Getwd error instead of swallowing it Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
4540dafccc
commit
6087bc922f
@@ -4,7 +4,10 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
controltowerv1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/controltower/v1"
|
||||
"github.com/google/uuid"
|
||||
"github.com/safedep/dry/cloud/endpointsync"
|
||||
"github.com/safedep/dry/log"
|
||||
@@ -14,9 +17,12 @@ import (
|
||||
type cloudSink struct {
|
||||
*SyncClientBundle
|
||||
invocationID string
|
||||
ciResolver CloudSinkCIResolver
|
||||
command string
|
||||
workingDir string
|
||||
}
|
||||
|
||||
func newCloudSink(cfg *config.RuntimeConfig) (*cloudSink, error) {
|
||||
func newCloudSink(cfg *config.RuntimeConfig, ciResolver CloudSinkCIResolver) (*cloudSink, error) {
|
||||
bundle, err := NewSyncClientBundle(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -30,13 +36,28 @@ func newCloudSink(cfg *config.RuntimeConfig) (*cloudSink, error) {
|
||||
return nil, fmt.Errorf("failed to generate invocation ID: %w", err)
|
||||
}
|
||||
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
if closeErr := bundle.Close(); closeErr != nil {
|
||||
log.Warnf("failed to close sync client bundle after getwd failure: %v", closeErr)
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get working directory: %w", err)
|
||||
}
|
||||
|
||||
return &cloudSink{
|
||||
SyncClientBundle: bundle,
|
||||
invocationID: invocationID.String(),
|
||||
ciResolver: ciResolver,
|
||||
workingDir: wd,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *cloudSink) Handle(ctx context.Context, event AuditEvent) error {
|
||||
if event.Type == EventTypeInstallStarted {
|
||||
s.command = buildCommand(event.PackageManager, event.Args)
|
||||
return nil
|
||||
}
|
||||
|
||||
pmgEvents := s.translateToPmgEvents(event)
|
||||
if len(pmgEvents) == 0 {
|
||||
return nil
|
||||
@@ -50,6 +71,11 @@ func (s *cloudSink) Handle(ctx context.Context, event AuditEvent) error {
|
||||
|
||||
toolEvent.SetPmgEvent(pmgEvent)
|
||||
toolEvent.SetInvocationId(s.invocationID)
|
||||
// Invocation context (CI, command, working dir) is set once per
|
||||
// execution on the session summary event to avoid redundancy.
|
||||
if event.Type == EventTypeSessionComplete {
|
||||
toolEvent.SetInvocationContext(s.buildInvocationContext())
|
||||
}
|
||||
|
||||
if err := s.syncClient.Emit(ctx, toolEvent); err != nil {
|
||||
if errors.Is(err, endpointsync.ErrWALFull) {
|
||||
@@ -63,6 +89,39 @@ func (s *cloudSink) Handle(ctx context.Context, event AuditEvent) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *cloudSink) buildInvocationContext() *controltowerv1.EndpointInvocationContext {
|
||||
ctx := &controltowerv1.EndpointInvocationContext{}
|
||||
ctx.SetCommand(s.command)
|
||||
ctx.SetWorkingDirectory(s.workingDir)
|
||||
|
||||
if s.ciResolver != nil {
|
||||
ci := &controltowerv1.EndpointCIContext{}
|
||||
ci.SetProvider(s.ciResolver.Provider())
|
||||
ci.SetRunId(s.ciResolver.RunId())
|
||||
ci.SetRepository(s.ciResolver.Repository())
|
||||
ci.SetBranch(s.ciResolver.Branch())
|
||||
ci.SetCommitSha(s.ciResolver.CommitSha())
|
||||
ci.SetActor(s.ciResolver.Actor())
|
||||
ci.SetPrNumber(s.ciResolver.PrNumber())
|
||||
if metadata := s.ciResolver.Metadata(); len(metadata) > 0 {
|
||||
ci.SetMetadata(metadata)
|
||||
}
|
||||
ctx.SetCi(ci)
|
||||
}
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
func buildCommand(packageManager string, args []string) string {
|
||||
if packageManager == "" {
|
||||
return ""
|
||||
}
|
||||
if len(args) == 0 {
|
||||
return packageManager
|
||||
}
|
||||
return packageManager + " " + strings.Join(args, " ")
|
||||
}
|
||||
|
||||
// Close delegates to the embedded SyncClientBundle.Close().
|
||||
func (s *cloudSink) Close() error {
|
||||
return s.SyncClientBundle.Close()
|
||||
|
||||
Reference in New Issue
Block a user