mirror of
https://github.com/go-gitea/gitea.git
synced 2026-08-14 08:52:53 +00:00
c186cc4b8d
Fixes #38773 ## Background Artifacts became attempt-scoped in #37119, and the runner-facing artifact APIs filter strictly by the attempt of the running job. "Re-run failed jobs" creates a new attempt whose passed-through jobs never upload their artifacts again, so a re-run job that downloads one of them fails with "artifact not found". ## Fix The read paths (v3 and v4 list and download) now resolve artifacts across the running job's attempt plus the attempts it inherits from, and an inherited artifact is shadowed by a same-named one from a newer attempt. ## Note GitHub's documentation does not document these behaviors. The conclusions below are based on manual testing, so consistency with GitHub cannot be guaranteed. - In a "partial re-run", a job can download artifacts uploaded by an earlier attempt, every attempt keeps its own copy of a name, and a lookup by name resolves to the newest one. - A full "Re-run all jobs" never downloads artifacts from earlier attempts. --------- Co-authored-by: bircni <bircni@icloud.com>
106 lines
3.5 KiB
Go
106 lines
3.5 KiB
Go
// Copyright 2023 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package actions
|
|
|
|
import (
|
|
"crypto/md5"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"gitea.dev/models/actions"
|
|
"gitea.dev/modules/log"
|
|
"gitea.dev/modules/util"
|
|
)
|
|
|
|
const (
|
|
artifactXTfsFileLengthHeader = "x-tfs-filelength"
|
|
artifactXActionsResultsMD5Header = "x-actions-results-md5"
|
|
)
|
|
|
|
// The rules are from https://github.com/actions/toolkit/blob/main/packages/artifact/src/internal/upload/path-and-artifact-name-validation.ts
|
|
const invalidArtifactNameChars = "\\/\":<>|*?\r\n"
|
|
|
|
func validateArtifactName(ctx *ArtifactContext, artifactName string) bool {
|
|
if strings.ContainsAny(artifactName, invalidArtifactNameChars) {
|
|
log.Error("Error checking artifact name contains invalid character")
|
|
ctx.HTTPError(http.StatusBadRequest, "Error checking artifact name contains invalid character")
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func validateRunID(ctx *ArtifactContext) (*actions.ActionTask, int64, bool) {
|
|
task := ctx.ActionTask
|
|
runID := ctx.PathParamInt64("run_id")
|
|
if task.Job.RunID != runID {
|
|
log.Error("Error runID not match")
|
|
ctx.HTTPError(http.StatusBadRequest, "run-id does not match")
|
|
return nil, 0, false
|
|
}
|
|
return task, runID, true
|
|
}
|
|
|
|
func validateRunIDV4(ctx *ArtifactContext, rawRunID string) (*actions.ActionTask, int64, bool) {
|
|
task := ctx.ActionTask
|
|
runID, err := strconv.ParseInt(rawRunID, 10, 64)
|
|
if err != nil || task.Job.RunID != runID {
|
|
log.Error("Error runID not match")
|
|
ctx.HTTPError(http.StatusBadRequest, "run-id does not match")
|
|
return nil, 0, false
|
|
}
|
|
return task, runID, true
|
|
}
|
|
|
|
// readableArtifactAttemptIDs resolves the attempts a task may read artifacts from:
|
|
// its own attempt, plus the attempts it inherits from when only a subset of the run's jobs was re-run.
|
|
func readableArtifactAttemptIDs(ctx *ArtifactContext, task *actions.ActionTask) ([]int64, bool) {
|
|
attemptIDs, err := actions.GetArtifactAttemptIDs(ctx, task.Job)
|
|
if err != nil {
|
|
log.Error("Error getting readable artifact attempts: %v", err)
|
|
ctx.HTTPError(http.StatusInternalServerError, "Error getting readable artifact attempts")
|
|
return nil, false
|
|
}
|
|
return attemptIDs, true
|
|
}
|
|
|
|
func validateArtifactHash(ctx *ArtifactContext, artifactName string) bool {
|
|
paramHash := ctx.PathParam("artifact_hash")
|
|
// use artifact name to create upload url
|
|
artifactHash := fmt.Sprintf("%x", md5.Sum([]byte(artifactName)))
|
|
if paramHash == artifactHash {
|
|
return true
|
|
}
|
|
log.Error("Invalid artifact hash: %s", paramHash)
|
|
ctx.HTTPError(http.StatusBadRequest, "Invalid artifact hash")
|
|
return false
|
|
}
|
|
|
|
func parseArtifactItemPath(ctx *ArtifactContext) (string, string, bool) {
|
|
// itemPath is generated from upload-artifact action
|
|
// it's formatted as {artifact_name}/{artfict_path_in_runner}
|
|
// runner in host mode on Windows, itemPath is joined by Windows slash '\'
|
|
itemPath := util.PathJoinRelX(ctx.Req.URL.Query().Get("itemPath"))
|
|
artifactName := strings.Split(itemPath, "/")[0]
|
|
artifactPath := strings.TrimPrefix(itemPath, artifactName+"/")
|
|
if !validateArtifactHash(ctx, artifactName) {
|
|
return "", "", false
|
|
}
|
|
if !validateArtifactName(ctx, artifactName) {
|
|
return "", "", false
|
|
}
|
|
return artifactName, artifactPath, true
|
|
}
|
|
|
|
// getUploadFileSize returns the size of the file to be uploaded.
|
|
// The raw size is the size of the file as reported by the header X-TFS-FileLength.
|
|
func getUploadFileSize(ctx *ArtifactContext) int64 {
|
|
xTfsLength, _ := strconv.ParseInt(ctx.Req.Header.Get(artifactXTfsFileLengthHeader), 10, 64)
|
|
if xTfsLength > 0 {
|
|
return xTfsLength
|
|
}
|
|
return ctx.Req.ContentLength
|
|
}
|