Files
runner/internal/pkg/action/downloadartifact/download.go
T
2026-11-06 03:32:59 +00:00

175 lines
5.8 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
// Package downloadartifact works like actions/download-artifact v8.
package downloadartifact
import (
"cmp"
"context"
"fmt"
"path/filepath"
"slices"
"strings"
"time"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/act/container"
"gitea.com/gitea/runner/internal/pkg/action"
"gitea.com/gitea/runner/internal/pkg/action/artifact"
"golang.org/x/sync/errgroup"
)
var digestBehaviors = []string{"ignore", "info", "warn", "error"}
const downloadAttempts = 5
func Main(ctx context.Context, c *action.Context) error {
if err := run(ctx, c); err != nil {
return action.Failf("Unable to download artifact(s): %w", err)
}
return nil
}
func run(ctx context.Context, c *action.Context) error {
c.ApplyDefaults("name", "", "artifact-ids", "", "path", "", "pattern", "", "merge-multiple", "false", "github-token", "",
"repository", c.Github.Repository, "run-id", c.Github.RunID, "skip-decompress", "false", "digest-mismatch", "error")
name, ids, pattern, token := c.Input("name"), c.Input("artifact-ids"), c.Input("pattern"), c.Input("github-token")
mergeMultiple, err := c.BoolInput("merge-multiple")
if err != nil {
return err
}
skipDecompress, err := c.BoolInput("skip-decompress")
if err != nil {
return err
}
digestMismatch := cmp.Or(c.Input("digest-mismatch"), "error")
if !slices.Contains(digestBehaviors, digestMismatch) {
return action.Failf("Invalid value for 'digest-mismatch': '%s'. Valid options are: %s", digestMismatch, strings.Join(digestBehaviors, ", "))
}
dest := cmp.Or(c.Input("path"), c.Env["GITHUB_WORKSPACE"], c.Workspace)
if strings.HasPrefix(dest, "~") {
home, err := c.Home(ctx)
if err != nil {
return err
}
dest = home + dest[1:]
}
dest = c.Resolve(dest)
if name != "" && ids != "" {
return action.Failf("Inputs 'name' and 'artifact-ids' cannot be used together. Please specify only one.")
}
src, err := newSource(c, token)
if err != nil {
return err
}
artifacts, err := src.list(ctx, name)
if err != nil {
return err
}
slices.SortStableFunc(artifacts, func(a, b artifact.Artifact) int { return cmp.Compare(b.ID, a.ID) })
seen := map[string]bool{}
artifacts = slices.DeleteFunc(artifacts, func(a artifact.Artifact) bool {
duplicate := seen[a.Name]
seen[a.Name] = true
return duplicate
})
if name == "" {
c.Infof("Found %d artifact(s)", len(artifacts))
}
switch {
case name != "":
if len(artifacts) == 0 {
return action.Failf("Artifact not found for name: %s\nPlease ensure that your artifact is not expired and the artifact was uploaded using a compatible version of toolkit/upload-artifact.", name)
}
artifacts = artifacts[:1]
case ids != "":
var wanted []int64
for id := range strings.SplitSeq(ids, ",") {
if id = strings.TrimSpace(id); id == "" {
continue
}
parsed, err := action.ParseInt(id)
if err != nil {
return action.Failf("Invalid artifact ID: '%s'. Must be a number.", id)
}
wanted = append(wanted, int64(parsed))
}
if len(wanted) == 0 {
return action.Failf("No valid artifact IDs provided in 'artifact-ids' input")
}
artifacts = slices.DeleteFunc(artifacts, func(a artifact.Artifact) bool { return !slices.Contains(wanted, a.ID) })
if len(artifacts) == 0 {
return action.Failf("None of the provided artifact IDs were found")
}
case pattern != "":
artifacts = slices.DeleteFunc(artifacts, func(a artifact.Artifact) bool { return !matchName(pattern, a.Name) })
}
for _, art := range artifacts {
if art.Name == "" || art.Name == "." || art.Name == ".." || strings.ContainsAny(art.Name, `/\`) || c.Windows() && !filepath.IsLocal(art.Name) {
return action.Failf("Artifact name is not valid: %s", art.Name)
}
}
mismatches := make([]string, len(artifacts))
group, groupCtx := errgroup.WithContext(ctx)
group.SetLimit(5)
for i, art := range artifacts {
target := dest
if name == "" && !mergeMultiple && len(artifacts) > 1 {
target = c.Join(dest, art.Name)
}
group.Go(func() error {
mismatch, err := fetch(groupCtx, c, src, art, target, skipDecompress)
if err != nil {
return err
}
if mismatch {
mismatches[i] = art.Name
message := fmt.Sprintf("Artifact '%s' digest validation failed. Please verify the integrity of the artifact.", art.Name)
switch digestMismatch {
case "info":
c.Infof("%s", message)
case "warn":
c.Warning("%s", message)
}
}
return nil
})
}
if err := group.Wait(); err != nil {
return err
}
mismatches = slices.DeleteFunc(mismatches, func(name string) bool { return name == "" })
if len(mismatches) > 0 && digestMismatch == "error" {
return action.Failf("Digest validation failed for artifact(s): %s. Use 'digest-mismatch: warn' to continue on mismatch.", strings.Join(mismatches, ", "))
}
c.Infof("Total of %d artifact(s) downloaded", len(artifacts))
return c.SetOutputs(ctx, "download-path", dest)
}
func fetch(ctx context.Context, c *action.Context, src *source, art artifact.Artifact, target string, skipDecompress bool) (bool, error) {
if err := c.Container.CopyTarStream(ctx, target, func(*container.TarWriter) error { return nil }); err != nil {
return false, err
}
signed, err := src.signedURL(ctx, art)
if err != nil {
return false, err
}
for attempt := 1; attempt <= downloadAttempts; attempt++ {
digest, err := fetchOnce(ctx, c, src, signed, target, skipDecompress)
if err == nil {
return art.Digest != "" && art.Digest != digest, nil
}
common.Logger(ctx).Debugf("Failed to download artifact after %d retries due to %v. Retrying in 5 seconds...", attempt, err)
select {
case <-ctx.Done():
return false, ctx.Err()
case <-time.After(5 * time.Second):
}
}
return false, action.Failf("Unable to download and extract artifact: Artifact download failed after %d retries.", downloadAttempts)
}