Skip to content

Commit 2a2433c

Browse files
authored
Merge pull request #174 from barbatos2011/feat/build-pipeline-phase1
build pipeline Phase 1: `trond build --artifact jar` (stacked on #173 design)
2 parents 7aee00c + 52ded62 commit 2a2433c

35 files changed

Lines changed: 2982 additions & 19 deletions

Makefile

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ endif
4141

4242
GOFLAGS ?=
4343

44-
.PHONY: build test lint e2e build-all clean clean-all fmt vet tidy sync-templates sync-schemas snapshot-schema-baseline update-render-golden docs man cover vuln bootstrap-go
44+
.PHONY: build test lint e2e build-all clean clean-all fmt vet tidy sync-templates sync-schemas snapshot-schema-baseline update-render-golden refresh-builder-pins docs man cover vuln bootstrap-go
4545

4646
## bootstrap-go: Download + verify the project-local Go toolchain
4747
## (idempotent; safe to re-run; no-op if already current)
@@ -163,3 +163,10 @@ snapshot-schema-baseline: $(GO_BOOTSTRAP)
163163
update-render-golden: $(GO_BOOTSTRAP)
164164
TROND_UPDATE_GOLDEN=1 $(GO) test -run TestRenderHOCON_Golden ./internal/render/
165165
@echo "render golden files refreshed under internal/render/testdata/golden/."
166+
167+
## refresh-builder-pins: Re-resolve Eclipse Temurin tags → sha256 digests
168+
## and rewrite internal/build/pins/builder_image_digests.json.
169+
## Run at trond release-prep so the binary ships current
170+
## digests. Requires docker + jq on PATH. spec/002 FR-012.
171+
refresh-builder-pins:
172+
@./scripts/refresh-builder-pins.sh

cmd/build.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"os/signal"
7+
"path/filepath"
8+
"syscall"
9+
10+
"github.com/spf13/cobra"
11+
12+
"github.com/tronprotocol/tron-deployment/internal/build"
13+
"github.com/tronprotocol/tron-deployment/internal/output"
14+
)
15+
16+
// `trond build` produces a deployable java-tron artifact (JAR in
17+
// Phase 1; image lands in Phase 3) from a source tree, by running
18+
// gradle inside a pinned Eclipse Temurin container.
19+
//
20+
// Design + rationale: specs/002-trond-build-pipeline/{spec,plan}.md.
21+
//
22+
// Output schema: schemas/output/build.schema.json.
23+
24+
var (
25+
buildSourcePath string
26+
buildRevisionSpec string
27+
buildArtifactKind string
28+
buildJDKVersion string
29+
buildGradleTask string
30+
buildGradleArgs []string
31+
buildBuilder string
32+
buildImageTag string
33+
buildImageOverride string
34+
)
35+
36+
var buildCmd = &cobra.Command{
37+
Use: "build",
38+
Short: "Build a java-tron artifact (JAR or image) from source",
39+
Long: `Build runs gradle inside a pinned Eclipse Temurin container against
40+
the given java-tron source tree, producing either a fat JAR or a
41+
docker image. Results are content-addressed by git revision + builder
42+
image digest + task + args, so repeated invocations against the same
43+
inputs return immediately.
44+
45+
trond ships no JDK or Gradle. The builder image is pulled on first
46+
use and pinned via go:embed so the build is reproducible across
47+
trond installs of the same version.
48+
49+
Examples:
50+
51+
# Build the default fat JAR from the current branch HEAD.
52+
trond build --source ./java-tron --artifact jar -o json
53+
54+
# Build with an explicit revision and gradle flags.
55+
trond build --source ./java-tron --revision v4.7.7 \
56+
--gradle-arg=--offline --gradle-arg=-Dversion=mytest -o json
57+
58+
# Override the builder image (emergency: pinned digest unreachable).
59+
trond build --source ./java-tron \
60+
--builder-image-override eclipse-temurin:8-jdk@sha256:abcd...`,
61+
RunE: runBuild,
62+
}
63+
64+
func init() {
65+
buildCmd.Flags().StringVar(&buildSourcePath, "source", "",
66+
"Path to the java-tron source tree (required; relative to CWD)")
67+
buildCmd.Flags().StringVar(&buildRevisionSpec, "revision", "HEAD",
68+
"Git revision to build (HEAD, branch, tag, or sha)")
69+
buildCmd.Flags().StringVar(&buildArtifactKind, "artifact", "jar",
70+
"Artifact kind: 'jar' or 'image'")
71+
buildCmd.Flags().StringVar(&buildJDKVersion, "jdk", "8",
72+
"JDK version for the builder container (8|11|17|21)")
73+
buildCmd.Flags().StringVar(&buildGradleTask, "gradle-task", "",
74+
"Gradle task name (defaults: 'shadowJar' for jar, 'dockerBuild' for image)")
75+
buildCmd.Flags().StringArrayVar(&buildGradleArgs, "gradle-arg", nil,
76+
"Extra gradle args (repeatable; e.g. --gradle-arg=--offline). "+
77+
"Restricted to a flag-name allowlist; see spec FR-022.")
78+
buildCmd.Flags().StringVar(&buildBuilder, "builder", "docker",
79+
"Builder backend: 'docker' (default) or 'host' (uses local gradle)")
80+
buildCmd.Flags().StringVar(&buildImageTag, "tag", "",
81+
"Image tag to apply when --artifact=image (e.g. mytest:dev)")
82+
buildCmd.Flags().StringVar(&buildImageOverride, "builder-image-override", "",
83+
"Override the pinned builder image (escape hatch; see FR-024)")
84+
rootCmd.AddCommand(buildCmd)
85+
}
86+
87+
// runBuild wires CLI flags into a build.Request, installs the
88+
// signal-aware context for SIGINT propagation (FR-016), and emits
89+
// either the success Result or a structured error envelope.
90+
func runBuild(cmd *cobra.Command, _ []string) error {
91+
// FR-021: --source relative to CWD.
92+
resolvedSource := buildSourcePath
93+
if resolvedSource != "" && !filepath.IsAbs(resolvedSource) {
94+
abs, err := filepath.Abs(resolvedSource)
95+
if err == nil {
96+
resolvedSource = abs
97+
}
98+
}
99+
100+
req := build.Request{
101+
SourcePath: resolvedSource,
102+
RevisionSpec: buildRevisionSpec,
103+
JDKVersion: buildJDKVersion,
104+
ArtifactKind: buildArtifactKind,
105+
GradleTask: buildGradleTask,
106+
GradleArgs: buildGradleArgs,
107+
Builder: buildBuilder,
108+
ImageTag: buildImageTag,
109+
BuilderImageOverride: buildImageOverride,
110+
}
111+
112+
// SIGINT-aware context. Build container + git subprocesses all
113+
// run under this; cancellation propagates to subprocess kill.
114+
ctx, cancel := signal.NotifyContext(cmd.Context(), os.Interrupt, syscall.SIGTERM)
115+
defer cancel()
116+
117+
res, err := build.Run(ctx, req)
118+
if err != nil {
119+
// StructuredError propagates through cobra RunE; cmd.Execute
120+
// renders it and sets the exit code (see cmd/root.go::Execute).
121+
// Returning here lets `defer cancel()` run normally.
122+
return err
123+
}
124+
125+
outputFmt, _ := cmd.Flags().GetString("output")
126+
if outputFmt == "json" {
127+
return output.WriteJSON(os.Stdout, res)
128+
}
129+
if res.CacheHit {
130+
fmt.Printf("✓ cache hit: %s (%d ms)\n", res.CacheKey, res.DurationMs)
131+
} else {
132+
fmt.Printf("✓ built: %s\n → %s\n sha256: %s\n %d ms\n",
133+
res.CacheKey, res.ArtifactPath, res.SHA256, res.DurationMs)
134+
}
135+
return nil
136+
}

cmd/recipe_matrix_e2e_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ func TestE2E_Recipe_DryRunMatrix(t *testing.T) {
3838
expectSteps []string
3939
}{
4040
{
41-
recipe: "nile-test-fullnode",
42-
params: []string{"intent_path=" + intentPath},
41+
recipe: "nile-test-fullnode",
42+
params: []string{"intent_path=" + intentPath},
4343
expectSteps: []string{"validate", "preflight", "apply", "verify"},
4444
},
4545
{
@@ -52,13 +52,13 @@ func TestE2E_Recipe_DryRunMatrix(t *testing.T) {
5252
expectSteps: []string{"validate", "preflight"},
5353
},
5454
{
55-
recipe: "destroy-private-network-cleanly",
56-
params: []string{"network=private-dev"},
55+
recipe: "destroy-private-network-cleanly",
56+
params: []string{"network=private-dev"},
5757
expectSteps: []string{"status-check", "destroy"},
5858
},
5959
{
60-
recipe: "recover-failed-upgrade",
61-
params: []string{"node=my-fullnode"},
60+
recipe: "recover-failed-upgrade",
61+
params: []string{"node=my-fullnode"},
6262
expectSteps: []string{"diagnose", "rollback"},
6363
},
6464
{

cmd/schema_coverage_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ func TestSchemaCoverage(t *testing.T) {
7373
lookup := map[string]string{
7474
"trond apply": "apply",
7575
"trond auto-heal": "auto-heal",
76+
"trond build": "build",
7677
"trond config validate": "config-validate",
7778
"trond config render": "config-render",
7879
"trond config diff": "config-diff",

cmd/schema_manifest_e2e_test.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ func TestE2E_SchemaManifestReverse(t *testing.T) {
3333
out := runTrondCtx(ctx, t, env, "schema", "--output", "json")
3434

3535
var manifest struct {
36-
SchemaVersion string `json:"schema_version"`
37-
Tool string `json:"tool"`
36+
SchemaVersion string `json:"schema_version"`
37+
Tool string `json:"tool"`
3838
Commands []manifestCommand `json:"commands"`
3939
}
4040
if err := json.Unmarshal(out, &manifest); err != nil {
@@ -59,12 +59,12 @@ func TestE2E_SchemaManifestReverse(t *testing.T) {
5959
}
6060

6161
type manifestCommand struct {
62-
Name string `json:"name"`
63-
FullName string `json:"full_name"`
64-
Use string `json:"use"`
65-
Aliases []string `json:"aliases"`
66-
Flags []manifestFlag `json:"flags"`
67-
Subcommands []manifestCommand `json:"subcommands"`
62+
Name string `json:"name"`
63+
FullName string `json:"full_name"`
64+
Use string `json:"use"`
65+
Aliases []string `json:"aliases"`
66+
Flags []manifestFlag `json:"flags"`
67+
Subcommands []manifestCommand `json:"subcommands"`
6868
}
6969

7070
type manifestFlag struct {

cmd/statedir_e2e_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ func TestE2E_StateDirPriority(t *testing.T) {
3636
wantNot []string
3737
}{
3838
{
39-
name: "flag-beats-env",
40-
args: []string{"--state-dir", flagDir},
39+
name: "flag-beats-env",
40+
args: []string{"--state-dir", flagDir},
4141
extraEnv: []string{
4242
"TROND_STATE_DIR=" + envDir,
4343
},

internal/build/audit.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package build
2+
3+
import (
4+
"time"
5+
6+
"github.com/tronprotocol/tron-deployment/internal/security"
7+
)
8+
9+
// AuditPhase represents where in the build lifecycle we are. Per
10+
// FR-023 we append an `in_progress` event at start, then write a
11+
// terminal event (success / failed / cancelled) on completion. A
12+
// crashed mid-build leaves the `in_progress` entry visible to
13+
// `trond events`, surfacing the forensic signal.
14+
type AuditPhase string
15+
16+
const (
17+
PhaseInProgress AuditPhase = "in_progress"
18+
PhaseSuccess AuditPhase = "success"
19+
PhaseFailed AuditPhase = "failed"
20+
PhaseCancelled AuditPhase = "cancelled"
21+
)
22+
23+
// AppendAuditEvent writes one build-related row to the audit log.
24+
//
25+
// We deliberately reuse the existing security.AuditEntry shape (the
26+
// same envelope `apply`, `start`, `stop` use) so downstream tooling —
27+
// `trond events`, MCP resources, the JSON contract — doesn't need a
28+
// new code path. The build-specific fields ride in the existing
29+
// ErrorCode/Result columns.
30+
func AppendAuditEvent(phase AuditPhase, cacheKey, errorCode string, startedAt time.Time) error {
31+
log, err := security.NewAuditLog("")
32+
if err != nil {
33+
return err
34+
}
35+
entry := security.AuditEntry{
36+
Timestamp: time.Now().UTC(),
37+
Command: "build",
38+
Target: "local", // build target is always local in v1
39+
IntentHash: cacheKey,
40+
Result: string(phase),
41+
ErrorCode: errorCode,
42+
}
43+
if phase != PhaseInProgress {
44+
entry.DurationMs = time.Since(startedAt).Milliseconds()
45+
}
46+
return log.Write(entry)
47+
}

0 commit comments

Comments
 (0)