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
44 changes: 15 additions & 29 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -110,53 +110,39 @@ jobs:
GOFLAGS: '-buildvcs=false'

release:
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/')
runs-on: windows-latest
needs: [build-and-test, sast-scan]
if: startsWith(github.ref, 'refs/tags/')

steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Install GPG
run: sudo apt-get update && sudo apt-get install -y gnupg

- name: Install Aqua
uses: aquaproj/aqua-installer@5e54e5cee8a95ee2ce7c04cb993da6dfad13e59c # v3.1.2
with:
aqua_version: v2.51.1

- name: Install tools
run: aqua install

- name: Cache Go Modules
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-

- name: Install Dependencies
run: go install ./...

- name: Import GPG key
id: import_gpg
uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec # v6.3.0
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.GPG_PASSPHRASE }}

- name: Check if prerelease
id: prerelease
run: |
if [[ "${{ github.ref }}" =~ ^refs/tags/v[0-9]+\.[0-9]+\.[0-9]+- ]]; then
echo "prerelease=true" >> $GITHUB_OUTPUT
else
echo "prerelease=false" >> $GITHUB_OUTPUT
fi
shell: bash

- name: Run GoReleaser
uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6.3.0
with:
version: "~> v2"
args: release --clean
args: release --clean ${{ steps.prerelease.outputs.prerelease == 'true' && '--skip=chocolatey,homebrew' || '' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GPG_FINGERPRINT: ${{ env.GPG_FINGERPRINT }}
HOMEBREW_CLI_WRITE_PAT: ${{ secrets.HOMEBREW_CLI_WRITE_PAT }}
GITHUB_SHA: ${{ github.sha }}
SKIP_HOMEBREW: ${{ steps.prerelease.outputs.prerelease == 'true' }}
HOMEBREW_CLI_WRITE_PAT: ${{ secrets.HOMEBREW_CLI_WRITE_PAT }}
CHOCOLATEY_API_KEY: ${{ secrets.CHOCOLATEY_API_KEY }}
57 changes: 53 additions & 4 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@ builds:
# Archive configuration
archives:
- id: windsor
formats: ["tar.gz"]
formats:
- tar.gz
format_overrides:
- goos: windows
format: zip

changelog:
sort: asc
Expand Down Expand Up @@ -61,14 +65,59 @@ signs:

brews:
- name: windsor
directory: Formula
skip_upload: "{{ eq .Env.SKIP_HOMEBREW \"true\" }}"
repository:
owner: windsorcli
name: homebrew-cli
branch: main
token: "{{ .Env.HOMEBREW_CLI_WRITE_PAT }}"
homepage: "https://windsorcli.github.io"
commit_author:
name: goreleaserbot
email: bot@goreleaser.com
homepage: "https://windsorcli.github.io"
description: "The Windsor Command Line Interface"
license: "MPL-2.0"
skip_upload: auto
download_strategy: GithubPrivateRepositoryReleaseDownloadStrategy
custom_require: "lib/custom_download_strategy"
install: |
bin.install "windsor"
# Install shell completions
output = Utils.safe_popen_read("#{bin}/windsor", "completion", "bash")
(bash_completion/"windsor").write output
output = Utils.safe_popen_read("#{bin}/windsor", "completion", "zsh")
(zsh_completion/"_windsor").write output
output = Utils.safe_popen_read("#{bin}/windsor", "completion", "fish")
(fish_completion/"windsor.fish").write output

chocolateys:
- name: windsor
ids:
- windsor
package_source_url: https://github.com/windsorcli/cli
owners: Windsor CLI
title: Windsor CLI
authors: Windsor CLI Team
project_url: https://windsorcli.github.io
icon_url: https://windsorcli.github.io/icon.png
copyright: "2025 Windsor CLI Team"
license_url: https://github.com/windsorcli/cli/blob/main/LICENSE
require_license_acceptance: false
project_source_url: https://github.com/windsorcli/cli
docs_url: https://windsorcli.github.io
bug_tracker_url: https://github.com/windsorcli/cli/issues
tags: "cli windows command-line"
summary: "The Windsor Command Line Interface"
description: |
The Windsor CLI assists your cloud native development workflow.
This package provides the Windows installer for Windsor CLI.

After installation, add the following line to your PowerShell profile to enable shell integration:
```powershell
Invoke-Expression (& windsor hook powershell)
```

Your PowerShell profile is located at: $PROFILE
release_notes: "https://github.com/windsorcli/cli/releases/tag/v{{ .Version }}"
api_key: "{{ .Env.CHOCOLATEY_API_KEY }}"
source_repo: "https://push.chocolatey.org/"
url_template: "https://github.com/windsorcli/cli/releases/download/v{{ .Version }}/windsor_{{ .Version }}_windows_amd64.zip"
5 changes: 5 additions & 0 deletions api/v1alpha1/config_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type Config struct {

// Context represents the context configuration
type Context struct {
ID *string `yaml:"id,omitempty"`
ProjectName *string `yaml:"projectName,omitempty"`
Blueprint *string `yaml:"blueprint,omitempty"`
Environment map[string]string `yaml:"environment,omitempty"`
Expand All @@ -40,6 +41,9 @@ func (base *Context) Merge(overlay *Context) {
if overlay == nil {
return
}
if overlay.ID != nil {
base.ID = overlay.ID
}
if overlay.ProjectName != nil {
base.ProjectName = overlay.ProjectName
}
Expand Down Expand Up @@ -123,6 +127,7 @@ func (c *Context) DeepCopy() *Context {
}
}
return &Context{
ID: c.ID,
ProjectName: c.ProjectName,
Blueprint: c.Blueprint,
Environment: environmentCopy,
Expand Down
16 changes: 16 additions & 0 deletions api/v1alpha1/config_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,22 @@ func TestConfig_Merge(t *testing.T) {
t.Errorf("ProjectName mismatch: expected 'OverlayProject', got '%s'", *base.ProjectName)
}
})

t.Run("MergeWithID", func(t *testing.T) {
base := &Context{
ID: ptrString("base-id"),
}

overlay := &Context{
ID: ptrString("overlay-id"),
}

base.Merge(overlay)

if base.ID == nil || *base.ID != "overlay-id" {
t.Errorf("ID mismatch: expected 'overlay-id', got '%s'", *base.ID)
}
})
}

func TestConfig_Copy(t *testing.T) {
Expand Down
1 change: 1 addition & 0 deletions pkg/config/config_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ type ConfigHandler interface {
Clean() error
IsLoaded() bool
SetSecretsProvider(provider secrets.SecretsProvider)
GenerateContextID() error
}

const (
Expand Down
9 changes: 9 additions & 0 deletions pkg/config/mock_config_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type MockConfigHandler struct {
GetConfigRootFunc func() (string, error)
CleanFunc func() error
SetSecretsProviderFunc func(provider secrets.SecretsProvider)
GenerateContextIDFunc func() error
}

// =============================================================================
Expand Down Expand Up @@ -216,5 +217,13 @@ func (m *MockConfigHandler) SetSecretsProvider(provider secrets.SecretsProvider)
}
}

// GenerateContextID calls the mock GenerateContextIDFunc if set, otherwise returns nil
func (m *MockConfigHandler) GenerateContextID() error {
if m.GenerateContextIDFunc != nil {
return m.GenerateContextIDFunc()
}
return nil
}

// Ensure MockConfigHandler implements ConfigHandler
var _ ConfigHandler = (*MockConfigHandler)(nil)
30 changes: 30 additions & 0 deletions pkg/config/mock_config_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -705,3 +705,33 @@ func TestMockConfigHandler_SetSecretsProvider(t *testing.T) {
handler.SetSecretsProvider(mockProvider)
})
}

func TestMockConfigHandler_GenerateContextID(t *testing.T) {
t.Run("WithMockFunction", func(t *testing.T) {
// Given a mock config handler with GenerateContextIDFunc set
handler := NewMockConfigHandler()
mockErr := fmt.Errorf("mock generate context ID error")
handler.GenerateContextIDFunc = func() error { return mockErr }

// When GenerateContextID is called
err := handler.GenerateContextID()

// Then the error should match the expected mock error
if err != mockErr {
t.Errorf("Expected error = %v, got = %v", mockErr, err)
}
})

t.Run("WithNoFuncSet", func(t *testing.T) {
// Given a mock config handler without GenerateContextIDFunc set
handler := NewMockConfigHandler()

// When GenerateContextID is called
err := handler.GenerateContextID()

// Then no error should be returned
if err != nil {
t.Errorf("Expected error = %v, got = %v", nil, err)
}
})
}
39 changes: 21 additions & 18 deletions pkg/config/shims.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package config

import (
"crypto/rand"
"os"

"github.com/goccy/go-yaml"
Expand All @@ -12,29 +13,31 @@ import (

// Shims provides mockable wrappers around system and runtime functions
type Shims struct {
ReadFile func(string) ([]byte, error)
WriteFile func(string, []byte, os.FileMode) error
RemoveAll func(string) error
Getenv func(string) string
Setenv func(string, string) error
Stat func(string) (os.FileInfo, error)
MkdirAll func(string, os.FileMode) error
YamlMarshal func(any) ([]byte, error)
YamlUnmarshal func([]byte, any) error
ReadFile func(string) ([]byte, error)
WriteFile func(string, []byte, os.FileMode) error
RemoveAll func(string) error
Getenv func(string) string
Setenv func(string, string) error
Stat func(string) (os.FileInfo, error)
MkdirAll func(string, os.FileMode) error
YamlMarshal func(any) ([]byte, error)
YamlUnmarshal func([]byte, any) error
CryptoRandRead func([]byte) (int, error)
}

// NewShims creates a new Shims instance with default implementations
func NewShims() *Shims {
return &Shims{
ReadFile: os.ReadFile,
WriteFile: os.WriteFile,
RemoveAll: os.RemoveAll,
Getenv: os.Getenv,
Setenv: os.Setenv,
Stat: os.Stat,
MkdirAll: os.MkdirAll,
YamlMarshal: yaml.Marshal,
YamlUnmarshal: yaml.Unmarshal,
ReadFile: os.ReadFile,
WriteFile: os.WriteFile,
RemoveAll: os.RemoveAll,
Getenv: os.Getenv,
Setenv: os.Setenv,
Stat: os.Stat,
MkdirAll: os.MkdirAll,
YamlMarshal: yaml.Marshal,
YamlUnmarshal: yaml.Unmarshal,
CryptoRandRead: func(b []byte) (int, error) { return rand.Read(b) },
}
}

Expand Down
20 changes: 20 additions & 0 deletions pkg/config/yaml_config_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -645,3 +645,23 @@ func convertValue(value string, targetType reflect.Type) (any, error) {

return convertedValue, nil
}

// GenerateContextID generates a random context ID if one doesn't exist
func (y *YamlConfigHandler) GenerateContextID() error {
if y.GetString("id") != "" {
return nil
}

const charset = "abcdefghijklmnopqrstuvwxyz0123456789"
b := make([]byte, 7)
if _, err := y.shims.CryptoRandRead(b); err != nil {
return fmt.Errorf("failed to generate random context ID: %w", err)
}

for i := range b {
b[i] = charset[int(b[i])%len(charset)]
}

id := "w" + string(b)
return y.SetContextValue("id", id)
}
Loading
Loading