Skip to content

Commit da50b92

Browse files
comchangsclaude
andcommitted
feat: #51 package_layout — customizable package directory structure
New config field: package_layout (map[string]string) Maps local source paths to package destination paths. Example (NSM legacy compat): package_layout: "script/": "bin/" "build/libs/*.jar": "lib/" "config/${ENV}/": "conf/" If package_layout is defined → structured tar with remapped paths If not defined → existing flat tar behavior (package_includes) Also: - Reverted unnecessary start IP argument (#50) - noriter-admin: added package_layout for NSM compatibility Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 038a54f commit da50b92

3 files changed

Lines changed: 57 additions & 4 deletions

File tree

internal/config/config.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,8 @@ type Module struct {
9797
SSH *SSHConfig `yaml:"ssh"` // per-module SSH config (optional)
9898
BuildCmd string `yaml:"build_cmd"` // build command (e.g., "./gradlew :module:bootJar")
9999
ArtifactPath string `yaml:"artifact_path"` // path to build artifact
100-
PackageIncludes []string `yaml:"package_includes"` // additional files/dirs to package
100+
PackageIncludes []string `yaml:"package_includes"` // additional files/dirs to package (flat structure)
101+
PackageLayout map[string]string `yaml:"package_layout"` // source → dest path mapping (structured package)
101102
HealthCheck HealthCheckConfig `yaml:"health_check"`
102103
StartCmd string `yaml:"start_cmd"` // remote start command
103104
StopCmd string `yaml:"stop_cmd"` // remote stop command

internal/deploy/lifecycle.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ func (d *Deployer) Start(envName, moduleName string, serverNum int) error {
3232
}
3333

3434
// Pass server host as argument (legacy compat: bin/server start {IP})
35-
cmd := fmt.Sprintf("cd %s/current && %s %s", baseDir, startCmd, srv.Host)
35+
cmd := fmt.Sprintf("cd %s/current && %s", baseDir, startCmd)
3636
result, err := d.ssh.Exec(env, srv.Host, cmd)
3737
if err != nil {
3838
return fmt.Errorf("start failed: %w", err)
@@ -81,7 +81,7 @@ func (d *Deployer) StartRolling(envName, moduleName string, serverNum int) error
8181
d.execHook(env, srv.Host, "pre_start", mod.Hooks.PreStart)
8282
}
8383

84-
cmd := fmt.Sprintf("cd %s/current && %s %s", baseDir, startCmd, srv.Host)
84+
cmd := fmt.Sprintf("cd %s/current && %s", baseDir, startCmd)
8585
result, err := d.ssh.Exec(env, srv.Host, cmd)
8686
if err != nil {
8787
return fmt.Errorf("[%s] start failed: %w", srv.Host, err)

internal/pipeline/pipeline.go

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package pipeline
22

33
import (
44
"fmt"
5+
"os"
56
"os/exec"
7+
"path/filepath"
68
"strings"
79

810
"github.com/neurosamAI/tow-cli/internal/config"
@@ -234,9 +236,14 @@ func (p *Pipeline) Package(moduleName, envName string) error {
234236
artifactPath = fmt.Sprintf("build/%s.tar.gz", moduleName)
235237
}
236238

239+
// If package_layout is defined, use structured packaging
240+
if len(mod.PackageLayout) > 0 {
241+
return p.packageWithLayout(moduleName, envName, mod, artifactPath)
242+
}
243+
244+
// Otherwise, use flat packaging (original behavior)
237245
var includes []string
238246

239-
// Get handler-specific package contents
240247
handler, err := module.Get(mod.Type)
241248
if err == nil {
242249
includes = append(includes, handler.PackageContents(moduleName, "")...)
@@ -267,6 +274,51 @@ func (p *Pipeline) Package(moduleName, envName string) error {
267274
return nil
268275
}
269276

277+
// packageWithLayout creates a structured tar.gz with remapped paths.
278+
// Example layout:
279+
//
280+
// package_layout:
281+
// "script/": "bin/"
282+
// "build/libs/*.jar": "lib/"
283+
// "config/${ENV}/": "conf/"
284+
func (p *Pipeline) packageWithLayout(moduleName, envName string, mod *config.Module, artifactPath string) error {
285+
// Create temp staging directory
286+
tmpDir, err := os.MkdirTemp("", "tow-package-*")
287+
if err != nil {
288+
return fmt.Errorf("creating temp dir: %w", err)
289+
}
290+
defer os.RemoveAll(tmpDir)
291+
292+
for src, dst := range mod.PackageLayout {
293+
// Substitute ${ENV} and ${MODULE} in source paths
294+
src = substituteVars(src, envName, moduleName, mod.Variables)
295+
dst = substituteVars(dst, envName, moduleName, mod.Variables)
296+
297+
destDir := filepath.Join(tmpDir, dst)
298+
os.MkdirAll(destDir, 0755)
299+
300+
// Use shell glob expansion for source
301+
cpCmd := fmt.Sprintf("cp -r %s %s/ 2>/dev/null || true", src, destDir)
302+
logger.Debug("Layout: %s → %s", src, dst)
303+
304+
if err := runLocalCmd(cpCmd); err != nil {
305+
logger.Warn("Layout copy failed for %s: %v", src, err)
306+
}
307+
}
308+
309+
// Ensure artifact parent dir exists
310+
os.MkdirAll(filepath.Dir(artifactPath), 0755)
311+
312+
// Create tar.gz from staging directory
313+
tarCmd := fmt.Sprintf("tar czf %s -C %s .", artifactPath, tmpDir)
314+
if err := runLocalCmd(tarCmd); err != nil {
315+
return fmt.Errorf("packaging failed: %w", err)
316+
}
317+
318+
logger.Success("Package created: %s (structured layout)", artifactPath)
319+
return nil
320+
}
321+
270322
// runSteps executes a sequence of named steps with progress logging
271323
func (p *Pipeline) runSteps(steps []struct {
272324
name string

0 commit comments

Comments
 (0)