Resolve prebuilt assets from the selected module
This commit is contained in:
89
cmd/releasekit/assets_test.go
Normal file
89
cmd/releasekit/assets_test.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit/internal/cli"
|
||||
)
|
||||
|
||||
func TestBuildAndPrebuiltDeliveryUseSameDirectoryBase(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
name, module string
|
||||
absolute bool
|
||||
}{
|
||||
{name: "root relative"},
|
||||
{name: "nested relative", module: "apps/widget"},
|
||||
{name: "nested absolute", module: "apps/widget", absolute: true},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
repository := newDeliveryBuildRepository(t)
|
||||
moduleDirectory := repository
|
||||
tag := "v1.2.3"
|
||||
if tt.module != "" {
|
||||
moduleDirectory = filepath.Join(repository, filepath.FromSlash(tt.module))
|
||||
tag = tt.module + "/v1.2.3"
|
||||
for _, name := range []string{"go.mod", "main.go", "docs/releases/v1.2.3.md"} {
|
||||
contents, err := os.ReadFile(filepath.Join(repository, filepath.FromSlash(name)))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
writeRepositoryFile(t, repository, tt.module+"/"+name, string(contents))
|
||||
}
|
||||
}
|
||||
writeRepositoryFile(t, repository, ".gitignore", "dist/\n")
|
||||
state := &deliveryServerState{tag: tag}
|
||||
server := newDeliveryServer(t, state)
|
||||
defer server.Close()
|
||||
settings := cli.Settings{Product: "Example Product", ModuleDir: tt.module,
|
||||
Build: &cli.BuildSettings{MainPackage: ".", BinaryName: "helper", Targets: []string{"linux/amd64"}},
|
||||
Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"},
|
||||
}
|
||||
writeDeliveryProfile(t, repository, settings)
|
||||
cliGitRun(t, repository, "add", ".")
|
||||
cliGitRun(t, repository, "commit", "-m", "Prepare module source and output exclusion")
|
||||
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD")
|
||||
cliGitRun(t, repository, "tag", tag)
|
||||
output, resolved := "dist", filepath.Join(moduleDirectory, "dist")
|
||||
if tt.absolute {
|
||||
output = filepath.Join(t.TempDir(), "assets")
|
||||
resolved = output
|
||||
}
|
||||
var stdout, stderr bytes.Buffer
|
||||
if status := run(context.Background(), []string{"build", "--repo", repository, "--output", output, "v1.2.3"}, &stdout, &stderr); status != 0 {
|
||||
t.Fatalf("build status=%d, stderr=%s", status, stderr.String())
|
||||
}
|
||||
receipt, err := cli.LoadBuildReceipt(filepath.Join(resolved, cli.BuildReceiptName))
|
||||
if err != nil || receipt.ModuleDir != moduleDirectory || receipt.Tag != tag {
|
||||
t.Fatalf("build receipt=%#v, error=%v", receipt, err)
|
||||
}
|
||||
t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token")
|
||||
args := []string{"deliver", "--repo", repository, "--assets", output, "v1.2.3"}
|
||||
stdout.Reset()
|
||||
stderr.Reset()
|
||||
if status := run(context.Background(), args, &stdout, &stderr); status != 0 || state.releaseCreates != 1 || state.assetUploads != 2 {
|
||||
t.Fatalf("delivery status=%d, creates=%d, uploads=%d, stderr=%s", status, state.releaseCreates, state.assetUploads, stderr.String())
|
||||
}
|
||||
if tt.module != "" {
|
||||
cliGitRun(t, repository, "tag", "v1.2.3")
|
||||
wrongModule := settings
|
||||
wrongModule.ModuleDir = ""
|
||||
invocation := cli.Invocation{Repository: repository, AssetsDir: resolved, Version: "v1.2.3"}
|
||||
requestsBefore := state.apiRequests
|
||||
if status := deliverWithSettings(context.Background(), invocation, wrongModule, &stdout, &stderr); status != 1 || state.apiRequests != requestsBefore {
|
||||
t.Fatalf("wrong module status=%d, requests=%d->%d, stderr=%s", status, requestsBefore, state.apiRequests, stderr.String())
|
||||
}
|
||||
}
|
||||
if err := os.WriteFile(receipt.Artifacts[0].Path, []byte("changed"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
requestsBefore := state.apiRequests
|
||||
if status := run(context.Background(), args, &stdout, &stderr); status != 1 || state.apiRequests != requestsBefore {
|
||||
t.Fatalf("changed content status=%d, requests=%d->%d, stderr=%s", status, requestsBefore, state.apiRequests, stderr.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/buildinfo"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit/internal/cli"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/artifact"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/git"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/releaseidentity"
|
||||
)
|
||||
@@ -342,10 +343,12 @@ func deliverWithDestination(ctx context.Context, invocation cli.Invocation, sett
|
||||
}
|
||||
if hasTargets {
|
||||
if invocation.AssetsDir != "" {
|
||||
assetsDir := invocation.AssetsDir
|
||||
if !filepath.IsAbs(assetsDir) {
|
||||
assetsDir = filepath.Join(invocation.Repository, assetsDir)
|
||||
selection, err := releaseidentity.ResolveSelection(invocation.Repository, settings.ModuleDir)
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "resolve asset module directory: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
assetsDir := artifact.OutputDirectory(selection.ModuleDir, invocation.AssetsDir)
|
||||
receipt, err := cli.LoadBuildReceipt(filepath.Join(assetsDir, cli.BuildReceiptName))
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "load verified build receipt: %v\n", err)
|
||||
|
||||
@@ -1136,6 +1136,7 @@ func writeDeliveryReceipt(t *testing.T, repository string, settings cli.BuildSet
|
||||
}
|
||||
|
||||
type deliveryServerState struct {
|
||||
tag string
|
||||
malformedCreate bool
|
||||
failAssetListing bool
|
||||
failAssetDownloads bool
|
||||
@@ -1155,6 +1156,9 @@ type deliveryServerState struct {
|
||||
|
||||
func newDeliveryServer(t *testing.T, state *deliveryServerState) *httptest.Server {
|
||||
t.Helper()
|
||||
if state.tag == "" {
|
||||
state.tag = "v1.2.3"
|
||||
}
|
||||
if state.assets == nil {
|
||||
state.assets = make(map[string][]byte)
|
||||
}
|
||||
@@ -1167,7 +1171,7 @@ func newDeliveryServer(t *testing.T, state *deliveryServerState) *httptest.Serve
|
||||
}
|
||||
}
|
||||
switch r.URL.Path {
|
||||
case "/api/v1/repos/owner/example/tags/v1.2.3":
|
||||
case "/api/v1/repos/owner/example/tags/" + state.tag:
|
||||
if state.commit == "" {
|
||||
t.Error("tag source commit was not initialized")
|
||||
}
|
||||
@@ -1175,8 +1179,8 @@ func newDeliveryServer(t *testing.T, state *deliveryServerState) *httptest.Serve
|
||||
if commit == "" {
|
||||
commit = state.commit
|
||||
}
|
||||
_, _ = fmt.Fprintf(w, `{"name":"v1.2.3","commit":{"sha":%q}}`, commit)
|
||||
case "/api/v1/repos/owner/example/releases/tags/v1.2.3":
|
||||
_, _ = fmt.Fprintf(w, `{"name":%q,"commit":{"sha":%q}}`, state.tag, commit)
|
||||
case "/api/v1/repos/owner/example/releases/tags/" + state.tag:
|
||||
if !state.pageExists {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
@@ -1197,13 +1201,13 @@ func newDeliveryServer(t *testing.T, state *deliveryServerState) *httptest.Serve
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
t.Errorf("decode create payload: %v", err)
|
||||
}
|
||||
if payload.Tag != "v1.2.3" || payload.Target != state.commit {
|
||||
if payload.Tag != state.tag || payload.Target != state.commit {
|
||||
t.Errorf("release source payload = %#v", payload)
|
||||
}
|
||||
state.pageExists, state.releaseCreates, state.noteBody = true, state.releaseCreates+1, payload.Body
|
||||
if state.malformedCreate {
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"id": 10, "tag_name": "v1.2.3", "name": "inconsistent response", "body": payload.Body})
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"id": 10, "tag_name": state.tag, "name": "inconsistent response", "body": payload.Body})
|
||||
return
|
||||
}
|
||||
writeDeliveryRelease(w, serverURL, state)
|
||||
@@ -1282,7 +1286,7 @@ func writeDeliveryRelease(w http.ResponseWriter, serverURL string, state *delive
|
||||
if product == "" {
|
||||
product = "Example Product"
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"id": 10, "html_url": serverURL + "/releases/10", "tag_name": "v1.2.3", "target_commitish": state.commit, "name": product + " v1.2.3", "body": state.noteBody, "draft": false, "prerelease": false})
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"id": 10, "html_url": serverURL + "/releases/10", "tag_name": state.tag, "target_commitish": state.commit, "name": product + " v1.2.3", "body": state.noteBody, "draft": false, "prerelease": false})
|
||||
}
|
||||
|
||||
func writeDeliveryProfile(t *testing.T, repository string, settings cli.Settings) {
|
||||
|
||||
@@ -185,7 +185,9 @@ the tagged source into temporary storage, removes it when the operation ends,
|
||||
and uploads its binaries and complete `SHA256SUMS`. When `--assets DIR` is
|
||||
supplied, that directory must contain the private `.releasekit-build.json`
|
||||
receipt and matching unchanged artifacts; this keeps the caller's original
|
||||
bytes available for retries. Profiles without targets use page-only delivery
|
||||
bytes available for retries. Relative `--assets` paths use the selected module
|
||||
directory, matching `build --output`; absolute paths retain their supplied base.
|
||||
Profiles without targets use page-only delivery
|
||||
and need no receipt, binary build, or manifest. `deliver` never runs candidate
|
||||
checks or publishes a tag.
|
||||
|
||||
|
||||
@@ -46,6 +46,10 @@ version, complete tag, source commit, normalized build settings, and artifact
|
||||
declarations. Older and unversioned receipts are rejected; rebuild with the
|
||||
updated `build` command before using `deliver --assets DIR`.
|
||||
|
||||
Relative `build --output DIR` and `deliver --assets DIR` paths both use the
|
||||
selected module directory. Nested-module consumers can use the same relative
|
||||
directory for building and retrying delivery; absolute directories remain usable.
|
||||
|
||||
The maintained [multi-module example](../../examples/multi-module/) shares a
|
||||
four-column `release-modules.tsv` and POSIX selector between its local launcher
|
||||
and mapped Woodpecker workflow. The workflow verifies and detaches the triggering
|
||||
|
||||
@@ -99,7 +99,14 @@ later partial-artifact behavior intact.
|
||||
|
||||
## Issue 3: Use the module path base for relative prebuilt assets (P2)
|
||||
|
||||
Status: open. Category: incomplete module path adoption.
|
||||
Status: complete. Category: incomplete module path adoption.
|
||||
|
||||
**Completion:** Build output and CLI prebuilt assets share the selected module's
|
||||
directory resolver. CLI and pending migration guidance describe the common base.
|
||||
Local hosted regression tests verify root and nested relative build/delivery
|
||||
pairs, absolute output, module binding, and rejection of changed artifact content
|
||||
before hosted requests. Existing receipt/content checks and nested build cases
|
||||
pass offline.
|
||||
|
||||
**Evidence:** [CLI prebuilt delivery](../../cmd/releasekit/main.go) joins a
|
||||
relative `--assets` directory to `invocation.Repository`. Builds now resolve
|
||||
|
||||
@@ -3,11 +3,24 @@ package artifact
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const ManifestFilename = "SHA256SUMS"
|
||||
|
||||
// OutputDirectory resolves build output and prebuilt asset directories from
|
||||
// the absolute selected module directory.
|
||||
func OutputDirectory(moduleDir, directory string) string {
|
||||
if directory == "" {
|
||||
directory = "dist"
|
||||
}
|
||||
if !filepath.IsAbs(directory) {
|
||||
directory = filepath.Join(moduleDir, directory)
|
||||
}
|
||||
return filepath.Clean(directory)
|
||||
}
|
||||
|
||||
type Target struct {
|
||||
OS string
|
||||
Arch string
|
||||
|
||||
@@ -113,11 +113,11 @@ func BuildResolved(ctx context.Context, request Request) (Result, error) {
|
||||
}
|
||||
|
||||
if len(result.Targets) == 0 {
|
||||
result.OutputDir = resolvedOutputDir(moduleDir, request.OutputDir)
|
||||
result.OutputDir = artifact.OutputDirectory(moduleDir, request.OutputDir)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
result.OutputDir = resolvedOutputDir(moduleDir, request.OutputDir)
|
||||
result.OutputDir = artifact.OutputDirectory(moduleDir, request.OutputDir)
|
||||
declarations := artifact.ExpectedBuild(settings.BinaryName, identity.Version, settings.Targets)
|
||||
filenames := make([]string, len(declarations))
|
||||
for i, declaration := range declarations {
|
||||
@@ -204,19 +204,6 @@ func BuildResolved(ctx context.Context, request Request) (Result, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func resolvedOutputDir(moduleDir, outputDir string) string {
|
||||
if outputDir == "" {
|
||||
outputDir = filepath.Join(moduleDir, "dist")
|
||||
} else if !filepath.IsAbs(outputDir) {
|
||||
outputDir = filepath.Join(moduleDir, outputDir)
|
||||
}
|
||||
resolved, err := filepath.Abs(outputDir)
|
||||
if err != nil {
|
||||
return filepath.Clean(outputDir)
|
||||
}
|
||||
return resolved
|
||||
}
|
||||
|
||||
func gitCommit(ctx context.Context, repositoryDir string) (string, error) {
|
||||
command := exec.CommandContext(ctx, "git", "-C", repositoryDir, "rev-parse", "--verify", "HEAD^{commit}")
|
||||
output, err := command.Output()
|
||||
|
||||
Reference in New Issue
Block a user