|
| 1 | +package main |
| 2 | + |
| 3 | +import ( |
| 4 | + "fmt" |
| 5 | + "io/ioutil" |
| 6 | + "os" |
| 7 | + "path/filepath" |
| 8 | + "strconv" |
| 9 | + "strings" |
| 10 | +) |
| 11 | + |
| 12 | +// incrementPatchVersion takes a version string (e.g., "v1.2.3") and returns the |
| 13 | +// version with the patch number incremented (e.g., "v1.2.4"). It expects a "v" prefix. |
| 14 | +func incrementPatchVersion(version string) (string, error) { |
| 15 | + // Ensure the version starts with 'v' |
| 16 | + if !strings.HasPrefix(version, "v") { |
| 17 | + return "", fmt.Errorf("version must start with 'v', got: %s", version) |
| 18 | + } |
| 19 | + // Remove the "v" prefix for easier parsing |
| 20 | + versionWithoutV := strings.TrimPrefix(version, "v") |
| 21 | + |
| 22 | + parts := strings.Split(versionWithoutV, ".") |
| 23 | + if len(parts) != 3 { |
| 24 | + return "", fmt.Errorf("invalid version format: %s. Expected vX.Y.Z", version) |
| 25 | + } |
| 26 | + |
| 27 | + // Parse major, minor, and patch versions |
| 28 | + major, err := strconv.Atoi(parts[0]) |
| 29 | + if err != nil { |
| 30 | + return "", fmt.Errorf("invalid major version '%s' in %s", parts[0], version) |
| 31 | + } |
| 32 | + minor, err := strconv.Atoi(parts[1]) |
| 33 | + if err != nil { |
| 34 | + return "", fmt.Errorf("invalid minor version '%s' in %s", parts[1], version) |
| 35 | + } |
| 36 | + patch, err := strconv.Atoi(parts[2]) |
| 37 | + if err != nil { |
| 38 | + return "", fmt.Errorf("invalid patch version '%s' in %s", parts[2], version) |
| 39 | + } |
| 40 | + |
| 41 | + // Increment the patch version |
| 42 | + newPatch := patch + 1 |
| 43 | + // Format the new version back with the "v" prefix |
| 44 | + return fmt.Sprintf("v%d.%d.%d", major, minor, newPatch), nil |
| 45 | +} |
| 46 | + |
| 47 | +// prependToFile reads the content of a file, prepends the new content, and writes it back. |
| 48 | +// If the file does not exist, it will be created. |
| 49 | +func prependToFile(filePath, contentToPrepend string) error { |
| 50 | + // Read existing content. If file doesn't exist, ReadFile returns an error |
| 51 | + // but we can proceed if it's an "is not exist" error. |
| 52 | + existingContent, err := ioutil.ReadFile(filePath) |
| 53 | + if err != nil && !os.IsNotExist(err) { |
| 54 | + return fmt.Errorf("error reading file %s: %w", filePath, err) |
| 55 | + } |
| 56 | + |
| 57 | + // Combine new content with existing content, ensuring a newline between them |
| 58 | + // and a final newline at the end for good measure. |
| 59 | + newContent := []byte(contentToPrepend + "\n" + string(existingContent)) |
| 60 | + |
| 61 | + // Write the combined content back to the file. 0644 sets read/write permissions for owner, |
| 62 | + // and read-only for others. |
| 63 | + return ioutil.WriteFile(filePath, newContent, 0644) |
| 64 | +} |
| 65 | + |
| 66 | +func main() { |
| 67 | + // Check if a changelog message is provided as a command-line argument |
| 68 | + if len(os.Args) < 2 { |
| 69 | + fmt.Println("Usage: go run generate.go \"<Your changelog message>\"") |
| 70 | + fmt.Println("Example: go run generate.go \"Fixed a minor bug in user authentication\"") |
| 71 | + os.Exit(1) |
| 72 | + } |
| 73 | + changelogMessage := os.Args[1] // The message for the changelog entry |
| 74 | + |
| 75 | + // Define paths relative to the script's execution directory (expected to be monorepo root) |
| 76 | + rootChangelogPath := "CHANGELOG.md" |
| 77 | + servicesDirPath := "services" |
| 78 | + |
| 79 | + // Get the current working directory to ensure all paths are absolute and correct |
| 80 | + cwd, err := os.Getwd() |
| 81 | + if err != nil { |
| 82 | + fmt.Printf("Error getting current working directory: %v\n", err) |
| 83 | + os.Exit(1) |
| 84 | + } |
| 85 | + |
| 86 | + // Construct the full path to the services directory |
| 87 | + fullServicesDirPath := filepath.Join(cwd, servicesDirPath) |
| 88 | + |
| 89 | + // Read all entries in the services directory |
| 90 | + entries, err := ioutil.ReadDir(fullServicesDirPath) |
| 91 | + if err != nil { |
| 92 | + fmt.Printf("Error reading service directory %s: %v\n", fullServicesDirPath, err) |
| 93 | + os.Exit(1) |
| 94 | + } |
| 95 | + reverseSlice(entries) |
| 96 | + |
| 97 | + fmt.Println("Starting service version and changelog updates...") |
| 98 | + fmt.Println("-------------------------------------------------") |
| 99 | + |
| 100 | + // Iterate over each entry found in the services directory |
| 101 | + for _, entry := range entries { |
| 102 | + // Process only directories that are not hidden (e.g., .git) |
| 103 | + if entry.IsDir() && !strings.HasPrefix(entry.Name(), ".") { |
| 104 | + serviceName := entry.Name() |
| 105 | + servicePath := filepath.Join(fullServicesDirPath, serviceName) |
| 106 | + serviceVersionFilePath := filepath.Join(servicePath, "VERSION") |
| 107 | + serviceChangelogFilePath := filepath.Join(servicePath, "CHANGELOG.md") |
| 108 | + |
| 109 | + fmt.Printf("Processing service: %s\n", serviceName) |
| 110 | + |
| 111 | + // 1. Read the current version from the service's VERSION file |
| 112 | + currentVersionBytes, err := ioutil.ReadFile(serviceVersionFilePath) |
| 113 | + if err != nil { |
| 114 | + fmt.Printf(" Error reading version file for %s (%s): %v. Skipping service.\n", serviceName, serviceVersionFilePath, err) |
| 115 | + continue // Move to the next service |
| 116 | + } |
| 117 | + currentVersion := strings.TrimSpace(string(currentVersionBytes)) |
| 118 | + |
| 119 | + // 2. Increment the patch version |
| 120 | + newVersion, err := incrementPatchVersion(currentVersion) |
| 121 | + if err != nil { |
| 122 | + fmt.Printf(" Error incrementing version for %s (current: %s): %v. Skipping service.\n", serviceName, currentVersion, err) |
| 123 | + continue // Move to the next service |
| 124 | + } |
| 125 | + fmt.Printf(" Version bumped from %s to %s\n", currentVersion, newVersion) |
| 126 | + |
| 127 | + // 3. Write the new version back to the service's VERSION file |
| 128 | + err = ioutil.WriteFile(serviceVersionFilePath, []byte(newVersion), 0644) |
| 129 | + if err != nil { |
| 130 | + fmt.Printf(" Error writing new version to %s: %v. Skipping service.\n", serviceVersionFilePath, err) |
| 131 | + continue // Move to the next service |
| 132 | + } |
| 133 | + |
| 134 | + // 4. Add the new changelog entry to the service's CHANGELOG.md |
| 135 | + serviceChangelogEntry := fmt.Sprintf("## %s\n - %s\n", newVersion, changelogMessage) |
| 136 | + err = prependToFile(serviceChangelogFilePath, serviceChangelogEntry) |
| 137 | + if err != nil { |
| 138 | + fmt.Printf(" Error adding changelog entry to %s: %v. Skipping service.\n", serviceChangelogFilePath, err) |
| 139 | + continue // Move to the next service |
| 140 | + } |
| 141 | + fmt.Printf(" Added changelog entry to %s\n", serviceChangelogFilePath) |
| 142 | + |
| 143 | + // 5. Add the new changelog entry to the root CHANGELOG.md |
| 144 | + rootChangelogEntry := fmt.Sprintf("- `%s`: [%s](services/%s/CHANGELOG.md#%s) \n - %s", serviceName, newVersion, serviceName, strings.ReplaceAll(newVersion, ".", ""), changelogMessage) |
| 145 | + err = prependToFile(filepath.Join(cwd, rootChangelogPath), rootChangelogEntry) |
| 146 | + if err != nil { |
| 147 | + fmt.Printf(" Error adding changelog entry to root %s: %v. Skipping service.\n", filepath.Join(cwd, rootChangelogPath), err) |
| 148 | + continue // Move to the next service |
| 149 | + } |
| 150 | + fmt.Printf(" Added changelog entry to root %s\n", filepath.Join(cwd, rootChangelogPath)) |
| 151 | + fmt.Println("-------------------------------------------------") |
| 152 | + } |
| 153 | + } |
| 154 | + |
| 155 | + fmt.Println("\nScript finished successfully!") |
| 156 | +} |
| 157 | + |
| 158 | +func reverseSlice[T any](s []T) { |
| 159 | + for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 { |
| 160 | + s[i], s[j] = s[j], s[i] |
| 161 | + } |
| 162 | +} |
| 163 | + |
0 commit comments