Return write evidence from Gitea operations
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Implementation plan: neutral forge delivery and credential preflight
|
# Implementation plan: neutral forge delivery and credential preflight
|
||||||
|
|
||||||
Status: implementation in progress; Stages 1–4 are complete and Stages 5–10
|
Status: implementation in progress; Stages 1–5 are complete and Stages 6–10
|
||||||
are pending.
|
are pending.
|
||||||
|
|
||||||
## Purpose and execution
|
## Purpose and execution
|
||||||
@@ -327,7 +327,7 @@ evidence. It uses no extra account/admin credentials or destructive probe.
|
|||||||
|
|
||||||
## Stage 5: Extract Gitea write operations and evidence
|
## Stage 5: Extract Gitea write operations and evidence
|
||||||
|
|
||||||
Status: pending.
|
Status: complete.
|
||||||
|
|
||||||
1. Implement `CreateRelease` from the current release POST and `UploadAsset`
|
1. Implement `CreateRelease` from the current release POST and `UploadAsset`
|
||||||
from multipart upload. Keep explicit target commit, stable flags, full note,
|
from multipart upload. Keep explicit target commit, stable flags, full note,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package gitea
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -75,6 +76,92 @@ func (b *Backend) Preflight(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *Backend) CreateRelease(ctx context.Context, request forge.CreateReleaseRequest) (forge.Release, forge.WriteEvidence, error) {
|
||||||
|
if strings.TrimSpace(request.Tag) == "" || !validObjectID(request.Commit) {
|
||||||
|
return forge.Release{}, forge.WriteEvidence{Disposition: forge.WriteNotAttempted}, &forge.Failure{
|
||||||
|
Kind: forge.FailureInvalid, Err: errors.New("a full tag and lowercase Git commit ID are required"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, apiRoot, err := b.destinationAPI("create release")
|
||||||
|
if err != nil {
|
||||||
|
return forge.Release{}, forge.WriteEvidence{Disposition: forge.WriteNotAttempted}, err
|
||||||
|
}
|
||||||
|
body, err := json.Marshal(createReleaseRequest{
|
||||||
|
TagName: request.Tag, TargetCommitish: request.Commit,
|
||||||
|
Name: request.Title, Body: request.Body,
|
||||||
|
Draft: request.Draft, Prerelease: request.Prerelease,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return forge.Release{}, forge.WriteEvidence{Disposition: forge.WriteNotAttempted}, &forge.Failure{Kind: forge.FailureInvalid, Err: err}
|
||||||
|
}
|
||||||
|
created, status, err := b.client.postRelease(ctx, b.client.httpClient(), b.destination.Token, apiRoot+"/releases", body)
|
||||||
|
if err == nil && created.ID <= 0 {
|
||||||
|
err = failure(FailureMalformed, "verify created release", status, errors.New("release response does not contain a positive ID"))
|
||||||
|
}
|
||||||
|
evidence := writeEvidence(status, err)
|
||||||
|
if err != nil {
|
||||||
|
return forge.Release{}, evidence, neutralFailure(err)
|
||||||
|
}
|
||||||
|
return forge.Release{
|
||||||
|
URL: created.URL,
|
||||||
|
Tag: created.TagName,
|
||||||
|
Title: created.Title,
|
||||||
|
Body: created.Body,
|
||||||
|
Draft: created.Draft,
|
||||||
|
Prerelease: created.Prerelease,
|
||||||
|
Handle: releaseHandle{id: created.ID},
|
||||||
|
}, evidence, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Backend) UploadAsset(ctx context.Context, handle any, filename string, content io.Reader) (forge.WriteEvidence, error) {
|
||||||
|
release, ok := handle.(releaseHandle)
|
||||||
|
if !ok || release.id <= 0 {
|
||||||
|
return forge.WriteEvidence{Disposition: forge.WriteNotAttempted}, &forge.Failure{Kind: forge.FailureInvalid, Err: errors.New("release handle was not created by the Gitea backend")}
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(filename) == "" || content == nil {
|
||||||
|
return forge.WriteEvidence{Disposition: forge.WriteNotAttempted}, &forge.Failure{Kind: forge.FailureInvalid, Err: errors.New("asset filename and content reader are required")}
|
||||||
|
}
|
||||||
|
_, apiRoot, err := b.destinationAPI("upload release asset")
|
||||||
|
if err != nil {
|
||||||
|
return forge.WriteEvidence{Disposition: forge.WriteNotAttempted}, err
|
||||||
|
}
|
||||||
|
status, err := b.client.uploadAsset(ctx, b.client.httpClient(), b.destination.Token, apiRoot, release.id, filename, content)
|
||||||
|
evidence := writeEvidence(status, err)
|
||||||
|
if err != nil {
|
||||||
|
return evidence, neutralFailure(err)
|
||||||
|
}
|
||||||
|
return evidence, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeEvidence(status int, err error) forge.WriteEvidence {
|
||||||
|
evidence := forge.WriteEvidence{Attempted: status != 0, Disposition: forge.WriteNotAttempted}
|
||||||
|
var backendFailure *Failure
|
||||||
|
if err != nil && errors.As(err, &backendFailure) && backendFailure.Kind == FailureTransport {
|
||||||
|
evidence.Attempted = true
|
||||||
|
}
|
||||||
|
if !evidence.Attempted {
|
||||||
|
return evidence
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
evidence.Disposition = forge.WriteAccepted
|
||||||
|
return evidence
|
||||||
|
}
|
||||||
|
if status == http.StatusConflict {
|
||||||
|
evidence.Disposition = forge.WriteConflict
|
||||||
|
return evidence
|
||||||
|
}
|
||||||
|
if status >= http.StatusInternalServerError || errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||||||
|
evidence.Disposition = forge.WriteUncertain
|
||||||
|
return evidence
|
||||||
|
}
|
||||||
|
if errors.As(err, &backendFailure) && (backendFailure.Kind == FailureTransport || backendFailure.Kind == FailureMalformed) {
|
||||||
|
evidence.Disposition = forge.WriteUncertain
|
||||||
|
return evidence
|
||||||
|
}
|
||||||
|
evidence.Disposition = forge.WriteRejected
|
||||||
|
return evidence
|
||||||
|
}
|
||||||
|
|
||||||
func (repository repositoryResponse) matches(destination Destination) bool {
|
func (repository repositoryResponse) matches(destination Destination) bool {
|
||||||
if repository.ID <= 0 || !strings.EqualFold(repository.Name, destination.Repository) {
|
if repository.ID <= 0 || !strings.EqualFold(repository.Name, destination.Repository) {
|
||||||
return false
|
return false
|
||||||
@@ -237,3 +324,5 @@ func neutralFailure(err error) error {
|
|||||||
}
|
}
|
||||||
return &forge.Failure{Kind: kind, Err: err}
|
return &forge.Failure{Kind: kind, Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _ forge.Backend = (*Backend)(nil)
|
||||||
|
|||||||
@@ -245,3 +245,171 @@ func TestBackendPreflightPreservesCancellation(t *testing.T) {
|
|||||||
t.Fatalf("Preflight() error = %v, want unavailable and context cancellation", err)
|
t.Fatalf("Preflight() error = %v, want unavailable and context cancellation", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBackendCreateReleaseSendsRequestedFieldsAndAcceptsProtocolAcknowledgment(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost || r.URL.Path != "/api/v1/repos/owner/project/releases" {
|
||||||
|
t.Errorf("request = %s %s", r.Method, r.URL.Path)
|
||||||
|
}
|
||||||
|
if got, want := r.Header.Get("Authorization"), "token fixture-secret-token"; got != want {
|
||||||
|
t.Errorf("authorization = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
var request createReleaseRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
||||||
|
t.Errorf("decode request: %v", err)
|
||||||
|
}
|
||||||
|
if request.TagName != "v1.2.3" || request.TargetCommitish != strings.Repeat("a", 40) || request.Name != "Release title" || request.Body != "# Release title\n\nComplete notes." || !request.Draft || !request.Prerelease {
|
||||||
|
t.Errorf("release request = %#v", request)
|
||||||
|
}
|
||||||
|
_ = json.NewEncoder(w).Encode(releaseResponse{ID: 12, TagName: "server tag", Name: "server title", Body: "server body", Draft: true, Prerelease: true})
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
backend := NewBackend(testDestination(server.URL), server.Client())
|
||||||
|
|
||||||
|
release, evidence, err := backend.CreateRelease(context.Background(), forge.CreateReleaseRequest{
|
||||||
|
Tag: "v1.2.3", Commit: strings.Repeat("a", 40), Title: "Release title",
|
||||||
|
Body: "# Release title\n\nComplete notes.", Draft: true, Prerelease: true,
|
||||||
|
})
|
||||||
|
if err != nil || evidence != (forge.WriteEvidence{Attempted: true, Disposition: forge.WriteAccepted}) {
|
||||||
|
t.Fatalf("CreateRelease() evidence = %#v, error = %v", evidence, err)
|
||||||
|
}
|
||||||
|
if release.Handle == nil || release.Tag != "server tag" || release.Title != "server title" || release.Body != "server body" || !release.Draft || !release.Prerelease {
|
||||||
|
t.Fatalf("CreateRelease() release = %#v", release)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackendCreateReleaseMarksInvalidAcknowledgmentUncertain(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
_ = json.NewEncoder(w).Encode(releaseResponse{TagName: "v1.2.3"})
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
backend := NewBackend(testDestination(server.URL), server.Client())
|
||||||
|
|
||||||
|
_, evidence, err := backend.CreateRelease(context.Background(), validCreateReleaseRequest())
|
||||||
|
var failure *forge.Failure
|
||||||
|
if !errors.As(err, &failure) || failure.Kind != forge.FailureMalformed || evidence != (forge.WriteEvidence{Attempted: true, Disposition: forge.WriteUncertain}) {
|
||||||
|
t.Fatalf("CreateRelease() evidence = %#v, error = %v", evidence, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackendCreateReleaseClassifiesConflictAndServerFailure(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
status int
|
||||||
|
disposition forge.WriteDisposition
|
||||||
|
}{
|
||||||
|
{status: http.StatusBadRequest, disposition: forge.WriteRejected},
|
||||||
|
{status: http.StatusConflict, disposition: forge.WriteConflict},
|
||||||
|
{status: http.StatusServiceUnavailable, disposition: forge.WriteUncertain},
|
||||||
|
} {
|
||||||
|
t.Run(http.StatusText(test.status), func(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(test.status) }))
|
||||||
|
defer server.Close()
|
||||||
|
backend := NewBackend(testDestination(server.URL), server.Client())
|
||||||
|
|
||||||
|
_, evidence, err := backend.CreateRelease(context.Background(), validCreateReleaseRequest())
|
||||||
|
if err == nil || evidence != (forge.WriteEvidence{Attempted: true, Disposition: test.disposition}) {
|
||||||
|
t.Fatalf("CreateRelease() evidence = %#v, error = %v", evidence, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackendUploadAssetBorrowsReaderAndValidatesAcknowledgment(t *testing.T) {
|
||||||
|
var uploaded string
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost || r.URL.Path != "/api/v1/repos/owner/project/releases/12/assets" || r.URL.Query().Get("name") != "app.bin" {
|
||||||
|
t.Errorf("request = %s %s?%s", r.Method, r.URL.Path, r.URL.RawQuery)
|
||||||
|
}
|
||||||
|
if got, want := r.Header.Get("Authorization"), "token fixture-secret-token"; got != want {
|
||||||
|
t.Errorf("authorization = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
file, header, err := r.FormFile("attachment")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("attachment: %v", err)
|
||||||
|
} else {
|
||||||
|
defer file.Close()
|
||||||
|
content, readErr := io.ReadAll(file)
|
||||||
|
if readErr != nil {
|
||||||
|
t.Errorf("read attachment: %v", readErr)
|
||||||
|
}
|
||||||
|
if header.Filename != "app.bin" {
|
||||||
|
t.Errorf("multipart filename = %q", header.Filename)
|
||||||
|
}
|
||||||
|
uploaded = string(content)
|
||||||
|
}
|
||||||
|
_ = json.NewEncoder(w).Encode(assetResponse{ID: 50, Name: "app.bin"})
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
backend := NewBackend(testDestination(server.URL), server.Client())
|
||||||
|
reader := &trackedReader{Reader: strings.NewReader("streamed bytes")}
|
||||||
|
|
||||||
|
evidence, err := backend.UploadAsset(context.Background(), releaseHandle{id: 12}, "app.bin", reader)
|
||||||
|
if err != nil || evidence != (forge.WriteEvidence{Attempted: true, Disposition: forge.WriteAccepted}) {
|
||||||
|
t.Fatalf("UploadAsset() evidence = %#v, error = %v", evidence, err)
|
||||||
|
}
|
||||||
|
if uploaded != "streamed bytes" || reader.closed {
|
||||||
|
t.Fatalf("uploaded = %q, caller reader closed = %v", uploaded, reader.closed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackendUploadAssetMarksBadAcknowledgmentUncertain(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
_ = json.NewEncoder(w).Encode(assetResponse{ID: 50, Name: "different.bin"})
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
backend := NewBackend(testDestination(server.URL), server.Client())
|
||||||
|
|
||||||
|
evidence, err := backend.UploadAsset(context.Background(), releaseHandle{id: 12}, "app.bin", strings.NewReader("data"))
|
||||||
|
var failure *forge.Failure
|
||||||
|
if !errors.As(err, &failure) || failure.Kind != forge.FailureMalformed || evidence != (forge.WriteEvidence{Attempted: true, Disposition: forge.WriteUncertain}) {
|
||||||
|
t.Fatalf("UploadAsset() evidence = %#v, error = %v", evidence, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackendUploadAssetClassifiesKnownRejectionAndConflict(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
status int
|
||||||
|
disposition forge.WriteDisposition
|
||||||
|
}{
|
||||||
|
{status: http.StatusBadRequest, disposition: forge.WriteRejected},
|
||||||
|
{status: http.StatusConflict, disposition: forge.WriteConflict},
|
||||||
|
{status: http.StatusInternalServerError, disposition: forge.WriteUncertain},
|
||||||
|
} {
|
||||||
|
t.Run(http.StatusText(test.status), func(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(test.status) }))
|
||||||
|
defer server.Close()
|
||||||
|
backend := NewBackend(testDestination(server.URL), server.Client())
|
||||||
|
|
||||||
|
evidence, err := backend.UploadAsset(context.Background(), releaseHandle{id: 12}, "app.bin", strings.NewReader("data"))
|
||||||
|
if err == nil || evidence != (forge.WriteEvidence{Attempted: true, Disposition: test.disposition}) {
|
||||||
|
t.Fatalf("UploadAsset() evidence = %#v, error = %v", evidence, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackendUploadAssetPreservesCancellationAsUncertain(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.NotFoundHandler())
|
||||||
|
defer server.Close()
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
evidence, err := NewBackend(testDestination(server.URL), server.Client()).UploadAsset(ctx, releaseHandle{id: 12}, "app.bin", strings.NewReader("data"))
|
||||||
|
if evidence != (forge.WriteEvidence{Attempted: true, Disposition: forge.WriteUncertain}) || !errors.Is(err, context.Canceled) {
|
||||||
|
t.Fatalf("UploadAsset() evidence = %#v, error = %v", evidence, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validCreateReleaseRequest() forge.CreateReleaseRequest {
|
||||||
|
return forge.CreateReleaseRequest{Tag: "v1.2.3", Commit: strings.Repeat("a", 40), Title: "Release", Body: "# Release"}
|
||||||
|
}
|
||||||
|
|
||||||
|
type trackedReader struct {
|
||||||
|
io.Reader
|
||||||
|
closed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (reader *trackedReader) Close() error {
|
||||||
|
reader.closed = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -146,7 +146,12 @@ func (c Client) uploadMissing(ctx context.Context, destination Destination, rele
|
|||||||
if _, needsUpload := missing[item.Filename]; !needsUpload {
|
if _, needsUpload := missing[item.Filename]; !needsUpload {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
status, uploadErr := c.uploadAsset(ctx, client, destination.Token, baseAPI, releaseID, item)
|
content, openErr := assetContent(item)
|
||||||
|
if openErr != nil {
|
||||||
|
return result, failure(FailureInvalid, "open local release asset", 0, openErr)
|
||||||
|
}
|
||||||
|
status, uploadErr := c.uploadAsset(ctx, client, destination.Token, baseAPI, releaseID, item.Filename, content)
|
||||||
|
_ = content.Close()
|
||||||
if uploadErr == nil {
|
if uploadErr == nil {
|
||||||
result.Uploaded = append(result.Uploaded, identity(item))
|
result.Uploaded = append(result.Uploaded, identity(item))
|
||||||
result.Pending = removeIdentity(result.Pending, item.Filename)
|
result.Pending = removeIdentity(result.Pending, item.Filename)
|
||||||
@@ -198,36 +203,25 @@ func (c Client) uploadMissing(ctx context.Context, destination Destination, rele
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Client) uploadAsset(ctx context.Context, client *http.Client, token, apiRoot string, releaseID int64, asset artifact.ExpectedAsset) (int, error) {
|
func (c Client) uploadAsset(ctx context.Context, client *http.Client, token, apiRoot string, releaseID int64, filename string, content io.Reader) (int, error) {
|
||||||
|
if content == nil {
|
||||||
|
return 0, failure(FailureInvalid, "upload release asset", 0, errors.New("asset content reader is required"))
|
||||||
|
}
|
||||||
uploadURL := apiRoot + "/releases/" + strconv.FormatInt(releaseID, 10) + "/assets"
|
uploadURL := apiRoot + "/releases/" + strconv.FormatInt(releaseID, 10) + "/assets"
|
||||||
parsed, err := url.Parse(uploadURL)
|
parsed, err := url.Parse(uploadURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, failure(FailureInvalid, "prepare asset upload", 0, err)
|
return 0, failure(FailureInvalid, "prepare asset upload", 0, err)
|
||||||
}
|
}
|
||||||
query := parsed.Query()
|
query := parsed.Query()
|
||||||
query.Set("name", asset.Filename)
|
query.Set("name", filename)
|
||||||
parsed.RawQuery = query.Encode()
|
parsed.RawQuery = query.Encode()
|
||||||
reader, writer := io.Pipe()
|
reader, writer := io.Pipe()
|
||||||
multipartWriter := multipart.NewWriter(writer)
|
multipartWriter := multipart.NewWriter(writer)
|
||||||
writeDone := make(chan error, 1)
|
writeDone := make(chan error, 1)
|
||||||
go func() {
|
go func() {
|
||||||
part, partErr := multipartWriter.CreateFormFile("attachment", asset.Filename)
|
part, partErr := multipartWriter.CreateFormFile("attachment", filename)
|
||||||
if partErr == nil {
|
if partErr == nil {
|
||||||
var source io.Reader
|
_, partErr = io.Copy(part, content)
|
||||||
if len(asset.Content) > 0 {
|
|
||||||
source = bytes.NewReader(asset.Content)
|
|
||||||
} else {
|
|
||||||
file, openErr := os.Open(asset.Path)
|
|
||||||
if openErr != nil {
|
|
||||||
partErr = openErr
|
|
||||||
} else {
|
|
||||||
defer file.Close()
|
|
||||||
source = file
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if partErr == nil {
|
|
||||||
_, partErr = io.Copy(part, source)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
closeErr := multipartWriter.Close()
|
closeErr := multipartWriter.Close()
|
||||||
if partErr == nil {
|
if partErr == nil {
|
||||||
@@ -261,7 +255,7 @@ func (c Client) uploadAsset(ctx context.Context, client *http.Client, token, api
|
|||||||
if err := decodeJSON(response.Body, &uploaded); err != nil {
|
if err := decodeJSON(response.Body, &uploaded); err != nil {
|
||||||
return response.StatusCode, failure(FailureMalformed, "decode uploaded asset", response.StatusCode, err)
|
return response.StatusCode, failure(FailureMalformed, "decode uploaded asset", response.StatusCode, err)
|
||||||
}
|
}
|
||||||
if uploaded.ID <= 0 || uploaded.Name != asset.Filename {
|
if uploaded.ID <= 0 || uploaded.Name != filename {
|
||||||
return response.StatusCode, failure(FailureMalformed, "verify uploaded asset", response.StatusCode, errors.New("upload response does not identify the selected asset"))
|
return response.StatusCode, failure(FailureMalformed, "verify uploaded asset", response.StatusCode, errors.New("upload response does not identify the selected asset"))
|
||||||
}
|
}
|
||||||
if writeErr != nil {
|
if writeErr != nil {
|
||||||
@@ -270,6 +264,13 @@ func (c Client) uploadAsset(ctx context.Context, client *http.Client, token, api
|
|||||||
return response.StatusCode, nil
|
return response.StatusCode, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func assetContent(asset artifact.ExpectedAsset) (io.ReadCloser, error) {
|
||||||
|
if len(asset.Content) > 0 {
|
||||||
|
return io.NopCloser(bytes.NewReader(asset.Content)), nil
|
||||||
|
}
|
||||||
|
return os.Open(asset.Path)
|
||||||
|
}
|
||||||
|
|
||||||
func orderedExpected(observations []AssetInspection) []artifact.ExpectedAsset {
|
func orderedExpected(observations []AssetInspection) []artifact.ExpectedAsset {
|
||||||
result := make([]artifact.ExpectedAsset, 0, len(observations))
|
result := make([]artifact.ExpectedAsset, 0, len(observations))
|
||||||
for _, observation := range observations {
|
for _, observation := range observations {
|
||||||
|
|||||||
Reference in New Issue
Block a user