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
101 changes: 96 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ jobs:
test:
name: Test (Go ${{ matrix.go-version }})
runs-on: ubuntu-latest
needs: [go-mod-tidy]
permissions:
contents: read
packages: read
Expand Down Expand Up @@ -68,6 +69,7 @@ jobs:
lint:
name: Lint
runs-on: ubuntu-latest
needs: [go-mod-tidy]
permissions:
contents: read
packages: read
Expand Down Expand Up @@ -105,6 +107,7 @@ jobs:
build:
name: Build
runs-on: ubuntu-latest
needs: [go-mod-tidy]
permissions:
contents: read
packages: read
Expand Down Expand Up @@ -139,18 +142,105 @@ jobs:
- name: Build all packages
run: go build -v ./...

- name: Build examples
run: |
cd example
go build -v ./...

- name: Upload UI build artifact
uses: actions/upload-artifact@v4
with:
name: admin-ui-dist
path: ui/dist/
retention-days: 3

build-examples:
name: Build Examples
runs-on: ubuntu-latest
needs: [go-mod-tidy]
permissions:
contents: read
packages: read

steps:
- name: Check out code
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.26'
cache: true

- name: Build examples
run: |
cd example
go build -v ./...

lint-examples:
name: Lint Examples
runs-on: ubuntu-latest
needs: [go-mod-tidy]
permissions:
contents: read
packages: read

steps:
- name: Check out code
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.26'
cache: true

- name: Run golangci-lint on examples
uses: golangci/golangci-lint-action@v7
with:
version: latest
args: --timeout=10m
working-directory: example

go-mod-tidy:
name: Go Mod Tidy
runs-on: ubuntu-latest
permissions:
contents: write
packages: read

steps:
- name: Check out code
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.26'
cache: true

- name: Run go mod tidy (workflow)
run: go mod tidy

- name: Run go mod tidy (examples)
run: |
cd example
go mod tidy

- name: Commit tidy changes
if: github.event_name == 'push'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git diff --quiet go.mod go.sum example/go.mod example/go.sum || {
git add go.mod go.sum example/go.mod example/go.sum
git commit -m "chore: go mod tidy"
git push
}

- name: Fail if go mod tidy produced changes (PR check)
if: github.event_name == 'pull_request'
run: |
git diff --exit-code go.mod go.sum
git diff --exit-code example/go.mod example/go.sum

ui-test:
name: UI Tests
runs-on: ubuntu-latest
Expand Down Expand Up @@ -191,6 +281,7 @@ jobs:
example-configs:
name: Validate Example Configs
runs-on: ubuntu-latest
needs: [go-mod-tidy]
permissions:
contents: read
packages: read
Expand Down
5 changes: 5 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ linters:
- errcheck
- gosec
- gocritic
# SA5011: staticcheck doesn't recognize t.Fatal as terminating; false positive in tests
- path: _test\.go
linters:
- staticcheck
text: "SA5011"
presets:
- std-error-handling

Expand Down
8 changes: 4 additions & 4 deletions ai/copilot/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ func (c *Client) GenerateWorkflow(ctx context.Context, req ai.GenerateRequest) (
if err != nil {
return nil, err
}
defer func() { _ = session.Destroy() }()
defer func() { _ = session.Disconnect() }()

prompt := ai.GeneratePrompt(req)

Expand Down Expand Up @@ -185,7 +185,7 @@ func (c *Client) GenerateComponent(ctx context.Context, spec ai.ComponentSpec) (
if err != nil {
return "", err
}
defer func() { _ = session.Destroy() }()
defer func() { _ = session.Disconnect() }()

prompt := ai.ComponentPrompt(spec)

Expand All @@ -207,7 +207,7 @@ func (c *Client) SuggestWorkflow(ctx context.Context, useCase string) ([]ai.Work
if err != nil {
return nil, err
}
defer func() { _ = session.Destroy() }()
defer func() { _ = session.Disconnect() }()

prompt := ai.SuggestPrompt(useCase)

Expand Down Expand Up @@ -244,7 +244,7 @@ func (c *Client) IdentifyMissingComponents(ctx context.Context, cfg *config.Work
if err != nil {
return nil, err
}
defer func() { _ = session.Destroy() }()
defer func() { _ = session.Disconnect() }()

prompt := ai.MissingComponentsPrompt(types)

Expand Down
2 changes: 1 addition & 1 deletion ai/copilot/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func (m *mockSession) SendAndWait(ctx context.Context, opts copilot.MessageOptio
return m.sendAndWaitFn(ctx, opts)
}

func (m *mockSession) Destroy() error {
func (m *mockSession) Disconnect() error {
m.destroyed = true
return nil
}
Expand Down
6 changes: 3 additions & 3 deletions ai/copilot/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
// SessionWrapper wraps the methods we use from copilot.Session so they can be mocked.
type SessionWrapper interface {
SendAndWait(ctx context.Context, opts copilot.MessageOptions) (*copilot.SessionEvent, error)
Destroy() error
Disconnect() error
}

// ClientWrapper wraps the methods we use from copilot.Client so they can be mocked.
Expand Down Expand Up @@ -39,6 +39,6 @@ func (w *realSessionWrapper) SendAndWait(ctx context.Context, opts copilot.Messa
return w.sess.SendAndWait(ctx, opts)
}

func (w *realSessionWrapper) Destroy() error {
return w.sess.Destroy()
func (w *realSessionWrapper) Disconnect() error {
return w.sess.Disconnect()
}
6 changes: 3 additions & 3 deletions cmd/wfctl/ci.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ func runCIGenerate(args []string) error {
return err
}

if err := os.MkdirAll(*outputDir, 0o755); err != nil {
if err := os.MkdirAll(*outputDir, 0o750); err != nil {
return fmt.Errorf("create output dir: %w", err)
}

Expand All @@ -91,11 +91,11 @@ func runCIGenerate(args []string) error {
// relPath is already a full relative path like .github/workflows/infra.yml
// Write relative to cwd, not outputDir
dest = relPath
if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil {
if err := os.MkdirAll(filepath.Dir(dest), 0o750); err != nil {
return fmt.Errorf("create dir for %s: %w", dest, err)
}
}
if err := os.WriteFile(dest, []byte(content), 0o644); err != nil {
if err := os.WriteFile(dest, []byte(content), 0o600); err != nil {
return fmt.Errorf("write %s: %w", dest, err)
}
fmt.Printf("wrote %s\n", dest)
Expand Down
12 changes: 7 additions & 5 deletions cmd/wfctl/infra.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,8 @@ func formatPlanTable(plan interfaces.IaCPlan) string {
tw := tabwriter.NewWriter(&sb, 0, 0, 2, ' ', 0)
fmt.Fprintln(tw, "Action\tResource\tType")
fmt.Fprintln(tw, "------\t--------\t----")
for _, a := range plan.Actions {
for i := range plan.Actions {
a := &plan.Actions[i]
symbol := actionSymbol(a.Action)
fmt.Fprintf(tw, "%s %s\t%s\t%s\n", symbol, a.Action, a.Resource.Name, a.Resource.Type)
}
Expand All @@ -317,7 +318,8 @@ func formatPlanMarkdown(plan interfaces.IaCPlan) string {
sb.WriteString("## Infrastructure Plan\n\n")
sb.WriteString("| Action | Resource | Type |\n")
sb.WriteString("|--------|----------|------|\n")
for _, a := range plan.Actions {
for i := range plan.Actions {
a := &plan.Actions[i]
symbol := actionSymbol(a.Action)
fmt.Fprintf(&sb, "| %s %s | `%s` | `%s` |\n",
symbol, a.Action, a.Resource.Name, a.Resource.Type)
Expand All @@ -343,8 +345,8 @@ func actionSymbol(action string) string {
}

func countActions(plan interfaces.IaCPlan) (creates, updates, deletes int) {
for _, a := range plan.Actions {
switch a.Action {
for i := range plan.Actions {
switch plan.Actions[i].Action {
case "create":
creates++
case "update":
Expand All @@ -362,7 +364,7 @@ func writePlanJSON(plan interfaces.IaCPlan, path string) error {
if err != nil {
return err
}
return os.WriteFile(path, data, 0o644)
return os.WriteFile(path, data, 0o600)
}

// runInfraImport imports an existing cloud resource into the IaC state.
Expand Down
20 changes: 11 additions & 9 deletions cmd/wfctl/infra_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@ func runInfraStateList(args []string) error {
var err error
cfgFile, err = resolveInfraConfig(fs)
if err != nil {
// No config found — list is empty.
// No config found — list is empty, not an error.
fmt.Println("No infrastructure config found. No state to list.")
return nil
return nil //nolint:nilerr // intentionally swallowing error - no config means nothing to list
}
}

Expand All @@ -73,7 +73,8 @@ func runInfraStateList(args []string) error {
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(tw, "Name\tType\tProvider\tProviderID")
fmt.Fprintln(tw, "----\t----\t--------\t----------")
for _, s := range states {
for i := range states {
s := &states[i]
fmt.Fprintf(tw, "%s\t%s\t%s\t%s\n", s.Name, s.Type, s.Provider, s.ProviderID)
}
tw.Flush()
Expand Down Expand Up @@ -117,7 +118,7 @@ func runInfraStateExport(args []string) error {
fmt.Println(string(data))
return nil
}
if err := os.WriteFile(*outputFlag, data, 0o644); err != nil {
if err := os.WriteFile(*outputFlag, data, 0o600); err != nil {
return fmt.Errorf("write %s: %w", *outputFlag, err)
}
fmt.Printf("State exported to %s (%s format)\n", *outputFlag, *format)
Expand Down Expand Up @@ -212,7 +213,8 @@ func exportAsTFState(states []interfaces.ResourceState) ([]byte, error) {
Outputs: map[string]any{},
}

for _, s := range states {
for i := range states {
s := &states[i]
attrs := map[string]any{
"id": s.ProviderID,
"name": s.Name,
Expand Down Expand Up @@ -252,7 +254,7 @@ func importFromTFState(srcFile, stateDir string) error {
return fmt.Errorf("parse tfstate: %w", err)
}

if err := os.MkdirAll(stateDir, 0o755); err != nil {
if err := os.MkdirAll(stateDir, 0o750); err != nil {
return fmt.Errorf("create state dir: %w", err)
}

Expand Down Expand Up @@ -281,7 +283,7 @@ func importFromTFState(srcFile, stateDir string) error {
continue
}
fname := stateDir + "/" + sanitizeStateID(id) + ".json"
if err := os.WriteFile(fname, out, 0o644); err != nil {
if err := os.WriteFile(fname, out, 0o600); err != nil {
return fmt.Errorf("write state record: %w", err)
}
imported++
Expand Down Expand Up @@ -314,7 +316,7 @@ func importFromPulumi(srcFile, stateDir string) error {
return fmt.Errorf("parse pulumi checkpoint: %w", err)
}

if err := os.MkdirAll(stateDir, 0o755); err != nil {
if err := os.MkdirAll(stateDir, 0o750); err != nil {
return fmt.Errorf("create state dir: %w", err)
}

Expand Down Expand Up @@ -344,7 +346,7 @@ func importFromPulumi(srcFile, stateDir string) error {
continue
}
fname := stateDir + "/" + sanitizeStateID(res.ID) + ".json"
if err := os.WriteFile(fname, out, 0o644); err != nil {
if err := os.WriteFile(fname, out, 0o600); err != nil {
return fmt.Errorf("write state record: %w", err)
}
imported++
Expand Down
Loading
Loading