From 84a56e68509fc7ff57a42b09c2a531ebbaafe39c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 15 Feb 2026 22:28:05 +0000
Subject: [PATCH 1/5] Initial plan
From 1ae09daf8fa46cb05251b77d833b34fe4743de99 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 15 Feb 2026 22:36:53 +0000
Subject: [PATCH 2/5] Add core skip-users functionality with tests
- Add extractSkipUsers() function in role_checks.go
- Add SkipUsers field to WorkflowData type
- Add CheckSkipUsersStepID and SkipUsersOkOutput constants
- Create check_skip_users.cjs JavaScript check script
- Generate skip-users check step in pre-activation job
- Add skip-users condition to activated output
- Update frontmatter YAML extraction for skip-users comments
- Add skip-users to JSON schema
- Add comprehensive tests in skip_users_test.go (all passing)
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
actions/setup/js/check_skip_users.cjs | 45 +++
pkg/constants/constants.go | 2 +
pkg/parser/schemas/main_workflow_schema.json | 18 +
pkg/workflow/compiler_activation_jobs.go | 26 ++
pkg/workflow/compiler_jobs.go | 7 +-
.../compiler_orchestrator_workflow.go | 1 +
pkg/workflow/compiler_types.go | 1 +
pkg/workflow/frontmatter_extraction_yaml.go | 26 ++
pkg/workflow/role_checks.go | 21 ++
pkg/workflow/skip_users_test.go | 313 ++++++++++++++++++
10 files changed, 457 insertions(+), 3 deletions(-)
create mode 100644 actions/setup/js/check_skip_users.cjs
create mode 100644 pkg/workflow/skip_users_test.go
diff --git a/actions/setup/js/check_skip_users.cjs b/actions/setup/js/check_skip_users.cjs
new file mode 100644
index 0000000000..d8490ea622
--- /dev/null
+++ b/actions/setup/js/check_skip_users.cjs
@@ -0,0 +1,45 @@
+// @ts-check
+///
+
+/**
+ * Check if the workflow should be skipped based on user identity
+ * Reads skip-users from GH_AW_SKIP_USERS environment variable
+ * If the github.actor is in the skip-users list, set skip_users_ok to false (skip the workflow)
+ * Otherwise, set skip_users_ok to true (allow the workflow to proceed)
+ */
+async function main() {
+ const { eventName } = context;
+ const actor = context.actor;
+
+ // Parse skip-users from environment variable
+ const skipUsersEnv = process.env.GH_AW_SKIP_USERS;
+ if (!skipUsersEnv || skipUsersEnv.trim() === "") {
+ // No skip-users configured, workflow should proceed
+ core.info("✅ No skip-users configured, workflow will proceed");
+ core.setOutput("skip_users_ok", "true");
+ core.setOutput("result", "no_skip_users");
+ return;
+ }
+
+ const skipUsers = skipUsersEnv
+ .split(",")
+ .map(u => u.trim())
+ .filter(u => u);
+ core.info(`Checking if user '${actor}' is in skip-users: ${skipUsers.join(", ")}`);
+
+ // Check if the actor is in the skip-users list
+ if (skipUsers.includes(actor)) {
+ // User is in skip-users, skip the workflow
+ core.info(`❌ User '${actor}' is in skip-users [${skipUsers.join(", ")}]. Workflow will be skipped.`);
+ core.setOutput("skip_users_ok", "false");
+ core.setOutput("result", "skipped");
+ core.setOutput("error_message", `Workflow skipped: User '${actor}' is in skip-users: [${skipUsers.join(", ")}]`);
+ } else {
+ // User is NOT in skip-users, allow workflow to proceed
+ core.info(`✅ User '${actor}' is NOT in skip-users [${skipUsers.join(", ")}]. Workflow will proceed.`);
+ core.setOutput("skip_users_ok", "true");
+ core.setOutput("result", "not_skipped");
+ }
+}
+
+module.exports = { main };
diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go
index f80782a1f9..9b00f8df45 100644
--- a/pkg/constants/constants.go
+++ b/pkg/constants/constants.go
@@ -649,6 +649,7 @@ const CheckSkipIfNoMatchStepID StepID = "check_skip_if_no_match"
const CheckCommandPositionStepID StepID = "check_command_position"
const CheckRateLimitStepID StepID = "check_rate_limit"
const CheckSkipRolesStepID StepID = "check_skip_roles"
+const CheckSkipUsersStepID StepID = "check_skip_users"
// Output names for pre-activation job steps
const IsTeamMemberOutput = "is_team_member"
@@ -659,6 +660,7 @@ const CommandPositionOkOutput = "command_position_ok"
const MatchedCommandOutput = "matched_command"
const RateLimitOkOutput = "rate_limit_ok"
const SkipRolesOkOutput = "skip_roles_ok"
+const SkipUsersOkOutput = "skip_users_ok"
const ActivatedOutput = "activated"
// Rate limit defaults
diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json
index e68ba93073..ca50204288 100644
--- a/pkg/parser/schemas/main_workflow_schema.json
+++ b/pkg/parser/schemas/main_workflow_schema.json
@@ -1362,6 +1362,24 @@
],
"description": "Skip workflow execution for users with specific repository roles. Useful for workflows that should only run for external contributors or specific permission levels."
},
+ "skip-users": {
+ "oneOf": [
+ {
+ "type": "string",
+ "minLength": 1,
+ "description": "Single GitHub username to skip workflow for (e.g., 'user1'). If the triggering user matches, the workflow will be skipped."
+ },
+ {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "minLength": 1
+ },
+ "description": "List of GitHub usernames to skip workflow for (e.g., ['user1', 'user2']). If the triggering user is in this list, the workflow will be skipped."
+ }
+ ],
+ "description": "Skip workflow execution for specific GitHub users. Useful for preventing workflows from running for specific accounts (e.g., bots, specific team members)."
+ },
"manual-approval": {
"type": "string",
"description": "Environment name that requires manual approval before the workflow can run. Must match a valid environment configured in the repository settings."
diff --git a/pkg/workflow/compiler_activation_jobs.go b/pkg/workflow/compiler_activation_jobs.go
index 4dddcc47b2..c7d79af9d7 100644
--- a/pkg/workflow/compiler_activation_jobs.go
+++ b/pkg/workflow/compiler_activation_jobs.go
@@ -171,6 +171,22 @@ func (c *Compiler) buildPreActivationJob(data *WorkflowData, needsPermissionChec
steps = append(steps, generateGitHubScriptWithRequire("check_skip_roles.cjs"))
}
+ // Add skip-users check if configured
+ if len(data.SkipUsers) > 0 {
+ // Extract workflow name for the skip-users check
+ workflowName := data.Name
+
+ steps = append(steps, " - name: Check skip-users\n")
+ steps = append(steps, fmt.Sprintf(" id: %s\n", constants.CheckSkipUsersStepID))
+ steps = append(steps, fmt.Sprintf(" uses: %s\n", GetActionPin("actions/github-script")))
+ steps = append(steps, " env:\n")
+ steps = append(steps, fmt.Sprintf(" GH_AW_SKIP_USERS: %s\n", strings.Join(data.SkipUsers, ",")))
+ steps = append(steps, fmt.Sprintf(" GH_AW_WORKFLOW_NAME: %q\n", workflowName))
+ steps = append(steps, " with:\n")
+ steps = append(steps, " script: |\n")
+ steps = append(steps, generateGitHubScriptWithRequire("check_skip_users.cjs"))
+ }
+
// Add command position check if this is a command workflow
if len(data.Command) > 0 {
steps = append(steps, " - name: Check command position\n")
@@ -247,6 +263,16 @@ func (c *Compiler) buildPreActivationJob(data *WorkflowData, needsPermissionChec
conditions = append(conditions, skipRolesCheckOk)
}
+ if len(data.SkipUsers) > 0 {
+ // Add skip-users check condition
+ skipUsersCheckOk := BuildComparison(
+ BuildPropertyAccess(fmt.Sprintf("steps.%s.outputs.%s", constants.CheckSkipUsersStepID, constants.SkipUsersOkOutput)),
+ "==",
+ BuildStringLiteral("true"),
+ )
+ conditions = append(conditions, skipUsersCheckOk)
+ }
+
if data.RateLimit != nil {
// Add rate limit check condition
rateLimitCheck := BuildComparison(
diff --git a/pkg/workflow/compiler_jobs.go b/pkg/workflow/compiler_jobs.go
index f47226ab15..870cf9771c 100644
--- a/pkg/workflow/compiler_jobs.go
+++ b/pkg/workflow/compiler_jobs.go
@@ -178,12 +178,13 @@ func (c *Compiler) buildPreActivationAndActivationJobs(data *WorkflowData, front
hasSkipIfMatch := data.SkipIfMatch != nil
hasSkipIfNoMatch := data.SkipIfNoMatch != nil
hasSkipRoles := len(data.SkipRoles) > 0
+ hasSkipUsers := len(data.SkipUsers) > 0
hasCommandTrigger := len(data.Command) > 0
hasRateLimit := data.RateLimit != nil
- compilerJobsLog.Printf("Job configuration: needsPermissionCheck=%v, hasStopTime=%v, hasSkipIfMatch=%v, hasSkipIfNoMatch=%v, hasSkipRoles=%v, hasCommand=%v, hasRateLimit=%v", needsPermissionCheck, hasStopTime, hasSkipIfMatch, hasSkipIfNoMatch, hasSkipRoles, hasCommandTrigger, hasRateLimit)
+ compilerJobsLog.Printf("Job configuration: needsPermissionCheck=%v, hasStopTime=%v, hasSkipIfMatch=%v, hasSkipIfNoMatch=%v, hasSkipRoles=%v, hasSkipUsers=%v, hasCommand=%v, hasRateLimit=%v", needsPermissionCheck, hasStopTime, hasSkipIfMatch, hasSkipIfNoMatch, hasSkipRoles, hasSkipUsers, hasCommandTrigger, hasRateLimit)
- // Build pre-activation job if needed (combines membership checks, stop-time validation, skip-if-match check, skip-if-no-match check, skip-roles check, rate limit check, and command position check)
- if needsPermissionCheck || hasStopTime || hasSkipIfMatch || hasSkipIfNoMatch || hasSkipRoles || hasCommandTrigger || hasRateLimit {
+ // Build pre-activation job if needed (combines membership checks, stop-time validation, skip-if-match check, skip-if-no-match check, skip-roles check, skip-users check, rate limit check, and command position check)
+ if needsPermissionCheck || hasStopTime || hasSkipIfMatch || hasSkipIfNoMatch || hasSkipRoles || hasSkipUsers || hasCommandTrigger || hasRateLimit {
compilerJobsLog.Print("Building pre-activation job")
preActivationJob, err := c.buildPreActivationJob(data, needsPermissionCheck)
if err != nil {
diff --git a/pkg/workflow/compiler_orchestrator_workflow.go b/pkg/workflow/compiler_orchestrator_workflow.go
index 1e233ff00d..fd78f7130e 100644
--- a/pkg/workflow/compiler_orchestrator_workflow.go
+++ b/pkg/workflow/compiler_orchestrator_workflow.go
@@ -438,6 +438,7 @@ func (c *Compiler) extractAdditionalConfigurations(
workflowData.Bots = c.extractBots(frontmatter)
workflowData.RateLimit = c.extractRateLimitConfig(frontmatter)
workflowData.SkipRoles = c.extractSkipRoles(frontmatter)
+ workflowData.SkipUsers = c.extractSkipUsers(frontmatter)
// Use the already extracted output configuration
workflowData.SafeOutputs = safeOutputs
diff --git a/pkg/workflow/compiler_types.go b/pkg/workflow/compiler_types.go
index 9fb4382184..54e729f4d0 100644
--- a/pkg/workflow/compiler_types.go
+++ b/pkg/workflow/compiler_types.go
@@ -417,6 +417,7 @@ type WorkflowData struct {
SkipIfMatch *SkipIfMatchConfig // skip-if-match configuration with query and max threshold
SkipIfNoMatch *SkipIfNoMatchConfig // skip-if-no-match configuration with query and min threshold
SkipRoles []string // roles to skip workflow for (e.g., [admin, maintainer, write])
+ SkipUsers []string // users to skip workflow for (e.g., [user1, user2])
ManualApproval string // environment name for manual approval from on: section
Command []string // for /command trigger support - multiple command names
CommandEvents []string // events where command should be active (nil = all events)
diff --git a/pkg/workflow/frontmatter_extraction_yaml.go b/pkg/workflow/frontmatter_extraction_yaml.go
index eb227509e3..4275956f6d 100644
--- a/pkg/workflow/frontmatter_extraction_yaml.go
+++ b/pkg/workflow/frontmatter_extraction_yaml.go
@@ -156,6 +156,7 @@ func (c *Compiler) commentOutProcessedFieldsInOnSection(yamlStr string, frontmat
inSkipIfMatch := false
inSkipIfNoMatch := false
inSkipRolesArray := false
+ inSkipUsersArray := false
currentSection := "" // Track which section we're in ("issues", "pull_request", "discussion", or "issue_comment")
for _, line := range lines {
@@ -230,6 +231,13 @@ func (c *Compiler) commentOutProcessedFieldsInOnSection(yamlStr string, frontmat
inSkipRolesArray = true
}
+ // Check if we're entering skip-users array
+ if !inPullRequest && !inIssues && !inDiscussion && !inIssueComment && strings.HasPrefix(trimmedLine, "skip-users:") {
+ // Check if this is an array (next line will be "- ")
+ // We'll set the flag and handle it on the next iteration
+ inSkipUsersArray = true
+ }
+
// Check if we're entering skip-if-match object
if !inPullRequest && !inIssues && !inDiscussion && !inIssueComment && !inSkipIfMatch {
// Check both uncommented and commented forms
@@ -296,6 +304,17 @@ func (c *Compiler) commentOutProcessedFieldsInOnSection(yamlStr string, frontmat
}
}
+ // Check if we're leaving the skip-users array by encountering another top-level field
+ if inSkipUsersArray && strings.TrimSpace(line) != "" {
+ // Get the indentation of the current line
+ lineIndent := len(line) - len(strings.TrimLeft(line, " \t"))
+
+ // If this is a non-dash line at the same level as skip-users (2 spaces), we're out of the array
+ if lineIndent == 2 && !strings.HasPrefix(trimmedLine, "-") && !strings.HasPrefix(trimmedLine, "skip-users:") && !strings.HasPrefix(trimmedLine, "#") {
+ inSkipUsersArray = false
+ }
+ }
+
// Determine if we should comment out this line
shouldComment := false
var commentReason string
@@ -329,6 +348,13 @@ func (c *Compiler) commentOutProcessedFieldsInOnSection(yamlStr string, frontmat
// Comment out array items in skip-roles
shouldComment = true
commentReason = " # Skip-roles processed as role check in pre-activation job"
+ } else if strings.HasPrefix(trimmedLine, "skip-users:") {
+ shouldComment = true
+ commentReason = " # Skip-users processed as user check in pre-activation job"
+ } else if inSkipUsersArray && strings.HasPrefix(trimmedLine, "-") {
+ // Comment out array items in skip-users
+ shouldComment = true
+ commentReason = " # Skip-users processed as user check in pre-activation job"
} else if strings.HasPrefix(trimmedLine, "reaction:") {
shouldComment = true
commentReason = " # Reaction processed as activation job step"
diff --git a/pkg/workflow/role_checks.go b/pkg/workflow/role_checks.go
index f31b40645c..e91b84b371 100644
--- a/pkg/workflow/role_checks.go
+++ b/pkg/workflow/role_checks.go
@@ -489,6 +489,27 @@ func (c *Compiler) extractSkipRoles(frontmatter map[string]any) []string {
return nil
}
+// extractSkipUsers extracts the 'skip-users' field from the 'on:' section of frontmatter
+// Returns nil if skip-users is not configured
+func (c *Compiler) extractSkipUsers(frontmatter map[string]any) []string {
+ // Check the "on" section in frontmatter
+ onValue, exists := frontmatter["on"]
+ if !exists || onValue == nil {
+ return nil
+ }
+
+ // Handle different formats of the on: section
+ switch on := onValue.(type) {
+ case map[string]any:
+ // Complex object format - look for skip-users
+ if skipUsersValue, exists := on["skip-users"]; exists && skipUsersValue != nil {
+ return extractStringSliceField(skipUsersValue, "skip-users")
+ }
+ }
+
+ return nil
+}
+
// extractStringSliceField extracts a string slice from various input formats
// Handles: string, []string, []any (with string elements)
// Returns nil if the input is empty or invalid
diff --git a/pkg/workflow/skip_users_test.go b/pkg/workflow/skip_users_test.go
new file mode 100644
index 0000000000..e7438f0d1a
--- /dev/null
+++ b/pkg/workflow/skip_users_test.go
@@ -0,0 +1,313 @@
+//go:build !integration
+
+package workflow
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/github/gh-aw/pkg/stringutil"
+ "github.com/github/gh-aw/pkg/testutil"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+// TestSkipUsersPreActivationJob tests that skip-users check is created correctly in pre-activation job
+func TestSkipUsersPreActivationJob(t *testing.T) {
+ tmpDir := testutil.TempDir(t, "skip-users-test")
+ compiler := NewCompiler()
+
+ t.Run("pre_activation_job_created_with_skip_users", func(t *testing.T) {
+ workflowContent := `---
+on:
+ issues:
+ types: [opened]
+ skip-users: [user1, user2, user3]
+engine: copilot
+---
+
+# Skip Users Workflow
+
+This workflow has a skip-users configuration.
+`
+ workflowFile := filepath.Join(tmpDir, "skip-users-workflow.md")
+ err := os.WriteFile(workflowFile, []byte(workflowContent), 0644)
+ require.NoError(t, err, "Failed to write workflow file")
+
+ err = compiler.CompileWorkflow(workflowFile)
+ require.NoError(t, err, "Compilation failed")
+
+ lockFile := stringutil.MarkdownToLockFile(workflowFile)
+ lockContent, err := os.ReadFile(lockFile)
+ require.NoError(t, err, "Failed to read lock file")
+
+ lockContentStr := string(lockContent)
+
+ // Verify pre_activation job exists
+ assert.Contains(t, lockContentStr, "pre_activation:", "Expected pre_activation job to be created")
+
+ // Verify skip-users check is present
+ assert.Contains(t, lockContentStr, "Check skip-users", "Expected skip-users check to be present")
+
+ // Verify the skip users environment variable is set correctly
+ assert.Contains(t, lockContentStr, "GH_AW_SKIP_USERS: user1,user2,user3", "Expected GH_AW_SKIP_USERS environment variable with correct value")
+
+ // Verify the check_skip_users step ID is present
+ assert.Contains(t, lockContentStr, "id: check_skip_users", "Expected check_skip_users step ID")
+
+ // Verify the activated output includes skip_users_ok condition
+ assert.Contains(t, lockContentStr, "steps.check_skip_users.outputs.skip_users_ok", "Expected activated output to include skip_users_ok condition")
+
+ // Verify skip-users is commented out in the frontmatter
+ assert.Contains(t, lockContentStr, "# skip-users:", "Expected skip-users to be commented out in lock file")
+ })
+
+ t.Run("skip_users_with_single_user", func(t *testing.T) {
+ workflowContent := `---
+on:
+ issues:
+ types: [opened]
+ skip-users: user1
+engine: copilot
+---
+
+# Skip Users Single User
+
+This workflow skips only for user1.
+`
+ workflowFile := filepath.Join(tmpDir, "skip-users-single.md")
+ err := os.WriteFile(workflowFile, []byte(workflowContent), 0644)
+ require.NoError(t, err, "Failed to write workflow file")
+
+ err = compiler.CompileWorkflow(workflowFile)
+ require.NoError(t, err, "Compilation failed")
+
+ lockFile := stringutil.MarkdownToLockFile(workflowFile)
+ lockContent, err := os.ReadFile(lockFile)
+ require.NoError(t, err, "Failed to read lock file")
+
+ lockContentStr := string(lockContent)
+
+ // Verify skip-users check is present
+ assert.Contains(t, lockContentStr, "Check skip-users", "Expected skip-users check to be present")
+
+ // Verify single user
+ assert.Contains(t, lockContentStr, "GH_AW_SKIP_USERS: user1", "Expected GH_AW_SKIP_USERS with single user")
+ })
+
+ t.Run("no_skip_users_no_check_created", func(t *testing.T) {
+ workflowContent := `---
+on:
+ issues:
+ types: [opened]
+engine: copilot
+---
+
+# No Skip Users Workflow
+
+This workflow has no skip-users configuration.
+`
+ workflowFile := filepath.Join(tmpDir, "no-skip-users.md")
+ err := os.WriteFile(workflowFile, []byte(workflowContent), 0644)
+ require.NoError(t, err, "Failed to write workflow file")
+
+ err = compiler.CompileWorkflow(workflowFile)
+ require.NoError(t, err, "Compilation failed")
+
+ lockFile := stringutil.MarkdownToLockFile(workflowFile)
+ lockContent, err := os.ReadFile(lockFile)
+ require.NoError(t, err, "Failed to read lock file")
+
+ lockContentStr := string(lockContent)
+
+ // Verify skip-users check is NOT present
+ assert.NotContains(t, lockContentStr, "Check skip-users", "Expected skip-users check to NOT be present")
+ assert.NotContains(t, lockContentStr, "GH_AW_SKIP_USERS", "Expected GH_AW_SKIP_USERS to NOT be present")
+ assert.NotContains(t, lockContentStr, "check_skip_users", "Expected check_skip_users step to NOT be present")
+ })
+
+ t.Run("skip_users_with_roles_field", func(t *testing.T) {
+ workflowContent := `---
+on:
+ issues:
+ types: [opened]
+ skip-users: [user1, user2]
+roles: [maintainer]
+engine: copilot
+---
+
+# Skip Users with Roles Field
+
+This workflow has both roles and skip-users.
+`
+ workflowFile := filepath.Join(tmpDir, "skip-users-with-roles.md")
+ err := os.WriteFile(workflowFile, []byte(workflowContent), 0644)
+ require.NoError(t, err, "Failed to write workflow file")
+
+ err = compiler.CompileWorkflow(workflowFile)
+ require.NoError(t, err, "Compilation failed")
+
+ lockFile := stringutil.MarkdownToLockFile(workflowFile)
+ lockContent, err := os.ReadFile(lockFile)
+ require.NoError(t, err, "Failed to read lock file")
+
+ lockContentStr := string(lockContent)
+
+ // Verify both membership check and skip-users check are present
+ assert.Contains(t, lockContentStr, "Check team membership", "Expected team membership check to be present")
+ assert.Contains(t, lockContentStr, "Check skip-users", "Expected skip-users check to be present")
+
+ // Verify GH_AW_REQUIRED_ROLES is set
+ assert.Contains(t, lockContentStr, "GH_AW_REQUIRED_ROLES: maintainer", "Expected GH_AW_REQUIRED_ROLES for roles field")
+
+ // Verify GH_AW_SKIP_USERS is set
+ assert.Contains(t, lockContentStr, "GH_AW_SKIP_USERS: user1,user2", "Expected GH_AW_SKIP_USERS for skip-users field")
+
+ // Verify both conditions in activated output
+ assert.Contains(t, lockContentStr, "steps.check_membership.outputs.is_team_member", "Expected membership check in activated output")
+ assert.Contains(t, lockContentStr, "steps.check_skip_users.outputs.skip_users_ok", "Expected skip-users check in activated output")
+ })
+
+ t.Run("skip_users_and_skip_roles_combined", func(t *testing.T) {
+ workflowContent := `---
+on:
+ issues:
+ types: [opened]
+ skip-roles: [admin, write]
+ skip-users: [user1, user2]
+engine: copilot
+---
+
+# Skip Users and Skip Roles Combined
+
+This workflow has both skip-roles and skip-users.
+`
+ workflowFile := filepath.Join(tmpDir, "skip-users-and-roles.md")
+ err := os.WriteFile(workflowFile, []byte(workflowContent), 0644)
+ require.NoError(t, err, "Failed to write workflow file")
+
+ err = compiler.CompileWorkflow(workflowFile)
+ require.NoError(t, err, "Compilation failed")
+
+ lockFile := stringutil.MarkdownToLockFile(workflowFile)
+ lockContent, err := os.ReadFile(lockFile)
+ require.NoError(t, err, "Failed to read lock file")
+
+ lockContentStr := string(lockContent)
+
+ // Verify both skip-roles and skip-users checks are present
+ assert.Contains(t, lockContentStr, "Check skip-roles", "Expected skip-roles check to be present")
+ assert.Contains(t, lockContentStr, "Check skip-users", "Expected skip-users check to be present")
+
+ // Verify both environment variables are set
+ assert.Contains(t, lockContentStr, "GH_AW_SKIP_ROLES: admin,write", "Expected GH_AW_SKIP_ROLES for skip-roles field")
+ assert.Contains(t, lockContentStr, "GH_AW_SKIP_USERS: user1,user2", "Expected GH_AW_SKIP_USERS for skip-users field")
+
+ // Verify both conditions in activated output
+ assert.Contains(t, lockContentStr, "steps.check_skip_roles.outputs.skip_roles_ok", "Expected skip-roles check in activated output")
+ assert.Contains(t, lockContentStr, "steps.check_skip_users.outputs.skip_users_ok", "Expected skip-users check in activated output")
+ })
+}
+
+// TestExtractSkipUsers tests the extractSkipUsers function
+func TestExtractSkipUsers(t *testing.T) {
+ compiler := NewCompiler()
+
+ tests := []struct {
+ name string
+ frontmatter map[string]any
+ expected []string
+ }{
+ {
+ name: "skip-users as array of strings",
+ frontmatter: map[string]any{
+ "on": map[string]any{
+ "issues": map[string]any{
+ "types": []string{"opened"},
+ },
+ "skip-users": []string{"user1", "user2"},
+ },
+ },
+ expected: []string{"user1", "user2"},
+ },
+ {
+ name: "skip-users as single string",
+ frontmatter: map[string]any{
+ "on": map[string]any{
+ "issues": map[string]any{
+ "types": []string{"opened"},
+ },
+ "skip-users": "user1",
+ },
+ },
+ expected: []string{"user1"},
+ },
+ {
+ name: "skip-users as array of any",
+ frontmatter: map[string]any{
+ "on": map[string]any{
+ "issues": map[string]any{
+ "types": []string{"opened"},
+ },
+ "skip-users": []any{"user1", "user2", "user3"},
+ },
+ },
+ expected: []string{"user1", "user2", "user3"},
+ },
+ {
+ name: "no skip-users configured",
+ frontmatter: map[string]any{
+ "on": map[string]any{
+ "issues": map[string]any{
+ "types": []string{"opened"},
+ },
+ },
+ },
+ expected: nil,
+ },
+ {
+ name: "empty skip-users array",
+ frontmatter: map[string]any{
+ "on": map[string]any{
+ "issues": map[string]any{
+ "types": []string{"opened"},
+ },
+ "skip-users": []string{},
+ },
+ },
+ expected: nil,
+ },
+ {
+ name: "skip-users as empty string",
+ frontmatter: map[string]any{
+ "on": map[string]any{
+ "issues": map[string]any{
+ "types": []string{"opened"},
+ },
+ "skip-users": "",
+ },
+ },
+ expected: nil,
+ },
+ {
+ name: "on as string (no skip-users possible)",
+ frontmatter: map[string]any{
+ "on": "push",
+ },
+ expected: nil,
+ },
+ {
+ name: "no on section",
+ frontmatter: map[string]any{},
+ expected: nil,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result := compiler.extractSkipUsers(tt.frontmatter)
+ assert.Equal(t, tt.expected, result, "extractSkipUsers result mismatch")
+ })
+ }
+}
From 23f76d562454f9f391e9b57c871e2c5fb278ee58 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 15 Feb 2026 22:42:44 +0000
Subject: [PATCH 3/5] Add skip-users import/merge support
- Add MergedSkipRoles and MergedSkipUsers to ImportsResult
- Create extractSkipRolesFromContent and extractSkipUsersFromContent functions
- Add extractOnSectionField helper for extracting from on: section
- Merge skip-roles and skip-users from imports in import_processor.go
- Add merge functions mergeSkipRoles and mergeSkipUsers in role_checks.go
- Update frontmatter hash to include merged skip-roles and skip-users
- All skip-users tests passing
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
pkg/parser/content_extractor.go | 72 +++++++++++++++++++
pkg/parser/frontmatter_hash.go | 2 +
pkg/parser/import_processor.go | 68 ++++++++++++++----
.../compiler_orchestrator_workflow.go | 4 +-
pkg/workflow/role_checks.go | 58 +++++++++++++++
5 files changed, 187 insertions(+), 17 deletions(-)
diff --git a/pkg/parser/content_extractor.go b/pkg/parser/content_extractor.go
index 3dd6d707ec..9e67418da4 100644
--- a/pkg/parser/content_extractor.go
+++ b/pkg/parser/content_extractor.go
@@ -145,6 +145,16 @@ func extractBotsFromContent(content string) (string, error) {
return extractFrontmatterField(content, "bots", "[]")
}
+// extractSkipRolesFromContent extracts skip-roles from on: section as JSON string
+func extractSkipRolesFromContent(content string) (string, error) {
+ return extractOnSectionField(content, "skip-roles")
+}
+
+// extractSkipUsersFromContent extracts skip-users from on: section as JSON string
+func extractSkipUsersFromContent(content string) (string, error) {
+ return extractOnSectionField(content, "skip-users")
+}
+
// extractPluginsFromContent extracts plugins section from frontmatter as JSON string
func extractPluginsFromContent(content string) (string, error) {
return extractFrontmatterField(content, "plugins", "[]")
@@ -213,3 +223,65 @@ func extractFrontmatterField(content, fieldName, emptyValue string) (string, err
contentExtractorLog.Printf("Successfully extracted field %s: size=%d bytes", fieldName, len(fieldJSON))
return strings.TrimSpace(string(fieldJSON)), nil
}
+
+// extractOnSectionField extracts a specific field from the on: section in frontmatter as JSON string
+func extractOnSectionField(content, fieldName string) (string, error) {
+ contentExtractorLog.Printf("Extracting on: section field: %s", fieldName)
+ result, err := ExtractFrontmatterFromContent(content)
+ if err != nil {
+ contentExtractorLog.Printf("Failed to extract frontmatter for field %s: %v", fieldName, err)
+ return "[]", nil // Return empty array on error
+ }
+
+ // Extract the "on" section
+ onValue, exists := result.Frontmatter["on"]
+ if !exists {
+ contentExtractorLog.Printf("Field 'on' not found in frontmatter")
+ return "[]", nil
+ }
+
+ // The on: section should be a map
+ onMap, ok := onValue.(map[string]any)
+ if !ok {
+ contentExtractorLog.Printf("Field 'on' is not a map: %T", onValue)
+ return "[]", nil
+ }
+
+ // Extract the requested field from the on: section
+ fieldValue, exists := onMap[fieldName]
+ if !exists {
+ contentExtractorLog.Printf("Field %s not found in 'on' section", fieldName)
+ return "[]", nil
+ }
+
+ // Normalize field value to an array
+ var normalizedValue []any
+ switch v := fieldValue.(type) {
+ case string:
+ // Single string value
+ if v != "" {
+ normalizedValue = []any{v}
+ }
+ case []any:
+ // Already an array
+ normalizedValue = v
+ case []string:
+ // String array - convert to []any
+ for _, s := range v {
+ normalizedValue = append(normalizedValue, s)
+ }
+ default:
+ contentExtractorLog.Printf("Unexpected type for field %s: %T", fieldName, fieldValue)
+ return "[]", nil
+ }
+
+ // Return JSON string
+ jsonData, err := json.Marshal(normalizedValue)
+ if err != nil {
+ contentExtractorLog.Printf("Failed to marshal field %s to JSON: %v", fieldName, err)
+ return "[]", nil
+ }
+
+ contentExtractorLog.Printf("Successfully extracted field %s from on: section: %d bytes", fieldName, len(jsonData))
+ return string(jsonData), nil
+}
diff --git a/pkg/parser/frontmatter_hash.go b/pkg/parser/frontmatter_hash.go
index 5357abdbb0..352daa1404 100644
--- a/pkg/parser/frontmatter_hash.go
+++ b/pkg/parser/frontmatter_hash.go
@@ -135,6 +135,8 @@ func buildCanonicalFrontmatter(frontmatter map[string]any, result *ImportsResult
addString("merged-secret-masking", result.MergedSecretMasking)
addSlice("merged-bots", result.MergedBots)
addString("merged-post-steps", result.MergedPostSteps)
+ addSlice("merged-skip-roles", result.MergedSkipRoles)
+ addSlice("merged-skip-users", result.MergedSkipUsers)
addSlice("merged-labels", result.MergedLabels)
addSlice("merged-caches", result.MergedCaches)
diff --git a/pkg/parser/import_processor.go b/pkg/parser/import_processor.go
index efe9a1abf5..d12e642f94 100644
--- a/pkg/parser/import_processor.go
+++ b/pkg/parser/import_processor.go
@@ -31,6 +31,8 @@ type ImportsResult struct {
MergedSecretMasking string // Merged secret-masking steps from all imports
MergedBots []string // Merged bots list from all imports (union of bot names)
MergedPlugins []string // Merged plugins list from all imports (union of plugin repos)
+ MergedSkipRoles []string // Merged skip-roles list from all imports (union of role names)
+ MergedSkipUsers []string // Merged skip-users list from all imports (union of usernames)
MergedPostSteps string // Merged post-steps configuration from all imports (appended in order)
MergedLabels []string // Merged labels from all imports (union of label names)
MergedCaches []string // Merged cache configurations from all imports (appended in order)
@@ -182,19 +184,23 @@ func processImportsFromFrontmatterWithManifestAndSource(frontmatter map[string]a
var engines []string
var safeOutputs []string
var safeInputs []string
- var bots []string // Track unique bot names
- botsSet := make(map[string]bool) // Set for deduplicating bots
- var plugins []string // Track unique plugin repos
- pluginsSet := make(map[string]bool) // Set for deduplicating plugins
- var labels []string // Track unique labels
- labelsSet := make(map[string]bool) // Set for deduplicating labels
- var caches []string // Track cache configurations (appended in order)
- var jobsBuilder strings.Builder // Track jobs from imported YAML workflows
- var features []map[string]any // Track features configurations from imports (parsed structures)
- var agentFile string // Track custom agent file
- var agentImportSpec string // Track agent import specification for remote imports
- var repositoryImports []string // Track repository-only imports for .github folder merging
- importInputs := make(map[string]any) // Aggregated input values from all imports
+ var bots []string // Track unique bot names
+ botsSet := make(map[string]bool) // Set for deduplicating bots
+ var plugins []string // Track unique plugin repos
+ pluginsSet := make(map[string]bool) // Set for deduplicating plugins
+ var labels []string // Track unique labels
+ labelsSet := make(map[string]bool) // Set for deduplicating labels
+ var skipRoles []string // Track unique skip-roles
+ skipRolesSet := make(map[string]bool) // Set for deduplicating skip-roles
+ var skipUsers []string // Track unique skip-users
+ skipUsersSet := make(map[string]bool) // Set for deduplicating skip-users
+ var caches []string // Track cache configurations (appended in order)
+ var jobsBuilder strings.Builder // Track jobs from imported YAML workflows
+ var features []map[string]any // Track features configurations from imports (parsed structures)
+ var agentFile string // Track custom agent file
+ var agentImportSpec string // Track agent import specification for remote imports
+ var repositoryImports []string // Track repository-only imports for .github folder merging
+ importInputs := make(map[string]any) // Aggregated input values from all imports
// Seed the queue with initial imports
for _, importSpec := range importSpecs {
@@ -570,7 +576,7 @@ func processImportsFromFrontmatterWithManifestAndSource(frontmatter map[string]a
secretMaskingBuilder.WriteString(secretMaskingContent + "\n")
}
- // Extract bots from imported file (merge into set to avoid duplicates)
+ // Extract and merge bots from imported file (merge into set to avoid duplicates)
botsContent, err := extractBotsFromContent(string(content))
if err == nil && botsContent != "" && botsContent != "[]" {
// Parse bots JSON array
@@ -585,7 +591,37 @@ func processImportsFromFrontmatterWithManifestAndSource(frontmatter map[string]a
}
}
- // Extract plugins from imported file (merge into set to avoid duplicates)
+ // Extract and merge skip-roles from imported file (merge into set to avoid duplicates)
+ skipRolesContent, err := extractSkipRolesFromContent(string(content))
+ if err == nil && skipRolesContent != "" && skipRolesContent != "[]" {
+ // Parse skip-roles JSON array
+ var importedSkipRoles []string
+ if jsonErr := json.Unmarshal([]byte(skipRolesContent), &importedSkipRoles); jsonErr == nil {
+ for _, role := range importedSkipRoles {
+ if !skipRolesSet[role] {
+ skipRolesSet[role] = true
+ skipRoles = append(skipRoles, role)
+ }
+ }
+ }
+ }
+
+ // Extract and merge skip-users from imported file (merge into set to avoid duplicates)
+ skipUsersContent, err := extractSkipUsersFromContent(string(content))
+ if err == nil && skipUsersContent != "" && skipUsersContent != "[]" {
+ // Parse skip-users JSON array
+ var importedSkipUsers []string
+ if jsonErr := json.Unmarshal([]byte(skipUsersContent), &importedSkipUsers); jsonErr == nil {
+ for _, user := range importedSkipUsers {
+ if !skipUsersSet[user] {
+ skipUsersSet[user] = true
+ skipUsers = append(skipUsers, user)
+ }
+ }
+ }
+ }
+
+ // Extract and merge plugins from imported file (merge into set to avoid duplicates)
// This now handles both simple string format and object format with MCP configs
pluginsContent, err := extractPluginsFromContent(string(content))
if err == nil && pluginsContent != "" && pluginsContent != "[]" {
@@ -676,6 +712,8 @@ func processImportsFromFrontmatterWithManifestAndSource(frontmatter map[string]a
MergedSecretMasking: secretMaskingBuilder.String(),
MergedBots: bots,
MergedPlugins: plugins,
+ MergedSkipRoles: skipRoles,
+ MergedSkipUsers: skipUsers,
MergedPostSteps: postStepsBuilder.String(),
MergedLabels: labels,
MergedCaches: caches,
diff --git a/pkg/workflow/compiler_orchestrator_workflow.go b/pkg/workflow/compiler_orchestrator_workflow.go
index fd78f7130e..b01f7a7872 100644
--- a/pkg/workflow/compiler_orchestrator_workflow.go
+++ b/pkg/workflow/compiler_orchestrator_workflow.go
@@ -437,8 +437,8 @@ func (c *Compiler) extractAdditionalConfigurations(
workflowData.Roles = c.extractRoles(frontmatter)
workflowData.Bots = c.extractBots(frontmatter)
workflowData.RateLimit = c.extractRateLimitConfig(frontmatter)
- workflowData.SkipRoles = c.extractSkipRoles(frontmatter)
- workflowData.SkipUsers = c.extractSkipUsers(frontmatter)
+ workflowData.SkipRoles = c.mergeSkipRoles(c.extractSkipRoles(frontmatter), importsResult.MergedSkipRoles)
+ workflowData.SkipUsers = c.mergeSkipUsers(c.extractSkipUsers(frontmatter), importsResult.MergedSkipUsers)
// Use the already extracted output configuration
workflowData.SafeOutputs = safeOutputs
diff --git a/pkg/workflow/role_checks.go b/pkg/workflow/role_checks.go
index e91b84b371..c370db1726 100644
--- a/pkg/workflow/role_checks.go
+++ b/pkg/workflow/role_checks.go
@@ -546,3 +546,61 @@ func extractStringSliceField(value any, fieldName string) []string {
roleLog.Printf("No valid %s found or unsupported type: %T", fieldName, value)
return nil
}
+
+// mergeSkipRoles merges top-level skip-roles with imported skip-roles (union)
+func (c *Compiler) mergeSkipRoles(topSkipRoles []string, importedSkipRoles []string) []string {
+ // Create a set for deduplication
+ rolesSet := make(map[string]bool)
+ var result []string
+
+ // Add top-level skip-roles first
+ for _, role := range topSkipRoles {
+ if !rolesSet[role] {
+ rolesSet[role] = true
+ result = append(result, role)
+ }
+ }
+
+ // Merge imported skip-roles
+ for _, role := range importedSkipRoles {
+ if !rolesSet[role] {
+ rolesSet[role] = true
+ result = append(result, role)
+ }
+ }
+
+ if len(result) > 0 {
+ roleLog.Printf("Merged skip-roles: %v (top=%d, imported=%d, total=%d)", result, len(topSkipRoles), len(importedSkipRoles), len(result))
+ }
+
+ return result
+}
+
+// mergeSkipUsers merges top-level skip-users with imported skip-users (union)
+func (c *Compiler) mergeSkipUsers(topSkipUsers []string, importedSkipUsers []string) []string {
+ // Create a set for deduplication
+ usersSet := make(map[string]bool)
+ var result []string
+
+ // Add top-level skip-users first
+ for _, user := range topSkipUsers {
+ if !usersSet[user] {
+ usersSet[user] = true
+ result = append(result, user)
+ }
+ }
+
+ // Merge imported skip-users
+ for _, user := range importedSkipUsers {
+ if !usersSet[user] {
+ usersSet[user] = true
+ result = append(result, user)
+ }
+ }
+
+ if len(result) > 0 {
+ roleLog.Printf("Merged skip-users: %v (top=%d, imported=%d, total=%d)", result, len(topSkipUsers), len(importedSkipUsers), len(result))
+ }
+
+ return result
+}
From 34062847990f85a3dfbe092595d4930b723fd750 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 15 Feb 2026 22:53:33 +0000
Subject: [PATCH 4/5] Add bot name matching to skip-users and update
ai-moderator
- Update check_skip_users.cjs to match both username and username[bot]
- Support bidirectional matching: "github-actions" matches "github-actions[bot]" and vice versa
- Add skip-users: [github-actions, copilot] to ai-moderator workflow
- Add comprehensive tests for bot name matching (9 tests, all passing)
- Compiled ai-moderator.lock.yml with skip-users check integrated
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
.github/workflows/ai-moderator.lock.yml | 19 ++-
.github/workflows/ai-moderator.md | 1 +
actions/setup/js/check_skip_users.cjs | 20 ++-
actions/setup/js/check_skip_users.test.cjs | 140 +++++++++++++++++++++
4 files changed, 177 insertions(+), 3 deletions(-)
create mode 100644 actions/setup/js/check_skip_users.test.cjs
diff --git a/.github/workflows/ai-moderator.lock.yml b/.github/workflows/ai-moderator.lock.yml
index c0f3c58654..611911982e 100644
--- a/.github/workflows/ai-moderator.lock.yml
+++ b/.github/workflows/ai-moderator.lock.yml
@@ -26,7 +26,7 @@
# Imports:
# - shared/mood.md
#
-# frontmatter-hash: 8c8c0cf5f9b5c9de3acbec28f94efe65b00e4c888c6be93df443665a560ce698
+# frontmatter-hash: c4cac2c490859849f54efaf002fa26ab5dc2170269973f9b684f3fc4c0da4c15
name: "AI Moderator"
"on":
@@ -43,6 +43,9 @@ name: "AI Moderator"
# - maintainer # Skip-roles processed as role check in pre-activation job
# - write # Skip-roles processed as role check in pre-activation job
# - triage # Skip-roles processed as role check in pre-activation job
+ # skip-users: # Skip-users processed as user check in pre-activation job
+ # - github-actions # Skip-users processed as user check in pre-activation job
+ # - copilot # Skip-users processed as user check in pre-activation job
permissions: {}
@@ -915,7 +918,7 @@ jobs:
actions: read
contents: read
outputs:
- activated: ${{ (steps.check_skip_roles.outputs.skip_roles_ok == 'true') && (steps.check_rate_limit.outputs.rate_limit_ok == 'true') }}
+ activated: ${{ ((steps.check_skip_roles.outputs.skip_roles_ok == 'true') && (steps.check_skip_users.outputs.skip_users_ok == 'true')) && (steps.check_rate_limit.outputs.rate_limit_ok == 'true') }}
steps:
- name: Checkout actions folder
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -955,6 +958,18 @@ jobs:
setupGlobals(core, github, context, exec, io);
const { main } = require('/opt/gh-aw/actions/check_skip_roles.cjs');
await main();
+ - name: Check skip-users
+ id: check_skip_users
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_AW_SKIP_USERS: github-actions,copilot
+ GH_AW_WORKFLOW_NAME: "AI Moderator"
+ with:
+ script: |
+ const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('/opt/gh-aw/actions/check_skip_users.cjs');
+ await main();
safe_outputs:
needs:
diff --git a/.github/workflows/ai-moderator.md b/.github/workflows/ai-moderator.md
index fed3ecf875..abec4f360e 100644
--- a/.github/workflows/ai-moderator.md
+++ b/.github/workflows/ai-moderator.md
@@ -10,6 +10,7 @@ on:
types: [created]
lock-for-agent: true
skip-roles: [admin, maintainer, write, triage]
+ skip-users: [github-actions, copilot]
rate-limit:
max: 5
window: 60
diff --git a/actions/setup/js/check_skip_users.cjs b/actions/setup/js/check_skip_users.cjs
index d8490ea622..07fb16fcfe 100644
--- a/actions/setup/js/check_skip_users.cjs
+++ b/actions/setup/js/check_skip_users.cjs
@@ -28,7 +28,25 @@ async function main() {
core.info(`Checking if user '${actor}' is in skip-users: ${skipUsers.join(", ")}`);
// Check if the actor is in the skip-users list
- if (skipUsers.includes(actor)) {
+ // Match both exact username and username with [bot] suffix
+ // e.g., "github-actions" matches both "github-actions" and "github-actions[bot]"
+ const isSkipped = skipUsers.some(skipUser => {
+ // Exact match
+ if (actor === skipUser) {
+ return true;
+ }
+ // Match with [bot] suffix
+ if (actor === `${skipUser}[bot]`) {
+ return true;
+ }
+ // Match if skip-user has [bot] suffix and actor matches base name
+ if (skipUser.endsWith("[bot]") && actor === skipUser.slice(0, -5)) {
+ return true;
+ }
+ return false;
+ });
+
+ if (isSkipped) {
// User is in skip-users, skip the workflow
core.info(`❌ User '${actor}' is in skip-users [${skipUsers.join(", ")}]. Workflow will be skipped.`);
core.setOutput("skip_users_ok", "false");
diff --git a/actions/setup/js/check_skip_users.test.cjs b/actions/setup/js/check_skip_users.test.cjs
new file mode 100644
index 0000000000..03e7f2885e
--- /dev/null
+++ b/actions/setup/js/check_skip_users.test.cjs
@@ -0,0 +1,140 @@
+import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
+
+describe("check_skip_users.cjs", () => {
+ let mockCore;
+ let mockContext;
+
+ beforeEach(() => {
+ // Mock core actions methods
+ mockCore = {
+ info: vi.fn(),
+ warning: vi.fn(),
+ error: vi.fn(),
+ setFailed: vi.fn(),
+ setOutput: vi.fn(),
+ };
+
+ mockContext = {
+ actor: "test-user",
+ eventName: "issues",
+ repo: {
+ owner: "test-owner",
+ repo: "test-repo",
+ },
+ };
+
+ // Set up global mocks
+ global.core = mockCore;
+ global.context = mockContext;
+
+ // Clear environment variables
+ delete process.env.GH_AW_SKIP_USERS;
+
+ // Clear module cache to ensure fresh import
+ vi.resetModules();
+ });
+
+ afterEach(() => {
+ vi.clearAllMocks();
+ delete global.core;
+ delete global.context;
+ });
+
+ it("should allow workflow when no skip-users configured", async () => {
+ delete process.env.GH_AW_SKIP_USERS;
+
+ const { main } = await import("./check_skip_users.cjs");
+ await main();
+
+ expect(mockCore.setOutput).toHaveBeenCalledWith("skip_users_ok", "true");
+ expect(mockCore.setOutput).toHaveBeenCalledWith("result", "no_skip_users");
+ });
+
+ it("should skip workflow for exact username match", async () => {
+ process.env.GH_AW_SKIP_USERS = "test-user,other-user";
+ mockContext.actor = "test-user";
+
+ const { main } = await import("./check_skip_users.cjs");
+ await main();
+
+ expect(mockCore.setOutput).toHaveBeenCalledWith("skip_users_ok", "false");
+ expect(mockCore.setOutput).toHaveBeenCalledWith("result", "skipped");
+ });
+
+ it("should allow workflow when user not in skip-users", async () => {
+ process.env.GH_AW_SKIP_USERS = "other-user,another-user";
+ mockContext.actor = "test-user";
+
+ const { main } = await import("./check_skip_users.cjs");
+ await main();
+
+ expect(mockCore.setOutput).toHaveBeenCalledWith("skip_users_ok", "true");
+ expect(mockCore.setOutput).toHaveBeenCalledWith("result", "not_skipped");
+ });
+
+ it("should skip workflow for bot with [bot] suffix when base name in skip-users", async () => {
+ process.env.GH_AW_SKIP_USERS = "github-actions,copilot";
+ mockContext.actor = "github-actions[bot]";
+
+ const { main } = await import("./check_skip_users.cjs");
+ await main();
+
+ expect(mockCore.setOutput).toHaveBeenCalledWith("skip_users_ok", "false");
+ expect(mockCore.setOutput).toHaveBeenCalledWith("result", "skipped");
+ });
+
+ it("should skip workflow for base name when skip-users has [bot] suffix", async () => {
+ process.env.GH_AW_SKIP_USERS = "github-actions[bot],copilot[bot]";
+ mockContext.actor = "github-actions";
+
+ const { main } = await import("./check_skip_users.cjs");
+ await main();
+
+ expect(mockCore.setOutput).toHaveBeenCalledWith("skip_users_ok", "false");
+ expect(mockCore.setOutput).toHaveBeenCalledWith("result", "skipped");
+ });
+
+ it("should skip workflow for exact match with [bot] suffix", async () => {
+ process.env.GH_AW_SKIP_USERS = "github-actions[bot]";
+ mockContext.actor = "github-actions[bot]";
+
+ const { main } = await import("./check_skip_users.cjs");
+ await main();
+
+ expect(mockCore.setOutput).toHaveBeenCalledWith("skip_users_ok", "false");
+ expect(mockCore.setOutput).toHaveBeenCalledWith("result", "skipped");
+ });
+
+ it("should handle multiple users with mixed bot syntax", async () => {
+ process.env.GH_AW_SKIP_USERS = "user1,github-actions,copilot[bot]";
+ mockContext.actor = "copilot";
+
+ const { main } = await import("./check_skip_users.cjs");
+ await main();
+
+ expect(mockCore.setOutput).toHaveBeenCalledWith("skip_users_ok", "false");
+ expect(mockCore.setOutput).toHaveBeenCalledWith("result", "skipped");
+ });
+
+ it("should not skip for partial matches", async () => {
+ process.env.GH_AW_SKIP_USERS = "github-actions";
+ mockContext.actor = "github-actions-bot";
+
+ const { main } = await import("./check_skip_users.cjs");
+ await main();
+
+ expect(mockCore.setOutput).toHaveBeenCalledWith("skip_users_ok", "true");
+ expect(mockCore.setOutput).toHaveBeenCalledWith("result", "not_skipped");
+ });
+
+ it("should handle whitespace in skip-users list", async () => {
+ process.env.GH_AW_SKIP_USERS = " github-actions , copilot , renovate ";
+ mockContext.actor = "copilot[bot]";
+
+ const { main } = await import("./check_skip_users.cjs");
+ await main();
+
+ expect(mockCore.setOutput).toHaveBeenCalledWith("skip_users_ok", "false");
+ expect(mockCore.setOutput).toHaveBeenCalledWith("result", "skipped");
+ });
+});
From a39d53f40baa9097bef77f3305ffcb702ad5d589 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 15 Feb 2026 23:01:29 +0000
Subject: [PATCH 5/5] Rename skip-users to skip-bots throughout codebase
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Rename check_skip_users.cjs → check_skip_bots.cjs
- Rename check_skip_users.test.cjs → check_skip_bots.test.cjs
- Rename skip_users_test.go → skip_bots_test.go
- Update all function names: extractSkipUsers → extractSkipBots
- Update all variable names: SkipUsers → SkipBots, skipUsers → skipBots
- Update all constants: CheckSkipUsersStepID → CheckSkipBotsStepID, SkipUsersOkOutput → SkipBotsOkOutput
- Update environment variable: GH_AW_SKIP_USERS → GH_AW_SKIP_BOTS
- Update frontmatter field: skip-users → skip-bots in schema and extraction
- Update ai-moderator.md: skip-users → skip-bots
- Recompiled ai-moderator.lock.yml with skip-bots
- All tests passing (Go: 5/5, JavaScript: 9/9)
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
.github/workflows/ai-moderator.lock.yml | 18 +--
.github/workflows/ai-moderator.md | 2 +-
actions/setup/js/check_skip_bots.cjs | 63 +++++++++
...sers.test.cjs => check_skip_bots.test.cjs} | 70 +++++-----
actions/setup/js/check_skip_users.cjs | 63 ---------
pkg/constants/constants.go | 4 +-
pkg/parser/content_extractor.go | 6 +-
pkg/parser/frontmatter_hash.go | 2 +-
pkg/parser/import_processor.go | 28 ++--
pkg/parser/schemas/main_workflow_schema.json | 2 +-
pkg/workflow/compiler_activation_jobs.go | 24 ++--
pkg/workflow/compiler_jobs.go | 8 +-
.../compiler_orchestrator_workflow.go | 2 +-
pkg/workflow/compiler_types.go | 2 +-
pkg/workflow/frontmatter_extraction_yaml.go | 28 ++--
pkg/workflow/role_checks.go | 26 ++--
.../{skip_users_test.go => skip_bots_test.go} | 124 +++++++++---------
17 files changed, 236 insertions(+), 236 deletions(-)
create mode 100644 actions/setup/js/check_skip_bots.cjs
rename actions/setup/js/{check_skip_users.test.cjs => check_skip_bots.test.cjs} (51%)
delete mode 100644 actions/setup/js/check_skip_users.cjs
rename pkg/workflow/{skip_users_test.go => skip_bots_test.go} (57%)
diff --git a/.github/workflows/ai-moderator.lock.yml b/.github/workflows/ai-moderator.lock.yml
index 611911982e..e6e562b3e2 100644
--- a/.github/workflows/ai-moderator.lock.yml
+++ b/.github/workflows/ai-moderator.lock.yml
@@ -26,7 +26,7 @@
# Imports:
# - shared/mood.md
#
-# frontmatter-hash: c4cac2c490859849f54efaf002fa26ab5dc2170269973f9b684f3fc4c0da4c15
+# frontmatter-hash: 2ebd01943a22487e072101433dd9319edd66b82cddac1934c765b2ad80f94251
name: "AI Moderator"
"on":
@@ -38,14 +38,14 @@ name: "AI Moderator"
# lock-for-agent: true # Lock-for-agent processed as issue locking in activation job
types:
- opened
+ # skip-bots: # Skip-bots processed as bot check in pre-activation job
+ # - github-actions # Skip-bots processed as bot check in pre-activation job
+ # - copilot # Skip-bots processed as bot check in pre-activation job
# skip-roles: # Skip-roles processed as role check in pre-activation job
# - admin # Skip-roles processed as role check in pre-activation job
# - maintainer # Skip-roles processed as role check in pre-activation job
# - write # Skip-roles processed as role check in pre-activation job
# - triage # Skip-roles processed as role check in pre-activation job
- # skip-users: # Skip-users processed as user check in pre-activation job
- # - github-actions # Skip-users processed as user check in pre-activation job
- # - copilot # Skip-users processed as user check in pre-activation job
permissions: {}
@@ -918,7 +918,7 @@ jobs:
actions: read
contents: read
outputs:
- activated: ${{ ((steps.check_skip_roles.outputs.skip_roles_ok == 'true') && (steps.check_skip_users.outputs.skip_users_ok == 'true')) && (steps.check_rate_limit.outputs.rate_limit_ok == 'true') }}
+ activated: ${{ ((steps.check_skip_roles.outputs.skip_roles_ok == 'true') && (steps.check_skip_bots.outputs.skip_bots_ok == 'true')) && (steps.check_rate_limit.outputs.rate_limit_ok == 'true') }}
steps:
- name: Checkout actions folder
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -958,17 +958,17 @@ jobs:
setupGlobals(core, github, context, exec, io);
const { main } = require('/opt/gh-aw/actions/check_skip_roles.cjs');
await main();
- - name: Check skip-users
- id: check_skip_users
+ - name: Check skip-bots
+ id: check_skip_bots
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
- GH_AW_SKIP_USERS: github-actions,copilot
+ GH_AW_SKIP_BOTS: github-actions,copilot
GH_AW_WORKFLOW_NAME: "AI Moderator"
with:
script: |
const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
setupGlobals(core, github, context, exec, io);
- const { main } = require('/opt/gh-aw/actions/check_skip_users.cjs');
+ const { main } = require('/opt/gh-aw/actions/check_skip_bots.cjs');
await main();
safe_outputs:
diff --git a/.github/workflows/ai-moderator.md b/.github/workflows/ai-moderator.md
index abec4f360e..a498167c18 100644
--- a/.github/workflows/ai-moderator.md
+++ b/.github/workflows/ai-moderator.md
@@ -10,7 +10,7 @@ on:
types: [created]
lock-for-agent: true
skip-roles: [admin, maintainer, write, triage]
- skip-users: [github-actions, copilot]
+ skip-bots: [github-actions, copilot]
rate-limit:
max: 5
window: 60
diff --git a/actions/setup/js/check_skip_bots.cjs b/actions/setup/js/check_skip_bots.cjs
new file mode 100644
index 0000000000..3dc528beb5
--- /dev/null
+++ b/actions/setup/js/check_skip_bots.cjs
@@ -0,0 +1,63 @@
+// @ts-check
+///
+
+/**
+ * Check if the workflow should be skipped based on bot/user identity
+ * Reads skip-bots from GH_AW_SKIP_BOTS environment variable
+ * If the github.actor is in the skip-bots list, set skip_bots_ok to false (skip the workflow)
+ * Otherwise, set skip_bots_ok to true (allow the workflow to proceed)
+ */
+async function main() {
+ const { eventName } = context;
+ const actor = context.actor;
+
+ // Parse skip-bots from environment variable
+ const skipBotsEnv = process.env.GH_AW_SKIP_BOTS;
+ if (!skipBotsEnv || skipBotsEnv.trim() === "") {
+ // No skip-bots configured, workflow should proceed
+ core.info("✅ No skip-bots configured, workflow will proceed");
+ core.setOutput("skip_bots_ok", "true");
+ core.setOutput("result", "no_skip_bots");
+ return;
+ }
+
+ const skipBots = skipBotsEnv
+ .split(",")
+ .map(u => u.trim())
+ .filter(u => u);
+ core.info(`Checking if user '${actor}' is in skip-bots: ${skipBots.join(", ")}`);
+
+ // Check if the actor is in the skip-bots list
+ // Match both exact username and username with [bot] suffix
+ // e.g., "github-actions" matches both "github-actions" and "github-actions[bot]"
+ const isSkipped = skipBots.some(skipBot => {
+ // Exact match
+ if (actor === skipBot) {
+ return true;
+ }
+ // Match with [bot] suffix
+ if (actor === `${skipBot}[bot]`) {
+ return true;
+ }
+ // Match if skip-bot has [bot] suffix and actor matches base name
+ if (skipBot.endsWith("[bot]") && actor === skipBot.slice(0, -5)) {
+ return true;
+ }
+ return false;
+ });
+
+ if (isSkipped) {
+ // User is in skip-bots, skip the workflow
+ core.info(`❌ User '${actor}' is in skip-bots [${skipBots.join(", ")}]. Workflow will be skipped.`);
+ core.setOutput("skip_bots_ok", "false");
+ core.setOutput("result", "skipped");
+ core.setOutput("error_message", `Workflow skipped: User '${actor}' is in skip-bots: [${skipBots.join(", ")}]`);
+ } else {
+ // User is NOT in skip-bots, allow workflow to proceed
+ core.info(`✅ User '${actor}' is NOT in skip-bots [${skipBots.join(", ")}]. Workflow will proceed.`);
+ core.setOutput("skip_bots_ok", "true");
+ core.setOutput("result", "not_skipped");
+ }
+}
+
+module.exports = { main };
diff --git a/actions/setup/js/check_skip_users.test.cjs b/actions/setup/js/check_skip_bots.test.cjs
similarity index 51%
rename from actions/setup/js/check_skip_users.test.cjs
rename to actions/setup/js/check_skip_bots.test.cjs
index 03e7f2885e..765e07279f 100644
--- a/actions/setup/js/check_skip_users.test.cjs
+++ b/actions/setup/js/check_skip_bots.test.cjs
@@ -1,6 +1,6 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
-describe("check_skip_users.cjs", () => {
+describe("check_skip_bots.cjs", () => {
let mockCore;
let mockContext;
@@ -28,7 +28,7 @@ describe("check_skip_users.cjs", () => {
global.context = mockContext;
// Clear environment variables
- delete process.env.GH_AW_SKIP_USERS;
+ delete process.env.GH_AW_SKIP_BOTS;
// Clear module cache to ensure fresh import
vi.resetModules();
@@ -40,101 +40,101 @@ describe("check_skip_users.cjs", () => {
delete global.context;
});
- it("should allow workflow when no skip-users configured", async () => {
- delete process.env.GH_AW_SKIP_USERS;
+ it("should allow workflow when no skip-bots configured", async () => {
+ delete process.env.GH_AW_SKIP_BOTS;
- const { main } = await import("./check_skip_users.cjs");
+ const { main } = await import("./check_skip_bots.cjs");
await main();
- expect(mockCore.setOutput).toHaveBeenCalledWith("skip_users_ok", "true");
- expect(mockCore.setOutput).toHaveBeenCalledWith("result", "no_skip_users");
+ expect(mockCore.setOutput).toHaveBeenCalledWith("skip_bots_ok", "true");
+ expect(mockCore.setOutput).toHaveBeenCalledWith("result", "no_skip_bots");
});
it("should skip workflow for exact username match", async () => {
- process.env.GH_AW_SKIP_USERS = "test-user,other-user";
+ process.env.GH_AW_SKIP_BOTS = "test-user,other-user";
mockContext.actor = "test-user";
- const { main } = await import("./check_skip_users.cjs");
+ const { main } = await import("./check_skip_bots.cjs");
await main();
- expect(mockCore.setOutput).toHaveBeenCalledWith("skip_users_ok", "false");
+ expect(mockCore.setOutput).toHaveBeenCalledWith("skip_bots_ok", "false");
expect(mockCore.setOutput).toHaveBeenCalledWith("result", "skipped");
});
- it("should allow workflow when user not in skip-users", async () => {
- process.env.GH_AW_SKIP_USERS = "other-user,another-user";
+ it("should allow workflow when user not in skip-bots", async () => {
+ process.env.GH_AW_SKIP_BOTS = "other-user,another-user";
mockContext.actor = "test-user";
- const { main } = await import("./check_skip_users.cjs");
+ const { main } = await import("./check_skip_bots.cjs");
await main();
- expect(mockCore.setOutput).toHaveBeenCalledWith("skip_users_ok", "true");
+ expect(mockCore.setOutput).toHaveBeenCalledWith("skip_bots_ok", "true");
expect(mockCore.setOutput).toHaveBeenCalledWith("result", "not_skipped");
});
- it("should skip workflow for bot with [bot] suffix when base name in skip-users", async () => {
- process.env.GH_AW_SKIP_USERS = "github-actions,copilot";
+ it("should skip workflow for bot with [bot] suffix when base name in skip-bots", async () => {
+ process.env.GH_AW_SKIP_BOTS = "github-actions,copilot";
mockContext.actor = "github-actions[bot]";
- const { main } = await import("./check_skip_users.cjs");
+ const { main } = await import("./check_skip_bots.cjs");
await main();
- expect(mockCore.setOutput).toHaveBeenCalledWith("skip_users_ok", "false");
+ expect(mockCore.setOutput).toHaveBeenCalledWith("skip_bots_ok", "false");
expect(mockCore.setOutput).toHaveBeenCalledWith("result", "skipped");
});
- it("should skip workflow for base name when skip-users has [bot] suffix", async () => {
- process.env.GH_AW_SKIP_USERS = "github-actions[bot],copilot[bot]";
+ it("should skip workflow for base name when skip-bots has [bot] suffix", async () => {
+ process.env.GH_AW_SKIP_BOTS = "github-actions[bot],copilot[bot]";
mockContext.actor = "github-actions";
- const { main } = await import("./check_skip_users.cjs");
+ const { main } = await import("./check_skip_bots.cjs");
await main();
- expect(mockCore.setOutput).toHaveBeenCalledWith("skip_users_ok", "false");
+ expect(mockCore.setOutput).toHaveBeenCalledWith("skip_bots_ok", "false");
expect(mockCore.setOutput).toHaveBeenCalledWith("result", "skipped");
});
it("should skip workflow for exact match with [bot] suffix", async () => {
- process.env.GH_AW_SKIP_USERS = "github-actions[bot]";
+ process.env.GH_AW_SKIP_BOTS = "github-actions[bot]";
mockContext.actor = "github-actions[bot]";
- const { main } = await import("./check_skip_users.cjs");
+ const { main } = await import("./check_skip_bots.cjs");
await main();
- expect(mockCore.setOutput).toHaveBeenCalledWith("skip_users_ok", "false");
+ expect(mockCore.setOutput).toHaveBeenCalledWith("skip_bots_ok", "false");
expect(mockCore.setOutput).toHaveBeenCalledWith("result", "skipped");
});
it("should handle multiple users with mixed bot syntax", async () => {
- process.env.GH_AW_SKIP_USERS = "user1,github-actions,copilot[bot]";
+ process.env.GH_AW_SKIP_BOTS = "user1,github-actions,copilot[bot]";
mockContext.actor = "copilot";
- const { main } = await import("./check_skip_users.cjs");
+ const { main } = await import("./check_skip_bots.cjs");
await main();
- expect(mockCore.setOutput).toHaveBeenCalledWith("skip_users_ok", "false");
+ expect(mockCore.setOutput).toHaveBeenCalledWith("skip_bots_ok", "false");
expect(mockCore.setOutput).toHaveBeenCalledWith("result", "skipped");
});
it("should not skip for partial matches", async () => {
- process.env.GH_AW_SKIP_USERS = "github-actions";
+ process.env.GH_AW_SKIP_BOTS = "github-actions";
mockContext.actor = "github-actions-bot";
- const { main } = await import("./check_skip_users.cjs");
+ const { main } = await import("./check_skip_bots.cjs");
await main();
- expect(mockCore.setOutput).toHaveBeenCalledWith("skip_users_ok", "true");
+ expect(mockCore.setOutput).toHaveBeenCalledWith("skip_bots_ok", "true");
expect(mockCore.setOutput).toHaveBeenCalledWith("result", "not_skipped");
});
- it("should handle whitespace in skip-users list", async () => {
- process.env.GH_AW_SKIP_USERS = " github-actions , copilot , renovate ";
+ it("should handle whitespace in skip-bots list", async () => {
+ process.env.GH_AW_SKIP_BOTS = " github-actions , copilot , renovate ";
mockContext.actor = "copilot[bot]";
- const { main } = await import("./check_skip_users.cjs");
+ const { main } = await import("./check_skip_bots.cjs");
await main();
- expect(mockCore.setOutput).toHaveBeenCalledWith("skip_users_ok", "false");
+ expect(mockCore.setOutput).toHaveBeenCalledWith("skip_bots_ok", "false");
expect(mockCore.setOutput).toHaveBeenCalledWith("result", "skipped");
});
});
diff --git a/actions/setup/js/check_skip_users.cjs b/actions/setup/js/check_skip_users.cjs
deleted file mode 100644
index 07fb16fcfe..0000000000
--- a/actions/setup/js/check_skip_users.cjs
+++ /dev/null
@@ -1,63 +0,0 @@
-// @ts-check
-///
-
-/**
- * Check if the workflow should be skipped based on user identity
- * Reads skip-users from GH_AW_SKIP_USERS environment variable
- * If the github.actor is in the skip-users list, set skip_users_ok to false (skip the workflow)
- * Otherwise, set skip_users_ok to true (allow the workflow to proceed)
- */
-async function main() {
- const { eventName } = context;
- const actor = context.actor;
-
- // Parse skip-users from environment variable
- const skipUsersEnv = process.env.GH_AW_SKIP_USERS;
- if (!skipUsersEnv || skipUsersEnv.trim() === "") {
- // No skip-users configured, workflow should proceed
- core.info("✅ No skip-users configured, workflow will proceed");
- core.setOutput("skip_users_ok", "true");
- core.setOutput("result", "no_skip_users");
- return;
- }
-
- const skipUsers = skipUsersEnv
- .split(",")
- .map(u => u.trim())
- .filter(u => u);
- core.info(`Checking if user '${actor}' is in skip-users: ${skipUsers.join(", ")}`);
-
- // Check if the actor is in the skip-users list
- // Match both exact username and username with [bot] suffix
- // e.g., "github-actions" matches both "github-actions" and "github-actions[bot]"
- const isSkipped = skipUsers.some(skipUser => {
- // Exact match
- if (actor === skipUser) {
- return true;
- }
- // Match with [bot] suffix
- if (actor === `${skipUser}[bot]`) {
- return true;
- }
- // Match if skip-user has [bot] suffix and actor matches base name
- if (skipUser.endsWith("[bot]") && actor === skipUser.slice(0, -5)) {
- return true;
- }
- return false;
- });
-
- if (isSkipped) {
- // User is in skip-users, skip the workflow
- core.info(`❌ User '${actor}' is in skip-users [${skipUsers.join(", ")}]. Workflow will be skipped.`);
- core.setOutput("skip_users_ok", "false");
- core.setOutput("result", "skipped");
- core.setOutput("error_message", `Workflow skipped: User '${actor}' is in skip-users: [${skipUsers.join(", ")}]`);
- } else {
- // User is NOT in skip-users, allow workflow to proceed
- core.info(`✅ User '${actor}' is NOT in skip-users [${skipUsers.join(", ")}]. Workflow will proceed.`);
- core.setOutput("skip_users_ok", "true");
- core.setOutput("result", "not_skipped");
- }
-}
-
-module.exports = { main };
diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go
index 9b00f8df45..38a0f587ad 100644
--- a/pkg/constants/constants.go
+++ b/pkg/constants/constants.go
@@ -649,7 +649,7 @@ const CheckSkipIfNoMatchStepID StepID = "check_skip_if_no_match"
const CheckCommandPositionStepID StepID = "check_command_position"
const CheckRateLimitStepID StepID = "check_rate_limit"
const CheckSkipRolesStepID StepID = "check_skip_roles"
-const CheckSkipUsersStepID StepID = "check_skip_users"
+const CheckSkipBotsStepID StepID = "check_skip_bots"
// Output names for pre-activation job steps
const IsTeamMemberOutput = "is_team_member"
@@ -660,7 +660,7 @@ const CommandPositionOkOutput = "command_position_ok"
const MatchedCommandOutput = "matched_command"
const RateLimitOkOutput = "rate_limit_ok"
const SkipRolesOkOutput = "skip_roles_ok"
-const SkipUsersOkOutput = "skip_users_ok"
+const SkipBotsOkOutput = "skip_bots_ok"
const ActivatedOutput = "activated"
// Rate limit defaults
diff --git a/pkg/parser/content_extractor.go b/pkg/parser/content_extractor.go
index 9e67418da4..c99321dd1c 100644
--- a/pkg/parser/content_extractor.go
+++ b/pkg/parser/content_extractor.go
@@ -150,9 +150,9 @@ func extractSkipRolesFromContent(content string) (string, error) {
return extractOnSectionField(content, "skip-roles")
}
-// extractSkipUsersFromContent extracts skip-users from on: section as JSON string
-func extractSkipUsersFromContent(content string) (string, error) {
- return extractOnSectionField(content, "skip-users")
+// extractSkipBotsFromContent extracts skip-bots from on: section as JSON string
+func extractSkipBotsFromContent(content string) (string, error) {
+ return extractOnSectionField(content, "skip-bots")
}
// extractPluginsFromContent extracts plugins section from frontmatter as JSON string
diff --git a/pkg/parser/frontmatter_hash.go b/pkg/parser/frontmatter_hash.go
index 352daa1404..111d5c16fc 100644
--- a/pkg/parser/frontmatter_hash.go
+++ b/pkg/parser/frontmatter_hash.go
@@ -136,7 +136,7 @@ func buildCanonicalFrontmatter(frontmatter map[string]any, result *ImportsResult
addSlice("merged-bots", result.MergedBots)
addString("merged-post-steps", result.MergedPostSteps)
addSlice("merged-skip-roles", result.MergedSkipRoles)
- addSlice("merged-skip-users", result.MergedSkipUsers)
+ addSlice("merged-skip-bots", result.MergedSkipBots)
addSlice("merged-labels", result.MergedLabels)
addSlice("merged-caches", result.MergedCaches)
diff --git a/pkg/parser/import_processor.go b/pkg/parser/import_processor.go
index d12e642f94..2a387d9f65 100644
--- a/pkg/parser/import_processor.go
+++ b/pkg/parser/import_processor.go
@@ -32,7 +32,7 @@ type ImportsResult struct {
MergedBots []string // Merged bots list from all imports (union of bot names)
MergedPlugins []string // Merged plugins list from all imports (union of plugin repos)
MergedSkipRoles []string // Merged skip-roles list from all imports (union of role names)
- MergedSkipUsers []string // Merged skip-users list from all imports (union of usernames)
+ MergedSkipBots []string // Merged skip-bots list from all imports (union of usernames)
MergedPostSteps string // Merged post-steps configuration from all imports (appended in order)
MergedLabels []string // Merged labels from all imports (union of label names)
MergedCaches []string // Merged cache configurations from all imports (appended in order)
@@ -192,8 +192,8 @@ func processImportsFromFrontmatterWithManifestAndSource(frontmatter map[string]a
labelsSet := make(map[string]bool) // Set for deduplicating labels
var skipRoles []string // Track unique skip-roles
skipRolesSet := make(map[string]bool) // Set for deduplicating skip-roles
- var skipUsers []string // Track unique skip-users
- skipUsersSet := make(map[string]bool) // Set for deduplicating skip-users
+ var skipBots []string // Track unique skip-bots
+ skipBotsSet := make(map[string]bool) // Set for deduplicating skip-bots
var caches []string // Track cache configurations (appended in order)
var jobsBuilder strings.Builder // Track jobs from imported YAML workflows
var features []map[string]any // Track features configurations from imports (parsed structures)
@@ -606,16 +606,16 @@ func processImportsFromFrontmatterWithManifestAndSource(frontmatter map[string]a
}
}
- // Extract and merge skip-users from imported file (merge into set to avoid duplicates)
- skipUsersContent, err := extractSkipUsersFromContent(string(content))
- if err == nil && skipUsersContent != "" && skipUsersContent != "[]" {
- // Parse skip-users JSON array
- var importedSkipUsers []string
- if jsonErr := json.Unmarshal([]byte(skipUsersContent), &importedSkipUsers); jsonErr == nil {
- for _, user := range importedSkipUsers {
- if !skipUsersSet[user] {
- skipUsersSet[user] = true
- skipUsers = append(skipUsers, user)
+ // Extract and merge skip-bots from imported file (merge into set to avoid duplicates)
+ skipBotsContent, err := extractSkipBotsFromContent(string(content))
+ if err == nil && skipBotsContent != "" && skipBotsContent != "[]" {
+ // Parse skip-bots JSON array
+ var importedSkipBots []string
+ if jsonErr := json.Unmarshal([]byte(skipBotsContent), &importedSkipBots); jsonErr == nil {
+ for _, user := range importedSkipBots {
+ if !skipBotsSet[user] {
+ skipBotsSet[user] = true
+ skipBots = append(skipBots, user)
}
}
}
@@ -713,7 +713,7 @@ func processImportsFromFrontmatterWithManifestAndSource(frontmatter map[string]a
MergedBots: bots,
MergedPlugins: plugins,
MergedSkipRoles: skipRoles,
- MergedSkipUsers: skipUsers,
+ MergedSkipBots: skipBots,
MergedPostSteps: postStepsBuilder.String(),
MergedLabels: labels,
MergedCaches: caches,
diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json
index ca50204288..8fb28ed72e 100644
--- a/pkg/parser/schemas/main_workflow_schema.json
+++ b/pkg/parser/schemas/main_workflow_schema.json
@@ -1362,7 +1362,7 @@
],
"description": "Skip workflow execution for users with specific repository roles. Useful for workflows that should only run for external contributors or specific permission levels."
},
- "skip-users": {
+ "skip-bots": {
"oneOf": [
{
"type": "string",
diff --git a/pkg/workflow/compiler_activation_jobs.go b/pkg/workflow/compiler_activation_jobs.go
index c7d79af9d7..bcd6a08ff2 100644
--- a/pkg/workflow/compiler_activation_jobs.go
+++ b/pkg/workflow/compiler_activation_jobs.go
@@ -171,20 +171,20 @@ func (c *Compiler) buildPreActivationJob(data *WorkflowData, needsPermissionChec
steps = append(steps, generateGitHubScriptWithRequire("check_skip_roles.cjs"))
}
- // Add skip-users check if configured
- if len(data.SkipUsers) > 0 {
- // Extract workflow name for the skip-users check
+ // Add skip-bots check if configured
+ if len(data.SkipBots) > 0 {
+ // Extract workflow name for the skip-bots check
workflowName := data.Name
- steps = append(steps, " - name: Check skip-users\n")
- steps = append(steps, fmt.Sprintf(" id: %s\n", constants.CheckSkipUsersStepID))
+ steps = append(steps, " - name: Check skip-bots\n")
+ steps = append(steps, fmt.Sprintf(" id: %s\n", constants.CheckSkipBotsStepID))
steps = append(steps, fmt.Sprintf(" uses: %s\n", GetActionPin("actions/github-script")))
steps = append(steps, " env:\n")
- steps = append(steps, fmt.Sprintf(" GH_AW_SKIP_USERS: %s\n", strings.Join(data.SkipUsers, ",")))
+ steps = append(steps, fmt.Sprintf(" GH_AW_SKIP_BOTS: %s\n", strings.Join(data.SkipBots, ",")))
steps = append(steps, fmt.Sprintf(" GH_AW_WORKFLOW_NAME: %q\n", workflowName))
steps = append(steps, " with:\n")
steps = append(steps, " script: |\n")
- steps = append(steps, generateGitHubScriptWithRequire("check_skip_users.cjs"))
+ steps = append(steps, generateGitHubScriptWithRequire("check_skip_bots.cjs"))
}
// Add command position check if this is a command workflow
@@ -263,14 +263,14 @@ func (c *Compiler) buildPreActivationJob(data *WorkflowData, needsPermissionChec
conditions = append(conditions, skipRolesCheckOk)
}
- if len(data.SkipUsers) > 0 {
- // Add skip-users check condition
- skipUsersCheckOk := BuildComparison(
- BuildPropertyAccess(fmt.Sprintf("steps.%s.outputs.%s", constants.CheckSkipUsersStepID, constants.SkipUsersOkOutput)),
+ if len(data.SkipBots) > 0 {
+ // Add skip-bots check condition
+ skipBotsCheckOk := BuildComparison(
+ BuildPropertyAccess(fmt.Sprintf("steps.%s.outputs.%s", constants.CheckSkipBotsStepID, constants.SkipBotsOkOutput)),
"==",
BuildStringLiteral("true"),
)
- conditions = append(conditions, skipUsersCheckOk)
+ conditions = append(conditions, skipBotsCheckOk)
}
if data.RateLimit != nil {
diff --git a/pkg/workflow/compiler_jobs.go b/pkg/workflow/compiler_jobs.go
index 870cf9771c..8b539bba39 100644
--- a/pkg/workflow/compiler_jobs.go
+++ b/pkg/workflow/compiler_jobs.go
@@ -178,13 +178,13 @@ func (c *Compiler) buildPreActivationAndActivationJobs(data *WorkflowData, front
hasSkipIfMatch := data.SkipIfMatch != nil
hasSkipIfNoMatch := data.SkipIfNoMatch != nil
hasSkipRoles := len(data.SkipRoles) > 0
- hasSkipUsers := len(data.SkipUsers) > 0
+ hasSkipBots := len(data.SkipBots) > 0
hasCommandTrigger := len(data.Command) > 0
hasRateLimit := data.RateLimit != nil
- compilerJobsLog.Printf("Job configuration: needsPermissionCheck=%v, hasStopTime=%v, hasSkipIfMatch=%v, hasSkipIfNoMatch=%v, hasSkipRoles=%v, hasSkipUsers=%v, hasCommand=%v, hasRateLimit=%v", needsPermissionCheck, hasStopTime, hasSkipIfMatch, hasSkipIfNoMatch, hasSkipRoles, hasSkipUsers, hasCommandTrigger, hasRateLimit)
+ compilerJobsLog.Printf("Job configuration: needsPermissionCheck=%v, hasStopTime=%v, hasSkipIfMatch=%v, hasSkipIfNoMatch=%v, hasSkipRoles=%v, hasSkipBots=%v, hasCommand=%v, hasRateLimit=%v", needsPermissionCheck, hasStopTime, hasSkipIfMatch, hasSkipIfNoMatch, hasSkipRoles, hasSkipBots, hasCommandTrigger, hasRateLimit)
- // Build pre-activation job if needed (combines membership checks, stop-time validation, skip-if-match check, skip-if-no-match check, skip-roles check, skip-users check, rate limit check, and command position check)
- if needsPermissionCheck || hasStopTime || hasSkipIfMatch || hasSkipIfNoMatch || hasSkipRoles || hasSkipUsers || hasCommandTrigger || hasRateLimit {
+ // Build pre-activation job if needed (combines membership checks, stop-time validation, skip-if-match check, skip-if-no-match check, skip-roles check, skip-bots check, rate limit check, and command position check)
+ if needsPermissionCheck || hasStopTime || hasSkipIfMatch || hasSkipIfNoMatch || hasSkipRoles || hasSkipBots || hasCommandTrigger || hasRateLimit {
compilerJobsLog.Print("Building pre-activation job")
preActivationJob, err := c.buildPreActivationJob(data, needsPermissionCheck)
if err != nil {
diff --git a/pkg/workflow/compiler_orchestrator_workflow.go b/pkg/workflow/compiler_orchestrator_workflow.go
index b01f7a7872..8ccab3c550 100644
--- a/pkg/workflow/compiler_orchestrator_workflow.go
+++ b/pkg/workflow/compiler_orchestrator_workflow.go
@@ -438,7 +438,7 @@ func (c *Compiler) extractAdditionalConfigurations(
workflowData.Bots = c.extractBots(frontmatter)
workflowData.RateLimit = c.extractRateLimitConfig(frontmatter)
workflowData.SkipRoles = c.mergeSkipRoles(c.extractSkipRoles(frontmatter), importsResult.MergedSkipRoles)
- workflowData.SkipUsers = c.mergeSkipUsers(c.extractSkipUsers(frontmatter), importsResult.MergedSkipUsers)
+ workflowData.SkipBots = c.mergeSkipBots(c.extractSkipBots(frontmatter), importsResult.MergedSkipBots)
// Use the already extracted output configuration
workflowData.SafeOutputs = safeOutputs
diff --git a/pkg/workflow/compiler_types.go b/pkg/workflow/compiler_types.go
index 54e729f4d0..013fbb89f0 100644
--- a/pkg/workflow/compiler_types.go
+++ b/pkg/workflow/compiler_types.go
@@ -417,7 +417,7 @@ type WorkflowData struct {
SkipIfMatch *SkipIfMatchConfig // skip-if-match configuration with query and max threshold
SkipIfNoMatch *SkipIfNoMatchConfig // skip-if-no-match configuration with query and min threshold
SkipRoles []string // roles to skip workflow for (e.g., [admin, maintainer, write])
- SkipUsers []string // users to skip workflow for (e.g., [user1, user2])
+ SkipBots []string // users to skip workflow for (e.g., [user1, user2])
ManualApproval string // environment name for manual approval from on: section
Command []string // for /command trigger support - multiple command names
CommandEvents []string // events where command should be active (nil = all events)
diff --git a/pkg/workflow/frontmatter_extraction_yaml.go b/pkg/workflow/frontmatter_extraction_yaml.go
index 4275956f6d..50dd76c69a 100644
--- a/pkg/workflow/frontmatter_extraction_yaml.go
+++ b/pkg/workflow/frontmatter_extraction_yaml.go
@@ -156,7 +156,7 @@ func (c *Compiler) commentOutProcessedFieldsInOnSection(yamlStr string, frontmat
inSkipIfMatch := false
inSkipIfNoMatch := false
inSkipRolesArray := false
- inSkipUsersArray := false
+ inSkipBotsArray := false
currentSection := "" // Track which section we're in ("issues", "pull_request", "discussion", or "issue_comment")
for _, line := range lines {
@@ -231,11 +231,11 @@ func (c *Compiler) commentOutProcessedFieldsInOnSection(yamlStr string, frontmat
inSkipRolesArray = true
}
- // Check if we're entering skip-users array
- if !inPullRequest && !inIssues && !inDiscussion && !inIssueComment && strings.HasPrefix(trimmedLine, "skip-users:") {
+ // Check if we're entering skip-bots array
+ if !inPullRequest && !inIssues && !inDiscussion && !inIssueComment && strings.HasPrefix(trimmedLine, "skip-bots:") {
// Check if this is an array (next line will be "- ")
// We'll set the flag and handle it on the next iteration
- inSkipUsersArray = true
+ inSkipBotsArray = true
}
// Check if we're entering skip-if-match object
@@ -304,14 +304,14 @@ func (c *Compiler) commentOutProcessedFieldsInOnSection(yamlStr string, frontmat
}
}
- // Check if we're leaving the skip-users array by encountering another top-level field
- if inSkipUsersArray && strings.TrimSpace(line) != "" {
+ // Check if we're leaving the skip-bots array by encountering another top-level field
+ if inSkipBotsArray && strings.TrimSpace(line) != "" {
// Get the indentation of the current line
lineIndent := len(line) - len(strings.TrimLeft(line, " \t"))
- // If this is a non-dash line at the same level as skip-users (2 spaces), we're out of the array
- if lineIndent == 2 && !strings.HasPrefix(trimmedLine, "-") && !strings.HasPrefix(trimmedLine, "skip-users:") && !strings.HasPrefix(trimmedLine, "#") {
- inSkipUsersArray = false
+ // If this is a non-dash line at the same level as skip-bots (2 spaces), we're out of the array
+ if lineIndent == 2 && !strings.HasPrefix(trimmedLine, "-") && !strings.HasPrefix(trimmedLine, "skip-bots:") && !strings.HasPrefix(trimmedLine, "#") {
+ inSkipBotsArray = false
}
}
@@ -348,13 +348,13 @@ func (c *Compiler) commentOutProcessedFieldsInOnSection(yamlStr string, frontmat
// Comment out array items in skip-roles
shouldComment = true
commentReason = " # Skip-roles processed as role check in pre-activation job"
- } else if strings.HasPrefix(trimmedLine, "skip-users:") {
+ } else if strings.HasPrefix(trimmedLine, "skip-bots:") {
shouldComment = true
- commentReason = " # Skip-users processed as user check in pre-activation job"
- } else if inSkipUsersArray && strings.HasPrefix(trimmedLine, "-") {
- // Comment out array items in skip-users
+ commentReason = " # Skip-bots processed as bot check in pre-activation job"
+ } else if inSkipBotsArray && strings.HasPrefix(trimmedLine, "-") {
+ // Comment out array items in skip-bots
shouldComment = true
- commentReason = " # Skip-users processed as user check in pre-activation job"
+ commentReason = " # Skip-bots processed as bot check in pre-activation job"
} else if strings.HasPrefix(trimmedLine, "reaction:") {
shouldComment = true
commentReason = " # Reaction processed as activation job step"
diff --git a/pkg/workflow/role_checks.go b/pkg/workflow/role_checks.go
index c370db1726..5ae43b5eab 100644
--- a/pkg/workflow/role_checks.go
+++ b/pkg/workflow/role_checks.go
@@ -489,9 +489,9 @@ func (c *Compiler) extractSkipRoles(frontmatter map[string]any) []string {
return nil
}
-// extractSkipUsers extracts the 'skip-users' field from the 'on:' section of frontmatter
-// Returns nil if skip-users is not configured
-func (c *Compiler) extractSkipUsers(frontmatter map[string]any) []string {
+// extractSkipBots extracts the 'skip-bots' field from the 'on:' section of frontmatter
+// Returns nil if skip-bots is not configured
+func (c *Compiler) extractSkipBots(frontmatter map[string]any) []string {
// Check the "on" section in frontmatter
onValue, exists := frontmatter["on"]
if !exists || onValue == nil {
@@ -501,9 +501,9 @@ func (c *Compiler) extractSkipUsers(frontmatter map[string]any) []string {
// Handle different formats of the on: section
switch on := onValue.(type) {
case map[string]any:
- // Complex object format - look for skip-users
- if skipUsersValue, exists := on["skip-users"]; exists && skipUsersValue != nil {
- return extractStringSliceField(skipUsersValue, "skip-users")
+ // Complex object format - look for skip-bots
+ if skipBotsValue, exists := on["skip-bots"]; exists && skipBotsValue != nil {
+ return extractStringSliceField(skipBotsValue, "skip-bots")
}
}
@@ -576,22 +576,22 @@ func (c *Compiler) mergeSkipRoles(topSkipRoles []string, importedSkipRoles []str
return result
}
-// mergeSkipUsers merges top-level skip-users with imported skip-users (union)
-func (c *Compiler) mergeSkipUsers(topSkipUsers []string, importedSkipUsers []string) []string {
+// mergeSkipBots merges top-level skip-bots with imported skip-bots (union)
+func (c *Compiler) mergeSkipBots(topSkipBots []string, importedSkipBots []string) []string {
// Create a set for deduplication
usersSet := make(map[string]bool)
var result []string
- // Add top-level skip-users first
- for _, user := range topSkipUsers {
+ // Add top-level skip-bots first
+ for _, user := range topSkipBots {
if !usersSet[user] {
usersSet[user] = true
result = append(result, user)
}
}
- // Merge imported skip-users
- for _, user := range importedSkipUsers {
+ // Merge imported skip-bots
+ for _, user := range importedSkipBots {
if !usersSet[user] {
usersSet[user] = true
result = append(result, user)
@@ -599,7 +599,7 @@ func (c *Compiler) mergeSkipUsers(topSkipUsers []string, importedSkipUsers []str
}
if len(result) > 0 {
- roleLog.Printf("Merged skip-users: %v (top=%d, imported=%d, total=%d)", result, len(topSkipUsers), len(importedSkipUsers), len(result))
+ roleLog.Printf("Merged skip-bots: %v (top=%d, imported=%d, total=%d)", result, len(topSkipBots), len(importedSkipBots), len(result))
}
return result
diff --git a/pkg/workflow/skip_users_test.go b/pkg/workflow/skip_bots_test.go
similarity index 57%
rename from pkg/workflow/skip_users_test.go
rename to pkg/workflow/skip_bots_test.go
index e7438f0d1a..607bc2d7dd 100644
--- a/pkg/workflow/skip_users_test.go
+++ b/pkg/workflow/skip_bots_test.go
@@ -13,25 +13,25 @@ import (
"github.com/stretchr/testify/require"
)
-// TestSkipUsersPreActivationJob tests that skip-users check is created correctly in pre-activation job
-func TestSkipUsersPreActivationJob(t *testing.T) {
- tmpDir := testutil.TempDir(t, "skip-users-test")
+// TestSkipBotsPreActivationJob tests that skip-bots check is created correctly in pre-activation job
+func TestSkipBotsPreActivationJob(t *testing.T) {
+ tmpDir := testutil.TempDir(t, "skip-bots-test")
compiler := NewCompiler()
- t.Run("pre_activation_job_created_with_skip_users", func(t *testing.T) {
+ t.Run("pre_activation_job_created_with_skip_bots", func(t *testing.T) {
workflowContent := `---
on:
issues:
types: [opened]
- skip-users: [user1, user2, user3]
+ skip-bots: [user1, user2, user3]
engine: copilot
---
# Skip Users Workflow
-This workflow has a skip-users configuration.
+This workflow has a skip-bots configuration.
`
- workflowFile := filepath.Join(tmpDir, "skip-users-workflow.md")
+ workflowFile := filepath.Join(tmpDir, "skip-bots-workflow.md")
err := os.WriteFile(workflowFile, []byte(workflowContent), 0644)
require.NoError(t, err, "Failed to write workflow file")
@@ -47,28 +47,28 @@ This workflow has a skip-users configuration.
// Verify pre_activation job exists
assert.Contains(t, lockContentStr, "pre_activation:", "Expected pre_activation job to be created")
- // Verify skip-users check is present
- assert.Contains(t, lockContentStr, "Check skip-users", "Expected skip-users check to be present")
+ // Verify skip-bots check is present
+ assert.Contains(t, lockContentStr, "Check skip-bots", "Expected skip-bots check to be present")
// Verify the skip users environment variable is set correctly
- assert.Contains(t, lockContentStr, "GH_AW_SKIP_USERS: user1,user2,user3", "Expected GH_AW_SKIP_USERS environment variable with correct value")
+ assert.Contains(t, lockContentStr, "GH_AW_SKIP_BOTS: user1,user2,user3", "Expected GH_AW_SKIP_BOTS environment variable with correct value")
- // Verify the check_skip_users step ID is present
- assert.Contains(t, lockContentStr, "id: check_skip_users", "Expected check_skip_users step ID")
+ // Verify the check_skip_bots step ID is present
+ assert.Contains(t, lockContentStr, "id: check_skip_bots", "Expected check_skip_bots step ID")
- // Verify the activated output includes skip_users_ok condition
- assert.Contains(t, lockContentStr, "steps.check_skip_users.outputs.skip_users_ok", "Expected activated output to include skip_users_ok condition")
+ // Verify the activated output includes skip_bots_ok condition
+ assert.Contains(t, lockContentStr, "steps.check_skip_bots.outputs.skip_bots_ok", "Expected activated output to include skip_bots_ok condition")
- // Verify skip-users is commented out in the frontmatter
- assert.Contains(t, lockContentStr, "# skip-users:", "Expected skip-users to be commented out in lock file")
+ // Verify skip-bots is commented out in the frontmatter
+ assert.Contains(t, lockContentStr, "# skip-bots:", "Expected skip-bots to be commented out in lock file")
})
- t.Run("skip_users_with_single_user", func(t *testing.T) {
+ t.Run("skip_bots_with_single_user", func(t *testing.T) {
workflowContent := `---
on:
issues:
types: [opened]
- skip-users: user1
+ skip-bots: user1
engine: copilot
---
@@ -76,7 +76,7 @@ engine: copilot
This workflow skips only for user1.
`
- workflowFile := filepath.Join(tmpDir, "skip-users-single.md")
+ workflowFile := filepath.Join(tmpDir, "skip-bots-single.md")
err := os.WriteFile(workflowFile, []byte(workflowContent), 0644)
require.NoError(t, err, "Failed to write workflow file")
@@ -89,14 +89,14 @@ This workflow skips only for user1.
lockContentStr := string(lockContent)
- // Verify skip-users check is present
- assert.Contains(t, lockContentStr, "Check skip-users", "Expected skip-users check to be present")
+ // Verify skip-bots check is present
+ assert.Contains(t, lockContentStr, "Check skip-bots", "Expected skip-bots check to be present")
// Verify single user
- assert.Contains(t, lockContentStr, "GH_AW_SKIP_USERS: user1", "Expected GH_AW_SKIP_USERS with single user")
+ assert.Contains(t, lockContentStr, "GH_AW_SKIP_BOTS: user1", "Expected GH_AW_SKIP_BOTS with single user")
})
- t.Run("no_skip_users_no_check_created", func(t *testing.T) {
+ t.Run("no_skip_bots_no_check_created", func(t *testing.T) {
workflowContent := `---
on:
issues:
@@ -106,9 +106,9 @@ engine: copilot
# No Skip Users Workflow
-This workflow has no skip-users configuration.
+This workflow has no skip-bots configuration.
`
- workflowFile := filepath.Join(tmpDir, "no-skip-users.md")
+ workflowFile := filepath.Join(tmpDir, "no-skip-bots.md")
err := os.WriteFile(workflowFile, []byte(workflowContent), 0644)
require.NoError(t, err, "Failed to write workflow file")
@@ -121,27 +121,27 @@ This workflow has no skip-users configuration.
lockContentStr := string(lockContent)
- // Verify skip-users check is NOT present
- assert.NotContains(t, lockContentStr, "Check skip-users", "Expected skip-users check to NOT be present")
- assert.NotContains(t, lockContentStr, "GH_AW_SKIP_USERS", "Expected GH_AW_SKIP_USERS to NOT be present")
- assert.NotContains(t, lockContentStr, "check_skip_users", "Expected check_skip_users step to NOT be present")
+ // Verify skip-bots check is NOT present
+ assert.NotContains(t, lockContentStr, "Check skip-bots", "Expected skip-bots check to NOT be present")
+ assert.NotContains(t, lockContentStr, "GH_AW_SKIP_BOTS", "Expected GH_AW_SKIP_BOTS to NOT be present")
+ assert.NotContains(t, lockContentStr, "check_skip_bots", "Expected check_skip_bots step to NOT be present")
})
- t.Run("skip_users_with_roles_field", func(t *testing.T) {
+ t.Run("skip_bots_with_roles_field", func(t *testing.T) {
workflowContent := `---
on:
issues:
types: [opened]
- skip-users: [user1, user2]
+ skip-bots: [user1, user2]
roles: [maintainer]
engine: copilot
---
# Skip Users with Roles Field
-This workflow has both roles and skip-users.
+This workflow has both roles and skip-bots.
`
- workflowFile := filepath.Join(tmpDir, "skip-users-with-roles.md")
+ workflowFile := filepath.Join(tmpDir, "skip-bots-with-roles.md")
err := os.WriteFile(workflowFile, []byte(workflowContent), 0644)
require.NoError(t, err, "Failed to write workflow file")
@@ -154,36 +154,36 @@ This workflow has both roles and skip-users.
lockContentStr := string(lockContent)
- // Verify both membership check and skip-users check are present
+ // Verify both membership check and skip-bots check are present
assert.Contains(t, lockContentStr, "Check team membership", "Expected team membership check to be present")
- assert.Contains(t, lockContentStr, "Check skip-users", "Expected skip-users check to be present")
+ assert.Contains(t, lockContentStr, "Check skip-bots", "Expected skip-bots check to be present")
// Verify GH_AW_REQUIRED_ROLES is set
assert.Contains(t, lockContentStr, "GH_AW_REQUIRED_ROLES: maintainer", "Expected GH_AW_REQUIRED_ROLES for roles field")
- // Verify GH_AW_SKIP_USERS is set
- assert.Contains(t, lockContentStr, "GH_AW_SKIP_USERS: user1,user2", "Expected GH_AW_SKIP_USERS for skip-users field")
+ // Verify GH_AW_SKIP_BOTS is set
+ assert.Contains(t, lockContentStr, "GH_AW_SKIP_BOTS: user1,user2", "Expected GH_AW_SKIP_BOTS for skip-bots field")
// Verify both conditions in activated output
assert.Contains(t, lockContentStr, "steps.check_membership.outputs.is_team_member", "Expected membership check in activated output")
- assert.Contains(t, lockContentStr, "steps.check_skip_users.outputs.skip_users_ok", "Expected skip-users check in activated output")
+ assert.Contains(t, lockContentStr, "steps.check_skip_bots.outputs.skip_bots_ok", "Expected skip-bots check in activated output")
})
- t.Run("skip_users_and_skip_roles_combined", func(t *testing.T) {
+ t.Run("skip_bots_and_skip_roles_combined", func(t *testing.T) {
workflowContent := `---
on:
issues:
types: [opened]
skip-roles: [admin, write]
- skip-users: [user1, user2]
+ skip-bots: [user1, user2]
engine: copilot
---
# Skip Users and Skip Roles Combined
-This workflow has both skip-roles and skip-users.
+This workflow has both skip-roles and skip-bots.
`
- workflowFile := filepath.Join(tmpDir, "skip-users-and-roles.md")
+ workflowFile := filepath.Join(tmpDir, "skip-bots-and-roles.md")
err := os.WriteFile(workflowFile, []byte(workflowContent), 0644)
require.NoError(t, err, "Failed to write workflow file")
@@ -196,22 +196,22 @@ This workflow has both skip-roles and skip-users.
lockContentStr := string(lockContent)
- // Verify both skip-roles and skip-users checks are present
+ // Verify both skip-roles and skip-bots checks are present
assert.Contains(t, lockContentStr, "Check skip-roles", "Expected skip-roles check to be present")
- assert.Contains(t, lockContentStr, "Check skip-users", "Expected skip-users check to be present")
+ assert.Contains(t, lockContentStr, "Check skip-bots", "Expected skip-bots check to be present")
// Verify both environment variables are set
assert.Contains(t, lockContentStr, "GH_AW_SKIP_ROLES: admin,write", "Expected GH_AW_SKIP_ROLES for skip-roles field")
- assert.Contains(t, lockContentStr, "GH_AW_SKIP_USERS: user1,user2", "Expected GH_AW_SKIP_USERS for skip-users field")
+ assert.Contains(t, lockContentStr, "GH_AW_SKIP_BOTS: user1,user2", "Expected GH_AW_SKIP_BOTS for skip-bots field")
// Verify both conditions in activated output
assert.Contains(t, lockContentStr, "steps.check_skip_roles.outputs.skip_roles_ok", "Expected skip-roles check in activated output")
- assert.Contains(t, lockContentStr, "steps.check_skip_users.outputs.skip_users_ok", "Expected skip-users check in activated output")
+ assert.Contains(t, lockContentStr, "steps.check_skip_bots.outputs.skip_bots_ok", "Expected skip-bots check in activated output")
})
}
-// TestExtractSkipUsers tests the extractSkipUsers function
-func TestExtractSkipUsers(t *testing.T) {
+// TestExtractSkipBots tests the extractSkipBots function
+func TestExtractSkipBots(t *testing.T) {
compiler := NewCompiler()
tests := []struct {
@@ -220,43 +220,43 @@ func TestExtractSkipUsers(t *testing.T) {
expected []string
}{
{
- name: "skip-users as array of strings",
+ name: "skip-bots as array of strings",
frontmatter: map[string]any{
"on": map[string]any{
"issues": map[string]any{
"types": []string{"opened"},
},
- "skip-users": []string{"user1", "user2"},
+ "skip-bots": []string{"user1", "user2"},
},
},
expected: []string{"user1", "user2"},
},
{
- name: "skip-users as single string",
+ name: "skip-bots as single string",
frontmatter: map[string]any{
"on": map[string]any{
"issues": map[string]any{
"types": []string{"opened"},
},
- "skip-users": "user1",
+ "skip-bots": "user1",
},
},
expected: []string{"user1"},
},
{
- name: "skip-users as array of any",
+ name: "skip-bots as array of any",
frontmatter: map[string]any{
"on": map[string]any{
"issues": map[string]any{
"types": []string{"opened"},
},
- "skip-users": []any{"user1", "user2", "user3"},
+ "skip-bots": []any{"user1", "user2", "user3"},
},
},
expected: []string{"user1", "user2", "user3"},
},
{
- name: "no skip-users configured",
+ name: "no skip-bots configured",
frontmatter: map[string]any{
"on": map[string]any{
"issues": map[string]any{
@@ -267,31 +267,31 @@ func TestExtractSkipUsers(t *testing.T) {
expected: nil,
},
{
- name: "empty skip-users array",
+ name: "empty skip-bots array",
frontmatter: map[string]any{
"on": map[string]any{
"issues": map[string]any{
"types": []string{"opened"},
},
- "skip-users": []string{},
+ "skip-bots": []string{},
},
},
expected: nil,
},
{
- name: "skip-users as empty string",
+ name: "skip-bots as empty string",
frontmatter: map[string]any{
"on": map[string]any{
"issues": map[string]any{
"types": []string{"opened"},
},
- "skip-users": "",
+ "skip-bots": "",
},
},
expected: nil,
},
{
- name: "on as string (no skip-users possible)",
+ name: "on as string (no skip-bots possible)",
frontmatter: map[string]any{
"on": "push",
},
@@ -306,8 +306,8 @@ func TestExtractSkipUsers(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- result := compiler.extractSkipUsers(tt.frontmatter)
- assert.Equal(t, tt.expected, result, "extractSkipUsers result mismatch")
+ result := compiler.extractSkipBots(tt.frontmatter)
+ assert.Equal(t, tt.expected, result, "extractSkipBots result mismatch")
})
}
}