fix(actions): let a rerun of selected jobs read the previous attempt's artifacts (#38857) (#38901)

Backport #38857

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.
This commit is contained in:
bircni
2026-08-13 15:31:42 +02:00
committed by GitHub
parent 0acbcc58a7
commit ba4db8a2d9
9 changed files with 368 additions and 63 deletions
@@ -947,7 +947,24 @@ func testActionRunAttemptArtifactV4(t *testing.T, repo *repo_model.Repository, s
assert.Equal(t, strings.Repeat("D", 32), downloadRepoArtifactV4Content(t, session, sharedArtifactsResp.Entries[1].ArchiveDownloadURL))
}
func uploadTestArtifactFileV4(t *testing.T, runID, jobID int64, authToken, artifactName, content string) {
func downloadArtifactContentV4ByTask(t *testing.T, runID, jobID int64, taskToken, artifactName string) string {
t.Helper()
req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/GetSignedArtifactURL", toProtoJSON(&actions.GetSignedArtifactURLRequest{
Name: artifactName,
WorkflowRunBackendId: strconv.FormatInt(runID, 10),
WorkflowJobRunBackendId: strconv.FormatInt(jobID, 10),
})).AddTokenAuth(taskToken)
resp := MakeRequest(t, req, http.StatusOK)
var urlResp actions.GetSignedArtifactURLResponse
require.NoError(t, protojson.Unmarshal(resp.Body.Bytes(), &urlResp))
require.NotEmpty(t, urlResp.SignedUrl)
return MakeRequest(t, NewRequest(t, "GET", urlResp.SignedUrl), http.StatusOK).Body.String()
}
// createTestArtifactV4 only creates the artifact record, leaving it pending until it is uploaded and finalized
func createTestArtifactV4(t *testing.T, runID, jobID int64, authToken, artifactName string) string {
t.Helper()
req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact", toProtoJSON(&actions.CreateArtifactRequest{
@@ -958,11 +975,17 @@ func uploadTestArtifactFileV4(t *testing.T, runID, jobID int64, authToken, artif
MimeType: wrapperspb.String("application/zip"),
})).AddTokenAuth(authToken)
resp := MakeRequest(t, req, http.StatusOK)
var uploadResp actions.CreateArtifactResponse
require.NoError(t, protojson.Unmarshal(resp.Body.Bytes(), &uploadResp))
require.True(t, uploadResp.Ok)
var createResp actions.CreateArtifactResponse
require.NoError(t, protojson.Unmarshal(resp.Body.Bytes(), &createResp))
require.True(t, createResp.Ok)
return createResp.SignedUploadUrl
}
req = NewRequestWithBody(t, "PUT", uploadResp.SignedUploadUrl+"&comp=appendBlock", strings.NewReader(content))
func uploadTestArtifactFileV4(t *testing.T, runID, jobID int64, authToken, artifactName, content string) {
t.Helper()
signedUploadURL := createTestArtifactV4(t, runID, jobID, authToken, artifactName)
req := NewRequestWithBody(t, "PUT", signedUploadURL+"&comp=appendBlock", strings.NewReader(content))
MakeRequest(t, req, http.StatusCreated)
sum := sha256.Sum256([]byte(content))
@@ -973,30 +996,59 @@ func uploadTestArtifactFileV4(t *testing.T, runID, jobID int64, authToken, artif
WorkflowRunBackendId: strconv.FormatInt(runID, 10),
WorkflowJobRunBackendId: strconv.FormatInt(jobID, 10),
})).AddTokenAuth(authToken)
resp = MakeRequest(t, req, http.StatusOK)
resp := MakeRequest(t, req, http.StatusOK)
var finalizeResp actions.FinalizeArtifactResponse
require.NoError(t, protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp))
require.True(t, finalizeResp.Ok)
}
func listArtifactsForRunV4(t *testing.T, taskToken string, req *actions.ListArtifactsRequest) []*actions.ListArtifactsResponse_MonolithArtifact {
t.Helper()
httpReq := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/ListArtifacts", toProtoJSON(req)).AddTokenAuth(taskToken)
resp := MakeRequest(t, httpReq, http.StatusOK)
var listResp actions.ListArtifactsResponse
require.NoError(t, protojson.Unmarshal(resp.Body.Bytes(), &listResp))
return listResp.Artifacts
}
func listArtifactNamesForRunV4(t *testing.T, runID, jobID int64, taskToken string) []string {
t.Helper()
req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/ListArtifacts", toProtoJSON(&actions.ListArtifactsRequest{
artifacts := listArtifactsForRunV4(t, taskToken, &actions.ListArtifactsRequest{
WorkflowRunBackendId: strconv.FormatInt(runID, 10),
WorkflowJobRunBackendId: strconv.FormatInt(jobID, 10),
})).AddTokenAuth(taskToken)
resp := MakeRequest(t, req, http.StatusOK)
var listResp actions.ListArtifactsResponse
require.NoError(t, protojson.Unmarshal(resp.Body.Bytes(), &listResp))
})
names := make([]string, 0, len(listResp.Artifacts))
for _, item := range listResp.Artifacts {
names := make([]string, 0, len(artifacts))
for _, item := range artifacts {
names = append(names, item.Name)
}
return names
}
func listArtifactIDForRunV4(t *testing.T, runID, jobID int64, taskToken, artifactName string) int64 {
t.Helper()
artifacts := listArtifactsForRunV4(t, taskToken, &actions.ListArtifactsRequest{
NameFilter: wrapperspb.String(artifactName),
WorkflowRunBackendId: strconv.FormatInt(runID, 10),
WorkflowJobRunBackendId: strconv.FormatInt(jobID, 10),
})
require.Len(t, artifacts, 1)
return artifacts[0].DatabaseId
}
func listArtifactsByIDV4(t *testing.T, runID, jobID, artifactID int64, taskToken string) []*actions.ListArtifactsResponse_MonolithArtifact {
t.Helper()
return listArtifactsForRunV4(t, taskToken, &actions.ListArtifactsRequest{
IdFilter: wrapperspb.Int64(artifactID),
WorkflowRunBackendId: strconv.FormatInt(runID, 10),
WorkflowJobRunBackendId: strconv.FormatInt(jobID, 10),
})
}
func downloadRepoArtifactV4Content(t *testing.T, session *TestSession, archiveDownloadURL string) string {
t.Helper()