From 6ffaddcc73fd071d51b2067cbb62736853e72dfa Mon Sep 17 00:00:00 2001 From: "Edgar Y. Walker" Date: Thu, 13 Nov 2025 13:44:42 -0800 Subject: [PATCH 1/3] feat: add ability to specify Git repos and Homebrew packages --- .github/workflows/release.yml | 15 +++++++++++++-- internal/config/types.go | 13 +++++++++++++ internal/config/validation.go | 19 +++++++++++++++++++ .../system/manifests/namespace.tmpl | 4 ++-- 4 files changed, 47 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bae9b30..ea38d75 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: "1.24" + go-version: "1.25" - name: Run tests run: go test -v ./... @@ -35,7 +35,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: "1.24" + go-version: "1.25" - name: Build binaries for all platforms run: | @@ -57,6 +57,10 @@ jobs: echo "Building Linux amd64..." GOOS=linux GOARCH=amd64 go build -ldflags="${LDFLAGS}" -o dist/devenv-linux-amd64 ./cmd/devenv + # Linux arm64 + echo "Building Linux arm64..." + GOOS=linux GOARCH=arm64 go build -ldflags="${LDFLAGS}" -o dist/devenv-linux-arm64 ./cmd/devenv + # macOS amd64 (Intel) echo "Building macOS amd64..." GOOS=darwin GOARCH=amd64 go build -ldflags="${LDFLAGS}" -o dist/devenv-darwin-amd64 ./cmd/devenv @@ -72,14 +76,21 @@ jobs: echo "✅ All binaries built successfully" ls -lh dist/ + - name: Generate checksums + run: | + cd dist + sha256sum * > checksums.txt + - name: Create Release uses: softprops/action-gh-release@v2 with: files: | dist/devenv-linux-amd64 + dist/devenv-linux-arm64 dist/devenv-darwin-amd64 dist/devenv-darwin-arm64 dist/devenv-windows-amd64.exe + dist/checksums.txt generate_release_notes: true draft: false prerelease: false diff --git a/internal/config/types.go b/internal/config/types.go index 1b2c7e5..d9cc32b 100644 --- a/internal/config/types.go +++ b/internal/config/types.go @@ -17,6 +17,9 @@ type BaseConfig struct { // Package management Packages PackageConfig `yaml:"packages,omitempty"` + // Git repos to be cloned + GitRepos []GitRepo `yaml:"gitRepos,omitempty" validate:"dive"` + // Storage configuration Volumes []VolumeMount `yaml:"volumes,omitempty" validate:"dive"` @@ -68,9 +71,17 @@ type GitConfig struct { type PackageConfig struct { Python []string `yaml:"python,omitempty" validate:"dive,min=1"` APT []string `yaml:"apt,omitempty" validate:"dive,min=1"` + Brew []string `yaml:"brew,omitempty" validate:"dive,min=1"` // Consider adding other package managers such as NPM, Yarn, etc. } +type GitRepo struct { + URL string `yaml:"url" validate:"required,min=1,url"` + Branch string `yaml:"branch,omitempty" validate:"omitempty,min=1"` + CommitHash string `yaml:"commitHash,omitempty" validate:"omitempty,min=1"` + Directory string `yaml:"directory,omitempty" validate:"omitempty,min=1,filepath"` +} + // ResourceConfig represents resource allocation type ResourceConfig struct { CPU any `yaml:"cpu,omitempty" validate:"omitempty,k8s_cpu"` @@ -112,7 +123,9 @@ func NewBaseConfigWithDefaults() BaseConfig { Packages: PackageConfig{ Python: []string{}, // Empty slice - no default packages APT: []string{}, // Empty slice - no default packages + Brew: []string{}, // Empty slice - no default packages }, + GitRepos: []GitRepo{}, // Empty slice - no default git repositories Volumes: []VolumeMount{}, // Empty slice - no default volumes Namespace: "devenv", // Default namespace EnvironmentName: "development", // Default environment name diff --git a/internal/config/validation.go b/internal/config/validation.go index 989cdad..dbc86c8 100644 --- a/internal/config/validation.go +++ b/internal/config/validation.go @@ -56,6 +56,7 @@ func init() { if err := validate.RegisterValidation("k8s_memory", validateKubernetesMemory); err != nil { panic(fmt.Errorf("register validator k8s_memory: %w", err)) } + validate.RegisterStructValidation(validateGitRepo, GitRepo{}) } // validateSSHKeys implements the "ssh_keys" tag. @@ -79,6 +80,17 @@ func validateSSHKeys(fl validator.FieldLevel) bool { return true } +// validateGitRepo implements the "git_repo" tag. +// Ensures that if both Branch and CommitHash are specified, an error is raised. +func validateGitRepo(sl validator.StructLevel) { + repo := sl.Current().Interface().(GitRepo) + // Both Branch and CommitHash cannot be specified simultaneously. + if repo.Branch != "" && repo.CommitHash != "" { + sl.ReportError(repo.Branch, "branch", "Branch", "branch_commit_conflict", "") + sl.ReportError(repo.CommitHash, "commitHash", "CommitHash", "branch_commit_conflict", "") + } +} + // validateKubernetesCPU implements the "k8s_cpu" tag for *raw* CPU fields. // Accepts: // - Strings: "", "unlimited", plain number ("2", "2.5"), or millicores ("500m") @@ -182,6 +194,13 @@ func ValidateDevEnvConfig(config *DevEnvConfig) error { return err // "memory must be >= 0" } + // If GitRepos are specified, ensure that hash & branch are not both specified. + for i, repo := range config.GitRepos { + if repo.CommitHash != "" && repo.Branch != "" { + return fmt.Errorf("gitRepos[%d]: commitHash and branch cannot both be specified", i) + } + } + if config.Resources.GPU < 0 { return fmt.Errorf("gpu must be >= 0") } diff --git a/internal/templates/template_files/system/manifests/namespace.tmpl b/internal/templates/template_files/system/manifests/namespace.tmpl index b0ac03b..19fb4cd 100644 --- a/internal/templates/template_files/system/manifests/namespace.tmpl +++ b/internal/templates/template_files/system/manifests/namespace.tmpl @@ -2,6 +2,6 @@ apiVersion: v1 kind: Namespace metadata: name: {{.Namespace}} - environment: {{.EnvironmentName}} annotations: - description: "Namespace for DevENV resources" \ No newline at end of file + description: "Namespace for DevENV resources" + environment: {{.EnvironmentName}} From d39f37bc8a4a4845d601373283ba6c72080bc164 Mon Sep 17 00:00:00 2001 From: "Edgar Y. Walker" Date: Thu, 13 Nov 2025 15:58:14 -0800 Subject: [PATCH 2/3] feat: add brew package support and git repo cloning --- internal/config/types.go | 1 + internal/config/validation.go | 33 ++++++++----- .../dev/scripts/templated/startup.sh | 46 +++++++++++++++++++ 3 files changed, 69 insertions(+), 11 deletions(-) diff --git a/internal/config/types.go b/internal/config/types.go index d9cc32b..6609cf4 100644 --- a/internal/config/types.go +++ b/internal/config/types.go @@ -78,6 +78,7 @@ type PackageConfig struct { type GitRepo struct { URL string `yaml:"url" validate:"required,min=1,url"` Branch string `yaml:"branch,omitempty" validate:"omitempty,min=1"` + Tag string `yaml:"tag,omitempty" validate:"omitempty,min=1"` CommitHash string `yaml:"commitHash,omitempty" validate:"omitempty,min=1"` Directory string `yaml:"directory,omitempty" validate:"omitempty,min=1,filepath"` } diff --git a/internal/config/validation.go b/internal/config/validation.go index dbc86c8..9df4428 100644 --- a/internal/config/validation.go +++ b/internal/config/validation.go @@ -84,10 +84,28 @@ func validateSSHKeys(fl validator.FieldLevel) bool { // Ensures that if both Branch and CommitHash are specified, an error is raised. func validateGitRepo(sl validator.StructLevel) { repo := sl.Current().Interface().(GitRepo) - // Both Branch and CommitHash cannot be specified simultaneously. - if repo.Branch != "" && repo.CommitHash != "" { - sl.ReportError(repo.Branch, "branch", "Branch", "branch_commit_conflict", "") - sl.ReportError(repo.CommitHash, "commitHash", "CommitHash", "branch_commit_conflict", "") + // Both Ref and CommitHash cannot be specified simultaneously. + targets := []string{} + if repo.Branch != "" { + targets = append(targets, repo.Branch) + } + if repo.Tag != "" { + targets = append(targets, repo.Tag) + } + if repo.CommitHash != "" { + targets = append(targets, repo.CommitHash) + } + + if len(targets) > 1 { + if repo.Branch != "" { + sl.ReportError(repo.Branch, "branch", "Branch", "too many target specifications", "") + } + if repo.Tag != "" { + sl.ReportError(repo.Tag, "tag", "Tag", "too many target specifications", "") + } + if repo.CommitHash != "" { + sl.ReportError(repo.CommitHash, "commitHash", "CommitHash", "too many target specifications", "") + } } } @@ -194,13 +212,6 @@ func ValidateDevEnvConfig(config *DevEnvConfig) error { return err // "memory must be >= 0" } - // If GitRepos are specified, ensure that hash & branch are not both specified. - for i, repo := range config.GitRepos { - if repo.CommitHash != "" && repo.Branch != "" { - return fmt.Errorf("gitRepos[%d]: commitHash and branch cannot both be specified", i) - } - } - if config.Resources.GPU < 0 { return fmt.Errorf("gpu must be >= 0") } diff --git a/internal/templates/template_files/dev/scripts/templated/startup.sh b/internal/templates/template_files/dev/scripts/templated/startup.sh index d72e25c..48aa35b 100644 --- a/internal/templates/template_files/dev/scripts/templated/startup.sh +++ b/internal/templates/template_files/dev/scripts/templated/startup.sh @@ -145,6 +145,11 @@ echo "Installing Python packages: {{range $i, $pkg := .Packages.Python}}{{if gt /bin/bash /scripts/run_with_git.sh ${DEV_USERNAME} ${PYTHON_PATH} -m pip install --no-user --no-cache-dir{{range .Packages.Python}} {{.}}{{end}} {{- end}} +{{- if gt (len .Packages.Brew) 0}} +echo "Installing Homebrew packages: {{range $i, $pkg := .Packages.Brew}}{{if gt $i 0}} {{end}}{{$pkg}}{{end}}" +sudo -u ${DEV_USERNAME} brew install{{range .Packages.Brew}} {{.}}{{end}} +{{- end}} + echo "Section 6: Package installation complete" # === USER ENVIRONMENT SETUP === @@ -181,6 +186,47 @@ chown -R ${DEV_USERNAME}:${DEV_USERNAME} /home/${DEV_USERNAME}/.vscode-server echo "Section 8: VSCode configuration complete" +# === GIT REPO CLONING === +{{- if gt (len .GitRepos) 0}} +echo "Cloning Git repositories" +{{- range .GitRepos}} +echo "Cloning repository: {{.URL}}" + +{{- /* Determine target directory */ -}} +{{- $targetDir := "" -}} +{{- if .Directory -}} + {{- $targetDir = .Directory -}} +{{- else -}} + {{- $targetDir = printf "/home/%s" $.Name -}} +{{- end }} + +# Clone the complete repository +git clone {{.URL}} {{$targetDir}} +cd {{$targetDir}} + +{{- /* Checkout specific reference */ -}} +{{- if .Tag}} +echo "Checking out tag: {{.Tag}}" +git checkout tags/{{.Tag}} +{{- else if .CommitHash}} +echo "Checking out commit: {{.CommitHash}}" +git checkout {{.CommitHash}} +{{- else if .Branch}} +echo "Checking out branch: {{.Branch}}" +git checkout {{.Branch}} +{{- else}} +echo "Staying on default branch" +{{- end}} + +echo "Repository cloned successfully to: {{$targetDir}}" +echo "Current commit: $(git rev-parse --short HEAD)" +echo "Current branch/ref: $(git branch --show-current 2>/dev/null || git describe --tags --exact-match 2>/dev/null || echo 'detached HEAD')" + +{{- end}} +{{- else}} +echo "No Git repositories to clone" +{{- end}} + # === SSH SERVER LAUNCH === echo "Starting SSH server" /usr/sbin/sshd -D \ No newline at end of file From ed1920889bea5225d1b29b2442f07125cf355c28 Mon Sep 17 00:00:00 2001 From: "Edgar Y. Walker" Date: Thu, 13 Nov 2025 16:07:27 -0800 Subject: [PATCH 3/3] test: update golden file for startup script --- internal/templates/testdata/golden/startup-scripts.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/templates/testdata/golden/startup-scripts.yaml b/internal/templates/testdata/golden/startup-scripts.yaml index b55ae86..c7eac05 100644 --- a/internal/templates/testdata/golden/startup-scripts.yaml +++ b/internal/templates/testdata/golden/startup-scripts.yaml @@ -151,6 +151,9 @@ data: echo "Section 8: VSCode configuration complete" + # === GIT REPO CLONING === + echo "No Git repositories to clone" + # === SSH SERVER LAUNCH === echo "Starting SSH server" /usr/sbin/sshd -D