mesh-delivery-sdk: module.json with its one package artifact; trunk, buildrecord and forgekey (the builder kind, the clean runner, Said) move in from mesh-sdk (D-6, D-9, ADR 0634 §3, ADR 0635 §4; issue 1966)
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
package buildrecord
|
||||
|
||||
import "time"
|
||||
|
||||
// BuildRecord is the layout the ended controller's chain wrote (mesh-controller internal/chain/records.go Build, line 91),
|
||||
// the fields a reader outside it read (novox/hq ADR 0437 §4 §5, issue 1223). The controller's record carries more; a decode ignores
|
||||
// what is not here. Path is the module's directory in its repository, as the controller writes it (Build.Path, records.go:100,
|
||||
// set from the build request trimmed of slashes at internal/inventory/chain_requests.go:19); empty for a module whose
|
||||
// manifest is the repository's root. SourceInputs has no json keys of its own: it is written under its Go field names, as the controller
|
||||
// writes it.
|
||||
type BuildRecord struct {
|
||||
ID string `json:"id"`
|
||||
State string `json:"state,omitempty"`
|
||||
Failed string `json:"failed,omitempty"`
|
||||
Repository string `json:"repository"`
|
||||
Ref string `json:"ref,omitempty"`
|
||||
Module string `json:"module,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
Commit string `json:"commit,omitempty"`
|
||||
At time.Time `json:"at,omitzero"`
|
||||
SourceFingerprint string `json:"source-fingerprint,omitempty"`
|
||||
SourceInputs *SourceInputs `json:"source-inputs,omitempty"`
|
||||
// Made is what the build made (Build.Made, records.go:110); the digest is the tail of an artifact's Reference.
|
||||
Made []Artifact `json:"made,omitempty"`
|
||||
}
|
||||
|
||||
// Artifact is one thing a build made: mesh-controller internal/chain/records.go Artifact, json keys name, kind and
|
||||
// reference, nothing else (novox/hq ADR 0478 clause 8).
|
||||
type Artifact struct {
|
||||
Name string `json:"name"`
|
||||
Kind string `json:"kind"`
|
||||
Reference string `json:"reference"`
|
||||
}
|
||||
|
||||
// BuildKey is a build's record under its module, a build that knew none under the request's key: copied from
|
||||
// mesh-controller internal/chain/keys.go:35-40 (BuildKey, with RequestKey at line 42).
|
||||
func BuildKey(module, id string) string {
|
||||
if module == "" {
|
||||
return "req." + id
|
||||
}
|
||||
return module + ".build." + id
|
||||
}
|
||||
|
||||
// NewestKey is the key of a module's newest build, of any state: copied from mesh-controller internal/chain/keys.go:44.
|
||||
// The key holds no record: its value is the newest build's id as raw bytes (internal/inventory/chain_builds.go:210 and
|
||||
// :546), and the record lives at BuildKey(module, id). The pointer is a hint, checked against that record.
|
||||
func NewestKey(module string) string { return module + ".newest" }
|
||||
|
||||
// SourceInputs are what the ended controller's source fingerprint (src1) was of, as its build records hold them
|
||||
// (BuildRecord), read and never computed again (novox/hq ADR 0437 §5): the module, the git tree of
|
||||
// its directory, its bases (references, of which only the digest counts), the root tree of each repository its
|
||||
// artifacts read (by artifact), each artifact's toolchain line, and the reason the source does not pin the build
|
||||
// when it does not (a build that resolves packages when built). A context is the value the builder records for it:
|
||||
// a repository's root tree, or for an image that compiles a Go program the `narrow:` hash of the build source it was
|
||||
// handed (mesh-controller internal/builder/builder.go:1146-1156).
|
||||
type SourceInputs struct {
|
||||
Module string
|
||||
Tree string
|
||||
Bases []string
|
||||
Contexts map[string]string
|
||||
Toolchains map[string]string
|
||||
Unpinned string
|
||||
}
|
||||
|
||||
// IsTrunkBuild is whether the record is a successful build of the trunk that recorded its source inputs: an outcome
|
||||
// (the controller writes no state on one; state asked is a request, records.go:57-59), not failed, and ref "main" or none. A build a delivery plan asked after a merge, or rebuild or build --behind, follows the branch
|
||||
// without naming it, so it carries no ref; a build of any other branch or of a commit names it (issue 1281).
|
||||
func (r BuildRecord) IsTrunkBuild() bool {
|
||||
return r.State == "" && r.Failed == "" && r.SourceInputs != nil && (r.Ref == "" || r.Ref == "main")
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
package buildrecord
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// The fixture's path is what the controller writes for a build request: strings.Trim(path, "/") at mesh-controller
|
||||
// internal/inventory/chain_requests.go:19, the module's directory in its repository (Build.Path, internal/chain/records.go:100).
|
||||
// The fixture started as a record the controller's own encoder wrote (json.Marshal of a chain.Build, mesh-controller
|
||||
// internal/chain, a throwaway test run once and deleted) with build-agent's build-1792395447088936520 values, plus a
|
||||
// manifest-digest and one made artifact (shape: mesh-controller internal/chain/records.go:91-117 Build, json tags, at commit
|
||||
// 1f708e2d8d72e2c27dbeaba02f14c128d700eada; an outcome has no state key and a build asked after a merge no ref, records.go:57-59), which a BuildRecord does not carry. It was then edited by hand
|
||||
// to the controller's layout for an outcome: state and ref removed, the repository a clone URL (issue 1281).
|
||||
func loadBuildFixture(t *testing.T) []byte {
|
||||
t.Helper()
|
||||
raw, err := os.ReadFile("testdata/build-record-build-agent.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
func TestBuildRecordDecodes(t *testing.T) { // 1223/AC1
|
||||
var r BuildRecord
|
||||
if err := json.Unmarshal(loadBuildFixture(t), &r); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, c := range []struct{ name, got, want string }{
|
||||
{"id", r.ID, "build-1792395447088936520"},
|
||||
{"module", r.Module, "build-agent"},
|
||||
{"repository", r.Repository, "http://novox.internal:20000/novox/mesh-controller.git"},
|
||||
{"ref", r.Ref, ""},
|
||||
{"commit", r.Commit, "7a74cee83f88e6585a5511c3cc53bdf0f5303d20"},
|
||||
{"state", r.State, ""},
|
||||
{"failed", r.Failed, ""},
|
||||
{"source-fingerprint", r.SourceFingerprint, "src1:e846efe73716b78e80860baaac96153de2cb76551325753838ca245d2160a3d7"},
|
||||
} {
|
||||
if c.got != c.want {
|
||||
t.Errorf("%s = %q, want %q", c.name, c.got, c.want)
|
||||
}
|
||||
}
|
||||
if want := time.Date(2026, 10, 19, 9, 38, 0, 0, time.UTC); !r.At.Equal(want) {
|
||||
t.Errorf("at = %v, want %v", r.At, want)
|
||||
}
|
||||
if !r.IsTrunkBuild() { // 1281/AC7
|
||||
t.Error("the fixture, a record as the controller writes it, is not a trunk build")
|
||||
}
|
||||
if r.SourceInputs == nil {
|
||||
t.Fatal("source-inputs missing")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildRecordPath(t *testing.T) { // 1233/AC1
|
||||
var r BuildRecord
|
||||
if err := json.Unmarshal(loadBuildFixture(t), &r); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if r.Path != "modules/build-agent" {
|
||||
t.Errorf("path = %q, want modules/build-agent", r.Path)
|
||||
}
|
||||
// mesh-controller's manifest is its repository's root: the controller writes no path (omitempty), so the path is "".
|
||||
var m map[string]json.RawMessage
|
||||
if err := json.Unmarshal(loadBuildFixture(t), &m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
delete(m, "path")
|
||||
raw, _ := json.Marshal(m)
|
||||
var root BuildRecord
|
||||
if err := json.Unmarshal(raw, &root); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if root.Path != "" {
|
||||
t.Errorf("path of a root module = %q, want empty", root.Path)
|
||||
}
|
||||
// Every field 1223/AC1 checks is still equal in both.
|
||||
if root.ID != r.ID || root.Module != r.Module || root.Repository != r.Repository || root.Ref != r.Ref ||
|
||||
root.Commit != r.Commit || root.State != r.State || root.SourceFingerprint != r.SourceFingerprint || !root.At.Equal(r.At) {
|
||||
t.Errorf("fields differ: %+v vs %+v", root, r)
|
||||
}
|
||||
// And the path survives a round trip through the SDK's own type.
|
||||
back, _ := json.Marshal(r)
|
||||
var again BuildRecord
|
||||
if err := json.Unmarshal(back, &again); err != nil || again.Path != r.Path {
|
||||
t.Errorf("round trip path = %q (%v)", again.Path, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildRecordWithoutInputs(t *testing.T) { // 1223/AC2
|
||||
var m map[string]json.RawMessage
|
||||
if err := json.Unmarshal(loadBuildFixture(t), &m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
delete(m, "source-inputs")
|
||||
raw, _ := json.Marshal(m)
|
||||
var r BuildRecord
|
||||
if err := json.Unmarshal(raw, &r); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if r.SourceInputs != nil {
|
||||
t.Errorf("source-inputs = %+v, want nil", r.SourceInputs)
|
||||
}
|
||||
if r.Module != "build-agent" {
|
||||
t.Errorf("module = %q, want build-agent", r.Module)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildKeys(t *testing.T) { // 1223/AC3
|
||||
// Literal values of mesh-controller internal/broker/controller_buckets.go:101 (BucketName(ControllerSeat, ChainBuilds)
|
||||
// is "mesh-controller_builds") and internal/chain/keys.go:35-44.
|
||||
if got := BuildKey("build-agent", "build-1"); got != "build-agent.build.build-1" {
|
||||
t.Errorf("BuildKey = %q", got)
|
||||
}
|
||||
if got := BuildKey("", "build-1"); got != "req.build-1" {
|
||||
t.Errorf("BuildKey without a module = %q", got)
|
||||
}
|
||||
if got := NewestKey("build-agent"); got != "build-agent.newest" {
|
||||
t.Errorf("NewestKey = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// The fixture's made artifacts are the controller's Artifact (mesh-controller internal/chain/records.go, json tags name,
|
||||
// kind, reference; the controller writes no digest key, the digest is the tail of the reference). The kinds and references
|
||||
// are those of the live record mesh-controller.builds printed for build-agent build-1792540091397811913 (2026-10-21 01:49).
|
||||
func TestBuildRecordMade(t *testing.T) { // 1588/AC2
|
||||
var r BuildRecord
|
||||
if err := json.Unmarshal(loadBuildFixture(t), &r); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := []Artifact{
|
||||
{Name: "server", Kind: "image", Reference: "artifact-store://build-agent/server@sha256:a79c13f1c727ff94959e80824cc704ea58b80d80ecdd00fd447698acebb35090"},
|
||||
{Name: "files", Kind: "archive", Reference: "artifact-store://build-agent/files/blobs/sha256:bccd3f47851436d1b45a3063cc2d7e34c5c109b2d030739bfbb4b9781703f9a9"},
|
||||
}
|
||||
if len(r.Made) != len(want) {
|
||||
t.Fatalf("made = %+v, want %+v", r.Made, want)
|
||||
}
|
||||
for i := range want {
|
||||
if r.Made[i] != want[i] {
|
||||
t.Errorf("made[%d] = %+v, want %+v", i, r.Made[i], want[i])
|
||||
}
|
||||
}
|
||||
var none BuildRecord
|
||||
if err := json.Unmarshal([]byte(`{"id":"b","repository":"a/b"}`), &none); err != nil || none.Made != nil {
|
||||
t.Errorf("a record without made: %+v, %v", none, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsTrunkBuild(t *testing.T) { // 1281/AC2
|
||||
inputs := &SourceInputs{Module: "zsh", Tree: "t"}
|
||||
for _, c := range []struct {
|
||||
name string
|
||||
r BuildRecord
|
||||
want bool
|
||||
}{
|
||||
{"built on main", BuildRecord{Ref: "main", SourceInputs: inputs}, true},
|
||||
// a build a delivery plan asked after a merge, or rebuild / build --behind, carries no ref (live builds,
|
||||
// 2026-10-19: zsh build-1792417485811374131, mesh-delivery build-1792435539606203626)
|
||||
{"built with no ref", BuildRecord{SourceInputs: inputs}, true},
|
||||
{"another branch", BuildRecord{Ref: "feat/x", SourceInputs: inputs}, false},
|
||||
{"a commit", BuildRecord{Ref: "9efdd87a", SourceInputs: inputs}, false},
|
||||
{"asked", BuildRecord{State: "asked", SourceInputs: inputs}, false},
|
||||
{"answered, state answered, no source inputs", BuildRecord{State: "answered"}, false},
|
||||
{"failed", BuildRecord{Failed: "boom", Ref: "main", SourceInputs: inputs}, false},
|
||||
{"no source inputs", BuildRecord{Ref: "main"}, false},
|
||||
{"no ref and no source inputs", BuildRecord{}, false},
|
||||
} {
|
||||
if got := c.r.IsTrunkBuild(); got != c.want {
|
||||
t.Errorf("%s: IsTrunkBuild = %v, want %v", c.name, got, c.want)
|
||||
}
|
||||
}
|
||||
// decoded as the controller writes it: the key "ref" is absent
|
||||
var r BuildRecord
|
||||
raw := `{"id":"build-1792417485811374131","repository":"http://novox.internal:20000/novox/mesh-catalog.git","module":"zsh","source-inputs":{"Module":"zsh","Tree":"t"}}`
|
||||
if err := json.Unmarshal([]byte(raw), &r); err != nil || !r.IsTrunkBuild() {
|
||||
t.Errorf("a decoded record with no ref: trunk = %v, err %v", r.IsTrunkBuild(), err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"id":"build-1792395447088936520","repository":"http://novox.internal:20000/novox/mesh-controller.git","module":"build-agent","commit":"7a74cee83f88e6585a5511c3cc53bdf0f5303d20","path":"modules/build-agent","manifest-digest":"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef","source-fingerprint":"src1:e846efe73716b78e80860baaac96153de2cb76551325753838ca245d2160a3d7","source-inputs":{"Module":"build-agent","Tree":"11c0b4f90cae0d6e4ef9cc48cbfdf9f6dbe52bca","Bases":["sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc","sha256:8ac98ca534ac3f51e1f420a1dd2c15e74c75cfa0f23f3ad27eb5d7236c349a0c"],"Contexts":{"server":"narrow:d51ca744fd11af59d616c858f76c27b3e58e16e11fbdc0c720eb9d7d19d55fac"},"Toolchains":null,"Unpinned":""},"made":[{"name":"server","kind":"image","reference":"artifact-store://build-agent/server@sha256:a79c13f1c727ff94959e80824cc704ea58b80d80ecdd00fd447698acebb35090"},{"name":"files","kind":"archive","reference":"artifact-store://build-agent/files/blobs/sha256:bccd3f47851436d1b45a3063cc2d7e34c5c109b2d030739bfbb4b9781703f9a9"}],"at":"2026-10-19T09:38:00Z"}
|
||||
@@ -0,0 +1,107 @@
|
||||
// Package forgekey holds the Delivery domain's names beside mesh-sdk's forgekey (novox/hq ADR 0635 §4): the kind of
|
||||
// key a build module keeps, the runner whose commands get exactly the environment they are given (a fetch holds no
|
||||
// registry password), and the words a failed command is reported in. mesh-sdk's forgekey keeps what every module's
|
||||
// key keeper shares: KeyKeeper, KeyPutter, Runner, ExecRunner and Ran.
|
||||
package forgekey
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
sdk "git.novox.be/novox/mesh-sdk/go/forgekey"
|
||||
)
|
||||
|
||||
// KindBuilder is the kind of key a build module's account keeps (ADR 0466 §2); sdk.KindMachine is a machine's own.
|
||||
const KindBuilder = "builder"
|
||||
|
||||
// ExecRunnerClean is a runner whose commands get exactly the env argument as their environment, and nothing of this
|
||||
// process's (issue 1497; novox/hq ADR 0466 §4: no registry password in a fetch's environment). The caller gives PATH
|
||||
// and HOME itself. A directory is the command's own business (`git -C`, `go -C`): it is started in the HOME it is
|
||||
// given, or the machine's temporary directory, never in this process's. bound 0 leaves only the step's own context.
|
||||
func ExecRunnerClean(bound time.Duration) sdk.Runner {
|
||||
return func(ctx context.Context, env []string, name string, args ...string) sdk.Ran {
|
||||
if bound > 0 {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithTimeout(ctx, bound)
|
||||
defer cancel()
|
||||
}
|
||||
cmd := exec.CommandContext(ctx, name, args...)
|
||||
cmd.Env = append([]string{}, env...) // non-nil, so an empty list is an empty environment
|
||||
cmd.Dir = homeOrTemp(env)
|
||||
var out, errb bytes.Buffer
|
||||
cmd.Stdout, cmd.Stderr = &out, &errb
|
||||
err := cmd.Run()
|
||||
r := sdk.Ran{Stdout: out.String(), Stderr: errb.String()}
|
||||
var exitErr *exec.ExitError
|
||||
switch {
|
||||
case errors.Is(ctx.Err(), context.DeadlineExceeded):
|
||||
r.Status, r.Err = 124, "ended by its step's deadline"
|
||||
if bound > 0 {
|
||||
r.Err = fmt.Sprintf("no answer within %d s", int(bound/time.Second))
|
||||
}
|
||||
case errors.Is(err, exec.ErrNotFound):
|
||||
r.Status, r.Err = 127, "ENOENT"
|
||||
case errors.As(err, &exitErr):
|
||||
r.Status = exitErr.ExitCode()
|
||||
case err != nil:
|
||||
r.Status, r.Err = 1, err.Error()
|
||||
}
|
||||
return r
|
||||
}
|
||||
}
|
||||
|
||||
// homeOrTemp is the directory a clean command starts in: the HOME it is given, or the temporary directory (novox/hq
|
||||
// issue 734: never one that may be deleted under it, as a module's build directory is when a new build replaces it).
|
||||
func homeOrTemp(env []string) string {
|
||||
for i := len(env) - 1; i >= 0; i-- {
|
||||
if home, ok := strings.CutPrefix(env[i], "HOME="); ok {
|
||||
if info, err := os.Stat(home); err == nil && info.IsDir() {
|
||||
return home
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return os.TempDir()
|
||||
}
|
||||
|
||||
// SaidBound bounds what Said keeps of a command's words, so a reason stays a line of a step's record and of a log.
|
||||
const SaidBound = 600
|
||||
|
||||
// Said is what a command said of its failure in its own words, for a reason: every line that says something, trimmed
|
||||
// and joined with "; ", bounded at SaidBound bytes. A line that is a bare prefix and nothing else (git writes "error:"
|
||||
// alone before and after the forge's own message, "remote:" or "fatal:" likewise) is left out, so the cause is never
|
||||
// cut off behind it.
|
||||
func Said(s string) string {
|
||||
var keep []string
|
||||
for _, l := range strings.Split(s, "\n") {
|
||||
l = strings.TrimSpace(l)
|
||||
if l == "" || barePrefix(l) {
|
||||
continue
|
||||
}
|
||||
keep = append(keep, l)
|
||||
}
|
||||
out := strings.Join(keep, "; ")
|
||||
if len(out) > SaidBound {
|
||||
cut := SaidBound
|
||||
for cut > 0 && !utf8.RuneStart(out[cut]) {
|
||||
cut--
|
||||
}
|
||||
out = out[:cut] + " …"
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// barePrefix says whether a line is one word and a colon, such as "error:" or "remote:", with nothing after it.
|
||||
func barePrefix(l string) bool {
|
||||
if !strings.HasSuffix(l, ":") {
|
||||
return false
|
||||
}
|
||||
return !strings.ContainsAny(strings.TrimSuffix(l, ":"), " \t:")
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package forgekey
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
sdk "git.novox.be/novox/mesh-sdk/go/forgekey"
|
||||
)
|
||||
|
||||
func TestSaidKeepsTheCauseBehindABarePrefix(t *testing.T) {
|
||||
forge := "error: \nerror: User: 22:nox-builder is not authorized to read novox/x.\nerror: \nfatal: Could not read from remote repository.\n\nPlease make sure you have the correct access rights\n"
|
||||
for _, c := range []struct{ name, in, want string }{
|
||||
{"forge refusal", forge, "error: User: 22:nox-builder is not authorized to read novox/x.; fatal: Could not read from remote repository.; Please make sure you have the correct access rights"},
|
||||
{"one line", "fatal: couldn't find remote ref x\n", "fatal: couldn't find remote ref x"},
|
||||
{"bare prefixes only", "error:\nremote:\n", ""},
|
||||
{"a colon inside stays", " ENOENT \nfatal: bad: thing\n", "ENOENT; fatal: bad: thing"},
|
||||
{"empty", "", ""},
|
||||
} {
|
||||
if got := Said(c.in); got != c.want {
|
||||
t.Errorf("%s: Said = %q, want %q", c.name, got, c.want)
|
||||
}
|
||||
}
|
||||
long := Said(strings.Repeat("é", SaidBound))
|
||||
if !strings.HasSuffix(long, " …") || len(long) > SaidBound+len(" …") || !strings.HasPrefix(long, "é") {
|
||||
t.Errorf("long: %d bytes, ends %q", len(long), long[len(long)-6:])
|
||||
}
|
||||
}
|
||||
|
||||
// ExecRunnerClean gives the command only the env argument, and starts it in the HOME it is given, even when this
|
||||
// process's directory was deleted under it (issue 1497, issue 734).
|
||||
func TestExecRunnerCleanGivesTheCommandOnlyTheEnvArgument(t *testing.T) {
|
||||
t.Setenv("MESH_TEST_SECRET", "x")
|
||||
var run sdk.Runner = ExecRunnerClean(0)
|
||||
r := run(context.Background(), []string{"PATH=" + os.Getenv("PATH"), "HOME=/h"}, "env")
|
||||
if r.Status != 0 || !strings.Contains(r.Stdout, "HOME=/h") || strings.Contains(r.Stdout, "MESH_TEST_SECRET") {
|
||||
t.Errorf("ran = %+v", r)
|
||||
}
|
||||
home, gone := t.TempDir(), t.TempDir()
|
||||
t.Chdir(gone)
|
||||
if err := os.Remove(gone); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r = run(context.Background(), []string{"PATH=" + os.Getenv("PATH"), "HOME=" + home}, "pwd")
|
||||
if r.Status != 0 || strings.TrimSpace(r.Stdout) != home {
|
||||
t.Errorf("a clean command run from a deleted directory = %+v, want it started in its HOME %s", r, home)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"module": "mesh-delivery-sdk",
|
||||
"version": "1",
|
||||
"build": {
|
||||
"artifacts": [
|
||||
{
|
||||
"name": "go-sdk",
|
||||
"kind": "package",
|
||||
"language": "go",
|
||||
"paths": [
|
||||
"."
|
||||
],
|
||||
"series": "0.1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"resources": []
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package trunk
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// A mesh package is a Go module of a repository of the mesh's forge (ForgeModulePrefix): mesh-sdk's Go module, and an
|
||||
// owner module's package (novox/hq ADR 0593 §1). The indexer alone reads which packages a module imports, their content
|
||||
// digests and their latest published versions, and writes them in its state (PublishedPackages, ADR 0593 §2); a build
|
||||
// writes the version it is asked into its own go.mod and go.sum (StepAsk.Packages).
|
||||
|
||||
// ForgeModulePrefix is the module path prefix of the Go modules the mesh's forge serves: a Go module whose path starts
|
||||
// with it is a mesh package, git.novox.be/novox/<repository>[/<directory of the module in it>].
|
||||
const ForgeModulePrefix = "git.novox.be/novox/"
|
||||
|
||||
// PackageRepository is the repository (<owner>/<name>) a mesh package is published from and the package's directory in
|
||||
// it ("" for its root); false for a path that is not a forge module's.
|
||||
func PackageRepository(pkg string) (repository, dir string, ok bool) {
|
||||
rest, found := strings.CutPrefix(pkg, ForgeModulePrefix)
|
||||
if !found || rest == "" {
|
||||
return "", "", false
|
||||
}
|
||||
name, dir, _ := strings.Cut(rest, "/")
|
||||
if name == "" {
|
||||
return "", "", false
|
||||
}
|
||||
return "novox/" + name, dir, true
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package trunk
|
||||
|
||||
import "strings"
|
||||
|
||||
// RepositoryName is the repository as the forge names it, "owner/name", from either spelling the mesh holds: the name
|
||||
// itself, or the clone URL a build record carries ("http://host:port/owner/name.git", with or without ".git" or a
|
||||
// trailing slash). A string of fewer than two path segments is returned as it is, trimmed (issue 1281).
|
||||
func RepositoryName(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
s = strings.TrimRight(s, "/")
|
||||
s = strings.TrimSuffix(s, ".git")
|
||||
s = strings.TrimRight(s, "/")
|
||||
parts := strings.FieldsFunc(s, func(r rune) bool { return r == '/' || r == ':' })
|
||||
if len(parts) < 2 {
|
||||
return s
|
||||
}
|
||||
return parts[len(parts)-2] + "/" + parts[len(parts)-1]
|
||||
}
|
||||
|
||||
// SameRepository is whether two spellings name one repository: the same owner and name, whichever way each is written
|
||||
// (RepositoryName), compared without regard to case as the forge does. An empty spelling names none (issue 1281).
|
||||
func SameRepository(a, b string) bool {
|
||||
a, b = RepositoryName(a), RepositoryName(b)
|
||||
return a != "" && strings.EqualFold(a, b)
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package trunk
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSameRepository(t *testing.T) { // 1281/AC1
|
||||
const forge = "novox/mesh-catalog"
|
||||
for _, c := range []struct {
|
||||
a, b string
|
||||
same bool
|
||||
}{
|
||||
// the clone URL as a build record carries it (mesh-controller.command builds, 2026-10-19 ~21:20 local)
|
||||
{forge, "http://novox.internal:20000/novox/mesh-catalog.git", true},
|
||||
{forge, "http://novox.internal:20000/novox/mesh-catalog", true},
|
||||
{forge, "http://novox.internal:20000/novox/mesh-catalog.git/", true},
|
||||
{"http://novox.internal:20000/novox/mesh-catalog.git/", forge, true},
|
||||
{forge, "novox/mesh-catalog", true},
|
||||
{forge, "Novox/Mesh-Catalog", true},
|
||||
{forge, "http://novox.internal:20000/novox/mesh-catalog-x.git", false},
|
||||
{forge, "novox/mesh-catalog-x", false},
|
||||
{forge, "http://novox.internal:20000/other/mesh-catalog.git", false},
|
||||
{forge, "other/mesh-catalog", false},
|
||||
{forge, "http://novox.internal:20000/novox/mesh-media-catalog.git", false},
|
||||
{forge, "", false},
|
||||
{"", "", false},
|
||||
} {
|
||||
if got := SameRepository(c.a, c.b); got != c.same {
|
||||
t.Errorf("SameRepository(%q, %q) = %v, want %v", c.a, c.b, got, c.same)
|
||||
}
|
||||
}
|
||||
if got := RepositoryName("http://novox.internal:20000/novox/mesh-catalog.git"); got != "novox/mesh-catalog" {
|
||||
t.Errorf("RepositoryName = %q", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user