Skip to content

Commit 2807adf

Browse files
authored
refactor: tag-and-release does not need local clone (#2185)
Fixes #2172 Fixes #2053
1 parent 45db386 commit 2807adf

File tree

11 files changed

+209
-64
lines changed

11 files changed

+209
-64
lines changed

internal/config/config.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ const (
5454
// ReleaseInitResponse is a JSON file that describes which library to change
5555
// after release.
5656
ReleaseInitResponse = "release-init-response.json"
57-
pipelineStateFile = "state.yaml"
57+
// LibrarianStateFile is the name of the pipeline state file.
58+
LibrarianStateFile = "state.yaml"
5859
)
5960

6061
// are variables so it can be replaced during testing.
@@ -294,7 +295,7 @@ func (c *Config) deriveRepo() error {
294295
if err != nil {
295296
return fmt.Errorf("getting working directory: %w", err)
296297
}
297-
stateFile := filepath.Join(wd, LibrarianDir, pipelineStateFile)
298+
stateFile := filepath.Join(wd, LibrarianDir, LibrarianStateFile)
298299
if _, err := os.Stat(stateFile); err != nil {
299300
return fmt.Errorf("repo flag not specified and no state file found in current working directory: %w", err)
300301
}

internal/config/config_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,7 @@ func TestDeriveRepo(t *testing.T) {
364364
if err := os.MkdirAll(stateDir, 0755); err != nil {
365365
t.Fatal(err)
366366
}
367-
stateFile := filepath.Join(stateDir, pipelineStateFile)
367+
stateFile := filepath.Join(stateDir, LibrarianStateFile)
368368
if err := os.WriteFile(stateFile, []byte("test"), 0644); err != nil {
369369
t.Fatal(err)
370370
}
@@ -439,7 +439,7 @@ func TestSetDefaults(t *testing.T) {
439439
if err := os.MkdirAll(stateDir, 0755); err != nil {
440440
t.Fatal(err)
441441
}
442-
stateFile := filepath.Join(stateDir, pipelineStateFile)
442+
stateFile := filepath.Join(stateDir, LibrarianStateFile)
443443
if err := os.WriteFile(stateFile, []byte("test"), 0644); err != nil {
444444
t.Fatal(err)
445445
}
@@ -454,7 +454,7 @@ func TestSetDefaults(t *testing.T) {
454454
if err := os.MkdirAll(stateDir, 0755); err != nil {
455455
t.Fatal(err)
456456
}
457-
stateFile := filepath.Join(stateDir, pipelineStateFile)
457+
stateFile := filepath.Join(stateDir, LibrarianStateFile)
458458
if err := os.WriteFile(stateFile, []byte("test"), 0644); err != nil {
459459
t.Fatal(err)
460460
}

internal/librarian/action_test.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,6 @@ func TestLibrarianAction(t *testing.T) {
3535
name: "init",
3636
fn: newCmdInit,
3737
},
38-
{
39-
name: "tag-and-release",
40-
fn: newCmdTagAndRelease,
41-
},
4238
} {
4339
t.Run(test.name, func(t *testing.T) {
4440
testActionConfig(t, test.fn())

internal/librarian/commit_version_analyzer.go

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ const defaultTagFormat = "{id}-{version}"
3030
// GetConventionalCommitsSinceLastRelease returns all conventional commits for the given library since the
3131
// version specified in the state file.
3232
func GetConventionalCommitsSinceLastRelease(repo gitrepo.Repository, library *config.LibraryState) ([]*conventionalcommits.ConventionalCommit, error) {
33-
tag := formatTag(library, "")
33+
tag := formatTag(library.TagFormat, library.ID, library.Version)
3434
commits, err := repo.GetCommitsForPathsSinceTag(library.SourceRoots, tag)
3535
if err != nil {
3636
return nil, fmt.Errorf("failed to get commits for library %s: %w", library.ID, err)
@@ -101,16 +101,11 @@ func shouldExclude(files, excludePaths []string) bool {
101101
}
102102

103103
// formatTag returns the git tag for a given library version.
104-
func formatTag(library *config.LibraryState, versionOverride string) string {
105-
version := library.Version
106-
if versionOverride != "" {
107-
version = versionOverride
108-
}
109-
tagFormat := library.TagFormat
104+
func formatTag(tagFormat string, libraryID string, version string) string {
110105
if tagFormat == "" {
111106
tagFormat = defaultTagFormat
112107
}
113-
r := strings.NewReplacer("{id}", library.ID, "{version}", version)
108+
r := strings.NewReplacer("{id}", libraryID, "{version}", version)
114109
return r.Replace(tagFormat)
115110
}
116111

internal/librarian/commit_version_analyzer_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ func TestFormatTag(t *testing.T) {
110110
},
111111
} {
112112
t.Run(test.name, func(t *testing.T) {
113-
got := formatTag(test.library, "")
113+
got := formatTag(test.library.TagFormat, test.library.ID, test.library.Version)
114114
if got != test.want {
115115
t.Errorf("formatTag() = %q, want %q", got, test.want)
116116
}

internal/librarian/mocks_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"github.com/googleapis/librarian/internal/docker"
2727
"github.com/googleapis/librarian/internal/github"
2828
"github.com/googleapis/librarian/internal/gitrepo"
29+
"gopkg.in/yaml.v3"
2930
)
3031

3132
// mockGitHubClient is a mock implementation of the GitHubClient interface for testing.
@@ -54,9 +55,13 @@ type mockGitHubClient struct {
5455
pullRequests []*github.PullRequest
5556
pullRequest *github.PullRequest
5657
createdRelease *github.RepositoryRelease
58+
librarianState *config.LibrarianState
5759
}
5860

5961
func (m *mockGitHubClient) GetRawContent(ctx context.Context, path, ref string) ([]byte, error) {
62+
if path == ".librarian/state.yaml" && m.librarianState != nil {
63+
return yaml.Marshal(m.librarianState)
64+
}
6065
return m.rawContent, m.rawErr
6166
}
6267

internal/librarian/release_notes.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -277,8 +277,8 @@ func formatLibraryReleaseNotes(repo gitrepo.Repository, library *config.LibraryS
277277

278278
// The version should already be updated to the next version.
279279
newVersion := library.Version
280-
newTag := formatTag(library, newVersion)
281-
previousTag := formatTag(library, library.PreviousVersion)
280+
newTag := formatTag(library.TagFormat, library.ID, newVersion)
281+
previousTag := formatTag(library.TagFormat, library.ID, library.PreviousVersion)
282282

283283
commitsByType := make(map[string][]*conventionalcommits.ConventionalCommit)
284284
for _, commit := range library.Changes {

internal/librarian/state.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,13 @@ package librarian
1616

1717
import (
1818
"bytes"
19+
"context"
1920
"encoding/json"
2021
"errors"
2122
"fmt"
2223
"log/slog"
2324
"os"
25+
"path"
2426
"path/filepath"
2527
"strings"
2628

@@ -47,6 +49,18 @@ func loadRepoState(repo *gitrepo.LocalRepository, source string) (*config.Librar
4749
return parseLibrarianState(path, source)
4850
}
4951

52+
func loadRepoStateFromGitHub(ctx context.Context, ghClient GitHubClient, branch string) (*config.LibrarianState, error) {
53+
content, err := ghClient.GetRawContent(ctx, path.Join(config.LibrarianDir, config.LibrarianStateFile), branch)
54+
if err != nil {
55+
return nil, err
56+
}
57+
cfg, err := loadLibrarianStateFromBytes(content, "")
58+
if err != nil {
59+
return nil, err
60+
}
61+
return cfg, nil
62+
}
63+
5064
func loadLibrarianConfig(repo *gitrepo.LocalRepository) (*config.LibrarianConfig, error) {
5165
if repo == nil {
5266
slog.Info("repo is nil, skipping state loading")
@@ -61,8 +75,12 @@ func parseLibrarianState(path, source string) (*config.LibrarianState, error) {
6175
if err != nil {
6276
return nil, err
6377
}
78+
return loadLibrarianStateFromBytes(bytes, source)
79+
}
80+
81+
func loadLibrarianStateFromBytes(data []byte, source string) (*config.LibrarianState, error) {
6482
var s config.LibrarianState
65-
if err := yaml.Unmarshal(bytes, &s); err != nil {
83+
if err := yaml.Unmarshal(data, &s); err != nil {
6684
return nil, fmt.Errorf("unmarshaling librarian state: %w", err)
6785
}
6886
if err := populateServiceConfigIfEmpty(&s, source); err != nil {

internal/librarian/state_test.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,15 @@
1515
package librarian
1616

1717
import (
18+
"context"
19+
"fmt"
1820
"os"
1921
"path/filepath"
2022
"strings"
2123
"testing"
2224

2325
"github.com/google/go-cmp/cmp"
26+
"github.com/google/go-cmp/cmp/cmpopts"
2427
"github.com/googleapis/librarian/internal/config"
2528
"gopkg.in/yaml.v3"
2629
)
@@ -423,3 +426,71 @@ func TestReadLibraryState(t *testing.T) {
423426
})
424427
}
425428
}
429+
430+
func TestLoadRepoStateFromGitHub(t *testing.T) {
431+
t.Parallel()
432+
433+
state := &config.LibrarianState{
434+
Image: "gcr.io/some-project-id/some-test-image:latest",
435+
Libraries: []*config.LibraryState{
436+
{
437+
ID: "google-cloud-storage",
438+
SourceRoots: []string{"some/path"},
439+
TagFormat: "v{version}",
440+
},
441+
},
442+
}
443+
for _, test := range []struct {
444+
name string
445+
branch string
446+
ghClient GitHubClient
447+
want *config.LibrarianState
448+
wantErr bool
449+
wantErrMsg string
450+
}{
451+
{
452+
name: "happy path",
453+
ghClient: &mockGitHubClient{
454+
librarianState: state,
455+
},
456+
want: state,
457+
},
458+
{
459+
name: "missing file",
460+
ghClient: &mockGitHubClient{
461+
rawErr: fmt.Errorf("file not found"),
462+
},
463+
wantErr: true,
464+
wantErrMsg: "file not found",
465+
},
466+
{
467+
name: "invalid state file",
468+
ghClient: &mockGitHubClient{
469+
librarianState: &config.LibrarianState{},
470+
},
471+
wantErr: true,
472+
wantErrMsg: "validating librarian state",
473+
},
474+
} {
475+
t.Run(test.name, func(t *testing.T) {
476+
got, err := loadRepoStateFromGitHub(context.Background(), test.ghClient, test.branch)
477+
if test.wantErr {
478+
if err == nil {
479+
t.Fatal("loadRepoStateFromGitHub() should fail")
480+
}
481+
if !strings.Contains(err.Error(), test.wantErrMsg) {
482+
t.Fatalf("want error message: %s, got %s", test.wantErrMsg, err.Error())
483+
}
484+
485+
return
486+
}
487+
488+
if err != nil {
489+
t.Fatalf("loadRepoStateFromGitHub() unexpected error: %v", err)
490+
}
491+
if diff := cmp.Diff(test.want, got, cmpopts.EquateEmpty()); diff != "" {
492+
t.Fatalf("Response library state mismatch (-want +got):\n%s", diff)
493+
}
494+
})
495+
}
496+
}

internal/librarian/tag_and_release.go

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ import (
2727

2828
"github.com/googleapis/librarian/internal/config"
2929
"github.com/googleapis/librarian/internal/github"
30-
"github.com/googleapis/librarian/internal/gitrepo"
3130
)
3231

3332
const (
@@ -45,23 +44,20 @@ var (
4544
type tagAndReleaseRunner struct {
4645
ghClient GitHubClient
4746
pullRequest string
48-
repo gitrepo.Repository
49-
state *config.LibrarianState
5047
}
5148

5249
func newTagAndReleaseRunner(cfg *config.Config) (*tagAndReleaseRunner, error) {
53-
runner, err := newCommandRunner(cfg)
54-
if err != nil {
55-
return nil, err
56-
}
5750
if cfg.GitHubToken == "" {
5851
return nil, fmt.Errorf("`LIBRARIAN_GITHUB_TOKEN` must be set")
5952
}
53+
repo, err := github.ParseRemote(cfg.Repo)
54+
if err != nil {
55+
return nil, err
56+
}
57+
ghClient := github.NewClient(cfg.GitHubToken, repo)
6058
return &tagAndReleaseRunner{
61-
ghClient: runner.ghClient,
59+
ghClient: ghClient,
6260
pullRequest: cfg.PullRequest,
63-
repo: runner.repo,
64-
state: runner.state,
6561
}, nil
6662
}
6763

@@ -130,24 +126,30 @@ func (r *tagAndReleaseRunner) processPullRequest(ctx context.Context, p *github.
130126
return nil
131127
}
132128

129+
// Load library state from remote repo
130+
libraryState, err := loadRepoStateFromGitHub(ctx, r.ghClient, *p.Base.Ref)
131+
if err != nil {
132+
return err
133+
}
134+
133135
// Add a tag to the release commit to trigger louhi flow: "release-please-{pr number}"
134-
//TODO: remove this logic as part of https://github.com/googleapis/librarian/issues/2044
136+
// TODO: remove this logic as part of https://github.com/googleapis/librarian/issues/2044
135137
commitSha := p.GetMergeCommitSHA()
136138
tagName := fmt.Sprintf("release-please-%d", p.GetNumber())
137139
if err := r.ghClient.CreateTag(ctx, tagName, commitSha); err != nil {
138140
return fmt.Errorf("failed to create tag %s: %w", tagName, err)
139141
}
140-
141142
for _, release := range releases {
142143
slog.Info("creating release", "library", release.Library, "version", release.Version)
143144

144-
lib := r.state.LibraryByID(release.Library)
145-
if lib == nil {
146-
return fmt.Errorf("library %s not found", release.Library)
145+
tagFormat, err := determineTagFormat(release.Library, libraryState)
146+
if err != nil {
147+
slog.Warn("could not determine tag format", "library", release.Library)
148+
return err
147149
}
148150

149151
// Create the release.
150-
tagName := formatTag(lib, release.Version)
152+
tagName := formatTag(tagFormat, release.Library, release.Version)
151153
releaseName := fmt.Sprintf("%s %s", release.Library, release.Version)
152154
if _, err := r.ghClient.CreateRelease(ctx, tagName, releaseName, release.Body, commitSha); err != nil {
153155
return fmt.Errorf("failed to create release: %w", err)
@@ -157,6 +159,18 @@ func (r *tagAndReleaseRunner) processPullRequest(ctx context.Context, p *github.
157159
return r.replacePendingLabel(ctx, p)
158160
}
159161

162+
func determineTagFormat(libraryID string, librarianState *config.LibrarianState) (string, error) {
163+
// TODO(#2177): read from LibrarianConfig
164+
libraryState := librarianState.LibraryByID(libraryID)
165+
if libraryState == nil {
166+
return "", fmt.Errorf("library %s not found", libraryID)
167+
}
168+
if libraryState.TagFormat != "" {
169+
return libraryState.TagFormat, nil
170+
}
171+
return "", fmt.Errorf("library %s did not configure tag_format", libraryID)
172+
}
173+
160174
// libraryRelease holds the parsed information from a pull request body.
161175
type libraryRelease struct {
162176
// Body contains the release notes.

0 commit comments

Comments
 (0)