diff --git a/cmd/mesh-builder/main.go b/cmd/mesh-builder/main.go index 5ec294a..c4fa5e1 100644 --- a/cmd/mesh-builder/main.go +++ b/cmd/mesh-builder/main.go @@ -24,6 +24,7 @@ import ( "encoding/json" "errors" "fmt" + "net/url" "os" "os/signal" "strings" @@ -202,6 +203,7 @@ func answer(ctx context.Context, channel *amqp.Channel, publisher builder.Publis // not after a clone that then fails at npm ci. built, err = builder.Build(ctx, builder.Command, publisher, request.Repository, request.Path, request.Ref, workspace, request.Held, npmrc, + forgeFrom(), func(step, message string) { fmt.Fprintf(os.Stderr, " [%s] %s\n", step, message) }) @@ -367,6 +369,53 @@ func packagesFrom() (builder.Npmrc, error) { return builder.Npmrc{Scope: scope, Registry: registry, Token: secret}, nil } +// forgeFrom is the git credential this builder may offer a clone, composed from the same binding +// and sealed secret its package-registry half already reads: the forge that answers npm is the +// forge that hosts the repositories, and its provisioner applies one password to one user for +// both. Anything missing means no credential, and every clone stays anonymous — which is all a +// mesh of public repositories ever needs. +// +// The URL names the binding's own address — the machine the mesh says the forge is on — so a +// private repository is registered and built by that address, and a clone of anything else is +// never shown this credential (git's credential store matches the whole origin). +func forgeFrom() builder.GitCredential { + path := strings.TrimSpace(os.Getenv("MESH_PACKAGE_BINDING")) + if path == "" { + return builder.GitCredential{} + } + raw, err := os.ReadFile(path) + if err != nil { + return builder.GitCredential{} + } + var told struct { + At string `json:"at"` + As string `json:"as"` + Serves map[string]any `json:"serves"` + } + if err := json.Unmarshal(raw, &told); err != nil || told.At == "" || told.As == "" { + return builder.GitCredential{} + } + secret := strings.TrimSpace(os.Getenv("MESH_NPM_TOKEN")) + if file := strings.TrimSpace(os.Getenv("MESH_NPM_TOKEN_FILE")); file != "" { + if raw, err := os.ReadFile(file); err == nil { + secret = strings.TrimSpace(string(raw)) + } + } + if secret == "" { + return builder.GitCredential{} + } + scheme := "https" + if s, ok := told.Serves["scheme"]; ok { + scheme = fmt.Sprintf("%v", s) + } + host := told.At + if port, ok := told.Serves["port"]; ok { + host = fmt.Sprintf("%s:%v", told.At, port) + } + made := url.URL{Scheme: scheme, User: url.UserPassword(told.As, secret), Host: host} + return builder.GitCredential{URL: made.String()} +} + func short(commit string) string { if len(commit) > 8 { return commit[:8] diff --git a/cmd/mesh-builder/once.go b/cmd/mesh-builder/once.go index 1ea9da7..97b2ef1 100644 --- a/cmd/mesh-builder/once.go +++ b/cmd/mesh-builder/once.go @@ -89,6 +89,7 @@ func buildOnce(ctx context.Context, args []string) error { return err } built, buildErr := builder.Build(ctx, builder.Command, publisher, repository, *path, *ref, where, bases, npmrc, + forgeFrom(), func(step, message string) { fmt.Fprintf(os.Stderr, " [%s] %s\n", step, message) }) if buildErr != nil { return buildErr diff --git a/internal/builder/builder.go b/internal/builder/builder.go index 222878d..3aa56f3 100644 --- a/internal/builder/builder.go +++ b/internal/builder/builder.go @@ -63,6 +63,19 @@ type Result struct { Built []catalogue.Built } +// GitCredential is the forge credential a clone may present when the server asks for one. +// +// **Offered, never pushed.** It is written as a git credential-store file and named to git with +// `-c credential.helper=store`, so git itself decides when it applies: only on an authentication +// challenge, and only for the URL it was written for — scheme, host and port included. A public +// repository clones exactly as before, and a repository on any other host is never shown it. +type GitCredential struct { + // URL is the credential-store line — scheme://user:password@host[:port] — naming the one + // server this credential belongs to. Empty means the builder holds none and every clone is + // anonymous, as it always was. + URL string +} + // Build clones a repository at a ref, reads its manifest, produces what it declares, publishes // each, and returns the manifest the mesh should hold. // @@ -70,7 +83,8 @@ type Result struct { // archive failed would otherwise leave half of itself in the store under a digest the mesh never // records — reachable, unreferenced, and indistinguishable from something in use. func Build(ctx context.Context, run Runner, publish Publisher, - repository, path, ref, workspace string, held map[string]string, npmrc Npmrc, log Log) (Result, error) { + repository, path, ref, workspace string, held map[string]string, npmrc Npmrc, + forge GitCredential, log Log) (Result, error) { say := logging(log) say("clone", "%s%s at %s", repository, describePath(path), refOrHead(ref)) @@ -80,6 +94,15 @@ func Build(ctx context.Context, run Runner, publish Publisher, if err := os.MkdirAll(workspace, 0o755); err != nil { return Result{}, err } + // The credential is a file git reads, never an argument: a URL carrying a password in argv + // would be readable by anything that can list processes for as long as a clone runs. + credentials := "" + if forge.URL != "" { + credentials = filepath.Join(workspace, "git-credentials") + if err := os.WriteFile(credentials, []byte(forge.URL+"\n"), 0o600); err != nil { + return Result{}, err + } + } tree := filepath.Join(workspace, "source") if err := os.RemoveAll(tree); err != nil { return Result{}, err @@ -87,7 +110,7 @@ func Build(ctx context.Context, run Runner, publish Publisher, // A fresh clone every time rather than a fetch into a tree that is already there. A build // that reuses a working tree can succeed because of something a previous build left behind, // and that is a build nobody can reproduce. - if _, err := run(ctx, workspace, "git", "clone", "--quiet", repository, tree); err != nil { + if _, err := run(ctx, workspace, "git", cloneWith(credentials, "clone", "--quiet", repository, tree)...); err != nil { say("clone", "FAILED: %v", err) return Result{}, fmt.Errorf("cannot clone %s: %w", repository, err) } @@ -177,7 +200,7 @@ func Build(ctx context.Context, run Runner, publish Publisher, sort.Slice(artifacts, func(i, j int) bool { return artifacts[i].Name < artifacts[j].Name }) for _, a := range artifacts { say("artifact", "%s (%s%s) — starting", a.Name, a.Kind, langSuffix(a)) - made, err := one(ctx, run, publish, manifest.Module, within, workspace, commit, a, args, held, npmrcPath, say) + made, err := one(ctx, run, publish, manifest.Module, within, workspace, commit, credentials, a, args, held, npmrcPath, say) if err != nil { say("artifact", "%s FAILED: %v", a.Name, err) return Result{}, err @@ -213,14 +236,14 @@ func logging(log Log) func(step, format string, args ...any) { // contextFrom clones an image artifact's own build context, when it names one apart from this // module's own repository — a fresh tree, the same way the module's own is, keyed by artifact // name so two artifacts of one module naming different contexts do not collide. -func contextFrom(ctx context.Context, run Runner, workspace, artifact string, +func contextFrom(ctx context.Context, run Runner, workspace, artifact, credentials string, from catalogue.ArtifactContext, say func(step, format string, args ...any)) (string, error) { say("context", "cloning %s at %s for %s", from.Repository, refOrHead(from.Ref), artifact) dir := filepath.Join(workspace, "context-"+artifact) if err := os.RemoveAll(dir); err != nil { return "", err } - if _, err := run(ctx, workspace, "git", "clone", "--quiet", from.Repository, dir); err != nil { + if _, err := run(ctx, workspace, "git", cloneWith(credentials, "clone", "--quiet", from.Repository, dir)...); err != nil { return "", fmt.Errorf("cannot clone %s: %w", from.Repository, err) } if from.Ref != "" { @@ -232,6 +255,21 @@ func contextFrom(ctx context.Context, run Runner, workspace, artifact string, return dir, nil } +// cloneWith is a git invocation that may offer a stored credential. +// +// The first `-c credential.helper=` clears every helper the environment might carry, so exactly +// one place answers an authentication challenge: the file the builder wrote. Without a file, the +// invocation is exactly what it always was. +func cloneWith(credentials string, rest ...string) []string { + if credentials == "" { + return rest + } + return append([]string{ + "-c", "credential.helper=", + "-c", "credential.helper=store --file=" + credentials, + }, rest...) +} + func describePath(path string) string { if path == "" { return "" @@ -355,7 +393,7 @@ func wantsPackages(manifest catalogue.Manifest, within string) bool { } func one(ctx context.Context, run Runner, publish Publisher, - module, tree, workspace, commit string, a catalogue.Artifact, args []string, + module, tree, workspace, commit, credentials string, a catalogue.Artifact, args []string, held map[string]string, npmrc string, say func(step, format string, args ...any)) (catalogue.Built, error) { switch a.Kind { @@ -433,7 +471,7 @@ func one(ctx context.Context, run Runner, publish Publisher, recipePath := a.From buildDir := tree if a.Context != nil { - cloned, err := contextFrom(ctx, run, workspace, a.Name, *a.Context, say) + cloned, err := contextFrom(ctx, run, workspace, a.Name, credentials, *a.Context, say) if err != nil { return catalogue.Built{}, fmt.Errorf("%s: %s's context: %w", module, a.Name, err) } diff --git a/internal/builder/builder_test.go b/internal/builder/builder_test.go index 175295e..0dbd714 100644 --- a/internal/builder/builder_test.go +++ b/internal/builder/builder_test.go @@ -42,8 +42,17 @@ func (r *recorded) run(_ context.Context, dir, name string, args ...string) (str line := name + " " + strings.Join(args, " ") r.ran = append(r.ran, line) r.dirs = append(r.dirs, dir) + // A clone may carry `-c` configuration in front of the verb — the credential store — so the + // verb is found rather than assumed first. + isClone := false + for _, a := range args { + if a == "clone" { + isClone = true + break + } + } switch { - case name == "git" && len(args) > 0 && args[0] == "clone": + case name == "git" && isClone: repository := args[len(args)-2] tree := args[len(args)-1] if err := os.MkdirAll(tree, 0o755); err != nil { @@ -119,7 +128,7 @@ func TestABuildProducesAManifestThePinsAreIn(t *testing.T) { r, workspace := aRepository(t, withBoth, map[string]string{ "Dockerfile": "FROM scratch", "files/theme.conf": "dark", }) - got, err := Build(context.Background(), r.run, r, "https://forge.invalid/meshboard.git", "", "", workspace, nil, Npmrc{}, nil) + got, err := Build(context.Background(), r.run, r, "https://forge.invalid/meshboard.git", "", "", workspace, nil, Npmrc{}, GitCredential{}, nil) if err != nil { t.Fatal(err) } @@ -145,7 +154,7 @@ func TestTwoBuildsOfOneCommitProduceOneDigest(t *testing.T) { }) // A year apart, so a packer carrying timestamps cannot accidentally agree. r.stamped = time.Date(2020+i, time.March, 3, 4, 5, 6, 0, time.UTC) - got, err := Build(context.Background(), r.run, r, "https://forge.invalid/x.git", "", "", workspace, nil, Npmrc{}, nil) + got, err := Build(context.Background(), r.run, r, "https://forge.invalid/x.git", "", "", workspace, nil, Npmrc{}, GitCredential{}, nil) if err != nil { t.Fatal(err) } @@ -165,7 +174,7 @@ func TestNothingIsPublishedUntilEverythingIsBuilt(t *testing.T) { // unreferenced, and indistinguishable from something in use. r, workspace := aRepository(t, withBoth, map[string]string{"Dockerfile": "FROM scratch"}) // `files` is missing, so packing the archive fails — after the image would have been pushed. - _, err := Build(context.Background(), r.run, r, "https://forge.invalid/x.git", "", "", workspace, nil, Npmrc{}, nil) + _, err := Build(context.Background(), r.run, r, "https://forge.invalid/x.git", "", "", workspace, nil, Npmrc{}, GitCredential{}, nil) if err == nil { t.Fatal("a build with a missing input succeeded") } @@ -177,7 +186,7 @@ func TestNothingIsPublishedUntilEverythingIsBuilt(t *testing.T) { func TestARepositoryWithNoManifestSaysSo(t *testing.T) { workspace := t.TempDir() r := &recorded{contents: map[string]string{"README.md": "nothing to see"}} - _, err := Build(context.Background(), r.run, r, "https://forge.invalid/x.git", "", "", workspace, nil, Npmrc{}, nil) + _, err := Build(context.Background(), r.run, r, "https://forge.invalid/x.git", "", "", workspace, nil, Npmrc{}, GitCredential{}, nil) if err == nil { t.Fatal("a repository with nothing saying what it is was built") } @@ -190,7 +199,7 @@ func TestAModuleThatBuildsNothingStillProducesAManifest(t *testing.T) { // Most of what a person installs is configuration. r, workspace := aRepository(t, `{"module":"shell","version":"1","resources":[ {"id":"rc","type":"file","path":"/etc/zsh/zshrc","content":"setopt"}]}`, nil) - got, err := Build(context.Background(), r.run, r, "https://forge.invalid/shell.git", "", "", workspace, nil, Npmrc{}, nil) + got, err := Build(context.Background(), r.run, r, "https://forge.invalid/shell.git", "", "", workspace, nil, Npmrc{}, GitCredential{}, nil) if err != nil { t.Fatal(err) } @@ -220,7 +229,7 @@ func TestTheTreeIsFreshEveryTime(t *testing.T) { if err := os.WriteFile(leftover, []byte("stale"), 0o644); err != nil { t.Fatal(err) } - if _, err := Build(context.Background(), r.run, r, "https://forge.invalid/x.git", "", "", workspace, nil, Npmrc{}, nil); err != nil { + if _, err := Build(context.Background(), r.run, r, "https://forge.invalid/x.git", "", "", workspace, nil, Npmrc{}, GitCredential{}, nil); err != nil { t.Fatal(err) } if _, err := os.Stat(leftover); err == nil { @@ -233,7 +242,7 @@ func TestABuildThatCannotPushFails(t *testing.T) { "Dockerfile": "FROM scratch", "files/a": "b", }) r.failPush = true - if _, err := Build(context.Background(), r.run, r, "https://forge.invalid/x.git", "", "", workspace, nil, Npmrc{}, nil); err == nil { + if _, err := Build(context.Background(), r.run, r, "https://forge.invalid/x.git", "", "", workspace, nil, Npmrc{}, GitCredential{}, nil); err == nil { t.Fatal("a build that could publish nothing reported success") } } @@ -247,7 +256,7 @@ func TestAnUpstreamImageIsMirroredRatherThanBuilt(t *testing.T) { "resources":[{"id":"db","type":"container","name":"mesh-postgres","artifact":"store"}]}` r, workspace := aRepository(t, mirrors, nil) - got, err := Build(context.Background(), r.run, r, "https://forge.invalid/postgres.git", "", "", workspace, nil, Npmrc{}, nil) + got, err := Build(context.Background(), r.run, r, "https://forge.invalid/postgres.git", "", "", workspace, nil, Npmrc{}, GitCredential{}, nil) if err != nil { t.Fatal(err) } @@ -313,7 +322,7 @@ func TestAModuleIsBuiltFromItsPathWithinTheRepository(t *testing.T) { "modules/other/" + ManifestName: `{"module":"other","version":"1"}`, }} got, err := Build(context.Background(), r.run, r, - "https://forge.invalid/catalogue.git", "modules/shell", "", t.TempDir(), nil, Npmrc{}, nil) + "https://forge.invalid/catalogue.git", "modules/shell", "", t.TempDir(), nil, Npmrc{}, GitCredential{}, nil) if err != nil { t.Fatal(err) } @@ -332,7 +341,7 @@ func TestAPathThatLeavesTheRepositoryIsRefused(t *testing.T) { for _, escaping := range []string{"../../etc", "/etc"} { r := &recorded{contents: map[string]string{ManifestName: withBoth}} _, err := Build(context.Background(), r.run, r, - "https://forge.invalid/x.git", escaping, "", t.TempDir(), nil, Npmrc{}, nil) + "https://forge.invalid/x.git", escaping, "", t.TempDir(), nil, Npmrc{}, GitCredential{}, nil) if err == nil { t.Fatalf("%q was accepted as a module's path", escaping) } @@ -369,7 +378,7 @@ func TestAnArtifactWithItsOwnContextIsBuiltFromThere(t *testing.T) { }, } _, err := Build(context.Background(), r.run, r, - "https://forge.invalid/catalogue.git", "", "", t.TempDir(), nil, Npmrc{}, nil) + "https://forge.invalid/catalogue.git", "", "", t.TempDir(), nil, Npmrc{}, GitCredential{}, nil) if err != nil { t.Fatal(err) } @@ -411,3 +420,99 @@ func TestAnArtifactWithItsOwnContextIsBuiltFromThere(t *testing.T) { t.Errorf("the build was not given a context: %s", build) } } + +// The forge credential is offered through git's own credential store — a file, never argv — and +// git decides when it applies. What is checked: the clone names the store, the secret never +// appears in a command line, and the file holds exactly the URL at 0600. +func TestABuildOffersTheForgesCredentialThroughGitsOwnStore(t *testing.T) { + r, workspace := aRepository(t, withBoth, map[string]string{ + "Dockerfile": "FROM scratch", "files/theme.conf": "dark", + }) + _, err := Build(context.Background(), r.run, r, "https://forge.invalid/meshboard.git", "", "", + workspace, nil, Npmrc{}, + GitCredential{URL: "http://mesh_novox_builder:sw0rdfi5h@forge.invalid:20000"}, nil) + if err != nil { + t.Fatal(err) + } + stored := filepath.Join(workspace, "git-credentials") + clone := r.ran[0] + if !strings.Contains(clone, "credential.helper=store --file="+stored) { + t.Fatalf("the clone does not name the credential store: %s", clone) + } + for _, line := range r.ran { + if strings.Contains(line, "sw0rdfi5h") { + t.Fatalf("the secret is in a command line, readable by anything that can list processes: %s", line) + } + } + raw, err := os.ReadFile(stored) + if err != nil { + t.Fatal(err) + } + if strings.TrimSpace(string(raw)) != "http://mesh_novox_builder:sw0rdfi5h@forge.invalid:20000" { + t.Fatalf("the store does not hold the credential as given: %q", raw) + } + info, err := os.Stat(stored) + if err != nil { + t.Fatal(err) + } + if info.Mode().Perm() != 0o600 { + t.Fatalf("the credential file is readable beyond its owner: %v", info.Mode()) + } +} + +// Without a credential, a clone is exactly the invocation it always was, and no credential file +// appears — the builder a mesh of public repositories runs is unchanged. +func TestABuildWithNoCredentialClonesExactlyAsBefore(t *testing.T) { + r, workspace := aRepository(t, withBoth, map[string]string{ + "Dockerfile": "FROM scratch", "files/theme.conf": "dark", + }) + _, err := Build(context.Background(), r.run, r, "https://forge.invalid/meshboard.git", "", "", + workspace, nil, Npmrc{}, GitCredential{}, nil) + if err != nil { + t.Fatal(err) + } + if !strings.HasPrefix(r.ran[0], "git clone --quiet ") { + t.Fatalf("a credential-less clone grew flags: %s", r.ran[0]) + } + if _, err := os.Stat(filepath.Join(workspace, "git-credentials")); !os.IsNotExist(err) { + t.Fatal("a credential file was written with no credential to put in it") + } +} + +// An artifact's own context is cloned with the same offer: a private module whose context is a +// second private repository on the same forge builds, and the secret still never reaches argv. +func TestAContextCloneCarriesTheSameCredentialStore(t *testing.T) { + const withContext = `{"module":"route-proxy","version":"1", + "build":{"artifacts":[ + {"name":"server","kind":"image","from":"Dockerfile", + "context":{"repository":"https://forge.invalid/source.git","ref":"main"}}]}}` + r := &recorded{ + contents: map[string]string{ + ManifestName: withContext, + "Dockerfile": "FROM scratch\nCOPY go.mod ./\n", + }, + secondary: map[string]map[string]string{ + "https://forge.invalid/source.git": {"go.mod": "module route-proxy\n"}, + }, + } + workspace := t.TempDir() + _, err := Build(context.Background(), r.run, r, + "https://forge.invalid/catalogue.git", "", "", workspace, nil, Npmrc{}, + GitCredential{URL: "https://builder:s3cret@forge.invalid"}, nil) + if err != nil { + t.Fatal(err) + } + stored := filepath.Join(workspace, "git-credentials") + var contextClone string + for _, line := range r.ran { + if strings.Contains(line, "clone") && strings.Contains(line, "source.git") { + contextClone = line + } + } + if contextClone == "" { + t.Fatalf("the context was never cloned: %v", r.ran) + } + if !strings.Contains(contextClone, "credential.helper=store --file="+stored) { + t.Fatalf("the context clone does not name the credential store: %s", contextClone) + } +} diff --git a/internal/builder/bundle_test.go b/internal/builder/bundle_test.go index 5cf8846..da718fd 100644 --- a/internal/builder/bundle_test.go +++ b/internal/builder/bundle_test.go @@ -54,7 +54,7 @@ func TestABundleIsCompiledAndPackedWithNoDockerfile(t *testing.T) { held := map[string]string{"mesh-tools/build": "registry.invalid/mesh-tools/build@sha256:" + strings.Repeat("b", 64)} got, err := Build(context.Background(), compiling{r}.run, r, - "https://forge.invalid/greeter.git", "", "", workspace, held, Npmrc{}, nil) + "https://forge.invalid/greeter.git", "", "", workspace, held, Npmrc{}, GitCredential{}, nil) if err != nil { t.Fatalf("a module with a language and no Dockerfile did not build: %v", err) } @@ -91,7 +91,7 @@ func TestABundleWhoseToolchainIsNotHeldIsRefusedFirst(t *testing.T) { r, workspace := aRepository(t, aBundle, map[string]string{"index.ts": "console.log(1)"}) _, err := Build(context.Background(), compiling{r}.run, r, - "https://forge.invalid/greeter.git", "", "", workspace, nil, Npmrc{}, nil) + "https://forge.invalid/greeter.git", "", "", workspace, nil, Npmrc{}, GitCredential{}, nil) if err == nil { t.Fatal("a bundle was built with no toolchain to compile it in") } @@ -112,7 +112,7 @@ func TestABundleInAnUnknownLanguageIsRefused(t *testing.T) { _, err := Build(context.Background(), compiling{r}.run, r, "https://forge.invalid/greeter.git", "", "", workspace, - map[string]string{"mesh-tools/build": "registry.invalid/x@sha256:" + strings.Repeat("c", 64)}, Npmrc{}, nil) + map[string]string{"mesh-tools/build": "registry.invalid/x@sha256:" + strings.Repeat("c", 64)}, Npmrc{}, GitCredential{}, nil) if err == nil { t.Fatal("a language nothing can compile was accepted") } @@ -140,7 +140,7 @@ func TestTwoBundlesInOneModuleArePackedSeparately(t *testing.T) { held := map[string]string{"mesh-tools/build": "registry.invalid/mesh-tools/build@sha256:" + strings.Repeat("b", 64)} got, err := Build(context.Background(), compiling{r}.run, r, - "https://forge.invalid/greeter.git", "", "", workspace, held, Npmrc{}, nil) + "https://forge.invalid/greeter.git", "", "", workspace, held, Npmrc{}, GitCredential{}, nil) if err != nil { t.Fatalf("a module with two bundles did not build: %v", err) } diff --git a/internal/builder/packages_test.go b/internal/builder/packages_test.go index 8a8ae8c..a87f19f 100644 --- a/internal/builder/packages_test.go +++ b/internal/builder/packages_test.go @@ -71,7 +71,7 @@ func TestAnImageBuildGetsTheCredentialInTheContextAndHostNetwork(t *testing.T) { r, workspace := aRepository(t, withBoth, map[string]string{"Dockerfile": "FROM scratch\nCOPY .npmrc ./", "files/x": "y"}) n := Npmrc{Scope: "@novox", Registry: "https://forge.invalid/api/packages/novox/npm/", Token: "t"} if _, err := Build(context.Background(), r.run, r, - "https://forge.invalid/meshboard.git", "", "", workspace, nil, n, nil); err != nil { + "https://forge.invalid/meshboard.git", "", "", workspace, nil, n, GitCredential{}, nil); err != nil { t.Fatalf("the build failed: %v", err) } @@ -101,7 +101,7 @@ func TestAnImageBuildGetsTheCredentialInTheContextAndHostNetwork(t *testing.T) { func TestAnImageBuildWithoutACredentialGetsNoHostNetwork(t *testing.T) { r, workspace := aRepository(t, withBoth, map[string]string{"Dockerfile": "FROM scratch", "files/x": "y"}) if _, err := Build(context.Background(), r.run, r, - "https://forge.invalid/meshboard.git", "", "", workspace, nil, Npmrc{}, nil); err != nil { + "https://forge.invalid/meshboard.git", "", "", workspace, nil, Npmrc{}, GitCredential{}, nil); err != nil { t.Fatalf("the build failed: %v", err) } for _, line := range r.ran { @@ -127,7 +127,7 @@ func TestAPackageIsBuiltOnAPublicBaseAndPublishedByVersion(t *testing.T) { }) n := Npmrc{Scope: "@novox", Registry: "https://forge.invalid/api/packages/novox/npm/", Token: "t"} got, err := Build(context.Background(), r.run, r, - "https://forge.invalid/mesh-sdk.git", "", "", workspace, nil, n, nil) + "https://forge.invalid/mesh-sdk.git", "", "", workspace, nil, n, GitCredential{}, nil) if err != nil { t.Fatalf("the package did not build: %v", err) } @@ -159,7 +159,7 @@ func TestAPackageWithNoRegistryIsRefused(t *testing.T) { "package.json": `{"name":"@novox/mesh-sdk","version":"0.1.0"}`, }) _, err := Build(context.Background(), r.run, r, - "https://forge.invalid/mesh-sdk.git", "", "", workspace, nil, Npmrc{}, nil) + "https://forge.invalid/mesh-sdk.git", "", "", workspace, nil, Npmrc{}, GitCredential{}, nil) if err == nil { t.Fatal("a package built with no registry to publish to, silently") } @@ -206,7 +206,7 @@ func TestAnImageThatDoesNotAskForTheCredentialDoesNotGetIt(t *testing.T) { r, workspace := aRepository(t, withBoth, map[string]string{"Dockerfile": "FROM scratch\nCOPY . .", "files/x": "y"}) n := Npmrc{Scope: "@novox", Registry: "https://forge.invalid/api/packages/novox/npm/", Token: "t"} if _, err := Build(context.Background(), r.run, r, - "https://forge.invalid/meshboard.git", "", "", workspace, nil, n, nil); err != nil { + "https://forge.invalid/meshboard.git", "", "", workspace, nil, n, GitCredential{}, nil); err != nil { t.Fatalf("the build failed: %v", err) } for _, line := range r.ran {