diff --git a/.github/workflows/module-release.yml b/.github/workflows/module-release.yml index f738a2a0..fb9b1e37 100644 --- a/.github/workflows/module-release.yml +++ b/.github/workflows/module-release.yml @@ -245,6 +245,58 @@ jobs: echo "reason=$REASON" >> $GITHUB_OUTPUT echo "Next version: ${NEXT_VERSION}, tag will be: modules/${MODULE}/${NEXT_VERSION} ($REASON)" + - name: Update go.mod for major version v2+ + if: steps.skipcheck.outputs.changed == 'true' + run: | + set -euo pipefail + MODULE="${{ steps.version.outputs.module }}" + VERSION="${{ steps.version.outputs.next_version }}" + + # Extract major version from VERSION (e.g., v2.0.0 -> 2) + MAJOR_VERSION="${VERSION#v}" + MAJOR_VERSION="${MAJOR_VERSION%%.*}" + + echo "Major version: $MAJOR_VERSION" + + # Only update go.mod if major version is 2 or greater + if [ "$MAJOR_VERSION" -ge 2 ]; then + GO_MOD_PATH="modules/${MODULE}/go.mod" + CURRENT_MODULE_PATH=$(grep "^module " "$GO_MOD_PATH" | awk '{print $2}') + echo "Current module path: $CURRENT_MODULE_PATH" + + # Check if module path already has version suffix + if [[ "$CURRENT_MODULE_PATH" =~ /v[0-9]+$ ]]; then + # Extract current major version from module path + CURRENT_MAJOR="${CURRENT_MODULE_PATH##*/v}" + echo "Current module path has version suffix: v$CURRENT_MAJOR" + + if [ "$CURRENT_MAJOR" -ne "$MAJOR_VERSION" ]; then + echo "ERROR: Module path has /v${CURRENT_MAJOR} but releasing v${MAJOR_VERSION}" + echo "Please manually update module path in ${GO_MOD_PATH} to include /v${MAJOR_VERSION}" + exit 1 + fi + echo "Module path already correct for v${MAJOR_VERSION}" + else + # No version suffix, need to add it + NEW_MODULE_PATH="${CURRENT_MODULE_PATH}/v${MAJOR_VERSION}" + echo "Updating module path to: $NEW_MODULE_PATH" + + # Update the module path in go.mod + sed -i "s|^module ${CURRENT_MODULE_PATH}|module ${NEW_MODULE_PATH}|" "$GO_MOD_PATH" + + # Commit the change + git config user.name 'github-actions[bot]' + git config user.email 'github-actions[bot]@users.noreply.github.com' + git add "$GO_MOD_PATH" + git commit -m "chore(${MODULE}): update module path for v${MAJOR_VERSION}" + git push origin HEAD:${{ github.ref_name }} + + echo "✓ Updated module path to $NEW_MODULE_PATH" + fi + else + echo "Major version is $MAJOR_VERSION (< 2), no module path update needed" + fi + - name: Generate changelog if: steps.skipcheck.outputs.changed == 'true' run: | @@ -336,10 +388,23 @@ jobs: - name: Announce to Go proxy if: steps.skipcheck.outputs.changed == 'true' run: | + set -euo pipefail VERSION=${{ steps.version.outputs.next_version }} - MODULE_NAME="github.com/CrisisTextLine/modular/modules/${{ steps.version.outputs.module }}" + MODULE="${{ steps.version.outputs.module }}" + + # Extract major version from VERSION + MAJOR_VERSION="${VERSION#v}" + MAJOR_VERSION="${MAJOR_VERSION%%.*}" + + # Construct correct module path with version suffix for v2+ + if [ "$MAJOR_VERSION" -ge 2 ]; then + MODULE_NAME="github.com/CrisisTextLine/modular/modules/${MODULE}/v${MAJOR_VERSION}" + else + MODULE_NAME="github.com/CrisisTextLine/modular/modules/${MODULE}" + fi - go get ${MODULE_NAME}@${VERSION} + echo "Announcing ${MODULE_NAME}@${VERSION} to Go proxy..." + GOPROXY=proxy.golang.org go list -m ${MODULE_NAME}@${VERSION} - echo "Announced version ${{steps.version.outputs.module}}@${VERSION} to Go proxy" + echo "✓ Announced version ${MODULE}@${VERSION} to Go proxy" diff --git a/.github/workflows/release-all.yml b/.github/workflows/release-all.yml index 57744d92..34eb1771 100644 --- a/.github/workflows/release-all.yml +++ b/.github/workflows/release-all.yml @@ -323,9 +323,22 @@ jobs: - name: Re-announce to Go proxy if: steps.ensure.outputs.release_url run: | + set -euo pipefail CURR=$(git tag -l 'v*' | grep -v '/' | sort -V | tail -n1 || true) [ -z "$CURR" ] && exit 0 - GOPROXY=proxy.golang.org go list -m github.com/CrisisTextLine/modular@${CURR} + + # Extract major version + MAJOR_VERSION="${CURR#v}" + MAJOR_VERSION="${MAJOR_VERSION%%.*}" + + # Construct correct module path with version suffix for v2+ + if [ "$MAJOR_VERSION" -ge 2 ]; then + MODULE_NAME="github.com/CrisisTextLine/modular/v${MAJOR_VERSION}" + else + MODULE_NAME="github.com/CrisisTextLine/modular" + fi + + GOPROXY=proxy.golang.org go list -m ${MODULE_NAME}@${CURR} ensure-modules: needs: detect @@ -359,7 +372,16 @@ jobs: TAG=$(git tag -l "modules/${M}/v*" | sort -V | tail -n1 || true) [ -z "$TAG" ] && { echo "Module $M has no tags yet; skipping."; continue; } VER=${TAG##*/} - MOD_PATH="github.com/CrisisTextLine/modular/modules/${M}" + + # Extract major version and construct correct module path + MAJOR_VERSION="${VER#v}" + MAJOR_VERSION="${MAJOR_VERSION%%.*}" + if [ "$MAJOR_VERSION" -ge 2 ]; then + MOD_PATH="github.com/CrisisTextLine/modular/modules/${M}/v${MAJOR_VERSION}" + else + MOD_PATH="github.com/CrisisTextLine/modular/modules/${M}" + fi + if gh release view "$TAG" >/dev/null 2>&1; then echo "Release exists for $TAG" URL=$(gh release view "$TAG" --json url --jq .url || echo '') diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index afb210e5..051f9e3f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -198,6 +198,57 @@ jobs: echo "reason=$REASON" >> $GITHUB_OUTPUT echo "Next version: $NEXT_VERSION ($REASON)" + - name: Update go.mod for major version v2+ + if: steps.detect.outputs.core_changed == 'true' + run: | + set -euo pipefail + VERSION="${{ steps.version.outputs.next_version }}" + + # Extract major version from VERSION (e.g., v2.0.0 -> 2) + MAJOR_VERSION="${VERSION#v}" + MAJOR_VERSION="${MAJOR_VERSION%%.*}" + + echo "Major version: $MAJOR_VERSION" + + # Only update go.mod if major version is 2 or greater + if [ "$MAJOR_VERSION" -ge 2 ]; then + GO_MOD_PATH="go.mod" + CURRENT_MODULE_PATH=$(grep "^module " "$GO_MOD_PATH" | awk '{print $2}') + echo "Current module path: $CURRENT_MODULE_PATH" + + # Check if module path already has version suffix + if [[ "$CURRENT_MODULE_PATH" =~ /v[0-9]+$ ]]; then + # Extract current major version from module path + CURRENT_MAJOR="${CURRENT_MODULE_PATH##*/v}" + echo "Current module path has version suffix: v$CURRENT_MAJOR" + + if [ "$CURRENT_MAJOR" -ne "$MAJOR_VERSION" ]; then + echo "ERROR: Module path has /v${CURRENT_MAJOR} but releasing v${MAJOR_VERSION}" + echo "Please manually update module path in ${GO_MOD_PATH} to include /v${MAJOR_VERSION}" + exit 1 + fi + echo "Module path already correct for v${MAJOR_VERSION}" + else + # No version suffix, need to add it + NEW_MODULE_PATH="${CURRENT_MODULE_PATH}/v${MAJOR_VERSION}" + echo "Updating module path to: $NEW_MODULE_PATH" + + # Update the module path in go.mod + sed -i "s|^module ${CURRENT_MODULE_PATH}|module ${NEW_MODULE_PATH}|" "$GO_MOD_PATH" + + # Commit the change + git config user.name 'github-actions[bot]' + git config user.email 'github-actions[bot]@users.noreply.github.com' + git add "$GO_MOD_PATH" + git commit -m "chore: update module path for v${MAJOR_VERSION}" + git push origin HEAD:${{ github.ref_name }} + + echo "✓ Updated module path to $NEW_MODULE_PATH" + fi + else + echo "Major version is $MAJOR_VERSION (< 2), no module path update needed" + fi + - name: Run tests if: steps.detect.outputs.core_changed == 'true' run: | @@ -290,10 +341,23 @@ jobs: - name: Announce to Go proxy if: steps.detect.outputs.core_changed == 'true' run: | + set -euo pipefail VERSION=${{ steps.version.outputs.next_version }} - MODULE_NAME="github.com/CrisisTextLine/modular" + + # Extract major version from VERSION + MAJOR_VERSION="${VERSION#v}" + MAJOR_VERSION="${MAJOR_VERSION%%.*}" + + # Construct correct module path with version suffix for v2+ + if [ "$MAJOR_VERSION" -ge 2 ]; then + MODULE_NAME="github.com/CrisisTextLine/modular/v${MAJOR_VERSION}" + else + MODULE_NAME="github.com/CrisisTextLine/modular" + fi + + echo "Announcing ${MODULE_NAME}@${VERSION} to Go proxy..." GOPROXY=proxy.golang.org go list -m ${MODULE_NAME}@${VERSION} - echo "Announced version ${VERSION} to Go proxy" + echo "✓ Announced version ${VERSION} to Go proxy" bump-modules: needs: release diff --git a/GO_MODULE_VERSIONING.md b/GO_MODULE_VERSIONING.md new file mode 100644 index 00000000..0e541347 --- /dev/null +++ b/GO_MODULE_VERSIONING.md @@ -0,0 +1,188 @@ +# Go Module Versioning Guide + +This document explains how the Modular framework handles Go semantic versioning for the core framework and its modules. + +## Overview + +Go modules follow semantic versioning (semver) with a special requirement for major versions 2 and above: the module path in `go.mod` must include a version suffix (e.g., `/v2`, `/v3`). + +## Version Naming Rules + +### For v0.x.x and v1.x.x + +**No module path suffix is required.** + +```go +// go.mod +module github.com/CrisisTextLine/modular +``` + +Tags: +- Core: `v1.0.0`, `v1.1.0`, `v1.2.3` +- Modules: `modules/reverseproxy/v1.0.0`, `modules/auth/v1.2.3` + +### For v2.x.x and Higher + +**Module path MUST include the `/vN` suffix.** + +```go +// go.mod for v2 +module github.com/CrisisTextLine/modular/v2 +``` + +Tags: +- Core: `v2.0.0`, `v2.1.0`, `v3.0.0` +- Modules: `modules/reverseproxy/v2.0.0`, `modules/auth/v3.1.0` + +## Automated Workflow Behavior + +The release workflows automatically handle major version transitions: + +### When Releasing v2.0.0 or Higher + +1. **Version Determination**: The workflow calculates the next version based on contract changes and user input +2. **Module Path Validation**: Before creating a release: + - Checks if releasing v2+ version + - Validates current module path in `go.mod` + - If no version suffix exists, adds `/vN` to the module path + - If version suffix exists but doesn't match, fails with error +3. **Auto-Update**: If needed, updates `go.mod` and commits the change +4. **Release Creation**: Creates the GitHub release with the correct tag +5. **Go Proxy Announcement**: Announces to Go proxy using the correct module path + +### Example Workflow + +**Initial State (v1.x.x):** +```go +// modules/reverseproxy/go.mod +module github.com/CrisisTextLine/modular/modules/reverseproxy +``` + +**After Releasing v2.0.0:** +```go +// modules/reverseproxy/go.mod +module github.com/CrisisTextLine/modular/modules/reverseproxy/v2 +``` + +The workflow: +1. Detects that next version is v2.0.0 +2. Updates `go.mod` module path to include `/v2` +3. Commits the change +4. Creates tag `modules/reverseproxy/v2.0.0` +5. Announces `github.com/CrisisTextLine/modular/modules/reverseproxy/v2@v2.0.0` to Go proxy + +## Manual Version Updates + +If you need to manually prepare for a v2+ release: + +### Core Framework + +```bash +# 1. Update go.mod +sed -i 's|^module github.com/CrisisTextLine/modular$|module github.com/CrisisTextLine/modular/v2|' go.mod + +# 2. Update import paths in all .go files (if any self-imports) +find . -name "*.go" -type f -not -path "*/modules/*" -not -path "*/examples/*" \ + -exec sed -i 's|github.com/CrisisTextLine/modular"|github.com/CrisisTextLine/modular/v2"|g' {} + + +# 3. Run go mod tidy +go mod tidy + +# 4. Test +go test ./... +``` + +### Module + +```bash +MODULE_NAME="reverseproxy" # Change this to your module name +MAJOR_VERSION="2" # Change to your target major version + +# 1. Update go.mod +sed -i "s|^module github.com/CrisisTextLine/modular/modules/${MODULE_NAME}$|module github.com/CrisisTextLine/modular/modules/${MODULE_NAME}/v${MAJOR_VERSION}|" \ + modules/${MODULE_NAME}/go.mod + +# 2. Update import paths (if module has self-imports - rare) +find modules/${MODULE_NAME} -name "*.go" -type f \ + -exec sed -i "s|github.com/CrisisTextLine/modular/modules/${MODULE_NAME}\"|github.com/CrisisTextLine/modular/modules/${MODULE_NAME}/v${MAJOR_VERSION}\"|g" {} + + +# 3. Run go mod tidy +cd modules/${MODULE_NAME} +go mod tidy + +# 4. Test +go test ./... +``` + +## Importing v2+ Modules + +When using v2+ versions in your code: + +```go +// For v1.x.x +import "github.com/CrisisTextLine/modular/modules/reverseproxy" + +// For v2.x.x +import "github.com/CrisisTextLine/modular/modules/reverseproxy/v2" + +// For v3.x.x +import "github.com/CrisisTextLine/modular/modules/reverseproxy/v3" +``` + +In `go.mod`: +```go +require ( + github.com/CrisisTextLine/modular/v2 v2.0.0 + github.com/CrisisTextLine/modular/modules/reverseproxy/v2 v2.0.0 +) +``` + +## Breaking Changes and Major Versions + +According to semantic versioning: +- **Breaking changes** require a major version bump (e.g., v1.5.0 → v2.0.0) +- **Backward-compatible additions** require a minor version bump (e.g., v1.5.0 → v1.6.0) +- **Bug fixes** require a patch version bump (e.g., v1.5.0 → v1.5.1) + +Our workflows use contract-based detection to suggest appropriate version bumps, but you can override this with manual version input or by selecting a different release type. + +## Troubleshooting + +### Error: "module contains a go.mod file, so module path must match major version" + +This error occurs when: +1. You're trying to release v2.0.0 or higher +2. The module path in `go.mod` doesn't include the `/vN` suffix + +**Solution**: The workflow should handle this automatically. If you see this error, it means the auto-update step failed. Manually update the module path as described above. + +### Error: "Module path has /vX but releasing vY" + +This error occurs when the module path already has a version suffix, but it doesn't match the version you're trying to release. + +**Solution**: Either: +1. Update the version number to match the existing suffix, or +2. Manually update the module path suffix to match your target version + +### Downgrading Major Versions + +**You cannot downgrade major versions** (e.g., from v3 to v2). If you need to maintain an older major version: +1. Create a branch from the appropriate tag (e.g., `v2-maintenance` from `v2.5.0`) +2. Apply fixes to that branch +3. Release patch versions on that branch (e.g., v2.5.1, v2.5.2) + +## References + +- [Go Modules: v2 and Beyond](https://go.dev/blog/v2-go-modules) +- [Go Module Reference](https://go.dev/ref/mod) +- [Semantic Versioning](https://semver.org/) + +## Testing + +To test the version handling logic locally, run: + +```bash +./scripts/test-version-handling.sh +``` + +This demonstrates how versions are mapped to module paths. diff --git a/README.md b/README.md index b773286e..65f1b918 100644 --- a/README.md +++ b/README.md @@ -754,7 +754,8 @@ Each command includes interactive prompts to guide you through the process of cr - **[Debugging and Troubleshooting](DOCUMENTATION.md#debugging-and-troubleshooting)** - Diagnostic tools and solutions for common issues - **[Available Modules](modules/README.md)** - Complete list of pre-built modules with documentation - **[Examples](examples/)** - Working example applications demonstrating various features - - **[Concurrency & Race Guidelines](CONCURRENCY_GUIDELINES.md)** - Official synchronization patterns, race detector usage, and safe module design +- **[Concurrency & Race Guidelines](CONCURRENCY_GUIDELINES.md)** - Official synchronization patterns, race detector usage, and safe module design +- **[Go Module Versioning Guide](GO_MODULE_VERSIONING.md)** - Understanding semantic versioning, v2+ module paths, and the automated release process ### Having Issues? diff --git a/application_issue_reproduction_test.go b/application_issue_reproduction_test.go index db92807b..1e4f90e7 100644 --- a/application_issue_reproduction_test.go +++ b/application_issue_reproduction_test.go @@ -16,39 +16,39 @@ import ( func Test_ReproduceIssue_LoggerCachingProblem_WithoutFix(t *testing.T) { // This test demonstrates the OLD problem (for documentation purposes) // We'll manually verify the issue exists without the hook - + type AppConfig struct { LogFormat string `yaml:"logFormat" default:"text"` } - + config := &AppConfig{LogFormat: "text"} - + // 1. Create initial logger (text format) var textLogOutput strings.Builder initialLogger := slog.New(slog.NewTextHandler(&textLogOutput, nil)) - + app := NewStdApplication(NewStdConfigProvider(config), initialLogger) - + // 2. Create a test module that caches logger testModule := &TestModuleWithCachedLogger{} app.RegisterModule(testModule) - + // 3. Initialize WITHOUT hook - module gets text logger if err := app.Init(); err != nil { t.Fatalf("Init failed: %v", err) } - + // 4. AFTER init, try to reconfigure logger (simulating old approach) var jsonLogOutput strings.Builder newLogger := slog.New(slog.NewJSONHandler(&jsonLogOutput, nil)) app.SetLogger(newLogger) - + // 5. Problem: Module still has the old text logger cached! // The module's cached logger is the initial one, not the new one if testModule.logger != initialLogger { t.Error("Expected module to have cached the initial logger (demonstrating the problem)") } - + if testModule.logger == newLogger { t.Error("Module should NOT have the new logger when reconfigured after Init (this is the problem)") } @@ -57,27 +57,27 @@ func Test_ReproduceIssue_LoggerCachingProblem_WithoutFix(t *testing.T) { // Test_SolutionWithOnConfigLoaded_CompleteScenario tests the complete solution func Test_SolutionWithOnConfigLoaded_CompleteScenario(t *testing.T) { // This test verifies the SOLUTION works correctly - + type AppConfig struct { LogFormat string `yaml:"logFormat" default:"text"` } - + config := &AppConfig{LogFormat: "json"} // Config says use JSON - + // 1. Create initial logger (text format) - will be replaced var textLogOutput strings.Builder initialLogger := slog.New(slog.NewTextHandler(&textLogOutput, nil)) - + app := NewStdApplication(NewStdConfigProvider(config), initialLogger) - + // 2. Create a test module that caches logger testModule := &TestModuleWithCachedLogger{} app.RegisterModule(testModule) - + // 3. Register hook to reconfigure logger BEFORE module init var jsonLogOutput strings.Builder newLogger := slog.New(slog.NewJSONHandler(&jsonLogOutput, nil)) - + app.OnConfigLoaded(func(app Application) error { cfg := app.ConfigProvider().GetConfig().(*AppConfig) if cfg.LogFormat == "json" { @@ -85,21 +85,21 @@ func Test_SolutionWithOnConfigLoaded_CompleteScenario(t *testing.T) { } return nil }) - + // 4. Initialize - hook runs, then module gets the NEW logger if err := app.Init(); err != nil { t.Fatalf("Init failed: %v", err) } - + // 5. Verify: Module has the NEW logger, not the initial one if testModule.logger == initialLogger { t.Error("Module should NOT have the initial logger when hook reconfigures it") } - + if testModule.logger != newLogger { t.Error("Module should have the reconfigured logger from the hook") } - + // 6. Verify app.Logger() also returns the new logger if app.Logger() != newLogger { t.Error("Application should return the reconfigured logger") @@ -115,23 +115,23 @@ func Test_CompleteWorkflow_FromProblemStatement(t *testing.T) { t.Fatal(err) } defer os.Remove(tmpfile.Name()) - + if _, err := tmpfile.Write(configContent); err != nil { t.Fatal(err) } if err := tmpfile.Close(); err != nil { t.Fatal(err) } - + type AppConfig struct { LogFormat string `yaml:"logFormat" default:"text"` } - + // 1. Create initial logger with default settings (text format) initialLogger := &MockLogger{} initialLogger.On("Debug", mock.Anything, mock.Anything).Return() initialLogger.On("Info", mock.Anything, mock.Anything).Return() - + // 2. Create application with initial logger app, err := NewApplication( WithLogger(initialLogger), @@ -150,43 +150,43 @@ func Test_CompleteWorkflow_FromProblemStatement(t *testing.T) { }), WithModules(&TestModuleWithCachedLogger{}), ) - + if err != nil { t.Fatalf("NewApplication failed: %v", err) } - + // Set up feeders to load from config file if stdApp, ok := app.(*StdApplication); ok { stdApp.SetConfigFeeders([]Feeder{ feeders.NewYamlFeeder(tmpfile.Name()), }) } - + // 4. Init - this loads config, runs hook, then initializes modules if err := app.Init(); err != nil { t.Fatalf("Init failed: %v", err) } - + // 5. Verify the logger was reconfigured if app.Logger() == initialLogger { t.Error("Logger should have been reconfigured from initial logger") } - + // Get the module and verify it has the reconfigured logger module := app.GetModule("test_module_with_logger") if module == nil { t.Fatal("Module not found") } - + cachedModule, ok := module.(*TestModuleWithCachedLogger) if !ok { t.Fatal("Module is not the expected type") } - + if cachedModule.logger == initialLogger { t.Error("Module should have the reconfigured logger, not the initial one") } - + if cachedModule.logger == nil { t.Error("Module logger should not be nil") } @@ -197,18 +197,18 @@ func Test_MultipleReconfigurationsInHooks(t *testing.T) { logger := &MockLogger{} logger.On("Debug", mock.Anything, mock.Anything).Return() logger.On("Info", mock.Anything, mock.Anything).Return() - + type AppConfig struct { Feature1 bool `yaml:"feature1" default:"true"` Feature2 bool `yaml:"feature2" default:"true"` } - + config := &AppConfig{} app := NewStdApplication(NewStdConfigProvider(config), logger) - + // Track what hooks executed var executedHooks []string - + // First hook: configure feature 1 app.OnConfigLoaded(func(app Application) error { executedHooks = append(executedHooks, "feature1") @@ -219,7 +219,7 @@ func Test_MultipleReconfigurationsInHooks(t *testing.T) { } return nil }) - + // Second hook: configure feature 2 app.OnConfigLoaded(func(app Application) error { executedHooks = append(executedHooks, "feature2") @@ -230,20 +230,20 @@ func Test_MultipleReconfigurationsInHooks(t *testing.T) { } return nil }) - + if err := app.Init(); err != nil { t.Fatalf("Init failed: %v", err) } - + // Verify both hooks executed in order if len(executedHooks) != 2 { t.Errorf("Expected 2 hooks to execute, got %d", len(executedHooks)) } - + if executedHooks[0] != "feature1" || executedHooks[1] != "feature2" { t.Errorf("Hooks executed in wrong order: %v", executedHooks) } - + // Verify both services were registered var service1, service2 string if err := app.GetService("feature1", &service1); err != nil { @@ -259,10 +259,10 @@ func Test_ErrorInHook_PreventsModuleInit(t *testing.T) { logger := &MockLogger{} logger.On("Debug", mock.Anything, mock.Anything).Return() logger.On("Info", mock.Anything, mock.Anything).Return() - + config := &struct{}{} app := NewStdApplication(NewStdConfigProvider(config), logger) - + module := &ConfigLoadedTestModule{ name: "test", initFunc: func(app Application) error { @@ -270,23 +270,23 @@ func Test_ErrorInHook_PreventsModuleInit(t *testing.T) { }, } app.RegisterModule(module) - + // Register a hook that fails app.OnConfigLoaded(func(app Application) error { return fmt.Errorf("hook failed intentionally") }) - + // Init should fail due to hook error err := app.Init() if err == nil { t.Fatal("Expected Init to fail when hook returns error") } - + // Verify the error message mentions the hook failure if !containsSubstring(err.Error(), "config loaded hook") { t.Errorf("Error should mention config loaded hook failure: %v", err) } - + // Note: Due to error collection strategy in Init, modules may still initialize // but the overall Init will return an error. This is consistent with how // the framework handles other initialization errors. diff --git a/scripts/test-version-handling.sh b/scripts/test-version-handling.sh new file mode 100755 index 00000000..6be3fedd --- /dev/null +++ b/scripts/test-version-handling.sh @@ -0,0 +1,61 @@ +#!/bin/bash +# Test script for validating Go module version handling +# This demonstrates the logic added to the release workflows + +set -euo pipefail + +echo "=== Go Module Version Handling Test ===" +echo + +# Function to extract major version from a version string +extract_major_version() { + local version="$1" + local major="${version#v}" + major="${major%%.*}" + echo "$major" +} + +# Function to determine correct module path for a version +get_module_path() { + local base_path="$1" + local version="$2" + local major + major=$(extract_major_version "$version") + + if [ "$major" -ge 2 ]; then + echo "${base_path}/v${major}" + else + echo "${base_path}" + fi +} + +# Test cases +test_version() { + local version="$1" + local base_path="github.com/CrisisTextLine/modular/modules/reverseproxy" + local expected_path + expected_path=$(get_module_path "$base_path" "$version") + + echo "Version: $version" + echo " Major: $(extract_major_version "$version")" + echo " Module Path: $expected_path" + echo +} + +echo "--- Test Cases ---" +test_version "v0.1.0" +test_version "v1.0.0" +test_version "v1.5.2" +test_version "v2.0.0" +test_version "v2.1.0" +test_version "v3.0.0" + +echo "=== Core Framework Test ===" +echo +base_path="github.com/CrisisTextLine/modular" +for version in "v1.0.0" "v2.0.0" "v3.0.0"; do + echo "Version: $version -> $(get_module_path "$base_path" "$version")" +done + +echo +echo "✓ All test cases executed successfully"