mesh-build-node: narrow the fetch to the named paths, build members against the SDK their lock names, and build, pack and publish a package (issues 1758, 1587 S15-S19; ADR 0499 §4, ADR 0498 §5, ADR 0500 §4-§6; lead decision L10)
build_node_fetch takes buildstep.FetchRequest (dir and paths narrow the workspace) and an
optional package {repository, commit, ref}: the group's package member, built and packed
shut off and published nowhere. Each member's lock decides its SDK: the group's packed
file when the version and integrity are the lock's, else the registry's (integrity
checked); a version the registry lacks fails waits for: @novox/mesh-sdk@<version>. A
package artifact is built by its own script and packed with npm pack, shut off; only a
trunk build publishes it, from this process with the write credential (never in a
workspace): absent is published, the same file is done, another is version taken:. The
registry configuration file now lies outside every workspace.
This commit is contained in:
@@ -58,6 +58,15 @@ toolchain install: a long step answers at once with its id and ends on `<node>.<
|
||||
other `@novox/` packages install. A module with packages and no lock does not fail the fetch: its result lists it
|
||||
under `refused` with the reason, and a build of that bundle fails naming it (issue 1459). Every command runs through `forgekey.ExecRunnerClean(0)` with PATH, HOME, the locale and its own
|
||||
words, nothing of this process's environment. Workspaces older than a day are removed at each fetch.
|
||||
The arguments are mesh-sdk's `buildstep.FetchRequest` (ADR 0499 §4): with `dir` and `paths` (from the builder, never
|
||||
from a manifest) the workspace is narrowed to those paths, the manifest and the lock files before anything else. A
|
||||
member is built against the SDK version its own lock names (the lead's decision L10 of R4e, ADR 0500 §5-§6): the
|
||||
fetch places that version in the module's `node_modules/@novox/mesh-sdk`, from the group's package when the call
|
||||
names one (`package: {repository, commit, ref}`: fetched, its dependencies installed from its own lock, built and
|
||||
packed shut off, published nowhere) and its version and integrity are the lock's, else from the registry (integrity
|
||||
checked); a version the registry lacks fails `waits for: @novox/mesh-sdk@<version>` (the builder queues the key), or,
|
||||
with a group package of another version, fails naming both. A module whose lock names no SDK keeps the toolchain's.
|
||||
A module whose artifact is a `package` gets its dependencies, devDependencies included, from its own lock instead.
|
||||
- `build_node_test` {workspace, module, level} (issue 1491): the unit level is the type-check (the toolchain's tsc, the
|
||||
package's `tsconfig.json` or the build agent's flags over its .ts files) and the `test` script; the integration level
|
||||
is the `test:integration` script, the type-check being its setup. Bounds (the Go module's; ADR 0445 names none for
|
||||
@@ -76,3 +85,8 @@ toolchain install: a long step answers at once with its id and ends on `<node>.<
|
||||
step `not stored: <artifact>: <the store's words>`. The packed bytes never stay on the node. tsc and esbuild, like the
|
||||
type-check and the test script, run shut off in bubblewrap (ADR 0448): no network, none of the account, the toolchain
|
||||
read-only, the npm cache beneath a throwaway layer unless the step is told `publish` (a trunk build).
|
||||
A `package` artifact (ADR 0498 §5) is built by its own `build` script and packed with `npm pack --ignore-scripts`, both
|
||||
shut off; its digest is the packed file's. Only with `publish` is it published, by this process with the registry's
|
||||
write credential (never in a workspace or a step's mounts): a version the registry lacks is published, the same
|
||||
packed file already there is done without a publish, another file under that version fails `version taken:
|
||||
<name>@<version>`; the reference `<name>@<version>` is answered only then. A candidate answers its digest alone.
|
||||
|
||||
@@ -36,6 +36,7 @@ type artifact struct {
|
||||
// module is the manifest's module name, which the artifact is stored under.
|
||||
module string
|
||||
Name string `json:"name"`
|
||||
Kind string `json:"kind"`
|
||||
Language string `json:"language"`
|
||||
Entrypoints []string `json:"entrypoints"`
|
||||
External []string `json:"external"`
|
||||
@@ -89,6 +90,9 @@ func (m *Module) build(ctx context.Context, args map[string]any) (any, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if a.Kind == "package" {
|
||||
return m.buildPackage(ctx, workspace, module, root, dir, a, publish)
|
||||
}
|
||||
if why, err := lockProblem(dir, module); err != nil || why != "" {
|
||||
if err == nil {
|
||||
err = fmt.Errorf("%s was refused by the fetch: %s", name, why)
|
||||
@@ -280,6 +284,10 @@ func readArtifact(dir, name string) (artifact, error) {
|
||||
if a.Name != name {
|
||||
continue
|
||||
}
|
||||
if a.Kind == "package" && a.Language == "typescript" {
|
||||
a.module = m.Module
|
||||
return a, nil // a package is built by its own build script and packed (package.go), from no entrypoint
|
||||
}
|
||||
if a.Language != "typescript" {
|
||||
return artifact{}, fmt.Errorf("artifact %q is %q, and this module builds typescript", name, a.Language)
|
||||
}
|
||||
|
||||
@@ -59,6 +59,18 @@ type FetchResult struct {
|
||||
Conflicts []string `json:"conflicts,omitempty"`
|
||||
Changed []string `json:"changed,omitempty"`
|
||||
Trees map[string]buildstep.ModuleTree `json:"trees,omitempty"`
|
||||
// Group is the group's package the fetch packed and served to the TypeScript members (the call's `package`):
|
||||
// "<name>@<version> <integrity>".
|
||||
Group string `json:"group,omitempty"`
|
||||
}
|
||||
|
||||
// narrowedMark is the file beside a workspace's tree saying the fetch narrowed it to a module's named paths.
|
||||
const narrowedMark = "narrowed"
|
||||
|
||||
// narrowed says whether the fetch narrowed the workspace whose tree this is.
|
||||
func narrowed(tree string) bool {
|
||||
_, err := os.Stat(filepath.Join(filepath.Dir(tree), narrowedMark))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// Refusal is a module a fetch did not install the packages of.
|
||||
@@ -79,7 +91,12 @@ func (m *Module) fetchTool() StepTool {
|
||||
"repository": map[string]any{"type": "string", "description": "<owner>/<repository> on the forge"},
|
||||
"commit": map[string]any{"type": "string", "description": "the full commit, 40 hex digits"},
|
||||
"ref": map[string]any{"type": "string", "description": "refs/pull/<n>/head when only a pull request's head carries the commit"},
|
||||
"base": map[string]any{"type": "string", "description": "optional trunk commit, 40 hex digits: the head is merged onto it as the forge's merge would, and the answer carries the merge commit, the files in conflict (an answer, the workspace is then left at the trunk commit), the changed paths and the tree of each module directory"},
|
||||
"dir": buildstep.FetchRequestProperties()["dir"],
|
||||
"paths": buildstep.FetchRequestProperties()["paths"],
|
||||
"package": map[string]any{"type": "object", "description": "optional: the group's package member (ADR 0500 §5), packed here, publishing nothing, and served to each TypeScript member whose lock names its version, in place of the registry's",
|
||||
"properties": map[string]any{"repository": map[string]any{"type": "string"}, "commit": map[string]any{"type": "string"}, "ref": map[string]any{"type": "string"}},
|
||||
"required": []string{"repository", "commit"}},
|
||||
"base": map[string]any{"type": "string", "description": "optional trunk commit, 40 hex digits: the head is merged onto it as the forge's merge would, and the answer carries the merge commit, the files in conflict (an answer, the workspace is then left at the trunk commit), the changed paths and the tree of each module directory"},
|
||||
},
|
||||
"required": []string{"repository", "commit"},
|
||||
},
|
||||
@@ -89,15 +106,16 @@ func (m *Module) fetchTool() StepTool {
|
||||
}
|
||||
|
||||
func (m *Module) fetch(ctx context.Context, args map[string]any) (any, error) {
|
||||
repository, _ := args["repository"].(string)
|
||||
commit, _ := args["commit"].(string)
|
||||
ref, _ := args["ref"].(string)
|
||||
base, _ := args["base"].(string)
|
||||
if base != "" && !commitOK.MatchString(base) {
|
||||
return nil, fmt.Errorf("%q is not a trunk commit (40 hex digits)", base)
|
||||
// What the builder and the checker ask, in mesh-sdk's one form (ADR 0499 §4): with the module's directory and its
|
||||
// named paths, the workspace is narrowed to them (narrow.go).
|
||||
req, err := buildstep.ParseFetchRequest(args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !repositoryOK.MatchString(repository) || strings.Contains(repository, "..") {
|
||||
return nil, fmt.Errorf("%q is not a repository, <owner>/<name>", repository)
|
||||
repository, commit, ref, base := req.Repository, req.Commit, req.Ref, req.Base
|
||||
group, err := groupPackageArg(args["package"])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.CleanWorkspaces(m.now())
|
||||
id := buildstep.NewID()
|
||||
@@ -123,16 +141,49 @@ func (m *Module) fetch(ctx context.Context, args map[string]any) (any, error) {
|
||||
return res, nil // a conflict: the tree is at the trunk, nothing of the head is installed
|
||||
}
|
||||
}
|
||||
dirs, err := m.typeScriptModules(ctx, tree)
|
||||
if len(req.Paths) > 0 {
|
||||
if err := narrow(tree, req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Said beside the tree, so the listing of the tree's modules leaves out those narrowed away.
|
||||
if err := os.WriteFile(filepath.Join(root, narrowedMark), nil, 0o600); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// The group's package, packed here and served to its members in place of the registry's version (ADR 0500 §5).
|
||||
var served *packed
|
||||
if group != nil {
|
||||
p, err := m.packGroup(ctx, root, gitRun, group)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
served, res.Group = &p, p.Name+"@"+p.Version+" "+p.Integrity
|
||||
}
|
||||
dirs, packages, err := m.typeScriptModules(ctx, tree)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, d := range dirs {
|
||||
res.Modules = append(res.Modules, d)
|
||||
did, refused, err := m.installOwn(ctx, root, filepath.Join(tree, filepath.FromSlash(d)), d)
|
||||
dir := filepath.Join(tree, filepath.FromSlash(d))
|
||||
if packages[d] {
|
||||
// A package is built from its own lock's dependencies, its devDependencies included (ADR 0498 §5).
|
||||
if err := m.installDev(ctx, root, dir, d); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res.Installed = append(res.Installed, d)
|
||||
continue
|
||||
}
|
||||
did, refused, err := m.installOwn(ctx, root, dir, d)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if refused == "" {
|
||||
// The SDK the module's own lock names, from the group or the registry (the lead's decision L10).
|
||||
if err := m.placeSDK(ctx, dir, d, served); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if refused != "" {
|
||||
res.Refused = append(res.Refused, Refusal{Module: d, Reason: refused})
|
||||
}
|
||||
@@ -145,19 +196,22 @@ func (m *Module) fetch(ctx context.Context, args map[string]any) (any, error) {
|
||||
|
||||
// typeScriptModules are the directories of the tree whose module.json declares a typescript artifact, relative to
|
||||
// it, in order.
|
||||
func (m *Module) typeScriptModules(ctx context.Context, tree string) ([]string, error) {
|
||||
func (m *Module) typeScriptModules(ctx context.Context, tree string) (dirs []string, packages map[string]bool, err error) {
|
||||
r := m.Run(ctx, m.baseEnv(), "git", "-C", tree, "ls-files", "--", "module.json", "*/module.json")
|
||||
if r.Status != 0 || r.Err != "" {
|
||||
return nil, fmt.Errorf("git ls-files: %s", firstLine(r.Err+" "+r.Stderr))
|
||||
return nil, nil, fmt.Errorf("git ls-files: %s", firstLine(r.Err+" "+r.Stderr))
|
||||
}
|
||||
var dirs []string
|
||||
packages = map[string]bool{}
|
||||
for _, l := range strings.Split(r.Stdout, "\n") {
|
||||
if l = strings.TrimSpace(l); l == "" {
|
||||
continue
|
||||
}
|
||||
raw, err := os.ReadFile(filepath.Join(tree, filepath.FromSlash(l)))
|
||||
if os.IsNotExist(err) && narrowed(tree) {
|
||||
continue // outside a narrowed workspace's paths (narrow.go): no module of this build
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
var mj struct {
|
||||
Build struct {
|
||||
@@ -169,13 +223,15 @@ func (m *Module) typeScriptModules(ctx context.Context, tree string) ([]string,
|
||||
}
|
||||
for _, a := range mj.Build.Artifacts {
|
||||
if a.Language == "typescript" {
|
||||
dirs = append(dirs, filepath.ToSlash(filepath.Dir(l)))
|
||||
d := filepath.ToSlash(filepath.Dir(l))
|
||||
dirs = append(dirs, d)
|
||||
packages[d] = a.Kind == "package"
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Strings(dirs)
|
||||
return dirs, nil
|
||||
return dirs, packages, nil
|
||||
}
|
||||
|
||||
// ownDependencies are the package.json's dependencies beyond the SDK and beyond what is build-time: the types and
|
||||
@@ -229,13 +285,6 @@ func (m *Module) installOwn(ctx context.Context, root, dir, rel string) (install
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
if m.Toolchain == nil || m.Toolchain.Registry == nil {
|
||||
return false, "", fmt.Errorf("the module has no package registry to install %s from", rel)
|
||||
}
|
||||
reg, err := m.Toolchain.Registry()
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
// A scratch copy, so the module's package.json and lock are never rewritten (dependencies.go installSteps).
|
||||
scratch := filepath.Join(root, "npm-"+strings.NewReplacer("/", "-", ".", "root").Replace(rel))
|
||||
if err := os.MkdirAll(scratch, 0o700); err != nil {
|
||||
@@ -255,10 +304,11 @@ func (m *Module) installOwn(ctx context.Context, root, dir, rel string) (install
|
||||
if err := os.WriteFile(filepath.Join(scratch, "package-lock.json"), lock, 0o600); err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
// The registry is given through a config file of its own, never on the command line, and removed after.
|
||||
npmrc := filepath.Join(scratch, ".npmrc-install")
|
||||
if err := os.WriteFile(npmrc, []byte(reg.Npmrc()), 0o600); err != nil {
|
||||
return false, "", err
|
||||
// The registry is given through a config file of its own, never on the command line, outside the workspace, and
|
||||
// removed after (npmrcIn).
|
||||
npmrc, err := m.npmrcIn(root)
|
||||
if err != nil {
|
||||
return false, "", fmt.Errorf("installing the packages of %s: %w", rel, err)
|
||||
}
|
||||
env := append(m.baseEnv(), "npm_config_cache="+filepath.Join(m.Toolchain.Paths.Cache, "npm"))
|
||||
r := m.Run(ctx, env, "npm", "ci", "--omit=dev", "--omit=peer", "--ignore-scripts", "--no-bin-links",
|
||||
|
||||
@@ -220,3 +220,21 @@ func TestTheModuleRequiresTheArtifactStoreAndIsToldItsAddress(t *testing.T) {
|
||||
t.Errorf("the registry resource is %v", r)
|
||||
}
|
||||
}
|
||||
|
||||
// 1587-S17 (ADR 0498 §5): the module publishes a trunk package itself, so its contribution to the package registry asks
|
||||
// for write access; the credential stays with the module's own process (a run step has neither the home nor a network).
|
||||
func TestTheModuleContributesWriteAccessToThePackageRegistry(t *testing.T) {
|
||||
raw, err := os.ReadFile(filepath.Join(moduleDir, "module.json"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var mj struct {
|
||||
Contributes map[string]map[string]any `json:"contributes"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &mj); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := mj.Contributes["npm-package-registry"]["access"]; got != "write" {
|
||||
t.Errorf("npm-package-registry access = %v, want write", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -48,6 +49,8 @@ type Module struct {
|
||||
// Ctx is the process's context, which a step runs under (the tool call's is cut at 30 s, the step's is not).
|
||||
Ctx context.Context
|
||||
Say func(format string, a ...any)
|
||||
// HTTP reads and publishes to the package registry; a client of its own when nil. A test serves its own.
|
||||
HTTP *http.Client
|
||||
// RegistryFile holds the artifact store's address (host:port), placed by the manifest; Push puts a blob there
|
||||
// (buildstep.PushBlob when nil; a test's fake store).
|
||||
RegistryFile string
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package main
|
||||
|
||||
// The narrowed workspace (novox/hq ADR 0499 §4; issue 1758, the lead's decision L8): a build of a module whose every
|
||||
// artifact names its paths sees only those paths, the module's manifest and its lock files. The paths come from the
|
||||
// builder's request (buildstep.FetchRequest's Dir and Paths, from the indexer's kept inputs), never from a manifest.
|
||||
// Everything else of the fetched tree is removed before any run step, so a read outside fails, naming the path.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/buildstep"
|
||||
)
|
||||
|
||||
// narrowKept are the files of the module's directory a narrowed workspace keeps beside its paths: the manifest and the
|
||||
// lock files of both languages.
|
||||
var narrowKept = []string{"module.json", "package.json", "package-lock.json", "go.mod", "go.sum"}
|
||||
|
||||
// narrow removes from tree everything but r's paths in its module directory, the manifest and the lock files (and the
|
||||
// repository's .git). A request naming no paths leaves the tree whole. A named path the tree lacks fails, naming it.
|
||||
func narrow(tree string, r buildstep.FetchRequest) error {
|
||||
if len(r.Paths) == 0 {
|
||||
return nil
|
||||
}
|
||||
dir := path.Clean(r.Dir)
|
||||
keep := map[string]bool{}
|
||||
for _, p := range r.Paths {
|
||||
rel := path.Join(dir, path.Clean(p))
|
||||
if _, err := os.Lstat(filepath.Join(tree, filepath.FromSlash(rel))); err != nil {
|
||||
return fmt.Errorf("the path %s the request names is not in the commit: %w", rel, err)
|
||||
}
|
||||
keep[rel] = true
|
||||
}
|
||||
for _, f := range narrowKept {
|
||||
keep[path.Join(dir, f)] = true
|
||||
}
|
||||
return prune(tree, "", keep)
|
||||
}
|
||||
|
||||
// prune removes under tree/rel everything that is neither kept nor above or below a kept path.
|
||||
func prune(tree, rel string, keep map[string]bool) error {
|
||||
entries, err := os.ReadDir(filepath.Join(tree, filepath.FromSlash(rel)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
names := make([]string, 0, len(entries))
|
||||
for _, e := range entries {
|
||||
names = append(names, e.Name())
|
||||
}
|
||||
sort.Strings(names)
|
||||
for _, name := range names {
|
||||
p := path.Join(rel, name)
|
||||
if rel == "" && name == ".git" {
|
||||
continue
|
||||
}
|
||||
switch {
|
||||
case keep[p]:
|
||||
continue // a kept path, whole
|
||||
case holdsKept(p, keep):
|
||||
if err := prune(tree, p, keep); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
if err := os.RemoveAll(filepath.Join(tree, filepath.FromSlash(p))); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// holdsKept says whether a kept path lies below p.
|
||||
func holdsKept(p string, keep map[string]bool) bool {
|
||||
for k := range keep {
|
||||
if len(k) > len(p) && k[:len(p)+1] == p+"/" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/buildstep"
|
||||
)
|
||||
|
||||
func files(t *testing.T, root string) []string {
|
||||
t.Helper()
|
||||
var out []string
|
||||
filepath.WalkDir(root, func(p string, d os.DirEntry, err error) error {
|
||||
if err == nil && !d.IsDir() {
|
||||
rel, _ := filepath.Rel(root, p)
|
||||
if !strings.HasPrefix(rel, ".git/") {
|
||||
out = append(out, filepath.ToSlash(rel))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
slices.Sort(out)
|
||||
return out
|
||||
}
|
||||
|
||||
func narrowFixture(t *testing.T) string {
|
||||
t.Helper()
|
||||
tree := t.TempDir()
|
||||
for _, f := range []string{".git/HEAD", "README.md", "module.json", "package.json", "package-lock.json", "tsconfig.json",
|
||||
"src/index.ts", "src/deep/a.ts", "test/a.test.ts", "go/go.mod", "go/x.go", "other/module.json", "srcx/b.ts"} {
|
||||
p := filepath.Join(tree, filepath.FromSlash(f))
|
||||
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(p, []byte(f), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
return tree
|
||||
}
|
||||
|
||||
// ADR 0499 §4: a request naming paths leaves only those, the manifest and the lock files (and .git); a path the commit
|
||||
// lacks fails naming it; a request naming none leaves the tree whole.
|
||||
func TestTheWorkspaceIsNarrowedToTheNamedPathsTheManifestAndTheLocks(t *testing.T) {
|
||||
tree := narrowFixture(t)
|
||||
r := buildstep.FetchRequest{Dir: ".", Paths: []string{"src", "test", "tsconfig.json"}}
|
||||
if err := narrow(tree, r); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := []string{"module.json", "package-lock.json", "package.json", "src/deep/a.ts", "src/index.ts", "test/a.test.ts", "tsconfig.json"}
|
||||
if got := files(t, tree); !slices.Equal(got, want) {
|
||||
t.Errorf("kept %v, want %v", got, want)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(tree, ".git", "HEAD")); err != nil {
|
||||
t.Errorf("the repository's .git went: %v", err)
|
||||
}
|
||||
tree = narrowFixture(t)
|
||||
if err := narrow(tree, buildstep.FetchRequest{Dir: "go", Paths: []string{"x.go"}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := files(t, tree); !slices.Equal(got, []string{"go/go.mod", "go/x.go"}) {
|
||||
t.Errorf("a module in a directory kept %v", got)
|
||||
}
|
||||
tree = narrowFixture(t)
|
||||
if err := narrow(tree, buildstep.FetchRequest{Dir: ".", Paths: []string{"lib"}}); err == nil || !strings.Contains(err.Error(), "lib") {
|
||||
t.Errorf("a path the commit lacks answered %v", err)
|
||||
}
|
||||
tree = narrowFixture(t)
|
||||
before := files(t, tree)
|
||||
if err := narrow(tree, buildstep.FetchRequest{}); err != nil || !slices.Equal(files(t, tree), before) {
|
||||
t.Errorf("a request naming no paths changed the tree: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,527 @@
|
||||
package main
|
||||
|
||||
// A TypeScript package (novox/hq ADR 0498 §5, ADR 0500 §4-§6; issue 1587, the lead's decision L10): built from its own
|
||||
// lock's devDependencies (the toolchain's SDK is never used to build the SDK itself), packed with npm pack, and, for a
|
||||
// group, served to its TypeScript members' fetch in place of the registry's version. A member is built against the SDK
|
||||
// version its own lock names: the fetch places that version in the member's node_modules, from the group's packed
|
||||
// package when the group carries that version, else from the registry; a version neither holds waits for its
|
||||
// publication (`waits for:`). The install of a package's devDependencies is the fetch's (it holds the network and runs
|
||||
// no script); its build and pack run shut off like every run step.
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/buildstep"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/forgekey"
|
||||
)
|
||||
|
||||
// maxPackage is the largest packed package read: far above the SDK's.
|
||||
const maxPackage = 64 << 20
|
||||
|
||||
// packed is a package built and packed: its name and version from its package.json, the packed file's bytes, its npm
|
||||
// integrity (sha512, as a lock names it) and its digest (sha256, as a done key names it).
|
||||
type packed struct {
|
||||
Name, Version, Integrity, Digest string
|
||||
tgz []byte
|
||||
}
|
||||
|
||||
// integrityOf is a packed file's integrity as npm writes it in a lock: sha512- and the base64 of its sha512.
|
||||
func integrityOf(b []byte) string {
|
||||
sum := sha512.Sum512(b)
|
||||
return "sha512-" + base64.StdEncoding.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
func digestOf(b []byte) string {
|
||||
sum := sha256.Sum256(b)
|
||||
return "sha256:" + hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
// packageJSON is the part of a package.json the package steps read.
|
||||
type packageJSON struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Scripts map[string]string `json:"scripts"`
|
||||
}
|
||||
|
||||
func readPackageJSON(dir string) (packageJSON, error) {
|
||||
var p packageJSON
|
||||
raw, err := os.ReadFile(filepath.Join(dir, "package.json"))
|
||||
if err != nil {
|
||||
return p, fmt.Errorf("the package has no package.json: %w", err)
|
||||
}
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
return p, fmt.Errorf("the package.json is not JSON: %w", err)
|
||||
}
|
||||
if p.Name == "" || p.Version == "" {
|
||||
return p, fmt.Errorf("the package.json names no name or no version")
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// installDev installs a package's own dependencies, its devDependencies included, from its lock (the fetch: network,
|
||||
// no script, no bin link), and writes a shim for each program they carry into <root>/bin-<name>, files and not links,
|
||||
// so its build script finds them by name.
|
||||
func (m *Module) installDev(ctx context.Context, root, dir, rel string) error {
|
||||
if _, err := os.Stat(filepath.Join(dir, "package-lock.json")); err != nil {
|
||||
return fmt.Errorf("the package in %s has no package-lock.json: commit one", rel)
|
||||
}
|
||||
npmrc, err := m.npmrcIn(root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(npmrc)
|
||||
env := append(m.baseEnv(), "npm_config_cache="+m.npmCache())
|
||||
r := m.Run(ctx, env, "npm", "ci", "--ignore-scripts", "--no-bin-links", "--no-audit", "--no-fund", "--prefix", dir, "--userconfig", npmrc)
|
||||
if why := ranFailed(r); why != "" {
|
||||
return fmt.Errorf("installing the dependencies of the package in %s: %s", rel, why)
|
||||
}
|
||||
return writeShims(filepath.Join(dir, "node_modules"), shimDir(root, rel))
|
||||
}
|
||||
|
||||
// npmrcIn writes the npm configuration for the mesh's registry, with its credential, for one command of this process;
|
||||
// the caller removes it. It lies in this process's temporary directory, never in a workspace: a run step is given the
|
||||
// workspace and a /tmp of its own, so no step's mounts hold the credential (ADR 0498 §5, ADR 0500 §4).
|
||||
func (m *Module) npmrcIn(root string) (string, error) {
|
||||
if m.Toolchain == nil || m.Toolchain.Registry == nil {
|
||||
return "", fmt.Errorf("the module has no package registry")
|
||||
}
|
||||
reg, err := m.Toolchain.Registry()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
f, err := os.CreateTemp("", ".npmrc-")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
if _, err := f.WriteString(reg.Npmrc()); err != nil {
|
||||
os.Remove(f.Name())
|
||||
return "", err
|
||||
}
|
||||
return f.Name(), nil
|
||||
}
|
||||
|
||||
// shimDir is where the shims of the package in rel are written.
|
||||
func shimDir(root, rel string) string {
|
||||
return filepath.Join(root, "bin-"+strings.NewReplacer("/", "-", ".", "root").Replace(rel))
|
||||
}
|
||||
|
||||
// writeShims writes, for every program a top-level package of nm names in its package.json `bin`, a shell script that
|
||||
// runs it by its real path.
|
||||
func writeShims(nm, into string) error {
|
||||
if err := os.MkdirAll(into, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
var dirs []string
|
||||
entries, _ := os.ReadDir(nm)
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() || strings.HasPrefix(e.Name(), ".") {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(e.Name(), "@") {
|
||||
scoped, _ := os.ReadDir(filepath.Join(nm, e.Name()))
|
||||
for _, s := range scoped {
|
||||
if s.IsDir() {
|
||||
dirs = append(dirs, filepath.Join(nm, e.Name(), s.Name()))
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
dirs = append(dirs, filepath.Join(nm, e.Name()))
|
||||
}
|
||||
sort.Strings(dirs)
|
||||
for _, d := range dirs {
|
||||
raw, err := os.ReadFile(filepath.Join(d, "package.json"))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
var p struct {
|
||||
Name string `json:"name"`
|
||||
Bin json.RawMessage `json:"bin"`
|
||||
}
|
||||
if json.Unmarshal(raw, &p) != nil || len(p.Bin) == 0 {
|
||||
continue
|
||||
}
|
||||
bins := map[string]string{}
|
||||
var one string
|
||||
if json.Unmarshal(p.Bin, &one) == nil {
|
||||
bins[path.Base(p.Name)] = one
|
||||
} else if json.Unmarshal(p.Bin, &bins) != nil {
|
||||
continue
|
||||
}
|
||||
for name, target := range bins {
|
||||
if name == "" || strings.ContainsAny(name, "/\\") {
|
||||
continue
|
||||
}
|
||||
body := "#!/bin/sh\nexec node '" + filepath.Join(d, filepath.FromSlash(target)) + "' \"$@\"\n"
|
||||
p := filepath.Join(into, name)
|
||||
if err := os.WriteFile(p, []byte(body), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Chmod(p, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildAndPack builds the package in dir with its own build script, if it has one, and packs it with npm pack, both
|
||||
// shut off (write: a trunk build's, which alone writes the shared cache). The packed file is read and removed: it never
|
||||
// stays on the node.
|
||||
func (m *Module) buildAndPack(ctx context.Context, root, dir, rel string, write bool) (packed, error) {
|
||||
p, err := readPackageJSON(dir)
|
||||
if err != nil {
|
||||
return packed{}, err
|
||||
}
|
||||
env := []string{"PATH=" + shimDir(root, rel) + ":" + pathOrDefault(), "npm_config_cache=" + m.npmCache()}
|
||||
if p.Scripts["build"] != "" {
|
||||
if why := ranFailed(m.runIn(ctx, root, write, dir, env, "npm", "run", "build")); why != "" {
|
||||
return packed{}, fmt.Errorf("the build of the package %s: %s", p.Name, why)
|
||||
}
|
||||
}
|
||||
out, err := os.MkdirTemp(root, "packed-")
|
||||
if err != nil {
|
||||
return packed{}, err
|
||||
}
|
||||
defer os.RemoveAll(out)
|
||||
if why := ranFailed(m.runIn(ctx, root, write, dir, env, "npm", "pack", "--ignore-scripts", "--pack-destination", out)); why != "" {
|
||||
return packed{}, fmt.Errorf("packing the package %s: %s", p.Name, why)
|
||||
}
|
||||
files, _ := filepath.Glob(filepath.Join(out, "*.tgz"))
|
||||
if len(files) != 1 {
|
||||
return packed{}, fmt.Errorf("npm pack of %s left %d packed files, want one", p.Name, len(files))
|
||||
}
|
||||
tgz, err := os.ReadFile(files[0])
|
||||
if err != nil {
|
||||
return packed{}, err
|
||||
}
|
||||
return packed{Name: p.Name, Version: p.Version, Integrity: integrityOf(tgz), Digest: digestOf(tgz), tgz: tgz}, nil
|
||||
}
|
||||
|
||||
// groupPackage is the fetch's `package` argument: the group's package member (ADR 0500 §5), which the fetch packs and
|
||||
// serves to its TypeScript members in place of the registry's version.
|
||||
type groupPackage struct{ Repository, Commit, Ref string }
|
||||
|
||||
func groupPackageArg(v any) (*groupPackage, error) {
|
||||
if v == nil {
|
||||
return nil, nil
|
||||
}
|
||||
o, ok := v.(map[string]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("package is {repository, commit, ref}")
|
||||
}
|
||||
g := &groupPackage{}
|
||||
g.Repository, _ = o["repository"].(string)
|
||||
g.Commit, _ = o["commit"].(string)
|
||||
g.Ref, _ = o["ref"].(string)
|
||||
if !repositoryOK.MatchString(g.Repository) || strings.Contains(g.Repository, "..") {
|
||||
return nil, fmt.Errorf("package: %q is not a repository, <owner>/<name>", g.Repository)
|
||||
}
|
||||
if !commitOK.MatchString(g.Commit) {
|
||||
return nil, fmt.Errorf("package: %q is not a full commit (40 hex digits)", g.Commit)
|
||||
}
|
||||
if g.Ref != "" && !strings.HasPrefix(g.Ref, "refs/pull/") {
|
||||
return nil, fmt.Errorf("package: %q is not a pull request's head", g.Ref)
|
||||
}
|
||||
return g, nil
|
||||
}
|
||||
|
||||
// packGroup fetches the group's package member into the workspace, installs its dependencies, builds and packs it,
|
||||
// publishing nothing (ADR 0500 §4).
|
||||
func (m *Module) packGroup(ctx context.Context, root string, run forgekey.Runner, g *groupPackage) (packed, error) {
|
||||
dir := filepath.Join(root, "package", "tree")
|
||||
if err := os.MkdirAll(dir, 0o700); err != nil {
|
||||
return packed{}, err
|
||||
}
|
||||
if err := fetchCommit(ctx, run, dir, m.remote(g.Repository), g.Commit, g.Ref, m); err != nil {
|
||||
return packed{}, fmt.Errorf("the group's package %s: %w", g.Repository, err)
|
||||
}
|
||||
if err := m.installDev(ctx, root, dir, "package"); err != nil {
|
||||
return packed{}, err
|
||||
}
|
||||
return m.buildAndPack(ctx, root, dir, "package", false)
|
||||
}
|
||||
|
||||
// sdkLocked is what a module's lock names of the SDK: its version, its integrity and where it was resolved from; ok is
|
||||
// false when the lock names none (the toolchain's SDK is then the module's, as before).
|
||||
func sdkLocked(dir string) (version, integrity string, ok bool, err error) {
|
||||
raw, err := os.ReadFile(filepath.Join(dir, "package-lock.json"))
|
||||
if os.IsNotExist(err) {
|
||||
return "", "", false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return "", "", false, err
|
||||
}
|
||||
var lock struct {
|
||||
Packages map[string]struct {
|
||||
Version string `json:"version"`
|
||||
Integrity string `json:"integrity"`
|
||||
} `json:"packages"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &lock); err != nil {
|
||||
return "", "", false, fmt.Errorf("the package-lock.json is not JSON: %w", err)
|
||||
}
|
||||
p, ok := lock.Packages["node_modules/"+sdkPackage]
|
||||
if !ok || p.Version == "" {
|
||||
return "", "", false, nil
|
||||
}
|
||||
return p.Version, p.Integrity, true, nil
|
||||
}
|
||||
|
||||
// placeSDK places in the module's node_modules the SDK version its lock names (the lead's decision L10): the group's
|
||||
// packed package when it is that version (its integrity must be the lock's), else the registry's; a version neither
|
||||
// holds is a wait (`waits for:`) when no group package was given, and a failure naming both when one was.
|
||||
func (m *Module) placeSDK(ctx context.Context, dir, rel string, group *packed) error {
|
||||
version, integrity, ok, err := sdkLocked(dir)
|
||||
if err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
want := sdkPackage + "@" + version
|
||||
var tgz []byte
|
||||
switch {
|
||||
case group != nil && group.Name == sdkPackage && group.Version == version:
|
||||
if integrity != "" && integrity != group.Integrity {
|
||||
return fmt.Errorf("the lock of %s names %s with the integrity %s, and the group's package packs to %s", rel, want, integrity, group.Integrity)
|
||||
}
|
||||
tgz = group.tgz
|
||||
default:
|
||||
got, held, err := m.registryPackage(ctx, sdkPackage, version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !held {
|
||||
if group != nil {
|
||||
return fmt.Errorf("the lock of %s names %s, which neither the registry nor the group (%s@%s) holds", rel, want, group.Name, group.Version)
|
||||
}
|
||||
return fmt.Errorf("%s%s", delivery.WaitsFor, want)
|
||||
}
|
||||
if integrity != "" && integrityOf(got) != integrity {
|
||||
return fmt.Errorf("the registry's %s is %s, and the lock of %s names %s", want, integrityOf(got), rel, integrity)
|
||||
}
|
||||
tgz = got
|
||||
}
|
||||
into := filepath.Join(dir, "node_modules", filepath.FromSlash(sdkPackage))
|
||||
if err := os.RemoveAll(into); err != nil {
|
||||
return err
|
||||
}
|
||||
return unpackPackage(tgz, into)
|
||||
}
|
||||
|
||||
// httpClient is the client of the registry's reads and publishes: HTTP when a test set it.
|
||||
func (m *Module) httpClient() *http.Client {
|
||||
if m.HTTP != nil {
|
||||
return m.HTTP
|
||||
}
|
||||
return &http.Client{Timeout: 5 * time.Minute}
|
||||
}
|
||||
|
||||
// packument is the part of the registry's document of a package the steps read: each version's packed file.
|
||||
type packument struct {
|
||||
Versions map[string]struct {
|
||||
Dist struct {
|
||||
Tarball string `json:"tarball"`
|
||||
Integrity string `json:"integrity"`
|
||||
} `json:"dist"`
|
||||
} `json:"versions"`
|
||||
}
|
||||
|
||||
// registryVersion reads what the registry holds of name@version: its packed file's address and integrity, and whether
|
||||
// it holds it at all.
|
||||
func (m *Module) registryVersion(ctx context.Context, name, version string) (tarball, integrity string, held bool, err error) {
|
||||
reg, err := m.Toolchain.Registry()
|
||||
if err != nil {
|
||||
return "", "", false, err
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reg.URL+url.PathEscape(name), nil)
|
||||
if err != nil {
|
||||
return "", "", false, err
|
||||
}
|
||||
reg.authorize(req)
|
||||
resp, err := m.httpClient().Do(req)
|
||||
if err != nil {
|
||||
return "", "", false, fmt.Errorf("asking the registry for %s: %w", name, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return "", "", false, nil
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
said, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
|
||||
return "", "", false, fmt.Errorf("the registry answered %s for %s: %s", resp.Status, name, strings.TrimSpace(string(said)))
|
||||
}
|
||||
var p packument
|
||||
if err := json.NewDecoder(io.LimitReader(resp.Body, 64<<20)).Decode(&p); err != nil {
|
||||
return "", "", false, fmt.Errorf("the registry's document of %s: %w", name, err)
|
||||
}
|
||||
v, ok := p.Versions[version]
|
||||
if !ok {
|
||||
return "", "", false, nil
|
||||
}
|
||||
return v.Dist.Tarball, v.Dist.Integrity, true, nil
|
||||
}
|
||||
|
||||
// registryPackage downloads name@version's packed file from the registry; held is false when the registry has none.
|
||||
func (m *Module) registryPackage(ctx context.Context, name, version string) ([]byte, bool, error) {
|
||||
tarball, _, held, err := m.registryVersion(ctx, name, version)
|
||||
if err != nil || !held {
|
||||
return nil, held, err
|
||||
}
|
||||
reg, err := m.Toolchain.Registry()
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, tarball, nil)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
reg.authorize(req)
|
||||
resp, err := m.httpClient().Do(req)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("downloading %s@%s: %w", name, version, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, false, fmt.Errorf("the registry answered %s for %s@%s's packed file", resp.Status, name, version)
|
||||
}
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, maxPackage+1))
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if len(body) > maxPackage {
|
||||
return nil, false, fmt.Errorf("%s@%s is larger than %d bytes", name, version, maxPackage)
|
||||
}
|
||||
return body, true, nil
|
||||
}
|
||||
|
||||
// unpackPackage unpacks a packed npm package (a gzipped tar whose entries lie under package/) into dir: files and
|
||||
// directories only, nothing outside.
|
||||
func unpackPackage(tgz []byte, dir string) error {
|
||||
zr, err := gzip.NewReader(bytes.NewReader(tgz))
|
||||
if err != nil {
|
||||
return fmt.Errorf("the packed package is not gzip: %w", err)
|
||||
}
|
||||
tr := tar.NewReader(zr)
|
||||
for {
|
||||
h, err := tr.Next()
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
name := path.Clean(strings.TrimPrefix(h.Name, "./"))
|
||||
rel, ok := strings.CutPrefix(name, "package/")
|
||||
if !ok || rel == "" || path.IsAbs(rel) || rel == ".." || strings.HasPrefix(rel, "../") {
|
||||
if name == "package" {
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("the packed package holds %s, outside package/", h.Name)
|
||||
}
|
||||
target := filepath.Join(dir, filepath.FromSlash(rel))
|
||||
switch h.Typeflag {
|
||||
case tar.TypeDir:
|
||||
if err := os.MkdirAll(target, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
case tar.TypeReg:
|
||||
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
body, err := io.ReadAll(io.LimitReader(tr, maxPackage))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(target, body, os.FileMode(h.Mode).Perm()|0o600); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("the packed package holds %s, neither a file nor a directory", h.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fetchCommit fetches one commit into dir as the fetch step fetches the module's own (buildstep.FetchCommit).
|
||||
func fetchCommit(ctx context.Context, run forgekey.Runner, dir, remote, commit, ref string, m *Module) error {
|
||||
return buildstep.FetchCommit(ctx, run, dir, remote, commit, ref, m.FetchOptions...)
|
||||
}
|
||||
|
||||
// buildPackage builds and packs a package artifact (ADR 0498 §5): its digest is the packed file's. Only a trunk build
|
||||
// (publish) publishes it, from this module's own process with the registry's write credential, which no run step is
|
||||
// given: a version the registry lacks is published; the same packed file already there is done without a publish;
|
||||
// another packed file under that version fails `version taken:`. The reference is <name>@<version>, answered only when
|
||||
// the registry holds this build's file; a candidate answers its digest alone.
|
||||
func (m *Module) buildPackage(ctx context.Context, workspace, module, root, dir string, a artifact, publish bool) (any, error) {
|
||||
rel := module
|
||||
if rel == "" {
|
||||
rel = "."
|
||||
}
|
||||
p, err := m.buildAndPack(ctx, root, dir, rel, publish)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res := BuildResult{Workspace: workspace, Module: module, Artifact: a.Name, Digest: p.Digest, Bytes: len(p.tgz)}
|
||||
if publish {
|
||||
if res.Reference, err = m.publish(ctx, root, p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// publish puts a packed package into the registry unless it holds that version, and answers <name>@<version>.
|
||||
func (m *Module) publish(ctx context.Context, root string, p packed) (string, error) {
|
||||
ref := p.Name + "@" + p.Version
|
||||
_, integrity, held, err := m.registryVersion(ctx, p.Name, p.Version)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if held {
|
||||
if integrity == p.Integrity {
|
||||
return ref, nil // this very file, published before: done without a publish
|
||||
}
|
||||
return "", fmt.Errorf("%s%s: the registry holds it as %s, and this build packs %s", delivery.VersionTaken, ref, integrity, p.Integrity)
|
||||
}
|
||||
file, err := os.CreateTemp(root, "publish-*.tgz")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer os.Remove(file.Name()) // the packed file leaves the node once its publish answered
|
||||
if _, err := file.Write(p.tgz); err != nil {
|
||||
file.Close()
|
||||
return "", err
|
||||
}
|
||||
if err := file.Close(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
npmrc, err := m.npmrcIn(root)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer os.Remove(npmrc)
|
||||
env := append(m.baseEnv(), "npm_config_cache="+m.npmCache())
|
||||
r := m.Run(ctx, env, "npm", "publish", file.Name(), "--ignore-scripts", "--userconfig", npmrc)
|
||||
if why := ranFailed(r); why != "" {
|
||||
return "", fmt.Errorf("publishing %s: %s", ref, why)
|
||||
}
|
||||
return ref, nil
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
//go:build integration
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/buildstep"
|
||||
"git.novox.be/novox/mesh-sdk/go/forgekey"
|
||||
)
|
||||
|
||||
// sdkFixture is a package named as the SDK at version 9.9.9 with no dependency, whose build script writes dist/: what
|
||||
// the group's package member is to its TypeScript members.
|
||||
var sdkFixture = map[string]string{
|
||||
"module.json": `{"module":"mesh-sdk","version":"1","build":{"artifacts":[{"name":"lib","kind":"package","language":"typescript"}]}}`,
|
||||
"package.json": `{"name":"@novox/mesh-sdk","version":"9.9.9","type":"module","files":["dist"],"scripts":{"build":"node build.mjs"}}`,
|
||||
"package-lock.json": `{"name":"@novox/mesh-sdk","version":"9.9.9","lockfileVersion":3,"requires":true,"packages":{"":{"name":"@novox/mesh-sdk","version":"9.9.9"}}}`,
|
||||
"build.mjs": "import fs from 'node:fs';\nfs.mkdirSync('dist', {recursive: true});\nfs.writeFileSync('dist/index.js', 'export const v = \"9.9.9\";\\n');\n",
|
||||
}
|
||||
|
||||
// forgeRepo makes a bare repository o/<name> under root/forge holding files at one commit, and answers the commit.
|
||||
func forgeRepo(t *testing.T, root, name string, files map[string]string) string {
|
||||
t.Helper()
|
||||
bare, work := filepath.Join(root, "forge", "o", name+".git"), filepath.Join(root, "work-"+name)
|
||||
os.MkdirAll(bare, 0o755)
|
||||
os.MkdirAll(work, 0o755)
|
||||
git(t, bare, "init", "-q", "--bare")
|
||||
git(t, work, "init", "-q", "-b", "main")
|
||||
for n, c := range files {
|
||||
write(t, filepath.Join(work, filepath.FromSlash(n)), c, 0o644)
|
||||
}
|
||||
git(t, work, "add", ".")
|
||||
git(t, work, "commit", "-q", "-m", "c")
|
||||
git(t, work, "push", "-q", bare, "HEAD:refs/heads/main")
|
||||
return git(t, work, "rev-parse", "HEAD")
|
||||
}
|
||||
|
||||
// ADR 0500 §5 with the lead's decision L10, on real git, npm and bubblewrap: the fetch packs the group's package member
|
||||
// (its build script and npm pack shut off, nothing published), and a member whose lock names that version with that
|
||||
// integrity gets it in its node_modules; a member whose lock names it with another integrity fails naming both.
|
||||
func TestTheFetchPacksTheGroupsPackageAndServesItToAMember(t *testing.T) {
|
||||
for _, tool := range []string{"node", "npm", "git", "bwrap"} {
|
||||
if _, err := exec.LookPath(tool); err != nil {
|
||||
t.Skipf("NOT RUN: no %s where this check runs; every build node has it", tool)
|
||||
}
|
||||
}
|
||||
root := t.TempDir()
|
||||
sdkCommit := forgeRepo(t, root, "sdk", sdkFixture)
|
||||
m, _, _ := newMachine(t)
|
||||
m.Run = forgekey.ExecRunner
|
||||
m.RemoteBase = "file://" + filepath.Join(root, "forge") + "/"
|
||||
m.FetchOptions = []buildstep.Option{buildstep.AllowFileRemote()}
|
||||
fakeRegistry(t, m, nil) // the registry holds no 9.9.9: only the group can serve it
|
||||
// What the group's member packs to, as the member's lock would name it: packed once here the way the fetch packs it.
|
||||
probe, err := m.fetch(context.Background(), map[string]any{"repository": "o/sdk", "commit": sdkCommit,
|
||||
"package": map[string]any{"repository": "o/sdk", "commit": sdkCommit}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
group := strings.Fields(probe.(FetchResult).Group)
|
||||
if len(group) != 2 || group[0] != "@novox/mesh-sdk@9.9.9" || !strings.HasPrefix(group[1], "sha512-") {
|
||||
t.Fatalf("the fetch answered the group %q", probe.(FetchResult).Group)
|
||||
}
|
||||
member := func(integrity string) string {
|
||||
lock, _ := json.Marshal(map[string]any{"name": "m", "lockfileVersion": 3, "requires": true, "packages": map[string]any{
|
||||
"": map[string]any{"name": "m", "dependencies": map[string]any{"@novox/mesh-sdk": "9.9.9"}},
|
||||
"node_modules/@novox/mesh-sdk": map[string]any{"version": "9.9.9", "integrity": integrity}}})
|
||||
files := typeScriptModule(`{"name":"m","dependencies":{"@novox/mesh-sdk":"9.9.9"}}`)
|
||||
files["modules/m/package-lock.json"] = string(lock)
|
||||
return forgeRepo(t, root, "m-"+integrity[len(integrity)-6:len(integrity)-2], files)
|
||||
}
|
||||
commit := member(group[1])
|
||||
name := "m-" + group[1][len(group[1])-6:len(group[1])-2]
|
||||
out, err := m.fetch(context.Background(), map[string]any{"repository": "o/" + name, "commit": commit,
|
||||
"package": map[string]any{"repository": "o/sdk", "commit": sdkCommit}})
|
||||
if err != nil {
|
||||
t.Fatalf("a member locking the group's version: %v", err)
|
||||
}
|
||||
placed := filepath.Join(m.Work, out.(FetchResult).Workspace, "tree", "modules", "m", "node_modules", "@novox", "mesh-sdk")
|
||||
if raw, err := os.ReadFile(filepath.Join(placed, "dist", "index.js")); err != nil || !strings.Contains(string(raw), "9.9.9") {
|
||||
t.Errorf("the member's SDK is not the group's built package: %q %v", raw, err)
|
||||
}
|
||||
other := member("sha512-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==")
|
||||
_, err = m.fetch(context.Background(), map[string]any{"repository": "o/m-AAAA", "commit": other,
|
||||
"package": map[string]any{"repository": "o/sdk", "commit": sdkCommit}})
|
||||
if err == nil || !strings.Contains(err.Error(), group[1]) {
|
||||
t.Errorf("a member locking another integrity answered %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ADR 0499 §4 (issue 1758): a fetch asked with the module's directory and its named paths leaves only those, the
|
||||
// manifest and the lock files; another module of the commit is not listed or installed.
|
||||
func TestAFetchWithNamedPathsNarrowsTheWorkspace(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
files := typeScriptModule(`{"name":"m"}`)
|
||||
files["modules/m/tools/index.ts"] = "export {}\n"
|
||||
files["modules/m/docs/a.md"] = "not named\n"
|
||||
files["modules/other/module.json"] = files["modules/m/module.json"]
|
||||
files["README.md"] = "the repository\n"
|
||||
commit := forgeRepo(t, root, "narrow", files)
|
||||
m, _, _ := newMachine(t)
|
||||
m.Run = forgekey.ExecRunner
|
||||
m.RemoteBase = "file://" + filepath.Join(root, "forge") + "/"
|
||||
m.FetchOptions = []buildstep.Option{buildstep.AllowFileRemote()}
|
||||
out, err := m.fetch(context.Background(), map[string]any{"repository": "o/narrow", "commit": commit, "dir": "modules/m", "paths": []any{"tools"}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r := out.(FetchResult)
|
||||
tree := filepath.Join(m.Work, r.Workspace, "tree")
|
||||
for _, gone := range []string{"README.md", "modules/m/docs/a.md", "modules/other/module.json"} {
|
||||
if _, err := os.Stat(filepath.Join(tree, gone)); err == nil {
|
||||
t.Errorf("%s is still in the workspace", gone)
|
||||
}
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(tree, "modules/m/tools/index.ts")); err != nil || strings.Join(r.Modules, ",") != "modules/m" {
|
||||
t.Errorf("the named path or the module went: %v, modules %v", err, r.Modules)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
// tgzOf is a packed npm package holding package/package.json of name@version and one more file.
|
||||
func tgzOf(t *testing.T, name, version string) []byte {
|
||||
t.Helper()
|
||||
var buf bytes.Buffer
|
||||
zw := gzip.NewWriter(&buf)
|
||||
tw := tar.NewWriter(zw)
|
||||
for _, f := range [][2]string{{"package/package.json", `{"name":"` + name + `","version":"` + version + `"}`}, {"package/dist/index.js", "export const v = '" + version + "';\n"}} {
|
||||
n, body := f[0], f[1]
|
||||
tw.WriteHeader(&tar.Header{Name: n, Mode: 0o644, Size: int64(len(body)), Typeflag: tar.TypeReg})
|
||||
tw.Write([]byte(body))
|
||||
}
|
||||
tw.Close()
|
||||
zw.Close()
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// fakeRegistry serves the packument and the packed files of the versions it holds; asked counts its answers.
|
||||
func fakeRegistry(t *testing.T, m *Module, held map[string][]byte) *atomic.Int32 {
|
||||
t.Helper()
|
||||
var asked atomic.Int32
|
||||
var srv *httptest.Server
|
||||
srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
asked.Add(1)
|
||||
switch {
|
||||
case r.URL.Path == "/npm/@novox%2fmesh-sdk" || r.URL.Path == "/npm/@novox/mesh-sdk":
|
||||
if len(held) == 0 {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
versions := map[string]any{}
|
||||
for v, tgz := range held {
|
||||
versions[v] = map[string]any{"dist": map[string]any{"tarball": srv.URL + "/files/" + v + ".tgz", "integrity": integrityOf(tgz)}}
|
||||
}
|
||||
json.NewEncoder(w).Encode(map[string]any{"versions": versions})
|
||||
case strings.HasPrefix(r.URL.Path, "/files/"):
|
||||
v := strings.TrimSuffix(strings.TrimPrefix(r.URL.Path, "/files/"), ".tgz")
|
||||
if tgz, ok := held[v]; ok {
|
||||
w.Write(tgz)
|
||||
return
|
||||
}
|
||||
http.NotFound(w, r)
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
m.Toolchain.Registry = func() (Registry, error) { return Registry{URL: srv.URL + "/npm/"}, nil }
|
||||
m.HTTP = srv.Client()
|
||||
return &asked
|
||||
}
|
||||
|
||||
// memberLocking is a member's directory whose lock names @novox/mesh-sdk@version with integrity (none when version is "").
|
||||
func memberLocking(t *testing.T, version, integrity string) string {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
lock := map[string]any{"lockfileVersion": 3, "packages": map[string]any{"": map[string]any{"name": "m"}}}
|
||||
if version != "" {
|
||||
lock["packages"].(map[string]any)["node_modules/@novox/mesh-sdk"] = map[string]any{"version": version, "integrity": integrity}
|
||||
}
|
||||
raw, _ := json.Marshal(lock)
|
||||
write(t, filepath.Join(dir, "package-lock.json"), string(raw), 0o644)
|
||||
return dir
|
||||
}
|
||||
|
||||
func placedVersion(dir string) string {
|
||||
raw, err := os.ReadFile(filepath.Join(dir, "node_modules", "@novox", "mesh-sdk", "package.json"))
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
var p packageJSON
|
||||
json.Unmarshal(raw, &p)
|
||||
return p.Version
|
||||
}
|
||||
|
||||
// The lead's decision L10 (ADR 0500 §6): a member is built against the SDK version its own lock names, downloaded from
|
||||
// the registry and checked against the lock's integrity; a version the registry does not hold is a wait, `waits for:`.
|
||||
func TestAMemberGetsTheSDKVersionItsLockNamesOrWaitsForIt(t *testing.T) {
|
||||
m, _, _ := newMachine(t)
|
||||
held := tgzOf(t, sdkPackage, "0.1.15")
|
||||
fakeRegistry(t, m, map[string][]byte{"0.1.15": held})
|
||||
dir := memberLocking(t, "0.1.15", integrityOf(held))
|
||||
if err := m.placeSDK(context.Background(), dir, "modules/m", nil); err != nil || placedVersion(dir) != "0.1.15" {
|
||||
t.Errorf("the registry's 0.1.15: %v, placed %q", err, placedVersion(dir))
|
||||
}
|
||||
dir = memberLocking(t, "0.1.16", "sha512-x")
|
||||
err := m.placeSDK(context.Background(), dir, "modules/m", nil)
|
||||
if err == nil || err.Error() != delivery.WaitsFor+"@novox/mesh-sdk@0.1.16" {
|
||||
t.Errorf("a version the registry lacks answered %v, want %s@novox/mesh-sdk@0.1.16", err, delivery.WaitsFor)
|
||||
}
|
||||
dir = memberLocking(t, "0.1.15", "sha512-another")
|
||||
if err := m.placeSDK(context.Background(), dir, "modules/m", nil); err == nil || !strings.Contains(err.Error(), "sha512-another") {
|
||||
t.Errorf("a registry file of another integrity than the lock's answered %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ADR 0500 §5 (S3, S4): the group's packed package is served in place of the registry's when the member's lock names its
|
||||
// version with its integrity; another integrity, or a version neither the group nor the registry holds, fails naming both.
|
||||
// A member whose lock names no SDK keeps the toolchain's and asks the registry nothing.
|
||||
func TestTheGroupsPackageIsServedToAMemberLockingItsVersion(t *testing.T) {
|
||||
m, _, _ := newMachine(t)
|
||||
asked := fakeRegistry(t, m, map[string][]byte{"0.1.14": tgzOf(t, sdkPackage, "0.1.14")})
|
||||
tgz := tgzOf(t, sdkPackage, "0.1.15")
|
||||
group := &packed{Name: sdkPackage, Version: "0.1.15", Integrity: integrityOf(tgz), Digest: digestOf(tgz), tgz: tgz}
|
||||
dir := memberLocking(t, "0.1.15", group.Integrity)
|
||||
if err := m.placeSDK(context.Background(), dir, "modules/m", group); err != nil || placedVersion(dir) != "0.1.15" || asked.Load() != 0 {
|
||||
t.Errorf("the group's own version: %v, placed %q, the registry asked %d times", err, placedVersion(dir), asked.Load())
|
||||
}
|
||||
dir = memberLocking(t, "0.1.15", "sha512-other")
|
||||
if err := m.placeSDK(context.Background(), dir, "modules/m", group); err == nil || !strings.Contains(err.Error(), "sha512-other") || !strings.Contains(err.Error(), group.Integrity) {
|
||||
t.Errorf("another integrity answered %v, want both named", err)
|
||||
}
|
||||
dir = memberLocking(t, "0.1.14", integrityOf(tgzOf(t, sdkPackage, "0.1.14")))
|
||||
if err := m.placeSDK(context.Background(), dir, "modules/m", group); err != nil || placedVersion(dir) != "0.1.14" {
|
||||
t.Errorf("a member locking the registry's 0.1.14 in a group: %v, placed %q", err, placedVersion(dir))
|
||||
}
|
||||
dir = memberLocking(t, "0.1.13", "sha512-x")
|
||||
if err := m.placeSDK(context.Background(), dir, "modules/m", group); err == nil || !strings.Contains(err.Error(), "0.1.13") || !strings.Contains(err.Error(), "0.1.15") || strings.HasPrefix(err.Error(), delivery.WaitsFor) {
|
||||
t.Errorf("a version neither holds answered %v, want both named and no wait", err)
|
||||
}
|
||||
before := asked.Load()
|
||||
dir = memberLocking(t, "", "")
|
||||
if err := m.placeSDK(context.Background(), dir, "modules/m", group); err != nil || placedVersion(dir) != "" || asked.Load() != before {
|
||||
t.Errorf("a lock naming no SDK: %v, placed %q, the registry asked", err, placedVersion(dir))
|
||||
}
|
||||
}
|
||||
|
||||
// The fetch's `package` argument is a repository and a full commit, with a pull request's head at most; anything else is
|
||||
// refused before anything is fetched.
|
||||
func TestTheGroupPackageArgumentIsRefusedWhenItIsNotOne(t *testing.T) {
|
||||
for _, bad := range []any{"novox/mesh-sdk", map[string]any{"repository": "novox/mesh-sdk", "commit": "main"},
|
||||
map[string]any{"repository": "../x", "commit": strings.Repeat("a", 40)},
|
||||
map[string]any{"repository": "novox/mesh-sdk", "commit": strings.Repeat("a", 40), "ref": "refs/heads/main"}} {
|
||||
if _, err := groupPackageArg(bad); err == nil {
|
||||
t.Errorf("%v was taken", bad)
|
||||
}
|
||||
}
|
||||
g, err := groupPackageArg(map[string]any{"repository": "novox/mesh-sdk", "commit": strings.Repeat("a", 40), "ref": "refs/pull/9/head"})
|
||||
if err != nil || g.Ref != "refs/pull/9/head" {
|
||||
t.Errorf("a pull request's head: %+v %v", g, err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
const packageManifest = `{"module":"mesh-sdk","version":"1","build":{"artifacts":[{"name":"lib","kind":"package","language":"typescript","paths":["src","package.json"]}]}}`
|
||||
|
||||
// packageMachine is a machine whose workspace w1 holds the package @novox/mesh-sdk@0.1.15 at its root; npm pack writes
|
||||
// the packed file tgz into its --pack-destination, as npm does.
|
||||
func packageMachine(t *testing.T, tgz []byte) (*Module, *fakeMachine, string) {
|
||||
t.Helper()
|
||||
m, f, work := newMachine(t)
|
||||
workspaceWith(t, work, map[string]string{"module.json": packageManifest, "src/index.ts": "export const v = 1;\n",
|
||||
"package.json": `{"name":"@novox/mesh-sdk","version":"0.1.15","scripts":{"build":"tsc"}}`, "package-lock.json": `{"lockfileVersion":3}`})
|
||||
f.effect = func(c call) {
|
||||
if c.name != "npm" || len(c.args) == 0 || c.args[0] != "pack" {
|
||||
return
|
||||
}
|
||||
for i, a := range c.args {
|
||||
if a == "--pack-destination" {
|
||||
write(t, filepath.Join(c.args[i+1], "novox-mesh-sdk-0.1.15.tgz"), string(tgz), 0o644)
|
||||
}
|
||||
}
|
||||
}
|
||||
return m, f, work
|
||||
}
|
||||
|
||||
func publishes(f *fakeMachine) []call {
|
||||
var out []call
|
||||
for _, c := range f.calls {
|
||||
if c.name == "npm" && len(c.args) > 0 && c.args[0] == "publish" {
|
||||
out = append(out, c)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// 1587-S15..S19 (ADR 0498 §5): a package is built by its own build script and packed, shut off; a candidate answers the
|
||||
// packed file's digest, publishes nothing and asks the registry nothing; its reference is empty.
|
||||
func TestACandidatesPackageIsBuiltAndPackedAndNeverPublished(t *testing.T) {
|
||||
tgz := tgzOf(t, sdkPackage, "0.1.15")
|
||||
m, f, _ := packageMachine(t, tgz)
|
||||
asked := fakeRegistry(t, m, nil)
|
||||
out, err := m.build(context.Background(), map[string]any{"workspace": "w1", "module": ".", "artifact": "lib"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r := out.(BuildResult)
|
||||
if r.Digest != digestOf(tgz) || r.Reference != "" || r.Bytes != len(tgz) {
|
||||
t.Errorf("result %+v, want the packed file's digest and no reference", r)
|
||||
}
|
||||
if len(publishes(f)) != 0 || asked.Load() != 0 {
|
||||
t.Errorf("a candidate published %d times and asked the registry %d times", len(publishes(f)), asked.Load())
|
||||
}
|
||||
var ran []string
|
||||
for _, s := range f.shut {
|
||||
ran = append(ran, s.command)
|
||||
}
|
||||
if len(f.shut) != 2 || !strings.Contains(ran[0], "npm run build") || !strings.Contains(ran[1], "npm pack --ignore-scripts") {
|
||||
t.Errorf("shut off %v, want the build and the pack", ran)
|
||||
}
|
||||
}
|
||||
|
||||
// ADR 0498 §5: a trunk build (publish) publishes a version the registry lacks and answers <name>@<version>; the same
|
||||
// packed file already there is done without a publish; another file under that version fails `version taken:`. The
|
||||
// packed file is gone from the node afterwards.
|
||||
func TestATrunkPackageIsPublishedOnceAndAVersionTakenIsRefused(t *testing.T) {
|
||||
tgz := tgzOf(t, sdkPackage, "0.1.15")
|
||||
args := map[string]any{"workspace": "w1", "module": ".", "artifact": "lib", "publish": true}
|
||||
m, f, work := packageMachine(t, tgz)
|
||||
fakeRegistry(t, m, nil)
|
||||
out, err := m.build(context.Background(), args)
|
||||
if err != nil || out.(BuildResult).Reference != "@novox/mesh-sdk@0.1.15" || len(publishes(f)) != 1 {
|
||||
t.Fatalf("an absent version: %+v %v, %d publishes", out, err, len(publishes(f)))
|
||||
}
|
||||
if p := publishes(f)[0]; !slices.Contains(p.args, "--ignore-scripts") || !strings.HasSuffix(p.args[1], ".tgz") {
|
||||
t.Errorf("published with %v", p.args)
|
||||
}
|
||||
// ADR 0500 §4: the registry's credential is never in a workspace, which a run step is given.
|
||||
for i, a := range publishes(f)[0].args {
|
||||
if a == "--userconfig" && strings.HasPrefix(publishes(f)[0].args[i+1], work) {
|
||||
t.Errorf("the credential was written into the workspace: %s", publishes(f)[0].args[i+1])
|
||||
}
|
||||
}
|
||||
var left []string
|
||||
filepath.WalkDir(work, func(p string, d os.DirEntry, err error) error {
|
||||
if err == nil && strings.HasSuffix(p, ".tgz") || strings.Contains(p, ".npmrc") {
|
||||
left = append(left, p)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if len(left) != 0 {
|
||||
t.Errorf("left on the node: %v", left)
|
||||
}
|
||||
|
||||
m, f, _ = packageMachine(t, tgz)
|
||||
fakeRegistry(t, m, map[string][]byte{"0.1.15": tgz})
|
||||
if out, err := m.build(context.Background(), args); err != nil || out.(BuildResult).Reference != "@novox/mesh-sdk@0.1.15" || len(publishes(f)) != 0 {
|
||||
t.Errorf("the same file published before: %+v %v, %d publishes", out, err, len(publishes(f)))
|
||||
}
|
||||
|
||||
m, f, _ = packageMachine(t, tgz)
|
||||
fakeRegistry(t, m, map[string][]byte{"0.1.15": tgzOf(t, sdkPackage, "another")})
|
||||
_, err = m.build(context.Background(), args)
|
||||
if err == nil || !strings.HasPrefix(err.Error(), delivery.VersionTaken+"@novox/mesh-sdk@0.1.15") || len(publishes(f)) != 0 {
|
||||
t.Errorf("another file under the version: %v, %d publishes", err, len(publishes(f)))
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
@@ -79,3 +80,10 @@ func (r Registry) Npmrc() string {
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// authorize gives a request to the registry the account's credential, when there is one.
|
||||
func (r Registry) authorize(req *http.Request) {
|
||||
if r.Username != "" && r.Password != "" {
|
||||
req.SetBasicAuth(r.Username, r.Password)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
],
|
||||
"contributes": {
|
||||
"npm-package-registry": {
|
||||
"access": "read"
|
||||
"access": "write"
|
||||
}
|
||||
},
|
||||
"binds": {
|
||||
|
||||
Reference in New Issue
Block a user