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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ pkg/gitprovider/ — Per-provider adapters: github.go, gitlab.go, bitb
Factory: NewProvider() dispatches on GitServer.Spec.GitProvider
pkg/client/jira/ — Jira REST client (go-jira wrapper + mockable Client interface)
pkg/gerrit/ — SSH-based Gerrit client
pkg/git/ — go-git wrapper + factory (supports SSH/HTTPS auth)
pkg/git/ — Git provider + factory (supports SSH/HTTPS auth): clone, push, fetch, checkout, commit run the git binary; ref, init and packless remote operations use go-git
pkg/webhook/ — Admission webhooks: Codebase and CodebaseBranch validation
pkg/objectmodifier/ — Applies default values / label patches before reconcile logic
pkg/util/ — Shared helpers (workdir, timeouts, URL utilities, templates)
Expand Down
12 changes: 6 additions & 6 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM alpine:3.18.12
FROM alpine:3.22.7

ARG TARGETARCH

Expand All @@ -9,15 +9,15 @@ ENV ASSETS_DIR=/usr/local/bin \
USER_NAME=codebase-operator \
USER_UID=1001

RUN apk add --no-cache ca-certificates=20241121-r1 \
openssh-client==9.3_p2-r3 \
git==2.40.4-r0
RUN apk add --no-cache ca-certificates=20261020-r0 \
openssh-client==10.0_p1-r10 \
git==2.49.1-r0

RUN adduser -h ${HOME} -s /bin/ash -D -u ${USER_UID} codebase-operator

COPY build/bin ${ASSETS_DIR}
COPY --chmod=0775 build/templates ${ASSETS_DIR}/templates
COPY build/configs ${ASSETS_DIR}/configs
COPY --chmod=0755 build/templates ${ASSETS_DIR}/templates
COPY --chmod=0755 build/configs ${ASSETS_DIR}/configs

RUN chmod u+x ${ASSETS_DIR}/user_setup && \
chmod ugo+x ${ASSETS_DIR}/entrypoint && \
Expand Down
17 changes: 7 additions & 10 deletions controllers/codebase/service/chain/put_project.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,22 +155,19 @@ func (h *PutProject) createProject(
}
}

// The remote branch, not local state, is the durable record of a push: a
// retry after any later failure (default-branch call, status patch,
// operator crash) regenerates history, and pushing it would silently
// replace the remote branch. Record intent only when the remote default
// branch is verified absent, so that on retry "in progress + branch
// present" can only mean our own push landed.
// The remote branch, not local state, records a push. A retry after a later
// failure (default-branch call, status patch, operator crash) regenerates
// local history. Record intent only when the remote default branch is
// absent: on retry, "in progress + branch present" then means this
// provisioning pushed it.
remoteBranchAbsent, err := h.remoteDefaultBranchAbsent(ctx, codebase, repoContext)
if err != nil {
return err
}

if !remoteBranchAbsent {
// go-git cannot verify fast-forward when the remote's current commit
// is absent from the freshly regenerated local history, so this push
// would silently replace the remote branch. Refuse instead of
// destroying history that this codebase provably did not just push.
// The remote default branch holds history this provisioning did not push;
// the regenerated local history does not contain it. Refuse to push.
return fmt.Errorf(
"remote repository already contains default branch %s with history not pushed by this provisioning; "+
"refusing to overwrite it - remove the remote branch or onboard the repository with the import strategy",
Expand Down
4 changes: 2 additions & 2 deletions deploy-templates/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ annotations:
artifacthub.io/images: |
- name: codebase-operator:2.36.0
image: epamedp/codebase-operator:2.36.0
- name: alpine:3.18.12
image: alpine:3.18.12
- name: alpine:3.22.7
image: alpine:3.22.7
artifacthub.io/operatorCapabilities: Full Lifecycle
artifacthub.io/crds: |
- kind: CDStageDeploy
Expand Down
52 changes: 46 additions & 6 deletions pkg/git/checkout_remote_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ func buildOriginWithClone(t *testing.T) (string, string) {
seedCommit(t, originDir, "base", "+refs/heads/master:refs/heads/master")

workDir := path.Join(t.TempDir(), "clone")
gp := NewGitProvider(Config{})
gp := NewCLIGitProvider(Config{})
require.NoError(t, gp.Clone(context.Background(), originDir, workDir))

return originDir, workDir
Expand All @@ -74,7 +74,7 @@ func workdirHead(t *testing.T, workDir string) *plumbing.Reference {
}

// A branch pushed to origin after the workdir was cloned is fetched into
// refs/heads by Checkout's refs/*:refs/* refspec; the existence check must look
// refs/heads by Checkout's branch refspec; the existence check must look
// there, or the checkout collides on Create with the ref the fetch just wrote.
func TestCheckout_RemoteBranchCreatedAfterClone(t *testing.T) {
originDir, workDir := buildOriginWithClone(t)
Expand All @@ -88,7 +88,7 @@ func TestCheckout_RemoteBranchCreatedAfterClone(t *testing.T) {
plumbing.NewHashReference(plumbing.NewBranchReferenceName("late-branch"), masterRef.Hash()),
))

gp := NewGitProvider(Config{})
gp := NewCLIGitProvider(Config{})
err = gp.Checkout(context.Background(), workDir, "late-branch", true)
require.NoError(t, err, "checkout of a branch created after clone must succeed")

Expand All @@ -106,7 +106,7 @@ func TestCheckout_RemoteBranchPresentAtCloneTime(t *testing.T) {
seedCommit(t, originDir, "base", "+refs/heads/master:refs/heads/feature")

workDir := path.Join(t.TempDir(), "clone")
gp := NewGitProvider(Config{})
gp := NewCLIGitProvider(Config{})
require.NoError(t, gp.Clone(context.Background(), originDir, workDir))

require.NoError(t, gp.Checkout(context.Background(), workDir, "feature", true))
Expand All @@ -118,7 +118,7 @@ func TestCheckout_RemoteBranchPresentAtCloneTime(t *testing.T) {
func TestCheckout_BranchAbsentEverywhere(t *testing.T) {
_, workDir := buildOriginWithClone(t)

gp := NewGitProvider(Config{})
gp := NewCLIGitProvider(Config{})
require.NoError(t, gp.Checkout(context.Background(), workDir, "brand-new", true))
require.Equal(t, "brand-new", workdirHead(t, workDir).Name().Short())
}
Expand All @@ -130,8 +130,48 @@ func TestCheckout_ForcePushedRemoteBranch(t *testing.T) {

rewritten := seedCommit(t, originDir, "rewritten-history", "+refs/heads/master:refs/heads/master")

gp := NewGitProvider(Config{})
gp := NewCLIGitProvider(Config{})
require.NoError(t, gp.Checkout(context.Background(), workDir, "master", true),
"force-pushed upstream branch must not fail the checkout fetch")
require.Equal(t, rewritten, workdirHead(t, workDir).Hash())
}

// TestFetch_SkipsNonBranchRefs: refs outside refs/heads and refs/tags, such as
// pull request heads, are not fetched, so their objects are not downloaded.
func TestFetch_SkipsNonBranchRefs(t *testing.T) {
tests := []struct {
name string
checkout func(gp *CLIGitProvider, workDir string) error
}{
{
name: "Checkout remote",
checkout: func(gp *CLIGitProvider, workDir string) error {
return gp.Checkout(context.Background(), workDir, "master", true)
},
},
{
name: "CheckoutRemoteBranch",
checkout: func(gp *CLIGitProvider, workDir string) error {
return gp.CheckoutRemoteBranch(context.Background(), workDir, "master")
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
originDir, workDir := buildOriginWithClone(t)
prHead := seedCommit(t, originDir, "unmerged", "+refs/heads/master:refs/pull/1/head")

require.NoError(t, tt.checkout(NewCLIGitProvider(Config{}), workDir))

repo, err := gogit.PlainOpen(workDir)
require.NoError(t, err)

_, err = repo.Reference("refs/pull/1/head", false)
require.ErrorIs(t, err, plumbing.ErrReferenceNotFound)

_, err = repo.CommitObject(prHead)
require.ErrorIs(t, err, plumbing.ErrObjectNotFound)
})
}
}
Loading
Loading