package actions import ( "context" "errors" "flag" "fmt" "path/filepath" "strings" "github.com/peterbourgon/ff/v3" "edge-infra.dev/pkg/f8n/devinfra/gcp/job/storage" "edge-infra.dev/pkg/lib/build/bazel" "edge-infra.dev/pkg/lib/cli/rags" "edge-infra.dev/pkg/lib/cli/sink" "edge-infra.dev/pkg/lib/fog" ) var ( // storage bucket to upload to bucket string dry bool ) // info needed for publishing artifacts for jobs ran on github actions type actions struct { jobID string workflow string runID string attempt string repo string } func actionsFlags(a *actions) []*rags.Rag { rs := rags.New("actions", flag.ExitOnError) storage.BindStorageBucketFlag(rs, &bucket) rs.StringVar(&a.jobID, "github-job", "", "[github actions] job_id") rs.StringVar(&a.workflow, "github-workflow", "", "[github actions] workflow name") rs.StringVar(&a.runID, "github-run-id", "", "[github actions] unique run id") rs.StringVar(&a.attempt, "github-run-attempt", "", "[github actions] attempt number") rs.StringVar(&a.repo, "github-repository", "", "[github actions] org/repo slug") rs.BoolVar(&dry, "dry-run", false, "print out paths being uploaded but dont upload them", rags.WithShort("d")) return rs.Rags() } func New() *sink.Command { actions := &actions{} cmd := &sink.Command{ Use: "actions [flags] ", Short: "upload artifacts from Github Actions to google cloud storage", Extensions: []sink.Extension{}, Flags: actionsFlags(actions), Options: []ff.Option{ ff.WithEnvVarNoPrefix(), }, Exec: func(ctx context.Context, r sink.Run) error { if err := actions.validate(); err != nil { return err } // translate org/repo value provided by actions into just repo actions.repo = strings.Split(actions.repo, "/")[1] storagePath := storage.BasePath( actions.repo, actions.workflow, fmt.Sprintf("%s-%s", actions.runID, actions.attempt), actions.jobID, ) return upload(ctx, storagePath, r.Args()) }, } return cmd } // TODO: handle this with RequiredFlags once sink/rags supports it func (a *actions) validate() error { if a.jobID == "" || a.workflow == "" || a.runID == "" || a.repo == "" || a.attempt == "" { return errors.New("not all [github actions] flags were provided, run -help for more info") } if !strings.Contains(a.repo, "/") { return errors.New("github-repository not in expected form based on " + "GITHUB_REPOSITORY variable, are you (incorrectly) faking the github actions runtime?") } return nil } func upload(ctx context.Context, storagePath string, dirs []string) error { log := fog.FromContext(ctx) // resolve cwd correctly regardless of whether or not we are running // in the bazel sandbox cwd, err := bazel.ResolveWd() if err != nil { return fmt.Errorf("failed to determine cwd: %w", err) } s, err := storage.New(ctx) if err != nil { return fmt.Errorf("failed to create storage client: %w", err) } b, err := s.NewBucket(ctx, storage.WithBucket(bucket)) if err != nil { return fmt.Errorf("failed to create bucket handler: %w", err) } log.Info("uploading job artifacts", "bucket", bucket, "storagePath", storagePath) for _, dir := range dirs { // dont upload results when dry running if dry { log.Info("[dry run] uploading", "dir", dir) continue } log.Info("uploading", "dir", dir) err := b.UploadArtifacts(ctx, storagePath, filepath.Join(cwd, dir)) if !errors.Is(err, storage.ErrEmptyDir) && err != nil { return fmt.Errorf("failed to upload artifacts: %w", err) } } return nil }