Classify missing release IDs when accessing assets
This commit is contained in:
@@ -755,6 +755,33 @@ func (writer deliveryCancelWriter) Write(content []byte) (int, error) {
|
|||||||
return len(content), nil
|
return len(content), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDeliverExistingPageWithoutReleaseID(t *testing.T) {
|
||||||
|
repository := newTaggedDeliveryConsumer(t)
|
||||||
|
prebuilt, err := releasekit.Build(context.Background(), releasekit.BuildRequest{
|
||||||
|
RepositoryDir: repository, Version: "v1.2.3", OutputDir: t.TempDir(),
|
||||||
|
Spec: releasekit.BuildSpec{MainPackage: "./cmd/app", BinaryName: "fixture", Targets: []releasekit.Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}}},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
server, remote := newFacadeDeliveryServer(t, repository)
|
||||||
|
defer server.Close()
|
||||||
|
remote.update(func(data *facadeDeliveryData) { data.pages[data.tag] = true; data.omitReleaseID = true })
|
||||||
|
request := releasekit.DeliveryRequest{RepositoryDir: repository, Product: "Fixture", Version: "v1.2.3", Destination: facadeDeliveryDestination(server.URL)}
|
||||||
|
page, err := releasekit.Deliver(context.Background(), request)
|
||||||
|
if err != nil || page.State != releasekit.DeliveryComplete {
|
||||||
|
t.Fatalf("page result = %#v, error = %v", page, err)
|
||||||
|
}
|
||||||
|
request.Prebuilt = &prebuilt
|
||||||
|
result, err := releasekit.Deliver(context.Background(), request)
|
||||||
|
if err == nil || result.State != releasekit.DeliveryNotAttempted || !strings.Contains(err.Error(), "release ID") {
|
||||||
|
t.Fatalf("asset result = %#v, error = %v", result, err)
|
||||||
|
}
|
||||||
|
if snapshot := remote.snapshot(); snapshot.writes != 0 {
|
||||||
|
t.Fatalf("unexpected hosted writes: %#v", snapshot)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type facadeDeliveryServer struct {
|
type facadeDeliveryServer struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
data facadeDeliveryData
|
data facadeDeliveryData
|
||||||
@@ -785,16 +812,17 @@ func (state *facadeDeliveryServer) snapshot() facadeDeliveryData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type facadeDeliveryData struct {
|
type facadeDeliveryData struct {
|
||||||
t *testing.T
|
t *testing.T
|
||||||
commit string
|
commit string
|
||||||
tag string
|
tag string
|
||||||
version string
|
version string
|
||||||
noteBody string
|
noteBody string
|
||||||
pages map[string]bool
|
pages map[string]bool
|
||||||
requests int
|
requests int
|
||||||
writes int
|
writes int
|
||||||
assets map[string][]byte
|
omitReleaseID bool
|
||||||
url string
|
assets map[string][]byte
|
||||||
|
url string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newFacadeDeliveryServer(t *testing.T, repository string) (*httptest.Server, *facadeDeliveryServer) {
|
func newFacadeDeliveryServer(t *testing.T, repository string) (*httptest.Server, *facadeDeliveryServer) {
|
||||||
@@ -894,11 +922,15 @@ func (state *facadeDeliveryServer) writeRelease(w http.ResponseWriter) {
|
|||||||
// The request handler holds state.mu.
|
// The request handler holds state.mu.
|
||||||
data := &state.data
|
data := &state.data
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
release := map[string]any{
|
||||||
"id": 10, "html_url": data.url + "/releases/10", "tag_name": data.tag,
|
"html_url": data.url + "/releases/10", "tag_name": data.tag,
|
||||||
"target_commitish": data.commit, "name": "Fixture " + data.version,
|
"target_commitish": data.commit, "name": "Fixture " + data.version,
|
||||||
"body": data.noteBody, "draft": false, "prerelease": false,
|
"body": data.noteBody, "draft": false, "prerelease": false,
|
||||||
})
|
}
|
||||||
|
if !data.omitReleaseID {
|
||||||
|
release["id"] = 10
|
||||||
|
}
|
||||||
|
_ = json.NewEncoder(w).Encode(release)
|
||||||
}
|
}
|
||||||
|
|
||||||
func mapKeys(values map[string][]byte) []string {
|
func mapKeys(values map[string][]byte) []string {
|
||||||
|
|||||||
@@ -54,6 +54,9 @@ is accepted by shared delivery policy only when it has that tag, the complete
|
|||||||
committed release note as its body, and stable
|
committed release note as its body, and stable
|
||||||
non-draft state. Its existing title is retained. ReleaseKit does not edit a
|
non-draft state. Its existing title is retained. ReleaseKit does not edit a
|
||||||
conflicting page. Slash-containing tags are escaped as one URL path segment.
|
conflicting page. Slash-containing tags are escaped as one URL path segment.
|
||||||
|
Page-only delivery does not require a release ID in this lookup response. Asset
|
||||||
|
operations require a positive release ID; a missing or nonpositive ID is
|
||||||
|
reported as a malformed service observation before an asset request is made.
|
||||||
|
|
||||||
If the page is absent, the adapter posts to
|
If the page is absent, the adapter posts to
|
||||||
`/api/v1/repos/{owner}/{repo}/releases` with the selected full tag and commit,
|
`/api/v1/repos/{owner}/{repo}/releases` with the selected full tag and commit,
|
||||||
|
|||||||
@@ -347,7 +347,9 @@ cancellation tests; run these with the race detector after they are coherent.
|
|||||||
|
|
||||||
## Stage 8: Classify missing Gitea release IDs at asset use
|
## Stage 8: Classify missing Gitea release IDs at asset use
|
||||||
|
|
||||||
**Status:** pending.
|
**Status:** complete. Offline Gitea and delivery package checks and public
|
||||||
|
page-only and prebuilt regressions pass, including missing, nonpositive, valid,
|
||||||
|
and wrong-type handles and not-attempted write evidence.
|
||||||
|
|
||||||
**Files:** `internal/gitea/backend.go`, its tests, a focused root delivery test,
|
**Files:** `internal/gitea/backend.go`, its tests, a focused root delivery test,
|
||||||
and `docs/integrations/gitea.md`.
|
and `docs/integrations/gitea.md`.
|
||||||
|
|||||||
@@ -114,9 +114,9 @@ func (b *Backend) CreateRelease(ctx context.Context, request forge.CreateRelease
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *Backend) UploadAsset(ctx context.Context, handle any, filename string, content io.Reader) (forge.WriteEvidence, error) {
|
func (b *Backend) UploadAsset(ctx context.Context, handle any, filename string, content io.Reader) (forge.WriteEvidence, error) {
|
||||||
release, ok := handle.(releaseHandle)
|
release, err := assetReleaseHandle(handle)
|
||||||
if !ok || release.id <= 0 {
|
if err != nil {
|
||||||
return forge.WriteEvidence{Disposition: forge.WriteNotAttempted}, &forge.Failure{Kind: forge.FailureInvalid, Err: errors.New("release handle was not created by the Gitea backend")}
|
return forge.WriteEvidence{Disposition: forge.WriteNotAttempted}, err
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(filename) == "" || content == nil {
|
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")}
|
return forge.WriteEvidence{Disposition: forge.WriteNotAttempted}, &forge.Failure{Kind: forge.FailureInvalid, Err: errors.New("asset filename and content reader are required")}
|
||||||
@@ -239,9 +239,9 @@ func (b *Backend) FindRelease(ctx context.Context, tag string) (forge.Release, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *Backend) ListAssets(ctx context.Context, handle any) ([]forge.Asset, error) {
|
func (b *Backend) ListAssets(ctx context.Context, handle any) ([]forge.Asset, error) {
|
||||||
release, ok := handle.(releaseHandle)
|
release, err := assetReleaseHandle(handle)
|
||||||
if !ok {
|
if err != nil {
|
||||||
return nil, &forge.Failure{Kind: forge.FailureInvalid, Err: errors.New("release handle was not created by the Gitea backend")}
|
return nil, err
|
||||||
}
|
}
|
||||||
_, apiRoot, err := b.destinationAPI("list release assets")
|
_, apiRoot, err := b.destinationAPI("list release assets")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -262,6 +262,17 @@ func (b *Backend) ListAssets(ctx context.Context, handle any) ([]forge.Asset, er
|
|||||||
return assets, nil
|
return assets, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func assetReleaseHandle(handle any) (releaseHandle, error) {
|
||||||
|
release, ok := handle.(releaseHandle)
|
||||||
|
if !ok {
|
||||||
|
return releaseHandle{}, &forge.Failure{Kind: forge.FailureInvalid, Err: errors.New("release handle was not created by the Gitea backend")}
|
||||||
|
}
|
||||||
|
if release.id <= 0 {
|
||||||
|
return releaseHandle{}, &forge.Failure{Kind: forge.FailureMalformed, Err: errors.New("release response does not contain a positive release ID")}
|
||||||
|
}
|
||||||
|
return release, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (b *Backend) OpenAsset(ctx context.Context, handle any) (io.ReadCloser, error) {
|
func (b *Backend) OpenAsset(ctx context.Context, handle any) (io.ReadCloser, error) {
|
||||||
asset, ok := handle.(assetHandle)
|
asset, ok := handle.(assetHandle)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
@@ -64,6 +64,70 @@ func TestBackendFindReleaseDoesNotRequireWriteResponseFields(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBackendAssetOperationsRequireObservedReleaseID(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name, body string
|
||||||
|
valid bool
|
||||||
|
}{
|
||||||
|
{"missing", `{"tag_name":"v1.2.3"}`, false},
|
||||||
|
{"zero", `{"id":0,"tag_name":"v1.2.3"}`, false},
|
||||||
|
{"negative", `{"id":-1,"tag_name":"v1.2.3"}`, false},
|
||||||
|
{"valid", `{"id":12,"tag_name":"v1.2.3"}`, true},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
requests := 0
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if strings.Contains(r.URL.Path, "/releases/tags/") {
|
||||||
|
_, _ = io.WriteString(w, tc.body)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
requests++
|
||||||
|
if r.Method == http.MethodGet {
|
||||||
|
_, _ = io.WriteString(w, `[]`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
_, _ = io.WriteString(w, `{"id":1,"name":"file"}`)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
backend := NewBackend(testDestination(server.URL), server.Client())
|
||||||
|
release, err := backend.FindRelease(context.Background(), "v1.2.3")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
_, listErr := backend.ListAssets(context.Background(), release.Handle)
|
||||||
|
evidence, uploadErr := backend.UploadAsset(context.Background(), release.Handle, "file", strings.NewReader("data"))
|
||||||
|
if tc.valid {
|
||||||
|
if listErr != nil || uploadErr != nil || requests != 2 {
|
||||||
|
t.Fatalf("valid: list %v, upload %v, requests %d", listErr, uploadErr, requests)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, err := range []error{listErr, uploadErr} {
|
||||||
|
var failure *forge.Failure
|
||||||
|
if !errors.As(err, &failure) || failure.Kind != forge.FailureMalformed {
|
||||||
|
t.Fatalf("failure = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if requests != 0 || evidence.Attempted || evidence.Disposition != forge.WriteNotAttempted {
|
||||||
|
t.Fatalf("requests = %d, evidence = %#v", requests, evidence)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
backend := NewBackend(Destination{}, nil)
|
||||||
|
_, listErr := backend.ListAssets(context.Background(), "wrong")
|
||||||
|
evidence, uploadErr := backend.UploadAsset(context.Background(), "wrong", "file", strings.NewReader("data"))
|
||||||
|
for _, err := range []error{listErr, uploadErr} {
|
||||||
|
var failure *forge.Failure
|
||||||
|
if !errors.As(err, &failure) || failure.Kind != forge.FailureInvalid {
|
||||||
|
t.Fatalf("wrong handle failure = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if evidence.Attempted || evidence.Disposition != forge.WriteNotAttempted {
|
||||||
|
t.Fatalf("wrong handle evidence = %#v", evidence)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestBackendListsMetadataAndOpensCallerOwnedAssetStream(t *testing.T) {
|
func TestBackendListsMetadataAndOpensCallerOwnedAssetStream(t *testing.T) {
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if got, want := r.Header.Get("Authorization"), "token fixture-secret-token"; got != want {
|
if got, want := r.Header.Get("Authorization"), "token fixture-secret-token"; got != want {
|
||||||
|
|||||||
Reference in New Issue
Block a user