mirror of
https://github.com/go-gitea/gitea.git
synced 2026-05-06 04:01:05 +09:00
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: wxiaoguang <2114189+wxiaoguang@users.noreply.github.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
549 lines
22 KiB
Go
549 lines
22 KiB
Go
// Copyright 2023 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package integration
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
"crypto/md5"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
auth_model "code.gitea.io/gitea/models/auth"
|
|
repo_model "code.gitea.io/gitea/models/repo"
|
|
"code.gitea.io/gitea/models/unittest"
|
|
user_model "code.gitea.io/gitea/models/user"
|
|
"code.gitea.io/gitea/tests"
|
|
|
|
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
type uploadArtifactResponse struct {
|
|
FileContainerResourceURL string `json:"fileContainerResourceUrl"`
|
|
}
|
|
|
|
type getUploadArtifactRequest struct {
|
|
Type string
|
|
Name string
|
|
RetentionDays int64
|
|
}
|
|
|
|
func prepareTestEnvActionsArtifacts(t *testing.T) func() {
|
|
t.Helper()
|
|
f := tests.PrepareTestEnv(t, 1)
|
|
tests.PrepareArtifactsStorage(t)
|
|
return f
|
|
}
|
|
|
|
func TestActionsArtifactUploadSingleFile(t *testing.T) {
|
|
defer prepareTestEnvActionsArtifacts(t)()
|
|
|
|
// acquire artifact upload url
|
|
req := NewRequestWithJSON(t, "POST", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts", getUploadArtifactRequest{
|
|
Type: "actions_storage",
|
|
Name: "artifact",
|
|
}).AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
|
resp := MakeRequest(t, req, http.StatusOK)
|
|
uploadResp := DecodeJSON(t, resp, &uploadArtifactResponse{})
|
|
assert.Contains(t, uploadResp.FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts")
|
|
|
|
// get upload url
|
|
idx := strings.Index(uploadResp.FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/")
|
|
url := uploadResp.FileContainerResourceURL[idx:] + "?itemPath=artifact/abc-2.txt"
|
|
|
|
// upload artifact chunk
|
|
body := strings.Repeat("C", 1024)
|
|
req = NewRequestWithBody(t, "PUT", url, strings.NewReader(body)).
|
|
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a").
|
|
SetHeader("Content-Range", "bytes 0-1023/1024").
|
|
SetHeader("x-tfs-filelength", "1024").
|
|
SetHeader("x-actions-results-md5", "XVlf820rMInUi64wmMi6EA==") // base64(md5(body))
|
|
MakeRequest(t, req, http.StatusOK)
|
|
|
|
t.Logf("Create artifact confirm")
|
|
|
|
// confirm artifact upload
|
|
req = NewRequest(t, "PATCH", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts?artifactName=artifact-single").
|
|
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
|
MakeRequest(t, req, http.StatusOK)
|
|
}
|
|
|
|
func TestActionsArtifactUploadInvalidHash(t *testing.T) {
|
|
defer prepareTestEnvActionsArtifacts(t)()
|
|
|
|
// artifact id 54321 not exist
|
|
url := "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts/8e5b948a454515dbabfc7eb718ddddddd/upload?itemPath=artifact/abc.txt"
|
|
body := strings.Repeat("A", 1024)
|
|
req := NewRequestWithBody(t, "PUT", url, strings.NewReader(body)).
|
|
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a").
|
|
SetHeader("Content-Range", "bytes 0-1023/1024").
|
|
SetHeader("x-tfs-filelength", "1024").
|
|
SetHeader("x-actions-results-md5", "1HsSe8LeLWh93ILaw1TEFQ==") // base64(md5(body))
|
|
resp := MakeRequest(t, req, http.StatusBadRequest)
|
|
assert.Contains(t, resp.Body.String(), "Invalid artifact hash")
|
|
}
|
|
|
|
func TestActionsArtifactConfirmUploadWithoutName(t *testing.T) {
|
|
defer prepareTestEnvActionsArtifacts(t)()
|
|
|
|
req := NewRequest(t, "PATCH", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts").
|
|
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
|
resp := MakeRequest(t, req, http.StatusBadRequest)
|
|
assert.Contains(t, resp.Body.String(), "artifact name is empty")
|
|
}
|
|
|
|
func TestActionsArtifactUploadWithoutToken(t *testing.T) {
|
|
defer prepareTestEnvActionsArtifacts(t)()
|
|
|
|
req := NewRequestWithJSON(t, "POST", "/api/actions_pipeline/_apis/pipelines/workflows/1/artifacts", nil)
|
|
MakeRequest(t, req, http.StatusUnauthorized)
|
|
}
|
|
|
|
type (
|
|
listArtifactsResponseItem struct {
|
|
Name string `json:"name"`
|
|
FileContainerResourceURL string `json:"fileContainerResourceUrl"`
|
|
}
|
|
listArtifactsResponse struct {
|
|
Count int64 `json:"count"`
|
|
Value []listArtifactsResponseItem `json:"value"`
|
|
}
|
|
downloadArtifactResponseItem struct {
|
|
Path string `json:"path"`
|
|
ItemType string `json:"itemType"`
|
|
ContentLocation string `json:"contentLocation"`
|
|
}
|
|
downloadArtifactResponse struct {
|
|
Value []downloadArtifactResponseItem `json:"value"`
|
|
}
|
|
)
|
|
|
|
func TestActionsArtifactDownload(t *testing.T) {
|
|
defer prepareTestEnvActionsArtifacts(t)()
|
|
|
|
req := NewRequest(t, "GET", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts").
|
|
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
|
resp := MakeRequest(t, req, http.StatusOK)
|
|
listResp := DecodeJSON(t, resp, &listArtifactsResponse{})
|
|
assert.Equal(t, int64(2), listResp.Count)
|
|
|
|
// Return list might be in any order. Get one file.
|
|
var artifactIdx int
|
|
for i, artifact := range listResp.Value {
|
|
if artifact.Name == "artifact-download" {
|
|
artifactIdx = i
|
|
break
|
|
}
|
|
}
|
|
assert.NotNil(t, artifactIdx)
|
|
assert.Equal(t, "artifact-download", listResp.Value[artifactIdx].Name)
|
|
assert.Contains(t, listResp.Value[artifactIdx].FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts")
|
|
|
|
idx := strings.Index(listResp.Value[artifactIdx].FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/")
|
|
url := listResp.Value[artifactIdx].FileContainerResourceURL[idx+1:] + "?itemPath=artifact-download"
|
|
req = NewRequest(t, "GET", url).
|
|
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
|
resp = MakeRequest(t, req, http.StatusOK)
|
|
downloadResp := DecodeJSON(t, resp, &downloadArtifactResponse{})
|
|
assert.Len(t, downloadResp.Value, 1)
|
|
assert.Equal(t, "artifact-download/abc.txt", downloadResp.Value[0].Path)
|
|
assert.Equal(t, "file", downloadResp.Value[0].ItemType)
|
|
assert.Contains(t, downloadResp.Value[0].ContentLocation, "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts")
|
|
|
|
idx = strings.Index(downloadResp.Value[0].ContentLocation, "/api/actions_pipeline/_apis/pipelines/")
|
|
url = downloadResp.Value[0].ContentLocation[idx:]
|
|
req = NewRequest(t, "GET", url).
|
|
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
|
resp = MakeRequest(t, req, http.StatusOK)
|
|
|
|
body := strings.Repeat("A", 1024)
|
|
assert.Equal(t, body, resp.Body.String())
|
|
}
|
|
|
|
func TestActionsArtifactUploadMultipleFile(t *testing.T) {
|
|
defer prepareTestEnvActionsArtifacts(t)()
|
|
|
|
const testArtifactName = "multi-files"
|
|
|
|
// acquire artifact upload url
|
|
req := NewRequestWithJSON(t, "POST", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts", getUploadArtifactRequest{
|
|
Type: "actions_storage",
|
|
Name: testArtifactName,
|
|
}).AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
|
resp := MakeRequest(t, req, http.StatusOK)
|
|
uploadResp := DecodeJSON(t, resp, &uploadArtifactResponse{})
|
|
assert.Contains(t, uploadResp.FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts")
|
|
|
|
type uploadingFile struct {
|
|
Path string
|
|
Content string
|
|
MD5 string
|
|
}
|
|
|
|
files := []uploadingFile{
|
|
{
|
|
Path: "abc-3.txt",
|
|
Content: strings.Repeat("D", 1024),
|
|
MD5: "9nqj7E8HZmfQtPifCJ5Zww==",
|
|
},
|
|
{
|
|
Path: "xyz/def-2.txt",
|
|
Content: strings.Repeat("E", 1024),
|
|
MD5: "/s1kKvxeHlUX85vaTaVxuA==",
|
|
},
|
|
}
|
|
|
|
for _, f := range files {
|
|
// get upload url
|
|
idx := strings.Index(uploadResp.FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/")
|
|
url := uploadResp.FileContainerResourceURL[idx:] + "?itemPath=" + testArtifactName + "/" + f.Path
|
|
|
|
// upload artifact chunk
|
|
req = NewRequestWithBody(t, "PUT", url, strings.NewReader(f.Content)).
|
|
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a").
|
|
SetHeader("Content-Range", "bytes 0-1023/1024").
|
|
SetHeader("x-tfs-filelength", "1024").
|
|
SetHeader("x-actions-results-md5", f.MD5) // base64(md5(body))
|
|
MakeRequest(t, req, http.StatusOK)
|
|
}
|
|
|
|
t.Logf("Create artifact confirm")
|
|
|
|
// confirm artifact upload
|
|
req = NewRequest(t, "PATCH", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts?artifactName="+testArtifactName).
|
|
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
|
MakeRequest(t, req, http.StatusOK)
|
|
}
|
|
|
|
func TestActionsArtifactDownloadMultiFiles(t *testing.T) {
|
|
defer prepareTestEnvActionsArtifacts(t)()
|
|
|
|
const testArtifactName = "multi-file-download"
|
|
|
|
req := NewRequest(t, "GET", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts").
|
|
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
|
resp := MakeRequest(t, req, http.StatusOK)
|
|
listResp := DecodeJSON(t, resp, &listArtifactsResponse{})
|
|
assert.Equal(t, int64(2), listResp.Count)
|
|
|
|
var fileContainerResourceURL string
|
|
for _, v := range listResp.Value {
|
|
if v.Name == testArtifactName {
|
|
fileContainerResourceURL = v.FileContainerResourceURL
|
|
break
|
|
}
|
|
}
|
|
assert.Contains(t, fileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts")
|
|
|
|
idx := strings.Index(fileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/")
|
|
url := fileContainerResourceURL[idx+1:] + "?itemPath=" + testArtifactName
|
|
req = NewRequest(t, "GET", url).
|
|
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
|
resp = MakeRequest(t, req, http.StatusOK)
|
|
downloadResp := DecodeJSON(t, resp, &downloadArtifactResponse{})
|
|
assert.Len(t, downloadResp.Value, 2)
|
|
|
|
downloads := [][]string{{"multi-file-download/abc.txt", "B"}, {"multi-file-download/xyz/def.txt", "C"}}
|
|
for _, v := range downloadResp.Value {
|
|
var bodyChar string
|
|
var path string
|
|
for _, d := range downloads {
|
|
if v.Path == d[0] {
|
|
path = d[0]
|
|
bodyChar = d[1]
|
|
break
|
|
}
|
|
}
|
|
value := v
|
|
assert.Equal(t, path, value.Path)
|
|
assert.Equal(t, "file", value.ItemType)
|
|
assert.Contains(t, value.ContentLocation, "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts")
|
|
|
|
idx = strings.Index(value.ContentLocation, "/api/actions_pipeline/_apis/pipelines/")
|
|
url = value.ContentLocation[idx:]
|
|
req = NewRequest(t, "GET", url).
|
|
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
|
resp = MakeRequest(t, req, http.StatusOK)
|
|
assert.Equal(t, strings.Repeat(bodyChar, 1024), resp.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestActionsArtifactUploadWithRetentionDays(t *testing.T) {
|
|
defer prepareTestEnvActionsArtifacts(t)()
|
|
|
|
// acquire artifact upload url
|
|
req := NewRequestWithJSON(t, "POST", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts", getUploadArtifactRequest{
|
|
Type: "actions_storage",
|
|
Name: "artifact-retention-days",
|
|
RetentionDays: 9,
|
|
}).AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
|
resp := MakeRequest(t, req, http.StatusOK)
|
|
uploadResp := DecodeJSON(t, resp, &uploadArtifactResponse{})
|
|
assert.Contains(t, uploadResp.FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts")
|
|
assert.Contains(t, uploadResp.FileContainerResourceURL, "?retentionDays=9")
|
|
|
|
// get upload url
|
|
idx := strings.Index(uploadResp.FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/")
|
|
url := uploadResp.FileContainerResourceURL[idx:] + "&itemPath=artifact-retention-days/abc.txt"
|
|
|
|
// upload artifact chunk
|
|
body := strings.Repeat("A", 1024)
|
|
req = NewRequestWithBody(t, "PUT", url, strings.NewReader(body)).
|
|
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a").
|
|
SetHeader("Content-Range", "bytes 0-1023/1024").
|
|
SetHeader("x-tfs-filelength", "1024").
|
|
SetHeader("x-actions-results-md5", "1HsSe8LeLWh93ILaw1TEFQ==") // base64(md5(body))
|
|
MakeRequest(t, req, http.StatusOK)
|
|
|
|
t.Logf("Create artifact confirm")
|
|
|
|
// confirm artifact upload
|
|
req = NewRequest(t, "PATCH", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts?artifactName=artifact-retention-days").
|
|
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
|
MakeRequest(t, req, http.StatusOK)
|
|
}
|
|
|
|
func TestActionsArtifactOverwrite(t *testing.T) {
|
|
defer prepareTestEnvActionsArtifacts(t)()
|
|
|
|
{
|
|
// download old artifact uploaded by tests above, it should 1024 A
|
|
req := NewRequest(t, "GET", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts").
|
|
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
|
resp := MakeRequest(t, req, http.StatusOK)
|
|
listResp := DecodeJSON(t, resp, &listArtifactsResponse{})
|
|
|
|
idx := strings.Index(listResp.Value[0].FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/")
|
|
url := listResp.Value[0].FileContainerResourceURL[idx+1:] + "?itemPath=artifact-download"
|
|
req = NewRequest(t, "GET", url).
|
|
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
|
resp = MakeRequest(t, req, http.StatusOK)
|
|
downloadResp := DecodeJSON(t, resp, &downloadArtifactResponse{})
|
|
|
|
idx = strings.Index(downloadResp.Value[0].ContentLocation, "/api/actions_pipeline/_apis/pipelines/")
|
|
url = downloadResp.Value[0].ContentLocation[idx:]
|
|
req = NewRequest(t, "GET", url).
|
|
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
|
resp = MakeRequest(t, req, http.StatusOK)
|
|
body := strings.Repeat("A", 1024)
|
|
assert.Equal(t, resp.Body.String(), body)
|
|
}
|
|
|
|
{
|
|
// upload same artifact, it uses 4096 B
|
|
req := NewRequestWithJSON(t, "POST", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts", getUploadArtifactRequest{
|
|
Type: "actions_storage",
|
|
Name: "artifact-download",
|
|
}).AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
|
resp := MakeRequest(t, req, http.StatusOK)
|
|
uploadResp := DecodeJSON(t, resp, &uploadArtifactResponse{})
|
|
|
|
idx := strings.Index(uploadResp.FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/")
|
|
url := uploadResp.FileContainerResourceURL[idx:] + "?itemPath=artifact-download/abc.txt"
|
|
body := strings.Repeat("B", 4096)
|
|
req = NewRequestWithBody(t, "PUT", url, strings.NewReader(body)).
|
|
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a").
|
|
SetHeader("Content-Range", "bytes 0-4095/4096").
|
|
SetHeader("x-tfs-filelength", "4096").
|
|
SetHeader("x-actions-results-md5", "wUypcJFeZCK5T6r4lfqzqg==") // base64(md5(body))
|
|
MakeRequest(t, req, http.StatusOK)
|
|
|
|
// confirm artifact upload
|
|
req = NewRequest(t, "PATCH", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts?artifactName=artifact-download").
|
|
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
|
MakeRequest(t, req, http.StatusOK)
|
|
}
|
|
|
|
{
|
|
// download artifact again, it should 4096 B
|
|
req := NewRequest(t, "GET", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts").
|
|
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
|
resp := MakeRequest(t, req, http.StatusOK)
|
|
listResp := DecodeJSON(t, resp, &listArtifactsResponse{})
|
|
|
|
var uploadedItem listArtifactsResponseItem
|
|
for _, item := range listResp.Value {
|
|
if item.Name == "artifact-download" {
|
|
uploadedItem = item
|
|
break
|
|
}
|
|
}
|
|
assert.Equal(t, "artifact-download", uploadedItem.Name)
|
|
|
|
idx := strings.Index(uploadedItem.FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/")
|
|
url := uploadedItem.FileContainerResourceURL[idx+1:] + "?itemPath=artifact-download"
|
|
req = NewRequest(t, "GET", url).
|
|
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
|
resp = MakeRequest(t, req, http.StatusOK)
|
|
downloadResp := DecodeJSON(t, resp, &downloadArtifactResponse{})
|
|
|
|
idx = strings.Index(downloadResp.Value[0].ContentLocation, "/api/actions_pipeline/_apis/pipelines/")
|
|
url = downloadResp.Value[0].ContentLocation[idx:]
|
|
req = NewRequest(t, "GET", url).
|
|
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
|
resp = MakeRequest(t, req, http.StatusOK)
|
|
body := strings.Repeat("B", 4096)
|
|
assert.Equal(t, resp.Body.String(), body)
|
|
}
|
|
}
|
|
|
|
func TestActionRunAttemptArtifact(t *testing.T) {
|
|
defer prepareTestEnvActionsArtifacts(t)()
|
|
|
|
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
|
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
|
session := loginUser(t, user2.Name)
|
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
|
|
|
apiRepo := createActionsTestRepo(t, token, "actions-run-attempt-artifact", false)
|
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID})
|
|
httpContext := NewAPITestContext(t, user2.Name, repo.Name, auth_model.AccessTokenScopeWriteRepository)
|
|
defer doAPIDeleteRepository(httpContext)(t)
|
|
|
|
runner := newMockRunner()
|
|
runner.registerAsRepoRunner(t, repo.OwnerName, repo.Name, "mock-runner", []string{"ubuntu-latest"}, false)
|
|
|
|
wfTreePath := ".gitea/workflows/run-attempt-artifact.yml"
|
|
wfFileContent := `name: run-attempt-artifact
|
|
on:
|
|
workflow_dispatch:
|
|
jobs:
|
|
job1:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- run: echo 'job1'
|
|
`
|
|
opts := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "create "+wfTreePath, wfFileContent)
|
|
createWorkflowFile(t, token, user2.Name, repo.Name, wfTreePath, opts)
|
|
|
|
urlStr := fmt.Sprintf("/%s/%s/actions/run?workflow=%s", user2.Name, repo.Name, "run-attempt-artifact.yml")
|
|
req := NewRequestWithValues(t, "POST", urlStr, map[string]string{
|
|
"ref": "refs/heads/main",
|
|
})
|
|
session.MakeRequest(t, req, http.StatusSeeOther)
|
|
|
|
t.Run("testActionRunAttemptArtifactV3", func(t *testing.T) {
|
|
testActionRunAttemptArtifactV3(t, repo, session, runner)
|
|
})
|
|
|
|
t.Run("testActionRunAttemptArtifactV4", func(t *testing.T) {
|
|
testActionRunAttemptArtifactV4(t, repo, session, runner)
|
|
})
|
|
})
|
|
}
|
|
|
|
func testActionRunAttemptArtifactV3(t *testing.T, repo *repo_model.Repository, session *TestSession, runner *mockRunner) {
|
|
// first run
|
|
task1 := runner.fetchTask(t)
|
|
_, job1, run := getTaskAndJobAndRunByTaskID(t, task1.Id)
|
|
require.NotZero(t, job1.RunAttemptID)
|
|
taskToken1 := task1.Context.GetFields()["gitea_runtime_token"].GetStringValue()
|
|
require.NotEmpty(t, taskToken1)
|
|
uploadTestArtifactFile(t, run.ID, taskToken1, "artifact-attempt-1", "attempt-1.txt", strings.Repeat("A", 32))
|
|
uploadTestArtifactFile(t, run.ID, taskToken1, "artifact-shared", "shared.txt", strings.Repeat("C", 32))
|
|
attempt1Names := listArtifactNamesForRun(t, run.ID, taskToken1)
|
|
assert.ElementsMatch(t, []string{"artifact-attempt-1", "artifact-shared"}, attempt1Names)
|
|
|
|
runner.execTask(t, task1, &mockTaskOutcome{result: runnerv1.Result_RESULT_SUCCESS}) // complete first run
|
|
|
|
// rerun
|
|
req := NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/rerun", repo.OwnerName, repo.Name, run.ID))
|
|
session.MakeRequest(t, req, http.StatusOK)
|
|
task2 := runner.fetchTask(t)
|
|
_, job2, _ := getTaskAndJobAndRunByTaskID(t, task2.Id)
|
|
require.NotZero(t, job2.RunAttemptID)
|
|
assert.NotEqual(t, job1.RunAttemptID, job2.RunAttemptID)
|
|
taskToken2 := task2.Context.GetFields()["gitea_runtime_token"].GetStringValue()
|
|
require.NotEmpty(t, taskToken2)
|
|
uploadTestArtifactFile(t, run.ID, taskToken2, "artifact-attempt-2", "attempt-2.txt", strings.Repeat("B", 32))
|
|
uploadTestArtifactFile(t, run.ID, taskToken2, "artifact-shared", "shared.txt", strings.Repeat("D", 32))
|
|
attempt2Names := listArtifactNamesForRun(t, run.ID, taskToken2)
|
|
assert.ElementsMatch(t, []string{"artifact-attempt-2", "artifact-shared"}, attempt2Names)
|
|
assert.NotContains(t, attempt2Names, "artifact-attempt-1")
|
|
|
|
// "artifact-attempt-1" belongs to the first attempt, so the rerun token cannot access it
|
|
req = NewRequest(t, "GET", fmt.Sprintf("/api/actions_pipeline/_apis/pipelines/workflows/%d/artifacts/%x/download_url?itemPath=artifact-attempt-1", run.ID, md5.Sum([]byte("artifact-attempt-1")))).
|
|
AddTokenAuth(taskToken2)
|
|
MakeRequest(t, req, http.StatusNotFound)
|
|
|
|
// "artifact-shared" for each attempt has different content
|
|
sharedContent1 := downloadArtifactFileContentByAttempt(t, session, repo.OwnerName, repo.Name, run.ID, "artifact-shared", 1, "shared.txt")
|
|
assert.Equal(t, strings.Repeat("C", 32), sharedContent1)
|
|
sharedContent2 := downloadArtifactFileContentByAttempt(t, session, repo.OwnerName, repo.Name, run.ID, "artifact-shared", 2, "shared.txt")
|
|
assert.Equal(t, strings.Repeat("D", 32), sharedContent2)
|
|
}
|
|
|
|
func uploadTestArtifactFile(t *testing.T, runID int64, authToken, artifactName, fileName, content string) {
|
|
t.Helper()
|
|
|
|
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/actions_pipeline/_apis/pipelines/workflows/%d/artifacts", runID), getUploadArtifactRequest{
|
|
Type: "actions_storage",
|
|
Name: artifactName,
|
|
}).AddTokenAuth(authToken)
|
|
resp := MakeRequest(t, req, http.StatusOK)
|
|
uploadResp := DecodeJSON(t, resp, &uploadArtifactResponse{})
|
|
|
|
idx := strings.Index(uploadResp.FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/")
|
|
uploadURL := uploadResp.FileContainerResourceURL[idx:] + "?itemPath=" + artifactName + "/" + fileName
|
|
contentLen := strconv.Itoa(len(content))
|
|
contentMD5 := md5.Sum([]byte(content))
|
|
req = NewRequestWithBody(t, "PUT", uploadURL, strings.NewReader(content)).
|
|
AddTokenAuth(authToken).
|
|
SetHeader("Content-Range", fmt.Sprintf("bytes 0-%d/%d", len(content)-1, len(content))).
|
|
SetHeader("x-tfs-filelength", contentLen).
|
|
SetHeader("x-actions-results-md5", base64.StdEncoding.EncodeToString(contentMD5[:]))
|
|
MakeRequest(t, req, http.StatusOK)
|
|
|
|
req = NewRequest(t, "PATCH", fmt.Sprintf("/api/actions_pipeline/_apis/pipelines/workflows/%d/artifacts?artifactName=%s", runID, artifactName)).
|
|
AddTokenAuth(authToken)
|
|
MakeRequest(t, req, http.StatusOK)
|
|
}
|
|
|
|
func listArtifactNamesForRun(t *testing.T, runID int64, taskToken string) []string {
|
|
t.Helper()
|
|
|
|
req := NewRequest(t, "GET", fmt.Sprintf("/api/actions_pipeline/_apis/pipelines/workflows/%d/artifacts", runID)).
|
|
AddTokenAuth(taskToken)
|
|
resp := MakeRequest(t, req, http.StatusOK)
|
|
listResp := DecodeJSON(t, resp, &listArtifactsResponse{})
|
|
|
|
names := make([]string, 0, len(listResp.Value))
|
|
for _, item := range listResp.Value {
|
|
names = append(names, item.Name)
|
|
}
|
|
return names
|
|
}
|
|
|
|
func downloadArtifactFileContentByAttempt(t *testing.T, session *TestSession, owner, repo string, runID int64, artifactName string, attempt int64, fileName string) string {
|
|
t.Helper()
|
|
|
|
req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/artifacts/%s?attempt=%d", owner, repo, runID, url.PathEscape(artifactName), attempt))
|
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
|
|
|
zr, err := zip.NewReader(bytes.NewReader(resp.Body.Bytes()), int64(resp.Body.Len()))
|
|
require.NoError(t, err)
|
|
for _, f := range zr.File {
|
|
if f.Name != fileName {
|
|
continue
|
|
}
|
|
rc, err := f.Open()
|
|
require.NoError(t, err)
|
|
content, err := io.ReadAll(rc)
|
|
rc.Close()
|
|
require.NoError(t, err)
|
|
return string(content)
|
|
}
|
|
|
|
require.FailNowf(t, "artifact file not found", "artifact %q attempt %d does not contain file %q", artifactName, attempt, fileName)
|
|
return ""
|
|
}
|