mirror of
https://github.com/go-gitea/gitea.git
synced 2026-08-09 05:38:54 +09:00
2087d4a1a5
Pairs with https://gitea.com/gitea/runner/pulls/1143. Gitea depends on `gitea.com/gitea/runner` for exactly two packages: `act/model` and `act/exprparser`, the workflow model and the expression evaluator it needs to parse workflows and to build the task payload the runner consumes. Pulling the whole runner module in for that is heavy and puts shared code in the repository of one of the two consumers. Both packages now live in `gitea.dev/actionslib` (`pkg/model`, `pkg/exprparser`), the module Gitea and the runner already share for the runner API, so the dependency on the runner repository is dropped here. ### Changes - `gitea.com/gitea/runner/act/model` -> `gitea.dev/actionslib/pkg/model`, `.../act/exprparser` -> `gitea.dev/actionslib/pkg/exprparser` (22 files, import paths only). - `routers/api/actions/runner/interceptor.go` takes the `x-runner-uuid` / `x-runner-token` names from `gitea.dev/actionslib/pkg/protocol` instead of repeating the literals the runner also has. - `go.mod`: `gitea.com/gitea/runner` removed. --------- Signed-off-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: silverwind <me@silverwind.io> Co-authored-by: Zettat123 <zettat123@gmail.com>
694 lines
28 KiB
Go
694 lines
28 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"
|
|
|
|
runnerv1 "gitea.dev/actionslib/runner/v1"
|
|
actions_model "gitea.dev/models/actions"
|
|
auth_model "gitea.dev/models/auth"
|
|
"gitea.dev/models/db"
|
|
repo_model "gitea.dev/models/repo"
|
|
"gitea.dev/models/unittest"
|
|
user_model "gitea.dev/models/user"
|
|
"gitea.dev/modules/util"
|
|
"gitea.dev/tests"
|
|
|
|
"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 getArtifactFixtureTask(t *testing.T) *actions_model.ActionTask {
|
|
t.Helper()
|
|
|
|
task, err := actions_model.GetRunningTaskByToken(t.Context(), "8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
|
require.NoError(t, err)
|
|
require.NoError(t, task.LoadJob(t.Context()))
|
|
ensureArtifactFixtureTaskSteps(t, task)
|
|
return task
|
|
}
|
|
|
|
func ensureArtifactFixtureTaskSteps(t *testing.T, task *actions_model.ActionTask) {
|
|
t.Helper()
|
|
|
|
steps, err := actions_model.GetTaskStepsByTaskID(t.Context(), task.ID)
|
|
require.NoError(t, err)
|
|
|
|
existingIndexes := make(map[int64]bool, len(steps))
|
|
for _, step := range steps {
|
|
existingIndexes[step.Index] = true
|
|
}
|
|
|
|
var stepsToInsert []*actions_model.ActionTaskStep
|
|
for _, idx := range []int64{0, 1} {
|
|
if existingIndexes[idx] {
|
|
continue
|
|
}
|
|
stepsToInsert = append(stepsToInsert, &actions_model.ActionTaskStep{
|
|
TaskID: task.ID,
|
|
Index: idx,
|
|
RepoID: task.RepoID,
|
|
Status: actions_model.StatusWaiting,
|
|
})
|
|
}
|
|
if len(stepsToInsert) == 0 {
|
|
return
|
|
}
|
|
|
|
_, err = db.GetEngine(t.Context()).Insert(stepsToInsert)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestActionsJobSummaryUpload(t *testing.T) {
|
|
defer prepareTestEnvActionsArtifacts(t)()
|
|
|
|
const runnerToken = "8061e833a55f6fc0157c98b883e91fcfeeb1a71a"
|
|
task := getArtifactFixtureTask(t)
|
|
summaryURL := func(stepIndex int64) string {
|
|
return fmt.Sprintf("/api/actions_pipeline/_apis/pipelines/workflows/%d/jobs/%d/steps/%d/summary", task.Job.RunID, task.Job.ID, stepIndex)
|
|
}
|
|
putSummary := func(stepIndex int64, body, contentType string) *RequestWrapper {
|
|
return NewRequestWithBody(t, "PUT", summaryURL(stepIndex), strings.NewReader(body)).
|
|
AddTokenAuth(runnerToken).
|
|
SetHeader("Content-Type", contentType)
|
|
}
|
|
|
|
t.Run("success", func(t *testing.T) {
|
|
body := "### Uploaded summary\n\n- line one\n"
|
|
MakeRequest(t, putSummary(0, body, "text/markdown; charset=utf-8"), http.StatusOK)
|
|
|
|
summary, err := actions_model.GetActionRunJobSummary(t.Context(), task.Job.RepoID, task.Job.RunID, task.Job.RunAttemptID, task.Job.ID, 0)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, actions_model.JobSummaryContentTypeMarkdown, summary.ContentType)
|
|
assert.Equal(t, body, summary.Content)
|
|
|
|
staleUpdated := summary.Updated - 60
|
|
_, err = db.GetEngine(t.Context()).ID(summary.ID).Cols("updated").Update(&actions_model.ActionRunJobSummary{Updated: staleUpdated})
|
|
require.NoError(t, err)
|
|
|
|
updatedBody := "### Updated summary\n\n- refreshed\n"
|
|
MakeRequest(t, putSummary(0, updatedBody, actions_model.JobSummaryContentTypeMarkdown), http.StatusOK)
|
|
|
|
summary, err = actions_model.GetActionRunJobSummary(t.Context(), task.Job.RepoID, task.Job.RunID, task.Job.RunAttemptID, task.Job.ID, 0)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, updatedBody, summary.Content)
|
|
assert.Greater(t, summary.Updated, staleUpdated)
|
|
|
|
stepTwoBody := "### Second step summary\n\n- another step\n"
|
|
MakeRequest(t, putSummary(1, stepTwoBody, actions_model.JobSummaryContentTypeMarkdown), http.StatusOK)
|
|
|
|
summary, err = actions_model.GetActionRunJobSummary(t.Context(), task.Job.RepoID, task.Job.RunID, task.Job.RunAttemptID, task.Job.ID, 1)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, stepTwoBody, summary.Content)
|
|
|
|
summaries, err := actions_model.ListActionRunJobSummaries(t.Context(), task.Job.RepoID, task.Job.RunID, task.Job.RunAttemptID, 0)
|
|
require.NoError(t, err)
|
|
require.Len(t, summaries, 2)
|
|
assert.Equal(t, int64(0), summaries[0].StepIndex)
|
|
assert.Equal(t, int64(1), summaries[1].StepIndex)
|
|
})
|
|
|
|
t.Run("invalid-content-type", func(t *testing.T) {
|
|
resp := MakeRequest(t, putSummary(0, "summary", "text/html"), http.StatusBadRequest)
|
|
assert.Contains(t, resp.Body.String(), "invalid summary content type")
|
|
})
|
|
|
|
t.Run("size-limit", func(t *testing.T) {
|
|
resp := MakeRequest(t, putSummary(0, strings.Repeat("a", actions_model.MaxJobSummarySize+1), actions_model.JobSummaryContentTypeMarkdown), http.StatusBadRequest)
|
|
assert.Contains(t, resp.Body.String(), "invalid summary")
|
|
})
|
|
|
|
t.Run("aggregate-size-limit", func(t *testing.T) {
|
|
require.NoError(t, actions_model.UpsertActionRunJobSummary(t.Context(), task.Job.RepoID, task.Job.RunID, task.Job.RunAttemptID, task.Job.ID, 0,
|
|
actions_model.JobSummaryContentTypeMarkdown, []byte(strings.Repeat("a", actions_model.MaxJobSummaryAggregateSize-1024))))
|
|
resp := MakeRequest(t, putSummary(1, strings.Repeat("b", 4096), actions_model.JobSummaryContentTypeMarkdown), http.StatusBadRequest)
|
|
assert.Contains(t, resp.Body.String(), "aggregate size exceeded")
|
|
})
|
|
|
|
t.Run("job-mismatch", func(t *testing.T) {
|
|
req := NewRequestWithBody(t, "PUT", fmt.Sprintf("/api/actions_pipeline/_apis/pipelines/workflows/%d/jobs/%d/steps/0/summary", task.Job.RunID, task.Job.ID+1), strings.NewReader("summary")).
|
|
AddTokenAuth(runnerToken).
|
|
SetHeader("Content-Type", actions_model.JobSummaryContentTypeMarkdown)
|
|
resp := MakeRequest(t, req, http.StatusBadRequest)
|
|
assert.Contains(t, resp.Body.String(), "job_id mismatch")
|
|
})
|
|
|
|
t.Run("run-mismatch", func(t *testing.T) {
|
|
req := NewRequestWithBody(t, "PUT", fmt.Sprintf("/api/actions_pipeline/_apis/pipelines/workflows/%d/jobs/%d/steps/0/summary", task.Job.RunID+1, task.Job.ID), strings.NewReader("summary")).
|
|
AddTokenAuth(runnerToken).
|
|
SetHeader("Content-Type", actions_model.JobSummaryContentTypeMarkdown)
|
|
resp := MakeRequest(t, req, http.StatusBadRequest)
|
|
assert.Contains(t, resp.Body.String(), "run-id does not match")
|
|
})
|
|
|
|
t.Run("invalid-step-index", func(t *testing.T) {
|
|
resp := MakeRequest(t, putSummary(-1, "summary", actions_model.JobSummaryContentTypeMarkdown), http.StatusBadRequest)
|
|
assert.Contains(t, resp.Body.String(), "invalid step_index")
|
|
})
|
|
|
|
t.Run("step-index-mismatch", func(t *testing.T) {
|
|
resp := MakeRequest(t, putSummary(999, "summary", actions_model.JobSummaryContentTypeMarkdown), http.StatusBadRequest)
|
|
assert.Contains(t, resp.Body.String(), "step_index mismatch")
|
|
})
|
|
|
|
t.Run("empty-body-clears", func(t *testing.T) {
|
|
MakeRequest(t, putSummary(0, "### keep me", actions_model.JobSummaryContentTypeMarkdown), http.StatusOK)
|
|
MakeRequest(t, putSummary(0, "", actions_model.JobSummaryContentTypeMarkdown), http.StatusOK)
|
|
|
|
_, err := actions_model.GetActionRunJobSummary(t.Context(), task.Job.RepoID, task.Job.RunID, task.Job.RunAttemptID, task.Job.ID, 0)
|
|
require.ErrorIs(t, err, util.ErrNotExist)
|
|
})
|
|
}
|
|
|
|
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:] + "?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:] + "?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:] + "?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:] + "?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 ""
|
|
}
|