diff --git a/pkg/parser/schema_test.go b/pkg/parser/schema_test.go index 86f389daa73..04f0fcde4d3 100644 --- a/pkg/parser/schema_test.go +++ b/pkg/parser/schema_test.go @@ -1227,6 +1227,46 @@ func TestMainWorkflowSchema_CreateDiscussionRequiredCategoryAllowed(t *testing.T } } +func TestMainWorkflowSchema_GitHubTokenAllowsStepOutputs(t *testing.T) { + t.Parallel() + + frontmatter := map[string]any{ + "on": "daily", + "safe-outputs": map[string]any{ + "github-token": "${{ steps.fetch-token.outputs.my-token }}", + "create-issue": map[string]any{ + "github-token": "${{ steps.fetch-token.outputs.my-token }}", + }, + }, + } + + if err := validateWithSchema(frontmatter, mainWorkflowSchema, "main workflow file"); err != nil { + t.Fatalf("expected steps.*.outputs.* github-token expression to pass schema validation, got: %v", err) + } +} + +func TestMainWorkflowSchema_SkillsGitHubTokenRejectsStepOutputs(t *testing.T) { + t.Parallel() + + frontmatter := map[string]any{ + "on": "daily", + "skills": []any{ + map[string]any{ + "skill": "githubnext/skills@1f181b37d3fe5862ab590648f25a292e345b5de6", + "github-token": "${{ steps.fetch-token.outputs.my-token }}", + }, + }, + } + + err := validateWithSchema(frontmatter, mainWorkflowSchema, "main workflow file") + if err == nil { + t.Fatal("expected skills[].github-token steps.*.outputs.* expression to fail schema validation") + } + if !strings.Contains(err.Error(), "github-token") { + t.Fatalf("expected schema error to mention github-token, got: %v", err) + } +} + func TestMainWorkflowSchemaPushToPullRequestBranchHasMaxPatchSize(t *testing.T) { schemaPath := "schemas/main_workflow_schema.json" schemaContent, err := os.ReadFile(schemaPath) diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index 5ee805b2450..3c2276b7133 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -4038,7 +4038,7 @@ "default": true }, "github-token": { - "$ref": "#/$defs/github_token", + "$ref": "#/$defs/github_token_same_job", "description": "Optional custom GitHub token (e.g., '${{ secrets.CUSTOM_PAT }}'). For 'remote' type, defaults to GH_AW_GITHUB_TOKEN if not specified." }, "toolsets": { @@ -4590,7 +4590,7 @@ "description": "Controls whether AI-generated footer is added to the managed comment. Defaults to true." }, "github-token": { - "$ref": "#/$defs/github_token", + "$ref": "#/$defs/github_token_same_job", "description": "GitHub token to use for comment-memory operations. Overrides global github-token if specified." }, "staged": { @@ -5248,7 +5248,7 @@ "default": true }, "github-token": { - "$ref": "#/$defs/github_token", + "$ref": "#/$defs/github_token_same_job", "description": "GitHub token to use for this specific output type. Overrides global github-token if specified." }, "staged": { @@ -5351,7 +5351,7 @@ "description": "List of additional repositories in format 'owner/repo' that agent sessions can be created in. When specified, the agent can use a 'repo' field in the output to specify which repository to create the agent session in. The target repository (current or target-repo) is always implicitly allowed." }, "github-token": { - "$ref": "#/$defs/github_token", + "$ref": "#/$defs/github_token_same_job", "description": "GitHub token to use for this specific output type. Overrides global github-token if specified." }, "staged": { @@ -5426,7 +5426,7 @@ "description": "List of additional repositories in format 'owner/repo' that agent sessions can be created in. When specified, the agent can use a 'repo' field in the output to specify which repository to create the agent session in. The target repository (current or target-repo) is always implicitly allowed." }, "github-token": { - "$ref": "#/$defs/github_token", + "$ref": "#/$defs/github_token_same_job", "description": "GitHub token to use for this specific output type. Overrides global github-token if specified." }, "staged": { @@ -5483,7 +5483,7 @@ ] }, "github-token": { - "$ref": "#/$defs/github_token", + "$ref": "#/$defs/github_token_same_job", "description": "GitHub token to use for this specific output type. Overrides global github-token if specified." }, "project": { @@ -5648,7 +5648,7 @@ ] }, "github-token": { - "$ref": "#/$defs/github_token", + "$ref": "#/$defs/github_token_same_job", "description": "GitHub token to use for this specific output type. Must have Projects write permission. Overrides global github-token if specified." }, "target-owner": { @@ -5779,7 +5779,7 @@ ] }, "github-token": { - "$ref": "#/$defs/github_token", + "$ref": "#/$defs/github_token_same_job", "description": "GitHub token to use for this specific output type. Overrides global github-token if specified. Must have Projects: Read+Write permission." }, "project": { @@ -5935,7 +5935,7 @@ "description": "Time until the discussion expires and should be automatically closed. Supports integer (days), relative time format like '2h' (2 hours), '7d' (7 days), '2w' (2 weeks), '1m' (1 month), '1y' (1 year), or false to disable expiration. Minimum duration: 2 hours. When set, a maintenance workflow will be generated. Defaults to 7 days if not specified." }, "github-token": { - "$ref": "#/$defs/github_token", + "$ref": "#/$defs/github_token_same_job", "description": "GitHub token to use for this specific output type. Overrides global github-token if specified." }, "staged": { @@ -6176,7 +6176,7 @@ ] }, "github-token": { - "$ref": "#/$defs/github_token", + "$ref": "#/$defs/github_token_same_job", "description": "GitHub token to use for this specific output type. Overrides global github-token if specified." } }, @@ -6360,7 +6360,7 @@ "description": "List of additional repositories in format 'owner/repo' that pull requests can be closed in. The target repository is always implicitly allowed." }, "github-token": { - "$ref": "#/$defs/github_token", + "$ref": "#/$defs/github_token_same_job", "description": "GitHub token to use for this specific output type. Overrides global github-token if specified." }, "staged": { @@ -6457,7 +6457,7 @@ "description": "List of additional repositories in format 'owner/repo' that pull requests can be marked as ready in. The target repository is always implicitly allowed." }, "github-token": { - "$ref": "#/$defs/github_token", + "$ref": "#/$defs/github_token_same_job", "description": "GitHub token to use for this specific output type. Overrides global github-token if specified." }, "staged": { @@ -6597,7 +6597,7 @@ "default": true }, "github-token": { - "$ref": "#/$defs/github_token", + "$ref": "#/$defs/github_token_same_job", "description": "GitHub token to use for this specific output type. Overrides global github-token if specified." }, "required-labels": { @@ -6811,11 +6811,11 @@ ] }, "github-token": { - "$ref": "#/$defs/github_token", + "$ref": "#/$defs/github_token_same_job", "description": "GitHub token to use for this specific output type. Overrides global github-token if specified." }, "head-github-token": { - "$ref": "#/$defs/github_token", + "$ref": "#/$defs/github_token_same_job", "description": "Optional GitHub token used only for branch writes to head-repo. Use this when the upstream PR repository and the fork head repository require different credentials." }, "head-github-app": { @@ -7117,7 +7117,7 @@ "description": "List of additional repositories in format 'owner/repo' that PR review comments can be created in. When specified, the agent can use a 'repo' field in the output to specify which repository to create the review comment in. The target repository (current or target-repo) is always implicitly allowed." }, "github-token": { - "$ref": "#/$defs/github_token", + "$ref": "#/$defs/github_token_same_job", "description": "GitHub token to use for this specific output type. Overrides global github-token if specified." }, "staged": { @@ -7226,7 +7226,7 @@ "description": "When true, after posting a replacement review this workflow dismisses older REQUEST_CHANGES reviews previously posted by the same workflow on the same pull request. This is best-effort and requires workflow markers in prior review bodies." }, "github-token": { - "$ref": "#/$defs/github_token", + "$ref": "#/$defs/github_token_same_job", "description": "GitHub token to use for this specific output type. Overrides global github-token if specified." }, "staged": { @@ -7308,7 +7308,7 @@ "description": "List of additional repositories in format 'owner/repo' where review dismissals are allowed. The target repository (current or target-repo) is always implicitly allowed." }, "github-token": { - "$ref": "#/$defs/github_token", + "$ref": "#/$defs/github_token_same_job", "description": "GitHub token to use for this specific output type. Overrides global github-token if specified." }, "staged": { @@ -7398,7 +7398,7 @@ "default": true }, "github-token": { - "$ref": "#/$defs/github_token", + "$ref": "#/$defs/github_token_same_job", "description": "GitHub token to use for this specific output type. Overrides global github-token if specified." }, "staged": { @@ -7480,7 +7480,7 @@ "description": "List of additional repositories in format 'owner/repo' that PR review threads can be resolved in. When specified, the agent can use a 'repo' field in the output to specify which repository to resolve threads in. The target repository (current or target-repo) is always implicitly allowed." }, "github-token": { - "$ref": "#/$defs/github_token", + "$ref": "#/$defs/github_token_same_job", "description": "GitHub token to use for this specific output type. Overrides global github-token if specified." }, "staged": { @@ -7550,7 +7550,7 @@ "description": "Driver name for SARIF tool.driver.name field (default: 'GitHub Agentic Workflows Security Scanner')" }, "github-token": { - "$ref": "#/$defs/github_token", + "$ref": "#/$defs/github_token_same_job", "description": "GitHub token to use for this specific output type. Overrides global github-token if specified." }, "target-repo": { @@ -7616,7 +7616,7 @@ ] }, "github-token": { - "$ref": "#/$defs/github_token", + "$ref": "#/$defs/github_token_same_job", "description": "GitHub token to use for this specific output type. Overrides global github-token if specified." }, "staged": { @@ -7675,7 +7675,7 @@ ] }, "github-token": { - "$ref": "#/$defs/github_token", + "$ref": "#/$defs/github_token_same_job", "description": "GitHub token to use for this specific output type. Overrides global github-token if specified." }, "staged": { @@ -7781,7 +7781,7 @@ "description": "Target repository in format 'owner/repo' for cross-repository label addition. Takes precedence over trial target repo settings." }, "github-token": { - "$ref": "#/$defs/github_token", + "$ref": "#/$defs/github_token_same_job", "description": "GitHub token to use for this specific output type. Overrides global github-token if specified." }, "allowed-repos": { @@ -7886,7 +7886,7 @@ "description": "Target repository in format 'owner/repo' for cross-repository label removal. Takes precedence over trial target repo settings." }, "github-token": { - "$ref": "#/$defs/github_token", + "$ref": "#/$defs/github_token_same_job", "description": "GitHub token to use for this specific output type. Overrides global github-token if specified." }, "allowed-repos": { @@ -8030,7 +8030,7 @@ "description": "List of additional repositories in format 'owner/repo' that reviewers can be added in. The target repository is always implicitly allowed." }, "github-token": { - "$ref": "#/$defs/github_token", + "$ref": "#/$defs/github_token_same_job", "description": "GitHub token to use for this specific output type. Overrides global github-token if specified." }, "staged": { @@ -8120,7 +8120,7 @@ "description": "List of additional repositories in format 'owner/repo' that milestone assignments can target. The target repository is always implicitly allowed." }, "github-token": { - "$ref": "#/$defs/github_token", + "$ref": "#/$defs/github_token_same_job", "description": "GitHub token to use for this specific output type. Overrides global github-token if specified." }, "staged": { @@ -8244,7 +8244,7 @@ "description": "Base branch for pull request creation in the target repository. Defaults to the target repo's default branch. Only relevant when pull-request-repo is configured." }, "github-token": { - "$ref": "#/$defs/github_token", + "$ref": "#/$defs/github_token_same_job", "description": "GitHub token to use for this specific output type. Overrides global github-token if specified." }, "staged": { @@ -8330,7 +8330,7 @@ "default": false }, "github-token": { - "$ref": "#/$defs/github_token", + "$ref": "#/$defs/github_token_same_job", "description": "GitHub token to use for this specific output type. Overrides global github-token if specified." }, "allowed-repos": { @@ -8436,7 +8436,7 @@ "description": "List of allowed repositories in format 'owner/repo' for cross-repository unassignment operations. Use with 'repo' field in tool calls." }, "github-token": { - "$ref": "#/$defs/github_token", + "$ref": "#/$defs/github_token_same_job", "description": "GitHub token to use for this specific output type. Overrides global github-token if specified." }, "staged": { @@ -8554,7 +8554,7 @@ ] }, "github-token": { - "$ref": "#/$defs/github_token", + "$ref": "#/$defs/github_token_same_job", "description": "GitHub token to use for this specific output type. Overrides global github-token if specified." }, "staged": { @@ -8643,7 +8643,7 @@ "description": "List of additional repositories in format 'owner/repo' that issues can be updated in. When specified, the agent can use a 'repo' field in the output to specify which repository to update the issue in. The target repository (current or target-repo) is always implicitly allowed." }, "github-token": { - "$ref": "#/$defs/github_token", + "$ref": "#/$defs/github_token_same_job", "description": "GitHub token to use for this specific output type. Overrides global github-token if specified." }, "staged": { @@ -8747,7 +8747,7 @@ "description": "List of additional repositories in format 'owner/repo' that pull requests can be updated in. The target repository is always implicitly allowed." }, "github-token": { - "$ref": "#/$defs/github_token", + "$ref": "#/$defs/github_token_same_job", "description": "GitHub token to use for this specific output type. Overrides global github-token if specified." }, "staged": { @@ -8857,7 +8857,7 @@ "description": "List of additional repositories in format 'owner/repo' that pull requests can be merged in. The target repository is always implicitly allowed." }, "github-token": { - "$ref": "#/$defs/github_token", + "$ref": "#/$defs/github_token_same_job", "description": "GitHub token to use for this specific output type. Overrides global github-token if specified." }, "staged": { @@ -8992,7 +8992,7 @@ "maximum": 10240 }, "github-token": { - "$ref": "#/$defs/github_token", + "$ref": "#/$defs/github_token_same_job", "description": "GitHub token to use for this specific output type. Overrides global github-token if specified." }, "staged": { @@ -9295,11 +9295,11 @@ "description": "List of additional repositories in format 'owner/repo' where issue types can be set. When specified, the agent can use a 'repo' field in the output to specify which repository to target. The target repository (current or target-repo) is always implicitly allowed." }, "github-token": { - "$ref": "#/$defs/github_token", + "$ref": "#/$defs/github_token_same_job", "description": "GitHub token to use for this specific output type. Overrides global github-token if specified." }, "head-github-token": { - "$ref": "#/$defs/github_token", + "$ref": "#/$defs/github_token_same_job", "description": "Optional GitHub token used only for branch writes to head-repo. Use this when the upstream pull request repository and the fork head repository require different credentials." }, "issue-intent": { @@ -9391,7 +9391,7 @@ "description": "List of additional repositories in format 'owner/repo' where issue fields can be updated. When specified, the agent can use a 'repo' field in the output to specify which repository to target. The target repository (current or target-repo) is always implicitly allowed." }, "github-token": { - "$ref": "#/$defs/github_token", + "$ref": "#/$defs/github_token_same_job", "description": "GitHub token to use for this specific output type. Overrides global github-token if specified." }, "issue-intent": { @@ -9469,7 +9469,7 @@ ] }, "github-token": { - "$ref": "#/$defs/github_token", + "$ref": "#/$defs/github_token_same_job", "description": "GitHub token to use for dispatching workflows. Overrides global github-token if specified." }, "target-repo": { @@ -9621,7 +9621,7 @@ ] }, "github-token": { - "$ref": "#/$defs/github_token", + "$ref": "#/$defs/github_token_same_job", "description": "GitHub token to use for dispatching. Overrides global github-token if specified." }, "staged": { @@ -9671,7 +9671,7 @@ ] }, "github-token": { - "$ref": "#/$defs/github_token", + "$ref": "#/$defs/github_token_same_job", "description": "GitHub token passed to called workflows. Overrides global github-token if specified." }, "staged": { @@ -9751,7 +9751,7 @@ "default": [] }, "github-token": { - "$ref": "#/$defs/github_token", + "$ref": "#/$defs/github_token_same_job", "description": "GitHub token to use for this specific output type. Overrides global github-token if specified." }, "staged": { @@ -9829,7 +9829,7 @@ "default": [] }, "github-token": { - "$ref": "#/$defs/github_token", + "$ref": "#/$defs/github_token_same_job", "description": "GitHub token to use for this specific output type. Overrides global github-token if specified." }, "staged": { @@ -9890,7 +9890,7 @@ ] }, "github-token": { - "$ref": "#/$defs/github_token", + "$ref": "#/$defs/github_token_same_job", "description": "GitHub token to use for this specific output type. Overrides global github-token if specified." }, "report-as-issue": { @@ -9976,7 +9976,7 @@ ] }, "github-token": { - "$ref": "#/$defs/github_token", + "$ref": "#/$defs/github_token_same_job", "description": "GitHub token to use for this specific output type. Overrides global github-token if specified." }, "staged": { @@ -10097,7 +10097,7 @@ "additionalProperties": false }, "github-token": { - "$ref": "#/$defs/github_token", + "$ref": "#/$defs/github_token_same_job", "description": "GitHub token to use for this specific output type. Overrides global github-token if specified." }, "staged": { @@ -10195,7 +10195,7 @@ "additionalProperties": false }, "github-token": { - "$ref": "#/$defs/github_token", + "$ref": "#/$defs/github_token_same_job", "description": "GitHub token to use for safe output jobs. Typically a secret reference like ${{ secrets.GITHUB_TOKEN }} or ${{ secrets.CUSTOM_PAT }}", "examples": ["${{ secrets.GITHUB_TOKEN }}", "${{ secrets.CUSTOM_PAT }}", "${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}"] }, @@ -10354,7 +10354,7 @@ "$ref": "#/properties/permissions" }, "github-token": { - "$ref": "#/$defs/github_token", + "$ref": "#/$defs/github_token_same_job", "description": "GitHub token for this specific job" }, "output": { @@ -10926,7 +10926,7 @@ "default": [] }, "github-token": { - "$ref": "#/$defs/github_token", + "$ref": "#/$defs/github_token_same_job", "description": "GitHub token to use for this specific output type. Overrides global github-token if specified." }, "staged": { @@ -11046,7 +11046,7 @@ "description": "Target repository in format 'owner/repo' for cross-repository label replacement. Takes precedence over trial target repo settings." }, "github-token": { - "$ref": "#/$defs/github_token", + "$ref": "#/$defs/github_token_same_job", "description": "GitHub token to use for this specific output type. Overrides global github-token if specified." }, "allowed-repos": { @@ -13251,9 +13251,15 @@ "github_token": { "type": "string", "pattern": "^\\$\\{\\{\\s*(secrets\\.[A-Za-z_][A-Za-z0-9_]*(\\s*\\|\\|\\s*secrets\\.[A-Za-z_][A-Za-z0-9_]*)*|needs\\.[A-Za-z_][A-Za-z0-9_]*\\.outputs\\.[A-Za-z_][A-Za-z0-9_]*)\\s*\\}\\}$", - "description": "GitHub token expression. Accepts a secrets expression (e.g., `${{ secrets.NAME }}` or `${{ secrets.NAME1 || secrets.NAME2 }}`) or a job output expression (e.g., `${{ needs.auth.outputs.token }}`). Pattern details: secret names match `[A-Za-z_][A-Za-z0-9_]*`; job IDs and output names in dot notation match `[A-Za-z_][A-Za-z0-9_]*` (identifiers without hyphens).", + "description": "GitHub token expression for cross-job or activation-time contexts. Accepts a secrets expression (e.g., `${{ secrets.NAME }}` or `${{ secrets.NAME1 || secrets.NAME2 }}`) or a cross-job output expression (e.g., `${{ needs.auth.outputs.token }}`). Pattern details: secret names, job IDs, and output names in dot notation match `[A-Za-z_][A-Za-z0-9_]*` (identifiers without hyphens).", "examples": ["${{ secrets.GITHUB_TOKEN }}", "${{ secrets.CUSTOM_PAT }}", "${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}", "${{ needs.auth.outputs.token }}"] }, + "github_token_same_job": { + "type": "string", + "pattern": "^\\$\\{\\{\\s*(secrets\\.[A-Za-z_][A-Za-z0-9_]*(\\s*\\|\\|\\s*secrets\\.[A-Za-z_][A-Za-z0-9_]*)*|needs\\.[A-Za-z_][A-Za-z0-9_]*\\.outputs\\.[A-Za-z_][A-Za-z0-9_]*|steps\\.[A-Za-z_][A-Za-z0-9_-]*\\.outputs\\.[A-Za-z_][A-Za-z0-9_-]*)\\s*\\}\\}$", + "description": "GitHub token expression for same-job contexts. Accepts a secrets expression (e.g., `${{ secrets.NAME }}` or `${{ secrets.NAME1 || secrets.NAME2 }}`), a cross-job output expression (e.g., `${{ needs.auth.outputs.token }}`), or a same-job step output expression (e.g., `${{ steps.fetch-token.outputs.my-token }}`). Pattern details: secret names and job IDs match `[A-Za-z_][A-Za-z0-9_]*`; same-job step IDs and output names match `[A-Za-z_][A-Za-z0-9_-]*`.", + "examples": ["${{ secrets.GITHUB_TOKEN }}", "${{ secrets.CUSTOM_PAT }}", "${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}", "${{ needs.auth.outputs.token }}", "${{ steps.fetch-token.outputs.my-token }}"] + }, "github_app": { "type": "object", "description": "GitHub App credentials for minting installation access tokens.", diff --git a/pkg/workflow/github_token_validation_test.go b/pkg/workflow/github_token_validation_test.go index 63ff6c9bc47..cd432c4aec5 100644 --- a/pkg/workflow/github_token_validation_test.go +++ b/pkg/workflow/github_token_validation_test.go @@ -174,6 +174,11 @@ func TestGitHubTokenValidationInSafeOutputs(t *testing.T) { token: "${{ needs.auth.outputs.token }}", expectError: false, }, + { + name: "valid same-job step output token in safe-outputs", + token: "${{ steps.fetch-token.outputs.my-token }}", + expectError: false, + }, { name: "invalid token in safe-outputs", token: "ghp_plaintext_token", @@ -236,6 +241,11 @@ func TestGitHubTokenValidationInIndividualSafeOutput(t *testing.T) { token: "${{ needs.auth.outputs.token }}", expectError: false, }, + { + name: "valid same-job step output token in individual safe-output", + token: "${{ steps.fetch-token.outputs.my-token }}", + expectError: false, + }, { name: "invalid token in individual safe-output", token: "github_pat_plaintext", @@ -298,6 +308,11 @@ func TestGitHubTokenValidationInGitHubTool(t *testing.T) { token: "${{ needs.auth.outputs.token }}", expectError: false, }, + { + name: "valid same-job step output token in github tool", + token: "${{ steps.fetch-token.outputs.my-token }}", + expectError: false, + }, { name: "invalid token in github tool", token: "plaintext_secret", diff --git a/pkg/workflow/skills_frontmatter.go b/pkg/workflow/skills_frontmatter.go index 8a0e90383c8..6f575801925 100644 --- a/pkg/workflow/skills_frontmatter.go +++ b/pkg/workflow/skills_frontmatter.go @@ -12,7 +12,7 @@ import ( var skillsFrontmatterLog = logger.New("workflow:skills_frontmatter") var skillSpecRegexp = regexp.MustCompile(`^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+(?:/[A-Za-z0-9_.-]+(?:/[A-Za-z0-9_.-]+)*)?@[0-9a-f]{40}$`) -var githubTokenExpressionRegexp = regexp.MustCompile(`^\$\{\{\s*(secrets\.[A-Za-z_][A-Za-z0-9_]*(\s*\|\|\s*secrets\.[A-Za-z_][A-Za-z0-9_]*)*|needs\.[A-Za-z_][A-Za-z0-9_]*\.outputs\.[A-Za-z_][A-Za-z0-9_]*)\s*\}\}$`) +var skillsGitHubTokenExpressionRegexp = regexp.MustCompile(`^\$\{\{\s*(secrets\.[A-Za-z_][A-Za-z0-9_]*(\s*\|\|\s*secrets\.[A-Za-z_][A-Za-z0-9_]*)*|needs\.[A-Za-z_][A-Za-z0-9_]*\.outputs\.[A-Za-z_][A-Za-z0-9_]*)\s*\}\}$`) // SkillReference describes a single skills[] entry in workflow frontmatter. // It supports both legacy string-only entries and object entries with per-skill auth. @@ -92,7 +92,7 @@ func validateFrontmatterSkills(frontmatter map[string]any) error { if !ok { return fmt.Errorf("skills[%d].github-token must be a string. Example: skills[%d].github-token: \"${{ secrets.MY_TOKEN }}\"", i, i) } - if !githubTokenExpressionRegexp.MatchString(token) { + if !skillsGitHubTokenExpressionRegexp.MatchString(token) { return fmt.Errorf( "skills[%d].github-token must be a valid GitHub token expression. Example: skills[%d].github-token: \"${{ secrets.NAME }}\" or \"${{ needs.auth.outputs.token }}\"", i, diff --git a/pkg/workflow/skills_frontmatter_test.go b/pkg/workflow/skills_frontmatter_test.go index f8e3170fbcb..20341036ea3 100644 --- a/pkg/workflow/skills_frontmatter_test.go +++ b/pkg/workflow/skills_frontmatter_test.go @@ -84,6 +84,19 @@ func TestValidateFrontmatterSkills(t *testing.T) { require.NoError(t, err) }) + t.Run("rejects object form with steps output github-token", func(t *testing.T) { + err := validateFrontmatterSkills(map[string]any{ + "skills": []any{ + map[string]any{ + "skill": "githubnext/skills@1f181b37d3fe5862ab590648f25a292e345b5de6", + "github-token": "${{ steps.fetch_token.outputs.token }}", + }, + }, + }) + require.Error(t, err) + require.ErrorContains(t, err, "skills[0].github-token must be a valid GitHub token expression") + }) + t.Run("rejects object form with github-token literal", func(t *testing.T) { err := validateFrontmatterSkills(map[string]any{ "skills": []any{