Skip to content
Draft
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
3 changes: 3 additions & 0 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Answer Style

- Always keep your answers short and concise unless I say to elaborate.
21 changes: 21 additions & 0 deletions .claude/commands/cmsg.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
allowed-tools: Bash(git add:*), Bash(git status:*), Bash(git commit:*), Bash(git log:*)
description: Create a git commit
---

## Context

- Current git status: !`git status`
- Current git diff (staged and unstaged changes): !`git diff HEAD`
- Current branch: !`git branch --show-current`
- Recent commits: !`git log --oneline -10`

## Your Task

- Summarize the changes of staged files into a concise commit message.
- Following conventional commits spec.
- Use the style of writing as I (Stanislav Jakuschevskij) do.
- Maximum subject width is 50 characters.
- Maximum body width is 90 characters which means break the line after 90 characters.
- Add issues reference at the bottom: "Issue $ARGUMENTS"
- Show me the final message in the end, don't commit it.
1 change: 1 addition & 0 deletions .claude/commands/oldocs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Generate "one liner" docs for $ARGUMENTS and break the line after 90 characters.
211 changes: 211 additions & 0 deletions .claude/features/issue-744-github-actions-workflow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
# Issue 744: GitHub Actions Workflow Generation

## Feature Summary

Add `func config ci --github` command to generate GitHub Actions workflow files for function deployment.

**Workflow contains:**
- Checkout code
- Setup func CLI (using knative-func-action)
- Optional test step (language-specific)
- Deploy using `func deploy` (local) or `func deploy --remote` (remote)

**Build modes:**
- Local build (default): Builds in GitHub runner, deploys to cluster
- Remote build (--remote): Build and deploy on-cluster

## Acceptance Criteria

1. **Basic Command Functionality**
- ✅ Creates `.github/workflows/` directory with valid workflow YAML
- ✅ Fails with error if workflow already exists (no overwrite)
- ✅ Fails fast if not in initialized function directory

2. **Local Build Mode (Default)**
- ✅ Generates workflow with: checkout → setup func → test → deploy
- ✅ Triggers on push to `main` branch (default)
- ✅ Runs on `ubuntu-latest`
- ✅ File named `deploy-local.yaml`

3. **Remote Build Mode**
- ✅ `--remote` flag generates `deploy-remote.yaml`
- ✅ Uses `func deploy --remote`
- ✅ Includes cluster auth configuration (defaults initially)

4. **Configuration Options**
- ✅ `--branch <name>` sets trigger branch
- ✅ Triggers on `push` events only (not pull_request yet)
- ✅ Sensible defaults when flags not specified

5. **Runtime Support**
- ✅ Go functions: includes test step
- ⏳ Python functions: future iteration
- ❌ Other runtimes: not in scope initially

## Key Decisions

- **Workflow naming:** `deploy-local.yaml` and `deploy-remote.yaml`
- **Default branch:** `main`
- **Event triggers:** `push` first, `pull_request` later, workflow_dispatch later
- **Runtime priority:** Go → Python → others
- **Test step:** Included for supported runtimes
- **Cluster configuration approach:**
1. Use defaults first
2. Add option flags
3. Read from existing function config
4. Interactive prompts (last)
- **Multiple workflows:** Support both local and remote (different clusters possible)

## Implementation Phases

### Phase 1: Test Infrastructure & Basic Command Structure

**Step 1.1: Command skeleton**

Test Cases:
- `TestNewConfigCICmd_CommandExists` - Command wired up correctly
- `TestNewConfigCICmd_FailsWhenNotInitialized` - Fail when not in function dir
- `TestConfigCI_RequiresGithubFlag` - --github flag required initially

Implementation:
- Basic command structure with --github flag
- Function initialization check using functionLoader
- Error handling and fail fast
- Wire into config.go

**Refactor:**
- Extract common patterns
- Consistent error messaging

---

### Phase 2: Workflow File Generation - Local Build

**Step 2.1: Directory and file creation**

Test Cases:
- `TestConfigCI_GitHub_CreatesWorkflowDirectory` - Creates .github/workflows/
- `TestConfigCI_GitHub_GeneratesLocalWorkflowFile` - Creates deploy-local.yaml
- `TestConfigCI_GitHub_LocalWorkflow_HasCorrectStructure` - Valid YAML structure

Implementation:
- Create workflow template (embedded or separate package)
- Directory creation logic
- File writing logic
- Basic YAML: checkout → setup func → deploy

**Refactor:**
- Extract template rendering
- Create workflow config struct

**Step 2.2: Go-specific workflow content**

Test Cases:
- `TestConfigCI_GitHub_GoFunction_IncludesTestStep` - Test step for Go
- `TestConfigCI_GitHub_DefaultTrigger_PushToMain` - Triggers on push to main
- `TestConfigCI_GitHub_UsesUbuntuRunner` - Runner is ubuntu-latest
- `TestConfigCI_GitHub_IncludesClusterConfig` - Cluster config placeholders

Implementation:
- Detect function runtime from Function struct
- Add conditional test step for Go
- Set default trigger: push on main
- Add cluster config (env vars/secrets placeholders)

**Refactor:**
- Runtime-specific customization logic
- Maintainable/extensible template

---

### Phase 3: Remote Build Support

**Step 3.1: Remote build flag**

Test Cases:
- `TestConfigCI_GitHub_Remote_GeneratesRemoteWorkflowFile` - Creates deploy-remote.yaml
- `TestConfigCI_GitHub_Remote_UsesRemoteDeployCommand` - Uses func deploy --remote
- `TestConfigCI_GitHub_Remote_IncludesAuthConfig` - Cluster auth config present

Implementation:
- Add --remote flag
- Template variation for remote builds
- Generate deploy-remote.yaml when --remote set
- Remote-specific cluster auth setup

**Refactor:**
- Consolidate local/remote template logic
- Use conditionals or separate templates

---

### Phase 4: Configuration Options

**Step 4.1: Branch configuration**

Test Cases:
- `TestConfigCI_GitHub_CustomBranch_SetsTrigger` - --branch flag sets trigger branch
- `TestConfigCI_GitHub_DefaultBranch_IsMain` - Default is main

Implementation:
- Add --branch flag
- Use flag value in template
- Default to "main"

**Refactor:**
- Create configuration struct for all workflow options

---

### Phase 5: Collision Detection & Error Handling

**Step 5.1: Existing workflow detection**

Test Cases:
- `TestConfigCI_GitHub_Local_FailsWhenFileExists` - Fails if deploy-local.yaml exists
- `TestConfigCI_GitHub_Remote_FailsWhenFileExists` - Fails if deploy-remote.yaml exists
- `TestConfigCI_GitHub_ExistingWorkflow_ShowsHelpfulError` - Helpful error message

Implementation:
- Check file existence before generation
- Return descriptive error on collision
- Suggest alternatives to user

**Refactor:**
- Extract file existence checking
- Improve error messaging

---

## Current Status

### ✅ Completed

**Phase 1: Test Infrastructure & Basic Command Structure** ✅
- Created `cmd/common` package for reusable loader/saver interfaces
- Created `cmd/testing` factory with `CreateFuncInTempDir()` helper
- Created `cmd/config_ci.go` with basic command structure
- Created `cmd/config_ci_test.go` with `ciOpts` struct pattern
- Wired command into `cmd/config.go:74`
- Tests passing (3/3):
- `TestNewConfigCICmd_CommandExists`
- `TestNewConfigCICmd_FailsWhenNotInitialized`
- `TestNewConfigCICmd_SuccessWhenInitialized`
- Commit: `bd22332f` - feat: add config ci command and refactor interfaces

### 🔄 In Progress

**Phase 2, Step 2.1: Workflow directory/file creation**
- Next: Implement Tests 1-3 (directory creation, file generation, YAML structure)

### ⏳ Next Steps

1. Complete Phase 2, Step 2.1 (3 tests)
2. Complete Phase 2, Step 2.2 (4 tests for Go-specific content)

---

## Resources

- Sample workflow: https://github.com/functions-dev/templates/blob/main/.github/workflows/invoke-all.yaml
- Func GitHub Action: https://github.com/gauron99/knative-func-action
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
/pkg/functions/testdata/default_home/go
/pkg/functions/testdata/default_home/.cache
/pkg/functions/testdata/migrations/*/.gitignore
/pkg/oci/testdata/test-links/absoluteLink
/pkg/oci/testdata/test-links/absoluteLinkWindows

# Go
/templates/go/cloudevents/go.sum
Expand All @@ -31,7 +33,7 @@ __pycache__
# E2E Tests
/e2e/testdata/default_home/go
/e2e/testdata/default_home/.cache

# Editors
.vscode
.idea
Expand Down
4 changes: 2 additions & 2 deletions cmd/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func TestBuild_ConfigApplied(t *testing.T) {
testConfigApplied(NewBuildCmd, t)
}

// TestBuild_ConfigPrecedence ensures that the correct precidence for config
// TestBuild_ConfigPrecedence ensures that the correct precedence for config
// are applied: static < global < function context < envs < flags
func TestBuild_ConfigPrecedence(t *testing.T) {
testConfigPrecedence(NewBuildCmd, t)
Expand All @@ -39,7 +39,7 @@ func TestBuild_Default(t *testing.T) {
testDefault(NewBuildCmd, t)
}

// TestBuild_FunctionContext ensures that the function contectually relevant
// TestBuild_FunctionContext ensures that the function contextually relevant
// to the current command execution is loaded and used for flag defaults by
// spot-checking the builder setting.
func TestBuild_FunctionContext(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion cmd/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func NewTestClient(options ...fn.Option) ClientFactory {
}

// NewClient constructs an fn.Client with the majority of
// the concrete implementations set. Provide additional Options to this constructor
// the concrete implementations set. Provide additional Options to this constructor
// to override or augment as needed, or override the ClientFactory passed to
// commands entirely to mock for testing. Note the returned cleanup function.
// 'Namespace' is optional. If not provided (see DefaultNamespace commentary),
Expand Down
47 changes: 47 additions & 0 deletions cmd/common/common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package common

import (
"fmt"

fn "knative.dev/func/pkg/functions"
)

// DefaultLoaderSaver implements FunctionLoaderSaver composite interface
var DefaultLoaderSaver standardLoaderSaver

// FunctionLoader loads a function from a filesystem path.
type FunctionLoader interface {
Load(path string) (fn.Function, error)
}

// FunctionSaver persists a function to storage.
type FunctionSaver interface {
Save(f fn.Function) error
}

// FunctionLoaderSaver combines loading and saving capabilities for functions.
type FunctionLoaderSaver interface {
FunctionLoader
FunctionSaver
}

type standardLoaderSaver struct{}

// Load creates and validates a function from the given filesystem path.
func (s standardLoaderSaver) Load(path string) (fn.Function, error) {
f, err := fn.NewFunction(path)
if err != nil {
return fn.Function{}, fmt.Errorf("failed to create new function (path: %q): %w", path, err)
}

if !f.Initialized() {
return fn.Function{}, fn.NewErrNotInitialized(f.Root)
}

return f, nil
}

// Save writes the function configuration to disk.
func (s standardLoaderSaver) Save(f fn.Function) error {
return f.Write()
}
51 changes: 51 additions & 0 deletions cmd/common/common_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package common_test

import (
"testing"

"gotest.tools/v3/assert"
"knative.dev/func/cmd/common"
cmdTest "knative.dev/func/cmd/testing"
fn "knative.dev/func/pkg/functions"
fnTest "knative.dev/func/pkg/testing"
)

func TestDefaultLoaderSaver_SuccessfulLoad(t *testing.T) {
existingFunc := cmdTest.CreateFuncInTempDir(t, "ls-func")

actualFunc, err := common.DefaultLoaderSaver.Load(existingFunc.Root)

assert.NilError(t, err)
assert.Equal(t, existingFunc.Name, actualFunc.Name)
}

func TestDefaultLoaderSaver_GenericFuncCreateError_WhenFuncPathInvalid(t *testing.T) {
_, err := common.DefaultLoaderSaver.Load("/non-existing-path")

assert.ErrorContains(t, err, "failed to create new function")
}

func TestDefaultLoaderSaver_IsNotInitializedError_WhenNoFuncAtPath(t *testing.T) {
expectedErrMsg := fn.NewErrNotInitialized(fnTest.Cwd()).Error()

_, err := common.DefaultLoaderSaver.Load(fnTest.Cwd())

assert.Error(t, err, expectedErrMsg)
}

func TestDefaultLoaderSaver_SuccessfulSave(t *testing.T) {
existingFunc := cmdTest.CreateFuncInTempDir(t, "")
name := "environment"
value := "test"
existingFunc.Run.Envs = append(existingFunc.Run.Envs, fn.Env{Name: &name, Value: &value})

err := common.DefaultLoaderSaver.Save(existingFunc)

assert.NilError(t, err)
}

func TestDefaultLoaderSaver_ForwardsSaveError(t *testing.T) {
err := common.DefaultLoaderSaver.Save(fn.Function{})

assert.Error(t, err, "function root path is required")
}
Loading