feat(hydrator): Commit message templating (#23679) (#24204)

Signed-off-by: pbhatnagar-oss <pbhatifiwork@gmail.com>
Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>
Co-authored-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>
This commit is contained in:
pbhatnagar-oss
2025-09-03 11:04:15 -07:00
committed by GitHub
parent 4c9291152b
commit 6f6c39d8f4
14 changed files with 600 additions and 26 deletions

View File

@@ -5,9 +5,7 @@ import (
"fmt"
"os"
"path/filepath"
"strings"
"text/template"
"time"
"github.com/Masterminds/sprig/v3"
log "github.com/sirupsen/logrus"
@@ -17,7 +15,7 @@ import (
"github.com/argoproj/argo-cd/v3/commitserver/apiclient"
"github.com/argoproj/argo-cd/v3/common"
appv1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v3/util/git"
"github.com/argoproj/argo-cd/v3/util/hydrator"
"github.com/argoproj/argo-cd/v3/util/io"
)
@@ -36,25 +34,13 @@ func init() {
// WriteForPaths writes the manifests, hydrator.metadata, and README.md files for each path in the provided paths. It
// also writes a root-level hydrator.metadata file containing the repo URL and dry SHA.
func WriteForPaths(root *os.Root, repoUrl, drySha string, dryCommitMetadata *appv1.RevisionMetadata, paths []*apiclient.PathDetails) error { //nolint:revive //FIXME(var-naming)
author := ""
message := ""
date := ""
var references []appv1.RevisionReference
if dryCommitMetadata != nil {
author = dryCommitMetadata.Author
message = dryCommitMetadata.Message
if dryCommitMetadata.Date != nil {
date = dryCommitMetadata.Date.Format(time.RFC3339)
}
references = dryCommitMetadata.References
hydratorMetadata, err := hydrator.GetCommitMetadata(repoUrl, drySha, dryCommitMetadata)
if err != nil {
return fmt.Errorf("failed to retrieve hydrator metadata: %w", err)
}
subject, body, _ := strings.Cut(message, "\n\n")
_, bodyMinusTrailers := git.GetReferences(log.WithFields(log.Fields{"repo": repoUrl, "revision": drySha}), body)
// Write the top-level readme.
err := writeMetadata(root, "", hydratorMetadataFile{DrySHA: drySha, RepoURL: repoUrl, Author: author, Subject: subject, Body: bodyMinusTrailers, Date: date, References: references})
err = writeMetadata(root, "", hydratorMetadata)
if err != nil {
return fmt.Errorf("failed to write top-level hydrator metadata: %w", err)
}
@@ -86,7 +72,7 @@ func WriteForPaths(root *os.Root, repoUrl, drySha string, dryCommitMetadata *app
}
// Write hydrator.metadata containing information about the hydration process.
hydratorMetadata := hydratorMetadataFile{
hydratorMetadata := hydrator.HydratorCommitMetadata{
Commands: p.Commands,
DrySHA: drySha,
RepoURL: repoUrl,
@@ -106,7 +92,7 @@ func WriteForPaths(root *os.Root, repoUrl, drySha string, dryCommitMetadata *app
}
// writeMetadata writes the metadata to the hydrator.metadata file.
func writeMetadata(root *os.Root, dirPath string, metadata hydratorMetadataFile) error {
func writeMetadata(root *os.Root, dirPath string, metadata hydrator.HydratorCommitMetadata) error {
hydratorMetadataPath := filepath.Join(dirPath, "hydrator.metadata")
f, err := root.Create(hydratorMetadataPath)
if err != nil {
@@ -125,7 +111,7 @@ func writeMetadata(root *os.Root, dirPath string, metadata hydratorMetadataFile)
}
// writeReadme writes the readme to the README.md file.
func writeReadme(root *os.Root, dirPath string, metadata hydratorMetadataFile) error {
func writeReadme(root *os.Root, dirPath string, metadata hydrator.HydratorCommitMetadata) error {
readmeTemplate, err := template.New("readme").Funcs(sprigFuncMap).Parse(manifestHydrationReadmeTemplate)
if err != nil {
return fmt.Errorf("failed to parse readme template: %w", err)

View File

@@ -18,6 +18,7 @@ import (
"github.com/argoproj/argo-cd/v3/commitserver/apiclient"
appsv1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v3/util/hydrator"
)
// tempRoot creates a temporary directory and returns an os.Root object for it.
@@ -144,7 +145,7 @@ Argocd-reference-commit-sha: abc123
func TestWriteMetadata(t *testing.T) {
root := tempRoot(t)
metadata := hydratorMetadataFile{
metadata := hydrator.HydratorCommitMetadata{
RepoURL: "https://github.com/example/repo",
DrySHA: "abc123",
}
@@ -156,7 +157,7 @@ func TestWriteMetadata(t *testing.T) {
metadataBytes, err := os.ReadFile(metadataPath)
require.NoError(t, err)
var readMetadata hydratorMetadataFile
var readMetadata hydrator.HydratorCommitMetadata
err = json.Unmarshal(metadataBytes, &readMetadata)
require.NoError(t, err)
assert.Equal(t, metadata, readMetadata)
@@ -171,7 +172,7 @@ func TestWriteReadme(t *testing.T) {
hash := sha256.Sum256(randomData)
sha := hex.EncodeToString(hash[:])
metadata := hydratorMetadataFile{
metadata := hydrator.HydratorCommitMetadata{
RepoURL: "https://github.com/example/repo",
DrySHA: "abc123",
References: []appsv1.RevisionReference{

View File

@@ -16,6 +16,7 @@ import (
"github.com/argoproj/argo-cd/v3/reposerver/apiclient"
applog "github.com/argoproj/argo-cd/v3/util/app/log"
"github.com/argoproj/argo-cd/v3/util/git"
"github.com/argoproj/argo-cd/v3/util/hydrator"
utilio "github.com/argoproj/argo-cd/v3/util/io"
)
@@ -59,6 +60,9 @@ type Dependencies interface {
// AddHydrationQueueItem adds a hydration queue item to the queue. This is used to trigger the hydration process for
// a group of applications which are hydrating to the same repo and target branch.
AddHydrationQueueItem(key types.HydrationQueueKey)
// GetHydratorCommitMessageTemplate gets the configured template for rendering commit messages.
GetHydratorCommitMessageTemplate() (string, error)
}
// Hydrator is the main struct that implements the hydration logic. It uses the Dependencies interface to access the
@@ -340,13 +344,22 @@ func (h *Hydrator) hydrate(logCtx *log.Entry, apps []*appv1.Application) (string
}
logCtx.Warn("no credentials found for repo, continuing without credentials")
}
// get the commit message template
commitMessageTemplate, err := h.dependencies.GetHydratorCommitMessageTemplate()
if err != nil {
return "", "", fmt.Errorf("failed to get hydrated commit message template: %w", err)
}
commitMessage, errMsg := getTemplatedCommitMessage(repoURL, targetRevision, commitMessageTemplate, revisionMetadata)
if errMsg != nil {
return "", "", fmt.Errorf("failed to get hydrator commit templated message: %w", errMsg)
}
manifestsRequest := commitclient.CommitHydratedManifestsRequest{
Repo: repo,
SyncBranch: syncBranch,
TargetBranch: targetBranch,
DrySha: targetRevision,
CommitMessage: "[Argo CD Bot] hydrate " + targetRevision,
CommitMessage: commitMessage,
Paths: paths,
DryCommitMetadata: revisionMetadata,
}
@@ -411,3 +424,18 @@ func appNeedsHydration(app *appv1.Application, statusHydrateTimeout time.Duratio
return false, ""
}
// Gets the multi-line commit message based on the template defined in the configmap. It is a two step process:
// 1. Get the metadata template engine would use to render the template
// 2. Pass the output of Step 1 and Step 2 to template Render
func getTemplatedCommitMessage(repoURL, revision, commitMessageTemplate string, dryCommitMetadata *appv1.RevisionMetadata) (string, error) {
hydratorCommitMetadata, err := hydrator.GetCommitMetadata(repoURL, revision, dryCommitMetadata)
if err != nil {
return "", fmt.Errorf("failed to get hydrated commit message: %w", err)
}
templatedCommitMsg, err := hydrator.Render(commitMessageTemplate, hydratorCommitMetadata)
if err != nil {
return "", fmt.Errorf("failed to parse template %s: %w", commitMessageTemplate, err)
}
return templatedCommitMsg, nil
}

View File

@@ -13,8 +13,15 @@ import (
"github.com/argoproj/argo-cd/v3/controller/hydrator/mocks"
"github.com/argoproj/argo-cd/v3/controller/hydrator/types"
"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v3/util/settings"
)
var message = `testn
Argocd-reference-commit-repourl: https://github.com/test/argocd-example-apps
Argocd-reference-commit-author: Argocd-reference-commit-author
Argocd-reference-commit-subject: testhydratormd
Signed-off-by: testUser <test@gmail.com>`
func Test_appNeedsHydration(t *testing.T) {
t.Parallel()
@@ -167,3 +174,80 @@ func Test_getRelevantAppsForHydration_RepoURLNormalization(t *testing.T) {
require.NoError(t, err)
assert.Len(t, relevantApps, 2, "Expected both apps to be considered relevant despite URL differences")
}
func TestHydrator_getTemplatedCommitMessage(t *testing.T) {
references := make([]v1alpha1.RevisionReference, 0)
revReference := v1alpha1.RevisionReference{
Commit: &v1alpha1.CommitMetadata{
Author: "testAuthor",
Subject: "test",
RepoURL: "https://github.com/test/argocd-example-apps",
SHA: "3ff41cc5247197a6caf50216c4c76cc29d78a97c",
},
}
references = append(references, revReference)
type args struct {
repoURL string
revision string
dryCommitMetadata *v1alpha1.RevisionMetadata
template string
}
tests := []struct {
name string
args args
want string
wantErr bool
}{
{
name: "test template",
args: args{
repoURL: "https://github.com/test/argocd-example-apps",
revision: "3ff41cc5247197a6caf50216c4c76cc29d78a97d",
dryCommitMetadata: &v1alpha1.RevisionMetadata{
Author: "test test@test.com",
Date: &metav1.Time{
Time: metav1.Now().Time,
},
Message: message,
References: references,
},
template: settings.CommitMessageTemplate,
},
want: `3ff41cc: testn
Argocd-reference-commit-repourl: https://github.com/test/argocd-example-apps
Argocd-reference-commit-author: Argocd-reference-commit-author
Argocd-reference-commit-subject: testhydratormd
Signed-off-by: testUser <test@gmail.com>
Co-authored-by: testAuthor
Co-authored-by: test test@test.com
`,
},
{
name: "test empty template",
args: args{
repoURL: "https://github.com/test/argocd-example-apps",
revision: "3ff41cc5247197a6caf50216c4c76cc29d78a97d",
dryCommitMetadata: &v1alpha1.RevisionMetadata{
Author: "test test@test.com",
Date: &metav1.Time{
Time: metav1.Now().Time,
},
Message: message,
References: references,
},
},
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := getTemplatedCommitMessage(tt.args.repoURL, tt.args.revision, tt.args.template, tt.args.dryCommitMetadata)
if (err != nil) != tt.wantErr {
t.Errorf("Hydrator.getHydratorCommitMessage() error = %v, wantErr %v", err, tt.wantErr)
return
}
assert.Equal(t, tt.want, got)
})
}
}

View File

@@ -81,6 +81,59 @@ func (_c *Dependencies_AddHydrationQueueItem_Call) RunAndReturn(run func(key typ
return _c
}
// GetHydratorCommitMessageTemplate provides a mock function for the type Dependencies
func (_mock *Dependencies) GetHydratorCommitMessageTemplate() (string, error) {
ret := _mock.Called()
if len(ret) == 0 {
panic("no return value specified for GetHydratorCommitMessageTemplate")
}
var r0 string
var r1 error
if returnFunc, ok := ret.Get(0).(func() (string, error)); ok {
return returnFunc()
}
if returnFunc, ok := ret.Get(0).(func() string); ok {
r0 = returnFunc()
} else {
r0 = ret.Get(0).(string)
}
if returnFunc, ok := ret.Get(1).(func() error); ok {
r1 = returnFunc()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Dependencies_GetHydratorCommitMessageTemplate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetHydratorCommitMessageTemplate'
type Dependencies_GetHydratorCommitMessageTemplate_Call struct {
*mock.Call
}
// GetHydratorCommitMessageTemplate is a helper method to define mock.On call
func (_e *Dependencies_Expecter) GetHydratorCommitMessageTemplate() *Dependencies_GetHydratorCommitMessageTemplate_Call {
return &Dependencies_GetHydratorCommitMessageTemplate_Call{Call: _e.mock.On("GetHydratorCommitMessageTemplate")}
}
func (_c *Dependencies_GetHydratorCommitMessageTemplate_Call) Run(run func()) *Dependencies_GetHydratorCommitMessageTemplate_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *Dependencies_GetHydratorCommitMessageTemplate_Call) Return(s string, err error) *Dependencies_GetHydratorCommitMessageTemplate_Call {
_c.Call.Return(s, err)
return _c
}
func (_c *Dependencies_GetHydratorCommitMessageTemplate_Call) RunAndReturn(run func() (string, error)) *Dependencies_GetHydratorCommitMessageTemplate_Call {
_c.Call.Return(run)
return _c
}
// GetProcessableAppProj provides a mock function for the type Dependencies
func (_mock *Dependencies) GetProcessableAppProj(app *v1alpha1.Application) (*v1alpha1.AppProject, error) {
ret := _mock.Called(app)

View File

@@ -97,3 +97,12 @@ func (ctrl *ApplicationController) PersistAppHydratorStatus(orig *appv1.Applicat
func (ctrl *ApplicationController) AddHydrationQueueItem(key types.HydrationQueueKey) {
ctrl.hydrationQueue.AddRateLimited(key)
}
func (ctrl *ApplicationController) GetHydratorCommitMessageTemplate() (string, error) {
sourceHydratorCommitMessageKey, err := ctrl.settingsMgr.GetSourceHydratorCommitMessageTemplate()
if err != nil {
return "", fmt.Errorf("failed to get sourceHydrator commit message template key: %w", err)
}
return sourceHydratorCommitMessageKey, nil
}

View File

@@ -14,6 +14,7 @@ import (
"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v3/reposerver/apiclient"
"github.com/argoproj/argo-cd/v3/test"
"github.com/argoproj/argo-cd/v3/util/settings"
)
func TestGetRepoObjs(t *testing.T) {
@@ -77,3 +78,46 @@ func TestGetRepoObjs(t *testing.T) {
assert.Equal(t, "ConfigMap", objs[0].GetKind())
}
func TestGetHydratorCommitMessageTemplate_WhenTemplateisNotDefined_FallbackToDefault(t *testing.T) {
cm := test.NewConfigMap()
cmBytes, _ := json.Marshal(cm)
data := fakeData{
manifestResponse: &apiclient.ManifestResponse{
Manifests: []string{string(cmBytes)},
Namespace: test.FakeDestNamespace,
Server: test.FakeClusterURL,
Revision: "abc123",
},
}
ctrl := newFakeControllerWithResync(&data, time.Minute, nil, errors.New("this should not be called"))
tmpl, err := ctrl.GetHydratorCommitMessageTemplate()
require.NoError(t, err)
assert.NotEmpty(t, tmpl) // should fallback to default
assert.Equal(t, settings.CommitMessageTemplate, tmpl)
}
func TestGetHydratorCommitMessageTemplate(t *testing.T) {
cm := test.NewFakeConfigMap()
cm.Data["sourceHydrator.commitMessageTemplate"] = settings.CommitMessageTemplate
cmBytes, _ := json.Marshal(cm)
data := fakeData{
manifestResponse: &apiclient.ManifestResponse{
Manifests: []string{string(cmBytes)},
Namespace: test.FakeDestNamespace,
Server: test.FakeClusterURL,
Revision: "abc123",
},
configMapData: cm.Data,
}
ctrl := newFakeControllerWithResync(&data, time.Minute, nil, errors.New("this should not be called"))
tmpl, err := ctrl.GetHydratorCommitMessageTemplate()
require.NoError(t, err)
assert.NotEmpty(t, tmpl)
}

View File

@@ -439,3 +439,22 @@ data:
# application.sync.impersonation.enabled enables application sync to use a custom service account, via impersonation. This allows decoupling sync from control-plane service account.
application.sync.impersonation.enabled: "false"
### SourceHydrator commit message template.
# This template iterates through the fields in the `.metadata` object,
# and formats them based on their type (map, array, or primitive values).
# This is the default template and targets specific metadata properties
sourceHydrator.commitMessageTemplate: |
{{.metadata.drySha | trunc 7}}: {{ .metadata.subject }}
{{- if .metadata.body }}
{{ .metadata.body }}
{{- end }}
{{ range $ref := .metadata.references }}
{{- if and $ref.commit $ref.commit.author }}
Co-authored-by: {{ $ref.commit.author }}
{{- end }}
{{- end }}
{{- if .metadata.author }}
Co-authored-by: {{ .metadata.author }}
{{- end }}

View File

@@ -262,6 +262,34 @@ specified more than once, the last one will be used.
All trailers are optional. If a trailer is not specified, the corresponding field in the metadata will be omitted.
## Commit Message Template
The commit message is generated using a [Go text/template](https://pkg.go.dev/text/template), optionally configured by the user via the argocd-cm ConfigMap. The template is rendered using the values from `hydrator.metadata`. The template can be multi-line, allowing users to define a subject line, body and optional trailers. To define the commit message template, you need to set the `sourceHydrator.commitMessageTemplate` field in argocd-cm ConfigMap.
The template may functions from the [Sprig function library](https://github.com/Masterminds/sprig).
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-cm
namespace: argocd
data:
sourceHydrator.commitMessageTemplate: |
{{.metadata.drySha | trunc 7}}: {{ .metadata.subject }}
{{- if .metadata.body }}
{{ .metadata.body }}
{{- end }}
{{ range $ref := .metadata.references }}
{{- if and $ref.commit $ref.commit.author }}
Co-authored-by: {{ $ref.commit.author }}
{{- end }}
{{- end }}
{{- if .metadata.author }}
Co-authored-by: {{ .metadata.author }}
{{- end }}
### Credential Templates
Credential templates allow a single credential to be used for multiple repositories. The source hydrator supports credential templates. For example, if you setup credential templates for the URL prefix `https://github.com/argoproj`, these credentials will be used for all repositories with this URL as prefix (e.g. `https://github.com/argoproj/argocd-example-apps`) that do not have their own credentials configured.

60
util/hydrator/hydrator.go Normal file
View File

@@ -0,0 +1,60 @@
package hydrator
import (
"strings"
"time"
log "github.com/sirupsen/logrus"
appv1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v3/util/git"
)
// HydratorCommitMetadata defines the struct used by both Controller and commitServer
// to define the templated commit message and the hydrated manifest
type HydratorCommitMetadata struct {
RepoURL string `json:"repoURL,omitempty"`
DrySHA string `json:"drySha,omitempty"`
Commands []string `json:"commands,omitempty"`
Author string `json:"author,omitempty"`
Date string `json:"date,omitempty"`
// Subject is the subject line of the DRY commit message, i.e. `git show --format=%s`.
Subject string `json:"subject,omitempty"`
// Body is the body of the DRY commit message, excluding the subject line, i.e. `git show --format=%b`.
// Known Argocd- trailers with valid values are removed, but all other trailers are kept.
Body string `json:"body,omitempty"`
References []appv1.RevisionReference `json:"references,omitempty"`
}
// GetCommitMetadata takes repo, drySha and commitMetadata and returns a HydratorCommitMetadata which is a
// common contract controller and commitServer
func GetCommitMetadata(repoUrl, drySha string, dryCommitMetadata *appv1.RevisionMetadata) (HydratorCommitMetadata, error) { //nolint:revive //FIXME(var-naming)
author := ""
message := ""
date := ""
var references []appv1.RevisionReference
if dryCommitMetadata != nil {
author = dryCommitMetadata.Author
message = dryCommitMetadata.Message
if dryCommitMetadata.Date != nil {
date = dryCommitMetadata.Date.Format(time.RFC3339)
}
references = dryCommitMetadata.References
}
subject, body, _ := strings.Cut(message, "\n\n")
_, bodyMinusTrailers := git.GetReferences(log.WithFields(log.Fields{"repo": repoUrl, "revision": drySha}), body)
hydratorCommitMetadata := HydratorCommitMetadata{
RepoURL: repoUrl,
DrySHA: drySha,
Author: author,
Subject: subject,
Body: bodyMinusTrailers,
Date: date,
References: references,
}
return hydratorCommitMetadata, nil
}

View File

@@ -0,0 +1,74 @@
package hydrator
import (
"reflect"
"testing"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
appv1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
)
func TestGetCommitMetadata(t *testing.T) {
repoURL := "https://github.com/test/argocd-example-apps"
drySHA := "3ff41cc5247197a6caf50216c4c76cc29d78a97d"
date := &metav1.Time{Time: metav1.Now().Time}
revisionAuthor := "test test@test.com"
references := make([]appv1.RevisionReference, 0)
revReference := appv1.RevisionReference{
Commit: &appv1.CommitMetadata{
Author: "testAuthor",
Subject: "test",
RepoURL: repoURL,
SHA: "3ff41cc5247197a6caf50216c4c76cc29d78a97c",
},
}
references = append(references, revReference)
hydratedCommitMetadata := HydratorCommitMetadata{
RepoURL: repoURL,
DrySHA: drySHA,
Author: revisionAuthor,
Date: date.Format(time.RFC3339),
References: references,
Subject: "testMessage",
}
type args struct {
repoURL string
drySha string
dryCommitMetadata *appv1.RevisionMetadata
}
tests := []struct {
name string
args args
want HydratorCommitMetadata
wantErr bool
}{
{
name: "test GetHydratorCommitMD",
args: args{
repoURL: repoURL,
drySha: drySHA,
dryCommitMetadata: &appv1.RevisionMetadata{
Author: revisionAuthor,
Date: date,
Message: "testMessage",
References: references,
},
},
want: hydratedCommitMetadata,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := GetCommitMetadata(tt.args.repoURL, tt.args.drySha, tt.args.dryCommitMetadata)
if (err != nil) != tt.wantErr {
t.Errorf("GetCommitMetadata() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("GetCommitMetadata() = %v, want %v", got, tt.want)
}
})
}
}

61
util/hydrator/template.go Normal file
View File

@@ -0,0 +1,61 @@
package hydrator
import (
"bytes"
"encoding/json"
"fmt"
"text/template"
"github.com/Masterminds/sprig/v3"
)
var sprigFuncMap = sprig.GenericFuncMap() // a singleton for better performance
func init() {
// Avoid allowing the user to learn things about the environment.
delete(sprigFuncMap, "env")
delete(sprigFuncMap, "expandenv")
delete(sprigFuncMap, "getHostByName")
}
// Render use a parsed template and calls the Execute to apply the data.
// currently the method supports struct and a map[string]any as data
func Render(tmpl string, data HydratorCommitMetadata) (string, error) {
var dataMap map[string]any
var err error
// short-circuit if template is not defined
if tmpl == "" {
return "", nil
}
dataMap, err = structToMap(data)
if err != nil {
return "", fmt.Errorf("marshaling failed: %w", err)
}
metadata := map[string]any{
"metadata": dataMap,
}
template, err := template.New("commit-template").Funcs(sprigFuncMap).Parse(tmpl)
if err != nil {
return "", fmt.Errorf("failed to parse template %s: %w", tmpl, err)
}
var replacedTmplBuffer bytes.Buffer
if err = template.Execute(&replacedTmplBuffer, metadata); err != nil {
return "", fmt.Errorf("failed to execute go template %s: %w", tmpl, err)
}
return replacedTmplBuffer.String(), nil
}
func structToMap(s any) (map[string]any, error) {
jsonOut, err := json.Marshal(s)
if err != nil {
return nil, err
}
var result map[string]any
err = json.Unmarshal(jsonOut, &result)
if err != nil {
return nil, err
}
return result, nil
}

View File

@@ -0,0 +1,99 @@
package hydrator
import (
"testing"
"github.com/stretchr/testify/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v3/util/settings"
)
func TestRender(t *testing.T) {
tests := []struct {
name string
metadata HydratorCommitMetadata
want string
wantErr bool
}{
{
name: "author and multiple references",
metadata: HydratorCommitMetadata{
RepoURL: "https://github.com/test/argocd-example-apps",
DrySHA: "3ff41cc5247197a6caf50216c4c76cc29d78a97d",
Author: "test <test@test.com>",
Date: metav1.Now().String(),
References: []v1alpha1.RevisionReference{
{
Commit: &v1alpha1.CommitMetadata{
Author: "ref test <ref-test@test.com>",
Subject: "test",
RepoURL: "https://github.com/test/argocd-example-apps",
SHA: "3ff41cc5247197a6caf50216c4c76cc29d78a97c",
},
},
{
Commit: &v1alpha1.CommitMetadata{
Author: "ref test 2 <ref-test-2@test.com>",
Subject: "test 2",
RepoURL: "https://github.com/test/argocd-example-apps",
SHA: "abc12345678912345678912345678912345678912",
},
},
},
Body: "testBody",
Subject: "testSubject",
},
want: `3ff41cc: testSubject
testBody
Co-authored-by: ref test <ref-test@test.com>
Co-authored-by: ref test 2 <ref-test-2@test.com>
Co-authored-by: test <test@test.com>
`,
},
{
name: "no references",
metadata: HydratorCommitMetadata{
RepoURL: "https://github.com/test/argocd-example-apps",
DrySHA: "3ff41cc5247197a6caf50216c4c76cc29d78a97d",
Author: "test <test@test.com>",
Date: metav1.Now().String(),
Body: "testBody",
Subject: "testSubject",
},
want: `3ff41cc: testSubject
testBody
Co-authored-by: test <test@test.com>
`,
},
{
name: "no body",
metadata: HydratorCommitMetadata{
RepoURL: "https://github.com/test/argocd-example-apps",
DrySHA: "3ff41cc5247197a6caf50216c4c76cc29d78a97d",
Author: "test <test@test.com>",
Date: metav1.Now().String(),
Subject: "testSubject",
},
want: `3ff41cc: testSubject
Co-authored-by: test <test@test.com>
`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Render(settings.CommitMessageTemplate, tt.metadata)
if (err != nil) != tt.wantErr {
t.Errorf("Render() error = %v, wantErr %v", err, tt.wantErr)
return
}
assert.Equal(t, tt.want, got)
})
}
}

View File

@@ -45,6 +45,21 @@ import (
tlsutil "github.com/argoproj/argo-cd/v3/util/tls"
)
var CommitMessageTemplate = `{{.metadata.drySha | trunc 7}}: {{ .metadata.subject }}
{{- if .metadata.body }}
{{ .metadata.body }}
{{- end }}
{{ range $ref := .metadata.references }}
{{- if and $ref.commit $ref.commit.author }}
Co-authored-by: {{ $ref.commit.author }}
{{- end }}
{{- end }}
{{- if .metadata.author }}
Co-authored-by: {{ .metadata.author }}
{{- end }}
`
// ArgoCDSettings holds in-memory runtime configuration options.
type ArgoCDSettings struct {
// URL is the externally facing URL users will visit to reach Argo CD.
@@ -494,6 +509,8 @@ const (
settingUIBannerPositionKey = "ui.bannerposition"
// settingsBinaryUrlsKey designates the key for the argocd binary URLs
settingsBinaryUrlsKey = "help.download"
// settingsApplicationInstanceLabelKey is the key to configure injected app instance label key
settingsSourceHydratorCommitMessageTemplateKey = "sourceHydrator.commitMessageTemplate"
// globalProjectsKey designates the key for global project settings
globalProjectsKey = "globalProjects"
// initialPasswordSecretName is the name of the secret that will hold the initial admin password
@@ -1005,6 +1022,17 @@ func (mgr *SettingsManager) GetResourceOverrides() (map[string]v1alpha1.Resource
return resourceOverrides, nil
}
func (mgr *SettingsManager) GetSourceHydratorCommitMessageTemplate() (string, error) {
argoCDCM, err := mgr.getConfigMap()
if err != nil {
return "", err
}
if argoCDCM.Data[settingsSourceHydratorCommitMessageTemplateKey] == "" {
return CommitMessageTemplate, nil // in case template is not defined return default
}
return argoCDCM.Data[settingsSourceHydratorCommitMessageTemplateKey], nil
}
func addStatusOverrideToGK(resourceOverrides map[string]v1alpha1.ResourceOverride, groupKind string) {
if val, ok := resourceOverrides[groupKind]; ok {
val.IgnoreDifferences.JSONPointers = append(val.IgnoreDifferences.JSONPointers, "/status")