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
2 changes: 1 addition & 1 deletion .github/workflows/agentic-campaign-generator.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions .github/workflows/agentic-campaign-generator.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,6 @@ safe-outputs:
- name: "Campaign Roadmap"
layout: "roadmap"
filter: "is:issue is:pr"
update-project:
max: 10
github-token: "${{ secrets.GH_AW_PROJECT_GITHUB_TOKEN }}"
field-definitions:
- name: "Campaign Id"
data-type: "TEXT"
Expand All @@ -57,6 +54,9 @@ safe-outputs:
data-type: "DATE"
- name: "End Date"
data-type: "DATE"
update-project:
max: 10
github-token: "${{ secrets.GH_AW_PROJECT_GITHUB_TOKEN }}"
messages:
run-started: "### :rocket: Campaign setup started

Expand Down
6 changes: 3 additions & 3 deletions pkg/campaign/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,6 @@ func buildGeneratorSafeOutputs() *workflow.SafeOutputsConfig {
Filter: "is:issue is:pr",
},
},
},
UpdateProjects: &workflow.UpdateProjectConfig{
GitHubToken: "${{ secrets.GH_AW_PROJECT_GITHUB_TOKEN }}",
FieldDefinitions: []workflow.ProjectFieldDefinition{
{
Name: "Campaign Id",
Expand Down Expand Up @@ -118,6 +115,9 @@ func buildGeneratorSafeOutputs() *workflow.SafeOutputsConfig {
},
},
},
UpdateProjects: &workflow.UpdateProjectConfig{
GitHubToken: "${{ secrets.GH_AW_PROJECT_GITHUB_TOKEN }}",
},
Messages: &workflow.SafeOutputMessagesConfig{
AppendOnlyComments: true,
RunStarted: "### :rocket: Campaign setup started\n\n" +
Expand Down
13 changes: 13 additions & 0 deletions pkg/cli/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -919,6 +919,19 @@ func renderCampaignGeneratorMarkdown(data *workflow.WorkflowData) string {
fmt.Fprintf(&b, " filter: \"%s\"\n", view.Filter)
}
}
if len(data.SafeOutputs.CreateProjects.FieldDefinitions) > 0 {
b.WriteString(" field-definitions:\n")
for _, field := range data.SafeOutputs.CreateProjects.FieldDefinitions {
fmt.Fprintf(&b, " - name: \"%s\"\n", field.Name)
fmt.Fprintf(&b, " data-type: \"%s\"\n", field.DataType)
if len(field.Options) > 0 {
b.WriteString(" options:\n")
for _, opt := range field.Options {
fmt.Fprintf(&b, " - \"%s\"\n", opt)
}
}
}
}
}

if data.SafeOutputs.UpdateProjects != nil {
Expand Down
27 changes: 27 additions & 0 deletions pkg/parser/schemas/main_workflow_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -4016,6 +4016,33 @@
},
"additionalProperties": false
}
},
"field-definitions": {
"type": "array",
"description": "Optional array of project custom fields to create automatically after project creation. Useful for campaign projects that require a fixed set of fields.",
"items": {
"type": "object",
"required": ["name", "data-type"],
"properties": {
"name": {
"type": "string",
"description": "The field name to create (e.g., 'Campaign Id', 'Priority')"
},
"data-type": {
"type": "string",
"enum": ["DATE", "TEXT", "NUMBER", "SINGLE_SELECT", "ITERATION"],
"description": "The GitHub Projects v2 custom field type"
},
"options": {
"type": "array",
"items": {
"type": "string"
},
"description": "Options for SINGLE_SELECT fields. GitHub does not support adding options later."
}
},
"additionalProperties": false
}
}
},
"additionalProperties": false
Expand Down
3 changes: 3 additions & 0 deletions pkg/workflow/compiler_safe_outputs_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,9 @@ func (c *Compiler) addProjectHandlerManagerConfigEnvVar(steps *[]string, data *W
if len(cfg.Views) > 0 {
handlerConfig["views"] = cfg.Views
}
if len(cfg.FieldDefinitions) > 0 {
handlerConfig["field_definitions"] = cfg.FieldDefinitions
}
config["create_project"] = handlerConfig
}

Expand Down
61 changes: 55 additions & 6 deletions pkg/workflow/create_project.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ var createProjectLog = logger.New("workflow:create_project")
// CreateProjectsConfig holds configuration for creating GitHub Projects V2
type CreateProjectsConfig struct {
BaseSafeOutputConfig `yaml:",inline"`
GitHubToken string `yaml:"github-token,omitempty"`
TargetOwner string `yaml:"target-owner,omitempty"` // Default target owner (org/user) for the new project
TitlePrefix string `yaml:"title-prefix,omitempty"` // Default prefix for auto-generated project titles
Views []ProjectView `yaml:"views,omitempty"` // Project views to create automatically after project creation
GitHubToken string `yaml:"github-token,omitempty"`
TargetOwner string `yaml:"target-owner,omitempty"` // Default target owner (org/user) for the new project
TitlePrefix string `yaml:"title-prefix,omitempty"` // Default prefix for auto-generated project titles
Views []ProjectView `yaml:"views,omitempty"` // Project views to create automatically after project creation
FieldDefinitions []ProjectFieldDefinition `yaml:"field-definitions,omitempty"` // Project field definitions to create automatically after project creation
}

// parseCreateProjectsConfig handles create-project configuration
Expand Down Expand Up @@ -105,10 +106,58 @@ func (c *Compiler) parseCreateProjectsConfig(outputMap map[string]any) *CreatePr
}
}
}

// Parse field-definitions if specified
fieldsData, hasFields := configMap["field-definitions"]
if !hasFields {
// Allow underscore variant as well
fieldsData, hasFields = configMap["field_definitions"]
}
if hasFields {
if fieldsList, ok := fieldsData.([]any); ok {
for i, fieldItem := range fieldsList {
fieldMap, ok := fieldItem.(map[string]any)
if !ok {
continue
}

field := ProjectFieldDefinition{}

if name, exists := fieldMap["name"]; exists {
if nameStr, ok := name.(string); ok {
field.Name = nameStr
}
}

dataType, hasDataType := fieldMap["data-type"]
if !hasDataType {
dataType = fieldMap["data_type"]
}
if dataTypeStr, ok := dataType.(string); ok {
field.DataType = dataTypeStr
}

if options, exists := fieldMap["options"]; exists {
if optionsList, ok := options.([]any); ok {
for _, opt := range optionsList {
if optStr, ok := opt.(string); ok {
field.Options = append(field.Options, optStr)
}
}
}
}

if field.Name != "" && field.DataType != "" {
createProjectsConfig.FieldDefinitions = append(createProjectsConfig.FieldDefinitions, field)
createProjectLog.Printf("Parsed field definition %d: %s (%s)", i+1, field.Name, field.DataType)
}
}
}
}
}

createProjectLog.Printf("Parsed create-project config: max=%d, hasCustomToken=%v, hasTargetOwner=%v, hasTitlePrefix=%v, viewCount=%d",
createProjectsConfig.Max, createProjectsConfig.GitHubToken != "", createProjectsConfig.TargetOwner != "", createProjectsConfig.TitlePrefix != "", len(createProjectsConfig.Views))
createProjectLog.Printf("Parsed create-project config: max=%d, hasCustomToken=%v, hasTargetOwner=%v, hasTitlePrefix=%v, viewCount=%d, fieldDefinitionCount=%d",
createProjectsConfig.Max, createProjectsConfig.GitHubToken != "", createProjectsConfig.TargetOwner != "", createProjectsConfig.TitlePrefix != "", len(createProjectsConfig.Views), len(createProjectsConfig.FieldDefinitions))
return createProjectsConfig
}
createProjectLog.Print("No create-project configuration found")
Expand Down
120 changes: 120 additions & 0 deletions pkg/workflow/create_project_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,14 @@ func TestParseCreateProjectsConfig(t *testing.T) {
assert.Equal(t, expectedView.VisibleFields, config.Views[i].VisibleFields, "View visible fields should match")
assert.Equal(t, expectedView.Description, config.Views[i].Description, "View description should match")
}

// Check field definitions
assert.Len(t, config.FieldDefinitions, len(tt.expectedConfig.FieldDefinitions), "Field definitions count should match")
for i, expectedField := range tt.expectedConfig.FieldDefinitions {
assert.Equal(t, expectedField.Name, config.FieldDefinitions[i].Name, "Field name should match")
assert.Equal(t, expectedField.DataType, config.FieldDefinitions[i].DataType, "Field data type should match")
assert.Equal(t, expectedField.Options, config.FieldDefinitions[i].Options, "Field options should match")
}
}
})
}
Expand Down Expand Up @@ -253,3 +261,115 @@ func TestCreateProjectsConfig_ViewsParsing(t *testing.T) {
assert.Equal(t, "roadmap", config.Views[1].Layout)
assert.Empty(t, config.Views[1].Filter) // No filter specified
}

func TestCreateProjectsConfig_FieldDefinitionsParsing(t *testing.T) {
compiler := NewCompiler(false, "", "test")

outputMap := map[string]any{
"create-project": map[string]any{
"max": 1,
"field-definitions": []any{
map[string]any{
"name": "Campaign Id",
"data-type": "TEXT",
},
map[string]any{
"name": "Priority",
"data-type": "SINGLE_SELECT",
"options": []any{"High", "Medium", "Low"},
},
map[string]any{
"name": "Start Date",
"data-type": "DATE",
},
},
},
}

config := compiler.parseCreateProjectsConfig(outputMap)
require.NotNil(t, config, "Config should not be nil")
require.Len(t, config.FieldDefinitions, 3, "Should parse 3 field definitions")

// Check first field
assert.Equal(t, "Campaign Id", config.FieldDefinitions[0].Name)
assert.Equal(t, "TEXT", config.FieldDefinitions[0].DataType)
assert.Empty(t, config.FieldDefinitions[0].Options)

// Check second field
assert.Equal(t, "Priority", config.FieldDefinitions[1].Name)
assert.Equal(t, "SINGLE_SELECT", config.FieldDefinitions[1].DataType)
assert.Equal(t, []string{"High", "Medium", "Low"}, config.FieldDefinitions[1].Options)

// Check third field
assert.Equal(t, "Start Date", config.FieldDefinitions[2].Name)
assert.Equal(t, "DATE", config.FieldDefinitions[2].DataType)
assert.Empty(t, config.FieldDefinitions[2].Options)
}

func TestCreateProjectsConfig_FieldDefinitionsWithUnderscores(t *testing.T) {
compiler := NewCompiler(false, "", "test")

// Test underscore variant of field-definitions and data-type
outputMap := map[string]any{
"create-project": map[string]any{
"max": 1,
"field_definitions": []any{
map[string]any{
"name": "Worker Workflow",
"data_type": "TEXT",
},
},
},
}

config := compiler.parseCreateProjectsConfig(outputMap)
require.NotNil(t, config, "Config should not be nil")
require.Len(t, config.FieldDefinitions, 1, "Should parse 1 field definition")

assert.Equal(t, "Worker Workflow", config.FieldDefinitions[0].Name)
assert.Equal(t, "TEXT", config.FieldDefinitions[0].DataType)
}

func TestCreateProjectsConfig_ViewsAndFieldDefinitions(t *testing.T) {
compiler := NewCompiler(false, "", "test")

outputMap := map[string]any{
"create-project": map[string]any{
"max": 1,
"target-owner": "myorg",
"views": []any{
map[string]any{
"name": "Campaign Board",
"layout": "board",
},
},
"field-definitions": []any{
map[string]any{
"name": "Campaign Id",
"data-type": "TEXT",
},
map[string]any{
"name": "Size",
"data-type": "SINGLE_SELECT",
"options": []any{"Small", "Medium", "Large"},
},
},
},
}

config := compiler.parseCreateProjectsConfig(outputMap)
require.NotNil(t, config, "Config should not be nil")

// Check views
require.Len(t, config.Views, 1, "Should have 1 view")
assert.Equal(t, "Campaign Board", config.Views[0].Name)
assert.Equal(t, "board", config.Views[0].Layout)

// Check field definitions
require.Len(t, config.FieldDefinitions, 2, "Should have 2 field definitions")
assert.Equal(t, "Campaign Id", config.FieldDefinitions[0].Name)
assert.Equal(t, "TEXT", config.FieldDefinitions[0].DataType)
assert.Equal(t, "Size", config.FieldDefinitions[1].Name)
assert.Equal(t, "SINGLE_SELECT", config.FieldDefinitions[1].DataType)
assert.Equal(t, []string{"Small", "Medium", "Large"}, config.FieldDefinitions[1].Options)
}
Loading