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"
|
||||||
"gitea.maximumdirect.net/eric/ReleaseKit/buildinfo"
|
"gitea.maximumdirect.net/eric/ReleaseKit/buildinfo"
|
||||||
"gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit/internal/cli"
|
"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/git"
|
||||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/releaseidentity"
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/releaseidentity"
|
||||||
)
|
)
|
||||||
@@ -342,10 +343,12 @@ func deliverWithDestination(ctx context.Context, invocation cli.Invocation, sett
|
|||||||
}
|
}
|
||||||
if hasTargets {
|
if hasTargets {
|
||||||
if invocation.AssetsDir != "" {
|
if invocation.AssetsDir != "" {
|
||||||
assetsDir := invocation.AssetsDir
|
selection, err := releaseidentity.ResolveSelection(invocation.Repository, settings.ModuleDir)
|
||||||
if !filepath.IsAbs(assetsDir) {
|
if err != nil {
|
||||||
assetsDir = filepath.Join(invocation.Repository, assetsDir)
|
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))
|
receipt, err := cli.LoadBuildReceipt(filepath.Join(assetsDir, cli.BuildReceiptName))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(stderr, "load verified build receipt: %v\n", err)
|
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 {
|
type deliveryServerState struct {
|
||||||
|
tag string
|
||||||
malformedCreate bool
|
malformedCreate bool
|
||||||
failAssetListing bool
|
failAssetListing bool
|
||||||
failAssetDownloads bool
|
failAssetDownloads bool
|
||||||
@@ -1155,6 +1156,9 @@ type deliveryServerState struct {
|
|||||||
|
|
||||||
func newDeliveryServer(t *testing.T, state *deliveryServerState) *httptest.Server {
|
func newDeliveryServer(t *testing.T, state *deliveryServerState) *httptest.Server {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
if state.tag == "" {
|
||||||
|
state.tag = "v1.2.3"
|
||||||
|
}
|
||||||
if state.assets == nil {
|
if state.assets == nil {
|
||||||
state.assets = make(map[string][]byte)
|
state.assets = make(map[string][]byte)
|
||||||
}
|
}
|
||||||
@@ -1167,7 +1171,7 @@ func newDeliveryServer(t *testing.T, state *deliveryServerState) *httptest.Serve
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
switch r.URL.Path {
|
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 == "" {
|
if state.commit == "" {
|
||||||
t.Error("tag source commit was not initialized")
|
t.Error("tag source commit was not initialized")
|
||||||
}
|
}
|
||||||
@@ -1175,8 +1179,8 @@ func newDeliveryServer(t *testing.T, state *deliveryServerState) *httptest.Serve
|
|||||||
if commit == "" {
|
if commit == "" {
|
||||||
commit = state.commit
|
commit = state.commit
|
||||||
}
|
}
|
||||||
_, _ = fmt.Fprintf(w, `{"name":"v1.2.3","commit":{"sha":%q}}`, commit)
|
_, _ = fmt.Fprintf(w, `{"name":%q,"commit":{"sha":%q}}`, state.tag, commit)
|
||||||
case "/api/v1/repos/owner/example/releases/tags/v1.2.3":
|
case "/api/v1/repos/owner/example/releases/tags/" + state.tag:
|
||||||
if !state.pageExists {
|
if !state.pageExists {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
@@ -1197,13 +1201,13 @@ func newDeliveryServer(t *testing.T, state *deliveryServerState) *httptest.Serve
|
|||||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||||
t.Errorf("decode create payload: %v", err)
|
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)
|
t.Errorf("release source payload = %#v", payload)
|
||||||
}
|
}
|
||||||
state.pageExists, state.releaseCreates, state.noteBody = true, state.releaseCreates+1, payload.Body
|
state.pageExists, state.releaseCreates, state.noteBody = true, state.releaseCreates+1, payload.Body
|
||||||
if state.malformedCreate {
|
if state.malformedCreate {
|
||||||
w.WriteHeader(http.StatusCreated)
|
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
|
return
|
||||||
}
|
}
|
||||||
writeDeliveryRelease(w, serverURL, state)
|
writeDeliveryRelease(w, serverURL, state)
|
||||||
@@ -1282,7 +1286,7 @@ func writeDeliveryRelease(w http.ResponseWriter, serverURL string, state *delive
|
|||||||
if product == "" {
|
if product == "" {
|
||||||
product = "Example 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) {
|
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
|
and uploads its binaries and complete `SHA256SUMS`. When `--assets DIR` is
|
||||||
supplied, that directory must contain the private `.releasekit-build.json`
|
supplied, that directory must contain the private `.releasekit-build.json`
|
||||||
receipt and matching unchanged artifacts; this keeps the caller's original
|
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
|
and need no receipt, binary build, or manifest. `deliver` never runs candidate
|
||||||
checks or publishes a tag.
|
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
|
declarations. Older and unversioned receipts are rejected; rebuild with the
|
||||||
updated `build` command before using `deliver --assets DIR`.
|
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
|
The maintained [multi-module example](../../examples/multi-module/) shares a
|
||||||
four-column `release-modules.tsv` and POSIX selector between its local launcher
|
four-column `release-modules.tsv` and POSIX selector between its local launcher
|
||||||
and mapped Woodpecker workflow. The workflow verifies and detaches the triggering
|
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)
|
## 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
|
**Evidence:** [CLI prebuilt delivery](../../cmd/releasekit/main.go) joins a
|
||||||
relative `--assets` directory to `invocation.Repository`. Builds now resolve
|
relative `--assets` directory to `invocation.Repository`. Builds now resolve
|
||||||
|
|||||||
@@ -3,11 +3,24 @@ package artifact
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const ManifestFilename = "SHA256SUMS"
|
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 {
|
type Target struct {
|
||||||
OS string
|
OS string
|
||||||
Arch string
|
Arch string
|
||||||
|
|||||||
@@ -113,11 +113,11 @@ func BuildResolved(ctx context.Context, request Request) (Result, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(result.Targets) == 0 {
|
if len(result.Targets) == 0 {
|
||||||
result.OutputDir = resolvedOutputDir(moduleDir, request.OutputDir)
|
result.OutputDir = artifact.OutputDirectory(moduleDir, request.OutputDir)
|
||||||
return result, nil
|
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)
|
declarations := artifact.ExpectedBuild(settings.BinaryName, identity.Version, settings.Targets)
|
||||||
filenames := make([]string, len(declarations))
|
filenames := make([]string, len(declarations))
|
||||||
for i, declaration := range declarations {
|
for i, declaration := range declarations {
|
||||||
@@ -204,19 +204,6 @@ func BuildResolved(ctx context.Context, request Request) (Result, error) {
|
|||||||
return result, nil
|
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) {
|
func gitCommit(ctx context.Context, repositoryDir string) (string, error) {
|
||||||
command := exec.CommandContext(ctx, "git", "-C", repositoryDir, "rev-parse", "--verify", "HEAD^{commit}")
|
command := exec.CommandContext(ctx, "git", "-C", repositoryDir, "rev-parse", "--verify", "HEAD^{commit}")
|
||||||
output, err := command.Output()
|
output, err := command.Output()
|
||||||
|
|||||||
Reference in New Issue
Block a user