mirror of
https://gitea.com/gitea/runner.git
synced 2026-11-09 16:50:28 +00:00
175 lines
5.8 KiB
Go
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)
|
|
}
|