Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 64 additions & 2 deletions pkg/build/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import (
"context"
"errors"
"fmt"
"io/fs"
"net/url"
"os"
"path/filepath"
"regexp"
Expand All @@ -28,6 +30,7 @@ import (
"cloud.google.com/go/storage"
"github.com/sirupsen/logrus"

"sigs.k8s.io/release-sdk/git"
"sigs.k8s.io/release-utils/helpers"
"sigs.k8s.io/release-utils/tar"

Expand Down Expand Up @@ -530,10 +533,15 @@ func (bi *Instance) StageLocalSourceTree(workDir, buildVersion string) error {
return fmt.Errorf("compile tarball exclude regex: %w", err)
}

excludeGit := regexp.MustCompile(`(^|/)\.git(/|$)`)
// The tarball ships the workspace clones, including their git
// directories, as the release process relies on their state. Make sure
// no repository leaks the credentials embedded in its remote URLs:
if err := stripRemoteCredentials(filepath.Join(workDir, "src")); err != nil {
return fmt.Errorf("stripping remote credentials from workspace repositories: %w", err)
}

if err := tar.Compress(
tarballPath, filepath.Join(workDir, "src"), excludeBuildDir, excludeGit,
tarballPath, filepath.Join(workDir, "src"), excludeBuildDir,
); err != nil {
return fmt.Errorf("create tarball: %w", err)
}
Expand All @@ -554,6 +562,60 @@ func (bi *Instance) StageLocalSourceTree(workDir, buildVersion string) error {
return nil
}

// stripRemoteCredentials replaces the remote URLs of every git repository
// found under path with their equivalents without credentials. The workspace
// clones authenticate with the GitHub token embedded in their remote URLs,
// which must not leak into the staged source tarball.
func stripRemoteCredentials(path string) error {
return filepath.WalkDir(path, func(entry string, d fs.DirEntry, err error) error {
if err != nil {
return err
}

if !d.IsDir() || d.Name() != ".git" {
Comment thread
saschagrunert marked this conversation as resolved.
return nil
}

repoPath := filepath.Dir(entry)

repo, err := git.OpenRepo(repoPath)
if err != nil {
return fmt.Errorf("opening workspace repository %s: %w", repoPath, err)
}

remotes, err := repo.Remotes()
if err != nil {
return fmt.Errorf("listing remotes of %s: %w", repoPath, err)
}

for _, remote := range remotes {
for _, remoteURL := range remote.URLs() {
parsed, err := url.Parse(remoteURL)
if err != nil || parsed.User == nil {
Comment thread
puerco marked this conversation as resolved.
continue
}

parsed.User = nil

logrus.Infof(
"Removing credentials from remote %s of %s", remote.Name(), repoPath,
)

if err := repo.SetURL(remote.Name(), parsed.String()); err != nil {
return fmt.Errorf("setting URL of remote %s: %w", remote.Name(), err)
}

// SetURL replaces all remote URLs; break to avoid
// iterating the now-stale URL list.
break
Comment thread
saschagrunert marked this conversation as resolved.
}
}

// Do not descend into the git directory itself
return filepath.SkipDir
})
}

// DeleteLocalSourceTarball the deletion of the tarball is now decoupled from
// StageLocalSourceTree to be able to use it during the anago.stage function.
func (bi *Instance) DeleteLocalSourceTarball(workDir string) error {
Expand Down
68 changes: 68 additions & 0 deletions pkg/build/push_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
Copyright 2026 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package build

import (
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/require"

"sigs.k8s.io/release-sdk/git"
"sigs.k8s.io/release-utils/command"
)

func testGit(t *testing.T, dir string, args ...string) {
t.Helper()

_, err := command.NewWithWorkDir(dir, "git", args...).RunSilentSuccessOutput()
require.NoError(t, err)
}

func TestStripRemoteCredentials(t *testing.T) {
t.Parallel()

workDir := t.TempDir()
repoPath := filepath.Join(workDir, "src", "k8s.io", "kubernetes")
require.NoError(t, os.MkdirAll(repoPath, 0o755))
testGit(t, repoPath, "init", "-q")
testGit(t, repoPath, "remote", "add", "origin", "https://git:supersecret@github.com/kubernetes/kubernetes")
testGit(t, repoPath, "remote", "add", "clean", "https://github.com/kubernetes/release")

require.NoError(t, stripRemoteCredentials(filepath.Join(workDir, "src")))

repo, err := git.OpenRepo(repoPath)
require.NoError(t, err)
remotes, err := repo.Remotes()
require.NoError(t, err)
require.Len(t, remotes, 2)

urls := map[string]string{}

for _, remote := range remotes {
require.Len(t, remote.URLs(), 1)
urls[remote.Name()] = remote.URLs()[0]
}

// The credentials are stripped, remotes without credentials are untouched
require.Equal(t, "https://github.com/kubernetes/kubernetes", urls["origin"])
require.Equal(t, "https://github.com/kubernetes/release", urls["clean"])

// No repositories under the path is not an error
require.NoError(t, stripRemoteCredentials(t.TempDir()))
}
2 changes: 1 addition & 1 deletion pkg/notes/notes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ func TestPrettySIG(t *testing.T) {
}

for input, expected := range cases {
require.Equal(t, expected, (prettySIG(input)))
require.Equal(t, expected, prettySIG(input))
}
}

Expand Down