|
| 1 | +// Copyright 2026 Google LLC |
| 2 | +// |
| 3 | +// Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +// you may not use this file except in compliance with the License. |
| 5 | +// You may obtain a copy of the License at |
| 6 | +// |
| 7 | +// https://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +// |
| 9 | +// Unless required by applicable law or agreed to in writing, software |
| 10 | +// distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +// See the License for the specific language governing permissions and |
| 13 | +// limitations under the License. |
| 14 | + |
| 15 | +// Package librarianops provides orchestration for running librarian across |
| 16 | +// multiple repositories. |
| 17 | +package librarianops |
| 18 | + |
| 19 | +import ( |
| 20 | + "context" |
| 21 | + "fmt" |
| 22 | + "os" |
| 23 | + "time" |
| 24 | + |
| 25 | + "github.com/googleapis/librarian/internal/command" |
| 26 | + "github.com/googleapis/librarian/internal/librarian" |
| 27 | + "github.com/urfave/cli/v3" |
| 28 | +) |
| 29 | + |
| 30 | +const ( |
| 31 | + repoRust = "google-cloud-rust" |
| 32 | + repoFake = "fake-repo" // used for testing |
| 33 | + |
| 34 | + branchPrefix = "librarianops-generateall-" |
| 35 | + commitTitle = "chore: run librarian update and generate --all" |
| 36 | +) |
| 37 | + |
| 38 | +var supportedRepositories = map[string]bool{ |
| 39 | + repoRust: true, |
| 40 | + repoFake: true, // used for testing |
| 41 | +} |
| 42 | + |
| 43 | +// Run executes the librarianops command with the given arguments. |
| 44 | +func Run(ctx context.Context, args ...string) error { |
| 45 | + cmd := &cli.Command{ |
| 46 | + Name: "librarianops", |
| 47 | + Usage: "orchestrate librarian operations across multiple repositories", |
| 48 | + UsageText: "librarianops [command]", |
| 49 | + Commands: []*cli.Command{ |
| 50 | + generateCommand(), |
| 51 | + }, |
| 52 | + } |
| 53 | + return cmd.Run(ctx, args) |
| 54 | +} |
| 55 | + |
| 56 | +func generateCommand() *cli.Command { |
| 57 | + return &cli.Command{ |
| 58 | + Name: "generate", |
| 59 | + Usage: "generate libraries across repositories", |
| 60 | + UsageText: "librarianops generate [<repo> | --all]", |
| 61 | + Description: `Examples: |
| 62 | + librarianops generate google-cloud-rust |
| 63 | + librarianops generate --all |
| 64 | + librarianops generate -C ~/workspace/google-cloud-rust google-cloud-rust |
| 65 | +
|
| 66 | +Specify a repository name (e.g., google-cloud-rust) to process a single repository, |
| 67 | +or use --all to process all repositories. |
| 68 | +
|
| 69 | +Use -C to work in a specific directory (assumes repository already exists there). |
| 70 | +
|
| 71 | +For each repository, librarianops will: |
| 72 | + 1. Clone the repository to a temporary directory |
| 73 | + 2. Create a branch: librarianops-generateall-YYYY-MM-DD |
| 74 | + 3. Run librarian update discovery (google-cloud-rust only) |
| 75 | + 4. Run librarian update googleapis |
| 76 | + 5. Run librarian generate --all |
| 77 | + 6. Run cargo update --workspace (google-cloud-rust only) |
| 78 | + 7. Commit changes |
| 79 | + 8. Create a pull request (pushes branch automatically)`, |
| 80 | + Flags: []cli.Flag{ |
| 81 | + &cli.BoolFlag{ |
| 82 | + Name: "all", |
| 83 | + Usage: "process all repositories", |
| 84 | + }, |
| 85 | + &cli.StringFlag{ |
| 86 | + Name: "C", |
| 87 | + Usage: "work in `directory` (assumes repo exists)", |
| 88 | + }, |
| 89 | + }, |
| 90 | + Action: func(ctx context.Context, cmd *cli.Command) error { |
| 91 | + all := cmd.Bool("all") |
| 92 | + workDir := cmd.String("C") |
| 93 | + repoName := "" |
| 94 | + if cmd.Args().Len() > 0 { |
| 95 | + repoName = cmd.Args().Get(0) |
| 96 | + } |
| 97 | + if all && repoName != "" { |
| 98 | + return fmt.Errorf("cannot specify both <repo> and --all") |
| 99 | + } |
| 100 | + if !all && repoName == "" { |
| 101 | + return fmt.Errorf("usage: librarianops generate [<repo> | --all]") |
| 102 | + } |
| 103 | + if all && workDir != "" { |
| 104 | + return fmt.Errorf("cannot use -C with --all") |
| 105 | + } |
| 106 | + return runGenerate(ctx, all, repoName, workDir) |
| 107 | + }, |
| 108 | + } |
| 109 | +} |
| 110 | + |
| 111 | +func runGenerate(ctx context.Context, all bool, repoName, repoDir string) error { |
| 112 | + if all { |
| 113 | + for name := range supportedRepositories { |
| 114 | + if err := processRepo(ctx, name, ""); err != nil { |
| 115 | + return err |
| 116 | + } |
| 117 | + } |
| 118 | + return nil |
| 119 | + } |
| 120 | + |
| 121 | + if !supportedRepositories[repoName] { |
| 122 | + return fmt.Errorf("repository %q not found in supported repositories list", repoName) |
| 123 | + } |
| 124 | + if err := processRepo(ctx, repoName, repoDir); err != nil { |
| 125 | + return err |
| 126 | + } |
| 127 | + return nil |
| 128 | +} |
| 129 | + |
| 130 | +func processRepo(ctx context.Context, repoName, repoDir string) (err error) { |
| 131 | + if repoDir == "" { |
| 132 | + repoDir, err = os.MkdirTemp("", "librarianops-"+repoName+"-*") |
| 133 | + if err != nil { |
| 134 | + return fmt.Errorf("failed to create temp directory: %w", err) |
| 135 | + } |
| 136 | + defer func() { |
| 137 | + cerr := os.RemoveAll(repoDir) |
| 138 | + if err == nil { |
| 139 | + err = cerr |
| 140 | + } |
| 141 | + }() |
| 142 | + if err := cloneRepo(ctx, repoDir, repoName); err != nil { |
| 143 | + return err |
| 144 | + } |
| 145 | + } |
| 146 | + originalWD, err := os.Getwd() |
| 147 | + if err != nil { |
| 148 | + return fmt.Errorf("failed to get current directory: %w", err) |
| 149 | + } |
| 150 | + if err := os.Chdir(repoDir); err != nil { |
| 151 | + return fmt.Errorf("failed to change directory to %s: %w", repoDir, err) |
| 152 | + } |
| 153 | + defer os.Chdir(originalWD) |
| 154 | + |
| 155 | + if err := createBranch(ctx, time.Now()); err != nil { |
| 156 | + return err |
| 157 | + } |
| 158 | + if repoName == repoRust { |
| 159 | + if err := librarian.Run(ctx, "librarian", "update", "discovery"); err != nil { |
| 160 | + return err |
| 161 | + } |
| 162 | + } |
| 163 | + if err := librarian.Run(ctx, "librarian", "update", "googleapis"); err != nil { |
| 164 | + return err |
| 165 | + } |
| 166 | + if err := librarian.Run(ctx, "librarian", "generate", "--all"); err != nil { |
| 167 | + return err |
| 168 | + } |
| 169 | + if repoName == repoRust { |
| 170 | + if err := runCargoUpdate(ctx); err != nil { |
| 171 | + return err |
| 172 | + } |
| 173 | + } |
| 174 | + |
| 175 | + if err := commitChanges(ctx); err != nil { |
| 176 | + return err |
| 177 | + } |
| 178 | + if repoName != repoFake { |
| 179 | + if err := createPR(ctx, repoName); err != nil { |
| 180 | + return err |
| 181 | + } |
| 182 | + } |
| 183 | + return nil |
| 184 | +} |
| 185 | + |
| 186 | +func cloneRepo(ctx context.Context, repoDir, repoName string) error { |
| 187 | + return command.Run(ctx, "gh", "repo", "clone", fmt.Sprintf("googleapis/%s", repoName), repoDir) |
| 188 | +} |
| 189 | + |
| 190 | +func createBranch(ctx context.Context, now time.Time) error { |
| 191 | + branchName := fmt.Sprintf("%s%s", branchPrefix, now.Format("2006-01-02")) |
| 192 | + return command.Run(ctx, "git", "checkout", "-b", branchName) |
| 193 | +} |
| 194 | + |
| 195 | +func commitChanges(ctx context.Context) error { |
| 196 | + if err := command.Run(ctx, "git", "add", "."); err != nil { |
| 197 | + return err |
| 198 | + } |
| 199 | + return command.Run(ctx, "git", "commit", "-m", commitTitle) |
| 200 | +} |
| 201 | + |
| 202 | +func createPR(ctx context.Context, repoName string) error { |
| 203 | + var body string |
| 204 | + if repoName == repoRust { |
| 205 | + body = `Update googleapis/googleapis and googleapis/discovery-artifact-manager |
| 206 | +to the latest commit and regenerate all client libraries.` |
| 207 | + } else { |
| 208 | + body = `Update googleapis/googleapis to the latest commit and regenerate all client libraries.` |
| 209 | + } |
| 210 | + return command.Run(ctx, "gh", "pr", "create", "--title", commitTitle, "--body", body) |
| 211 | +} |
| 212 | + |
| 213 | +func runCargoUpdate(ctx context.Context) error { |
| 214 | + return command.Run(ctx, "cargo", "update", "--workspace") |
| 215 | +} |
0 commit comments