mirror of
https://gitea.com/gitea/runner.git
synced 2026-11-15 12:23:42 +00:00
enhance: add builtin:upload-artifact (#1343)
Co-authored-by: bircni <[email protected]>
This commit is contained in:
1 parent
8c8950d9ef
commit
02446620df
30 files changed
+1881
-20
No files matched your search
@@ -0,0 +1,182 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package artifact
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.com/gitea/runner/act/artifacts"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestClientRoundTrip(t *testing.T) {
|
||||
router := http.NewServeMux()
|
||||
artifacts.RegisterV4(router, t.TempDir())
|
||||
var blocks atomic.Int32
|
||||
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
if request.URL.Query().Get("comp") == "block" {
|
||||
blocks.Add(1)
|
||||
}
|
||||
router.ServeHTTP(writer, request)
|
||||
}))
|
||||
defer server.Close()
|
||||
client, err := New(server.URL, artifacts.LocalRuntimeToken("7", time.Hour), nil)
|
||||
require.NoError(t, err)
|
||||
for _, content := range [][]byte{nil, bytes.Repeat([]byte("payload"), 2*BlockSize/7+123)} {
|
||||
sum := sha256.Sum256(content)
|
||||
uploadURL, err := client.Create(t.Context(), "art", "text/plain", nil)
|
||||
require.NoError(t, err)
|
||||
uploaded, err := client.Upload(t.Context(), uploadURL, "text/plain", bytes.NewReader(content), 2, time.Minute)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int32((len(content)+BlockSize-1)/BlockSize), blocks.Swap(0))
|
||||
assert.Equal(t, Uploaded{Size: int64(len(content)), Digest: "sha256:" + hex.EncodeToString(sum[:])}, uploaded)
|
||||
id, err := client.Finalize(t.Context(), "art", uploaded.Size, uploaded.Digest)
|
||||
require.NoError(t, err)
|
||||
listed, err := client.List(t.Context(), "art")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []Artifact{{ID: id, Name: "art", Size: int64(len(content)), Digest: uploaded.Digest}}, listed)
|
||||
signed, err := client.SignedURL(t.Context(), "art")
|
||||
require.NoError(t, err)
|
||||
resp, err := client.Download(t.Context(), signed, time.Minute)
|
||||
require.NoError(t, err)
|
||||
got, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, resp.Body.Close())
|
||||
assert.True(t, bytes.Equal(content, got))
|
||||
deleted, err := client.Delete(t.Context(), "art")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, id, deleted)
|
||||
_, err = client.SignedURL(t.Context(), "art")
|
||||
assert.True(t, IsNotFound(err), err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientRetries(t *testing.T) {
|
||||
var calls atomic.Int32
|
||||
var blockIDs []string
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodPut {
|
||||
blockIDs = append(blockIDs, r.URL.Query().Get("blockid"))
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
return
|
||||
}
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
assert.NotContains(t, string(body), "mime_type", "older Gitea rejects the field")
|
||||
assert.True(t, strings.HasPrefix(r.Header.Get("Authorization"), "Bearer ey"), "the runtime token is the bearer")
|
||||
switch calls.Add(1) {
|
||||
case 1:
|
||||
w.Header().Set("Retry-After", "0")
|
||||
w.WriteHeader(http.StatusTooManyRequests)
|
||||
case 2:
|
||||
http.Error(w, "Error create or get artifact", http.StatusInternalServerError)
|
||||
case 3:
|
||||
w.WriteHeader(http.StatusFound)
|
||||
_, _ = io.WriteString(w, `{"ok":true,"signedUploadUrl":"http://blob/u?sig=x"}`)
|
||||
default:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = io.WriteString(w, `{"code":"invalid_argument","msg":"bad name"}`)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
client, err := New(server.URL, artifacts.LocalRuntimeToken("7", time.Hour), nil)
|
||||
require.NoError(t, err)
|
||||
client.Retry = Retry{Attempts: 3}
|
||||
var logged []string
|
||||
client.Logf = func(format string, args ...any) { logged = append(logged, fmt.Sprintf(format, args...)) }
|
||||
|
||||
uploadURL, err := client.Create(t.Context(), "art", "", nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "http://blob/u?sig=x", uploadURL)
|
||||
assert.Contains(t, logged[1], "Error create or get artifact")
|
||||
|
||||
_, err = client.Create(t.Context(), "art", "", nil)
|
||||
require.ErrorContains(t, err, "(400) Bad Request: bad name")
|
||||
assert.Equal(t, int32(4), calls.Load(), "a 400 is not retried")
|
||||
|
||||
for range 2 {
|
||||
_, err = client.Upload(t.Context(), server.URL+"/u?sig=secret", "application/zip", strings.NewReader("x"), 1, time.Minute)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
assert.NotEqual(t, blockIDs[0], blockIDs[2], "each upload names its blocks anew")
|
||||
server.Close()
|
||||
_, err = client.Upload(t.Context(), server.URL+"/u?sig=secret", "application/zip", strings.NewReader("x"), 1, time.Minute)
|
||||
require.Error(t, err)
|
||||
assert.NotContains(t, err.Error()+strings.Join(logged, ""), "secret")
|
||||
}
|
||||
|
||||
func TestTransferStalls(t *testing.T) {
|
||||
for _, test := range []struct{ name, want string }{
|
||||
{"api", "Client.Timeout"},
|
||||
{"failed upload", "(400) Bad Request"},
|
||||
{"source", "upload progress stalled"},
|
||||
{"commit", "upload progress stalled"},
|
||||
{"download", "did not respond"},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
_, _ = io.Copy(io.Discard, request.Body)
|
||||
if test.name == "failed upload" {
|
||||
writer.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if test.name == "download" {
|
||||
_, _ = io.WriteString(writer, "partial")
|
||||
writer.(http.Flusher).Flush()
|
||||
}
|
||||
<-request.Context().Done()
|
||||
}))
|
||||
defer server.Close()
|
||||
client, err := New(server.URL, artifacts.LocalRuntimeToken("7", time.Hour), nil)
|
||||
require.NoError(t, err)
|
||||
client.Retry = Retry{Attempts: 1}
|
||||
ctx, cancel := context.WithTimeout(t.Context(), time.Second)
|
||||
defer cancel()
|
||||
switch test.name {
|
||||
case "api":
|
||||
assert.Equal(t, 3*time.Minute, client.api.Timeout)
|
||||
client.api.Timeout = time.Millisecond
|
||||
_, err = client.Create(ctx, "art", "", nil)
|
||||
case "download":
|
||||
var resp *http.Response
|
||||
resp, err = client.Download(ctx, server.URL, time.Millisecond)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
_, err = io.ReadAll(resp.Body)
|
||||
case "commit":
|
||||
_, err = client.Upload(ctx, server.URL+"?sig=x", "text/plain", strings.NewReader(""), 1, time.Millisecond)
|
||||
default:
|
||||
reader, writer := io.Pipe()
|
||||
defer reader.Close()
|
||||
defer writer.Close()
|
||||
done := make(chan error, 1)
|
||||
stall := time.Millisecond
|
||||
if test.name == "failed upload" {
|
||||
stall = time.Minute
|
||||
go func() {
|
||||
_, err := writer.Write(make([]byte, BlockSize))
|
||||
done <- err
|
||||
}()
|
||||
}
|
||||
_, err = client.Upload(ctx, server.URL+"?sig=x", "text/plain", reader, 2, stall)
|
||||
if test.name == "failed upload" {
|
||||
require.NoError(t, <-done)
|
||||
}
|
||||
}
|
||||
require.ErrorContains(t, err, test.want)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user