Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ endif

GOFLAGS ?=

.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
.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

## bootstrap-go: Download + verify the project-local Go toolchain
## (idempotent; safe to re-run; no-op if already current)
Expand Down Expand Up @@ -163,3 +163,10 @@ snapshot-schema-baseline: $(GO_BOOTSTRAP)
update-render-golden: $(GO_BOOTSTRAP)
TROND_UPDATE_GOLDEN=1 $(GO) test -run TestRenderHOCON_Golden ./internal/render/
@echo "render golden files refreshed under internal/render/testdata/golden/."

## refresh-builder-pins: Re-resolve Eclipse Temurin tags → sha256 digests
## and rewrite internal/build/pins/builder_image_digests.json.
## Run at trond release-prep so the binary ships current
## digests. Requires docker + jq on PATH. spec/002 FR-012.
refresh-builder-pins:
@./scripts/refresh-builder-pins.sh
136 changes: 136 additions & 0 deletions cmd/build.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package cmd

import (
"fmt"
"os"
"os/signal"
"path/filepath"
"syscall"

"github.com/spf13/cobra"

"github.com/tronprotocol/tron-deployment/internal/build"
"github.com/tronprotocol/tron-deployment/internal/output"
)

// `trond build` produces a deployable java-tron artifact (JAR in
// Phase 1; image lands in Phase 3) from a source tree, by running
// gradle inside a pinned Eclipse Temurin container.
//
// Design + rationale: specs/002-trond-build-pipeline/{spec,plan}.md.
//
// Output schema: schemas/output/build.schema.json.

var (
buildSourcePath string
buildRevisionSpec string
buildArtifactKind string
buildJDKVersion string
buildGradleTask string
buildGradleArgs []string
buildBuilder string
buildImageTag string
buildImageOverride string
)

var buildCmd = &cobra.Command{
Use: "build",
Short: "Build a java-tron artifact (JAR or image) from source",
Long: `Build runs gradle inside a pinned Eclipse Temurin container against
the given java-tron source tree, producing either a fat JAR or a
docker image. Results are content-addressed by git revision + builder
image digest + task + args, so repeated invocations against the same
inputs return immediately.

trond ships no JDK or Gradle. The builder image is pulled on first
use and pinned via go:embed so the build is reproducible across
trond installs of the same version.

Examples:

# Build the default fat JAR from the current branch HEAD.
trond build --source ./java-tron --artifact jar -o json

# Build with an explicit revision and gradle flags.
trond build --source ./java-tron --revision v4.7.7 \
--gradle-arg=--offline --gradle-arg=-Dversion=mytest -o json

# Override the builder image (emergency: pinned digest unreachable).
trond build --source ./java-tron \
--builder-image-override eclipse-temurin:8-jdk@sha256:abcd...`,
RunE: runBuild,
}

func init() {
buildCmd.Flags().StringVar(&buildSourcePath, "source", "",
"Path to the java-tron source tree (required; relative to CWD)")
buildCmd.Flags().StringVar(&buildRevisionSpec, "revision", "HEAD",
"Git revision to build (HEAD, branch, tag, or sha)")
buildCmd.Flags().StringVar(&buildArtifactKind, "artifact", "jar",
"Artifact kind: 'jar' or 'image'")
buildCmd.Flags().StringVar(&buildJDKVersion, "jdk", "8",
"JDK version for the builder container (8|11|17|21)")
buildCmd.Flags().StringVar(&buildGradleTask, "gradle-task", "",
"Gradle task name (defaults: 'shadowJar' for jar, 'dockerBuild' for image)")
buildCmd.Flags().StringArrayVar(&buildGradleArgs, "gradle-arg", nil,
"Extra gradle args (repeatable; e.g. --gradle-arg=--offline). "+
"Restricted to a flag-name allowlist; see spec FR-022.")
buildCmd.Flags().StringVar(&buildBuilder, "builder", "docker",
"Builder backend: 'docker' (default) or 'host' (uses local gradle)")
buildCmd.Flags().StringVar(&buildImageTag, "tag", "",
"Image tag to apply when --artifact=image (e.g. mytest:dev)")
buildCmd.Flags().StringVar(&buildImageOverride, "builder-image-override", "",
"Override the pinned builder image (escape hatch; see FR-024)")
rootCmd.AddCommand(buildCmd)
}

// runBuild wires CLI flags into a build.Request, installs the
// signal-aware context for SIGINT propagation (FR-016), and emits
// either the success Result or a structured error envelope.
func runBuild(cmd *cobra.Command, _ []string) error {
// FR-021: --source relative to CWD.
resolvedSource := buildSourcePath
if resolvedSource != "" && !filepath.IsAbs(resolvedSource) {
abs, err := filepath.Abs(resolvedSource)
if err == nil {
resolvedSource = abs
}
}

req := build.Request{
SourcePath: resolvedSource,
RevisionSpec: buildRevisionSpec,
JDKVersion: buildJDKVersion,
ArtifactKind: buildArtifactKind,
GradleTask: buildGradleTask,
GradleArgs: buildGradleArgs,
Builder: buildBuilder,
ImageTag: buildImageTag,
BuilderImageOverride: buildImageOverride,
}

// SIGINT-aware context. Build container + git subprocesses all
// run under this; cancellation propagates to subprocess kill.
ctx, cancel := signal.NotifyContext(cmd.Context(), os.Interrupt, syscall.SIGTERM)
defer cancel()

res, err := build.Run(ctx, req)
if err != nil {
// StructuredError propagates through cobra RunE; cmd.Execute
// renders it and sets the exit code (see cmd/root.go::Execute).
// Returning here lets `defer cancel()` run normally.
return err
}

outputFmt, _ := cmd.Flags().GetString("output")
if outputFmt == "json" {
return output.WriteJSON(os.Stdout, res)
}
if res.CacheHit {
fmt.Printf("✓ cache hit: %s (%d ms)\n", res.CacheKey, res.DurationMs)
} else {
fmt.Printf("✓ built: %s\n → %s\n sha256: %s\n %d ms\n",
res.CacheKey, res.ArtifactPath, res.SHA256, res.DurationMs)
}
return nil
}
12 changes: 6 additions & 6 deletions cmd/recipe_matrix_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ func TestE2E_Recipe_DryRunMatrix(t *testing.T) {
expectSteps []string
}{
{
recipe: "nile-test-fullnode",
params: []string{"intent_path=" + intentPath},
recipe: "nile-test-fullnode",
params: []string{"intent_path=" + intentPath},
expectSteps: []string{"validate", "preflight", "apply", "verify"},
},
{
Expand All @@ -52,13 +52,13 @@ func TestE2E_Recipe_DryRunMatrix(t *testing.T) {
expectSteps: []string{"validate", "preflight"},
},
{
recipe: "destroy-private-network-cleanly",
params: []string{"network=private-dev"},
recipe: "destroy-private-network-cleanly",
params: []string{"network=private-dev"},
expectSteps: []string{"status-check", "destroy"},
},
{
recipe: "recover-failed-upgrade",
params: []string{"node=my-fullnode"},
recipe: "recover-failed-upgrade",
params: []string{"node=my-fullnode"},
expectSteps: []string{"diagnose", "rollback"},
},
{
Expand Down
1 change: 1 addition & 0 deletions cmd/schema_coverage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ func TestSchemaCoverage(t *testing.T) {
lookup := map[string]string{
"trond apply": "apply",
"trond auto-heal": "auto-heal",
"trond build": "build",
"trond config validate": "config-validate",
"trond config render": "config-render",
"trond config diff": "config-diff",
Expand Down
16 changes: 8 additions & 8 deletions cmd/schema_manifest_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ func TestE2E_SchemaManifestReverse(t *testing.T) {
out := runTrondCtx(ctx, t, env, "schema", "--output", "json")

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

type manifestCommand struct {
Name string `json:"name"`
FullName string `json:"full_name"`
Use string `json:"use"`
Aliases []string `json:"aliases"`
Flags []manifestFlag `json:"flags"`
Subcommands []manifestCommand `json:"subcommands"`
Name string `json:"name"`
FullName string `json:"full_name"`
Use string `json:"use"`
Aliases []string `json:"aliases"`
Flags []manifestFlag `json:"flags"`
Subcommands []manifestCommand `json:"subcommands"`
}

type manifestFlag struct {
Expand Down
4 changes: 2 additions & 2 deletions cmd/statedir_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ func TestE2E_StateDirPriority(t *testing.T) {
wantNot []string
}{
{
name: "flag-beats-env",
args: []string{"--state-dir", flagDir},
name: "flag-beats-env",
args: []string{"--state-dir", flagDir},
extraEnv: []string{
"TROND_STATE_DIR=" + envDir,
},
Expand Down
47 changes: 47 additions & 0 deletions internal/build/audit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package build

import (
"time"

"github.com/tronprotocol/tron-deployment/internal/security"
)

// AuditPhase represents where in the build lifecycle we are. Per
// FR-023 we append an `in_progress` event at start, then write a
// terminal event (success / failed / cancelled) on completion. A
// crashed mid-build leaves the `in_progress` entry visible to
// `trond events`, surfacing the forensic signal.
type AuditPhase string

const (
PhaseInProgress AuditPhase = "in_progress"
PhaseSuccess AuditPhase = "success"
PhaseFailed AuditPhase = "failed"
PhaseCancelled AuditPhase = "cancelled"
)

// AppendAuditEvent writes one build-related row to the audit log.
//
// We deliberately reuse the existing security.AuditEntry shape (the
// same envelope `apply`, `start`, `stop` use) so downstream tooling —
// `trond events`, MCP resources, the JSON contract — doesn't need a
// new code path. The build-specific fields ride in the existing
// ErrorCode/Result columns.
func AppendAuditEvent(phase AuditPhase, cacheKey, errorCode string, startedAt time.Time) error {
log, err := security.NewAuditLog("")
if err != nil {
return err
}
entry := security.AuditEntry{
Timestamp: time.Now().UTC(),
Command: "build",
Target: "local", // build target is always local in v1
IntentHash: cacheKey,
Result: string(phase),
ErrorCode: errorCode,
}
if phase != PhaseInProgress {
entry.DurationMs = time.Since(startedAt).Milliseconds()
}
return log.Write(entry)
}
Loading
Loading