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
1 change: 1 addition & 0 deletions .github/workflows/dependabot-burner.lock.yml

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

1 change: 1 addition & 0 deletions .github/workflows/security-alert-burndown.lock.yml

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

1 change: 1 addition & 0 deletions .github/workflows/smoke-project.lock.yml

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

1 change: 1 addition & 0 deletions .github/workflows/test-project-url-default.lock.yml

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

32 changes: 27 additions & 5 deletions pkg/workflow/compiler_safe_outputs_steps.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,26 +172,48 @@ func (c *Compiler) buildHandlerManagerStep(data *WorkflowData) []string {
// Add all safe output configuration env vars (still needed by individual handlers)
c.addAllSafeOutputConfigEnvVars(&steps, data)

// Add GH_AW_PROJECT_URL environment variable for project operations
// This is set from the project URL configured in update-project or create-project-status-update safe-outputs
// The project field is REQUIRED in both configurations (enforced by schema validation)
// Add GH_AW_PROJECT_URL and GH_AW_PROJECT_GITHUB_TOKEN environment variables for project operations
// These are set from the project URL and token configured in any project-related safe-output:
// - update-project
// - create-project-status-update
// - create-project
//
// The project field is REQUIRED in update-project and create-project-status-update (enforced by schema validation)
// Agents can optionally override this per-message by including a project field in their output
//
// Note: If both update-project and create-project-status-update are configured, we prefer update-project's URL
// This is only relevant for the environment variable - each configuration must explicitly specify its own project URL
// Note: If multiple project configs are present, we prefer update-project > create-project-status-update > create-project
// This is only relevant for the environment variables - each configuration must explicitly specify its own settings
var projectURL string
var projectToken string

// Check update-project first (highest priority)
if data.SafeOutputs.UpdateProjects != nil && data.SafeOutputs.UpdateProjects.Project != "" {
projectURL = data.SafeOutputs.UpdateProjects.Project
projectToken = getEffectiveProjectGitHubToken(data.SafeOutputs.UpdateProjects.GitHubToken, data.GitHubToken)
consolidatedSafeOutputsStepsLog.Printf("Setting GH_AW_PROJECT_URL from update-project config: %s", projectURL)
consolidatedSafeOutputsStepsLog.Printf("Setting GH_AW_PROJECT_GITHUB_TOKEN from update-project config")
} else if data.SafeOutputs.CreateProjectStatusUpdates != nil && data.SafeOutputs.CreateProjectStatusUpdates.Project != "" {
projectURL = data.SafeOutputs.CreateProjectStatusUpdates.Project
projectToken = getEffectiveProjectGitHubToken(data.SafeOutputs.CreateProjectStatusUpdates.GitHubToken, data.GitHubToken)
consolidatedSafeOutputsStepsLog.Printf("Setting GH_AW_PROJECT_URL from create-project-status-update config: %s", projectURL)
consolidatedSafeOutputsStepsLog.Printf("Setting GH_AW_PROJECT_GITHUB_TOKEN from create-project-status-update config")
}

// Check create-project for token even if no URL is set (create-project doesn't have a project URL field)
// This ensures GH_AW_PROJECT_GITHUB_TOKEN is set when create-project is configured
if projectToken == "" && data.SafeOutputs.CreateProjects != nil {
projectToken = getEffectiveProjectGitHubToken(data.SafeOutputs.CreateProjects.GitHubToken, data.GitHubToken)
consolidatedSafeOutputsStepsLog.Printf("Setting GH_AW_PROJECT_GITHUB_TOKEN from create-project config")
}

if projectURL != "" {
steps = append(steps, fmt.Sprintf(" GH_AW_PROJECT_URL: %q\n", projectURL))
}

if projectToken != "" {
steps = append(steps, fmt.Sprintf(" GH_AW_PROJECT_GITHUB_TOKEN: %s\n", projectToken))
}

// With section for github-token
// Use the standard safe outputs token for all operations
// Project-specific handlers (create_project) will use custom tokens from their handler config
Expand Down
156 changes: 156 additions & 0 deletions pkg/workflow/safe_outputs_handler_manager_token_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
//go:build !integration

package workflow

import (
"strings"
"testing"

"github.com/stretchr/testify/assert"
)

// TestHandlerManagerProjectGitHubTokenEnvVar verifies that GH_AW_PROJECT_GITHUB_TOKEN
// is exposed as an environment variable in the consolidated safe outputs handler step
// when any project-related safe output is configured
func TestHandlerManagerProjectGitHubTokenEnvVar(t *testing.T) {
tests := []struct {
name string
frontmatter map[string]any
expectedEnvVarValue string
shouldHaveToken bool
}{
{
name: "update-project with custom github-token",
frontmatter: map[string]any{
"name": "Test Workflow",
"safe-outputs": map[string]any{
"update-project": map[string]any{
"github-token": "${{ secrets.PROJECTS_PAT }}",
"project": "https://github.com/orgs/myorg/projects/1",
},
},
},
expectedEnvVarValue: "GH_AW_PROJECT_GITHUB_TOKEN: ${{ secrets.PROJECTS_PAT }}",
shouldHaveToken: true,
},
{
name: "update-project without custom github-token (uses GH_AW_PROJECT_GITHUB_TOKEN)",
frontmatter: map[string]any{
"name": "Test Workflow",
"safe-outputs": map[string]any{
"update-project": map[string]any{
"project": "https://github.com/orgs/myorg/projects/1",
},
},
},
expectedEnvVarValue: "GH_AW_PROJECT_GITHUB_TOKEN: ${{ secrets.GH_AW_PROJECT_GITHUB_TOKEN }}",
shouldHaveToken: true,
},
{
name: "update-project with top-level github-token",
frontmatter: map[string]any{
"name": "Test Workflow",
"github-token": "${{ secrets.CUSTOM_TOKEN }}",
"safe-outputs": map[string]any{
"update-project": map[string]any{
"project": "https://github.com/orgs/myorg/projects/1",
},
},
},
expectedEnvVarValue: "GH_AW_PROJECT_GITHUB_TOKEN: ${{ secrets.CUSTOM_TOKEN }}",
shouldHaveToken: true,
},
{
name: "create-project-status-update with custom github-token",
frontmatter: map[string]any{
"name": "Test Workflow",
"safe-outputs": map[string]any{
"create-project-status-update": map[string]any{
"github-token": "${{ secrets.STATUS_PAT }}",
"project": "https://github.com/orgs/myorg/projects/2",
},
},
},
expectedEnvVarValue: "GH_AW_PROJECT_GITHUB_TOKEN: ${{ secrets.STATUS_PAT }}",
shouldHaveToken: true,
},
{
name: "create-project with custom github-token (no project URL)",
frontmatter: map[string]any{
"name": "Test Workflow",
"safe-outputs": map[string]any{
"create-project": map[string]any{
"github-token": "${{ secrets.CREATE_PAT }}",
},
},
},
expectedEnvVarValue: "GH_AW_PROJECT_GITHUB_TOKEN: ${{ secrets.CREATE_PAT }}",
shouldHaveToken: true,
},
{
name: "multiple project configs - update-project takes precedence",
frontmatter: map[string]any{
"name": "Test Workflow",
"safe-outputs": map[string]any{
"update-project": map[string]any{
"github-token": "${{ secrets.UPDATE_PAT }}",
"project": "https://github.com/orgs/myorg/projects/1",
},
"create-project-status-update": map[string]any{
"github-token": "${{ secrets.STATUS_PAT }}",
"project": "https://github.com/orgs/myorg/projects/2",
},
"create-project": map[string]any{
"github-token": "${{ secrets.CREATE_PAT }}",
},
},
},
expectedEnvVarValue: "GH_AW_PROJECT_GITHUB_TOKEN: ${{ secrets.UPDATE_PAT }}",
shouldHaveToken: true,
},
{
name: "no project configs - no token set",
frontmatter: map[string]any{
"name": "Test Workflow",
"safe-outputs": map[string]any{
"add-comment": map[string]any{
"max": 5,
},
},
},
shouldHaveToken: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
compiler := NewCompiler()

// Parse frontmatter
workflowData := &WorkflowData{
Name: "test-workflow",
SafeOutputs: compiler.extractSafeOutputsConfig(tt.frontmatter),
}

// Set top-level github-token if present in frontmatter
if githubToken, ok := tt.frontmatter["github-token"].(string); ok {
workflowData.GitHubToken = githubToken
}

// Build the handler manager step
steps := compiler.buildHandlerManagerStep(workflowData)
yamlStr := strings.Join(steps, "")

if tt.shouldHaveToken {
// Check that the environment variable is present with the expected value
assert.Contains(t, yamlStr, tt.expectedEnvVarValue,
"Expected environment variable %q to be set in handler manager step",
tt.expectedEnvVarValue)
} else {
// Check that GH_AW_PROJECT_GITHUB_TOKEN is NOT set
assert.NotContains(t, yamlStr, "GH_AW_PROJECT_GITHUB_TOKEN",
"Expected GH_AW_PROJECT_GITHUB_TOKEN to NOT be set when no project configs are present")
}
})
}
}
Loading