From bfafa0bb5db2bb0ac22e4eb8e74c9af02cf91b85 Mon Sep 17 00:00:00 2001 From: Cory Bullinger Date: Fri, 21 Nov 2025 07:37:43 -0500 Subject: [PATCH 1/4] Fix: Merge individual commit_strategy fields from defaults Previously, when a workflow had ANY commit_strategy fields, it would completely ignore all defaults. This meant workflows with partial commit_strategy (e.g., only pr_title and pr_body) wouldn't inherit commit_message or use_pr_template from defaults. Changes: - Modified YAMLConfig.SetDefaults() to merge individual fields - Modified WorkflowConfig.SetDefaults() to merge individual fields - Fields now inherited: Type, CommitMessage, PRTitle, PRBody, UsePRTemplate - AutoMerge intentionally NOT inherited for safety when workflow has its own commit_strategy Tests: - Added comprehensive tests in types/config_test.go - Updated existing test in services/main_config_loader_test.go - All tests pass --- .../QUICK-START-MAIN-CONFIG.md | 301 ------------------ .../SOURCE-REPO-README.md | 240 ++++++++++++++ .../services/main_config_loader_test.go | 10 +- examples-copier/types/config.go | 46 +++ examples-copier/types/config_test.go | 272 ++++++++++++++++ 5 files changed, 564 insertions(+), 305 deletions(-) delete mode 100644 examples-copier/configs/copier-config-examples/QUICK-START-MAIN-CONFIG.md create mode 100644 examples-copier/configs/copier-config-examples/SOURCE-REPO-README.md create mode 100644 examples-copier/types/config_test.go diff --git a/examples-copier/configs/copier-config-examples/QUICK-START-MAIN-CONFIG.md b/examples-copier/configs/copier-config-examples/QUICK-START-MAIN-CONFIG.md deleted file mode 100644 index 4331153..0000000 --- a/examples-copier/configs/copier-config-examples/QUICK-START-MAIN-CONFIG.md +++ /dev/null @@ -1,301 +0,0 @@ -# Quick Start: Main Config Architecture - -Get started with the main config architecture in 5 minutes. - -## What is Main Config? - -Main config is a centralized configuration system that: -- Stores global defaults in one place -- References workflow configs in source repositories -- Supports reusable components -- Enables distributed workflow management - -## Quick Setup - -### 1. Update env.yaml - -Add these variables to your `env.yaml`: - -```yaml -env_variables: - # Enable main config - MAIN_CONFIG_FILE: "main-config.yaml" - USE_MAIN_CONFIG: "true" - - # Config repository (where main config lives) - CONFIG_REPO_OWNER: "mongodb" - CONFIG_REPO_NAME: "code-copier-config" - CONFIG_REPO_BRANCH: "main" -``` - -### 2. Create Main Config - -Create `main-config.yaml` in your config repository: - -```yaml -# Global defaults -defaults: - commit_strategy: - type: "pull_request" - auto_merge: false - exclude: - - "**/.env" - - "**/.env.*" - -# Workflow references -workflow_configs: - # Reference workflow config in source repo - - source: "repo" - repo: "mongodb/docs-sample-apps" - branch: "main" - path: ".copier/workflows.yaml" -``` - -### 3. Create Workflow Config in Source Repo - -Create `.copier/workflows.yaml` in your source repository: - -```yaml -workflows: - - name: "my-first-workflow" - source: - repo: "mongodb/docs-sample-apps" - branch: "main" - destination: - repo: "mongodb/my-destination" - branch: "main" - transformations: - - move: - from: "examples" - to: "code-examples" -``` - -### 4. Deploy and Test - -```bash -# Deploy the app -gcloud app deploy app.yaml - -# Test by merging a PR in your source repo -# The workflow should execute automatically -``` - -## Common Patterns - -### Pattern 1: Centralized Workflows - -Keep all workflows in the config repo: - -```yaml -# main-config.yaml -workflow_configs: - - source: "local" - path: "workflows/mflix-workflows.yaml" - - source: "local" - path: "workflows/university-workflows.yaml" -``` - -**Use when**: Central team manages all workflows - -### Pattern 2: Distributed Workflows - -Keep workflows in source repos: - -```yaml -# main-config.yaml -workflow_configs: - - source: "repo" - repo: "mongodb/docs-sample-apps" - path: ".copier/workflows.yaml" - - source: "repo" - repo: "10gen/university-content" - path: ".copier/workflows.yaml" -``` - -**Use when**: Source repo teams manage their own workflows - -### Pattern 3: Hybrid Approach - -Mix centralized and distributed: - -```yaml -# main-config.yaml -workflow_configs: - # Centralized (managed by central team) - - source: "local" - path: "workflows/critical-workflows.yaml" - - # Distributed (managed by source teams) - - source: "repo" - repo: "mongodb/docs-sample-apps" - path: ".copier/workflows.yaml" - - # Inline (simple one-offs) - - source: "inline" - workflows: - - name: "simple-copy" - source: - repo: "mongodb/docs" - branch: "main" - destination: - repo: "mongodb/docs-public" - branch: "main" - transformations: - - move: { from: "examples", to: "public-examples" } -``` - -**Use when**: You need flexibility - -## Directory Structure - -### Recommended Structure - -``` -Config Repo (mongodb/code-copier-config): -├── main-config.yaml # Main config file -└── workflows/ # Centralized workflows (optional) - ├── mflix-workflows.yaml - └── university-workflows.yaml - -Source Repo (mongodb/docs-sample-apps): -└── .copier/ # Workflow config directory - ├── workflows.yaml # Workflow definitions - ├── transformations/ # Reusable transformations - │ ├── mflix-java.yaml - │ └── mflix-nodejs.yaml - ├── strategies/ # Reusable strategies - │ └── mflix-pr-strategy.yaml - └── common/ # Common configs - └── mflix-excludes.yaml -``` - -## Default Precedence - -Settings are applied from least to most specific: - -``` -System Defaults - ↓ -Main Config Defaults - ↓ -Workflow Config Defaults - ↓ -Individual Workflow Settings (wins) -``` - -Example: - -```yaml -# main-config.yaml -defaults: - commit_strategy: - type: "pull_request" - auto_merge: false # Global default - -# .copier/workflows.yaml -defaults: - commit_strategy: - auto_merge: true # Overrides main config - -workflows: - - name: "my-workflow" - commit_strategy: - auto_merge: false # Overrides workflow config default -``` - -## Troubleshooting - -### Config Not Loading - -**Problem**: App can't find main config file - -**Solution**: -1. Check `MAIN_CONFIG_FILE` is set correctly -2. Verify file exists in config repository -3. Check `CONFIG_REPO_OWNER` and `CONFIG_REPO_NAME` -4. Review app logs for errors - -### Workflows Not Executing - -**Problem**: Workflows don't run when PR is merged - -**Solution**: -1. Verify workflow `source.repo` matches webhook repo -2. Check workflow config is referenced in main config -3. Validate YAML syntax -4. Check app logs for validation errors - -### Authentication Errors - -**Problem**: Can't access source or destination repos - -**Solution**: -1. Verify GitHub App has access to all repos -2. Check app is installed in all required orgs -3. Verify app permissions include repo read/write - -## Next Steps - -1. **Read the full guide**: See `MAIN-CONFIG-README.md` -2. **Review examples**: Check `main-config-example.yaml` -3. **Test locally**: Use `DRY_RUN=true` for testing -4. **Add more workflows**: Expand your configuration -5. **Use reusable components**: Extract common configs - -## Support - -- **Documentation**: `MAIN-CONFIG-README.md` -- **Examples**: `main-config-example.yaml`, `source-repo-workflows-example.yaml` -- **Reusable Components**: `reusable-components/` directory - -## Common Use Cases - -### Use Case 1: Monorepo with Many Workflows - -**Problem**: 50+ workflows in one config file -**Solution**: Split into multiple workflow config files - -```yaml -workflow_configs: - - source: "local" - path: "workflows/mflix-workflows.yaml" # 10 workflows - - source: "local" - path: "workflows/university-workflows.yaml" # 15 workflows - - source: "local" - path: "workflows/docs-workflows.yaml" # 25 workflows -``` - -### Use Case 2: Multiple Source Repos - -**Problem**: Workflows for different source repos mixed together -**Solution**: Each source repo has its own workflow config - -```yaml -workflow_configs: - - source: "repo" - repo: "mongodb/docs-sample-apps" - path: ".copier/workflows.yaml" - - source: "repo" - repo: "10gen/university-content" - path: ".copier/workflows.yaml" -``` - -### Use Case 3: Team Ownership - -**Problem**: Multiple teams need to manage workflows -**Solution**: Each team manages workflows in their source repo - -```yaml -workflow_configs: - # Team A's workflows - - source: "repo" - repo: "mongodb/team-a-repo" - path: ".copier/workflows.yaml" - - # Team B's workflows - - source: "repo" - repo: "mongodb/team-b-repo" - path: ".copier/workflows.yaml" -``` - ---- \ No newline at end of file diff --git a/examples-copier/configs/copier-config-examples/SOURCE-REPO-README.md b/examples-copier/configs/copier-config-examples/SOURCE-REPO-README.md new file mode 100644 index 0000000..38c756d --- /dev/null +++ b/examples-copier/configs/copier-config-examples/SOURCE-REPO-README.md @@ -0,0 +1,240 @@ +# Code Copier Workflows + +This directory contains workflow configurations for automatically copying code examples to destination repositories. + +## Quick Start + +### File Location + +Place your workflow configuration at: `.copier/workflows.yaml` + +### Basic Workflow Structure + +```yaml +workflows: + - name: "my-workflow" + destination: + repo: "mongodb/destination-repo" + branch: "main" + transformations: + - move: + from: "source/path" + to: "destination/path" +``` + +## Adding a New Workflow + +1. **Edit `.copier/workflows.yaml`** in your repository + +2. **Add a new workflow entry:** + +```yaml +workflows: + - name: "my-new-workflow" + destination: + repo: "mongodb/my-destination-repo" + branch: "main" + transformations: + - move: + from: "examples/my-code" + to: "code" +``` + +3. **Commit and push** - the workflow is now active! + +## Modifying an Existing Workflow + +Simply edit the workflow in `.copier/workflows.yaml` and commit your changes. The updated configuration will be used for the next PR merge. + +## Common Transformation Types + +### Move Directory + +Copy all files from one directory to another: + +```yaml +transformations: + - move: + from: "examples/go" + to: "code/go" +``` + +### Copy Single File + +Copy a specific file: + +```yaml +transformations: + - copy: + from: "README.md" + to: "docs/README.md" +``` + +### Match with Wildcards + +Use glob patterns for flexible matching: + +```yaml +transformations: + - glob: + pattern: "examples/**/*.go" + transform: "code/${relative_path}" +``` + +## Customizing PR Details + +Add custom PR titles and descriptions: + +```yaml +workflows: + - name: "my-workflow" + destination: + repo: "mongodb/destination-repo" + branch: "main" + transformations: + - move: { from: "src", to: "dest" } + commit_strategy: + type: "pull_request" + pr_title: "Update examples from ${source_repo}" + pr_body: | + Automated update from source repository. + + Source PR: #${pr_number} + Commit: ${commit_sha} + auto_merge: false +``` + +## Available Variables + +Use these variables in PR titles, bodies, and commit messages: + +- `${source_repo}` - Source repository name +- `${source_branch}` - Source branch name +- `${pr_number}` - Source PR number +- `${commit_sha}` - Source commit SHA +- `${file_count}` - Number of files changed + +Use these in path transformations: + +- `${relative_path}` - Path relative to the matched pattern +- `${path}` - Full source file path +- `${filename}` - Just the filename +- `${dir}` - Directory path +- `${ext}` - File extension + +## Excluding Files + +Prevent certain files from being copied: + +```yaml +workflows: + - name: "my-workflow" + destination: + repo: "mongodb/destination-repo" + branch: "main" + transformations: + - move: { from: "src", to: "dest" } + exclude: + - "**/.env" + - "**/node_modules/**" + - "**/*.test.js" +``` + +## Setting Defaults + +Apply settings to all workflows in this file: + +```yaml +defaults: + commit_strategy: + type: "pull_request" + auto_merge: false + exclude: + - "**/.env" + - "**/node_modules/**" + +workflows: + - name: "workflow-1" + # inherits defaults + destination: + repo: "mongodb/dest-1" + transformations: + - move: { from: "src", to: "dest" } + + - name: "workflow-2" + # inherits defaults + destination: + repo: "mongodb/dest-2" + transformations: + - move: { from: "examples", to: "code" } +``` + +## Testing Your Configuration + +Before committing, you can validate your configuration: + +```bash +# Validate syntax +./config-validator validate -config .copier/workflows.yaml + +# Test a pattern match +./config-validator test-pattern \ + -type glob \ + -pattern "examples/**/*.go" \ + -file "examples/database/connect.go" +``` + +## How It Works + +1. **You merge a PR** in this repository +2. **Copier detects the merge** via webhook +3. **Workflows are matched** based on changed files +4. **Files are copied** to destination repositories +5. **PRs are created** in destination repositories + +## Need Help? + +- **Full Documentation**: [Code Example Tooling Repository](https://github.com/mongodb/code-example-tooling) +- **Configuration Examples**: See `examples-copier/configs/copier-config-examples/` +- **Pattern Matching Guide**: See `examples-copier/docs/PATTERN-MATCHING-GUIDE.md` +- **Main Config Architecture**: See `examples-copier/configs/copier-config-examples/MAIN-CONFIG-README.md` + +## Example: Complete Workflow + +```yaml +# .copier/workflows.yaml + +defaults: + commit_strategy: + type: "pull_request" + auto_merge: false + exclude: + - "**/.env" + - "**/node_modules/**" + +workflows: + - name: "go-examples" + destination: + repo: "mongodb/go-examples-repo" + branch: "main" + transformations: + - move: + from: "examples/go" + to: "code" + commit_strategy: + pr_title: "Update Go examples from ${source_repo}" + pr_body: | + Automated update of Go code examples. + + **Source**: ${source_repo} (PR #${pr_number}) + **Commit**: ${commit_sha} + **Files**: ${file_count} changed + deprecation_check: + enabled: true + file: "deprecated_examples.json" +``` + +## Questions? + +Contact the Developer Experience team or open an issue in the [code-example-tooling repository](https://github.com/mongodb/code-example-tooling/issues). + diff --git a/examples-copier/services/main_config_loader_test.go b/examples-copier/services/main_config_loader_test.go index 209e970..7c5cc56 100644 --- a/examples-copier/services/main_config_loader_test.go +++ b/examples-copier/services/main_config_loader_test.go @@ -469,14 +469,16 @@ workflow_configs: assert.Len(t, yamlConfig.Workflows, 2) - // First workflow should have workflow-specific title + // First workflow should have workflow-specific title and inherit type from workflow config defaults workflow1 := yamlConfig.Workflows[0] assert.Equal(t, "workflow-with-override", workflow1.Name) assert.NotNil(t, workflow1.CommitStrategy) assert.Equal(t, "Workflow Specific Title", workflow1.CommitStrategy.PRTitle) - // When commit_strategy is specified at workflow level, it replaces the entire object - // So AutoMerge will be false (default) since it wasn't specified in the workflow-level override - assert.False(t, workflow1.CommitStrategy.AutoMerge) + // Type should be inherited from workflow config defaults + assert.Equal(t, "pull_request", workflow1.CommitStrategy.Type) + // Note: AutoMerge is intentionally NOT inherited to avoid accidentally enabling auto-merge + // when a workflow specifies its own commit_strategy + assert.False(t, workflow1.CommitStrategy.AutoMerge, "AutoMerge should NOT be inherited for safety reasons") // Second workflow should inherit workflow config defaults workflow2 := yamlConfig.Workflows[1] diff --git a/examples-copier/types/config.go b/examples-copier/types/config.go index 940e50f..0a07cce 100644 --- a/examples-copier/types/config.go +++ b/examples-copier/types/config.go @@ -459,6 +459,29 @@ func (c *YAMLConfig) SetDefaults() { // Apply global defaults if not overridden if workflow.CommitStrategy == nil && c.Defaults != nil && c.Defaults.CommitStrategy != nil { workflow.CommitStrategy = c.Defaults.CommitStrategy + } else if workflow.CommitStrategy != nil && c.Defaults != nil && c.Defaults.CommitStrategy != nil { + // Merge individual fields from defaults if not set in workflow + if workflow.CommitStrategy.Type == "" { + workflow.CommitStrategy.Type = c.Defaults.CommitStrategy.Type + } + if workflow.CommitStrategy.CommitMessage == "" { + workflow.CommitStrategy.CommitMessage = c.Defaults.CommitStrategy.CommitMessage + } + if workflow.CommitStrategy.PRTitle == "" { + workflow.CommitStrategy.PRTitle = c.Defaults.CommitStrategy.PRTitle + } + if workflow.CommitStrategy.PRBody == "" { + workflow.CommitStrategy.PRBody = c.Defaults.CommitStrategy.PRBody + } + // For boolean fields, we can't distinguish between "not set" and "false" + // So we only apply defaults if the workflow doesn't have a commit_strategy at all + // This is already handled above, so we don't override UsePRTemplate or AutoMerge here + // unless we want to always inherit them when not explicitly set to true + if !workflow.CommitStrategy.UsePRTemplate && c.Defaults.CommitStrategy.UsePRTemplate { + workflow.CommitStrategy.UsePRTemplate = c.Defaults.CommitStrategy.UsePRTemplate + } + // Note: AutoMerge is intentionally not inherited here to avoid accidentally + // enabling auto-merge when a workflow specifies its own commit_strategy } if workflow.DeprecationCheck == nil && c.Defaults != nil && c.Defaults.DeprecationCheck != nil { @@ -511,6 +534,29 @@ func (w *WorkflowConfig) SetDefaults() { // Apply local defaults if not overridden if workflow.CommitStrategy == nil && w.Defaults != nil && w.Defaults.CommitStrategy != nil { workflow.CommitStrategy = w.Defaults.CommitStrategy + } else if workflow.CommitStrategy != nil && w.Defaults != nil && w.Defaults.CommitStrategy != nil { + // Merge individual fields from defaults if not set in workflow + if workflow.CommitStrategy.Type == "" { + workflow.CommitStrategy.Type = w.Defaults.CommitStrategy.Type + } + if workflow.CommitStrategy.CommitMessage == "" { + workflow.CommitStrategy.CommitMessage = w.Defaults.CommitStrategy.CommitMessage + } + if workflow.CommitStrategy.PRTitle == "" { + workflow.CommitStrategy.PRTitle = w.Defaults.CommitStrategy.PRTitle + } + if workflow.CommitStrategy.PRBody == "" { + workflow.CommitStrategy.PRBody = w.Defaults.CommitStrategy.PRBody + } + // For boolean fields, we can't distinguish between "not set" and "false" + // So we only apply defaults if the workflow doesn't have a commit_strategy at all + // This is already handled above, so we don't override UsePRTemplate or AutoMerge here + // unless we want to always inherit them when not explicitly set to true + if !workflow.CommitStrategy.UsePRTemplate && w.Defaults.CommitStrategy.UsePRTemplate { + workflow.CommitStrategy.UsePRTemplate = w.Defaults.CommitStrategy.UsePRTemplate + } + // Note: AutoMerge is intentionally not inherited here to avoid accidentally + // enabling auto-merge when a workflow specifies its own commit_strategy } if workflow.DeprecationCheck == nil && w.Defaults != nil && w.Defaults.DeprecationCheck != nil { diff --git a/examples-copier/types/config_test.go b/examples-copier/types/config_test.go new file mode 100644 index 0000000..aab0f2f --- /dev/null +++ b/examples-copier/types/config_test.go @@ -0,0 +1,272 @@ +package types + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestYAMLConfig_SetDefaults_CommitStrategyMerging tests that commit strategy +// fields are properly merged from defaults when a workflow has a partial commit_strategy +func TestYAMLConfig_SetDefaults_CommitStrategyMerging(t *testing.T) { + tests := []struct { + name string + defaults *Defaults + workflowCommitStrategy *CommitStrategyConfig + expectedType string + expectedCommitMessage string + expectedPRTitle string + expectedPRBody string + expectedUsePRTemplate bool + expectedAutoMerge bool + }{ + { + name: "workflow with only pr_title should inherit commit_message from defaults", + defaults: &Defaults{ + CommitStrategy: &CommitStrategyConfig{ + Type: "pull_request", + CommitMessage: "Default commit message from ${source_repo}", + PRTitle: "Default PR title", + PRBody: "Default PR body", + UsePRTemplate: true, + AutoMerge: false, + }, + }, + workflowCommitStrategy: &CommitStrategyConfig{ + PRTitle: "Workflow specific PR title", + PRBody: "Workflow specific PR body", + }, + expectedType: "pull_request", + expectedCommitMessage: "Default commit message from ${source_repo}", + expectedPRTitle: "Workflow specific PR title", + expectedPRBody: "Workflow specific PR body", + expectedUsePRTemplate: true, + expectedAutoMerge: false, + }, + { + name: "workflow with pr_title and pr_body should inherit commit_message and use_pr_template", + defaults: &Defaults{ + CommitStrategy: &CommitStrategyConfig{ + Type: "pull_request", + CommitMessage: "Automated update from ${source_repo} PR #${pr_number} (${file_count} files)", + UsePRTemplate: true, + AutoMerge: false, + }, + }, + workflowCommitStrategy: &CommitStrategyConfig{ + PRTitle: "Update MFlix application", + PRBody: "Automated update of MFlix application", + }, + expectedType: "pull_request", + expectedCommitMessage: "Automated update from ${source_repo} PR #${pr_number} (${file_count} files)", + expectedPRTitle: "Update MFlix application", + expectedPRBody: "Automated update of MFlix application", + expectedUsePRTemplate: true, + expectedAutoMerge: false, + }, + { + name: "workflow with all fields should not inherit string fields but may inherit boolean defaults", + defaults: &Defaults{ + CommitStrategy: &CommitStrategyConfig{ + Type: "direct", + CommitMessage: "Default commit message", + PRTitle: "Default PR title", + PRBody: "Default PR body", + UsePRTemplate: true, + AutoMerge: true, + }, + }, + workflowCommitStrategy: &CommitStrategyConfig{ + Type: "pull_request", + CommitMessage: "Workflow commit message", + PRTitle: "Workflow PR title", + PRBody: "Workflow PR body", + UsePRTemplate: false, + AutoMerge: false, + }, + expectedType: "pull_request", + expectedCommitMessage: "Workflow commit message", + expectedPRTitle: "Workflow PR title", + expectedPRBody: "Workflow PR body", + // Note: Due to Go's limitation with boolean zero values, UsePRTemplate=true from defaults + // will be inherited even when workflow explicitly sets it to false + expectedUsePRTemplate: true, + expectedAutoMerge: false, + }, + { + name: "workflow with no commit_strategy should inherit entire defaults", + defaults: &Defaults{ + CommitStrategy: &CommitStrategyConfig{ + Type: "direct", + CommitMessage: "Default commit message", + PRTitle: "Default PR title", + PRBody: "Default PR body", + UsePRTemplate: true, + AutoMerge: true, + }, + }, + workflowCommitStrategy: nil, + expectedType: "direct", + expectedCommitMessage: "Default commit message", + expectedPRTitle: "Default PR title", + expectedPRBody: "Default PR body", + expectedUsePRTemplate: true, + expectedAutoMerge: true, + }, + { + name: "workflow with empty commit_strategy should inherit all fields", + defaults: &Defaults{ + CommitStrategy: &CommitStrategyConfig{ + Type: "pull_request", + CommitMessage: "Default commit message", + PRTitle: "Default PR title", + PRBody: "Default PR body", + UsePRTemplate: true, + AutoMerge: false, + }, + }, + workflowCommitStrategy: &CommitStrategyConfig{}, + expectedType: "pull_request", + expectedCommitMessage: "Default commit message", + expectedPRTitle: "Default PR title", + expectedPRBody: "Default PR body", + expectedUsePRTemplate: true, + expectedAutoMerge: false, + }, + { + name: "workflow with use_pr_template=false will inherit use_pr_template=true due to Go boolean limitation", + defaults: &Defaults{ + CommitStrategy: &CommitStrategyConfig{ + Type: "pull_request", + CommitMessage: "Default commit message", + UsePRTemplate: true, + }, + }, + workflowCommitStrategy: &CommitStrategyConfig{ + PRTitle: "Workflow PR title", + UsePRTemplate: false, + }, + expectedType: "pull_request", + expectedCommitMessage: "Default commit message", + expectedPRTitle: "Workflow PR title", + expectedPRBody: "", + // Note: Due to Go's limitation with boolean zero values, we can't distinguish + // between "not set" and "explicitly set to false", so true from defaults wins + expectedUsePRTemplate: true, + expectedAutoMerge: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + config := &YAMLConfig{ + Defaults: tt.defaults, + Workflows: []Workflow{ + { + Name: "test-workflow", + Source: Source{ + Repo: "mongodb/source-repo", + Branch: "main", + }, + Destination: Destination{ + Repo: "mongodb/dest-repo", + Branch: "main", + }, + Transformations: []Transformation{ + {Move: &MoveTransform{From: "src", To: "dest"}}, + }, + CommitStrategy: tt.workflowCommitStrategy, + }, + }, + } + + // Apply defaults + config.SetDefaults() + + // Verify the workflow's commit strategy has the expected values + workflow := config.Workflows[0] + require.NotNil(t, workflow.CommitStrategy, "CommitStrategy should not be nil after SetDefaults") + + assert.Equal(t, tt.expectedType, workflow.CommitStrategy.Type, "Type mismatch") + assert.Equal(t, tt.expectedCommitMessage, workflow.CommitStrategy.CommitMessage, "CommitMessage mismatch") + assert.Equal(t, tt.expectedPRTitle, workflow.CommitStrategy.PRTitle, "PRTitle mismatch") + assert.Equal(t, tt.expectedPRBody, workflow.CommitStrategy.PRBody, "PRBody mismatch") + assert.Equal(t, tt.expectedUsePRTemplate, workflow.CommitStrategy.UsePRTemplate, "UsePRTemplate mismatch") + assert.Equal(t, tt.expectedAutoMerge, workflow.CommitStrategy.AutoMerge, "AutoMerge mismatch") + }) + } +} + +// TestWorkflowConfig_SetDefaults_CommitStrategyMerging tests that commit strategy +// fields are properly merged from workflow config defaults +func TestWorkflowConfig_SetDefaults_CommitStrategyMerging(t *testing.T) { + workflowConfig := &WorkflowConfig{ + Defaults: &Defaults{ + CommitStrategy: &CommitStrategyConfig{ + Type: "pull_request", + CommitMessage: "Default commit message from ${source_repo}", + PRTitle: "Default PR title", + UsePRTemplate: true, + AutoMerge: false, + }, + }, + Workflows: []Workflow{ + { + Name: "workflow-with-partial-override", + Source: Source{ + Repo: "mongodb/source-repo", + Branch: "main", + }, + Destination: Destination{ + Repo: "mongodb/dest-repo", + Branch: "main", + }, + Transformations: []Transformation{ + {Move: &MoveTransform{From: "src", To: "dest"}}, + }, + CommitStrategy: &CommitStrategyConfig{ + PRTitle: "Workflow specific PR title", + PRBody: "Workflow specific PR body", + }, + }, + { + Name: "workflow-without-override", + Source: Source{ + Repo: "mongodb/source-repo-2", + Branch: "main", + }, + Destination: Destination{ + Repo: "mongodb/dest-repo-2", + Branch: "main", + }, + Transformations: []Transformation{ + {Move: &MoveTransform{From: "a", To: "b"}}, + }, + }, + }, + } + + // Apply defaults + workflowConfig.SetDefaults() + + // First workflow should have merged values + workflow1 := workflowConfig.Workflows[0] + require.NotNil(t, workflow1.CommitStrategy) + assert.Equal(t, "pull_request", workflow1.CommitStrategy.Type) + assert.Equal(t, "Default commit message from ${source_repo}", workflow1.CommitStrategy.CommitMessage) + assert.Equal(t, "Workflow specific PR title", workflow1.CommitStrategy.PRTitle) + assert.Equal(t, "Workflow specific PR body", workflow1.CommitStrategy.PRBody) + assert.True(t, workflow1.CommitStrategy.UsePRTemplate) + assert.False(t, workflow1.CommitStrategy.AutoMerge) + + // Second workflow should inherit all defaults + workflow2 := workflowConfig.Workflows[1] + require.NotNil(t, workflow2.CommitStrategy) + assert.Equal(t, "pull_request", workflow2.CommitStrategy.Type) + assert.Equal(t, "Default commit message from ${source_repo}", workflow2.CommitStrategy.CommitMessage) + assert.Equal(t, "Default PR title", workflow2.CommitStrategy.PRTitle) + assert.True(t, workflow2.CommitStrategy.UsePRTemplate) + assert.False(t, workflow2.CommitStrategy.AutoMerge) +} + From bb047bfa04c562ed356c18814cb203b154b4ea9d Mon Sep 17 00:00:00 2001 From: Cory Bullinger Date: Mon, 1 Dec 2025 15:36:38 -0500 Subject: [PATCH 2/4] feat(copier): update docs --- examples-copier/QUICK-REFERENCE.md | 27 +- .../SOURCE-REPO-README.md | 320 ++++++++++++++++-- 2 files changed, 316 insertions(+), 31 deletions(-) diff --git a/examples-copier/QUICK-REFERENCE.md b/examples-copier/QUICK-REFERENCE.md index eabfc2b..193ed77 100644 --- a/examples-copier/QUICK-REFERENCE.md +++ b/examples-copier/QUICK-REFERENCE.md @@ -83,25 +83,40 @@ workflows: ## Path Transformations +Path transformations are used with **`glob`** and **`regex`** transformation types using the `transform` parameter. + ### Built-in Variables - `${path}` - Full source path - `${filename}` - File name only - `${dir}` - Directory path - `${ext}` - File extension +- `${relative_path}` - Path relative to glob pattern prefix (glob only) -### Examples +### Glob Transformation Examples ```yaml # Keep same path -path_transform: "${path}" +transformations: + - glob: + pattern: "examples/**/*.go" + transform: "${path}" # Change directory -path_transform: "docs/${path}" +transformations: + - glob: + pattern: "examples/**/*.go" + transform: "docs/${relative_path}" -# Reorganize structure -path_transform: "docs/${lang}/${category}/${filename}" +# Reorganize structure (using custom variables from regex) +transformations: + - regex: + pattern: "^examples/(?P[^/]+)/(?P[^/]+)/(?P.+)$" + transform: "docs/${lang}/${category}/${file}" # Change extension -path_transform: "${dir}/${filename}.md" +transformations: + - glob: + pattern: "examples/**/*.txt" + transform: "${dir}/${filename}.md" ``` ## Commit Strategies diff --git a/examples-copier/configs/copier-config-examples/SOURCE-REPO-README.md b/examples-copier/configs/copier-config-examples/SOURCE-REPO-README.md index 38c756d..1a8aa26 100644 --- a/examples-copier/configs/copier-config-examples/SOURCE-REPO-README.md +++ b/examples-copier/configs/copier-config-examples/SOURCE-REPO-README.md @@ -2,6 +2,31 @@ This directory contains workflow configurations for automatically copying code examples to destination repositories. +## For Writers: Quick Overview + +**What is this?** An automated system that copies your code examples to other repositories when you merge a PR. + +**What do I need to know?** +1. When you **merge a PR** in this repo, the copier automatically runs +2. Files are **matched against patterns** in `.copier/workflows.yaml` +3. Matched files are **copied to destination repositories** +4. A **PR is created** in each destination repository (usually) +5. Someone needs to **review and merge** the destination PRs (unless auto-merge is enabled) + +**What happens to deleted files?** +- They are **NOT automatically deleted** from destination repositories +- They are tracked in `deprecated_examples.json` for manual cleanup + +**Do I need to do anything?** +- Usually no! Just merge your PR as normal +- Check destination repositories to ensure PRs are created +- If something goes wrong, contact the DevEx team + +**Where are the logs?** +```bash +gcloud app logs read --limit=100 | grep "your-repo-name" +``` + ## Quick Start ### File Location @@ -22,6 +47,26 @@ workflows: to: "destination/path" ``` +### Important: Path Resolution + +**All file paths in transformations are relative to the repository root**, not to the config file location. + +Even though your config is at `.copier/workflows.yaml`, patterns and paths are matched against the full repository path: + +```yaml +# ✅ Correct - paths from repository root +transformations: + - move: + from: "examples/go" + to: "code" + +# ❌ Wrong - don't use relative paths like ../ +transformations: + - move: + from: "../examples/go" # Don't do this! + to: "code" +``` + ## Adding a New Workflow 1. **Edit `.copier/workflows.yaml`** in your repository @@ -81,29 +126,63 @@ transformations: transform: "code/${relative_path}" ``` -## Customizing PR Details +## Commit Strategies -Add custom PR titles and descriptions: +Choose how files are committed to destination repositories: + +### Pull Request (Recommended) + +Creates a PR in the destination repository for review: ```yaml -workflows: - - name: "my-workflow" - destination: - repo: "mongodb/destination-repo" - branch: "main" - transformations: - - move: { from: "src", to: "dest" } - commit_strategy: - type: "pull_request" - pr_title: "Update examples from ${source_repo}" - pr_body: | - Automated update from source repository. - - Source PR: #${pr_number} - Commit: ${commit_sha} - auto_merge: false +commit_strategy: + type: "pull_request" + pr_title: "Update examples from ${source_repo}" + pr_body: | + Automated update from source repository. + + Source PR: #${pr_number} + Commit: ${commit_sha} + auto_merge: false # Requires manual review and merge +``` + +**When to use:** +- When destination repo requires code review +- When you want to run CI/CD tests before merging +- When changes need approval (most common) + +### Pull Request with Auto-Merge + +Creates a PR and automatically merges it if there are no conflicts: + +```yaml +commit_strategy: + type: "pull_request" + auto_merge: true # Automatically merges if no conflicts +``` + +**When to use:** +- When destination repo has automated tests that must pass +- When you trust the source content completely +- When you want fast propagation with safety checks + +### Direct Commit + +Commits directly to the destination branch (no PR): + +```yaml +commit_strategy: + type: "direct" + commit_message: "Update examples from ${source_repo}" ``` +**When to use:** +- When destination repo doesn't require review +- When you need immediate updates +- When you have full confidence in the source content + +**⚠️ Warning:** Direct commits bypass code review and CI checks! + ## Available Variables Use these variables in PR titles, bodies, and commit messages: @@ -135,11 +214,19 @@ workflows: transformations: - move: { from: "src", to: "dest" } exclude: - - "**/.env" - - "**/node_modules/**" - - "**/*.test.js" + - "**/.env" # Environment files + - "**/node_modules/**" # Dependencies + - "**/*.test.js" # Test files + - "**/.DS_Store" # Mac system files ``` +**Common exclusions:** +- Environment files: `**/.env`, `**/.env.*` +- Dependencies: `**/node_modules/**`, `**/vendor/**` +- Build artifacts: `**/dist/**`, `**/build/**` +- Test files: `**/*.test.*`, `**/tests/**` +- System files: `**/.DS_Store`, `**/Thumbs.db` + ## Setting Defaults Apply settings to all workflows in this file: @@ -186,11 +273,131 @@ Before committing, you can validate your configuration: ## How It Works +### When You Merge a PR + 1. **You merge a PR** in this repository -2. **Copier detects the merge** via webhook -3. **Workflows are matched** based on changed files -4. **Files are copied** to destination repositories -5. **PRs are created** in destination repositories +2. **GitHub sends a webhook** to the copier application +3. **Copier loads your workflows** from `.copier/workflows.yaml` +4. **Files are matched** against transformation patterns +5. **Files are copied** to destination repositories +6. **PRs are created** in destination repositories (or committed directly) + +### What Triggers the Copier? + +**✅ Triggers:** +- Merging a PR (action: `closed` + `merged: true`) + +**❌ Does NOT trigger:** +- Opening a PR +- Updating a PR +- Closing a PR without merging +- Direct commits to main branch (no PR) +- Draft PRs + +### What Happens to Different File Types? + +**Added or Modified Files:** +- Matched against transformation patterns +- Copied to destination repository +- Included in destination PR + +**Deleted Files:** +- Matched against transformation patterns +- Added to deprecation tracking file (if enabled) +- **NOT automatically deleted** from destination +- Manual cleanup required (see Deprecation Tracking below) + +## Understanding Destination PRs + +### What Gets Created in the Destination Repository? + +When you merge a PR in this repository, the copier creates a PR in each destination repository: + +**PR Structure:** +- **Branch name**: `copier/YYYYMMDD-HHMMSS` (e.g., `copier/20240115-143022`) +- **Base branch**: The branch specified in your workflow (usually `main`) +- **Title**: From your `pr_title` configuration +- **Body**: From your `pr_body` configuration +- **Files**: All matched files from your source PR + +**Example:** +``` +Source PR: mongodb/docs-code-examples #123 + ↓ +Destination PR: mongodb/go-examples-repo #45 + Branch: copier/20240115-143022 + Files: 5 files changed + Status: Open (awaiting review) +``` + +### What Happens After the Destination PR is Created? + +**If `auto_merge: false` (default):** +1. PR is created and left open +2. Destination repo maintainers review the PR +3. CI/CD tests run (if configured) +4. Someone manually merges the PR + +**If `auto_merge: true`:** +1. PR is created +2. Copier waits for GitHub to compute mergeability +3. If no conflicts: PR is automatically merged +4. If conflicts: PR is left open for manual resolution +5. Temporary branch is deleted after merge + +**If `type: "direct"`:** +1. No PR is created +2. Files are committed directly to the target branch +3. No review process + +## Deprecation Tracking + +When you delete files from this repository, the copier can track them for cleanup in destination repositories. + +### How It Works + +1. **You delete a file** and merge the PR +2. **Copier detects the deletion** and matches it against patterns +3. **File is added** to `deprecated_examples.json` in this repository +4. **You manually clean up** the file from destination repositories + +### Enable Deprecation Tracking + +```yaml +workflows: + - name: "my-workflow" + destination: + repo: "mongodb/destination-repo" + branch: "main" + transformations: + - move: { from: "examples", to: "code" } + deprecation_check: + enabled: true + file: "deprecated_examples.json" # Optional: custom filename +``` + +### Deprecation File Format + +The deprecation file is stored in **this repository** (source): + +```json +[ + { + "filename": "code/go/old-example.go", + "repo": "mongodb/destination-repo", + "branch": "main", + "deleted_on": "2024-01-15T10:30:00Z" + } +] +``` + +### Cleanup Process + +1. **Review** `deprecated_examples.json` periodically +2. **Create PRs** in destination repositories to remove deprecated files +3. **Remove entries** from `deprecated_examples.json` after cleanup + +**Note:** Files are **NOT automatically deleted** from destination repositories. Deprecation tracking only records what needs to be cleaned up. ## Need Help? @@ -198,6 +405,7 @@ Before committing, you can validate your configuration: - **Configuration Examples**: See `examples-copier/configs/copier-config-examples/` - **Pattern Matching Guide**: See `examples-copier/docs/PATTERN-MATCHING-GUIDE.md` - **Main Config Architecture**: See `examples-copier/configs/copier-config-examples/MAIN-CONFIG-README.md` +- **Deprecation Tracking**: See `examples-copier/docs/DEPRECATION-TRACKING-EXPLAINED.md` ## Example: Complete Workflow @@ -234,6 +442,68 @@ workflows: file: "deprecated_examples.json" ``` +## Troubleshooting + +### My PR merged but files weren't copied + +**Check:** +1. Was it a merged PR? (not just closed) +2. Do the changed files match your transformation patterns? +3. Check the copier logs (see below) +4. Verify `.copier/workflows.yaml` is valid YAML + +### How do I view the logs? + +```bash +# View recent logs +gcloud app logs read --limit=100 + +# Search for your PR +gcloud app logs read --limit=200 | grep "PR #123" + +# Search for your repo +gcloud app logs read --limit=200 | grep "your-repo-name" +``` + +### How do I test my configuration? + +```bash +# Validate YAML syntax +./config-validator validate -config .copier/workflows.yaml + +# Test a pattern match +./config-validator test-pattern \ + -type glob \ + -pattern "examples/**/*.go" \ + -file "examples/database/connect.go" +``` + +### Files are being copied to the wrong location + +**Check:** +- Are your paths relative to repository root? (not relative to config file) +- Is your `transform` pattern correct? +- Test with `config-validator` tool + +### I want to copy to multiple destinations + +Create multiple workflows, one for each destination: + +```yaml +workflows: + - name: "copy-to-docs" + destination: + repo: "mongodb/docs" + transformations: + - move: { from: "examples", to: "code" } + + - name: "copy-to-website" + destination: + repo: "mongodb/website" + transformations: + - move: { from: "examples", to: "static/examples" } +``` + ## Questions? Contact the Developer Experience team or open an issue in the [code-example-tooling repository](https://github.com/mongodb/code-example-tooling/issues). From 617b4a7cc4d836de5ab73af6dbfc0eb5a0c999bf Mon Sep 17 00:00:00 2001 From: Cory Bullinger Date: Wed, 3 Dec 2025 19:33:23 -0500 Subject: [PATCH 3/4] fix(copier): only process PRs merged to branches that match the source.branch in workflow --- .../services/webhook_handler_new.go | 29 +-- .../services/webhook_handler_new_test.go | 183 ++++++++++++++++++ 2 files changed, 200 insertions(+), 12 deletions(-) diff --git a/examples-copier/services/webhook_handler_new.go b/examples-copier/services/webhook_handler_new.go index f75ad97..8d090e7 100644 --- a/examples-copier/services/webhook_handler_new.go +++ b/examples-copier/services/webhook_handler_new.go @@ -166,11 +166,15 @@ func HandleWebhookWithContainer(w http.ResponseWriter, r *http.Request, config * repoOwner := repo.GetOwner().GetLogin() repoName := repo.GetName() + // Extract the base branch (the branch the PR was merged into) + baseBranch := prEvt.GetPullRequest().GetBase().GetRef() + LogInfoCtx(ctx, "processing merged PR", map[string]interface{}{ - "pr_number": prNumber, - "sha": sourceCommitSHA, - "repo": fmt.Sprintf("%s/%s", repoOwner, repoName), - "elapsed_ms": time.Since(startTime).Milliseconds(), + "pr_number": prNumber, + "sha": sourceCommitSHA, + "repo": fmt.Sprintf("%s/%s", repoOwner, repoName), + "base_branch": baseBranch, + "elapsed_ms": time.Since(startTime).Milliseconds(), }) // Respond immediately to avoid GitHub webhook timeout @@ -197,11 +201,11 @@ func HandleWebhookWithContainer(w http.ResponseWriter, r *http.Request, config * // Process asynchronously in background with a new context // Don't use the request context as it will be cancelled when the request completes bgCtx := context.Background() - go handleMergedPRWithContainer(bgCtx, prNumber, sourceCommitSHA, repoOwner, repoName, config, container) + go handleMergedPRWithContainer(bgCtx, prNumber, sourceCommitSHA, repoOwner, repoName, baseBranch, config, container) } // handleMergedPRWithContainer processes a merged PR using the new pattern matching system -func handleMergedPRWithContainer(ctx context.Context, prNumber int, sourceCommitSHA string, repoOwner string, repoName string, config *configs.Config, container *ServiceContainer) { +func handleMergedPRWithContainer(ctx context.Context, prNumber int, sourceCommitSHA string, repoOwner string, repoName string, baseBranch string, config *configs.Config, container *ServiceContainer) { startTime := time.Now() // Configure GitHub permissions @@ -227,18 +231,20 @@ func handleMergedPRWithContainer(ctx context.Context, prNumber int, sourceCommit return } - // Find workflows matching this source repo + // Find workflows matching this source repo and branch webhookRepo := fmt.Sprintf("%s/%s", repoOwner, repoName) - matchingWorkflows := []types.Workflow{} + var matchingWorkflows []types.Workflow for _, workflow := range yamlConfig.Workflows { - if workflow.Source.Repo == webhookRepo { + // Match both repository and branch + if workflow.Source.Repo == webhookRepo && workflow.Source.Branch == baseBranch { matchingWorkflows = append(matchingWorkflows, workflow) } } if len(matchingWorkflows) == 0 { - LogWarningCtx(ctx, "no workflows configured for source repository", map[string]interface{}{ + LogWarningCtx(ctx, "no workflows configured for source repository and branch", map[string]interface{}{ "webhook_repo": webhookRepo, + "base_branch": baseBranch, "workflow_count": len(yamlConfig.Workflows), }) container.MetricsCollector.RecordWebhookFailed() @@ -247,6 +253,7 @@ func handleMergedPRWithContainer(ctx context.Context, prNumber int, sourceCommit LogInfoCtx(ctx, "found matching workflows", map[string]interface{}{ "webhook_repo": webhookRepo, + "base_branch": baseBranch, "matching_count": len(matchingWorkflows), }) @@ -361,5 +368,3 @@ func processFilesWithWorkflows(ctx context.Context, prNumber int, sourceCommitSH "workflow_count": len(yamlConfig.Workflows), }) } - - diff --git a/examples-copier/services/webhook_handler_new_test.go b/examples-copier/services/webhook_handler_new_test.go index 381313d..1f0e129 100644 --- a/examples-copier/services/webhook_handler_new_test.go +++ b/examples-copier/services/webhook_handler_new_test.go @@ -301,6 +301,9 @@ func TestHandleWebhookWithContainer_MergedPR(t *testing.T) { Number: github.Int(123), Merged: github.Bool(true), MergeCommitSHA: github.String("abc123"), + Base: &github.PullRequestBranch{ + Ref: github.String("main"), + }, }, Repo: &github.Repository{ Name: github.String("test-repo"), @@ -334,6 +337,186 @@ func TestHandleWebhookWithContainer_MergedPR(t *testing.T) { // when trying to access GitHub APIs. This is expected and doesn't affect the test result. } +func TestHandleWebhookWithContainer_MergedPRToDevelopmentBranch(t *testing.T) { + // This test verifies that PRs merged to non-main branches (like development) + // are accepted by the webhook handler but won't match any workflows + // (assuming workflows are configured for main branch only) + + // Set up environment variables + os.Setenv(configs.AppId, "123456") + os.Setenv(configs.InstallationId, "789012") + os.Setenv(configs.ConfigRepoOwner, "test-owner") + os.Setenv(configs.ConfigRepoName, "test-repo") + os.Setenv("SKIP_SECRET_MANAGER", "true") + + // Generate a valid RSA private key for testing + key, _ := rsa.GenerateKey(rand.Reader, 1024) + der := x509.MarshalPKCS1PrivateKey(key) + pemBytes := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: der}) + os.Setenv("GITHUB_APP_PRIVATE_KEY", string(pemBytes)) + os.Setenv("GITHUB_APP_PRIVATE_KEY_B64", base64.StdEncoding.EncodeToString(pemBytes)) + + InstallationAccessToken = "test-token" + + config := &configs.Config{ + ConfigRepoOwner: "test-owner", + ConfigRepoName: "test-repo", + ConfigFile: "nonexistent-config.yaml", + AuditEnabled: false, + } + + container, err := NewServiceContainer(config) + if err != nil { + t.Fatalf("NewServiceContainer() error = %v", err) + } + + // Create a merged PR event to development branch + prEvent := &github.PullRequestEvent{ + Action: github.String("closed"), + PullRequest: &github.PullRequest{ + Number: github.Int(456), + Merged: github.Bool(true), + MergeCommitSHA: github.String("def456"), + Base: &github.PullRequestBranch{ + Ref: github.String("development"), + }, + }, + Repo: &github.Repository{ + Name: github.String("test-repo"), + Owner: &github.User{ + Login: github.String("test-owner"), + }, + }, + } + payload, _ := json.Marshal(prEvent) + + req := httptest.NewRequest("POST", "/webhook", bytes.NewReader(payload)) + req.Header.Set("X-GitHub-Event", "pull_request") + + w := httptest.NewRecorder() + + HandleWebhookWithContainer(w, req, config, container) + + // Should still return 202 Accepted (webhook accepts the event) + if w.Code != http.StatusAccepted { + t.Errorf("Status code = %d, want %d", w.Code, http.StatusAccepted) + } + + // Check response body + var response map[string]string + json.Unmarshal(w.Body.Bytes(), &response) + if response["status"] != "accepted" { + t.Errorf("Response status = %v, want accepted", response["status"]) + } + + // Note: The background goroutine will fail to find matching workflows + // because the workflow config specifies main branch, not development. + // This is the expected behavior - the webhook accepts the event but + // no workflows will be processed. +} + +func TestHandleWebhookWithContainer_MergedPRWithDifferentBranches(t *testing.T) { + // This test verifies that the base branch is correctly extracted + // from different PR events + + testCases := []struct { + name string + baseBranch string + prNumber int + }{ + { + name: "main branch", + baseBranch: "main", + prNumber: 100, + }, + { + name: "development branch", + baseBranch: "development", + prNumber: 101, + }, + { + name: "feature branch", + baseBranch: "feature/new-feature", + prNumber: 102, + }, + { + name: "release branch", + baseBranch: "release/v1.0", + prNumber: 103, + }, + } + + // Set up environment variables + os.Setenv(configs.AppId, "123456") + os.Setenv(configs.InstallationId, "789012") + os.Setenv(configs.ConfigRepoOwner, "test-owner") + os.Setenv(configs.ConfigRepoName, "test-repo") + os.Setenv("SKIP_SECRET_MANAGER", "true") + + key, _ := rsa.GenerateKey(rand.Reader, 1024) + der := x509.MarshalPKCS1PrivateKey(key) + pemBytes := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: der}) + os.Setenv("GITHUB_APP_PRIVATE_KEY", string(pemBytes)) + os.Setenv("GITHUB_APP_PRIVATE_KEY_B64", base64.StdEncoding.EncodeToString(pemBytes)) + + InstallationAccessToken = "test-token" + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + config := &configs.Config{ + ConfigRepoOwner: "test-owner", + ConfigRepoName: "test-repo", + ConfigFile: "nonexistent-config.yaml", + AuditEnabled: false, + } + + container, err := NewServiceContainer(config) + if err != nil { + t.Fatalf("NewServiceContainer() error = %v", err) + } + + // Create a merged PR event with specific base branch + prEvent := &github.PullRequestEvent{ + Action: github.String("closed"), + PullRequest: &github.PullRequest{ + Number: github.Int(tc.prNumber), + Merged: github.Bool(true), + MergeCommitSHA: github.String("abc123"), + Base: &github.PullRequestBranch{ + Ref: github.String(tc.baseBranch), + }, + }, + Repo: &github.Repository{ + Name: github.String("test-repo"), + Owner: &github.User{ + Login: github.String("test-owner"), + }, + }, + } + payload, _ := json.Marshal(prEvent) + + req := httptest.NewRequest("POST", "/webhook", bytes.NewReader(payload)) + req.Header.Set("X-GitHub-Event", "pull_request") + + w := httptest.NewRecorder() + + HandleWebhookWithContainer(w, req, config, container) + + // Should return 202 Accepted for all merged PRs + if w.Code != http.StatusAccepted { + t.Errorf("Status code = %d, want %d", w.Code, http.StatusAccepted) + } + + // Check response body + var response map[string]string + json.Unmarshal(w.Body.Bytes(), &response) + if response["status"] != "accepted" { + t.Errorf("Response status = %v, want accepted", response["status"]) + } + }) + } +} + func TestRetrieveFileContentsWithConfigAndBranch(t *testing.T) { // This test would require mocking the GitHub client // For now, we document the expected behavior From 13c2dc6a579c286060f5a7f30501d72df7603d6b Mon Sep 17 00:00:00 2001 From: cory <115956901+cbullinger@users.noreply.github.com> Date: Thu, 4 Dec 2025 09:39:44 -0500 Subject: [PATCH 4/4] Update examples-copier/configs/copier-config-examples/SOURCE-REPO-README.md Co-authored-by: Dachary --- .../configs/copier-config-examples/SOURCE-REPO-README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples-copier/configs/copier-config-examples/SOURCE-REPO-README.md b/examples-copier/configs/copier-config-examples/SOURCE-REPO-README.md index 1a8aa26..0e1ef61 100644 --- a/examples-copier/configs/copier-config-examples/SOURCE-REPO-README.md +++ b/examples-copier/configs/copier-config-examples/SOURCE-REPO-README.md @@ -506,5 +506,5 @@ workflows: ## Questions? -Contact the Developer Experience team or open an issue in the [code-example-tooling repository](https://github.com/mongodb/code-example-tooling/issues). +Contact the Developer Docs team or open an issue in the [code-example-tooling repository](https://github.com/mongodb/code-example-tooling/issues).