package push import ( "archive/zip" "compress/flate" "context" "fmt" "io" "os" "runtime" "strings" "time" "code.geekeey.de/actions/sdk" "code.geekeey.de/actions/sdk/artifact" "code.geekeey.de/actions/sdk/glob" ) type Action struct { *sdk.Action } func New(action *sdk.Action) *Action { return &Action{Action: action} } type config struct { Name string Path string Patterns []string } func parseConfig(get func(string) string) (*config, error) { name := get("name") if len(name) == 0 { return nil, fmt.Errorf("input 'name': is empty") } path := get("path") if len(path) == 0 { return nil, fmt.Errorf("input 'path': is empty") } cfg := &config{Name: name, Path: path} for _, pattern := range strings.Split(get("pattern"), "\n") { if len(pattern) == 0 { continue } cfg.Patterns = append(cfg.Patterns, pattern) } return cfg, nil } func (a *Action) Run(ctx context.Context) error { cfg, err := parseConfig(a.GetInput) if err != nil { return err } run, job, err := artifact.JobInfo() if err != nil { return fmt.Errorf("unable to get job info: %v", err) } client := artifact.NewClientFromEnv(os.Getenv) expire := time.Now().Add(20 * time.Hour) create, err := client.CreateArtifact(ctx, artifact.CreateArtifactRequest{ RunID: run, JobRunID: job, Name: cfg.Name, Version: 4, ExpiresAt: &expire, }) if err != nil { return fmt.Errorf("cannot create artifact: %w", err) } if !create.Ok { return fmt.Errorf("cannot get pre-signed URL") } rd, err := createArchive(cfg.Path, cfg.Patterns) if err != nil { return fmt.Errorf("cannot create archive: %w", err) } res, err := artifact.PushBlob(ctx, nil, rd, create.SignedUploadUrl, 1024*1024, runtime.NumCPU()) if err != nil { return fmt.Errorf("cannot upload artifact: %w", err) } finish, err := client.FinalizeArtifact(ctx, artifact.FinalizeArtifactRequest{ RunID: run, JobRunID: job, Name: cfg.Name, Size: res.Size, Hash: res.SHA256Sum, }) if err != nil { return fmt.Errorf("cannot finish artifact: %w", err) } if !finish.Ok { return fmt.Errorf("cannot finish artifact upload") } a.SetOutput("artifact-id", finish.ArtifactId) a.SetOutput("artifact-digest", res.SHA256Sum) if url := artifactURL(a.Context(), finish.ArtifactId); url != "" { a.SetOutput("artifact-url", url) } a.Noticef("created artifact: %s", finish.ArtifactId) return nil } // artifactURL builds the URL of an artifact on the server, following the same // scheme as GitHub's upload-artifact output. func artifactURL(context *sdk.GitHubContext, artifactID string) string { if context.ServerURL == "" || context.Repository == "" || context.RunID == "" || artifactID == "" { return "" } return fmt.Sprintf("%s/%s/actions/runs/%s/artifacts/%s", strings.TrimRight(context.ServerURL, "/"), context.Repository, context.RunID, artifactID) } // createArchive zips the files in path matching any of the given glob patterns // and returns a reader for the resulting archive. func createArchive(path string, patterns []string) (io.Reader, error) { stat, err := os.Stat(path) if err != nil { return nil, fmt.Errorf("cannot find directory: %w", err) } if !stat.IsDir() { return nil, fmt.Errorf("source path is not a directory") } gfs, err := glob.NewGlobFS(os.DirFS(path), patterns...) if err != nil { return nil, fmt.Errorf("cannot apply glob patterns: %w", err) } rd, wr := io.Pipe() go func() { defer wr.Close() zw := zip.NewWriter(wr) zw.RegisterCompressor(zip.Deflate, func(w io.Writer) (io.WriteCloser, error) { return flate.NewWriter(w, flate.BestCompression) }) if err := zw.AddFS(gfs); err != nil { wr.CloseWithError(err) return } if err := zw.Close(); err != nil { wr.CloseWithError(err) } }() return rd, nil }