mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-11-03 21:16:26 +01:00 
			
		
		
		
	Currently, Artifact does not have an expiration and automatic cleanup mechanism, and this feature needs to be added. It contains the following key points: - [x] add global artifact retention days option in config file. Default value is 90 days. - [x] add cron task to clean up expired artifacts. It should run once a day. - [x] support custom retention period from `retention-days: 5` in `upload-artifact@v3`. - [x] artifacts link in actions view should be non-clickable text when expired.
		
			
				
	
	
		
			293 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			293 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
// Copyright 2023 The Gitea Authors. All rights reserved.
 | 
						|
// SPDX-License-Identifier: MIT
 | 
						|
 | 
						|
package integration
 | 
						|
 | 
						|
import (
 | 
						|
	"net/http"
 | 
						|
	"strings"
 | 
						|
	"testing"
 | 
						|
 | 
						|
	"code.gitea.io/gitea/tests"
 | 
						|
 | 
						|
	"github.com/stretchr/testify/assert"
 | 
						|
)
 | 
						|
 | 
						|
type uploadArtifactResponse struct {
 | 
						|
	FileContainerResourceURL string `json:"fileContainerResourceUrl"`
 | 
						|
}
 | 
						|
 | 
						|
type getUploadArtifactRequest struct {
 | 
						|
	Type          string
 | 
						|
	Name          string
 | 
						|
	RetentionDays int64
 | 
						|
}
 | 
						|
 | 
						|
func TestActionsArtifactUploadSingleFile(t *testing.T) {
 | 
						|
	defer tests.PrepareTestEnv(t)()
 | 
						|
 | 
						|
	// acquire artifact upload url
 | 
						|
	req := NewRequestWithJSON(t, "POST", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts", getUploadArtifactRequest{
 | 
						|
		Type: "actions_storage",
 | 
						|
		Name: "artifact",
 | 
						|
	})
 | 
						|
	req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
 | 
						|
	resp := MakeRequest(t, req, http.StatusOK)
 | 
						|
	var uploadResp uploadArtifactResponse
 | 
						|
	DecodeJSON(t, resp, &uploadResp)
 | 
						|
	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.txt"
 | 
						|
 | 
						|
	// upload artifact chunk
 | 
						|
	body := strings.Repeat("A", 1024)
 | 
						|
	req = NewRequestWithBody(t, "PUT", url, strings.NewReader(body))
 | 
						|
	req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
 | 
						|
	req.Header.Add("Content-Range", "bytes 0-1023/1024")
 | 
						|
	req.Header.Add("x-tfs-filelength", "1024")
 | 
						|
	req.Header.Add("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")
 | 
						|
	req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
 | 
						|
	MakeRequest(t, req, http.StatusOK)
 | 
						|
}
 | 
						|
 | 
						|
func TestActionsArtifactUploadInvalidHash(t *testing.T) {
 | 
						|
	defer tests.PrepareTestEnv(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))
 | 
						|
	req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
 | 
						|
	req.Header.Add("Content-Range", "bytes 0-1023/1024")
 | 
						|
	req.Header.Add("x-tfs-filelength", "1024")
 | 
						|
	req.Header.Add("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 tests.PrepareTestEnv(t)()
 | 
						|
 | 
						|
	req := NewRequest(t, "PATCH", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts")
 | 
						|
	req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
 | 
						|
	resp := MakeRequest(t, req, http.StatusBadRequest)
 | 
						|
	assert.Contains(t, resp.Body.String(), "artifact name is empty")
 | 
						|
}
 | 
						|
 | 
						|
func TestActionsArtifactUploadWithoutToken(t *testing.T) {
 | 
						|
	defer tests.PrepareTestEnv(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 tests.PrepareTestEnv(t)()
 | 
						|
 | 
						|
	req := NewRequest(t, "GET", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts")
 | 
						|
	req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
 | 
						|
	resp := MakeRequest(t, req, http.StatusOK)
 | 
						|
	var listResp listArtifactsResponse
 | 
						|
	DecodeJSON(t, resp, &listResp)
 | 
						|
	assert.Equal(t, int64(1), listResp.Count)
 | 
						|
	assert.Equal(t, "artifact", listResp.Value[0].Name)
 | 
						|
	assert.Contains(t, listResp.Value[0].FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts")
 | 
						|
 | 
						|
	idx := strings.Index(listResp.Value[0].FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/")
 | 
						|
	url := listResp.Value[0].FileContainerResourceURL[idx+1:] + "?itemPath=artifact"
 | 
						|
	req = NewRequest(t, "GET", url)
 | 
						|
	req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
 | 
						|
	resp = MakeRequest(t, req, http.StatusOK)
 | 
						|
	var downloadResp downloadArtifactResponse
 | 
						|
	DecodeJSON(t, resp, &downloadResp)
 | 
						|
	assert.Len(t, downloadResp.Value, 1)
 | 
						|
	assert.Equal(t, "artifact/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)
 | 
						|
	req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
 | 
						|
	resp = MakeRequest(t, req, http.StatusOK)
 | 
						|
	body := strings.Repeat("A", 1024)
 | 
						|
	assert.Equal(t, resp.Body.String(), body)
 | 
						|
}
 | 
						|
 | 
						|
func TestActionsArtifactUploadMultipleFile(t *testing.T) {
 | 
						|
	defer tests.PrepareTestEnv(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,
 | 
						|
	})
 | 
						|
	req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
 | 
						|
	resp := MakeRequest(t, req, http.StatusOK)
 | 
						|
	var uploadResp uploadArtifactResponse
 | 
						|
	DecodeJSON(t, resp, &uploadResp)
 | 
						|
	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.txt",
 | 
						|
			Content: strings.Repeat("A", 1024),
 | 
						|
			MD5:     "1HsSe8LeLWh93ILaw1TEFQ==",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			Path:    "xyz/def.txt",
 | 
						|
			Content: strings.Repeat("B", 1024),
 | 
						|
			MD5:     "6fgADK/7zjadf+6cB9Q1CQ==",
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	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))
 | 
						|
		req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
 | 
						|
		req.Header.Add("Content-Range", "bytes 0-1023/1024")
 | 
						|
		req.Header.Add("x-tfs-filelength", "1024")
 | 
						|
		req.Header.Add("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)
 | 
						|
	req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
 | 
						|
	MakeRequest(t, req, http.StatusOK)
 | 
						|
}
 | 
						|
 | 
						|
func TestActionsArtifactDownloadMultiFiles(t *testing.T) {
 | 
						|
	defer tests.PrepareTestEnv(t)()
 | 
						|
 | 
						|
	const testArtifactName = "multi-files"
 | 
						|
 | 
						|
	req := NewRequest(t, "GET", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts")
 | 
						|
	req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
 | 
						|
	resp := MakeRequest(t, req, http.StatusOK)
 | 
						|
	var listResp listArtifactsResponse
 | 
						|
	DecodeJSON(t, resp, &listResp)
 | 
						|
	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)
 | 
						|
	req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
 | 
						|
	resp = MakeRequest(t, req, http.StatusOK)
 | 
						|
	var downloadResp downloadArtifactResponse
 | 
						|
	DecodeJSON(t, resp, &downloadResp)
 | 
						|
	assert.Len(t, downloadResp.Value, 2)
 | 
						|
 | 
						|
	downloads := [][]string{{"multi-files/abc.txt", "A"}, {"multi-files/xyz/def.txt", "B"}}
 | 
						|
	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)
 | 
						|
		req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
 | 
						|
		resp = MakeRequest(t, req, http.StatusOK)
 | 
						|
		body := strings.Repeat(bodyChar, 1024)
 | 
						|
		assert.Equal(t, resp.Body.String(), body)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func TestActionsArtifactUploadWithRetentionDays(t *testing.T) {
 | 
						|
	defer tests.PrepareTestEnv(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,
 | 
						|
	})
 | 
						|
	req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
 | 
						|
	resp := MakeRequest(t, req, http.StatusOK)
 | 
						|
	var uploadResp uploadArtifactResponse
 | 
						|
	DecodeJSON(t, resp, &uploadResp)
 | 
						|
	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))
 | 
						|
	req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
 | 
						|
	req.Header.Add("Content-Range", "bytes 0-1023/1024")
 | 
						|
	req.Header.Add("x-tfs-filelength", "1024")
 | 
						|
	req.Header.Add("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")
 | 
						|
	req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
 | 
						|
	MakeRequest(t, req, http.StatusOK)
 | 
						|
}
 |