From 6567c2e8cb6d44320b911a570b7032d696b18a37 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 21 Jun 2026 00:12:44 +0000 Subject: [PATCH 1/3] Initial plan From 0b0e1713e05578dfbdb2cdc82ffb91ac42b17b77 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 21 Jun 2026 00:24:17 +0000 Subject: [PATCH 2/3] refactor: consolidate hasStringKey into pkg/setutil.Contains - Add pkg/setutil with generic Contains[K comparable] function - Replace 217 hasStringKey call sites across 6 packages with setutil.Contains - Delete 6 per-package seenmapbool_helpers.go copies Closes #40531 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/agentdrain/mask.go | 3 +- pkg/agentdrain/seenmapbool_helpers.go | 6 --- pkg/cli/access_log.go | 5 ++- pkg/cli/add_interactive_engine.go | 6 ++- pkg/cli/codemod_engine_env_secrets.go | 7 ++-- ...odemod_safe_output_require_title_prefix.go | 3 +- pkg/cli/codemod_steps_run_secrets_env.go | 9 +++-- pkg/cli/compile_file_operations.go | 3 +- pkg/cli/compile_post_processing.go | 3 +- pkg/cli/compile_workflow_processor.go | 3 +- pkg/cli/dependency_graph.go | 3 +- pkg/cli/devcontainer.go | 5 ++- pkg/cli/drain3_integration.go | 3 +- pkg/cli/enable.go | 5 ++- pkg/cli/engine_secrets.go | 24 ++++++------ pkg/cli/engine_secrets_test.go | 6 ++- pkg/cli/experiments_command.go | 6 ++- pkg/cli/gateway_logs_rpc.go | 3 +- pkg/cli/generate_action_metadata_command.go | 7 ++-- pkg/cli/imports.go | 7 ++-- pkg/cli/includes.go | 5 ++- pkg/cli/interactive.go | 4 +- pkg/cli/jsonworkflow_to_markdown.go | 6 ++- pkg/cli/logs_artifact_set.go | 3 +- pkg/cli/logs_report_test.go | 7 ++-- pkg/cli/logs_safe_output_chains.go | 5 ++- pkg/cli/mcp_inspect_mcp.go | 6 ++- pkg/cli/mcp_tool_table.go | 5 ++- pkg/cli/packages.go | 3 +- pkg/cli/remove_command.go | 7 ++-- pkg/cli/run_push.go | 7 ++-- pkg/cli/secrets.go | 5 ++- pkg/cli/seenmapbool_helpers.go | 6 --- pkg/cli/workflow_secrets.go | 5 ++- pkg/cli/workflows.go | 3 +- pkg/cli/zizmor.go | 3 +- pkg/constants/engine_constants.go | 8 ++-- pkg/constants/seenmapbool_helpers.go | 6 --- pkg/linters/ssljson/seenmapbool_helpers.go | 6 --- pkg/linters/ssljson/ssljson.go | 10 +++-- pkg/parser/frontmatter_hash.go | 7 ++-- pkg/parser/import_bfs.go | 8 ++-- pkg/parser/import_cycle.go | 10 +++-- pkg/parser/import_field_extractor.go | 3 +- pkg/parser/import_topological.go | 6 ++- pkg/parser/include_processor.go | 5 ++- pkg/parser/schema_errors.go | 5 ++- pkg/parser/schema_suggestions.go | 9 +++-- pkg/parser/schema_validation.go | 3 +- pkg/parser/seenmapbool_helpers.go | 6 --- pkg/parser/tools_merger.go | 5 ++- pkg/setutil/README.md | 27 +++++++++++++ pkg/setutil/setutil.go | 9 +++++ pkg/setutil/setutil_test.go | 39 +++++++++++++++++++ pkg/workflow/action_cache.go | 3 +- pkg/workflow/agentic_engine.go | 7 ++-- pkg/workflow/awf_config.go | 6 ++- pkg/workflow/awf_helpers.go | 3 +- pkg/workflow/bot_aliases.go | 5 ++- pkg/workflow/checkout_manager.go | 9 +++-- pkg/workflow/command.go | 3 +- pkg/workflow/compiler_activation_job.go | 20 +++++----- pkg/workflow/compiler_jobs.go | 10 +++-- pkg/workflow/compiler_main_job.go | 3 +- pkg/workflow/compiler_orchestrator_tools.go | 3 +- .../compiler_orchestrator_workflow.go | 5 ++- pkg/workflow/compiler_pre_activation_job.go | 3 +- pkg/workflow/compiler_safe_outputs_job.go | 3 +- pkg/workflow/copilot_engine_tools.go | 3 +- pkg/workflow/dependabot.go | 8 ++-- pkg/workflow/dependabot_test.go | 3 +- pkg/workflow/docker.go | 27 ++++++------- pkg/workflow/engine_auth_test.go | 6 ++- pkg/workflow/engine_helpers.go | 3 +- pkg/workflow/expression_extraction.go | 3 +- pkg/workflow/frontmatter_extraction_yaml.go | 8 ++-- pkg/workflow/git_configuration_steps.go | 3 +- pkg/workflow/github_toolsets.go | 11 +++--- pkg/workflow/imports.go | 9 +++-- pkg/workflow/known_needs_expressions.go | 3 +- pkg/workflow/map_helpers.go | 3 +- pkg/workflow/mcp_config_custom.go | 3 +- pkg/workflow/mcp_config_validation.go | 3 +- pkg/workflow/mcp_github_config.go | 5 ++- pkg/workflow/mcp_setup_generator.go | 3 +- pkg/workflow/model_alias_validation.go | 5 ++- pkg/workflow/on_needs_validation.go | 11 +++--- pkg/workflow/package_extraction.go | 7 ++-- pkg/workflow/permissions_validation.go | 6 ++- pkg/workflow/publish_artifacts_test.go | 4 +- pkg/workflow/run_step_sanitizer.go | 6 ++- pkg/workflow/runtime_deduplication.go | 6 ++- pkg/workflow/runtime_import_validation.go | 3 +- pkg/workflow/safe_jobs_needs_validation.go | 3 +- .../safe_jobs_needs_validation_test.go | 28 ++++++------- pkg/workflow/safe_outputs_needs_validation.go | 3 +- pkg/workflow/safe_update_enforcement.go | 7 ++-- pkg/workflow/safe_update_manifest.go | 7 ++-- pkg/workflow/seenmapbool_helpers.go | 6 --- pkg/workflow/shell.go | 3 +- pkg/workflow/side_repo_maintenance.go | 3 +- pkg/workflow/strict_mode_env_validation.go | 3 +- pkg/workflow/submit_pr_review.go | 3 +- pkg/workflow/template.go | 3 +- pkg/workflow/time_delta.go | 3 +- pkg/workflow/tools_parser.go | 5 ++- .../tools_validation_github_toolsets.go | 3 +- pkg/workflow/trigger_parser.go | 16 ++++---- 108 files changed, 432 insertions(+), 268 deletions(-) delete mode 100644 pkg/agentdrain/seenmapbool_helpers.go delete mode 100644 pkg/cli/seenmapbool_helpers.go delete mode 100644 pkg/constants/seenmapbool_helpers.go delete mode 100644 pkg/linters/ssljson/seenmapbool_helpers.go delete mode 100644 pkg/parser/seenmapbool_helpers.go create mode 100644 pkg/setutil/README.md create mode 100644 pkg/setutil/setutil.go create mode 100644 pkg/setutil/setutil_test.go delete mode 100644 pkg/workflow/seenmapbool_helpers.go diff --git a/pkg/agentdrain/mask.go b/pkg/agentdrain/mask.go index 1a4e947bcf9..bc1158165a2 100644 --- a/pkg/agentdrain/mask.go +++ b/pkg/agentdrain/mask.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/setutil" "github.com/github/gh-aw/pkg/sliceutil" ) @@ -67,7 +68,7 @@ func FlattenEvent(evt AgentEvent, excludeFields []string) string { } keys := sliceutil.FilterMapKeys(evt.Fields, func(k string, _ string) bool { - return !hasStringKey(excluded, k) + return !setutil.Contains(excluded, k) }) sort.Strings(keys) diff --git a/pkg/agentdrain/seenmapbool_helpers.go b/pkg/agentdrain/seenmapbool_helpers.go deleted file mode 100644 index af30fd49d53..00000000000 --- a/pkg/agentdrain/seenmapbool_helpers.go +++ /dev/null @@ -1,6 +0,0 @@ -package agentdrain - -func hasStringKey(set map[string]struct{}, key string) bool { - _, ok := set[key] - return ok -} diff --git a/pkg/cli/access_log.go b/pkg/cli/access_log.go index 3a0de966e17..01e8ae9229b 100644 --- a/pkg/cli/access_log.go +++ b/pkg/cli/access_log.go @@ -10,6 +10,7 @@ import ( "github.com/github/gh-aw/pkg/console" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/setutil" "github.com/github/gh-aw/pkg/stringutil" ) @@ -101,14 +102,14 @@ func parseSquidAccessLog(logPath string, verbose bool) (*DomainAnalysis, error) if isAllowed { analysis.AllowedCount++ - if !hasStringKey(allowedDomainsSet, domain) { + if !setutil.Contains(allowedDomainsSet, domain) { allowedDomainsSet[domain] = struct { }{} analysis.AllowedDomains = append(analysis.AllowedDomains, domain) } } else { analysis.BlockedCount++ - if !hasStringKey(blockedDomainsSet, domain) { + if !setutil.Contains(blockedDomainsSet, domain) { blockedDomainsSet[domain] = struct { }{} analysis.BlockedDomains = append(analysis.BlockedDomains, domain) diff --git a/pkg/cli/add_interactive_engine.go b/pkg/cli/add_interactive_engine.go index eb74a5b949b..cec47c0b319 100644 --- a/pkg/cli/add_interactive_engine.go +++ b/pkg/cli/add_interactive_engine.go @@ -7,8 +7,10 @@ import ( "strings" "charm.land/huh/v2" + "github.com/github/gh-aw/pkg/console" "github.com/github/gh-aw/pkg/constants" + "github.com/github/gh-aw/pkg/setutil" "github.com/github/gh-aw/pkg/sliceutil" "github.com/github/gh-aw/pkg/styles" "github.com/github/gh-aw/pkg/workflow" @@ -46,7 +48,7 @@ func (c *AddInteractiveConfig) selectAIEngineAndKey() error { // Priority 1: Check existing repository secrets using EngineOptions // This takes precedence over workflow preference since users should use what's already available for _, opt := range constants.EngineOptions { - if hasStringKey(c.existingSecrets, opt.SecretName) { + if setutil.Contains(c.existingSecrets, opt.SecretName) { defaultEngine = opt.Value addInteractiveLog.Printf("Found existing secret %s, recommending engine: %s", opt.SecretName, opt.Value) break @@ -94,7 +96,7 @@ func (c *AddInteractiveConfig) selectAIEngineAndKey() error { // Add markers for secret availability and workflow specification. // opt may be nil for catalog engines not yet represented in EngineOptions; // in that case we conservatively show '[no secret]'. - if opt != nil && hasStringKey(c.existingSecrets, opt.SecretName) { + if opt != nil && setutil.Contains(c.existingSecrets, opt.SecretName) { label += " [secret exists]" } else { label += " [no secret]" diff --git a/pkg/cli/codemod_engine_env_secrets.go b/pkg/cli/codemod_engine_env_secrets.go index ad3c37764d0..be164f683e7 100644 --- a/pkg/cli/codemod_engine_env_secrets.go +++ b/pkg/cli/codemod_engine_env_secrets.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/setutil" "github.com/github/gh-aw/pkg/sliceutil" "github.com/github/gh-aw/pkg/workflow" ) @@ -133,7 +134,7 @@ func findUnsafeEngineEnvSecretKeys(envMap map[string]any, allowed map[string]str unsafe := make(map[string]struct { }) for key, value := range envMap { - if hasStringKey(allowed, key) { + if setutil.Contains(allowed, key) { continue } strVal, ok := value.(string) @@ -208,7 +209,7 @@ func removeUnsafeEngineEnvKeys(lines []string, unsafeKeys map[string]struct { if inEnv && !removingKey && len(trimmed) > 0 && !strings.HasPrefix(trimmed, "#") && len(indent) > len(envIndent) { key := parseYAMLMapKey(trimmed) - if key != "" && hasStringKey(unsafeKeys, key) { + if key != "" && setutil.Contains(unsafeKeys, key) { modified = true removingKey = true removingKeyIndent = indent @@ -288,7 +289,7 @@ func getTopLevelEnvSecretsGuidedErrorCodemod() Codemod { seenExpressions := make(map[string]struct { }) for _, expr := range secretExpressions { - if hasStringKey(seenExpressions, expr) { + if setutil.Contains(seenExpressions, expr) { continue } seenExpressions[expr] = struct { diff --git a/pkg/cli/codemod_safe_output_require_title_prefix.go b/pkg/cli/codemod_safe_output_require_title_prefix.go index ca2c02d125d..27c673fb93d 100644 --- a/pkg/cli/codemod_safe_output_require_title_prefix.go +++ b/pkg/cli/codemod_safe_output_require_title_prefix.go @@ -4,6 +4,7 @@ import ( "strings" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/setutil" ) var safeOutputRequireTitlePrefixCodemodLog = logger.New("cli:codemod_safe_output_require_title_prefix") @@ -140,7 +141,7 @@ func renameSafeOutputTitlePrefixConstraints(lines []string, handlersToRename map continue } key := strings.TrimSuffix(trimmed, ":") - if hasStringKey(handlersToRename, key) { + if setutil.Contains(handlersToRename, key) { activeHandler = key activeHandlerIndent = indent handlerChildIndent = "" diff --git a/pkg/cli/codemod_steps_run_secrets_env.go b/pkg/cli/codemod_steps_run_secrets_env.go index bf29674d24a..05ba1073055 100644 --- a/pkg/cli/codemod_steps_run_secrets_env.go +++ b/pkg/cli/codemod_steps_run_secrets_env.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/setutil" ) var stepsRunSecretsEnvCodemodLog = logger.New("cli:codemod_steps_run_secrets_env") @@ -247,7 +248,7 @@ func rewriteStepRunSecretsToEnv(stepLines []string, stepIndent string) ([]string modified = true } for _, binding := range bindings { - if !hasStringKey(seen, binding.Name) { + if !setutil.Contains(seen, binding.Name) { seen[binding.Name] = struct { }{} orderedBindings = append(orderedBindings, binding.Name) @@ -264,7 +265,7 @@ func rewriteStepRunSecretsToEnv(stepLines []string, stepIndent string) ([]string modified = true } for _, binding := range bindings { - if !hasStringKey(seen, binding.Name) { + if !setutil.Contains(seen, binding.Name) { seen[binding.Name] = struct { }{} orderedBindings = append(orderedBindings, binding.Name) @@ -281,7 +282,7 @@ func rewriteStepRunSecretsToEnv(stepLines []string, stepIndent string) ([]string missingBindings := make([]string, 0, len(orderedBindings)) for _, name := range orderedBindings { - if !hasStringKey(existingEnvKeys, name) { + if !setutil.Contains(existingEnvKeys, name) { missingBindings = append(missingBindings, name) } } @@ -387,7 +388,7 @@ func replaceStepExpressionRefs(line string, shellIsPowerShell bool, existingBind } else { result.WriteString("$" + envName) } - if !hasStringKey(registeredNames, envName) { + if !setutil.Contains(registeredNames, envName) { registeredNames[envName] = struct { }{} ordered = append(ordered, stepExpressionBinding{ diff --git a/pkg/cli/compile_file_operations.go b/pkg/cli/compile_file_operations.go index 4362b47f8e0..bf0fa1c42ff 100644 --- a/pkg/cli/compile_file_operations.go +++ b/pkg/cli/compile_file_operations.go @@ -40,6 +40,7 @@ import ( "os" "path/filepath" + "github.com/github/gh-aw/pkg/setutil" "github.com/github/gh-aw/pkg/stringutil" "github.com/github/gh-aw/pkg/console" @@ -165,7 +166,7 @@ func compileModifiedFilesWithDependencies(ctx context.Context, compiler *workflo // Add to unique set for _, workflow := range affected { - if !hasStringKey(uniqueWorkflows, workflow) { + if !setutil.Contains(uniqueWorkflows, workflow) { uniqueWorkflows[workflow] = struct { }{} workflowsToCompile = append(workflowsToCompile, workflow) diff --git a/pkg/cli/compile_post_processing.go b/pkg/cli/compile_post_processing.go index 7d63dbd5b40..47c8717854b 100644 --- a/pkg/cli/compile_post_processing.go +++ b/pkg/cli/compile_post_processing.go @@ -43,6 +43,7 @@ import ( "github.com/github/gh-aw/pkg/console" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/setutil" "github.com/github/gh-aw/pkg/workflow" ) @@ -176,7 +177,7 @@ func purgeOrphanedLockFiles(workflowsDir string, expectedLockFiles []string, ver if strings.HasSuffix(existing, ".campaign.lock.yml") { continue } - if !hasStringKey(expectedLockFileSet, existing) { + if !setutil.Contains(expectedLockFileSet, existing) { orphanedFiles = append(orphanedFiles, existing) } } diff --git a/pkg/cli/compile_workflow_processor.go b/pkg/cli/compile_workflow_processor.go index 85bf3ad76f2..a3fd8ab1819 100644 --- a/pkg/cli/compile_workflow_processor.go +++ b/pkg/cli/compile_workflow_processor.go @@ -30,6 +30,7 @@ import ( "github.com/github/gh-aw/pkg/console" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/setutil" "github.com/github/gh-aw/pkg/stringutil" "github.com/github/gh-aw/pkg/workflow" ) @@ -202,7 +203,7 @@ func extractSafeOutputLabels(data *workflow.WorkflowData) []string { var labels []string addLabel := func(label string) { - if label != "" && !hasStringKey(seen, label) { + if label != "" && !setutil.Contains(seen, label) { seen[label] = struct { }{} labels = append(labels, label) diff --git a/pkg/cli/dependency_graph.go b/pkg/cli/dependency_graph.go index 7c490b26146..d7b1e8f6ed2 100644 --- a/pkg/cli/dependency_graph.go +++ b/pkg/cli/dependency_graph.go @@ -8,6 +8,7 @@ import ( "github.com/github/gh-aw/pkg/logger" "github.com/github/gh-aw/pkg/parser" + "github.com/github/gh-aw/pkg/setutil" "github.com/github/gh-aw/pkg/workflow" ) @@ -297,7 +298,7 @@ func (g *DependencyGraph) findAffectedTopLevelWorkflows(filePath string) []strin // Get all workflows that import this file importers := g.reverseImports[current] for _, importer := range importers { - if hasStringKey(visited, importer) { + if setutil.Contains(visited, importer) { continue } visited[importer] = struct { diff --git a/pkg/cli/devcontainer.go b/pkg/cli/devcontainer.go index 61d9c7df8ce..95680a74c53 100644 --- a/pkg/cli/devcontainer.go +++ b/pkg/cli/devcontainer.go @@ -10,6 +10,7 @@ import ( "github.com/github/gh-aw/pkg/constants" "github.com/github/gh-aw/pkg/fileutil" + "github.com/github/gh-aw/pkg/setutil" "github.com/github/gh-aw/pkg/gitutil" "github.com/github/gh-aw/pkg/logger" @@ -289,7 +290,7 @@ func mergeExtensions(existing, toAdd []string) []string { // Add existing extensions for _, ext := range existing { - if !hasStringKey(extensionSet, ext) { + if !setutil.Contains(extensionSet, ext) { extensionSet[ext] = struct { }{} result = append(result, ext) @@ -298,7 +299,7 @@ func mergeExtensions(existing, toAdd []string) []string { // Add new extensions if not already present for _, ext := range toAdd { - if !hasStringKey(extensionSet, ext) { + if !setutil.Contains(extensionSet, ext) { extensionSet[ext] = struct { }{} result = append(result, ext) diff --git a/pkg/cli/drain3_integration.go b/pkg/cli/drain3_integration.go index 99f7bef1bca..40af6b519f2 100644 --- a/pkg/cli/drain3_integration.go +++ b/pkg/cli/drain3_integration.go @@ -7,6 +7,7 @@ import ( "github.com/github/gh-aw/pkg/agentdrain" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/setutil" ) var drain3Log = logger.New("cli:drain3_integration") @@ -359,7 +360,7 @@ func buildAnomalyReasons(anomalies []struct { }) for _, a := range anomalies { r := fmt.Sprintf("stage=%s score=%.2f: %s", a.evt.Stage, a.report.AnomalyScore, a.report.Reason) - if !hasStringKey(seen, r) { + if !setutil.Contains(seen, r) { reasons = append(reasons, r) seen[r] = struct { }{} diff --git a/pkg/cli/enable.go b/pkg/cli/enable.go index 412b6aa955b..540426b6aa8 100644 --- a/pkg/cli/enable.go +++ b/pkg/cli/enable.go @@ -10,6 +10,7 @@ import ( "strconv" "strings" + "github.com/github/gh-aw/pkg/setutil" "github.com/github/gh-aw/pkg/stringutil" "github.com/github/gh-aw/pkg/console" @@ -321,7 +322,7 @@ func DisableAllWorkflowsExcept(repoSlug string, exceptWorkflows []string, verbos base := filepath.Base(yamlFile) // Skip if it's in the keep-enabled set - if hasStringKey(keepEnabled, base) { + if setutil.Contains(keepEnabled, base) { if verbose { fmt.Fprintf(os.Stderr, "Keeping enabled: %s\n", base) } @@ -330,7 +331,7 @@ func DisableAllWorkflowsExcept(repoSlug string, exceptWorkflows []string, verbos // Check if the base name without extension matches nameWithoutExt := strings.TrimSuffix(base, filepath.Ext(base)) - if hasStringKey(keepEnabled, nameWithoutExt) { + if setutil.Contains(keepEnabled, nameWithoutExt) { if verbose { fmt.Fprintf(os.Stderr, "Keeping enabled: %s\n", base) } diff --git a/pkg/cli/engine_secrets.go b/pkg/cli/engine_secrets.go index 97a4d3ab64c..bd8b32689a9 100644 --- a/pkg/cli/engine_secrets.go +++ b/pkg/cli/engine_secrets.go @@ -8,10 +8,12 @@ import ( "strings" "charm.land/huh/v2" + "github.com/github/gh-aw/pkg/console" "github.com/github/gh-aw/pkg/constants" "github.com/github/gh-aw/pkg/logger" "github.com/github/gh-aw/pkg/repoutil" + "github.com/github/gh-aw/pkg/setutil" "github.com/github/gh-aw/pkg/sliceutil" "github.com/github/gh-aw/pkg/stringutil" "github.com/github/gh-aw/pkg/styles" @@ -165,8 +167,8 @@ func getMissingRequiredSecrets(requirements []SecretRequirement, existingSecrets continue } - exists := hasStringKey(existingSecrets, req.Name) || sliceutil.Any(req.AlternativeEnvVars, func(alt string) bool { - return hasStringKey(existingSecrets, alt) + exists := setutil.Contains(existingSecrets, req.Name) || sliceutil.Any(req.AlternativeEnvVars, func(alt string) bool { + return setutil.Contains(existingSecrets, alt) }) if !exists { missing = append(missing, req) @@ -216,14 +218,14 @@ func ensureSecretAvailable(req SecretRequirement, config EngineSecretConfig) err engineSecretsLog.Printf("Ensuring secret available: %s", req.Name) // Check if secret already exists in the repository - if hasStringKey(config.ExistingSecrets, req.Name) { + if setutil.Contains(config.ExistingSecrets, req.Name) { fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Using existing %s secret in repository", req.Name))) return nil } // Check alternative secret names in repository for _, alt := range req.AlternativeEnvVars { - if hasStringKey(config.ExistingSecrets, alt) { + if setutil.Contains(config.ExistingSecrets, alt) { fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Using existing %s secret in repository (alternative for %s)", alt, req.Name))) return nil } @@ -436,7 +438,7 @@ func promptForGenericAPIKeyUnified(req SecretRequirement, config EngineSecretCon // checkOptionalSecret checks if an optional secret is available (without prompting) func checkOptionalSecret(req SecretRequirement, config EngineSecretConfig) error { // Check repository - if hasStringKey(config.ExistingSecrets, req.Name) { + if setutil.Contains(config.ExistingSecrets, req.Name) { if config.Verbose { fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Optional secret %s exists in repository", req.Name))) } @@ -537,14 +539,14 @@ func GetEngineSecretNameAndValue(engine string, existingSecrets map[string]struc secretName := opt.SecretName // Check if secret already exists in repository - if hasStringKey(existingSecrets, secretName) { + if setutil.Contains(existingSecrets, secretName) { engineSecretsLog.Printf("Secret %s already exists in repository", secretName) return secretName, "", true, nil } // Check alternative secret names in repository for _, alt := range opt.AlternativeSecrets { - if hasStringKey(existingSecrets, alt) { + if setutil.Contains(existingSecrets, alt) { engineSecretsLog.Printf("Alternative secret %s exists in repository", alt) return secretName, "", true, nil } @@ -579,8 +581,8 @@ func displayMissingSecrets(requirements []SecretRequirement, repoSlug string, ex for _, req := range requirements { // Check if secret exists - exists := hasStringKey(existingSecrets, req.Name) || sliceutil.Any(req.AlternativeEnvVars, func(alt string) bool { - return hasStringKey(existingSecrets, alt) + exists := setutil.Contains(existingSecrets, req.Name) || sliceutil.Any(req.AlternativeEnvVars, func(alt string) bool { + return setutil.Contains(existingSecrets, alt) }) if !exists { @@ -654,12 +656,12 @@ func displaySecretsSummaryTable(requirements []SecretRequirement, existingSecret // Display each required secret with status for _, req := range requiredOnly { // Check if secret exists - exists := hasStringKey(existingSecrets, req.Name) + exists := setutil.Contains(existingSecrets, req.Name) var altUsed string if !exists { // Check alternatives for _, alt := range req.AlternativeEnvVars { - if hasStringKey(existingSecrets, alt) { + if setutil.Contains(existingSecrets, alt) { exists = true altUsed = alt break diff --git a/pkg/cli/engine_secrets_test.go b/pkg/cli/engine_secrets_test.go index 395ebc0ba8a..92c70bf64fe 100644 --- a/pkg/cli/engine_secrets_test.go +++ b/pkg/cli/engine_secrets_test.go @@ -6,9 +6,11 @@ import ( "os" "testing" - "github.com/github/gh-aw/pkg/constants" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/github/gh-aw/pkg/constants" + "github.com/github/gh-aw/pkg/setutil" ) func TestGetRequiredSecretsForEngine(t *testing.T) { @@ -295,7 +297,7 @@ func TestEngineSecretConfigStructure(t *testing.T) { assert.Equal(t, "owner/repo", config.RepoSlug) assert.Equal(t, "copilot", config.Engine) assert.True(t, config.Verbose) - assert.True(t, hasStringKey(config.ExistingSecrets, "SECRET1")) + assert.True(t, setutil.Contains(config.ExistingSecrets, "SECRET1")) assert.True(t, config.IncludeSystemSecrets) assert.False(t, config.IncludeOptional) }) diff --git a/pkg/cli/experiments_command.go b/pkg/cli/experiments_command.go index 5331d7941be..04b49f00667 100644 --- a/pkg/cli/experiments_command.go +++ b/pkg/cli/experiments_command.go @@ -14,12 +14,14 @@ import ( "sort" "strings" + "github.com/spf13/cobra" + "github.com/github/gh-aw/pkg/console" "github.com/github/gh-aw/pkg/constants" "github.com/github/gh-aw/pkg/logger" "github.com/github/gh-aw/pkg/parser" + "github.com/github/gh-aw/pkg/setutil" "github.com/github/gh-aw/pkg/workflow" - "github.com/spf13/cobra" ) var experimentsLog = logger.New("cli:experiments_command") @@ -503,7 +505,7 @@ func fetchLocalExperiments() ([]ExperimentInfo, error) { continue } workflowID := extractExperimentName(line) - if workflowID == "" || hasStringKey(seen, workflowID) { + if workflowID == "" || setutil.Contains(seen, workflowID) { continue } seen[workflowID] = struct { diff --git a/pkg/cli/gateway_logs_rpc.go b/pkg/cli/gateway_logs_rpc.go index 9474ce2247f..c39443a3b98 100644 --- a/pkg/cli/gateway_logs_rpc.go +++ b/pkg/cli/gateway_logs_rpc.go @@ -13,6 +13,7 @@ import ( "time" "github.com/github/gh-aw/pkg/console" + "github.com/github/gh-aw/pkg/setutil" "github.com/github/gh-aw/pkg/timeutil" ) @@ -369,7 +370,7 @@ func buildToolCallsFromRPCMessages(logPath string) ([]MCPToolCall, error) { // Emit any requests that never received a response for key, p := range pending { - if !hasStringKey(processedKeys, key) { + if !setutil.Contains(processedKeys, key) { toolCalls = append(toolCalls, MCPToolCall{ Timestamp: p.timestamp.Format(time.RFC3339Nano), ServerName: p.serverID, diff --git a/pkg/cli/generate_action_metadata_command.go b/pkg/cli/generate_action_metadata_command.go index 36e066dfb51..776992072c1 100644 --- a/pkg/cli/generate_action_metadata_command.go +++ b/pkg/cli/generate_action_metadata_command.go @@ -11,6 +11,7 @@ import ( "strings" "github.com/github/gh-aw/pkg/constants" + "github.com/github/gh-aw/pkg/setutil" "github.com/github/gh-aw/pkg/console" "github.com/github/gh-aw/pkg/logger" @@ -223,7 +224,7 @@ func extractInputs(content string) []ActionInput { for _, match := range matches { if len(match) > 1 { inputName := match[1] - if !hasStringKey(seen, inputName) { + if !setutil.Contains(seen, inputName) { inputs = append(inputs, ActionInput{ Name: inputName, Description: "Input parameter: " + inputName, @@ -263,7 +264,7 @@ func extractOutputs(content string) []ActionOutput { for _, match := range matches { if len(match) > 1 { outputName := match[1] - if !hasStringKey(seen, outputName) { + if !setutil.Contains(seen, outputName) { outputs = append(outputs, ActionOutput{ Name: outputName, Description: "Output parameter: " + outputName, @@ -301,7 +302,7 @@ func extractDependencies(content string) []string { for _, match := range matches { if len(match) > 1 { dep := match[1] - if !hasStringKey(seen, dep) { + if !setutil.Contains(seen, dep) { deps = append(deps, dep) seen[dep] = struct { }{} diff --git a/pkg/cli/imports.go b/pkg/cli/imports.go index 142a07bc8ec..63d55aea51c 100644 --- a/pkg/cli/imports.go +++ b/pkg/cli/imports.go @@ -11,6 +11,7 @@ import ( "github.com/github/gh-aw/pkg/constants" "github.com/github/gh-aw/pkg/logger" "github.com/github/gh-aw/pkg/parser" + "github.com/github/gh-aw/pkg/setutil" "github.com/github/gh-aw/pkg/workflow" ) @@ -199,7 +200,7 @@ func processIncludesWithWorkflowSpec(content string, workflow *WorkflowSpec, com importsLog.Printf("Include path exists locally, preserving: %s", filePath) result.WriteString(line + "\n") // Add file to queue for processing nested includes (first visit only) - if !hasStringKey(visited, filePath) { + if !setutil.Contains(visited, filePath) { visited[filePath] = struct { }{} queue = append(queue, filePath) @@ -223,7 +224,7 @@ func processIncludesWithWorkflowSpec(content string, workflow *WorkflowSpec, com writeImportDirective(&result, workflowSpec, isOptional) // Only enqueue for nested-include processing on the first visit to prevent cycles - if !hasStringKey(visited, filePath) { + if !setutil.Contains(visited, filePath) { visited[filePath] = struct { }{} queue = append(queue, filePath) @@ -279,7 +280,7 @@ func processIncludesWithWorkflowSpec(content string, workflow *WorkflowSpec, com nestedFilePath, _ := splitImportPath(includePath) // Check for cycle detection - if hasStringKey(visited, nestedFilePath) { + if setutil.Contains(visited, nestedFilePath) { if verbose { fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Cycle detected for include: %s, skipping", nestedFilePath))) } diff --git a/pkg/cli/includes.go b/pkg/cli/includes.go index 3fa08b955f5..c4635ce2b2f 100644 --- a/pkg/cli/includes.go +++ b/pkg/cli/includes.go @@ -12,6 +12,7 @@ import ( "strings" "github.com/github/gh-aw/pkg/constants" + "github.com/github/gh-aw/pkg/setutil" "github.com/github/gh-aw/pkg/console" "github.com/github/gh-aw/pkg/parser" @@ -302,7 +303,7 @@ func fetchFrontmatterImportsRecursive(content, currentBaseDir string, opts front } // Cycle/duplicate prevention: use the fully-resolved remote path as the key. - if hasStringKey(opts.seen, remoteFilePath) { + if setutil.Contains(opts.seen, remoteFilePath) { remoteWorkflowLog.Printf("Skipping already-seen import: %s", remoteFilePath) continue } @@ -431,7 +432,7 @@ func fetchAndSaveRemoteIncludes(content string, spec *WorkflowSpec, targetDir st } // Skip if already processed - if hasStringKey(seen, filePath) { + if setutil.Contains(seen, filePath) { continue } seen[filePath] = struct { diff --git a/pkg/cli/interactive.go b/pkg/cli/interactive.go index 25509749cd6..8b5bd84feaf 100644 --- a/pkg/cli/interactive.go +++ b/pkg/cli/interactive.go @@ -11,9 +11,11 @@ import ( "strings" "charm.land/huh/v2" + "github.com/github/gh-aw/pkg/console" "github.com/github/gh-aw/pkg/constants" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/setutil" "github.com/github/gh-aw/pkg/styles" "github.com/github/gh-aw/pkg/workflow" ) @@ -603,7 +605,7 @@ func detectNetworkFromRepo() []string { _, err := os.Stat(filepath.Join(cwd, m.file)) found = err == nil } - if found && !hasStringKey(seen, m.bucket) { + if found && !setutil.Contains(seen, m.bucket) { seen[m.bucket] = struct { }{} } diff --git a/pkg/cli/jsonworkflow_to_markdown.go b/pkg/cli/jsonworkflow_to_markdown.go index dc8f40af8f5..e17cc8f590c 100644 --- a/pkg/cli/jsonworkflow_to_markdown.go +++ b/pkg/cli/jsonworkflow_to_markdown.go @@ -8,9 +8,11 @@ import ( "sort" "strings" + "github.com/goccy/go-yaml" + "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/setutil" "github.com/github/gh-aw/pkg/stringutil" - "github.com/goccy/go-yaml" ) var jsonWorkflowLog = logger.New("cli:jsonworkflow_to_markdown") @@ -113,7 +115,7 @@ func (w *JSONWorkflow) UnmarshalJSON(data []byte) error { "created_at": {}, "created_by": {}, "disabled": {}, "disabled_state": {}, "updated_at": {}, } for k, v := range raw { - if !hasStringKey(knownKeys, k) { + if !setutil.Contains(knownKeys, k) { if w.Extra == nil { w.Extra = make(map[string]any) } diff --git a/pkg/cli/logs_artifact_set.go b/pkg/cli/logs_artifact_set.go index 92b59714c32..dad59c873e8 100644 --- a/pkg/cli/logs_artifact_set.go +++ b/pkg/cli/logs_artifact_set.go @@ -20,6 +20,7 @@ import ( "github.com/github/gh-aw/pkg/constants" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/setutil" ) var artifactSetLog = logger.New("cli:logs_artifact_set") @@ -193,7 +194,7 @@ func ResolveArtifactFilter(sets []string) []string { var names []string for _, s := range sets { for _, name := range artifactSetArtifacts[ArtifactSet(s)] { - if !hasStringKey(seen, name) { + if !setutil.Contains(seen, name) { seen[name] = struct { }{} names = append(names, name) diff --git a/pkg/cli/logs_report_test.go b/pkg/cli/logs_report_test.go index eefd8f5e8ad..31d6bda8ac2 100644 --- a/pkg/cli/logs_report_test.go +++ b/pkg/cli/logs_report_test.go @@ -8,6 +8,7 @@ import ( "testing" "time" + "github.com/github/gh-aw/pkg/setutil" "github.com/github/gh-aw/pkg/sliceutil" ) @@ -532,13 +533,13 @@ func TestAggregateDomainStats(t *testing.T) { } // Verify specific domains - if !hasStringKey(agg.allAllowedDomains, "example.com") { + if !setutil.Contains(agg.allAllowedDomains, "example.com") { t.Error("Expected example.com in allowed domains") } - if !hasStringKey(agg.allAllowedDomains, "api.github.com") { + if !setutil.Contains(agg.allAllowedDomains, "api.github.com") { t.Error("Expected api.github.com in allowed domains") } - if !hasStringKey(agg.allBlockedDomains, "blocked.com") { + if !setutil.Contains(agg.allBlockedDomains, "blocked.com") { t.Error("Expected blocked.com in blocked domains") } }) diff --git a/pkg/cli/logs_safe_output_chains.go b/pkg/cli/logs_safe_output_chains.go index a81a2086c57..63b68cbf000 100644 --- a/pkg/cli/logs_safe_output_chains.go +++ b/pkg/cli/logs_safe_output_chains.go @@ -8,6 +8,7 @@ import ( "github.com/github/gh-aw/pkg/constants" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/setutil" ) var safeOutputChainsLog = logger.New("cli:logs_safe_output_chains") @@ -87,10 +88,10 @@ func buildSafeOutputChainMetrics(logsPath string) SafeOutputChainMetrics { metrics.ChainedTargetCount++ metrics.ChainedFollowupActionCount += count - 1 } - if hasStringKey(delegatedTargets, key) { + if setutil.Contains(delegatedTargets, key) { metrics.DelegatedTempTargetCount++ } - if hasStringKey(closedTargets, key) { + if setutil.Contains(closedTargets, key) { metrics.ClosedTempTargetCount++ } } diff --git a/pkg/cli/mcp_inspect_mcp.go b/pkg/cli/mcp_inspect_mcp.go index 614cdc69169..d7d21b32e36 100644 --- a/pkg/cli/mcp_inspect_mcp.go +++ b/pkg/cli/mcp_inspect_mcp.go @@ -11,11 +11,13 @@ import ( "strings" "time" + "github.com/modelcontextprotocol/go-sdk/mcp" + "github.com/github/gh-aw/pkg/console" "github.com/github/gh-aw/pkg/logger" "github.com/github/gh-aw/pkg/parser" + "github.com/github/gh-aw/pkg/setutil" "github.com/github/gh-aw/pkg/stringutil" - "github.com/modelcontextprotocol/go-sdk/mcp" ) var mcpInspectServerLog = logger.New("cli:mcp_inspect_server") @@ -548,7 +550,7 @@ func displayToolAllowanceHint(info *parser.MCPServerInfo) { // Count blocked tools and collect their names var blockedTools []string for _, tool := range info.Tools { - if len(info.Config.Allowed) > 0 && !hasStringKey(allowedMap, tool.Name) { + if len(info.Config.Allowed) > 0 && !setutil.Contains(allowedMap, tool.Name) { blockedTools = append(blockedTools, tool.Name) } } diff --git a/pkg/cli/mcp_tool_table.go b/pkg/cli/mcp_tool_table.go index d3741edcaee..b3f7a22187c 100644 --- a/pkg/cli/mcp_tool_table.go +++ b/pkg/cli/mcp_tool_table.go @@ -6,6 +6,7 @@ import ( "github.com/github/gh-aw/pkg/console" "github.com/github/gh-aw/pkg/logger" "github.com/github/gh-aw/pkg/parser" + "github.com/github/gh-aw/pkg/setutil" ) var mcpToolTableLog = logger.New("cli:mcp_tool_table") @@ -71,7 +72,7 @@ func renderMCPToolTable(info *parser.MCPServerInfo, opts MCPToolTableOptions) st if len(info.Config.Allowed) == 0 || hasWildcard { // If no allowed list is specified or "*" wildcard is present, assume all tools are allowed status = "✅" - } else if hasStringKey(allowedMap, tool.Name) { + } else if setutil.Contains(allowedMap, tool.Name) { status = "✅" } @@ -90,7 +91,7 @@ func renderMCPToolTable(info *parser.MCPServerInfo, opts MCPToolTableOptions) st if opts.ShowSummary { allowedCount := 0 for _, tool := range info.Tools { - if len(info.Config.Allowed) == 0 || hasWildcard || hasStringKey(allowedMap, tool.Name) { + if len(info.Config.Allowed) == 0 || hasWildcard || setutil.Contains(allowedMap, tool.Name) { allowedCount++ } } diff --git a/pkg/cli/packages.go b/pkg/cli/packages.go index d4e13e1d638..46273f52fdc 100644 --- a/pkg/cli/packages.go +++ b/pkg/cli/packages.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/github/gh-aw/pkg/constants" + "github.com/github/gh-aw/pkg/setutil" "github.com/github/gh-aw/pkg/console" "github.com/github/gh-aw/pkg/logger" @@ -61,7 +62,7 @@ func collectLocalIncludeDependenciesRecursive(content, baseDir string, dependenc fullSourcePath := filepath.Join(baseDir, filePath) // Skip if we've already processed this file - if hasStringKey(seen, fullSourcePath) { + if setutil.Contains(seen, fullSourcePath) { continue } seen[fullSourcePath] = struct { diff --git a/pkg/cli/remove_command.go b/pkg/cli/remove_command.go index 024b38179bf..de40854bbbf 100644 --- a/pkg/cli/remove_command.go +++ b/pkg/cli/remove_command.go @@ -6,6 +6,7 @@ import ( "path/filepath" "strings" + "github.com/github/gh-aw/pkg/setutil" "github.com/github/gh-aw/pkg/stringutil" "github.com/github/gh-aw/pkg/console" @@ -244,7 +245,7 @@ func cleanupOrphanedIncludes(verbose bool) error { // Remove unused includes for _, include := range allIncludes { - if !hasStringKey(usedIncludes, include) { + if !setutil.Contains(usedIncludes, include) { includePath := filepath.Join(workflowsDir, include) if err := os.Remove(includePath); err != nil { if verbose { @@ -278,7 +279,7 @@ func previewOrphanedIncludes(filesToRemove []string, verbose bool) ([]string, er // Get the files that would remain after removal var remainingFiles []string for _, file := range allMdFiles { - if !hasStringKey(removeMap, file) { + if !setutil.Contains(removeMap, file) { remainingFiles = append(remainingFiles, file) } } @@ -324,7 +325,7 @@ func previewOrphanedIncludes(filesToRemove []string, verbose bool) ([]string, er var orphanedIncludes []string for _, include := range allIncludes { - if !hasStringKey(usedIncludes, include) { + if !setutil.Contains(usedIncludes, include) { orphanedIncludes = append(orphanedIncludes, include) } } diff --git a/pkg/cli/run_push.go b/pkg/cli/run_push.go index 383e3570669..e36cc005f1a 100644 --- a/pkg/cli/run_push.go +++ b/pkg/cli/run_push.go @@ -10,6 +10,7 @@ import ( "sort" "strings" + "github.com/github/gh-aw/pkg/setutil" "github.com/github/gh-aw/pkg/stringutil" "github.com/github/gh-aw/pkg/console" @@ -233,7 +234,7 @@ func collectImports(workflowPath string, files map[string]struct { }, visited map[string]struct { }, verbose bool) error { // Avoid processing the same file multiple times - if hasStringKey(visited, workflowPath) { + if setutil.Contains(visited, workflowPath) { runPushLog.Printf("Skipping already visited file: %s", workflowPath) return nil } @@ -488,12 +489,12 @@ func pushWorkflowFiles(ctx context.Context, workflowName string, files []string, if err == nil { // Validate the staged path validPath, validErr := fileutil.ValidateAbsolutePath(absStagedPath) - if validErr == nil && hasStringKey(ourFiles, validPath) { + if validErr == nil && setutil.Contains(ourFiles, validPath) { runPushLog.Printf("Staged file %s matches our file %s (absolute)", stagedFile, validPath) continue } } - if hasStringKey(ourFiles, stagedFile) { + if setutil.Contains(ourFiles, stagedFile) { runPushLog.Printf("Staged file %s matches our file (relative)", stagedFile) continue } diff --git a/pkg/cli/secrets.go b/pkg/cli/secrets.go index 10191f58206..05ac3927767 100644 --- a/pkg/cli/secrets.go +++ b/pkg/cli/secrets.go @@ -11,6 +11,7 @@ import ( "github.com/github/gh-aw/pkg/errorutil" "github.com/github/gh-aw/pkg/logger" "github.com/github/gh-aw/pkg/parser" + "github.com/github/gh-aw/pkg/setutil" "github.com/github/gh-aw/pkg/workflow" ) @@ -71,7 +72,7 @@ func extractSecretsFromConfig(config parser.RegistryMCPServerConfig) []SecretInf // Extract from HTTP headers for key, value := range config.Headers { secretName := workflow.ExtractSecretName(value) - if secretName != "" && !hasStringKey(seen, secretName) { + if secretName != "" && !setutil.Contains(seen, secretName) { secrets = append(secrets, SecretInfo{ Name: secretName, EnvKey: key, @@ -84,7 +85,7 @@ func extractSecretsFromConfig(config parser.RegistryMCPServerConfig) []SecretInf // Extract from environment variables for key, value := range config.Env { secretName := workflow.ExtractSecretName(value) - if secretName != "" && !hasStringKey(seen, secretName) { + if secretName != "" && !setutil.Contains(seen, secretName) { secrets = append(secrets, SecretInfo{ Name: secretName, EnvKey: key, diff --git a/pkg/cli/seenmapbool_helpers.go b/pkg/cli/seenmapbool_helpers.go deleted file mode 100644 index 1de518cab2c..00000000000 --- a/pkg/cli/seenmapbool_helpers.go +++ /dev/null @@ -1,6 +0,0 @@ -package cli - -func hasStringKey(set map[string]struct{}, key string) bool { - _, ok := set[key] - return ok -} diff --git a/pkg/cli/workflow_secrets.go b/pkg/cli/workflow_secrets.go index 1bdda8b082b..49464815842 100644 --- a/pkg/cli/workflow_secrets.go +++ b/pkg/cli/workflow_secrets.go @@ -7,6 +7,7 @@ import ( "github.com/github/gh-aw/pkg/constants" "github.com/github/gh-aw/pkg/logger" "github.com/github/gh-aw/pkg/parser" + "github.com/github/gh-aw/pkg/setutil" "github.com/github/gh-aw/pkg/workflow" ) @@ -25,7 +26,7 @@ func getSecretsRequirementsForWorkflows(workflowFiles []string) []SecretRequirem for _, workflowFile := range workflowFiles { secrets := getSecretRequirementsForWorkflow(workflowFile) for _, req := range secrets { - if !hasStringKey(seenSecrets, req.Name) { + if !setutil.Contains(seenSecrets, req.Name) { seenSecrets[req.Name] = struct { }{} allRequirements = append(allRequirements, req) @@ -35,7 +36,7 @@ func getSecretsRequirementsForWorkflows(workflowFiles []string) []SecretRequirem // Always add system secrets (deduplicated) for _, sys := range constants.SystemSecrets { - if hasStringKey(seenSecrets, sys.Name) { + if setutil.Contains(seenSecrets, sys.Name) { continue } seenSecrets[sys.Name] = struct { diff --git a/pkg/cli/workflows.go b/pkg/cli/workflows.go index 6eb8e775cc0..c3b0288a3a0 100644 --- a/pkg/cli/workflows.go +++ b/pkg/cli/workflows.go @@ -17,6 +17,7 @@ import ( "github.com/github/gh-aw/pkg/constants" "github.com/github/gh-aw/pkg/logger" "github.com/github/gh-aw/pkg/parser" + "github.com/github/gh-aw/pkg/setutil" "github.com/github/gh-aw/pkg/sliceutil" "github.com/github/gh-aw/pkg/stringutil" "github.com/github/gh-aw/pkg/workflow" @@ -148,7 +149,7 @@ func fetchGitHubWorkflows(repoOverride string, verbose bool) (map[string]*GitHub var userWorkflowCount int for name := range workflowMap { - if hasStringKey(mdWorkflowNames, name) { + if setutil.Contains(mdWorkflowNames, name) { userWorkflowCount++ } } diff --git a/pkg/cli/zizmor.go b/pkg/cli/zizmor.go index 64fb7e46924..3a217a0b8a2 100644 --- a/pkg/cli/zizmor.go +++ b/pkg/cli/zizmor.go @@ -14,6 +14,7 @@ import ( "github.com/github/gh-aw/pkg/console" "github.com/github/gh-aw/pkg/gitutil" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/setutil" ) var zizmorLog = logger.New("cli:zizmor") @@ -212,7 +213,7 @@ func parseAndDisplayZizmorOutput(stdout, stderr string, verbose bool) (int, erro }) for _, location := range finding.Locations { filePath := location.Symbolic.Key.Local.GivenPath - if filePath != "" && !hasStringKey(affectedFiles, filePath) { + if filePath != "" && !setutil.Contains(affectedFiles, filePath) { affectedFiles[filePath] = struct { }{} fileFindings[filePath] = append(fileFindings[filePath], finding) diff --git a/pkg/constants/engine_constants.go b/pkg/constants/engine_constants.go index 6ac77ec5dd4..f2dc890c4eb 100644 --- a/pkg/constants/engine_constants.go +++ b/pkg/constants/engine_constants.go @@ -1,5 +1,7 @@ package constants +import "github.com/github/gh-aw/pkg/setutil" + // EngineName represents an AI engine name identifier (copilot, claude, codex, custom). // This semantic type distinguishes engine names from arbitrary strings, // making engine selection explicit and type-safe. @@ -178,13 +180,13 @@ func GetAllEngineSecretNames() []string { // Add primary and alternative secrets from all engines for _, opt := range EngineOptions { - if opt.SecretName != "" && !hasStringKey(seen, opt.SecretName) { + if opt.SecretName != "" && !setutil.Contains(seen, opt.SecretName) { seen[opt.SecretName] = struct { }{} secrets = append(secrets, opt.SecretName) } for _, alt := range opt.AlternativeSecrets { - if alt != "" && !hasStringKey(seen, alt) { + if alt != "" && !setutil.Contains(seen, alt) { seen[alt] = struct { }{} secrets = append(secrets, alt) @@ -194,7 +196,7 @@ func GetAllEngineSecretNames() []string { // Add system-level secrets from SystemSecrets for _, s := range SystemSecrets { - if s.Name != "" && !hasStringKey(seen, s.Name) { + if s.Name != "" && !setutil.Contains(seen, s.Name) { seen[s.Name] = struct { }{} secrets = append(secrets, s.Name) diff --git a/pkg/constants/seenmapbool_helpers.go b/pkg/constants/seenmapbool_helpers.go deleted file mode 100644 index 136cf924f35..00000000000 --- a/pkg/constants/seenmapbool_helpers.go +++ /dev/null @@ -1,6 +0,0 @@ -package constants - -func hasStringKey(set map[string]struct{}, key string) bool { - _, ok := set[key] - return ok -} diff --git a/pkg/linters/ssljson/seenmapbool_helpers.go b/pkg/linters/ssljson/seenmapbool_helpers.go deleted file mode 100644 index 86b2aa969b4..00000000000 --- a/pkg/linters/ssljson/seenmapbool_helpers.go +++ /dev/null @@ -1,6 +0,0 @@ -package ssljson - -func hasStringKey(set map[string]struct{}, key string) bool { - _, ok := set[key] - return ok -} diff --git a/pkg/linters/ssljson/ssljson.go b/pkg/linters/ssljson/ssljson.go index 0df9e0959fd..6d38c44b453 100644 --- a/pkg/linters/ssljson/ssljson.go +++ b/pkg/linters/ssljson/ssljson.go @@ -14,6 +14,8 @@ import ( "path/filepath" "golang.org/x/tools/go/analysis" + + "github.com/github/gh-aw/pkg/setutil" ) const anchorPkg = "github.com/github/gh-aw/pkg/linters/ssljson" @@ -107,7 +109,7 @@ func ValidateDoc(doc SSLDoc) []string { } // Rule 1: entry_scene must reference an existing scene. - if doc.Scheduling.EntryScene != "" && !hasStringKey(sceneIDs, doc.Scheduling.EntryScene) { + if doc.Scheduling.EntryScene != "" && !setutil.Contains(sceneIDs, doc.Scheduling.EntryScene) { msgs = append(msgs, fmt.Sprintf("entry_scene %q not found in scenes", doc.Scheduling.EntryScene)) } @@ -117,12 +119,12 @@ func ValidateDoc(doc SSLDoc) []string { msgs = append(msgs, fmt.Sprintf("scene %q has invalid type %q", scene.ID, scene.Type)) } // Rule 3: entry_logic_step must reference an existing logic step. - if scene.EntryLogicStep != "" && !hasStringKey(stepIDs, scene.EntryLogicStep) { + if scene.EntryLogicStep != "" && !setutil.Contains(stepIDs, scene.EntryLogicStep) { msgs = append(msgs, fmt.Sprintf("scene %q entry_logic_step %q not found in logic_steps", scene.ID, scene.EntryLogicStep)) } // Rule 4: scene transition targets must resolve to a scene ID or terminal. for _, rule := range scene.NextSceneRules { - if !hasStringKey(sceneIDs, rule.Target) && !sceneTerminals[rule.Target] { + if !setutil.Contains(sceneIDs, rule.Target) && !sceneTerminals[rule.Target] { msgs = append(msgs, fmt.Sprintf( "scene %q transition target %q is not a scene ID or END_SUCCESS/END_FAIL", scene.ID, rule.Target, @@ -141,7 +143,7 @@ func ValidateDoc(doc SSLDoc) []string { msgs = append(msgs, fmt.Sprintf("logic step %q has invalid resource_scope %q", step.ID, step.ResourceScope)) } // Rule 7: logic-step next must be a step ID or a terminal target. - if !hasStringKey(stepIDs, step.Next) && !stepTerminals[step.Next] { + if !setutil.Contains(stepIDs, step.Next) && !stepTerminals[step.Next] { msgs = append(msgs, fmt.Sprintf( "logic step %q next %q is not a step ID or YIELD_SUCCESS/YIELD_FAIL", step.ID, step.Next, diff --git a/pkg/parser/frontmatter_hash.go b/pkg/parser/frontmatter_hash.go index 68414a4bd33..aad6d6495af 100644 --- a/pkg/parser/frontmatter_hash.go +++ b/pkg/parser/frontmatter_hash.go @@ -13,6 +13,7 @@ import ( "github.com/github/gh-aw/pkg/jsonutil" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/setutil" "github.com/github/gh-aw/pkg/typeutil" ) @@ -236,7 +237,7 @@ func extractRelevantTemplateExpressions(markdown string) []string { // Store the full expression including ${{ }} expr := match[0] // Deduplicate expressions - if !hasStringKey(seen, expr) { + if !setutil.Contains(seen, expr) { expressions = append(expressions, expr) seen[expr] = struct { }{} @@ -435,7 +436,7 @@ func processImportsTextBased(frontmatterText, baseDir string, visited map[string fullPath := filepath.Join(baseDir, importPath) // Skip if already visited (cycle detection) - if hasStringKey(visited, fullPath) { + if setutil.Contains(visited, fullPath) { frontmatterHashLog.Printf("Skipping already-visited import (cycle detection): %s", fullPath) continue } @@ -493,7 +494,7 @@ func collectImportedBodies(frontmatterText, baseDir string, visited map[string]s for _, importPath := range imports { fullPath := filepath.Join(baseDir, importPath) - if hasStringKey(visited, fullPath) { + if setutil.Contains(visited, fullPath) { continue } visited[fullPath] = struct { diff --git a/pkg/parser/import_bfs.go b/pkg/parser/import_bfs.go index 3e0ab96aebe..dafa0d3f406 100644 --- a/pkg/parser/import_bfs.go +++ b/pkg/parser/import_bfs.go @@ -13,8 +13,10 @@ import ( "path/filepath" "strings" - "github.com/github/gh-aw/pkg/constants" "github.com/goccy/go-yaml" + + "github.com/github/gh-aw/pkg/constants" + "github.com/github/gh-aw/pkg/setutil" ) // processImportsFromFrontmatterWithManifestAndSource is the internal implementation that includes source tracking. @@ -193,7 +195,7 @@ func detectRemoteImportOrigin(filePath string) *remoteImportOrigin { } func enqueueImportPath(state *importBFSState, importPath, fullPath, sectionName, baseDir string, inputs map[string]any, origin *remoteImportOrigin) error { - if !hasStringKey(state.visited, fullPath) { + if !setutil.Contains(state.visited, fullPath) { state.visited[fullPath] = struct{}{} state.visitedInputs[fullPath] = inputs state.queue = append(state.queue, importQueueItem{ @@ -464,7 +466,7 @@ func canonicalizeNestedImportPath(nestedImportPath, nestedBaseDir, baseDir strin } func enqueueNestedVisitedPath(state *importBFSState, nestedImportPath, nestedFullPath, nestedSectionName, baseDir string, inputs map[string]any, nestedRemoteOrigin *remoteImportOrigin) error { - if !hasStringKey(state.visited, nestedFullPath) { + if !setutil.Contains(state.visited, nestedFullPath) { state.visited[nestedFullPath] = struct{}{} state.visitedInputs[nestedFullPath] = inputs state.queue = append(state.queue, importQueueItem{ diff --git a/pkg/parser/import_cycle.go b/pkg/parser/import_cycle.go index f79a1622700..f3b3c419cdc 100644 --- a/pkg/parser/import_cycle.go +++ b/pkg/parser/import_cycle.go @@ -3,7 +3,11 @@ // depth-first search to find and report circular import chains. package parser -import "sort" +import ( + "sort" + + "github.com/github/gh-aw/pkg/setutil" +) // findCyclePath uses DFS to find a complete cycle path in the dependency graph. // Returns a path showing the full chain including the back-edge (e.g., ["b.md", "c.md", "d.md", "b.md"]). @@ -54,7 +58,7 @@ func dfsForCycle(current, target string, cycleNodes map[string]struct { sortedDeps := make([]string, 0, len(deps)) for _, dep := range deps { // Only follow edges within the cycle subgraph - if hasStringKey(cycleNodes, dep) { + if setutil.Contains(cycleNodes, dep) { sortedDeps = append(sortedDeps, dep) } } @@ -70,7 +74,7 @@ func dfsForCycle(current, target string, cycleNodes map[string]struct { } // Continue DFS if not visited - if !hasStringKey(visited, dep) { + if !setutil.Contains(visited, dep) { if dfsForCycle(dep, target, cycleNodes, dependencies, visited, path, false) { return true } diff --git a/pkg/parser/import_field_extractor.go b/pkg/parser/import_field_extractor.go index 9b2932130a6..f1baa9d5764 100644 --- a/pkg/parser/import_field_extractor.go +++ b/pkg/parser/import_field_extractor.go @@ -14,6 +14,7 @@ import ( "github.com/github/gh-aw/pkg/constants" "github.com/github/gh-aw/pkg/importinpututil" + "github.com/github/gh-aw/pkg/setutil" ) // importAccumulator centralizes the builder/slice/set variables used during @@ -846,7 +847,7 @@ func mergeObservabilityConfigs(configs []string) string { continue } for _, e := range extractOTLPEndpointsFromObsMap(obs) { - if !hasStringKey(seen, e.URL) { + if !setutil.Contains(seen, e.URL) { seen[e.URL] = struct { }{} allEndpoints = append(allEndpoints, e) diff --git a/pkg/parser/import_topological.go b/pkg/parser/import_topological.go index 2c4442122c5..3e42debe8fa 100644 --- a/pkg/parser/import_topological.go +++ b/pkg/parser/import_topological.go @@ -8,6 +8,8 @@ import ( "slices" "sort" "strings" + + "github.com/github/gh-aw/pkg/setutil" ) // topologicalSortImports sorts imports in topological order using Kahn's algorithm. @@ -120,7 +122,7 @@ func calculateInDegree(imports []string, dependencies map[string][]string, allIm sortedImports := sortedDependencyKeys(dependencies) for _, imp := range sortedImports { for _, dep := range dependencies[imp] { - if hasStringKey(allImportsSet, dep) { + if setutil.Contains(allImportsSet, dep) { inDegree[imp]++ } } @@ -178,7 +180,7 @@ func reduceDependentInDegrees( ) []string { for _, imp := range sortedImports { for _, dep := range dependencies[imp] { - if dep == current && hasStringKey(allImportsSet, imp) { + if dep == current && setutil.Contains(allImportsSet, imp) { inDegree[imp]-- importLog.Printf("Reduced in-degree of %s to %d (resolved dependency on %s)", imp, inDegree[imp], current) if inDegree[imp] == 0 { diff --git a/pkg/parser/include_processor.go b/pkg/parser/include_processor.go index 21a43015980..154da95d401 100644 --- a/pkg/parser/include_processor.go +++ b/pkg/parser/include_processor.go @@ -12,6 +12,7 @@ import ( "github.com/github/gh-aw/pkg/console" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/setutil" ) var includeLog = logger.New("parser:include_processor") @@ -144,7 +145,7 @@ func resolveDirectiveWithVisited( return includeDirectiveResolution{}, false, fmt.Errorf("failed to resolve required include '%s': %w", filePath, err) } - if hasStringKey(visited, fullPath) { + if setutil.Contains(visited, fullPath) { includeLog.Printf("Skipping already included file: %s", fullPath) if !extractTools { fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Already included: %s, skipping", filePath))) @@ -274,7 +275,7 @@ func collectUnexpectedIncludedFrontmatterFields(frontmatter map[string]any) []st } var unexpectedFields []string for key := range frontmatter { - if !hasStringKey(validFields, key) { + if !setutil.Contains(validFields, key) { unexpectedFields = append(unexpectedFields, key) } } diff --git a/pkg/parser/schema_errors.go b/pkg/parser/schema_errors.go index e8921843407..ca20e8a15fa 100644 --- a/pkg/parser/schema_errors.go +++ b/pkg/parser/schema_errors.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/setutil" ) var schemaErrorsLog = logger.New("parser:schema_errors") @@ -179,7 +180,7 @@ func synthesizeOneOfTypeConflictMessage(lines []string) string { }) var uniqueWantTypes []string for _, t := range wantTypes { - if !hasStringKey(seen, t) { + if !setutil.Contains(seen, t) { seen[t] = struct { }{} uniqueWantTypes = append(uniqueWantTypes, t) @@ -426,7 +427,7 @@ func uniqueClosestScopeSuggestions(unknownProps []string, scopes []string) []str }) var unique []string for _, s := range allSuggestions { - if !hasStringKey(seen, s) { + if !setutil.Contains(seen, s) { seen[s] = struct { }{} unique = append(unique, s) diff --git a/pkg/parser/schema_suggestions.go b/pkg/parser/schema_suggestions.go index ef4f3138d89..3147bd8cfd3 100644 --- a/pkg/parser/schema_suggestions.go +++ b/pkg/parser/schema_suggestions.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/setutil" "github.com/github/gh-aw/pkg/sliceutil" "github.com/github/gh-aw/pkg/stringutil" ) @@ -453,7 +454,7 @@ func collectRequiredFields(schema map[string]any) map[string]struct { func addObjectExamples(result map[string]any, properties map[string]any, requiredFields map[string]struct { }, includeRequired bool, count int) int { for propName, propSchema := range properties { - if hasStringKey(requiredFields, propName) != includeRequired || count >= maxExampleFields { + if setutil.Contains(requiredFields, propName) != includeRequired || count >= maxExampleFields { continue } propSchemaMap, ok := propSchema.(map[string]any) @@ -664,7 +665,7 @@ func findFieldLocationsInSchema(schemaDoc any, targetField, currentPath string) continue } key := loc.FieldName + "|" + loc.SchemaPath - if hasStringKey(seen, key) { + if setutil.Contains(seen, key) { continue } seen[key] = struct { @@ -690,7 +691,7 @@ func findFieldLocationsInSchema(schemaDoc any, targetField, currentPath string) continue } key := loc.FieldName + "|" + loc.SchemaPath - if hasStringKey(seenFuzzy, key) { + if setutil.Contains(seenFuzzy, key) { continue } seenFuzzy[key] = struct { @@ -760,7 +761,7 @@ func generatePathLocationSuggestion(invalidProps []string, schemaDoc any, curren }) for _, loc := range locations { display := "'" + formatSchemaPathForDisplay(loc.SchemaPath) + "'" - if !hasStringKey(seenPaths, display) { + if !setutil.Contains(seenPaths, display) { seenPaths[display] = struct { }{} pathNames = append(pathNames, display) diff --git a/pkg/parser/schema_validation.go b/pkg/parser/schema_validation.go index c90689a1328..56b04d3bfb1 100644 --- a/pkg/parser/schema_validation.go +++ b/pkg/parser/schema_validation.go @@ -8,6 +8,7 @@ import ( "github.com/github/gh-aw/pkg/constants" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/setutil" ) var schemaValidationLog = logger.New("parser:schema_validation") @@ -57,7 +58,7 @@ func validateSharedWorkflowFields(frontmatter map[string]any) error { } continue } - if hasStringKey(sharedWorkflowForbiddenFields, key) { + if setutil.Contains(sharedWorkflowForbiddenFields, key) { forbiddenFound = append(forbiddenFound, key) } } diff --git a/pkg/parser/seenmapbool_helpers.go b/pkg/parser/seenmapbool_helpers.go deleted file mode 100644 index 44d302362a2..00000000000 --- a/pkg/parser/seenmapbool_helpers.go +++ /dev/null @@ -1,6 +0,0 @@ -package parser - -func hasStringKey(set map[string]struct{}, key string) bool { - _, ok := set[key] - return ok -} diff --git a/pkg/parser/tools_merger.go b/pkg/parser/tools_merger.go index 616326f52af..c2f884b6830 100644 --- a/pkg/parser/tools_merger.go +++ b/pkg/parser/tools_merger.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/setutil" ) var toolsMergerLog = logger.New("parser:tools_merger") @@ -191,7 +192,7 @@ func mergeAllowedArrays(existing, new any) []any { if existingSlice, ok := existing.([]any); ok { for _, item := range existingSlice { if str, ok := item.(string); ok { - if !hasStringKey(seen, str) { + if !setutil.Contains(seen, str) { result = append(result, str) seen[str] = struct { }{} @@ -204,7 +205,7 @@ func mergeAllowedArrays(existing, new any) []any { if newSlice, ok := new.([]any); ok { for _, item := range newSlice { if str, ok := item.(string); ok { - if !hasStringKey(seen, str) { + if !setutil.Contains(seen, str) { result = append(result, str) seen[str] = struct { }{} diff --git a/pkg/setutil/README.md b/pkg/setutil/README.md new file mode 100644 index 00000000000..45f69426199 --- /dev/null +++ b/pkg/setutil/README.md @@ -0,0 +1,27 @@ +# setutil Package + +The `setutil` package provides utility functions for working with sets implemented as `map[K]struct{}`. + +## Overview + +All functions in this package are pure: they never modify their input. They are generic and work with any comparable key type using Go's type-parameter syntax. + +## Public API + +### Functions + +| Function | Signature | Description | +|----------|-----------|-------------| +| `Contains` | `func[K comparable](set map[K]struct{}, key K) bool` | Reports whether `key` is present in `set` | + +## Usage Examples + +```go +import "github.com/github/gh-aw/pkg/setutil" + +// Check membership in a string set +seen := map[string]struct{}{"foo": {}, "bar": {}} +if setutil.Contains(seen, "foo") { + // ... +} +``` diff --git a/pkg/setutil/setutil.go b/pkg/setutil/setutil.go new file mode 100644 index 00000000000..8d85795c5cc --- /dev/null +++ b/pkg/setutil/setutil.go @@ -0,0 +1,9 @@ +// Package setutil provides utility functions for working with sets implemented +// as map[K]struct{}. +package setutil + +// Contains reports whether key is present in a set built as map[K]struct{}. +func Contains[K comparable](set map[K]struct{}, key K) bool { + _, ok := set[key] + return ok +} diff --git a/pkg/setutil/setutil_test.go b/pkg/setutil/setutil_test.go new file mode 100644 index 00000000000..b44c28eff20 --- /dev/null +++ b/pkg/setutil/setutil_test.go @@ -0,0 +1,39 @@ +//go:build !integration + +package setutil + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +// TestSpec_PublicAPI_Contains validates the documented behavior of Contains as +// described in the setutil README.md specification. +func TestSpec_PublicAPI_Contains(t *testing.T) { + t.Run("returns true when key is present", func(t *testing.T) { + set := map[string]struct{}{"a": {}, "b": {}} + assert.True(t, Contains(set, "a"), "Contains should return true for a present key") + }) + + t.Run("returns false when key is absent", func(t *testing.T) { + set := map[string]struct{}{"a": {}, "b": {}} + assert.False(t, Contains(set, "c"), "Contains should return false for an absent key") + }) + + t.Run("returns false for nil set", func(t *testing.T) { + var set map[string]struct{} + assert.False(t, Contains(set, "a"), "Contains should return false for a nil set") + }) + + t.Run("returns false for empty set", func(t *testing.T) { + set := map[string]struct{}{} + assert.False(t, Contains(set, "a"), "Contains should return false for an empty set") + }) + + t.Run("works with non-string comparable key types", func(t *testing.T) { + set := map[int]struct{}{1: {}, 2: {}} + assert.True(t, Contains(set, 1), "Contains should work with int keys") + assert.False(t, Contains(set, 3), "Contains should return false for absent int key") + }) +} diff --git a/pkg/workflow/action_cache.go b/pkg/workflow/action_cache.go index b94796ddd8d..619461b074c 100644 --- a/pkg/workflow/action_cache.go +++ b/pkg/workflow/action_cache.go @@ -11,6 +11,7 @@ import ( "time" "github.com/github/gh-aw/pkg/constants" + "github.com/github/gh-aw/pkg/setutil" "github.com/github/gh-aw/pkg/logger" ) @@ -160,7 +161,7 @@ func (c *ActionCache) PruneStaleContainerPins(knownImages map[string]struct { } pruned := 0 for image := range c.ContainerPins { - if !hasStringKey(knownImages, image) { + if !setutil.Contains(knownImages, image) { delete(c.ContainerPins, image) c.dirty = true pruned++ diff --git a/pkg/workflow/agentic_engine.go b/pkg/workflow/agentic_engine.go index dcc54ce19b8..e6c03a31260 100644 --- a/pkg/workflow/agentic_engine.go +++ b/pkg/workflow/agentic_engine.go @@ -9,6 +9,7 @@ import ( "github.com/github/gh-aw/pkg/constants" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/setutil" ) var agenticEngineLog = logger.New("workflow:agentic_engine") @@ -562,7 +563,7 @@ func (r *EngineRegistry) computeAllAgentManifestFolders() []string { } for _, prefix := range provider.GetAgentManifestPathPrefixes() { folder := strings.TrimSuffix(prefix, "/") - if folder != "" && !hasStringKey(seen, folder) { + if folder != "" && !setutil.Contains(seen, folder) { seen[folder] = struct { }{} result = append(result, folder) @@ -571,7 +572,7 @@ func (r *EngineRegistry) computeAllAgentManifestFolders() []string { } // Always include .agents — the gh-aw platform agent directory. // It is not owned by any specific engine but must always be snapshotted. - if !hasStringKey(seen, ".agents") { + if !setutil.Contains(seen, ".agents") { result = append(result, ".agents") } sort.Strings(result) @@ -604,7 +605,7 @@ func (r *EngineRegistry) computeAllAgentManifestFiles() []string { continue } for _, file := range provider.GetAgentManifestFiles() { - if !hasStringKey(seen, file) { + if !setutil.Contains(seen, file) { seen[file] = struct { }{} result = append(result, file) diff --git a/pkg/workflow/awf_config.go b/pkg/workflow/awf_config.go index 4b466bf700c..473b63a62d4 100644 --- a/pkg/workflow/awf_config.go +++ b/pkg/workflow/awf_config.go @@ -65,10 +65,12 @@ import ( "strings" "sync" + "github.com/santhosh-tekuri/jsonschema/v6" + "github.com/github/gh-aw/pkg/constants" "github.com/github/gh-aw/pkg/jsonutil" "github.com/github/gh-aw/pkg/logger" - "github.com/santhosh-tekuri/jsonschema/v6" + "github.com/github/gh-aw/pkg/setutil" ) //go:embed schemas/awf-config.schema.json @@ -496,7 +498,7 @@ func splitDomainList(domains string) []string { }) for d := range strings.SplitSeq(domains, ",") { d = strings.TrimSpace(d) - if d != "" && !hasStringKey(seen, d) { + if d != "" && !setutil.Contains(seen, d) { seen[d] = struct { }{} result = append(result, d) diff --git a/pkg/workflow/awf_helpers.go b/pkg/workflow/awf_helpers.go index d8670d54709..a0276116f10 100644 --- a/pkg/workflow/awf_helpers.go +++ b/pkg/workflow/awf_helpers.go @@ -31,6 +31,7 @@ import ( "github.com/github/gh-aw/pkg/constants" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/setutil" "github.com/github/gh-aw/pkg/workflow/compilerenv" ) @@ -786,7 +787,7 @@ func ComputeAWFExcludeEnvVarNames(workflowData *WorkflowData, coreSecretVarNames var names []string addUnique := func(name string) { - if !hasStringKey(seen, name) { + if !setutil.Contains(seen, name) { seen[name] = struct { }{} names = append(names, name) diff --git a/pkg/workflow/bot_aliases.go b/pkg/workflow/bot_aliases.go index 5bf37f3a833..aabea0aaabe 100644 --- a/pkg/workflow/bot_aliases.go +++ b/pkg/workflow/bot_aliases.go @@ -3,6 +3,7 @@ package workflow import ( "github.com/github/gh-aw/pkg/constants" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/setutil" "github.com/github/gh-aw/pkg/sliceutil" ) @@ -33,7 +34,7 @@ func expandBotNames(bots []string) []string { } needsExpansion := false for _, b := range bots { - if hasStringKey(copilotBotSet, b) { + if setutil.Contains(copilotBotSet, b) { needsExpansion = true break } @@ -45,7 +46,7 @@ func expandBotNames(bots []string) []string { // identifier that expands to len(constants.CopilotBotNames) entries. expanded := make([]string, 0, len(bots)*len(constants.CopilotBotNames)) for _, b := range bots { - if hasStringKey(copilotBotSet, b) { + if setutil.Contains(copilotBotSet, b) { expanded = append(expanded, constants.CopilotBotNames...) } else { expanded = append(expanded, b) diff --git a/pkg/workflow/checkout_manager.go b/pkg/workflow/checkout_manager.go index 678d5f5254e..22cfca330e4 100644 --- a/pkg/workflow/checkout_manager.go +++ b/pkg/workflow/checkout_manager.go @@ -4,6 +4,7 @@ import ( "strings" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/setutil" ) var checkoutManagerLog = logger.New("workflow:checkout_manager") @@ -453,7 +454,7 @@ func mergeSparsePatterns(existing []string, newPatterns string) []string { for _, p := range existing { p = strings.TrimSpace(p) - if p != "" && !hasStringKey(seen, p) { + if p != "" && !setutil.Contains(seen, p) { seen[p] = struct { }{} result = append(result, p) @@ -462,7 +463,7 @@ func mergeSparsePatterns(existing []string, newPatterns string) []string { for p := range strings.SplitSeq(newPatterns, "\n") { p = strings.TrimSpace(p) - if p != "" && !hasStringKey(seen, p) { + if p != "" && !setutil.Contains(seen, p) { seen[p] = struct { }{} result = append(result, p) @@ -479,7 +480,7 @@ func mergeFetchRefs(existing []string, newRefs []string) []string { result := make([]string, 0) for _, r := range existing { r = strings.TrimSpace(r) - if r != "" && !hasStringKey(seen, r) { + if r != "" && !setutil.Contains(seen, r) { seen[r] = struct { }{} result = append(result, r) @@ -487,7 +488,7 @@ func mergeFetchRefs(existing []string, newRefs []string) []string { } for _, r := range newRefs { r = strings.TrimSpace(r) - if r != "" && !hasStringKey(seen, r) { + if r != "" && !setutil.Contains(seen, r) { seen[r] = struct { }{} result = append(result, r) diff --git a/pkg/workflow/command.go b/pkg/workflow/command.go index 1898e99a3b7..1a777a9dcf7 100644 --- a/pkg/workflow/command.go +++ b/pkg/workflow/command.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/setutil" ) var commandLog = logger.New("workflow:command") @@ -194,7 +195,7 @@ func buildEventAwareCommandCondition(commandNames []string, commandEvents []stri }) for _, eventName := range eventNames { actualName := GetActualGitHubEventName(eventName) - if !hasStringKey(actualEventNames, actualName) { + if !setutil.Contains(actualEventNames, actualName) { actualEventNames[actualName] = struct { }{} commentEventTerms = append(commentEventTerms, BuildEventTypeEquals(actualName)) diff --git a/pkg/workflow/compiler_activation_job.go b/pkg/workflow/compiler_activation_job.go index 635adefcfc0..6a4cf595478 100644 --- a/pkg/workflow/compiler_activation_job.go +++ b/pkg/workflow/compiler_activation_job.go @@ -4,9 +4,11 @@ import ( "fmt" "strings" + "github.com/goccy/go-yaml" + "github.com/github/gh-aw/pkg/constants" "github.com/github/gh-aw/pkg/logger" - "github.com/goccy/go-yaml" + "github.com/github/gh-aw/pkg/setutil" ) var compilerActivationJobLog = logger.New("workflow:compiler_activation_job") @@ -136,12 +138,12 @@ func addActivationInteractionPermissionsMap( return } - hasIssuesEvent := hasStringKey(eventSet, "issues") - hasIssueCommentEvent := hasStringKey(eventSet, "issue_comment") - hasPullRequestEvent := hasStringKey(eventSet, "pull_request") - hasPullRequestReviewCommentEvent := hasStringKey(eventSet, "pull_request_review_comment") - hasDiscussionEvent := hasStringKey(eventSet, "discussion") - hasDiscussionCommentEvent := hasStringKey(eventSet, "discussion_comment") + hasIssuesEvent := setutil.Contains(eventSet, "issues") + hasIssueCommentEvent := setutil.Contains(eventSet, "issue_comment") + hasPullRequestEvent := setutil.Contains(eventSet, "pull_request") + hasPullRequestReviewCommentEvent := setutil.Contains(eventSet, "pull_request_review_comment") + hasDiscussionEvent := setutil.Contains(eventSet, "discussion") + hasDiscussionCommentEvent := setutil.Contains(eventSet, "discussion_comment") if options.hasReaction { // Reactions on issues, issue comments, and pull requests use issues endpoints. @@ -312,7 +314,7 @@ func buildCentralizedCommandOnSection(commandEvents []string) string { }) for _, mapping := range GetAllCommentEvents() { name := GetActualGitHubEventName(mapping.EventName) - if hasStringKey(eventSet, name) && !hasStringKey(seen, name) { + if setutil.Contains(eventSet, name) && !setutil.Contains(seen, name) { seen[name] = struct { }{} b.WriteString(" " + name + ":\n types: [created]\n") @@ -419,7 +421,7 @@ func (c *Compiler) generateCheckoutGitHubFolderForActivation(data *WorkflowData) }{".github": {}, ".agents": {}} registry := GetGlobalEngineRegistry() for _, folder := range registry.GetAllAgentManifestFolders() { - if !hasStringKey(defaultSparseCheckoutDirs, folder) { + if !setutil.Contains(defaultSparseCheckoutDirs, folder) { extraPaths = append(extraPaths, folder) } } diff --git a/pkg/workflow/compiler_jobs.go b/pkg/workflow/compiler_jobs.go index 8bad50bbae0..9c8a53c3ec4 100644 --- a/pkg/workflow/compiler_jobs.go +++ b/pkg/workflow/compiler_jobs.go @@ -7,12 +7,14 @@ import ( "sort" "strings" + "github.com/github/gh-aw/pkg/setutil" "github.com/github/gh-aw/pkg/stringutil" + "github.com/goccy/go-yaml" + "github.com/github/gh-aw/pkg/constants" "github.com/github/gh-aw/pkg/logger" "github.com/github/gh-aw/pkg/sliceutil" - "github.com/goccy/go-yaml" ) var compilerJobsLog = logger.New("workflow:compiler_jobs") @@ -494,7 +496,7 @@ func (c *Compiler) ensureConclusionIsLastJob() error { sort.Strings(jobNames) for _, jobName := range jobNames { - if hasStringKey(exclude, jobName) || hasStringKey(currentNeeds, jobName) { + if setutil.Contains(exclude, jobName) || setutil.Contains(currentNeeds, jobName) { continue } conclusionJob.Needs = append(conclusionJob.Needs, jobName) @@ -644,8 +646,8 @@ func (c *Compiler) applyAutomaticActivationDependency( // This ensures custom jobs wait for workflow validation before executing. // Exception: jobs whose outputs are referenced in the markdown body run before activation // (so the activation job can include their outputs in the prompt). - isReferencedInMarkdown := hasStringKey(promptReferencedJobs, jobName) - isOnNeedsDependency := hasStringKey(onNeedsJobs, jobName) + isReferencedInMarkdown := setutil.Contains(promptReferencedJobs, jobName) + isOnNeedsDependency := setutil.Contains(onNeedsJobs, jobName) if !hasExplicitNeeds && activationJobCreated && !isReferencedInMarkdown && !isOnNeedsDependency { job.Needs = append(job.Needs, string(constants.ActivationJobName)) diff --git a/pkg/workflow/compiler_main_job.go b/pkg/workflow/compiler_main_job.go index 8e2f450910d..f5b83baa166 100644 --- a/pkg/workflow/compiler_main_job.go +++ b/pkg/workflow/compiler_main_job.go @@ -12,6 +12,7 @@ import ( "github.com/github/gh-aw/pkg/console" "github.com/github/gh-aw/pkg/constants" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/setutil" ) var compilerMainJobLog = logger.New("workflow:compiler_main_job") @@ -190,7 +191,7 @@ func (c *Compiler) buildMainJob(data *WorkflowData, activationJobCreated bool) ( if slices.Contains(depends, builtinJobName) { continue } - if !hasStringKey(builtinsWarned, builtinJobName) && strings.Contains(engineEnvContent, fmt.Sprintf("needs.%s.", builtinJobName)) { + if !setutil.Contains(builtinsWarned, builtinJobName) && strings.Contains(engineEnvContent, fmt.Sprintf("needs.%s.", builtinJobName)) { builtinsWarned[builtinJobName] = struct { }{} warningMsg := fmt.Sprintf( diff --git a/pkg/workflow/compiler_orchestrator_tools.go b/pkg/workflow/compiler_orchestrator_tools.go index 1a8e2316006..7e0a7a74d95 100644 --- a/pkg/workflow/compiler_orchestrator_tools.go +++ b/pkg/workflow/compiler_orchestrator_tools.go @@ -9,6 +9,7 @@ import ( "github.com/github/gh-aw/pkg/console" "github.com/github/gh-aw/pkg/logger" "github.com/github/gh-aw/pkg/parser" + "github.com/github/gh-aw/pkg/setutil" ) var orchestratorToolsLog = logger.New("workflow:compiler_orchestrator_tools") @@ -483,7 +484,7 @@ func (c *Compiler) hasContentContext(frontmatter map[string]any) bool { } for eventName := range onMap { - if hasStringKey(contentEventKeys, eventName) { + if setutil.Contains(contentEventKeys, eventName) { orchestratorToolsLog.Printf("Detected content context: workflow triggered by %s", eventName) return true } diff --git a/pkg/workflow/compiler_orchestrator_workflow.go b/pkg/workflow/compiler_orchestrator_workflow.go index 7f52317746b..dab055d9d34 100644 --- a/pkg/workflow/compiler_orchestrator_workflow.go +++ b/pkg/workflow/compiler_orchestrator_workflow.go @@ -6,6 +6,7 @@ import ( "github.com/github/gh-aw/pkg/logger" "github.com/github/gh-aw/pkg/parser" + "github.com/github/gh-aw/pkg/setutil" ) var orchestratorWorkflowLog = logger.New("workflow:compiler_orchestrator_workflow") @@ -291,7 +292,7 @@ func mergeRawOTLPEndpoints(mainObs map[string]any, importedObs map[string]any) ( seen := make(map[string]struct { }) for _, ep := range extractRawOTLPEndpointMaps(mainObs) { - if url, _ := ep["url"].(string); url != "" && !hasStringKey(seen, url) { + if url, _ := ep["url"].(string); url != "" && !setutil.Contains(seen, url) { seen[url] = struct { }{} mergedEndpoints = append(mergedEndpoints, ep) @@ -299,7 +300,7 @@ func mergeRawOTLPEndpoints(mainObs map[string]any, importedObs map[string]any) ( } mainCount = len(mergedEndpoints) for _, ep := range extractRawOTLPEndpointMaps(importedObs) { - if url, _ := ep["url"].(string); url != "" && !hasStringKey(seen, url) { + if url, _ := ep["url"].(string); url != "" && !setutil.Contains(seen, url) { seen[url] = struct { }{} mergedEndpoints = append(mergedEndpoints, ep) diff --git a/pkg/workflow/compiler_pre_activation_job.go b/pkg/workflow/compiler_pre_activation_job.go index 2646c13ca33..79b5b856d3b 100644 --- a/pkg/workflow/compiler_pre_activation_job.go +++ b/pkg/workflow/compiler_pre_activation_job.go @@ -9,6 +9,7 @@ import ( "github.com/github/gh-aw/pkg/constants" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/setutil" "github.com/github/gh-aw/pkg/sliceutil" "github.com/github/gh-aw/pkg/stringutil" ) @@ -604,7 +605,7 @@ func (c *Compiler) extractPreActivationCustomFields(jobs map[string]any) ([]stri jobName, ) } - if !hasStringKey(allowedFields, field) { + if !setutil.Contains(allowedFields, field) { return nil, nil, fmt.Errorf("jobs.%s: unsupported field '%s' - only 'steps', 'outputs', and 'pre-steps' are allowed", jobName, field) } } diff --git a/pkg/workflow/compiler_safe_outputs_job.go b/pkg/workflow/compiler_safe_outputs_job.go index 89afb024111..78ce61a5d16 100644 --- a/pkg/workflow/compiler_safe_outputs_job.go +++ b/pkg/workflow/compiler_safe_outputs_job.go @@ -8,6 +8,7 @@ import ( "github.com/github/gh-aw/pkg/constants" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/setutil" "github.com/github/gh-aw/pkg/stringutil" ) @@ -587,7 +588,7 @@ func (c *Compiler) buildSafeOutputsJobFromParts( } if data.SafeOutputs != nil { for _, need := range data.SafeOutputs.Needs { - if hasStringKey(seenNeeds, need) { + if setutil.Contains(seenNeeds, need) { continue } needs = append(needs, need) diff --git a/pkg/workflow/copilot_engine_tools.go b/pkg/workflow/copilot_engine_tools.go index ce1e698bf58..44a8cf78764 100644 --- a/pkg/workflow/copilot_engine_tools.go +++ b/pkg/workflow/copilot_engine_tools.go @@ -30,6 +30,7 @@ import ( "github.com/github/gh-aw/pkg/console" "github.com/github/gh-aw/pkg/constants" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/setutil" ) var copilotEngineToolsLog = logger.New("workflow:copilot_engine_tools") @@ -179,7 +180,7 @@ func (e *CopilotEngine) computeCopilotToolArguments(tools map[string]any, safeOu // Handle MCP server tools for toolName, toolConfig := range tools { // Skip built-in tools we've already handled - if hasStringKey(builtInTools, toolName) { + if setutil.Contains(builtInTools, toolName) { continue } diff --git a/pkg/workflow/dependabot.go b/pkg/workflow/dependabot.go index 98bdc117375..4b89a261bc3 100644 --- a/pkg/workflow/dependabot.go +++ b/pkg/workflow/dependabot.go @@ -16,10 +16,12 @@ import ( "strings" "github.com/github/gh-aw/pkg/constants" + "github.com/github/gh-aw/pkg/setutil" + + "github.com/goccy/go-yaml" "github.com/github/gh-aw/pkg/console" "github.com/github/gh-aw/pkg/logger" - "github.com/goccy/go-yaml" ) var dependabotLog = logger.New("workflow:dependabot") @@ -504,7 +506,7 @@ func (c *Compiler) ReconcileManagedDependabotIgnores(path string) error { if dependencyName == pattern { managedPresent[pattern] = struct { }{} - if !hasStringKey(managedPatternsWithComment, pattern) { + if !setutil.Contains(managedPatternsWithComment, pattern) { changed = true } } @@ -512,7 +514,7 @@ func (c *Compiler) ReconcileManagedDependabotIgnores(path string) error { } for _, pattern := range managedPatterns { - if hasStringKey(managedPresent, pattern) { + if setutil.Contains(managedPresent, pattern) { continue } ignoreEntries = append(ignoreEntries, map[string]any{"dependency-name": pattern}) diff --git a/pkg/workflow/dependabot_test.go b/pkg/workflow/dependabot_test.go index e0bf0ab0ac5..0c5af851a9a 100644 --- a/pkg/workflow/dependabot_test.go +++ b/pkg/workflow/dependabot_test.go @@ -9,6 +9,7 @@ import ( "strings" "testing" + "github.com/github/gh-aw/pkg/setutil" "github.com/github/gh-aw/pkg/testutil" "github.com/goccy/go-yaml" @@ -1026,7 +1027,7 @@ func TestGenerateDependabotConfig_MultipleEcosystems(t *testing.T) { } for ecosystem := range ecosystems { - if !hasStringKey(ecosystemsFound, ecosystem) { + if !setutil.Contains(ecosystemsFound, ecosystem) { t.Errorf("ecosystem %q not found in dependabot.yml", ecosystem) } } diff --git a/pkg/workflow/docker.go b/pkg/workflow/docker.go index a1954d6d3bd..888ecc817db 100644 --- a/pkg/workflow/docker.go +++ b/pkg/workflow/docker.go @@ -7,6 +7,7 @@ import ( "github.com/github/gh-aw/pkg/constants" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/setutil" ) var dockerLog = logger.New("workflow:docker") @@ -29,7 +30,7 @@ func collectDockerImages(tools map[string]any, workflowData *WorkflowData, actio if githubType == GitHubMCPModeLocal { githubDockerImageVersion := getGitHubDockerImageVersion(githubTool) image := "ghcr.io/github/github-mcp-server:" + githubDockerImageVersion - if !hasStringKey(imageSet, image) { + if !setutil.Contains(imageSet, image) { images = append(images, image) imageSet[image] = struct { }{} @@ -43,7 +44,7 @@ func collectDockerImages(tools map[string]any, workflowData *WorkflowData, actio if _, hasPlaywright := tools["playwright"]; hasPlaywright { if !isPlaywrightCLIMode(tools) { image := "mcr.microsoft.com/playwright/mcp" - if !hasStringKey(imageSet, image) { + if !setutil.Contains(imageSet, image) { images = append(images, image) imageSet[image] = struct { }{} @@ -56,7 +57,7 @@ func collectDockerImages(tools map[string]any, workflowData *WorkflowData, actio // the default predownload set and lock-file manifest whenever enabled. if workflowData != nil && HasSafeOutputsEnabled(workflowData.SafeOutputs) { image := constants.DefaultGhAwNodeImage - if !hasStringKey(imageSet, image) { + if !setutil.Contains(imageSet, image) { images = append(images, image) imageSet[image] = struct { }{} @@ -71,7 +72,7 @@ func collectDockerImages(tools map[string]any, workflowData *WorkflowData, actio if !actionMode.IsDev() { // Release/script mode: Use alpine:latest (needs to be pulled) image := constants.DefaultAlpineImage - if !hasStringKey(imageSet, image) { + if !setutil.Contains(imageSet, image) { images = append(images, image) imageSet[image] = struct { }{} @@ -90,7 +91,7 @@ func collectDockerImages(tools map[string]any, workflowData *WorkflowData, actio // Add squid (proxy) container squidImage := constants.DefaultFirewallRegistry + "/squid:" + awfImageTag - if !hasStringKey(imageSet, squidImage) { + if !setutil.Contains(imageSet, squidImage) { images = append(images, squidImage) imageSet[squidImage] = struct { }{} @@ -99,7 +100,7 @@ func collectDockerImages(tools map[string]any, workflowData *WorkflowData, actio // Add default agent container agentImage := constants.DefaultFirewallRegistry + "/agent:" + awfImageTag - if !hasStringKey(imageSet, agentImage) { + if !setutil.Contains(imageSet, agentImage) { images = append(images, agentImage) imageSet[agentImage] = struct { }{} @@ -111,7 +112,7 @@ func collectDockerImages(tools map[string]any, workflowData *WorkflowData, actio // Each engine uses its own dedicated port for communication if workflowData != nil && workflowData.AI != "" { apiProxyImage := constants.DefaultFirewallRegistry + "/api-proxy:" + awfImageTag - if !hasStringKey(imageSet, apiProxyImage) { + if !setutil.Contains(imageSet, apiProxyImage) { images = append(images, apiProxyImage) imageSet[apiProxyImage] = struct { }{} @@ -123,7 +124,7 @@ func collectDockerImages(tools map[string]any, workflowData *WorkflowData, actio // Without this, --skip-pull causes AWF to fail because the cli-proxy image was never pulled. if isCliProxyNeeded(workflowData) { cliProxyImage := constants.DefaultFirewallRegistry + "/cli-proxy:" + awfImageTag - if !hasStringKey(imageSet, cliProxyImage) { + if !setutil.Contains(imageSet, cliProxyImage) { images = append(images, cliProxyImage) imageSet[cliProxyImage] = struct { }{} @@ -148,7 +149,7 @@ func collectDockerImages(tools map[string]any, workflowData *WorkflowData, actio // Use default version if not specified (consistent with mcp_servers.go) image += ":" + string(constants.DefaultMCPGatewayVersion) } - if !hasStringKey(imageSet, image) { + if !setutil.Contains(imageSet, image) { images = append(images, image) imageSet[image] = struct { }{} @@ -169,7 +170,7 @@ func collectDockerImages(tools map[string]any, workflowData *WorkflowData, actio // Check for direct container field if mcpConf.Container != "" { image := mcpConf.Container - if !hasStringKey(imageSet, image) { + if !setutil.Contains(imageSet, image) { images = append(images, image) imageSet[image] = struct { }{} @@ -180,7 +181,7 @@ func collectDockerImages(tools map[string]any, workflowData *WorkflowData, actio // The container image is the last arg image := mcpConf.Args[len(mcpConf.Args)-1] // Skip if it's a docker flag (starts with -) - if !strings.HasPrefix(image, "-") && !hasStringKey(imageSet, image) { + if !strings.HasPrefix(image, "-") && !setutil.Contains(imageSet, image) { images = append(images, image) imageSet[image] = struct { }{} @@ -248,7 +249,7 @@ func mergeDockerImages(existing, newImages []string) []string { } result := existing for _, img := range newImages { - if !hasStringKey(seen, img) { + if !setutil.Contains(seen, img) { result = append(result, img) seen[img] = struct { }{} @@ -268,7 +269,7 @@ func mergeDockerImagePins(existing, newPins []GHAWManifestContainer) []GHAWManif } result := existing for _, p := range newPins { - if p.Image != "" && !hasStringKey(seen, p.Image) { + if p.Image != "" && !setutil.Contains(seen, p.Image) { result = append(result, p) seen[p.Image] = struct { }{} diff --git a/pkg/workflow/engine_auth_test.go b/pkg/workflow/engine_auth_test.go index 7c68eb06e3b..3ddd4c05994 100644 --- a/pkg/workflow/engine_auth_test.go +++ b/pkg/workflow/engine_auth_test.go @@ -15,6 +15,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/github/gh-aw/pkg/setutil" ) // TestAuthDefinition_RequiredSecretNames verifies that RequiredSecretNames returns the @@ -386,8 +388,8 @@ func TestStrictModeGetEngineBaseEnvVarKeys_IncludesAuthSecrets(t *testing.T) { compiler.registerInlineEngineDefinition(config) keys := compiler.getEngineBaseEnvVarKeys("codex") - assert.True(t, hasStringKey(keys, "MY_CLIENT_ID"), "client ID secret should be in allowed env-var keys") - assert.True(t, hasStringKey(keys, "MY_CLIENT_SECRET"), "client secret should be in allowed env-var keys") + assert.True(t, setutil.Contains(keys, "MY_CLIENT_ID"), "client ID secret should be in allowed env-var keys") + assert.True(t, setutil.Contains(keys, "MY_CLIENT_SECRET"), "client secret should be in allowed env-var keys") } // TestBuiltInEngineAuthUnchanged is a regression test verifying that the built-in engines diff --git a/pkg/workflow/engine_helpers.go b/pkg/workflow/engine_helpers.go index eba073fafce..b0915e24fbb 100644 --- a/pkg/workflow/engine_helpers.go +++ b/pkg/workflow/engine_helpers.go @@ -36,6 +36,7 @@ import ( "strings" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/setutil" ) var engineHelpersLog = logger.New("workflow:engine_helpers") @@ -326,7 +327,7 @@ func FilterEnvForSecrets(env map[string]string, allowedNamesAndKeys []string) ma // Format: ${{ secrets.SECRET_NAME }} or ${{ secrets.SECRET_NAME || ... }} secretName := ExtractSecretName(value) // Allow the secret if the secret name OR the env var key is in the allowed set. - if secretName != "" && !hasStringKey(allowedSet, secretName) && !hasStringKey(allowedSet, key) { + if secretName != "" && !setutil.Contains(allowedSet, secretName) && !setutil.Contains(allowedSet, key) { engineHelpersLog.Printf("Removing unauthorized secret from env: %s (secret: %s)", key, secretName) secretsRemoved++ continue diff --git a/pkg/workflow/expression_extraction.go b/pkg/workflow/expression_extraction.go index 1e19b7e3f40..21e1d7a6df4 100644 --- a/pkg/workflow/expression_extraction.go +++ b/pkg/workflow/expression_extraction.go @@ -12,6 +12,7 @@ import ( "github.com/github/gh-aw/pkg/console" "github.com/github/gh-aw/pkg/importinpututil" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/setutil" ) var expressionExtractionLog = logger.New("workflow:expression_extraction") @@ -371,7 +372,7 @@ func extractTerminalSubExpressions(content string) []string { var result []string _ = VisitExpressionTree(tree, func(node *ExpressionNode) error { expr := strings.TrimSpace(node.Expression) - if isQualifyingSubExpression(expr) && !hasStringKey(seen, expr) { + if isQualifyingSubExpression(expr) && !setutil.Contains(seen, expr) { seen[expr] = struct { }{} result = append(result, expr) diff --git a/pkg/workflow/frontmatter_extraction_yaml.go b/pkg/workflow/frontmatter_extraction_yaml.go index 1d4c6f3043a..85878db3605 100644 --- a/pkg/workflow/frontmatter_extraction_yaml.go +++ b/pkg/workflow/frontmatter_extraction_yaml.go @@ -6,10 +6,12 @@ import ( "slices" "strings" + "github.com/goccy/go-yaml" + "github.com/github/gh-aw/pkg/console" "github.com/github/gh-aw/pkg/logger" "github.com/github/gh-aw/pkg/parser" - "github.com/goccy/go-yaml" + "github.com/github/gh-aw/pkg/setutil" ) var frontmatterLog = logger.New("workflow:frontmatter_extraction") @@ -674,7 +676,7 @@ func (c *Compiler) commentOutProcessedFieldsInOnSection(yamlStr string, frontmat commentReason = " # Lock-for-agent processed as issue locking in activation job" } else if (inPullRequest || inIssues || inDiscussion || inIssueComment) && strings.HasPrefix(trimmedLine, "names:") { // Only comment out names if NOT using native label filtering for this section - if !hasStringKey(nativeLabelFilterSections, currentSection) { + if !setutil.Contains(nativeLabelFilterSections, currentSection) { shouldComment = true commentReason = " # Label filtering applied via job conditions" } @@ -682,7 +684,7 @@ func (c *Compiler) commentOutProcessedFieldsInOnSection(yamlStr string, frontmat // Check if we're in a names array (after "names:" line) // Look back to see if the previous uncommented line was "names:" // Only do this if NOT using native label filtering for this section - if !hasStringKey(nativeLabelFilterSections, currentSection) { + if !setutil.Contains(nativeLabelFilterSections, currentSection) { if len(result) > 0 { for i := range slices.Backward(result) { prevLine := result[i] diff --git a/pkg/workflow/git_configuration_steps.go b/pkg/workflow/git_configuration_steps.go index 8a093b4ff65..5dda3f32533 100644 --- a/pkg/workflow/git_configuration_steps.go +++ b/pkg/workflow/git_configuration_steps.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/setutil" ) var gitConfigStepsLog = logger.New("workflow:git_configuration_steps") @@ -82,7 +83,7 @@ func (c *Compiler) generateCredentialsCleanerStep(envVars map[string]struct{}) [ lines = append(lines, " env:\n") // Emit env vars in a stable, deterministic order (knownCredentialLeakingActions order) for _, known := range knownCredentialLeakingActions { - if hasStringKey(envVars, known.envVar) { + if setutil.Contains(envVars, known.envVar) { lines = append(lines, fmt.Sprintf(" %s: \"true\"\n", known.envVar)) } } diff --git a/pkg/workflow/github_toolsets.go b/pkg/workflow/github_toolsets.go index 8d6ab9b7ca4..f60ec7fd612 100644 --- a/pkg/workflow/github_toolsets.go +++ b/pkg/workflow/github_toolsets.go @@ -4,6 +4,7 @@ import ( "strings" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/setutil" ) var toolsetsLog = logger.New("workflow:github_toolsets") @@ -48,7 +49,7 @@ func ParseGitHubToolsets(toolsetsStr string) []string { // Add default toolsets toolsetsLog.Printf("Expanding 'default' to %d toolsets", len(DefaultGitHubToolsets)) for _, dt := range DefaultGitHubToolsets { - if !hasStringKey(seenToolsets, dt) { + if !setutil.Contains(seenToolsets, dt) { expanded = append(expanded, dt) seenToolsets[dt] = struct { }{} @@ -58,7 +59,7 @@ func ParseGitHubToolsets(toolsetsStr string) []string { // Add action-friendly toolsets (excludes "users" which GitHub Actions tokens don't support) toolsetsLog.Printf("Expanding 'action-friendly' to %d toolsets", len(ActionFriendlyGitHubToolsets)) for _, dt := range ActionFriendlyGitHubToolsets { - if !hasStringKey(seenToolsets, dt) { + if !setutil.Contains(seenToolsets, dt) { expanded = append(expanded, dt) seenToolsets[dt] = struct { }{} @@ -75,10 +76,10 @@ func ParseGitHubToolsets(toolsetsStr string) []string { }{} } for t := range toolsetPermissionsMap { - if hasStringKey(excludedMap, t) { + if setutil.Contains(excludedMap, t) { continue } - if !hasStringKey(seenToolsets, t) { + if !setutil.Contains(seenToolsets, t) { expanded = append(expanded, t) seenToolsets[t] = struct { }{} @@ -86,7 +87,7 @@ func ParseGitHubToolsets(toolsetsStr string) []string { } default: // Add individual toolset - if !hasStringKey(seenToolsets, toolset) { + if !setutil.Contains(seenToolsets, toolset) { expanded = append(expanded, toolset) seenToolsets[toolset] = struct { }{} diff --git a/pkg/workflow/imports.go b/pkg/workflow/imports.go index 533e9b7615e..fd586b83b22 100644 --- a/pkg/workflow/imports.go +++ b/pkg/workflow/imports.go @@ -10,6 +10,7 @@ import ( "github.com/github/gh-aw/pkg/logger" "github.com/github/gh-aw/pkg/parser" + "github.com/github/gh-aw/pkg/setutil" "github.com/github/gh-aw/pkg/sliceutil" ) @@ -144,7 +145,7 @@ func (c *Compiler) MergeNetworkPermissions(topNetwork *NetworkPermissions, impor // Merge allowed domains from imported network for _, domain := range importedNetwork.Allowed { - if !hasStringKey(domainSet, domain) { + if !setutil.Contains(domainSet, domain) { result.Allowed = append(result.Allowed, domain) domainSet[domain] = struct { }{} @@ -249,7 +250,7 @@ func (c *Compiler) MergeSafeOutputs(topSafeOutputs *SafeOutputsConfig, importedS // exclude lists from imported configs are merged as a set into the result. for _, key := range typeKeys { if _, exists := config[key]; exists { - if hasStringKey(topDefinedTypes, key) { + if setutil.Contains(topDefinedTypes, key) { // Main workflow overrides imported definition — extract protected-files // exclude lists before removing the type entry. if handlerCfg, ok := config[key].(map[string]any); ok { @@ -264,7 +265,7 @@ func (c *Compiler) MergeSafeOutputs(topSafeOutputs *SafeOutputsConfig, importedS delete(config, key) continue } - if hasStringKey(importedDefinedTypes, key) { + if setutil.Contains(importedDefinedTypes, key) { return nil, fmt.Errorf("safe-outputs conflict: '%s' is defined in multiple imported workflows. Each safe-output type can only be defined once", key) } importedDefinedTypes[key] = struct { @@ -367,7 +368,7 @@ func mergeSafeOutputConfig(result *SafeOutputsConfig, config map[string]any, c * "ThreatDetection": {}, } for _, handler := range safeOutputHandlers { - if hasStringKey(specialMergeFields, handler.StructField) { + if setutil.Contains(specialMergeFields, handler.StructField) { continue } mergeSafeOutputFieldIfNil(result, importedConfig, handler.StructField) diff --git a/pkg/workflow/known_needs_expressions.go b/pkg/workflow/known_needs_expressions.go index e06ac8ad82b..a3a2b13db14 100644 --- a/pkg/workflow/known_needs_expressions.go +++ b/pkg/workflow/known_needs_expressions.go @@ -8,6 +8,7 @@ import ( "github.com/github/gh-aw/pkg/constants" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/setutil" ) var knownNeedsLog = logger.New("workflow:known_needs") @@ -150,7 +151,7 @@ func filterExpressionsForActivation(mappings []*ExpressionMapping, customJobs ma continue } // If it's a custom job NOT in beforeActivationJobs, drop it - if _, isCustomJob := customJobs[jobName]; isCustomJob && !hasStringKey(beforeActivationSet, jobName) { + if _, isCustomJob := customJobs[jobName]; isCustomJob && !setutil.Contains(beforeActivationSet, jobName) { knownNeedsLog.Printf("Filtered post-activation expression from activation substitution step: %s", m.Content) continue } diff --git a/pkg/workflow/map_helpers.go b/pkg/workflow/map_helpers.go index 514a521f4b1..9b1b90b7712 100644 --- a/pkg/workflow/map_helpers.go +++ b/pkg/workflow/map_helpers.go @@ -38,6 +38,7 @@ import ( "slices" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/setutil" ) var mapHelpersLog = logger.New("workflow:map_helpers") @@ -56,7 +57,7 @@ func excludeMapKeys(original map[string]any, excludeKeys ...string) map[string]a result := make(map[string]any) for key, value := range original { - if !hasStringKey(excludeSet, key) { + if !setutil.Contains(excludeSet, key) { result[key] = value } } diff --git a/pkg/workflow/mcp_config_custom.go b/pkg/workflow/mcp_config_custom.go index 5e9bc93e483..4b1a90abb3b 100644 --- a/pkg/workflow/mcp_config_custom.go +++ b/pkg/workflow/mcp_config_custom.go @@ -10,6 +10,7 @@ import ( "github.com/github/gh-aw/pkg/console" "github.com/github/gh-aw/pkg/logger" "github.com/github/gh-aw/pkg/parser" + "github.com/github/gh-aw/pkg/setutil" "github.com/github/gh-aw/pkg/sliceutil" "github.com/github/gh-aw/pkg/types" ) @@ -612,7 +613,7 @@ func getMCPConfig(toolConfig map[string]any, toolName string) (*parser.RegistryM } for key := range toolConfig { - if !hasStringKey(knownProperties, key) { + if !setutil.Contains(knownProperties, key) { mcpCustomLog.Printf("Unknown property '%s' in MCP config for tool '%s'", key, toolName) // Build list of valid properties validProps := []string{} diff --git a/pkg/workflow/mcp_config_validation.go b/pkg/workflow/mcp_config_validation.go index c6cc4a5b95a..7b2876784c7 100644 --- a/pkg/workflow/mcp_config_validation.go +++ b/pkg/workflow/mcp_config_validation.go @@ -20,6 +20,7 @@ import ( "github.com/github/gh-aw/pkg/constants" "github.com/github/gh-aw/pkg/parser" + "github.com/github/gh-aw/pkg/setutil" ) var mcpValidationLog = newValidationLogger("mcp_config") @@ -194,7 +195,7 @@ func getRawMCPConfig(toolConfig map[string]any) (map[string]any, error) { // Check for unknown fields that might be typos or deprecated (like "network") for field := range toolConfig { - if !hasStringKey(knownToolFields, field) { + if !setutil.Contains(knownToolFields, field) { // Build list of valid fields for the error message validFields := []string{} for k := range knownToolFields { diff --git a/pkg/workflow/mcp_github_config.go b/pkg/workflow/mcp_github_config.go index 376f3ae29a3..fe0b85dfd14 100644 --- a/pkg/workflow/mcp_github_config.go +++ b/pkg/workflow/mcp_github_config.go @@ -68,6 +68,7 @@ import ( "github.com/github/gh-aw/pkg/constants" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/setutil" "github.com/github/gh-aw/pkg/typeutil" ) @@ -230,7 +231,7 @@ func expandDefaultToolset(toolsetsStr string) string { githubConfigLog.Printf("Expanding %q keyword to action-friendly toolsets", toolset) // Expand "default" or "action-friendly" to action-friendly toolsets (excludes "users") for _, dt := range ActionFriendlyGitHubToolsets { - if !hasStringKey(seenToolsets, dt) { + if !setutil.Contains(seenToolsets, dt) { result = append(result, dt) seenToolsets[dt] = struct { }{} @@ -238,7 +239,7 @@ func expandDefaultToolset(toolsetsStr string) string { } } else { // Keep other toolsets as-is (including "all", individual toolsets, etc.) - if !hasStringKey(seenToolsets, toolset) { + if !setutil.Contains(seenToolsets, toolset) { result = append(result, toolset) seenToolsets[toolset] = struct { }{} diff --git a/pkg/workflow/mcp_setup_generator.go b/pkg/workflow/mcp_setup_generator.go index 453e13b0648..cc750117c13 100644 --- a/pkg/workflow/mcp_setup_generator.go +++ b/pkg/workflow/mcp_setup_generator.go @@ -68,6 +68,7 @@ import ( "github.com/github/gh-aw/pkg/constants" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/setutil" "github.com/github/gh-aw/pkg/sliceutil" ) @@ -830,7 +831,7 @@ func appendMCPGatewayCustomAndHTTPEnvFlags(containerCmd *strings.Builder, workfl addedEnvVars := buildAddedGatewayEnvVarSet(workflowData, gatewayConfig, hasGitHub, githubTool, tools, engine) var envVarNames []string for envVarName := range mcpEnvVars { - if !hasStringKey(addedEnvVars, envVarName) { + if !setutil.Contains(addedEnvVars, envVarName) { envVarNames = append(envVarNames, envVarName) } } diff --git a/pkg/workflow/model_alias_validation.go b/pkg/workflow/model_alias_validation.go index 703501a96da..31d0b254959 100644 --- a/pkg/workflow/model_alias_validation.go +++ b/pkg/workflow/model_alias_validation.go @@ -29,6 +29,7 @@ import ( "strings" "github.com/github/gh-aw/pkg/console" + "github.com/github/gh-aw/pkg/setutil" ) var modelAliasValidationLog = newValidationLogger("model_alias") @@ -253,7 +254,7 @@ func detectCircularModelAliases(aliasMap map[string][]string, markdownPath strin sort.Strings(keys) for _, key := range keys { - if hasStringKey(visited, key) { + if setutil.Contains(visited, key) { continue } path := []string{} // current DFS path (ordered) @@ -298,7 +299,7 @@ type dfsState struct { } func (s *dfsState) dfs(node string) []string { - if hasStringKey(s.visited, node) { + if setutil.Contains(s.visited, node) { return nil } if s.onPath[node] { diff --git a/pkg/workflow/on_needs_validation.go b/pkg/workflow/on_needs_validation.go index f2d6e06a772..4936d231b0d 100644 --- a/pkg/workflow/on_needs_validation.go +++ b/pkg/workflow/on_needs_validation.go @@ -7,6 +7,7 @@ import ( "github.com/github/gh-aw/pkg/constants" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/setutil" ) var onNeedsValidationLog = logger.New("workflow:on_needs_validation") @@ -57,7 +58,7 @@ func validateOnNeedsTargets(data *WorkflowData) error { need, ) } - if !hasStringKey(customJobs, need) { + if !setutil.Contains(customJobs, need) { return fmt.Errorf( "on.needs: unknown job %q. Expected one of the workflow's custom jobs. Example: on.needs: [secrets_fetcher]", need, @@ -117,7 +118,7 @@ func (c *Compiler) validateOnGitHubAppNeedsExpressions(data *WorkflowData) error if _, exists := data.Jobs[jobName]; !exists { return fmt.Errorf("on.github-app.%s: unknown job %q in needs expression", fieldName, jobName) } - if !hasStringKey(allowed, jobName) { + if !setutil.Contains(allowed, jobName) { return fmt.Errorf( "on.github-app.%s references needs.%s.outputs.* but job %q is not available before activation. Add it to on.needs (example: on.needs: [%s])", fieldName, @@ -174,10 +175,10 @@ func validateOnNeedsDependencyChain( }, visiting map[string]struct { }, visited map[string]struct { }) error { - if hasStringKey(visited, current) { + if setutil.Contains(visited, current) { return nil } - if hasStringKey(visiting, current) { + if setutil.Contains(visiting, current) { return fmt.Errorf("on.needs: cycle detected while validating dependency chain for %q", root) } @@ -215,7 +216,7 @@ func validateOnNeedsDependencyChain( } _, depHasExplicitNeeds := depConfig["needs"] - if !depHasExplicitNeeds && !hasStringKey(onNeedsSet, dep) && !hasStringKey(promptReferencedSet, dep) { + if !depHasExplicitNeeds && !setutil.Contains(onNeedsSet, dep) && !setutil.Contains(promptReferencedSet, dep) { return fmt.Errorf( "on.needs: job %q depends on %q, but %q has no explicit needs and is not in on.needs. It may get an implicit needs: activation and create a cycle. Add %q to on.needs or give %q explicit needs that run before activation", current, diff --git a/pkg/workflow/package_extraction.go b/pkg/workflow/package_extraction.go index 70ed42bc990..890c931d3d4 100644 --- a/pkg/workflow/package_extraction.go +++ b/pkg/workflow/package_extraction.go @@ -100,6 +100,7 @@ import ( "strings" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/setutil" ) var pkgLog = logger.New("workflow:package_extraction") @@ -358,7 +359,7 @@ func collectPackagesFromWorkflow( if workflowData.CustomSteps != "" { pkgs := extractor(workflowData.CustomSteps) for _, pkg := range pkgs { - if !hasStringKey(seen, pkg) { + if !setutil.Contains(seen, pkg) { packages = append(packages, pkg) seen[pkg] = struct { }{} @@ -379,7 +380,7 @@ func collectPackagesFromWorkflow( for _, arg := range argsSlice { if pkgStr, ok := arg.(string); ok { // Skip flags (arguments starting with - or --) - if !strings.HasPrefix(pkgStr, "-") && !hasStringKey(seen, pkgStr) { + if !strings.HasPrefix(pkgStr, "-") && !setutil.Contains(seen, pkgStr) { packages = append(packages, pkgStr) seen[pkgStr] = struct { }{} @@ -396,7 +397,7 @@ func collectPackagesFromWorkflow( // Use the extractor function to parse the command string pkgs := extractor(cmdStr) for _, pkg := range pkgs { - if !hasStringKey(seen, pkg) { + if !setutil.Contains(seen, pkg) { packages = append(packages, pkg) seen[pkg] = struct { }{} diff --git a/pkg/workflow/permissions_validation.go b/pkg/workflow/permissions_validation.go index 5f0162c297b..a79d79847b1 100644 --- a/pkg/workflow/permissions_validation.go +++ b/pkg/workflow/permissions_validation.go @@ -9,9 +9,11 @@ import ( "sort" "strings" + "github.com/goccy/go-yaml" + "github.com/github/gh-aw/pkg/constants" + "github.com/github/gh-aw/pkg/setutil" "github.com/github/gh-aw/pkg/stringutil" - "github.com/goccy/go-yaml" ) // PermissionsValidationResult contains the result of permissions validation @@ -446,7 +448,7 @@ func ValidatePermissionScopeNames(permissionsYAML string) error { } for scopeKey := range permsMap { - if hasStringKey(validMeta, scopeKey) { + if setutil.Contains(validMeta, scopeKey) { continue } if _, ok := validPermissionScopes[scopeKey]; ok { diff --git a/pkg/workflow/publish_artifacts_test.go b/pkg/workflow/publish_artifacts_test.go index 6537acda8da..c12df10cd79 100644 --- a/pkg/workflow/publish_artifacts_test.go +++ b/pkg/workflow/publish_artifacts_test.go @@ -8,6 +8,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/github/gh-aw/pkg/setutil" ) func TestParseUploadArtifactConfig(t *testing.T) { @@ -213,7 +215,7 @@ func TestComputeEnabledToolNamesIncludesUploadArtifact(t *testing.T) { }, } tools := computeEnabledToolNames(data) - assert.True(t, hasStringKey(tools, "upload_artifact"), "upload_artifact should be in enabled tools") + assert.True(t, setutil.Contains(tools, "upload_artifact"), "upload_artifact should be in enabled tools") } func TestGenerateSafeOutputsArtifactStagingUpload(t *testing.T) { diff --git a/pkg/workflow/run_step_sanitizer.go b/pkg/workflow/run_step_sanitizer.go index 65a47dddaa1..645ab1a3cf2 100644 --- a/pkg/workflow/run_step_sanitizer.go +++ b/pkg/workflow/run_step_sanitizer.go @@ -50,8 +50,10 @@ import ( "slices" "strings" - "github.com/github/gh-aw/pkg/logger" "github.com/goccy/go-yaml" + + "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/setutil" ) var runStepSanitizerLog = logger.New("workflow:run_step_sanitizer") @@ -108,7 +110,7 @@ func sanitizeRunStepExpressions(step map[string]any) (map[string]any, []string, for _, match := range matches { original := match[0] - if hasStringKey(seen, original) { + if setutil.Contains(seen, original) { continue } seen[original] = struct { diff --git a/pkg/workflow/runtime_deduplication.go b/pkg/workflow/runtime_deduplication.go index 8f34e598cc6..ed94b50f870 100644 --- a/pkg/workflow/runtime_deduplication.go +++ b/pkg/workflow/runtime_deduplication.go @@ -4,8 +4,10 @@ import ( "fmt" "strings" - "github.com/github/gh-aw/pkg/logger" "github.com/goccy/go-yaml" + + "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/setutil" ) var runtimeDeduplicationLog = logger.New("workflow:runtime_deduplication") @@ -208,7 +210,7 @@ func DeduplicateRuntimeSetupStepsFromCustomSteps(customSteps string, runtimeRequ // Filter runtime requirements to exclude those with user-customized setup actions var filteredRequirements []RuntimeRequirement for _, req := range runtimeRequirements { - if !hasStringKey(filteredRuntimeIDs, req.Runtime.ID) { + if !setutil.Contains(filteredRuntimeIDs, req.Runtime.ID) { filteredRequirements = append(filteredRequirements, req) } else { runtimeDeduplicationLog.Printf(" Excluding runtime %s from generated setup steps (user has custom setup)", req.Runtime.ID) diff --git a/pkg/workflow/runtime_import_validation.go b/pkg/workflow/runtime_import_validation.go index 9038d6c9df9..37681d07d9f 100644 --- a/pkg/workflow/runtime_import_validation.go +++ b/pkg/workflow/runtime_import_validation.go @@ -25,6 +25,7 @@ import ( "github.com/github/gh-aw/pkg/constants" "github.com/github/gh-aw/pkg/parser" + "github.com/github/gh-aw/pkg/setutil" ) // runtimeImportMacroRe matches {{#runtime-import filepath}} or {{#runtime-import? filepath}}. @@ -73,7 +74,7 @@ func extractRuntimeImportPaths(markdownContent string) []string { } // Add to list if not already seen - if !hasStringKey(seen, importPath) { + if !setutil.Contains(seen, importPath) { paths = append(paths, importPath) seen[importPath] = struct { }{} diff --git a/pkg/workflow/safe_jobs_needs_validation.go b/pkg/workflow/safe_jobs_needs_validation.go index 5a7cb6053b3..f5e827eb30c 100644 --- a/pkg/workflow/safe_jobs_needs_validation.go +++ b/pkg/workflow/safe_jobs_needs_validation.go @@ -7,6 +7,7 @@ import ( "github.com/github/gh-aw/pkg/constants" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/setutil" "github.com/github/gh-aw/pkg/stringutil" ) @@ -47,7 +48,7 @@ func validateSafeJobNeeds(data *WorkflowData) error { normalizedJobName := stringutil.NormalizeSafeOutputIdentifier(originalName) for i, need := range jobConfig.Needs { normalizedNeed := stringutil.NormalizeSafeOutputIdentifier(need) - if !hasStringKey(validIDs, normalizedNeed) { + if !setutil.Contains(validIDs, normalizedNeed) { return fmt.Errorf( "safe-outputs.jobs.%s: unknown needs target %q\n\nValid dependency targets for custom safe-jobs are:\n%s\n\n"+ "Custom safe-jobs cannot depend on workflow control jobs such as 'conclusion' or 'activation'", diff --git a/pkg/workflow/safe_jobs_needs_validation_test.go b/pkg/workflow/safe_jobs_needs_validation_test.go index 0a29946f369..cda975439d8 100644 --- a/pkg/workflow/safe_jobs_needs_validation_test.go +++ b/pkg/workflow/safe_jobs_needs_validation_test.go @@ -7,6 +7,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/github/gh-aw/pkg/setutil" ) // TestValidateSafeJobNeeds_NoSafeOutputs verifies that validation is a no-op @@ -435,9 +437,9 @@ func TestComputeValidSafeJobNeeds(t *testing.T) { t.Run("base – no safe-outputs", func(t *testing.T) { data := &WorkflowData{} valid := computeValidSafeJobNeeds(data) - assert.True(t, hasStringKey(valid, "agent"), "agent should always be valid") - assert.False(t, hasStringKey(valid, "detection"), "detection not valid without safe-outputs") - assert.False(t, hasStringKey(valid, "safe_outputs"), "safe_outputs not valid without safe-outputs") + assert.True(t, setutil.Contains(valid, "agent"), "agent should always be valid") + assert.False(t, setutil.Contains(valid, "detection"), "detection not valid without safe-outputs") + assert.False(t, setutil.Contains(valid, "safe_outputs"), "safe_outputs not valid without safe-outputs") }) t.Run("only custom jobs configured – safe_outputs absent", func(t *testing.T) { @@ -451,7 +453,7 @@ func TestComputeValidSafeJobNeeds(t *testing.T) { }, } valid := computeValidSafeJobNeeds(data) - assert.False(t, hasStringKey(valid, "safe_outputs"), "safe_outputs should not be valid when only custom jobs present") + assert.False(t, setutil.Contains(valid, "safe_outputs"), "safe_outputs should not be valid when only custom jobs present") }) t.Run("builtin type configured – safe_outputs present", func(t *testing.T) { @@ -462,11 +464,11 @@ func TestComputeValidSafeJobNeeds(t *testing.T) { }, } valid := computeValidSafeJobNeeds(data) - assert.True(t, hasStringKey(valid, "agent")) - assert.True(t, hasStringKey(valid, "safe_outputs")) - assert.True(t, hasStringKey(valid, "detection"), "detection enabled when ThreatDetection is non-nil") - assert.False(t, hasStringKey(valid, "upload_assets")) - assert.False(t, hasStringKey(valid, "unlock")) + assert.True(t, setutil.Contains(valid, "agent")) + assert.True(t, setutil.Contains(valid, "safe_outputs")) + assert.True(t, setutil.Contains(valid, "detection"), "detection enabled when ThreatDetection is non-nil") + assert.False(t, setutil.Contains(valid, "upload_assets")) + assert.False(t, setutil.Contains(valid, "unlock")) }) t.Run("with upload-asset configured", func(t *testing.T) { @@ -474,7 +476,7 @@ func TestComputeValidSafeJobNeeds(t *testing.T) { SafeOutputs: &SafeOutputsConfig{UploadAssets: &UploadAssetsConfig{}}, } valid := computeValidSafeJobNeeds(data) - assert.True(t, hasStringKey(valid, "upload_assets")) + assert.True(t, setutil.Contains(valid, "upload_assets")) }) t.Run("with lock-for-agent enabled", func(t *testing.T) { @@ -483,7 +485,7 @@ func TestComputeValidSafeJobNeeds(t *testing.T) { SafeOutputs: &SafeOutputsConfig{}, } valid := computeValidSafeJobNeeds(data) - assert.True(t, hasStringKey(valid, "unlock")) + assert.True(t, setutil.Contains(valid, "unlock")) }) t.Run("custom safe-job names are included", func(t *testing.T) { @@ -496,8 +498,8 @@ func TestComputeValidSafeJobNeeds(t *testing.T) { }, } valid := computeValidSafeJobNeeds(data) - assert.True(t, hasStringKey(valid, "my_packager"), "dash-to-underscore normalized name should be valid") - assert.True(t, hasStringKey(valid, "notify_team")) + assert.True(t, setutil.Contains(valid, "my_packager"), "dash-to-underscore normalized name should be valid") + assert.True(t, setutil.Contains(valid, "notify_team")) }) } diff --git a/pkg/workflow/safe_outputs_needs_validation.go b/pkg/workflow/safe_outputs_needs_validation.go index 848ce80abdd..ebe553d93cc 100644 --- a/pkg/workflow/safe_outputs_needs_validation.go +++ b/pkg/workflow/safe_outputs_needs_validation.go @@ -5,6 +5,7 @@ import ( "github.com/github/gh-aw/pkg/constants" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/setutil" ) var safeOutputsNeedsValidationLog = logger.New("workflow:safe_outputs_needs_validation") @@ -49,7 +50,7 @@ func validateSafeOutputsNeedsField(data *WorkflowData, fieldName string, needs [ fieldName, ) } - if !hasStringKey(customJobs, need) { + if !setutil.Contains(customJobs, need) { safeOutputsNeedsValidationLog.Printf("Validation failed: %q is not a known custom job", need) return fmt.Errorf( "safe-outputs.%s: unknown job %q. Expected one of the workflow's custom jobs. Example: safe-outputs.%s: [secrets_fetcher]", diff --git a/pkg/workflow/safe_update_enforcement.go b/pkg/workflow/safe_update_enforcement.go index 2315491b9cf..c1b4b4fb860 100644 --- a/pkg/workflow/safe_update_enforcement.go +++ b/pkg/workflow/safe_update_enforcement.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/setutil" ) var safeUpdateLog = logger.New("workflow:safe_update") @@ -108,7 +109,7 @@ func collectSecretViolations(manifest *GHAWManifest, secretNames []string) []str if ghAwInternalSecrets[full] { continue } - if hasStringKey(known, full) { + if setutil.Contains(known, full) { continue } violations = append(violations, full) @@ -188,7 +189,7 @@ func collectActionViolations(manifest *GHAWManifest, actionRefs []string) (added if isTrustedActionRepo(repo) { continue } - if !hasStringKey(knownRepos, repo) { + if !setutil.Contains(knownRepos, repo) { added = append(added, repo) } } @@ -199,7 +200,7 @@ func collectActionViolations(manifest *GHAWManifest, actionRefs []string) (added if isTrustedActionRepo(repo) { continue } - if !hasStringKey(newRepos, repo) { + if !setutil.Contains(newRepos, repo) { removed = append(removed, repo) } } diff --git a/pkg/workflow/safe_update_manifest.go b/pkg/workflow/safe_update_manifest.go index ed93802ee3a..5758c1ec80c 100644 --- a/pkg/workflow/safe_update_manifest.go +++ b/pkg/workflow/safe_update_manifest.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/setutil" ) var safeUpdateManifestLog = logger.New("workflow:safe_update_manifest") @@ -76,7 +77,7 @@ func NewGHAWManifest(secretNames []string, actionRefs []string, failures []GHAWM secrets := make([]string, 0, len(secretNames)) for _, name := range secretNames { full := normalizeSecretName(name) - if !hasStringKey(seen, full) { + if !setutil.Contains(seen, full) { seen[full] = struct { }{} secrets = append(secrets, full) @@ -92,7 +93,7 @@ func NewGHAWManifest(secretNames []string, actionRefs []string, failures []GHAWM }, len(containers)) sortedContainers := make([]GHAWManifestContainer, 0, len(containers)) for _, c := range containers { - if c.Image != "" && !hasStringKey(seenContainers, c.Image) { + if c.Image != "" && !setutil.Contains(seenContainers, c.Image) { seenContainers[c.Image] = struct { }{} sortedContainers = append(sortedContainers, c) @@ -165,7 +166,7 @@ func parseActionRefs(refs []string) []GHAWManifestAction { } key := repo + "@" + sha - if hasStringKey(seen, key) { + if setutil.Contains(seen, key) { continue } seen[key] = struct { diff --git a/pkg/workflow/seenmapbool_helpers.go b/pkg/workflow/seenmapbool_helpers.go deleted file mode 100644 index c6a55a082a9..00000000000 --- a/pkg/workflow/seenmapbool_helpers.go +++ /dev/null @@ -1,6 +0,0 @@ -package workflow - -func hasStringKey(set map[string]struct{}, key string) bool { - _, ok := set[key] - return ok -} diff --git a/pkg/workflow/shell.go b/pkg/workflow/shell.go index fc5faae7850..9756d8e6cfa 100644 --- a/pkg/workflow/shell.go +++ b/pkg/workflow/shell.go @@ -4,6 +4,7 @@ import ( "strings" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/setutil" ) var shellLog = logger.New("workflow:shell") @@ -198,7 +199,7 @@ func findExpandableVars(s string) []string { break } varRef := s[start : start+end+1] - if !hasStringKey(seen, varRef) { + if !setutil.Contains(seen, varRef) { seen[varRef] = struct { }{} vars = append(vars, varRef) diff --git a/pkg/workflow/side_repo_maintenance.go b/pkg/workflow/side_repo_maintenance.go index a3c16283cd9..7f76f9d260b 100644 --- a/pkg/workflow/side_repo_maintenance.go +++ b/pkg/workflow/side_repo_maintenance.go @@ -10,6 +10,7 @@ import ( "strings" "github.com/github/gh-aw/pkg/constants" + "github.com/github/gh-aw/pkg/setutil" "github.com/github/gh-aw/pkg/stringutil" ) @@ -161,7 +162,7 @@ func generateAllSideRepoMaintenanceWorkflows( if !strings.HasPrefix(name, "agentics-maintenance-") || !strings.HasSuffix(name, ".yml") { continue } - if hasStringKey(generatedFiles, name) { + if setutil.Contains(generatedFiles, name) { continue } stalePath := filepath.Join(workflowDir, name) diff --git a/pkg/workflow/strict_mode_env_validation.go b/pkg/workflow/strict_mode_env_validation.go index 7b46f01891a..ae917724efa 100644 --- a/pkg/workflow/strict_mode_env_validation.go +++ b/pkg/workflow/strict_mode_env_validation.go @@ -11,6 +11,7 @@ import ( "strings" "github.com/github/gh-aw/pkg/console" + "github.com/github/gh-aw/pkg/setutil" ) // validateEnvSecrets detects secrets in the top-level env section and the engine.env section, @@ -114,7 +115,7 @@ func (c *Compiler) validateEnvSecretsSection(config map[string]any, sectionName // are explicitly allowed (e.g. engine env var overrides in engine.env). envStrings := make(map[string]string) for key, value := range envMap { - if allowedEnvVarKeys != nil && hasStringKey(allowedEnvVarKeys, key) { + if allowedEnvVarKeys != nil && setutil.Contains(allowedEnvVarKeys, key) { strictModeValidationLog.Printf("Skipping allowed engine env var key in %s: %s", sectionName, key) continue } diff --git a/pkg/workflow/submit_pr_review.go b/pkg/workflow/submit_pr_review.go index 8a1dcc75912..18ecdf44997 100644 --- a/pkg/workflow/submit_pr_review.go +++ b/pkg/workflow/submit_pr_review.go @@ -4,6 +4,7 @@ import ( "strings" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/setutil" ) var submitPRReviewLog = logger.New("workflow:submit_pr_review") @@ -89,7 +90,7 @@ func (c *Compiler) parseSubmitPullRequestReviewConfig(outputMap map[string]any) for _, e := range eventsSlice { if eventStr, ok := e.(string); ok { upper := strings.ToUpper(eventStr) - if hasStringKey(validEvents, upper) { + if setutil.Contains(validEvents, upper) { config.AllowedEvents = append(config.AllowedEvents, upper) } else { submitPRReviewLog.Printf("Ignoring invalid allowed-events value: %s", eventStr) diff --git a/pkg/workflow/template.go b/pkg/workflow/template.go index 49865332393..13aac56fa47 100644 --- a/pkg/workflow/template.go +++ b/pkg/workflow/template.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/setutil" ) var templateLog = logger.New("workflow:template") @@ -123,7 +124,7 @@ func (c *Compiler) generateInterpolationAndTemplateStep(yaml *strings.Builder, e // Add environment variables for extracted expressions (deduplicated by EnvVar) seen := make(map[string]struct{}) for _, mapping := range expressionMappings { - if hasStringKey(seen, mapping.EnvVar) { + if setutil.Contains(seen, mapping.EnvVar) { continue } seen[mapping.EnvVar] = struct{}{} diff --git a/pkg/workflow/time_delta.go b/pkg/workflow/time_delta.go index 6aa5d83bd10..1cdb3529893 100644 --- a/pkg/workflow/time_delta.go +++ b/pkg/workflow/time_delta.go @@ -9,6 +9,7 @@ import ( "time" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/setutil" ) var timeDeltaLog = logger.New("workflow:time_delta") @@ -107,7 +108,7 @@ func parseTimeDeltaWithMinutes(deltaStr string, allowMinutes bool) (*TimeDelta, unit := match[2] // Check for duplicate units - if hasStringKey(seenUnits, unit) { + if setutil.Contains(seenUnits, unit) { return nil, fmt.Errorf("duplicate unit '%s' in time delta: +%s", unit, deltaStr) } seenUnits[unit] = struct { diff --git a/pkg/workflow/tools_parser.go b/pkg/workflow/tools_parser.go index 975dd83e89a..3a59a7b703e 100644 --- a/pkg/workflow/tools_parser.go +++ b/pkg/workflow/tools_parser.go @@ -56,6 +56,7 @@ import ( "strings" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/setutil" ) var toolsParserLog = logger.New("workflow:tools_parser") @@ -174,7 +175,7 @@ func NewTools(toolsMap map[string]any) *Tools { customCount := 0 for name, config := range toolsMap { - if !hasStringKey(knownTools, name) { + if !setutil.Contains(knownTools, name) { tools.Custom[name] = parseMCPServerConfig(config) customCount++ } @@ -717,7 +718,7 @@ func parseMCPServerConfig(val any) MCPServerConfig { } for key, value := range configMap { - if !hasStringKey(knownFields, key) { + if !setutil.Contains(knownFields, key) { config.CustomFields[key] = value } } diff --git a/pkg/workflow/tools_validation_github_toolsets.go b/pkg/workflow/tools_validation_github_toolsets.go index 79f32ccf6a0..d5cf314aa72 100644 --- a/pkg/workflow/tools_validation_github_toolsets.go +++ b/pkg/workflow/tools_validation_github_toolsets.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/github/gh-aw/pkg/parser" + "github.com/github/gh-aw/pkg/setutil" "github.com/github/gh-aw/pkg/stringutil" ) @@ -67,7 +68,7 @@ func validateGitHubToolsAgainstToolsetsCore(allowedTools []string, enabledToolse continue } - if !hasStringKey(enabledSet, requiredToolset) { + if !setutil.Contains(enabledSet, requiredToolset) { githubToolToToolsetLog.Printf("Tool %s requires missing toolset: %s", tool, requiredToolset) missingToolsets[requiredToolset] = append(missingToolsets[requiredToolset], tool) } diff --git a/pkg/workflow/trigger_parser.go b/pkg/workflow/trigger_parser.go index b6506b76617..de916acdb05 100644 --- a/pkg/workflow/trigger_parser.go +++ b/pkg/workflow/trigger_parser.go @@ -7,10 +7,12 @@ import ( "path/filepath" "strings" + "github.com/goccy/go-yaml" + "github.com/github/gh-aw/pkg/logger" "github.com/github/gh-aw/pkg/parser" + "github.com/github/gh-aw/pkg/setutil" "github.com/github/gh-aw/pkg/sliceutil" - "github.com/goccy/go-yaml" ) var triggerParserLog = logger.New("workflow:trigger_parser") @@ -265,7 +267,7 @@ func parsePullRequestTrigger(tokens []string) (*TriggerIR, error) { }, nil } - if hasStringKey(validTypes, activityType) { + if setutil.Contains(validTypes, activityType) { ir := &TriggerIR{ Event: "pull_request", Types: []string{activityType}, @@ -343,7 +345,7 @@ func parseIssueTrigger(tokens []string) (*TriggerIR, error) { "transferred": {}, } - if !hasStringKey(validTypes, activityType) { + if !setutil.Contains(validTypes, activityType) { return nil, fmt.Errorf("invalid issue activity type: '%s'. Valid types: opened, edited, closed, reopened, assigned, unassigned, labeled, unlabeled, deleted, transferred. Example: 'issue opened'", activityType) } @@ -395,7 +397,7 @@ func parseDiscussionTrigger(tokens []string) (*TriggerIR, error) { "unanswered": {}, } - if !hasStringKey(validTypes, activityType) { + if !setutil.Contains(validTypes, activityType) { return nil, fmt.Errorf("invalid discussion activity type: '%s'. Valid types: created, edited, deleted, transferred, pinned, unpinned, labeled, unlabeled, locked, unlocked, category_changed, answered, unanswered. Example: 'discussion created'", activityType) } @@ -514,7 +516,7 @@ func parseReleaseTrigger(tokens []string) (*TriggerIR, error) { "released": {}, } - if !hasStringKey(validTypes, activityType) { + if !setutil.Contains(validTypes, activityType) { return nil, fmt.Errorf("invalid release activity type: '%s'. Valid types: published, unpublished, created, edited, deleted, prereleased, released. Example: 'release published'", activityType) } @@ -669,11 +671,11 @@ func parseDeploymentTrigger(input string) (*TriggerIR, error) { }{"or": {}, "and": {}} for _, tok := range tokens[1:] { tok = strings.ToLower(strings.TrimRight(tok, ",")) - if hasStringKey(conjunctions, tok) { + if setutil.Contains(conjunctions, tok) { continue } if state, ok := stateAliases[tok]; ok { - if !hasStringKey(seenStates, state) { + if !setutil.Contains(seenStates, state) { states = append(states, state) seenStates[state] = struct { }{} From 8fe2d27135d8ca120eeea1063f8b4a0829f68b38 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 21 Jun 2026 02:01:08 +0000 Subject: [PATCH 3/3] docs: fix Contains function signature in setutil README Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/setutil/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/setutil/README.md b/pkg/setutil/README.md index 45f69426199..de8d665b137 100644 --- a/pkg/setutil/README.md +++ b/pkg/setutil/README.md @@ -12,7 +12,7 @@ All functions in this package are pure: they never modify their input. They are | Function | Signature | Description | |----------|-----------|-------------| -| `Contains` | `func[K comparable](set map[K]struct{}, key K) bool` | Reports whether `key` is present in `set` | +| `Contains` | `func Contains[K comparable](set map[K]struct{}, key K) bool` | Reports whether `key` is present in `set` | ## Usage Examples