Build from the selected module
This commit is contained in:
51
build.go
51
build.go
@@ -4,7 +4,9 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/artifact"
|
||||||
internalbuild "gitea.maximumdirect.net/eric/ReleaseKit/internal/build"
|
internalbuild "gitea.maximumdirect.net/eric/ReleaseKit/internal/build"
|
||||||
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/releaseidentity"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Target identifies one explicit Go operating-system and architecture pair.
|
// Target identifies one explicit Go operating-system and architecture pair.
|
||||||
@@ -28,7 +30,7 @@ type BuildRequest struct {
|
|||||||
ModuleDir string
|
ModuleDir string
|
||||||
Version string // stable vMAJOR.MINOR.PATCH value injected at link time.
|
Version string // stable vMAJOR.MINOR.PATCH value injected at link time.
|
||||||
Spec BuildSpec // application package, artifact name, and targets.
|
Spec BuildSpec // application package, artifact name, and targets.
|
||||||
OutputDir string // output location; empty selects <repository>/dist.
|
OutputDir string // output location; relative paths use ModuleDir and empty selects <module>/dist.
|
||||||
Output io.Writer // optional build progress destination.
|
Output io.Writer // optional build progress destination.
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,22 +61,27 @@ type BuildResult struct {
|
|||||||
Compiler string // output of the selected local Go toolchain's version command.
|
Compiler string // output of the selected local Go toolchain's version command.
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build compiles the selected targets with Version injected through buildinfo.
|
// Build resolves the selected module's working-tree identity, then compiles the
|
||||||
// It uses the available compatible local Go toolchain, applies ReleaseKit's
|
// selected targets with Version injected through buildinfo. It uses the
|
||||||
// readonly offline build settings, preserves effective cache and tool settings,
|
// available compatible local Go toolchain, applies ReleaseKit's readonly
|
||||||
// and verifies the version command on the Go host before succeeding. An empty
|
// offline build settings, preserves effective cache and tool settings, and
|
||||||
// target set records source identity without running the Go toolchain. Errors
|
// verifies the version command on the Go host before succeeding. Relative
|
||||||
// return partial build facts in the result.
|
// output paths use ModuleDir; an empty path selects its dist directory. An
|
||||||
|
// empty target set records source identity without running the Go toolchain.
|
||||||
|
// Errors return partial build facts in the result.
|
||||||
func Build(ctx context.Context, request BuildRequest) (BuildResult, error) {
|
func Build(ctx context.Context, request BuildRequest) (BuildResult, error) {
|
||||||
result, err := internalbuild.Build(ctx, internalbuild.Request{
|
public := BuildResult{Version: request.Version}
|
||||||
RepositoryDir: request.RepositoryDir,
|
identity, err := releaseidentity.FromWorkingTree(ctx, request.RepositoryDir, request.ModuleDir, request.Version)
|
||||||
ModuleDir: request.ModuleDir,
|
if err != nil {
|
||||||
Version: request.Version,
|
return public, err
|
||||||
Spec: *internalBuildSpec(&request.Spec),
|
}
|
||||||
OutputDir: request.OutputDir,
|
result, err := internalbuild.BuildResolved(ctx, internalbuild.Request{
|
||||||
Output: request.Output,
|
Identity: identity,
|
||||||
|
Spec: *internalBuildSpec(&request.Spec),
|
||||||
|
OutputDir: request.OutputDir,
|
||||||
|
Output: request.Output,
|
||||||
})
|
})
|
||||||
public := BuildResult{
|
public = BuildResult{
|
||||||
RepositoryDir: result.RepositoryDir,
|
RepositoryDir: result.RepositoryDir,
|
||||||
ModuleDir: result.ModuleDir,
|
ModuleDir: result.ModuleDir,
|
||||||
ModulePath: result.ModulePath,
|
ModulePath: result.ModulePath,
|
||||||
@@ -82,14 +89,14 @@ func Build(ctx context.Context, request BuildRequest) (BuildResult, error) {
|
|||||||
CandidateCommit: result.CandidateCommit,
|
CandidateCommit: result.CandidateCommit,
|
||||||
SourceVerified: result.SourceVerified,
|
SourceVerified: result.SourceVerified,
|
||||||
Version: result.Version,
|
Version: result.Version,
|
||||||
MainPackage: result.MainPackage,
|
MainPackage: result.Settings.MainPackage,
|
||||||
BinaryName: result.BinaryName,
|
BinaryName: result.Settings.BinaryName,
|
||||||
OutputDir: result.OutputDir,
|
OutputDir: result.OutputDir,
|
||||||
Compiler: result.Compiler,
|
Compiler: result.Compiler,
|
||||||
Targets: make([]Target, 0, len(result.Targets)),
|
Targets: make([]Target, 0, len(result.Settings.Targets)),
|
||||||
Artifacts: make([]BuildArtifact, 0, len(result.Artifacts)),
|
Artifacts: make([]BuildArtifact, 0, len(result.Artifacts)),
|
||||||
}
|
}
|
||||||
for _, target := range result.Targets {
|
for _, target := range result.Settings.Targets {
|
||||||
public.Targets = append(public.Targets, Target{OS: target.OS, Arch: target.Arch})
|
public.Targets = append(public.Targets, Target{OS: target.OS, Arch: target.Arch})
|
||||||
}
|
}
|
||||||
for _, artifact := range result.Artifacts {
|
for _, artifact := range result.Artifacts {
|
||||||
@@ -115,8 +122,10 @@ func internalBuildSpec(spec *BuildSpec) *internalbuild.Spec {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func internalBuildResult(result BuildResult) *internalbuild.Result {
|
func internalBuildResult(result BuildResult) *internalbuild.Result {
|
||||||
converted := &internalbuild.Result{RepositoryDir: result.RepositoryDir, ModuleDir: result.ModuleDir, ModulePath: result.ModulePath, Tag: result.Tag, CandidateCommit: result.CandidateCommit, SourceVerified: result.SourceVerified, Version: result.Version, MainPackage: result.MainPackage, BinaryName: result.BinaryName, OutputDir: result.OutputDir, Compiler: result.Compiler}
|
input := internalBuildSpec(&BuildSpec{MainPackage: result.MainPackage, BinaryName: result.BinaryName, Targets: result.Targets})
|
||||||
for _, target := range result.Targets {
|
settings := artifact.NormalizeSettings(input.MainPackage, input.BinaryName, input.Targets)
|
||||||
|
converted := &internalbuild.Result{RepositoryDir: result.RepositoryDir, ModuleDir: result.ModuleDir, ModulePath: result.ModulePath, Tag: result.Tag, CandidateCommit: result.CandidateCommit, SourceVerified: result.SourceVerified, Settings: settings, Version: result.Version, MainPackage: settings.MainPackage, BinaryName: settings.BinaryName, OutputDir: result.OutputDir, Compiler: result.Compiler}
|
||||||
|
for _, target := range settings.Targets {
|
||||||
converted.Targets = append(converted.Targets, internalbuild.Target{OS: target.OS, Arch: target.Arch})
|
converted.Targets = append(converted.Targets, internalbuild.Target{OS: target.OS, Arch: target.Arch})
|
||||||
}
|
}
|
||||||
for _, artifact := range result.Artifacts {
|
for _, artifact := range result.Artifacts {
|
||||||
|
|||||||
157
build_test.go
157
build_test.go
@@ -115,7 +115,7 @@ func main() {
|
|||||||
t.Setenv("GOARCH", "arm64")
|
t.Setenv("GOARCH", "arm64")
|
||||||
result, err := releasekit.Build(context.Background(), releasekit.BuildRequest{
|
result, err := releasekit.Build(context.Background(), releasekit.BuildRequest{
|
||||||
RepositoryDir: repository,
|
RepositoryDir: repository,
|
||||||
Version: "v2.3.4",
|
Version: "v1.2.3",
|
||||||
Spec: releasekit.BuildSpec{
|
Spec: releasekit.BuildSpec{
|
||||||
MainPackage: "./cmd/app",
|
MainPackage: "./cmd/app",
|
||||||
BinaryName: "fixture",
|
BinaryName: "fixture",
|
||||||
@@ -126,7 +126,7 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Build() error = %v; partial result = %#v", err, result)
|
t.Fatalf("Build() error = %v; partial result = %#v", err, result)
|
||||||
}
|
}
|
||||||
if len(result.Artifacts) != 1 || result.Artifacts[0].Filename != "fixture-v2.3.4-windows-arm64.exe" {
|
if len(result.Artifacts) != 1 || result.Artifacts[0].Filename != "fixture-v1.2.3-windows-arm64.exe" {
|
||||||
t.Fatalf("build artifacts = %#v", result.Artifacts)
|
t.Fatalf("build artifacts = %#v", result.Artifacts)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -166,7 +166,7 @@ func main() { if len(os.Args) == 2 && os.Args[1] == "version" { fmt.Printf("fixt
|
|||||||
|
|
||||||
result, err := releasekit.Build(context.Background(), releasekit.BuildRequest{
|
result, err := releasekit.Build(context.Background(), releasekit.BuildRequest{
|
||||||
RepositoryDir: repository,
|
RepositoryDir: repository,
|
||||||
Version: "v2.3.4",
|
Version: "v1.2.3",
|
||||||
Spec: releasekit.BuildSpec{
|
Spec: releasekit.BuildSpec{
|
||||||
MainPackage: "./cmd/app",
|
MainPackage: "./cmd/app",
|
||||||
BinaryName: "fixture",
|
BinaryName: "fixture",
|
||||||
@@ -242,6 +242,145 @@ func main() {}
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuildUsesNestedWorkingTreeModuleAndModuleRelativeOutput(t *testing.T) {
|
||||||
|
repository := newBuildConsumer(t, "package main\nfunc main() {}\n")
|
||||||
|
moduleDir := filepath.Join(repository, "integrations", "nested")
|
||||||
|
writeNestedBuildConsumer(t, repository, "integrations/nested", "example.com/nestedbuild", `package main
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"gitea.maximumdirect.net/eric/ReleaseKit/buildinfo"
|
||||||
|
)
|
||||||
|
func main() {
|
||||||
|
if len(os.Args) == 2 && os.Args[1] == "version" {
|
||||||
|
if logPath := os.Getenv("RELEASEKIT_SMOKE_LOG"); logPath != "" {
|
||||||
|
if executable, err := os.Executable(); err == nil {
|
||||||
|
_ = os.WriteFile(logPath, []byte(executable), 0o600)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Printf("nested-app %s\n", buildinfo.Version())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
outputDirectory := "artifacts"
|
||||||
|
crossTarget := releasekit.Target{OS: "darwin", Arch: "arm64"}
|
||||||
|
if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {
|
||||||
|
crossTarget = releasekit.Target{OS: "linux", Arch: "amd64"}
|
||||||
|
}
|
||||||
|
nativeTarget := releasekit.Target{OS: runtime.GOOS, Arch: runtime.GOARCH}
|
||||||
|
result, err := releasekit.Build(context.Background(), releasekit.BuildRequest{
|
||||||
|
RepositoryDir: repository,
|
||||||
|
ModuleDir: "integrations/nested",
|
||||||
|
Version: "v1.2.3",
|
||||||
|
Spec: releasekit.BuildSpec{
|
||||||
|
MainPackage: "./cmd/app",
|
||||||
|
BinaryName: "nested-app",
|
||||||
|
Targets: []releasekit.Target{nativeTarget, crossTarget},
|
||||||
|
},
|
||||||
|
OutputDir: outputDirectory,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Build() error = %v; result = %#v", err, result)
|
||||||
|
}
|
||||||
|
if result.ModuleDir != moduleDir || result.ModulePath != "example.com/nestedbuild" || result.Tag != "integrations/nested/v1.2.3" {
|
||||||
|
t.Fatalf("nested build identity = %#v", result)
|
||||||
|
}
|
||||||
|
wantOutput := filepath.Join(moduleDir, outputDirectory)
|
||||||
|
if result.OutputDir != wantOutput || len(result.Artifacts) != 2 {
|
||||||
|
t.Fatalf("nested build output = %#v, want directory %q and two artifacts", result, wantOutput)
|
||||||
|
}
|
||||||
|
var native string
|
||||||
|
for _, artifact := range result.Artifacts {
|
||||||
|
if filepath.Dir(artifact.Path) != wantOutput || !strings.Contains(artifact.Filename, "nested-app-v1.2.3-") {
|
||||||
|
t.Fatalf("nested artifact = %#v", artifact)
|
||||||
|
}
|
||||||
|
if artifact.Target == nativeTarget {
|
||||||
|
native = artifact.Path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if native == "" {
|
||||||
|
t.Fatalf("native artifact missing from %#v", result.Artifacts)
|
||||||
|
}
|
||||||
|
if output, err := exec.Command(native, "version").CombinedOutput(); err != nil || string(output) != "nested-app v1.2.3\n" {
|
||||||
|
t.Fatalf("nested native version = %q, error = %v", output, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
localTemporary := filepath.Join(repository, "source-temporary")
|
||||||
|
if err := os.Mkdir(localTemporary, 0o700); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
crossOutput := filepath.Join(t.TempDir(), "cross-artifacts")
|
||||||
|
smokeLog := filepath.Join(t.TempDir(), "smoke-executable")
|
||||||
|
t.Setenv("RELEASEKIT_SMOKE_LOG", smokeLog)
|
||||||
|
t.Setenv("TMPDIR", localTemporary)
|
||||||
|
smokeResult, err := releasekit.Build(context.Background(), releasekit.BuildRequest{
|
||||||
|
RepositoryDir: repository,
|
||||||
|
ModuleDir: "integrations/nested",
|
||||||
|
Version: "v1.2.3",
|
||||||
|
Spec: releasekit.BuildSpec{
|
||||||
|
MainPackage: "./cmd/app",
|
||||||
|
BinaryName: "nested-app",
|
||||||
|
Targets: []releasekit.Target{crossTarget},
|
||||||
|
},
|
||||||
|
OutputDir: crossOutput,
|
||||||
|
})
|
||||||
|
if err != nil || len(smokeResult.Artifacts) != 1 {
|
||||||
|
t.Fatalf("nested cross build with source-local TMPDIR = %#v, %v", smokeResult, err)
|
||||||
|
}
|
||||||
|
smokePathBytes, err := os.ReadFile(smokeLog)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read native smoke executable path: %v", err)
|
||||||
|
}
|
||||||
|
smokePath := string(smokePathBytes)
|
||||||
|
relativeSmokePath, err := filepath.Rel(repository, smokePath)
|
||||||
|
if err != nil || relativeSmokePath == "." || (!filepath.IsAbs(relativeSmokePath) && relativeSmokePath != ".." && !strings.HasPrefix(relativeSmokePath, ".."+string(filepath.Separator))) {
|
||||||
|
t.Fatalf("native smoke output path %q is inside repository %q (relative %q, error %v)", smokePath, repository, relativeSmokePath, err)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(smokePath); !errors.Is(err, os.ErrNotExist) {
|
||||||
|
t.Fatalf("native smoke output %q was not removed: %v", smokePath, err)
|
||||||
|
}
|
||||||
|
remaining, err := os.ReadDir(localTemporary)
|
||||||
|
if err != nil || len(remaining) != 0 {
|
||||||
|
t.Fatalf("native smoke output remained in source-local TMPDIR: %v %v", remaining, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
emptyResult, err := releasekit.Build(context.Background(), releasekit.BuildRequest{
|
||||||
|
RepositoryDir: repository,
|
||||||
|
ModuleDir: "integrations/nested",
|
||||||
|
Version: "v1.2.3",
|
||||||
|
})
|
||||||
|
if err != nil || emptyResult.Compiler != "" || emptyResult.OutputDir != filepath.Join(moduleDir, "dist") {
|
||||||
|
t.Fatalf("nested empty build = %#v, %v", emptyResult, err)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(emptyResult.OutputDir); !errors.Is(err, os.ErrNotExist) {
|
||||||
|
t.Fatalf("empty nested build created dist: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildResolvesDirtyWorkingTreeDeclaration(t *testing.T) {
|
||||||
|
repository := newBuildConsumer(t, "package main\nfunc main() {}\n")
|
||||||
|
declarationPath := filepath.Join(repository, "go.mod")
|
||||||
|
contents, err := os.ReadFile(declarationPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
updated := strings.Replace(string(contents), "module example.com/buildfixture", "module example.com/changedbuild", 1)
|
||||||
|
if updated == string(contents) {
|
||||||
|
t.Fatal("fixture module declaration was not replaced")
|
||||||
|
}
|
||||||
|
writeBuildFixture(t, repository, "go.mod", updated)
|
||||||
|
result, err := releasekit.Build(context.Background(), releasekit.BuildRequest{
|
||||||
|
RepositoryDir: repository,
|
||||||
|
Version: "v1.2.3",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if result.ModulePath != "example.com/changedbuild" || result.Tag != "v1.2.3" {
|
||||||
|
t.Fatalf("dirty declaration identity = %#v", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestBuildRegeneratesOwnedOutputsAndPreservesUnrelatedFiles(t *testing.T) {
|
func TestBuildRegeneratesOwnedOutputsAndPreservesUnrelatedFiles(t *testing.T) {
|
||||||
const source = `package main
|
const source = `package main
|
||||||
import("fmt";"gitea.maximumdirect.net/eric/ReleaseKit/buildinfo")
|
import("fmt";"gitea.maximumdirect.net/eric/ReleaseKit/buildinfo")
|
||||||
@@ -563,6 +702,18 @@ func newBuildConsumer(t *testing.T, mainSource string) string {
|
|||||||
return repository
|
return repository
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func writeNestedBuildConsumer(t *testing.T, repository, moduleDir, modulePath, mainSource string) string {
|
||||||
|
t.Helper()
|
||||||
|
root, err := filepath.Abs(".")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
module := fmt.Sprintf("module %s\n\ngo 1.26.0\n\nrequire gitea.maximumdirect.net/eric/ReleaseKit v0.0.0\n\nreplace gitea.maximumdirect.net/eric/ReleaseKit => %s\n", modulePath, filepath.ToSlash(root))
|
||||||
|
writeBuildFixture(t, repository, filepath.ToSlash(filepath.Join(moduleDir, "go.mod")), module)
|
||||||
|
writeBuildFixture(t, repository, filepath.ToSlash(filepath.Join(moduleDir, "cmd/app/main.go")), mainSource)
|
||||||
|
return filepath.Join(repository, filepath.FromSlash(moduleDir))
|
||||||
|
}
|
||||||
|
|
||||||
func prepareCandidateBuild(t *testing.T, repository, mainSource string) {
|
func prepareCandidateBuild(t *testing.T, repository, mainSource string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
root, err := filepath.Abs(".")
|
root, err := filepath.Abs(".")
|
||||||
|
|||||||
@@ -36,19 +36,24 @@ func build(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`Build` records the repository's current `HEAD`, injects the selected stable
|
`Build` resolves `ModuleDir` and the selected module's working-tree declaration
|
||||||
version into `buildinfo.Version`, and returns the compiler identity, normalized
|
once, records the repository's current `HEAD`, injects the selected stable
|
||||||
targets, artifact paths, filenames, and SHA-256 digests. Names follow
|
version into `buildinfo.Version`, and returns repository/module identity, the
|
||||||
|
derived tag, compiler identity, normalized targets, artifact paths, filenames,
|
||||||
|
and SHA-256 digests. It does not require a release note, unused tag, clean
|
||||||
|
`main`, or publication. Names follow
|
||||||
`<binary>-<version>-<goos>-<goarch>`; Windows targets add `.exe`. A repeated
|
`<binary>-<version>-<goos>-<goarch>`; Windows targets add `.exe`. A repeated
|
||||||
target pair is built once, at its first position. Local builds may use modified
|
target pair is built once, at its first position. Local builds may use modified
|
||||||
source. `SourceVerified` is true only after a complete build observes clean
|
source. `SourceVerified` is true only after a complete build observes clean
|
||||||
source and the same HEAD before and after compilation; binary delivery requires
|
source and the same HEAD before and after compilation; binary delivery requires
|
||||||
that fact. This observation is not a source snapshot or attestation guarantee.
|
that fact. This observation is not a source snapshot or attestation guarantee.
|
||||||
|
|
||||||
The default output directory is `<repository>/dist`; a relative `OutputDir` is
|
Go commands run from `ModuleDir`, so `MainPackage` is interpreted within that
|
||||||
resolved from the repository. Selected artifact filenames are regenerated. Other files in the output
|
module. The default output directory is `<module>/dist`; a relative `OutputDir`
|
||||||
directory are left in place. A failed later build or native version check returns the binaries already
|
is resolved from the module, while an absolute path is used as supplied.
|
||||||
generated in `BuildResult.Artifacts`.
|
Selected artifact filenames are regenerated. Other files in the output
|
||||||
|
directory are left in place. A failed later build or native version check
|
||||||
|
returns the binaries already generated in `BuildResult.Artifacts`.
|
||||||
|
|
||||||
Builds use the available compatible Go toolchain selected locally; ReleaseKit
|
Builds use the available compatible Go toolchain selected locally; ReleaseKit
|
||||||
does not download a different toolchain automatically. It disables workspace
|
does not download a different toolchain automatically. It disables workspace
|
||||||
@@ -72,7 +77,9 @@ The [`build` command](../cli.md) reads the same build specification from the
|
|||||||
profile and writes a private receipt alongside its artifacts. `check` and
|
profile and writes a private receipt alongside its artifacts. `check` and
|
||||||
`publish` run selected builds after the standard suite and any additional
|
`publish` run selected builds after the standard suite and any additional
|
||||||
checks using temporary output outside the worktree. Candidate checks do not
|
checks using temporary output outside the worktree. Candidate checks do not
|
||||||
write a receipt or leave build files in the repository.
|
write a receipt or leave build files in the repository. Disposable native
|
||||||
|
version-check output also remains outside the whole repository when `TMPDIR`
|
||||||
|
points inside the source tree.
|
||||||
|
|
||||||
For delivery from a tagged checkout, `deliver` builds selected tagged-source
|
For delivery from a tagged checkout, `deliver` builds selected tagged-source
|
||||||
targets into temporary storage by default. Use `--assets DIR` to deliver a
|
targets into temporary storage by default. Use `--assets DIR` to deliver a
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ reports these implemented packages:
|
|||||||
| `cmd/releasekit/internal/cli` | Command parsing, strict JSON profile loading, configured checks, and private build receipts. |
|
| `cmd/releasekit/internal/cli` | Command parsing, strict JSON profile loading, configured checks, and private build receipts. |
|
||||||
| `internal/apicheck` | Compiled-export and import-boundary checks run during development. |
|
| `internal/apicheck` | Compiled-export and import-boundary checks run during development. |
|
||||||
| `internal/artifact` | Shared build-target normalization, artifact declarations, filename rules, and checksum-manifest naming. |
|
| `internal/artifact` | Shared build-target normalization, artifact declarations, filename rules, and checksum-manifest naming. |
|
||||||
| `internal/build` | Candidate-build coordination, selected Go target compilation, version injection, native smoke check, and artifact digests. |
|
| `internal/build` | Resolved-identity builds, normalized settings, selected-module compilation, version injection, external native smoke output, and artifact digests. |
|
||||||
| `internal/candidate` | Stable-version, note, repository-state, candidate capture, and post-validation checks. |
|
| `internal/candidate` | Stable-version, note, repository-state, candidate capture, and post-validation checks. |
|
||||||
| `internal/delivery` | Tagged-source and release-note checks, managed builds in disposable storage outside the checkout, structural artifact declaration, manifest assembly, one local digest-and-size preflight before hosted requests, and delivery coordination. |
|
| `internal/delivery` | Tagged-source and release-note checks, managed builds in disposable storage outside the checkout, structural artifact declaration, manifest assembly, one local digest-and-size preflight before hosted requests, and delivery coordination. |
|
||||||
| `internal/git` | Git command boundary, committed-tree reads, lazy local-only batched blob reads, push-destination resolution, exact ref inspection, tagged checkout preparation, and tag operations, including captured-endpoint verification during recovery. |
|
| `internal/git` | Git command boundary, committed-tree reads, lazy local-only batched blob reads, push-destination resolution, exact ref inspection, tagged checkout preparation, and tag operations, including captured-endpoint verification during recovery. |
|
||||||
@@ -87,7 +87,7 @@ not a separate release matrix:
|
|||||||
| Piped child-process exchanges and cancellation cleanup | `internal/process` | `TestStartPipedExchangesBeforeInputEOFAndPreservesSettings`, `TestStartPipedCancellationAfterReadinessReapsChild`, `TestStartPipedPreservesNonzeroExit` |
|
| Piped child-process exchanges and cancellation cleanup | `internal/process` | `TestStartPipedExchangesBeforeInputEOFAndPreservesSettings`, `TestStartPipedCancellationAfterReadinessReapsChild`, `TestStartPipedPreservesNonzeroExit` |
|
||||||
| Authoritative endpoint and exact annotated tag publication | `releasekit.Publish`, `internal/publish`, `internal/git` | `TestPublishPushesOnlyTheCapturedAnnotatedTag`, `TestPublishRejectsUnexpectedPushURLBeforeValidation`, `TestPublishComparesExpectedURLWithResolvedInsteadOfDestination`, `TestPublishExpectedPushURLUsesPushURLWhenFetchAndPushDiffer`, `TestCheckIgnoresExpectedPushURL`, `TestResolveUsesPushURLAndQueriesWithoutMutation` |
|
| Authoritative endpoint and exact annotated tag publication | `releasekit.Publish`, `internal/publish`, `internal/git` | `TestPublishPushesOnlyTheCapturedAnnotatedTag`, `TestPublishRejectsUnexpectedPushURLBeforeValidation`, `TestPublishComparesExpectedURLWithResolvedInsteadOfDestination`, `TestPublishExpectedPushURLUsesPushURLWhenFetchAndPushDiffer`, `TestCheckIgnoresExpectedPushURL`, `TestResolveUsesPushURLAndQueriesWithoutMutation` |
|
||||||
| Publication partial and uncertain recovery | `internal/publish`, `internal/git` | `TestPublishRejectedPushKeepsLocalTagAndReportsNotPublished`, `TestPublishLostAcknowledgmentConfirmsExactRemoteObject`, `TestPublishLostAcknowledgmentWithChangedEndpointRemainsUnknown`, `TestPublishUnavailableReconciliationRemainsUnknownAndPreservesPushError` |
|
| Publication partial and uncertain recovery | `internal/publish`, `internal/git` | `TestPublishRejectedPushKeepsLocalTagAndReportsNotPublished`, `TestPublishLostAcknowledgmentConfirmsExactRemoteObject`, `TestPublishLostAcknowledgmentWithChangedEndpointRemainsUnknown`, `TestPublishUnavailableReconciliationRemainsUnknownAndPreservesPushError` |
|
||||||
| Cross-builds, version smoke checks, empty target sets, and partial outputs | `releasekit.Build`, `internal/build`, `buildinfo` | `TestBuildCreatesSelectedTargetsAndInjectsVersion`, `TestBuildWithNoTargetsDoesNotInvokeCompiler`, `TestBuildReturnsPartialArtifactsAfterTargetFailure` |
|
| Cross-builds, version smoke checks, module paths, empty target sets, and partial outputs | `releasekit.Build`, `internal/build`, `internal/artifact`, `internal/tempdir`, `buildinfo` | `TestBuildCreatesSelectedTargetsAndInjectsVersion`, `TestBuildUsesNestedWorkingTreeModuleAndModuleRelativeOutput`, `TestBuildResolvesDirtyWorkingTreeDeclaration`, `TestBuildWithNoTargetsDoesNotInvokeCompiler`, `TestBuildReturnsPartialArtifactsAfterTargetFailure`, `TestNormalizeSettingsKeepsBuildInputsAndNormalizesTargets`, `TestOutsideFallsBackWhenTemporaryDirectoryIsInsideWorktree` |
|
||||||
| Page-only and managed or prebuilt binary delivery | `releasekit.Deliver`, `internal/delivery`, `internal/gitea`, CLI | `TestDeliverPageOnlySkipsBuildAndHostedAssets`, `TestDeliverManagedBuildUploadsArtifactsAndCleansTemporaryOutput`, `TestDeliverBinaryConsumerWithBuildinfoAndSelectedTargets`, `TestDeliverPrebuiltArtifactsRemainCallerOwned`, `TestDeliverRejectsSameSizePrebuiltTamperingBeforeHostedWrites`, `TestDeliverManagedBuildFailureCleansTemporaryOutputBeforeHostedWrites`, `TestDeliverManagedBuildCancellationCleansTemporaryOutput`, `TestDeliverBuildsTaggedArtifactsAndReusesAssets`, `TestDeliverRejectsTamperedAssetsAndStaleReceiptBeforeRemoteWrites` |
|
| Page-only and managed or prebuilt binary delivery | `releasekit.Deliver`, `internal/delivery`, `internal/gitea`, CLI | `TestDeliverPageOnlySkipsBuildAndHostedAssets`, `TestDeliverManagedBuildUploadsArtifactsAndCleansTemporaryOutput`, `TestDeliverBinaryConsumerWithBuildinfoAndSelectedTargets`, `TestDeliverPrebuiltArtifactsRemainCallerOwned`, `TestDeliverRejectsSameSizePrebuiltTamperingBeforeHostedWrites`, `TestDeliverManagedBuildFailureCleansTemporaryOutputBeforeHostedWrites`, `TestDeliverManagedBuildCancellationCleansTemporaryOutput`, `TestDeliverBuildsTaggedArtifactsAndReusesAssets`, `TestDeliverRejectsTamperedAssetsAndStaleReceiptBeforeRemoteWrites` |
|
||||||
| Tagged checkout and build dependency preparation | `releasekit.PrepareTaggedCheckout`, `releasekit.PrepareBuildDependencies`, `internal/preparation`, `internal/git`, `internal/gotool` | `TestPrepareTaggedCheckoutFetchesExpectedCommitAndDetaches`, `TestPrepareTaggedCheckoutRejectsWrongCommitBeforeCheckout`, `TestPrepareTaggedCheckoutReportsLocalCheckoutConflict`, `TestPrepareTaggedCheckoutReportsConflictingLocalTag`, `TestPrepareBuildDependenciesPreservesPreparationEnvironment`, `TestPrepareBuildDependenciesPropagatesCommandFailure`, `TestPrepareBuildDependenciesPropagatesCancellation` |
|
| Tagged checkout and build dependency preparation | `releasekit.PrepareTaggedCheckout`, `releasekit.PrepareBuildDependencies`, `internal/preparation`, `internal/git`, `internal/gotool` | `TestPrepareTaggedCheckoutFetchesExpectedCommitAndDetaches`, `TestPrepareTaggedCheckoutRejectsWrongCommitBeforeCheckout`, `TestPrepareTaggedCheckoutReportsLocalCheckoutConflict`, `TestPrepareTaggedCheckoutReportsConflictingLocalTag`, `TestPrepareBuildDependenciesPreservesPreparationEnvironment`, `TestPrepareBuildDependenciesPropagatesCommandFailure`, `TestPrepareBuildDependenciesPropagatesCancellation` |
|
||||||
| Page, manifest, asset comparison, and additive retry outcomes | `internal/gitea` | `TestEnsureReleaseCreatesMissingPageFromCommittedNote`, `TestInspectAssetsDetectsSameNameAndSizeWithDifferentBytes`, `TestUploadMissingPartialFailureThenRerunOnlyMissingAssets` |
|
| Page, manifest, asset comparison, and additive retry outcomes | `internal/gitea` | `TestEnsureReleaseCreatesMissingPageFromCommittedNote`, `TestInspectAssetsDetectsSameNameAndSizeWithDifferentBytes`, `TestUploadMissingPartialFailureThenRerunOnlyMissingAssets` |
|
||||||
|
|||||||
@@ -418,6 +418,15 @@ identity for composites, empty targets, and partial failures. Confirm temporary
|
|||||||
outside the repository even with a nested selection and source-local `TMPDIR`.
|
outside the repository even with a nested selection and source-local `TMPDIR`.
|
||||||
Run build and runtime-boundary tests.
|
Run build and runtime-boundary tests.
|
||||||
|
|
||||||
|
**Completed:** Standalone builds now resolve working-tree module identity once;
|
||||||
|
candidate and managed delivery builds consume their committed identity and Go
|
||||||
|
tool owner through the resolved build path. Normalized build settings are shared
|
||||||
|
by execution and delivery evidence. Builds run from `ModuleDir`, use module-based
|
||||||
|
output paths, and keep temporary native smoke output outside the repository.
|
||||||
|
Verified nested native version output, a second cross target, dirty declarations,
|
||||||
|
new uncommitted nested modules, empty targets, temporary-output fallback and
|
||||||
|
cleanup, full `go test ./...`, `go run ./internal/apicheck`, and `git diff --check`.
|
||||||
|
|
||||||
## Stage 8: Deliver module-local notes and assets to full-tag pages
|
## Stage 8: Deliver module-local notes and assets to full-tag pages
|
||||||
|
|
||||||
**Outcome:** Page-only, managed-build, and API prebuilt delivery share committed
|
**Outcome:** Page-only, managed-build, and API prebuilt delivery share committed
|
||||||
|
|||||||
@@ -13,11 +13,28 @@ type Target struct {
|
|||||||
Arch string
|
Arch string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Settings contains normalized inputs shared by build execution and evidence.
|
||||||
|
type Settings struct {
|
||||||
|
MainPackage string
|
||||||
|
BinaryName string
|
||||||
|
Targets []Target
|
||||||
|
}
|
||||||
|
|
||||||
type Declaration struct {
|
type Declaration struct {
|
||||||
Target Target
|
Target Target
|
||||||
Filename string
|
Filename string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NormalizeSettings copies build inputs and removes duplicate targets while
|
||||||
|
// preserving their first occurrence.
|
||||||
|
func NormalizeSettings(mainPackage, binaryName string, targets []Target) Settings {
|
||||||
|
return Settings{
|
||||||
|
MainPackage: mainPackage,
|
||||||
|
BinaryName: binaryName,
|
||||||
|
Targets: NormalizeTargets(targets),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// NormalizeTargets removes duplicate targets while preserving their first
|
// NormalizeTargets removes duplicate targets while preserving their first
|
||||||
// occurrence.
|
// occurrence.
|
||||||
func NormalizeTargets(targets []Target) []Target {
|
func NormalizeTargets(targets []Target) []Target {
|
||||||
|
|||||||
@@ -17,6 +17,22 @@ func TestNormalizeTargetsPreservesOrderAndFirstOccurrence(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNormalizeSettingsKeepsBuildInputsAndNormalizesTargets(t *testing.T) {
|
||||||
|
settings := NormalizeSettings("./cmd/app", "app", []Target{
|
||||||
|
{OS: "linux", Arch: "amd64"},
|
||||||
|
{OS: "windows", Arch: "arm64"},
|
||||||
|
{OS: "linux", Arch: "amd64"},
|
||||||
|
})
|
||||||
|
want := Settings{
|
||||||
|
MainPackage: "./cmd/app",
|
||||||
|
BinaryName: "app",
|
||||||
|
Targets: []Target{{OS: "linux", Arch: "amd64"}, {OS: "windows", Arch: "arm64"}},
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(settings, want) {
|
||||||
|
t.Fatalf("NormalizeSettings() = %#v, want %#v", settings, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestExpectedBuildUsesSharedNamesInTargetOrder(t *testing.T) {
|
func TestExpectedBuildUsesSharedNamesInTargetOrder(t *testing.T) {
|
||||||
targets := []Target{{OS: "windows", Arch: "arm64"}, {OS: "linux", Arch: "amd64"}}
|
targets := []Target{{OS: "windows", Arch: "arm64"}, {OS: "linux", Arch: "amd64"}}
|
||||||
want := []Declaration{
|
want := []Declaration{
|
||||||
|
|||||||
@@ -16,25 +16,20 @@ import (
|
|||||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/artifact"
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/artifact"
|
||||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/candidate"
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/candidate"
|
||||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gotool"
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gotool"
|
||||||
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/releaseidentity"
|
||||||
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/tempdir"
|
||||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/versioncontract"
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/versioncontract"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Target = artifact.Target
|
type Target = artifact.Target
|
||||||
|
type Spec = artifact.Settings
|
||||||
type Spec struct {
|
|
||||||
MainPackage string
|
|
||||||
BinaryName string
|
|
||||||
Targets []Target
|
|
||||||
}
|
|
||||||
|
|
||||||
type Request struct {
|
type Request struct {
|
||||||
RepositoryDir string
|
Identity releaseidentity.Identity
|
||||||
ModuleDir string
|
Spec Spec
|
||||||
Version string
|
OutputDir string
|
||||||
Spec Spec
|
Output io.Writer
|
||||||
OutputDir string
|
ToolOwner *gotool.Owner
|
||||||
Output io.Writer
|
|
||||||
ToolOwner *gotool.Owner
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Artifact struct {
|
type Artifact struct {
|
||||||
@@ -51,6 +46,7 @@ type Result struct {
|
|||||||
Tag string
|
Tag string
|
||||||
CandidateCommit string
|
CandidateCommit string
|
||||||
SourceVerified bool
|
SourceVerified bool
|
||||||
|
Settings artifact.Settings
|
||||||
Version string
|
Version string
|
||||||
MainPackage string
|
MainPackage string
|
||||||
BinaryName string
|
BinaryName string
|
||||||
@@ -60,25 +56,32 @@ type Result struct {
|
|||||||
Compiler string
|
Compiler string
|
||||||
}
|
}
|
||||||
|
|
||||||
func Build(ctx context.Context, request Request) (Result, error) {
|
func BuildResolved(ctx context.Context, request Request) (Result, error) {
|
||||||
|
identity := request.Identity
|
||||||
|
settings := artifact.NormalizeSettings(request.Spec.MainPackage, request.Spec.BinaryName, request.Spec.Targets)
|
||||||
result := Result{
|
result := Result{
|
||||||
Version: request.Version,
|
RepositoryDir: identity.RepositoryDir,
|
||||||
MainPackage: request.Spec.MainPackage,
|
ModuleDir: identity.ModuleDir,
|
||||||
BinaryName: request.Spec.BinaryName,
|
ModulePath: identity.ModulePath,
|
||||||
Targets: artifact.NormalizeTargets(request.Spec.Targets),
|
Tag: identity.Tag,
|
||||||
|
Settings: settings,
|
||||||
|
Version: identity.Version,
|
||||||
|
MainPackage: settings.MainPackage,
|
||||||
|
BinaryName: settings.BinaryName,
|
||||||
|
Targets: settings.Targets,
|
||||||
}
|
}
|
||||||
if !versioncontract.ValidStableVersion(request.Version) {
|
if !versioncontract.ValidStableVersion(identity.Version) {
|
||||||
return result, fmt.Errorf("%w: %q", candidate.ErrInvalidVersion, request.Version)
|
return result, fmt.Errorf("%w: %q", candidate.ErrInvalidVersion, identity.Version)
|
||||||
}
|
}
|
||||||
if request.RepositoryDir == "" {
|
if identity.RepositoryDir == "" || identity.ModuleDir == "" || identity.ModulePath == "" {
|
||||||
return result, errors.New("build requires a repository directory")
|
return result, errors.New("build requires a resolved module identity")
|
||||||
}
|
}
|
||||||
if len(result.Targets) != 0 {
|
if len(result.Targets) != 0 {
|
||||||
if request.Spec.MainPackage == "" {
|
if settings.MainPackage == "" {
|
||||||
return result, errors.New("build requires a main package when targets are selected")
|
return result, errors.New("build requires a main package when targets are selected")
|
||||||
}
|
}
|
||||||
if !artifact.ValidComponent(request.Spec.BinaryName) {
|
if !artifact.ValidComponent(settings.BinaryName) {
|
||||||
return result, fmt.Errorf("unsafe binary name %q", request.Spec.BinaryName)
|
return result, fmt.Errorf("unsafe binary name %q", settings.BinaryName)
|
||||||
}
|
}
|
||||||
for _, target := range result.Targets {
|
for _, target := range result.Targets {
|
||||||
if !artifact.ValidComponent(target.OS) || !artifact.ValidComponent(target.Arch) {
|
if !artifact.ValidComponent(target.OS) || !artifact.ValidComponent(target.Arch) {
|
||||||
@@ -89,11 +92,16 @@ func Build(ctx context.Context, request Request) (Result, error) {
|
|||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return result, fmt.Errorf("build canceled: %w", err)
|
return result, fmt.Errorf("build canceled: %w", err)
|
||||||
}
|
}
|
||||||
repositoryDir, err := filepath.Abs(request.RepositoryDir)
|
repositoryDir, err := filepath.Abs(identity.RepositoryDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return result, fmt.Errorf("resolve repository directory: %w", err)
|
return result, fmt.Errorf("resolve repository directory: %w", err)
|
||||||
}
|
}
|
||||||
result.RepositoryDir = repositoryDir
|
result.RepositoryDir = repositoryDir
|
||||||
|
moduleDir, err := filepath.Abs(identity.ModuleDir)
|
||||||
|
if err != nil {
|
||||||
|
return result, fmt.Errorf("resolve module directory: %w", err)
|
||||||
|
}
|
||||||
|
result.ModuleDir = moduleDir
|
||||||
commit, err := gitCommit(ctx, repositoryDir)
|
commit, err := gitCommit(ctx, repositoryDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return result, err
|
return result, err
|
||||||
@@ -105,12 +113,12 @@ func Build(ctx context.Context, request Request) (Result, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(result.Targets) == 0 {
|
if len(result.Targets) == 0 {
|
||||||
result.OutputDir = resolvedOutputDir(repositoryDir, request.OutputDir)
|
result.OutputDir = resolvedOutputDir(moduleDir, request.OutputDir)
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
result.OutputDir = resolvedOutputDir(repositoryDir, request.OutputDir)
|
result.OutputDir = resolvedOutputDir(moduleDir, request.OutputDir)
|
||||||
declarations := artifact.ExpectedBuild(request.Spec.BinaryName, request.Version, result.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 {
|
||||||
filenames[i] = declaration.Filename
|
filenames[i] = declaration.Filename
|
||||||
@@ -121,13 +129,13 @@ func Build(ctx context.Context, request Request) (Result, error) {
|
|||||||
|
|
||||||
owner := request.ToolOwner
|
owner := request.ToolOwner
|
||||||
if owner == nil {
|
if owner == nil {
|
||||||
owner = gotool.NewOwner(repositoryDir)
|
owner = gotool.NewOwner(moduleDir)
|
||||||
}
|
}
|
||||||
goTool, err := owner.Acquire(ctx)
|
goTool, err := owner.Acquire(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return result, fmt.Errorf("prepare Go toolchain: %w", err)
|
return result, fmt.Errorf("prepare Go toolchain: %w", err)
|
||||||
}
|
}
|
||||||
compiler, err := goTool.Output(ctx, repositoryDir, gotool.Native, gotool.Target{}, nil, "version")
|
compiler, err := goTool.Output(ctx, moduleDir, gotool.Native, gotool.Target{}, nil, "version")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return result, fmt.Errorf("identify Go compiler: %w", err)
|
return result, fmt.Errorf("identify Go compiler: %w", err)
|
||||||
}
|
}
|
||||||
@@ -144,7 +152,7 @@ func Build(ctx context.Context, request Request) (Result, error) {
|
|||||||
if request.Output != nil {
|
if request.Output != nil {
|
||||||
fmt.Fprintf(request.Output, "building %s/%s as %s\n", target.OS, target.Arch, filename)
|
fmt.Fprintf(request.Output, "building %s/%s as %s\n", target.OS, target.Arch, filename)
|
||||||
}
|
}
|
||||||
if err := buildTarget(ctx, goTool, repositoryDir, request.Spec.MainPackage, path, request.Version, target); err != nil {
|
if err := buildTarget(ctx, goTool, moduleDir, settings.MainPackage, path, identity.Version, target); err != nil {
|
||||||
return result, fmt.Errorf("build target %s/%s: %w", target.OS, target.Arch, err)
|
return result, fmt.Errorf("build target %s/%s: %w", target.OS, target.Arch, err)
|
||||||
}
|
}
|
||||||
artifact := Artifact{Path: path, Filename: filename, Target: target}
|
artifact := Artifact{Path: path, Filename: filename, Target: target}
|
||||||
@@ -162,26 +170,26 @@ func Build(ctx context.Context, request Request) (Result, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if smokePath == "" {
|
if smokePath == "" {
|
||||||
temporaryDir, err := os.MkdirTemp("", "releasekit-native-smoke-")
|
temporaryDir, err := tempdir.Outside(repositoryDir, "releasekit-native-smoke-")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return result, fmt.Errorf("create native smoke directory: %w", err)
|
return result, fmt.Errorf("create native smoke directory: %w", err)
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(temporaryDir)
|
defer os.RemoveAll(temporaryDir)
|
||||||
smokePath = filepath.Join(temporaryDir, request.Spec.BinaryName)
|
smokePath = filepath.Join(temporaryDir, settings.BinaryName)
|
||||||
if host.OS == "windows" {
|
if host.OS == "windows" {
|
||||||
smokePath += ".exe"
|
smokePath += ".exe"
|
||||||
}
|
}
|
||||||
if request.Output != nil {
|
if request.Output != nil {
|
||||||
fmt.Fprintf(request.Output, "building temporary version check for %s/%s\n", host.OS, host.Arch)
|
fmt.Fprintf(request.Output, "building temporary version check for %s/%s\n", host.OS, host.Arch)
|
||||||
}
|
}
|
||||||
if err := buildTarget(ctx, goTool, repositoryDir, request.Spec.MainPackage, smokePath, request.Version, host); err != nil {
|
if err := buildTarget(ctx, goTool, moduleDir, settings.MainPackage, smokePath, identity.Version, host); err != nil {
|
||||||
return result, fmt.Errorf("build native version check: %w", err)
|
return result, fmt.Errorf("build native version check: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if request.Output != nil {
|
if request.Output != nil {
|
||||||
fmt.Fprintf(request.Output, "checking %s version output\n", request.Spec.BinaryName)
|
fmt.Fprintf(request.Output, "checking %s version output\n", settings.BinaryName)
|
||||||
}
|
}
|
||||||
if err := checkVersion(ctx, smokePath, request.Spec.BinaryName, request.Version); err != nil {
|
if err := checkVersion(ctx, smokePath, settings.BinaryName, identity.Version); err != nil {
|
||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
commitAfter, err := gitCommit(ctx, repositoryDir)
|
commitAfter, err := gitCommit(ctx, repositoryDir)
|
||||||
@@ -196,11 +204,11 @@ func Build(ctx context.Context, request Request) (Result, error) {
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolvedOutputDir(repositoryDir, outputDir string) string {
|
func resolvedOutputDir(moduleDir, outputDir string) string {
|
||||||
if outputDir == "" {
|
if outputDir == "" {
|
||||||
outputDir = filepath.Join(repositoryDir, "dist")
|
outputDir = filepath.Join(moduleDir, "dist")
|
||||||
} else if !filepath.IsAbs(outputDir) {
|
} else if !filepath.IsAbs(outputDir) {
|
||||||
outputDir = filepath.Join(repositoryDir, outputDir)
|
outputDir = filepath.Join(moduleDir, outputDir)
|
||||||
}
|
}
|
||||||
resolved, err := filepath.Abs(outputDir)
|
resolved, err := filepath.Abs(outputDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -221,9 +229,9 @@ func gitCommit(ctx context.Context, repositoryDir string) (string, error) {
|
|||||||
return strings.TrimSpace(string(output)), nil
|
return strings.TrimSpace(string(output)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildTarget(ctx context.Context, goTool *gotool.Tool, repositoryDir, mainPackage, outputPath, version string, target Target) error {
|
func buildTarget(ctx context.Context, goTool *gotool.Tool, moduleDir, mainPackage, outputPath, version string, target Target) error {
|
||||||
ldflags := "-s -w -X " + versioncontract.InjectedVersionLinkerSymbol + "=" + version
|
ldflags := "-s -w -X " + versioncontract.InjectedVersionLinkerSymbol + "=" + version
|
||||||
err := goTool.Run(ctx, repositoryDir, gotool.Static, gotool.Target{OS: target.OS, Arch: target.Arch}, nil,
|
err := goTool.Run(ctx, moduleDir, gotool.Static, gotool.Target{OS: target.OS, Arch: target.Arch}, nil,
|
||||||
"build", "-o", outputPath, "-mod=readonly", "-trimpath", "-buildvcs=false", "-ldflags", ldflags, mainPackage)
|
"build", "-o", outputPath, "-mod=readonly", "-trimpath", "-buildvcs=false", "-ldflags", ldflags, mainPackage)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/candidate"
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/candidate"
|
||||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gotool"
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gotool"
|
||||||
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/releaseidentity"
|
||||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/tempdir"
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/tempdir"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,18 +20,32 @@ func CandidateCallback(spec *Spec, output io.Writer) func(context.Context, candi
|
|||||||
selected := *spec
|
selected := *spec
|
||||||
selected.Targets = append([]Target(nil), spec.Targets...)
|
selected.Targets = append([]Target(nil), spec.Targets...)
|
||||||
return func(ctx context.Context, captured candidate.Candidate, owner *gotool.Owner) error {
|
return func(ctx context.Context, captured candidate.Candidate, owner *gotool.Owner) error {
|
||||||
|
moduleSubdir, err := filepath.Rel(captured.RepositoryDir, captured.ModuleDir)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("resolve captured module path: %w", err)
|
||||||
|
}
|
||||||
|
identity := releaseidentity.Identity{
|
||||||
|
Selection: releaseidentity.Selection{
|
||||||
|
RepositoryDir: captured.RepositoryDir,
|
||||||
|
ModuleDir: captured.ModuleDir,
|
||||||
|
ModuleSubdir: filepath.ToSlash(moduleSubdir),
|
||||||
|
},
|
||||||
|
ModulePath: captured.ModulePath,
|
||||||
|
Version: captured.Version,
|
||||||
|
Tag: captured.Tag,
|
||||||
|
NotePath: filepath.ToSlash(filepath.Join(moduleSubdir, "docs", "releases", captured.Version+".md")),
|
||||||
|
}
|
||||||
temporaryDir, err := tempdir.Outside(captured.RepositoryDir, "releasekit-candidate-build-")
|
temporaryDir, err := tempdir.Outside(captured.RepositoryDir, "releasekit-candidate-build-")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("create temporary candidate build output: %w", err)
|
return fmt.Errorf("create temporary candidate build output: %w", err)
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(temporaryDir)
|
defer os.RemoveAll(temporaryDir)
|
||||||
_, err = Build(ctx, Request{
|
_, err = BuildResolved(ctx, Request{
|
||||||
RepositoryDir: captured.RepositoryDir,
|
Identity: identity,
|
||||||
Version: captured.Version,
|
Spec: selected,
|
||||||
Spec: selected,
|
OutputDir: temporaryDir,
|
||||||
OutputDir: temporaryDir,
|
Output: output,
|
||||||
Output: output,
|
ToolOwner: owner,
|
||||||
ToolOwner: owner,
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("build candidate: %w", err)
|
return fmt.Errorf("build candidate: %w", err)
|
||||||
|
|||||||
@@ -11,16 +11,17 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func declaredBuildAssets(result internalbuild.Result) ([]gitea.ExpectedAsset, error) {
|
func declaredBuildAssets(result internalbuild.Result) ([]gitea.ExpectedAsset, error) {
|
||||||
if len(result.Targets) == 0 {
|
settings := result.Settings
|
||||||
|
if len(settings.Targets) == 0 {
|
||||||
if len(result.Artifacts) != 0 {
|
if len(result.Artifacts) != 0 {
|
||||||
return nil, fmt.Errorf("source-only build result contains artifacts")
|
return nil, fmt.Errorf("source-only build result contains artifacts")
|
||||||
}
|
}
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
if len(result.Artifacts) != len(result.Targets) {
|
if len(result.Artifacts) != len(settings.Targets) {
|
||||||
return nil, fmt.Errorf("build result must contain one artifact for each selected target")
|
return nil, fmt.Errorf("build result must contain one artifact for each selected target")
|
||||||
}
|
}
|
||||||
declarations := artifact.ExpectedBuild(result.BinaryName, result.Version, result.Targets)
|
declarations := artifact.ExpectedBuild(settings.BinaryName, result.Version, settings.Targets)
|
||||||
wantByTarget := make(map[artifact.Target]string, len(declarations))
|
wantByTarget := make(map[artifact.Target]string, len(declarations))
|
||||||
for _, declaration := range declarations {
|
for _, declaration := range declarations {
|
||||||
target := declaration.Target
|
target := declaration.Target
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/artifact"
|
||||||
internalbuild "gitea.maximumdirect.net/eric/ReleaseKit/internal/build"
|
internalbuild "gitea.maximumdirect.net/eric/ReleaseKit/internal/build"
|
||||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gitea"
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gitea"
|
||||||
)
|
)
|
||||||
@@ -25,8 +26,10 @@ func TestDeclaredBuildAssetsGenerateManifestFromBuildResult(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
result, err := declaredBuildAssets(internalbuild.Result{
|
result, err := declaredBuildAssets(internalbuild.Result{
|
||||||
Version: "v1.2.3", BinaryName: "app",
|
Version: "v1.2.3",
|
||||||
Targets: []internalbuild.Target{{OS: "linux", Arch: "amd64"}, {OS: "darwin", Arch: "arm64"}},
|
Settings: artifact.NormalizeSettings("./cmd/app", "app", []artifact.Target{
|
||||||
|
{OS: "linux", Arch: "amd64"}, {OS: "darwin", Arch: "arm64"},
|
||||||
|
}),
|
||||||
Artifacts: []internalbuild.Artifact{
|
Artifacts: []internalbuild.Artifact{
|
||||||
{Path: firstPath, Filename: filepath.Base(firstPath), Target: internalbuild.Target{OS: "linux", Arch: "amd64"}, SHA256: assetDigest(first)},
|
{Path: firstPath, Filename: filepath.Base(firstPath), Target: internalbuild.Target{OS: "linux", Arch: "amd64"}, SHA256: assetDigest(first)},
|
||||||
{Path: secondPath, Filename: filepath.Base(secondPath), Target: internalbuild.Target{OS: "darwin", Arch: "arm64"}, SHA256: assetDigest(second)},
|
{Path: secondPath, Filename: filepath.Base(secondPath), Target: internalbuild.Target{OS: "darwin", Arch: "arm64"}, SHA256: assetDigest(second)},
|
||||||
@@ -54,7 +57,7 @@ func TestDeclaredBuildAssetsRejectsArtifactDeclarationMismatch(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
_, err := declaredBuildAssets(internalbuild.Result{
|
_, err := declaredBuildAssets(internalbuild.Result{
|
||||||
Version: "v1.2.3", BinaryName: "app", Targets: []internalbuild.Target{{OS: "linux", Arch: "amd64"}},
|
Version: "v1.2.3", Settings: artifact.NormalizeSettings("./cmd/app", "app", []artifact.Target{{OS: "linux", Arch: "amd64"}}),
|
||||||
Artifacts: []internalbuild.Artifact{{
|
Artifacts: []internalbuild.Artifact{{
|
||||||
Path: path, Filename: filepath.Base(path), Target: internalbuild.Target{OS: "linux", Arch: "amd64"}, SHA256: assetDigest(contents),
|
Path: path, Filename: filepath.Base(path), Target: internalbuild.Target{OS: "linux", Arch: "amd64"}, SHA256: assetDigest(contents),
|
||||||
}},
|
}},
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/candidate"
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/candidate"
|
||||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/git"
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/git"
|
||||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gitea"
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gitea"
|
||||||
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/releaseidentity"
|
||||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/tempdir"
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/tempdir"
|
||||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/versioncontract"
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/versioncontract"
|
||||||
)
|
)
|
||||||
@@ -90,17 +91,24 @@ func Deliver(ctx context.Context, request Request) (Result, error) {
|
|||||||
|
|
||||||
build := request.Prebuilt
|
build := request.Prebuilt
|
||||||
if request.Build != nil && len(request.Build.Targets) != 0 {
|
if request.Build != nil && len(request.Build.Targets) != 0 {
|
||||||
|
selection, err := releaseidentity.ResolveSelection(repositoryDir, request.ModuleDir)
|
||||||
|
if err != nil {
|
||||||
|
return result, fmt.Errorf("resolve managed build module: %w", err)
|
||||||
|
}
|
||||||
|
identity, err := releaseidentity.FromCommittedSelection(ctx, selection, request.Version, source.TagCommit)
|
||||||
|
if err != nil {
|
||||||
|
return result, fmt.Errorf("resolve managed build identity: %w", err)
|
||||||
|
}
|
||||||
temporaryDir, err := tempdir.Outside(repositoryDir, "releasekit-delivery-build-*")
|
temporaryDir, err := tempdir.Outside(repositoryDir, "releasekit-delivery-build-*")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return result, fmt.Errorf("create temporary delivery build directory: %w", err)
|
return result, fmt.Errorf("create temporary delivery build directory: %w", err)
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(temporaryDir)
|
defer os.RemoveAll(temporaryDir)
|
||||||
built, err := internalbuild.Build(ctx, internalbuild.Request{
|
built, err := internalbuild.BuildResolved(ctx, internalbuild.Request{
|
||||||
RepositoryDir: repositoryDir,
|
Identity: identity,
|
||||||
Version: request.Version,
|
Spec: *request.Build,
|
||||||
Spec: *request.Build,
|
OutputDir: filepath.Join(temporaryDir, "assets"),
|
||||||
OutputDir: filepath.Join(temporaryDir, "assets"),
|
Output: request.Output,
|
||||||
Output: request.Output,
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return result, fmt.Errorf("build delivery assets: %w", err)
|
return result, fmt.Errorf("build delivery assets: %w", err)
|
||||||
@@ -122,7 +130,7 @@ func Deliver(ctx context.Context, request Request) (Result, error) {
|
|||||||
return result, fmt.Errorf("verify build assets: %w", err)
|
return result, fmt.Errorf("verify build assets: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if build != nil && len(build.Targets) != 0 {
|
if build != nil && len(build.Settings.Targets) != 0 {
|
||||||
current, err := git.InspectTaggedSource(ctx, repositoryDir, request.Version)
|
current, err := git.InspectTaggedSource(ctx, repositoryDir, request.Version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return result, fmt.Errorf("recheck tagged source after build: %w", err)
|
return result, fmt.Errorf("recheck tagged source after build: %w", err)
|
||||||
@@ -179,7 +187,7 @@ func validateBuild(repositoryDir, commit, version string, build *internalbuild.R
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if len(build.Targets) == 0 {
|
if len(build.Settings.Targets) == 0 {
|
||||||
if len(assets) != 0 {
|
if len(assets) != 0 {
|
||||||
return errors.New("page-only delivery cannot include binary assets")
|
return errors.New("page-only delivery cannot include binary assets")
|
||||||
}
|
}
|
||||||
@@ -195,7 +203,7 @@ func validateBuild(repositoryDir, commit, version string, build *internalbuild.R
|
|||||||
if buildRepository != repositoryDir || build.CandidateCommit != commit || build.Version != version {
|
if buildRepository != repositoryDir || build.CandidateCommit != commit || build.Version != version {
|
||||||
return errors.New("build result does not identify the selected tagged source")
|
return errors.New("build result does not identify the selected tagged source")
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(build.MainPackage) == "" || strings.TrimSpace(build.BinaryName) == "" || len(assets) != len(build.Targets)+1 || assets[len(assets)-1].Filename != gitea.ManifestFilename {
|
if strings.TrimSpace(build.Settings.MainPackage) == "" || strings.TrimSpace(build.Settings.BinaryName) == "" || len(assets) != len(build.Settings.Targets)+1 || assets[len(assets)-1].Filename != gitea.ManifestFilename {
|
||||||
return errors.New("build result does not contain the selected artifacts and complete checksum manifest")
|
return errors.New("build result does not contain the selected artifacts and complete checksum manifest")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/artifact"
|
||||||
internalbuild "gitea.maximumdirect.net/eric/ReleaseKit/internal/build"
|
internalbuild "gitea.maximumdirect.net/eric/ReleaseKit/internal/build"
|
||||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gitea"
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gitea"
|
||||||
)
|
)
|
||||||
@@ -60,14 +61,13 @@ func performanceBuildResult(b *testing.B) (internalbuild.Result, int64) {
|
|||||||
}
|
}
|
||||||
digest := sha256.Sum256(data)
|
digest := sha256.Sum256(data)
|
||||||
result := internalbuild.Result{
|
result := internalbuild.Result{
|
||||||
Version: "v1.2.3",
|
Version: "v1.2.3",
|
||||||
BinaryName: "releasekit",
|
Settings: artifact.NormalizeSettings("./cmd/releasekit", "releasekit", []artifact.Target{
|
||||||
Targets: []internalbuild.Target{
|
|
||||||
{OS: "linux", Arch: "amd64"},
|
{OS: "linux", Arch: "amd64"},
|
||||||
{OS: "windows", Arch: "amd64"},
|
{OS: "windows", Arch: "amd64"},
|
||||||
},
|
}),
|
||||||
}
|
}
|
||||||
for _, target := range result.Targets {
|
for _, target := range result.Settings.Targets {
|
||||||
name := fmt.Sprintf("releasekit-v1.2.3-%s-%s", target.OS, target.Arch)
|
name := fmt.Sprintf("releasekit-v1.2.3-%s-%s", target.OS, target.Arch)
|
||||||
if target.OS == "windows" {
|
if target.OS == "windows" {
|
||||||
name += ".exe"
|
name += ".exe"
|
||||||
|
|||||||
@@ -8,32 +8,77 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Outside uses the system temporary directory and leaves cleanup to its caller.
|
// Outside uses system temporary storage and falls back to the repository's
|
||||||
|
// parent when TMPDIR resolves inside the worktree. Cleanup belongs to the caller.
|
||||||
func Outside(repositoryDir, pattern string) (string, error) {
|
func Outside(repositoryDir, pattern string) (string, error) {
|
||||||
|
repository, err := filepath.Abs(repositoryDir)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
repository, err = filepath.EvalSymlinks(repository)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
directory, err := os.MkdirTemp("", pattern)
|
directory, err := os.MkdirTemp("", pattern)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
absolute, err := filepath.Abs(directory)
|
created := directory
|
||||||
|
directory, err = filepath.Abs(created)
|
||||||
|
if err != nil {
|
||||||
|
os.RemoveAll(created)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
directory, err = filepath.EvalSymlinks(directory)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
os.RemoveAll(directory)
|
os.RemoveAll(directory)
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
directory = absolute
|
outside, err := isOutside(repository, directory)
|
||||||
repository, err := filepath.Abs(repositoryDir)
|
if err != nil {
|
||||||
if err == nil && !strings.EqualFold(filepath.VolumeName(repository), filepath.VolumeName(directory)) {
|
os.RemoveAll(directory)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if outside {
|
||||||
return directory, nil
|
return directory, nil
|
||||||
}
|
}
|
||||||
if err == nil {
|
if err := os.RemoveAll(directory); err != nil {
|
||||||
var relative string
|
return "", fmt.Errorf("remove temporary storage inside repository %q: %w", repository, err)
|
||||||
relative, err = filepath.Rel(repository, directory)
|
|
||||||
if err == nil && (relative == "." || relative != ".." && !strings.HasPrefix(relative, ".."+string(filepath.Separator))) {
|
|
||||||
err = fmt.Errorf("temporary storage %q is inside repository %q; select an external temporary directory", directory, repository)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
directory, err = os.MkdirTemp(filepath.Dir(repository), pattern)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("create temporary storage outside repository %q: %w", repository, err)
|
||||||
|
}
|
||||||
|
created = directory
|
||||||
|
directory, err = filepath.Abs(created)
|
||||||
|
if err != nil {
|
||||||
|
os.RemoveAll(created)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
directory, err = filepath.EvalSymlinks(directory)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
os.RemoveAll(directory)
|
os.RemoveAll(directory)
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
outside, err = isOutside(repository, directory)
|
||||||
|
if err != nil {
|
||||||
|
os.RemoveAll(directory)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if !outside {
|
||||||
|
os.RemoveAll(directory)
|
||||||
|
return "", fmt.Errorf("temporary storage %q is inside repository %q", directory, repository)
|
||||||
|
}
|
||||||
return directory, nil
|
return directory, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isOutside(repository, directory string) (bool, error) {
|
||||||
|
if !strings.EqualFold(filepath.VolumeName(repository), filepath.VolumeName(directory)) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
relative, err := filepath.Rel(repository, directory)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return relative == ".." || strings.HasPrefix(relative, ".."+string(filepath.Separator)), nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,16 +29,28 @@ func TestOutsideHonorsRelativeTemporaryDirectory(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOutsideRejectsAndCleansStorageInsideWorktree(t *testing.T) {
|
func TestOutsideFallsBackWhenTemporaryDirectoryIsInsideWorktree(t *testing.T) {
|
||||||
repository := t.TempDir()
|
parent := t.TempDir()
|
||||||
setTemporaryDirectory(t, repository)
|
repository := filepath.Join(parent, "worktree")
|
||||||
if directory, err := Outside(repository, "fixture-"); err == nil {
|
if err := os.Mkdir(repository, 0o700); err != nil {
|
||||||
os.RemoveAll(directory)
|
t.Fatal(err)
|
||||||
t.Fatal("storage created inside worktree")
|
}
|
||||||
|
localTemporary := filepath.Join(repository, "tmp")
|
||||||
|
if err := os.Mkdir(localTemporary, 0o700); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
setTemporaryDirectory(t, localTemporary)
|
||||||
|
directory, err := Outside(repository, "fixture-")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(directory)
|
||||||
|
if filepath.Dir(directory) != parent {
|
||||||
|
t.Fatalf("temporary storage=%q want external parent %q", directory, parent)
|
||||||
}
|
}
|
||||||
remaining, err := os.ReadDir(repository)
|
remaining, err := os.ReadDir(repository)
|
||||||
if err != nil || len(remaining) != 0 {
|
if err != nil || len(remaining) != 1 || remaining[0].Name() != "tmp" {
|
||||||
t.Fatalf("temporary storage left behind: %v %v", remaining, err)
|
t.Fatalf("temporary storage left behind in worktree: %v %v", remaining, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user