Compare commits

...

5 Commits

Author SHA1 Message Date
github-actions[bot]
fd2d0adae9 Bump version to 3.3.0-rc2 on release-3.3 branch (#25712)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: crenshaw-dev <350466+crenshaw-dev@users.noreply.github.com>
2025-12-17 12:55:58 -05:00
argo-cd-cherry-pick-bot[bot]
708c63683c fix(hydrator): race when pushing notes (cherry-pick #25700 for 3.3) (#25709)
Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>
Co-authored-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>
2025-12-17 11:30:44 -05:00
argo-cd-cherry-pick-bot[bot]
393cb97042 fix(hydrator): hydrated sha missing on no-ops (#25694) (cherry-pick #25695 for 3.3) (#25697)
Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>
Co-authored-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>
2025-12-17 10:31:28 -05:00
argo-cd-cherry-pick-bot[bot]
99434863c9 chore(deps): update module k8s.io/kubernetes to v1.34.2 [security] (cherry-pick #25682 for 3.3) (#25683)
Signed-off-by: reggie-k <regina.voloshin@codefresh.io>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: reggie-k <regina.voloshin@codefresh.io>
2025-12-16 10:47:00 +02:00
github-actions[bot]
814db444c3 Bump version to 3.3.0-rc1 on release-3.3 branch (#25667)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: reggie-k <19544836+reggie-k@users.noreply.github.com>
2025-12-15 18:47:04 +02:00
26 changed files with 491 additions and 108 deletions

View File

@@ -1 +1 @@
3.3.0
3.3.0-rc2

View File

@@ -0,0 +1,209 @@
package commit
import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/argoproj/argo-cd/v3/util/git"
)
// TestAddNoteConcurrentStaggered tests that when multiple AddNote operations run
// with slightly staggered timing, all notes persist correctly.
// Each operation gets its own git clone, simulating multiple concurrent hydration requests.
func TestAddNoteConcurrentStaggered(t *testing.T) {
t.Parallel()
remotePath, localPath := setupRepoWithRemote(t)
// Create 3 branches with commits (simulating different hydration targets)
branches := []string{"env/dev", "env/staging", "env/prod"}
commitSHAs := make([]string, 3)
for i, branch := range branches {
commitSHAs[i] = commitAndPushBranch(t, localPath, branch)
}
// Create separate clones for concurrent operations
cloneClients := make([]git.Client, 3)
for i := 0; i < 3; i++ {
cloneClients[i] = getClientForClone(t, remotePath)
}
// Add notes concurrently with slight stagger
var wg sync.WaitGroup
errors := make([]error, 3)
for i := 0; i < 3; i++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
time.Sleep(time.Duration(idx*50) * time.Millisecond)
errors[idx] = AddNote(cloneClients[idx], fmt.Sprintf("dry-sha-%d", idx), commitSHAs[idx])
}(i)
}
wg.Wait()
// Verify all notes persisted
verifyClient := getClientForClone(t, remotePath)
for i, commitSHA := range commitSHAs {
note, err := verifyClient.GetCommitNote(commitSHA, NoteNamespace)
require.NoError(t, err, "Note should exist for commit %d", i)
assert.Contains(t, note, fmt.Sprintf("dry-sha-%d", i))
}
}
// TestAddNoteConcurrentSimultaneous tests that when multiple AddNote operations run
// simultaneously (without delays), all notes persist correctly.
// Each operation gets its own git clone, simulating multiple concurrent hydration requests.
func TestAddNoteConcurrentSimultaneous(t *testing.T) {
t.Parallel()
remotePath, localPath := setupRepoWithRemote(t)
// Create 3 branches with commits (simulating different hydration targets)
branches := []string{"env/dev", "env/staging", "env/prod"}
commitSHAs := make([]string, 3)
for i, branch := range branches {
commitSHAs[i] = commitAndPushBranch(t, localPath, branch)
}
// Create separate clones for concurrent operations
cloneClients := make([]git.Client, 3)
for i := 0; i < 3; i++ {
cloneClients[i] = getClientForClone(t, remotePath)
}
// Add notes concurrently without delays
var wg sync.WaitGroup
startChan := make(chan struct{})
for i := 0; i < 3; i++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
<-startChan
_ = AddNote(cloneClients[idx], fmt.Sprintf("dry-sha-%d", idx), commitSHAs[idx])
}(i)
}
close(startChan)
wg.Wait()
// Verify all notes persisted
verifyClient := getClientForClone(t, remotePath)
for i, commitSHA := range commitSHAs {
note, err := verifyClient.GetCommitNote(commitSHA, NoteNamespace)
require.NoError(t, err, "Note should exist for commit %d", i)
assert.Contains(t, note, fmt.Sprintf("dry-sha-%d", i))
}
}
// setupRepoWithRemote creates a bare remote repo and a local repo configured to push to it.
// Returns the remote path and local path.
func setupRepoWithRemote(t *testing.T) (remotePath, localPath string) {
t.Helper()
ctx := t.Context()
// Create bare remote repository
remoteDir := t.TempDir()
remotePath = filepath.Join(remoteDir, "remote.git")
err := os.MkdirAll(remotePath, 0o755)
require.NoError(t, err)
_, err = runGitCmd(ctx, remotePath, "init", "--bare")
require.NoError(t, err)
// Create local repository
localDir := t.TempDir()
localPath = filepath.Join(localDir, "local")
err = os.MkdirAll(localPath, 0o755)
require.NoError(t, err)
_, err = runGitCmd(ctx, localPath, "init")
require.NoError(t, err)
_, err = runGitCmd(ctx, localPath, "config", "user.name", "Test User")
require.NoError(t, err)
_, err = runGitCmd(ctx, localPath, "config", "user.email", "test@example.com")
require.NoError(t, err)
_, err = runGitCmd(ctx, localPath, "remote", "add", "origin", remotePath)
require.NoError(t, err)
return remotePath, localPath
}
// commitAndPushBranch writes a file, commits it, creates a branch, and pushes to remote.
// Returns the commit SHA.
func commitAndPushBranch(t *testing.T, localPath, branch string) string {
t.Helper()
ctx := t.Context()
testFile := filepath.Join(localPath, "test.txt")
err := os.WriteFile(testFile, []byte("content for "+branch), 0o644)
require.NoError(t, err)
_, err = runGitCmd(ctx, localPath, "add", ".")
require.NoError(t, err)
_, err = runGitCmd(ctx, localPath, "commit", "-m", "commit "+branch)
require.NoError(t, err)
sha, err := runGitCmd(ctx, localPath, "rev-parse", "HEAD")
require.NoError(t, err)
_, err = runGitCmd(ctx, localPath, "branch", branch)
require.NoError(t, err)
_, err = runGitCmd(ctx, localPath, "push", "origin", branch)
require.NoError(t, err)
return sha
}
// getClientForClone creates a git client with a fresh clone of the remote repo.
func getClientForClone(t *testing.T, remotePath string) git.Client {
t.Helper()
ctx := t.Context()
workDir := t.TempDir()
client, err := git.NewClientExt(remotePath, workDir, &git.NopCreds{}, false, false, "", "")
require.NoError(t, err)
err = client.Init()
require.NoError(t, err)
_, err = runGitCmd(ctx, workDir, "config", "user.name", "Test User")
require.NoError(t, err)
_, err = runGitCmd(ctx, workDir, "config", "user.email", "test@example.com")
require.NoError(t, err)
err = client.Fetch("", 0)
require.NoError(t, err)
return client
}
// runGitCmd is a helper function to run git commands
func runGitCmd(ctx context.Context, dir string, args ...string) (string, error) {
cmd := exec.CommandContext(ctx, "git", args...)
cmd.Dir = dir
output, err := cmd.CombinedOutput()
return strings.TrimSpace(string(output)), err
}

View File

@@ -187,7 +187,7 @@ func (s *Service) handleCommitRequest(logCtx *log.Entry, r *apiclient.CommitHydr
// short-circuit if already hydrated
if isHydrated {
logCtx.Debugf("this dry sha %s is already hydrated", r.DrySha)
return "", "", nil
return "", hydratedSha, nil
}
logCtx.Debug("Writing manifests")
@@ -197,13 +197,14 @@ func (s *Service) handleCommitRequest(logCtx *log.Entry, r *apiclient.CommitHydr
return "", "", fmt.Errorf("failed to write manifests: %w", err)
}
if !shouldCommit {
// add the note and return
// Manifests did not change, so we don't need to create a new commit.
// Add a git note to track that this dry SHA has been processed, and return the existing hydrated SHA.
logCtx.Debug("Adding commit note")
err = AddNote(gitClient, r.DrySha, hydratedSha)
if err != nil {
return "", "", fmt.Errorf("failed to add commit note: %w", err)
}
return "", "", nil
return "", hydratedSha, nil
}
logCtx.Debug("Committing and pushing changes")
out, err = gitClient.CommitAndPush(r.TargetBranch, r.CommitMessage)

View File

@@ -108,7 +108,7 @@ func Test_CommitHydratedManifests(t *testing.T) {
resp, err := service.CommitHydratedManifests(t.Context(), validRequest)
require.NoError(t, err)
require.NotNil(t, resp)
assert.Empty(t, resp.HydratedSha) // changes introduced by commit note. hydration won't happen if there are no new manifest|s to commit
assert.Equal(t, "it-worked!", resp.HydratedSha, "Should return existing hydrated SHA for no-op")
})
t.Run("root path with dot and blank - no directory removal", func(t *testing.T) {
@@ -283,12 +283,13 @@ func Test_CommitHydratedManifests(t *testing.T) {
TargetBranch: "main",
SyncBranch: "env/test",
CommitMessage: "test commit message",
DrySha: "dry-sha-456",
}
resp, err := service.CommitHydratedManifests(t.Context(), requestWithEmptyPaths)
require.NoError(t, err)
require.NotNil(t, resp)
assert.Empty(t, resp.HydratedSha) // changes introduced by commit note. hydration won't happen if there are no new manifest|s to commit
assert.Equal(t, "empty-paths-sha", resp.HydratedSha, "Should return existing hydrated SHA for no-op")
})
t.Run("duplicate request already hydrated", func(t *testing.T) {
@@ -329,7 +330,7 @@ func Test_CommitHydratedManifests(t *testing.T) {
resp, err := service.CommitHydratedManifests(t.Context(), request)
require.NoError(t, err)
require.NotNil(t, resp)
assert.Empty(t, resp.HydratedSha) // changes introduced by commit note. hydration won't happen if there are no new manifest|s to commit
assert.Equal(t, "dupe-test-sha", resp.HydratedSha, "Should return existing hydrated SHA when already hydrated")
})
t.Run("root path with dot - no changes to manifest - should commit note only", func(t *testing.T) {
@@ -355,6 +356,7 @@ func Test_CommitHydratedManifests(t *testing.T) {
TargetBranch: "main",
SyncBranch: "env/test",
CommitMessage: "test commit message",
DrySha: "dry-sha-123",
Paths: []*apiclient.PathDetails{
{
Path: ".",
@@ -370,7 +372,8 @@ func Test_CommitHydratedManifests(t *testing.T) {
resp, err := service.CommitHydratedManifests(t.Context(), requestWithRootAndBlank)
require.NoError(t, err)
require.NotNil(t, resp)
assert.Empty(t, resp.HydratedSha)
// BUG FIX: When manifests don't change (no-op), the existing hydrated SHA should be returned.
assert.Equal(t, "root-and-blank-sha", resp.HydratedSha, "Should return existing hydrated SHA for no-op")
})
}

View File

@@ -401,3 +401,69 @@ func TestAddNote(t *testing.T) {
err = AddNote(mockGitClient, drySha, commitShaErr)
require.Error(t, err)
}
// TestWriteForPaths_NoOpScenario tests that when manifests don't change between two hydrations,
// shouldCommit returns false. This reproduces the bug where a new DRY commit that doesn't affect
// manifests should not create a new hydrated commit.
func TestWriteForPaths_NoOpScenario(t *testing.T) {
root := tempRoot(t)
repoURL := "https://github.com/example/repo"
drySha1 := "abc123"
drySha2 := "def456" // Different dry SHA
paths := []*apiclient.PathDetails{
{
Path: "guestbook",
Manifests: []*apiclient.HydratedManifestDetails{
{ManifestJSON: `{"apiVersion":"v1","kind":"Service","metadata":{"name":"guestbook-ui"}}`},
{ManifestJSON: `{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"name":"guestbook-ui"}}`},
},
Commands: []string{"kustomize build ."},
},
}
now1 := metav1.NewTime(time.Now())
metadata1 := &appsv1.RevisionMetadata{
Author: "test-author",
Date: &now1,
Message: "Initial commit",
}
// First hydration - manifests are new, so HasFileChanged should return true
mockGitClient1 := gitmocks.NewClient(t)
mockGitClient1.On("HasFileChanged", "guestbook/manifest.yaml").Return(true, nil).Once()
shouldCommit1, err := WriteForPaths(root, repoURL, drySha1, metadata1, paths, mockGitClient1)
require.NoError(t, err)
require.True(t, shouldCommit1, "First hydration should commit because manifests are new")
// Second hydration - same manifest content but different dry SHA and metadata
// Simulate adding a README.md to the dry source (which doesn't affect manifests)
now2 := metav1.NewTime(time.Now().Add(1 * time.Hour)) // Different timestamp
metadata2 := &appsv1.RevisionMetadata{
Author: "test-author",
Date: &now2,
Message: "Add README.md", // Different commit message
}
// The manifests are identical, so HasFileChanged should return false
mockGitClient2 := gitmocks.NewClient(t)
mockGitClient2.On("HasFileChanged", "guestbook/manifest.yaml").Return(false, nil).Once()
shouldCommit2, err := WriteForPaths(root, repoURL, drySha2, metadata2, paths, mockGitClient2)
require.NoError(t, err)
require.False(t, shouldCommit2, "Second hydration should NOT commit because manifests didn't change")
// Verify that the root-level metadata WAS updated (even though we're not committing)
// The files get written to the working directory, but since shouldCommit is false, they won't be committed
topMetadataPath := filepath.Join(root.Name(), "hydrator.metadata")
topMetadataBytes, err := os.ReadFile(topMetadataPath)
require.NoError(t, err)
var topMetadata hydratorMetadataFile
err = json.Unmarshal(topMetadataBytes, &topMetadata)
require.NoError(t, err)
// The top-level metadata should have the NEW dry SHA (files are written, just not committed)
assert.Equal(t, drySha2, topMetadata.DrySHA)
assert.Equal(t, metadata2.Date.Format(time.RFC3339), topMetadata.Date)
}

View File

@@ -1,2 +1,5 @@
This page is populated for released Argo CD versions. Use the version selector to view this table for a specific
version.
| Argo CD version | Kubernetes versions |
|-----------------|---------------------|
| 3.3 | v1.33, v1.32, v1.31, v1.30 |
| 3.2 | v1.33, v1.32, v1.31, v1.30 |
| 3.1 | v1.33, v1.32, v1.31, v1.30 |

View File

@@ -22,7 +22,7 @@ require (
k8s.io/kube-aggregator v0.34.0
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b
k8s.io/kubectl v0.34.0
k8s.io/kubernetes v1.34.0
k8s.io/kubernetes v1.34.2
sigs.k8s.io/structured-merge-diff/v6 v6.3.1-0.20251003215857-446d8398e19c
sigs.k8s.io/yaml v1.6.0
)

View File

@@ -249,6 +249,8 @@ k8s.io/kubectl v0.34.0 h1:NcXz4TPTaUwhiX4LU+6r6udrlm0NsVnSkP3R9t0dmxs=
k8s.io/kubectl v0.34.0/go.mod h1:bmd0W5i+HuG7/p5sqicr0Li0rR2iIhXL0oUyLF3OjR4=
k8s.io/kubernetes v1.34.0 h1:NvUrwPAVB4W3mSOpJ/RtNGHWWYyUP/xPaX5rUSpzA0w=
k8s.io/kubernetes v1.34.0/go.mod h1:iu+FhII+Oc/1gGWLJcer6wpyih441aNFHl7Pvm8yPto=
k8s.io/kubernetes v1.34.2 h1:WQdDvYJazkmkwSncgNwGvVtaCt4TYXIU3wSMRgvp3MI=
k8s.io/kubernetes v1.34.2/go.mod h1:m6pZk6a179pRo2wsTiCPORJ86iOEQmfIzUvtyEF8BwA=
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y=
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=

4
go.mod
View File

@@ -22,6 +22,7 @@ require (
github.com/bradleyfalzon/ghinstallation/v2 v2.17.0
github.com/casbin/casbin/v2 v2.135.0
github.com/casbin/govaluate v1.10.0
github.com/cenkalti/backoff/v5 v5.0.3
github.com/cespare/xxhash/v2 v2.3.0
github.com/chainguard-dev/git-urls v1.0.2
github.com/coreos/go-oidc/v3 v3.14.1
@@ -160,7 +161,6 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/chai2010/gettext-go v1.0.3 // indirect
github.com/clipperhouse/displaywidth v0.6.0 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
@@ -304,7 +304,7 @@ require (
k8s.io/controller-manager v0.34.0 // indirect
k8s.io/gengo/v2 v2.0.0-20250604051438-85fd79dbfd9f // indirect
k8s.io/kube-aggregator v0.34.0 // indirect
k8s.io/kubernetes v1.34.0 // indirect
k8s.io/kubernetes v1.34.2 // indirect
nhooyr.io/websocket v1.8.7 // indirect
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
sigs.k8s.io/kustomize/api v0.20.1 // indirect

4
go.sum
View File

@@ -1461,8 +1461,8 @@ k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOP
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts=
k8s.io/kubectl v0.34.0 h1:NcXz4TPTaUwhiX4LU+6r6udrlm0NsVnSkP3R9t0dmxs=
k8s.io/kubectl v0.34.0/go.mod h1:bmd0W5i+HuG7/p5sqicr0Li0rR2iIhXL0oUyLF3OjR4=
k8s.io/kubernetes v1.34.0 h1:NvUrwPAVB4W3mSOpJ/RtNGHWWYyUP/xPaX5rUSpzA0w=
k8s.io/kubernetes v1.34.0/go.mod h1:iu+FhII+Oc/1gGWLJcer6wpyih441aNFHl7Pvm8yPto=
k8s.io/kubernetes v1.34.2 h1:WQdDvYJazkmkwSncgNwGvVtaCt4TYXIU3wSMRgvp3MI=
k8s.io/kubernetes v1.34.2/go.mod h1:m6pZk6a179pRo2wsTiCPORJ86iOEQmfIzUvtyEF8BwA=
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y=
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=

View File

@@ -12,4 +12,4 @@ resources:
images:
- name: quay.io/argoproj/argocd
newName: quay.io/argoproj/argocd
newTag: latest
newTag: v3.3.0-rc2

View File

@@ -5,7 +5,7 @@ kind: Kustomization
images:
- name: quay.io/argoproj/argocd
newName: quay.io/argoproj/argocd
newTag: latest
newTag: v3.3.0-rc2
resources:
- ./application-controller
- ./dex

View File

@@ -31273,7 +31273,7 @@ spec:
key: applicationsetcontroller.status.max.resources.count
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
name: argocd-applicationset-controller
ports:
@@ -31408,7 +31408,7 @@ spec:
key: log.format.timestamp
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -31536,7 +31536,7 @@ spec:
- argocd
- admin
- redis-initial-password
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: IfNotPresent
name: secret-init
securityContext:
@@ -31833,7 +31833,7 @@ spec:
value: /helm-working-dir
- name: HELM_DATA_HOME
value: /helm-working-dir
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -31886,7 +31886,7 @@ spec:
command:
- sh
- -c
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
name: copyutil
securityContext:
allowPrivilegeEscalation: false
@@ -32234,7 +32234,7 @@ spec:
optional: true
- name: KUBECACHEDIR
value: /tmp/kubecache
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
name: argocd-application-controller
ports:

View File

@@ -31241,7 +31241,7 @@ spec:
key: applicationsetcontroller.status.max.resources.count
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
name: argocd-applicationset-controller
ports:
@@ -31370,7 +31370,7 @@ spec:
- argocd
- admin
- redis-initial-password
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: IfNotPresent
name: secret-init
securityContext:
@@ -31667,7 +31667,7 @@ spec:
value: /helm-working-dir
- name: HELM_DATA_HOME
value: /helm-working-dir
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -31720,7 +31720,7 @@ spec:
command:
- sh
- -c
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
name: copyutil
securityContext:
allowPrivilegeEscalation: false
@@ -32068,7 +32068,7 @@ spec:
optional: true
- name: KUBECACHEDIR
value: /tmp/kubecache
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
name: argocd-application-controller
ports:

View File

@@ -12,4 +12,4 @@ resources:
images:
- name: quay.io/argoproj/argocd
newName: quay.io/argoproj/argocd
newTag: latest
newTag: v3.3.0-rc2

View File

@@ -12,7 +12,7 @@ patches:
images:
- name: quay.io/argoproj/argocd
newName: quay.io/argoproj/argocd
newTag: latest
newTag: v3.3.0-rc2
resources:
- ../../base/application-controller
- ../../base/applicationset-controller

View File

@@ -32639,7 +32639,7 @@ spec:
key: applicationsetcontroller.status.max.resources.count
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
name: argocd-applicationset-controller
ports:
@@ -32774,7 +32774,7 @@ spec:
key: log.format.timestamp
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -32925,7 +32925,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /shared/argocd-dex
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
name: copyutil
securityContext:
@@ -33021,7 +33021,7 @@ spec:
key: notificationscontroller.repo.server.plaintext
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
livenessProbe:
tcpSocket:
@@ -33145,7 +33145,7 @@ spec:
- argocd
- admin
- redis-initial-password
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: IfNotPresent
name: secret-init
securityContext:
@@ -33468,7 +33468,7 @@ spec:
value: /helm-working-dir
- name: HELM_DATA_HOME
value: /helm-working-dir
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -33521,7 +33521,7 @@ spec:
command:
- sh
- -c
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
name: copyutil
securityContext:
allowPrivilegeEscalation: false
@@ -33895,7 +33895,7 @@ spec:
key: server.sync.replace.allowed
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
livenessProbe:
httpGet:
@@ -34279,7 +34279,7 @@ spec:
optional: true
- name: KUBECACHEDIR
value: /tmp/kubecache
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
name: argocd-application-controller
ports:

View File

@@ -32609,7 +32609,7 @@ spec:
key: applicationsetcontroller.status.max.resources.count
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
name: argocd-applicationset-controller
ports:
@@ -32761,7 +32761,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /shared/argocd-dex
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
name: copyutil
securityContext:
@@ -32857,7 +32857,7 @@ spec:
key: notificationscontroller.repo.server.plaintext
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
livenessProbe:
tcpSocket:
@@ -32981,7 +32981,7 @@ spec:
- argocd
- admin
- redis-initial-password
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: IfNotPresent
name: secret-init
securityContext:
@@ -33304,7 +33304,7 @@ spec:
value: /helm-working-dir
- name: HELM_DATA_HOME
value: /helm-working-dir
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -33357,7 +33357,7 @@ spec:
command:
- sh
- -c
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
name: copyutil
securityContext:
allowPrivilegeEscalation: false
@@ -33731,7 +33731,7 @@ spec:
key: server.sync.replace.allowed
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
livenessProbe:
httpGet:
@@ -34115,7 +34115,7 @@ spec:
optional: true
- name: KUBECACHEDIR
value: /tmp/kubecache
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
name: argocd-application-controller
ports:

View File

@@ -1897,7 +1897,7 @@ spec:
key: applicationsetcontroller.status.max.resources.count
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
name: argocd-applicationset-controller
ports:
@@ -2032,7 +2032,7 @@ spec:
key: log.format.timestamp
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -2183,7 +2183,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /shared/argocd-dex
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
name: copyutil
securityContext:
@@ -2279,7 +2279,7 @@ spec:
key: notificationscontroller.repo.server.plaintext
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
livenessProbe:
tcpSocket:
@@ -2403,7 +2403,7 @@ spec:
- argocd
- admin
- redis-initial-password
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: IfNotPresent
name: secret-init
securityContext:
@@ -2726,7 +2726,7 @@ spec:
value: /helm-working-dir
- name: HELM_DATA_HOME
value: /helm-working-dir
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -2779,7 +2779,7 @@ spec:
command:
- sh
- -c
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
name: copyutil
securityContext:
allowPrivilegeEscalation: false
@@ -3153,7 +3153,7 @@ spec:
key: server.sync.replace.allowed
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
livenessProbe:
httpGet:
@@ -3537,7 +3537,7 @@ spec:
optional: true
- name: KUBECACHEDIR
value: /tmp/kubecache
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
name: argocd-application-controller
ports:

View File

@@ -1867,7 +1867,7 @@ spec:
key: applicationsetcontroller.status.max.resources.count
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
name: argocd-applicationset-controller
ports:
@@ -2019,7 +2019,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /shared/argocd-dex
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
name: copyutil
securityContext:
@@ -2115,7 +2115,7 @@ spec:
key: notificationscontroller.repo.server.plaintext
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
livenessProbe:
tcpSocket:
@@ -2239,7 +2239,7 @@ spec:
- argocd
- admin
- redis-initial-password
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: IfNotPresent
name: secret-init
securityContext:
@@ -2562,7 +2562,7 @@ spec:
value: /helm-working-dir
- name: HELM_DATA_HOME
value: /helm-working-dir
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -2615,7 +2615,7 @@ spec:
command:
- sh
- -c
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
name: copyutil
securityContext:
allowPrivilegeEscalation: false
@@ -2989,7 +2989,7 @@ spec:
key: server.sync.replace.allowed
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
livenessProbe:
httpGet:
@@ -3373,7 +3373,7 @@ spec:
optional: true
- name: KUBECACHEDIR
value: /tmp/kubecache
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
name: argocd-application-controller
ports:

View File

@@ -31717,7 +31717,7 @@ spec:
key: applicationsetcontroller.status.max.resources.count
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
name: argocd-applicationset-controller
ports:
@@ -31852,7 +31852,7 @@ spec:
key: log.format.timestamp
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -32003,7 +32003,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /shared/argocd-dex
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
name: copyutil
securityContext:
@@ -32099,7 +32099,7 @@ spec:
key: notificationscontroller.repo.server.plaintext
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
livenessProbe:
tcpSocket:
@@ -32201,7 +32201,7 @@ spec:
- argocd
- admin
- redis-initial-password
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: IfNotPresent
name: secret-init
securityContext:
@@ -32498,7 +32498,7 @@ spec:
value: /helm-working-dir
- name: HELM_DATA_HOME
value: /helm-working-dir
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -32551,7 +32551,7 @@ spec:
command:
- sh
- -c
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
name: copyutil
securityContext:
allowPrivilegeEscalation: false
@@ -32923,7 +32923,7 @@ spec:
key: server.sync.replace.allowed
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
livenessProbe:
httpGet:
@@ -33307,7 +33307,7 @@ spec:
optional: true
- name: KUBECACHEDIR
value: /tmp/kubecache
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
name: argocd-application-controller
ports:

16
manifests/install.yaml generated
View File

@@ -31685,7 +31685,7 @@ spec:
key: applicationsetcontroller.status.max.resources.count
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
name: argocd-applicationset-controller
ports:
@@ -31837,7 +31837,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /shared/argocd-dex
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
name: copyutil
securityContext:
@@ -31933,7 +31933,7 @@ spec:
key: notificationscontroller.repo.server.plaintext
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
livenessProbe:
tcpSocket:
@@ -32035,7 +32035,7 @@ spec:
- argocd
- admin
- redis-initial-password
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: IfNotPresent
name: secret-init
securityContext:
@@ -32332,7 +32332,7 @@ spec:
value: /helm-working-dir
- name: HELM_DATA_HOME
value: /helm-working-dir
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -32385,7 +32385,7 @@ spec:
command:
- sh
- -c
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
name: copyutil
securityContext:
allowPrivilegeEscalation: false
@@ -32757,7 +32757,7 @@ spec:
key: server.sync.replace.allowed
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
livenessProbe:
httpGet:
@@ -33141,7 +33141,7 @@ spec:
optional: true
- name: KUBECACHEDIR
value: /tmp/kubecache
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
name: argocd-application-controller
ports:

View File

@@ -975,7 +975,7 @@ spec:
key: applicationsetcontroller.status.max.resources.count
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
name: argocd-applicationset-controller
ports:
@@ -1110,7 +1110,7 @@ spec:
key: log.format.timestamp
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -1261,7 +1261,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /shared/argocd-dex
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
name: copyutil
securityContext:
@@ -1357,7 +1357,7 @@ spec:
key: notificationscontroller.repo.server.plaintext
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
livenessProbe:
tcpSocket:
@@ -1459,7 +1459,7 @@ spec:
- argocd
- admin
- redis-initial-password
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: IfNotPresent
name: secret-init
securityContext:
@@ -1756,7 +1756,7 @@ spec:
value: /helm-working-dir
- name: HELM_DATA_HOME
value: /helm-working-dir
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -1809,7 +1809,7 @@ spec:
command:
- sh
- -c
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
name: copyutil
securityContext:
allowPrivilegeEscalation: false
@@ -2181,7 +2181,7 @@ spec:
key: server.sync.replace.allowed
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
livenessProbe:
httpGet:
@@ -2565,7 +2565,7 @@ spec:
optional: true
- name: KUBECACHEDIR
value: /tmp/kubecache
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
name: argocd-application-controller
ports:

View File

@@ -943,7 +943,7 @@ spec:
key: applicationsetcontroller.status.max.resources.count
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
name: argocd-applicationset-controller
ports:
@@ -1095,7 +1095,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /shared/argocd-dex
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
name: copyutil
securityContext:
@@ -1191,7 +1191,7 @@ spec:
key: notificationscontroller.repo.server.plaintext
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
livenessProbe:
tcpSocket:
@@ -1293,7 +1293,7 @@ spec:
- argocd
- admin
- redis-initial-password
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: IfNotPresent
name: secret-init
securityContext:
@@ -1590,7 +1590,7 @@ spec:
value: /helm-working-dir
- name: HELM_DATA_HOME
value: /helm-working-dir
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -1643,7 +1643,7 @@ spec:
command:
- sh
- -c
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
name: copyutil
securityContext:
allowPrivilegeEscalation: false
@@ -2015,7 +2015,7 @@ spec:
key: server.sync.replace.allowed
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
livenessProbe:
httpGet:
@@ -2399,7 +2399,7 @@ spec:
optional: true
- name: KUBECACHEDIR
value: /tmp/kubecache
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v3.3.0-rc2
imagePullPolicy: Always
name: argocd-application-controller
ports:

View File

@@ -298,3 +298,52 @@ func TestHydratorWithPlugin(t *testing.T) {
require.Equal(t, "inline-plugin-value", output)
})
}
func TestHydratorNoOp(t *testing.T) {
// Test that when hydration is run for a no-op (manifests do not change),
// the hydrated SHA is persisted to the app's source hydrator status instead of an empty string.
var firstHydratedSHA string
var firstDrySHA string
Given(t).
DrySourcePath("guestbook").
DrySourceRevision("HEAD").
SyncSourcePath("guestbook").
SyncSourceBranch("env/test").
When().
CreateApp().
Refresh(RefreshTypeNormal).
Wait("--hydrated").
Then().
Expect(HydrationPhaseIs(HydrateOperationPhaseHydrated)).
And(func(app *Application) {
require.NotEmpty(t, app.Status.SourceHydrator.CurrentOperation.HydratedSHA, "First hydration should have a hydrated SHA")
require.NotEmpty(t, app.Status.SourceHydrator.CurrentOperation.DrySHA, "First hydration should have a dry SHA")
firstHydratedSHA = app.Status.SourceHydrator.CurrentOperation.HydratedSHA
firstDrySHA = app.Status.SourceHydrator.CurrentOperation.DrySHA
t.Logf("First hydration - drySHA: %s, hydratedSHA: %s", firstDrySHA, firstHydratedSHA)
}).
When().
// Make a change to the dry source that doesn't affect the generated manifests.
AddFile("guestbook/README.md", "# Guestbook\n\nThis is documentation.").
Refresh(RefreshTypeNormal).
Wait("--hydrated").
Then().
Expect(HydrationPhaseIs(HydrateOperationPhaseHydrated)).
And(func(app *Application) {
require.NotEmpty(t, app.Status.SourceHydrator.CurrentOperation.HydratedSHA,
"Hydrated SHA must not be empty")
require.NotEmpty(t, app.Status.SourceHydrator.CurrentOperation.DrySHA)
// The dry SHA should be different (new commit in the dry source)
require.NotEqual(t, firstDrySHA, app.Status.SourceHydrator.CurrentOperation.DrySHA,
"Dry SHA should change after pushing a new commit")
t.Logf("Second hydration - drySHA: %s, hydratedSHA: %s",
app.Status.SourceHydrator.CurrentOperation.DrySHA,
app.Status.SourceHydrator.CurrentOperation.HydratedSHA)
require.Equal(t, firstHydratedSHA, app.Status.SourceHydrator.CurrentOperation.HydratedSHA,
"Hydrated SHA should remain the same for no-op hydration")
})
}

View File

@@ -23,6 +23,7 @@ import (
"unicode/utf8"
"github.com/bmatcuk/doublestar/v4"
"github.com/cenkalti/backoff/v5"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing"
@@ -1142,26 +1143,75 @@ func (m *nativeGitClient) GetCommitNote(sha string, namespace string) (string, e
}
// AddAndPushNote adds a note to a DRY sha and then pushes it.
// It uses a retry mechanism to handle concurrent note updates from multiple clients.
func (m *nativeGitClient) AddAndPushNote(sha string, namespace string, note string) error {
if namespace == "" {
namespace = "commit"
}
ctx := context.Background()
ref := "--ref=" + namespace
_, err := m.runCmd(ctx, "notes", ref, "add", "-f", "-m", note, sha)
if err != nil {
return fmt.Errorf("failed to push: %w", err)
}
if m.OnPush != nil {
done := m.OnPush(m.repoURL)
defer done()
notesRef := "refs/notes/" + namespace
// Configure exponential backoff with jitter to handle concurrent note updates
b := backoff.NewExponentialBackOff()
b.InitialInterval = 50 * time.Millisecond
b.MaxInterval = 1 * time.Second
attempt := 0
operation := func() (struct{}, error) {
attempt++
// Fetch the latest notes BEFORE adding to merge concurrent updates
// Use + prefix to force update local ref (safe because we want latest remote notes)
_, fetchErr := m.runCmd(ctx, "fetch", "origin", fmt.Sprintf("+%s:%s", notesRef, notesRef))
// Ignore "couldn't find remote ref" errors (notes don't exist yet - first time)
if fetchErr != nil && !strings.Contains(fetchErr.Error(), "couldn't find remote ref") {
log.Debugf("Failed to fetch notes (will continue): %v", fetchErr)
}
// Add note locally (use -f to overwrite if this specific commit already has a note locally)
_, err := m.runCmd(ctx, "notes", ref, "add", "-f", "-m", note, sha)
if err != nil {
return struct{}{}, backoff.Permanent(fmt.Errorf("failed to add note: %w", err))
}
if m.OnPush != nil {
done := m.OnPush(m.repoURL)
defer done()
}
// Push WITHOUT -f flag to avoid overwriting other notes
err = m.runCredentialedCmd(ctx, "push", "origin", notesRef)
if err == nil {
if attempt > 1 {
log.Debugf("AddAndPushNote succeeded after %d retries for commit %s", attempt-1, sha)
}
return struct{}{}, nil
}
log.Debugf("AddAndPushNote push failed (attempt %d): %v", attempt, err)
// Check if this is a retryable error
errStr := err.Error()
isRetryable := strings.Contains(errStr, "fetch first") || // Remote updated after our fetch (concurrent push completed between our fetch and push)
strings.Contains(errStr, "reference already exists") || // Concurrent push is holding the lock (git server-side lock)
strings.Contains(errStr, "incorrect old value") || // Git detected our local ref is stale (concurrent update)
strings.Contains(errStr, "failed to update ref") // Generic ref update failure that may include transient issues
if !isRetryable {
return struct{}{}, backoff.Permanent(fmt.Errorf("failed to push note: %w", err))
}
return struct{}{}, err
}
err = m.runCredentialedCmd(ctx, "push", "-f", "origin", "refs/notes/"+namespace)
_, err := backoff.Retry(ctx, operation,
backoff.WithBackOff(b),
backoff.WithMaxElapsedTime(5*time.Second),
)
if err != nil {
return fmt.Errorf("failed to push: %w", err)
return fmt.Errorf("failed to push note after retries: %w", err)
}
return nil
}