From b7d8028656ff2760c0e326037c87460ac751abcd Mon Sep 17 00:00:00 2001 From: Remco Beckers Date: Thu, 15 Oct 2026 17:52:14 +0200 Subject: [PATCH 1/8] Group compatible Go minor updates --- README.md | 15 +++--- renovate.json | 83 +++++++++++++++++++++++++------- scripts/renovate-config-test.cjs | 48 +++++++++++++----- 3 files changed, 109 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 531f8aa..794d69a 100644 --- a/README.md +++ b/README.md @@ -461,18 +461,21 @@ Renovate runs daily and keeps compiler updates separate from library updates. GitHub Actions patches, minors, and digest updates use a separate automerge PR. Major action upgrades require manual review. CI tools use a separate manual group; their major upgrades get individual PRs. -Module patches and `golang.org/x/*` module minors share one library PR. -Manual AWS SDK and Kubernetes client updates have separate groups. -Other manual module updates get individual PRs. +Module patches share one library PR, separate from minor updates. +Module minors use separate AWS SDK, Kubernetes, ClickHouse, Elasticsearch, +OpenTelemetry, OpenAPI, and `golang.org/x/*` groups. +Other module minors get individual PRs. +Module patches and minors use native automerge after artifact validation and App approval. +Renovate limits concurrent PRs and inherits the shared hourly creation limit. Compiler updates stay within the release line declared in `renovate.json`. Module candidates that declare a newer required Go release line are filtered out. CI rejects a module update that selects a compiler from another release line. Manual groups rebase only when conflicting; automatic groups stay current with main. The shared approval action approves verified Renovate commits with native automerge enabled. GitHub requires the existing CI checks and an approving review before merging. -Compiler patches and the library group use native automerge after artifact validation and App approval. -The library group includes direct and indirect modules. -Other module minors, module majors, and Go release-line migrations require manual review. +Compiler patches use native automerge after artifact validation and App approval. +Library updates include direct and indirect modules. +Module majors and Go release-line migrations require individual manual review. Dependency updates reach published binaries through the existing tag release process. The approval workflow calls the pinned public `renovate-config` action directly. diff --git a/renovate.json b/renovate.json index 7c5280b..32cd831 100644 --- a/renovate.json +++ b/renovate.json @@ -4,8 +4,7 @@ "github>StackVista/renovate-config", "github>StackVista/renovate-config:go" ], - "prConcurrentLimit": 0, - "prHourlyLimit": 0, + "prConcurrentLimit": 5, "rebaseWhen": "behind-base-branch", "packageRules": [ { @@ -56,21 +55,23 @@ } }, { - "description": "Review other module minors individually without rebasing for unrelated changes", + "description": "Automerge individual module minors after required validation and App approval", "matchManagers": ["gomod"], "matchDepTypes": ["require", "indirect", "replace"], "matchUpdateTypes": ["minor"], "groupName": null, "groupSlug": null, - "automerge": false, - "rebaseWhen": "conflicted" + "automerge": true, + "automergeType": "pr", + "platformAutomerge": true, + "rebaseWhen": "behind-base-branch" }, { - "description": "Review coupled AWS SDK updates together", + "description": "Group coupled AWS SDK minor updates", "matchManagers": ["gomod"], "matchDepTypes": ["require", "indirect", "replace"], "matchPackageNames": ["github.com/aws/aws-sdk-go-v2{,/**}", "github.com/aws/smithy-go"], - "matchUpdateTypes": ["minor", "major"], + "matchUpdateTypes": ["minor"], "groupName": "AWS Go SDK", "groupSlug": "go-aws-sdk", "group": { @@ -78,11 +79,11 @@ } }, { - "description": "Review coupled Kubernetes clients and schema migrations together", + "description": "Group coupled Kubernetes client minor updates", "matchManagers": ["gomod"], "matchDepTypes": ["require", "indirect", "replace"], "matchPackageNames": ["k8s.io/**", "sigs.k8s.io/**"], - "matchUpdateTypes": ["minor", "major"], + "matchUpdateTypes": ["minor"], "groupName": "Kubernetes Go clients", "groupSlug": "go-kubernetes", "group": { @@ -90,10 +91,57 @@ } }, { - "description": "Keep automerged patches available alongside manual module minors", + "description": "Group coupled ClickHouse client minor updates", + "matchManagers": ["gomod"], + "matchDepTypes": ["require", "indirect", "replace"], + "matchPackageNames": ["github.com/ClickHouse/**"], + "matchUpdateTypes": ["minor"], + "groupName": "ClickHouse Go clients", + "groupSlug": "go-clickhouse", + "group": { + "branchTopic": "go-clickhouse" + } + }, + { + "description": "Group coupled Elasticsearch client minor updates", + "matchManagers": ["gomod"], + "matchDepTypes": ["require", "indirect", "replace"], + "matchPackageNames": ["github.com/elastic/**"], + "matchUpdateTypes": ["minor"], + "groupName": "Elasticsearch Go clients", + "groupSlug": "go-elasticsearch", + "group": { + "branchTopic": "go-elasticsearch" + } + }, + { + "description": "Group coupled OpenTelemetry module minor updates", + "matchManagers": ["gomod"], + "matchDepTypes": ["require", "indirect", "replace"], + "matchPackageNames": ["go.opentelemetry.io/**"], + "matchUpdateTypes": ["minor"], + "groupName": "OpenTelemetry Go modules", + "groupSlug": "go-opentelemetry", + "group": { + "branchTopic": "go-opentelemetry" + } + }, + { + "description": "Group coupled OpenAPI module minor updates", + "matchManagers": ["gomod"], + "matchDepTypes": ["require", "indirect", "replace"], + "matchPackageNames": ["github.com/go-openapi/**"], + "matchUpdateTypes": ["minor"], + "groupName": "OpenAPI Go modules", + "groupSlug": "go-openapi", + "group": { + "branchTopic": "go-openapi" + } + }, + { + "description": "Keep module patches separate from minor updates", "matchManagers": ["gomod"], "matchDepTypes": ["require", "indirect", "replace"], - "matchPackageNames": ["!golang.org/x/**"], "separateMinorPatch": true }, { @@ -130,17 +178,16 @@ "platformAutomerge": true }, { - "description": "Include golang.org/x module minors in the library group", + "description": "Group golang.org/x module minor updates separately from patches", "matchManagers": ["gomod"], "matchDepTypes": ["require", "indirect", "replace"], "matchPackageNames": ["golang.org/x/**"], "matchUpdateTypes": ["minor"], - "groupName": "Go library updates", - "groupSlug": "go-libraries", - "automerge": true, - "automergeType": "pr", - "platformAutomerge": true, - "rebaseWhen": "behind-base-branch" + "groupName": "Go supplementary modules", + "groupSlug": "go-x", + "group": { + "branchTopic": "go-x" + } } ] } diff --git a/scripts/renovate-config-test.cjs b/scripts/renovate-config-test.cjs index 1428ff7..8a17a4c 100644 --- a/scripts/renovate-config-test.cjs +++ b/scripts/renovate-config-test.cjs @@ -29,6 +29,8 @@ async function main() { extends: [], semanticCommits: 'disabled', }); + assert.equal(config.prConcurrentLimit, 5); + assert(config.prHourlyLimit > 0); getDatasourceFor('go').getReleases = async () => ({ releases: [ ['v1.0.0', '1.18.0'], @@ -64,8 +66,9 @@ async function main() { assert.equal(patch.branchName, 'renovate/go-libraries'); assert.equal(patch.automerge, true); assert.equal(patch.rebaseWhen, 'behind-base-branch'); - assert.equal(minor.automerge, false); - assert.equal(minor.rebaseWhen, 'conflicted'); + assert.equal(minor.automerge, true); + assert.equal(minor.platformAutomerge, true); + assert.equal(minor.rebaseWhen, 'behind-base-branch'); assert.notEqual(patch.branchName, minor.branchName); const libraries = await flattenUpdates(config, { gomod: [{ @@ -83,9 +86,9 @@ async function main() { }); assert.deepEqual( [...new Set(libraries.filter(branch => branch.automerge).map(branch => branch.branchName))], - ['renovate/go-libraries'], + ['renovate/go-libraries', minor.branchName, 'renovate/go-x'], ); - assert.equal(libraries.filter(branch => branch.automerge).length, 2); + assert.equal(libraries.filter(branch => branch.automerge).length, 3); } const actionBranches = await flattenUpdates(config, { 'github-actions': [{ @@ -144,15 +147,28 @@ async function main() { const modules = [ ['github.com/aws/aws-sdk-go-v2', 'minor', 'go-aws-sdk'], ['github.com/aws/aws-sdk-go-v2/config', 'minor', 'go-aws-sdk'], - ['github.com/aws/smithy-go', 'major', 'go-aws-sdk'], + ['github.com/aws/smithy-go', 'minor', 'go-aws-sdk'], ['k8s.io/api', 'minor', 'go-kubernetes'], ['k8s.io/apimachinery', 'minor', 'go-kubernetes'], ['k8s.io/client-go', 'minor', 'go-kubernetes'], - ['sigs.k8s.io/structured-merge-diff/v6', 'major', 'go-kubernetes'], + ['sigs.k8s.io/structured-merge-diff/v6', 'minor', 'go-kubernetes'], + ['github.com/ClickHouse/clickhouse-go/v2', 'minor', 'go-clickhouse'], + ['github.com/ClickHouse/ch-go', 'minor', 'go-clickhouse'], + ['github.com/elastic/go-elasticsearch/v8', 'minor', 'go-elasticsearch'], + ['github.com/elastic/elastic-transport-go/v8', 'minor', 'go-elasticsearch'], + ['go.opentelemetry.io/otel', 'minor', 'go-opentelemetry'], + ['go.opentelemetry.io/otel/trace', 'minor', 'go-opentelemetry'], + ['github.com/go-openapi/jsonpointer', 'minor', 'go-openapi'], + ['github.com/go-openapi/swag', 'minor', 'go-openapi'], + ['golang.org/x/net', 'minor', 'go-x'], + ['golang.org/x/sys', 'minor', 'go-x'], + ['github.com/aws/smithy-go', 'major', null], + ['sigs.k8s.io/structured-merge-diff/v6', 'major', null], ['github.com/elastic/go-elasticsearch/v8', 'major', null], + ['github.com/go-openapi/jsonpointer', 'major', null], ['github.com/stretchr/testify', 'minor', null], ]; - const manual = await flattenUpdates(config, { + const moduleBranches = await flattenUpdates(config, { gomod: [{ packageFile: 'go.mod', deps: modules.map(([name, type]) => ({ @@ -165,10 +181,15 @@ async function main() { })), }], }); - for (const branch of manual) { - const group = modules.find(([name]) => name === branch.depName)[2]; - assert.equal(branch.automerge, false); - assert.equal(branch.rebaseWhen, 'conflicted'); + for (const branch of moduleBranches) { + const group = modules.find(([name, type]) => + name === branch.depName && type === branch.updateType)[2]; + assert.equal(branch.automerge, branch.updateType === 'minor'); + assert.equal(branch.rebaseWhen, branch.updateType === 'minor' ? 'behind-base-branch' : 'conflicted'); + if (branch.automerge) { + assert.equal(branch.automergeType, 'pr'); + assert.equal(branch.platformAutomerge, true); + } if (group) assert.equal(branch.branchName, `renovate/${group}`); else assert.equal(branch.groupName, null); } @@ -194,9 +215,10 @@ async function main() { assert.equal(policy.groupName, type === 'major' ? null : 'CI tools', name); } } - console.log('PASS: available module patches automerge independently of manual minors'); + console.log('PASS: module patches and minors automerge in separate branches'); console.log('PASS: compatible GitHub Actions updates automerge separately; major actions stay manual'); - console.log('PASS: unsupported Go versions are filtered; manual families and CI tools stay separate'); + console.log('PASS: module minor families are isolated; majors and CI tools stay manual'); + console.log('PASS: unsupported Go versions are filtered and PR creation is limited'); } main().catch(error => { From f6b1bbb816b01f8d8528f57d23002606b79235f0 Mon Sep 17 00:00:00 2001 From: Remco Beckers Date: Thu, 15 Oct 2026 19:45:50 +0200 Subject: [PATCH 2/8] Fix goconst compatibility with newer lint --- .golangci.yml | 4 ++++ cmd/settings/check_and_finalize.go | 2 +- cmd/settings/restore.go | 2 +- cmd/settings/settings.go | 4 +++- cmd/stackgraphv2/restore.go | 15 +++++++++------ cmd/stackgraphv2/simplejob.go | 12 ++++++------ .../orchestration/replication/applicability.go | 15 +++++++++------ internal/orchestration/replication/checker.go | 17 +++++++++++------ .../orchestration/replication/clickhouse.go | 6 +++--- .../orchestration/replication/elasticsearch.go | 4 ++-- internal/orchestration/replication/hdfs.go | 4 ++-- .../orchestration/replication/hdfs_audit.go | 2 +- internal/orchestration/replication/hdfs_fsck.go | 2 +- internal/orchestration/replication/kafka.go | 6 +++--- internal/orchestration/replication/report.go | 2 +- internal/orchestration/replication/zookeeper.go | 4 ++-- 16 files changed, 59 insertions(+), 42 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 63f93a9..ada50b1 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -63,6 +63,10 @@ linters: - common-false-positives - legacy - std-error-handling + rules: + - linters: + - goconst + path: _test\.go$ paths: - third_party$ - builtin$ diff --git a/cmd/settings/check_and_finalize.go b/cmd/settings/check_and_finalize.go index ced0ded..f731db7 100644 --- a/cmd/settings/check_and_finalize.go +++ b/cmd/settings/check_and_finalize.go @@ -47,7 +47,7 @@ func runCheckAndFinalize(appCtx *app.Context) error { K8sClient: appCtx.K8sClient, Namespace: appCtx.Namespace, JobName: checkJobName, - ServiceName: "settings", + ServiceName: settingsServiceName, ScaleUpFn: scale.ScaleUpAndReleaseLock, ScaleDownFn: scale.ScaleDown, ScaleSelector: appCtx.Config.Settings.Restore.ScaleDownLabelSelector, diff --git a/cmd/settings/restore.go b/cmd/settings/restore.go index 86742b5..68622b5 100644 --- a/cmd/settings/restore.go +++ b/cmd/settings/restore.go @@ -309,7 +309,7 @@ func buildVolumes(config *config.Config, defaultMode int32) []corev1.Volume { // buildContainers constructs containers for the restore job func buildContainer(envVar []corev1.EnvVar, command []string, config *config.Config) corev1.Container { return corev1.Container{ - Name: "settings", + Name: settingsServiceName, Image: config.Settings.Restore.Job.Image, ImagePullPolicy: corev1.PullIfNotPresent, SecurityContext: k8s.ConvertSecurityContext(config.Settings.Restore.Job.ContainerSecurityContext), diff --git a/cmd/settings/settings.go b/cmd/settings/settings.go index 6ed4702..166216c 100644 --- a/cmd/settings/settings.go +++ b/cmd/settings/settings.go @@ -5,9 +5,11 @@ import ( "github.com/stackvista/stackstate-backup-cli/internal/foundation/config" ) +const settingsServiceName = "settings" + func Cmd(globalFlags *config.CLIGlobalFlags) *cobra.Command { cmd := &cobra.Command{ - Use: "settings", + Use: settingsServiceName, Short: "Settings backup and restore operations", } diff --git a/cmd/stackgraphv2/restore.go b/cmd/stackgraphv2/restore.go index be2f1ab..9c0fafa 100644 --- a/cmd/stackgraphv2/restore.go +++ b/cmd/stackgraphv2/restore.go @@ -26,6 +26,9 @@ const ( restoreNameTemplate = "stackgraph-v2-restore" configMapDefaultFileMode = 0755 purgeStackgraphDataFlag = "-force" + backupLogVolumeName = "backup-log" + restoreScriptsVolumeName = "backup-restore-scripts" + storageKeysVolumeName = "minio-keys" ) // Restore command flags @@ -306,9 +309,9 @@ func buildRestoreEnvVars(backupFile string, config *config.Config) []corev1.EnvV // buildRestoreVolumeMounts constructs volume mounts for the restore job container func buildRestoreVolumeMounts(config *config.Config) []corev1.VolumeMount { volumeMounts := []corev1.VolumeMount{ - {Name: "backup-log", MountPath: "/opt/docker/etc_log"}, - {Name: "backup-restore-scripts", MountPath: "/backup-restore-scripts"}, - {Name: "minio-keys", MountPath: "/aws-keys"}, + {Name: backupLogVolumeName, MountPath: "/opt/docker/etc_log"}, + {Name: restoreScriptsVolumeName, MountPath: "/backup-restore-scripts"}, + {Name: storageKeysVolumeName, MountPath: "/aws-keys"}, {Name: "tmp-data", MountPath: "/tmp-data"}, } @@ -344,7 +347,7 @@ func buildRestoreInitContainers(config *config.Config) []corev1.Container { func buildRestoreVolumes(jobName string, config *config.Config, defaultMode int32) []corev1.Volume { volumes := []corev1.Volume{ { - Name: "backup-log", + Name: backupLogVolumeName, VolumeSource: corev1.VolumeSource{ ConfigMap: &corev1.ConfigMapVolumeSource{ LocalObjectReference: corev1.LocalObjectReference{ @@ -354,7 +357,7 @@ func buildRestoreVolumes(jobName string, config *config.Config, defaultMode int3 }, }, { - Name: "backup-restore-scripts", + Name: restoreScriptsVolumeName, VolumeSource: corev1.VolumeSource{ ConfigMap: &corev1.ConfigMapVolumeSource{ LocalObjectReference: corev1.LocalObjectReference{ @@ -365,7 +368,7 @@ func buildRestoreVolumes(jobName string, config *config.Config, defaultMode int3 }, }, { - Name: "minio-keys", + Name: storageKeysVolumeName, VolumeSource: corev1.VolumeSource{ Secret: &corev1.SecretVolumeSource{ SecretName: restore.MinioKeysSecretName, diff --git a/cmd/stackgraphv2/simplejob.go b/cmd/stackgraphv2/simplejob.go index e6ca0e2..dd5efd5 100644 --- a/cmd/stackgraphv2/simplejob.go +++ b/cmd/stackgraphv2/simplejob.go @@ -58,9 +58,9 @@ func buildSimpleJobEnvVars(config *config.Config) []corev1.EnvVar { // buildSimpleJobVolumeMounts constructs volume mounts shared by the abort and backfill job containers func buildSimpleJobVolumeMounts() []corev1.VolumeMount { return []corev1.VolumeMount{ - {Name: "backup-log", MountPath: "/opt/docker/etc_log"}, - {Name: "backup-restore-scripts", MountPath: "/backup-restore-scripts"}, - {Name: "minio-keys", MountPath: "/aws-keys"}, + {Name: backupLogVolumeName, MountPath: "/opt/docker/etc_log"}, + {Name: restoreScriptsVolumeName, MountPath: "/backup-restore-scripts"}, + {Name: storageKeysVolumeName, MountPath: "/aws-keys"}, } } @@ -86,7 +86,7 @@ func buildSimpleJobInitContainers(config *config.Config) []corev1.Container { func buildSimpleJobVolumes(config *config.Config, defaultMode int32) []corev1.Volume { return []corev1.Volume{ { - Name: "backup-log", + Name: backupLogVolumeName, VolumeSource: corev1.VolumeSource{ ConfigMap: &corev1.ConfigMapVolumeSource{ LocalObjectReference: corev1.LocalObjectReference{ @@ -96,7 +96,7 @@ func buildSimpleJobVolumes(config *config.Config, defaultMode int32) []corev1.Vo }, }, { - Name: "backup-restore-scripts", + Name: restoreScriptsVolumeName, VolumeSource: corev1.VolumeSource{ ConfigMap: &corev1.ConfigMapVolumeSource{ LocalObjectReference: corev1.LocalObjectReference{ @@ -107,7 +107,7 @@ func buildSimpleJobVolumes(config *config.Config, defaultMode int32) []corev1.Vo }, }, { - Name: "minio-keys", + Name: storageKeysVolumeName, VolumeSource: corev1.VolumeSource{ Secret: &corev1.SecretVolumeSource{ SecretName: restore.MinioKeysSecretName, diff --git a/internal/orchestration/replication/applicability.go b/internal/orchestration/replication/applicability.go index 8edc4bd..1d0ef2b 100644 --- a/internal/orchestration/replication/applicability.go +++ b/internal/orchestration/replication/applicability.go @@ -7,11 +7,14 @@ import ( ) const ( - hdfsComponent = "hdfs" - clickhouseComponent = "clickhouse" - nameNodeComponent = "hdfs-nn" - dataNodeComponent = "hdfs-dn" - monoComponent = "stackgraph" + hdfsComponent = "hdfs" + clickhouseComponent = "clickhouse" + elasticsearchComponent = "elasticsearch" + kafkaComponent = "kafka" + zookeeperComponent = "zookeeper" + nameNodeComponent = "hdfs-nn" + dataNodeComponent = "hdfs-dn" + monoComponent = "stackgraph" ) func workloadComponent(workload appsv1.StatefulSet) string { @@ -26,7 +29,7 @@ func workloadComponent(workload appsv1.StatefulSet) string { } } switch name { - case "elasticsearch", clickhouseComponent, "kafka", "zookeeper": + case elasticsearchComponent, clickhouseComponent, kafkaComponent, zookeeperComponent: return name default: return "" diff --git a/internal/orchestration/replication/checker.go b/internal/orchestration/replication/checker.go index 538039d..07d2e2f 100644 --- a/internal/orchestration/replication/checker.go +++ b/internal/orchestration/replication/checker.go @@ -6,7 +6,12 @@ import ( "time" ) -const requestTimeout = 30 * time.Second +const ( + requestTimeout = 30 * time.Second + bashExecutable = "bash" + bashExecFlag = "-ec" + replicationScriptName = "replication-check" +) // Checker observes databases without invoking backup or restore operations. type Checker struct { @@ -83,15 +88,15 @@ func (c *Checker) query(ctx context.Context, pod, container string, command []st func (c *Checker) checkComponent(ctx context.Context, inventory inventory, component string) Result { switch component { - case "hdfs": + case hdfsComponent: return c.checkHDFS(ctx, inventory) - case "elasticsearch": + case elasticsearchComponent: return c.checkElasticsearch(ctx, inventory) - case "kafka": + case kafkaComponent: return c.checkKafka(ctx, inventory) - case "clickhouse": + case clickhouseComponent: return c.checkClickHouse(ctx, inventory) - case "zookeeper": + case zookeeperComponent: return c.checkZooKeeper(ctx, inventory) default: return result(component, Unknown, "unsupported component") diff --git a/internal/orchestration/replication/clickhouse.go b/internal/orchestration/replication/clickhouse.go index f520882..95c07ce 100644 --- a/internal/orchestration/replication/clickhouse.go +++ b/internal/orchestration/replication/clickhouse.go @@ -45,7 +45,7 @@ func (c *Checker) checkClickHouse(ctx context.Context, inventory inventory) Resu if member.replicas < minReplicas { return result("clickhouse", Degraded, fmt.Sprintf("%s belongs to a shard with fewer than two replicas", member.pod.Name)) } - command := []string{"bash", "-ec", clickhouseQuery, "replication-check", clickhouseSQL} + command := []string{bashExecutable, bashExecFlag, clickhouseQuery, replicationScriptName, clickhouseSQL} data, err := c.query(ctx, member.pod.Name, "clickhouse", command) if err != nil { return result("clickhouse", Unknown, err.Error()) @@ -67,7 +67,7 @@ func (c *Checker) verifyEmptyLogs(ctx context.Context, pod string, rows []replic if !rows[n].needsLogVerification { continue } - command := []string{"bash", "-ec", clickhouseQuery, "replication-check", clickhouseEmptyLogSQL, "--param_log_path=" + rows[n].path + "/log"} + command := []string{bashExecutable, bashExecFlag, clickhouseQuery, replicationScriptName, clickhouseEmptyLogSQL, "--param_log_path=" + rows[n].path + "/log"} data, err := c.query(ctx, pod, "clickhouse", command) if err != nil { return fmt.Errorf("%s: could not verify empty replication log: %w", pod, err) @@ -178,7 +178,7 @@ func evaluateClickHouse(observations []replicaObservation) Result { } if len(problems) > 0 { sort.Strings(problems) - return Result{Component: "clickhouse", Status: Degraded, Messages: problems} + return Result{Component: clickhouseComponent, Status: Degraded, Messages: problems} } report := result("clickhouse", Healthy, fmt.Sprintf("%d replicated table groups checked on every member; no pending data replication tasks", len(groups))) if historicalErrors > 0 { diff --git a/internal/orchestration/replication/elasticsearch.go b/internal/orchestration/replication/elasticsearch.go index 0bf0c86..994a012 100644 --- a/internal/orchestration/replication/elasticsearch.go +++ b/internal/orchestration/replication/elasticsearch.go @@ -21,7 +21,7 @@ func (c *Checker) checkElasticsearch(ctx context.Context, inventory inventory) R if err := expectedMembers(members); err != nil { return result("elasticsearch", Degraded, err.Error()) } - command := []string{"bash", "-ec", elasticsearchQuery} + command := []string{bashExecutable, bashExecFlag, elasticsearchQuery} data, err := c.query(ctx, members[0].pod.Name, "elasticsearch", command) if err != nil { return result("elasticsearch", Unknown, err.Error()) @@ -66,7 +66,7 @@ func evaluateElasticsearch(data []byte, expected int) Result { } if len(problems) > 0 { sort.Strings(problems) - return Result{Component: "elasticsearch", Status: Degraded, Messages: problems} + return Result{Component: elasticsearchComponent, Status: Degraded, Messages: problems} } return result("elasticsearch", Healthy, fmt.Sprintf("%d indices have replica shards; all shards allocated with no recovery or relocation", len(indices))) } diff --git a/internal/orchestration/replication/hdfs.go b/internal/orchestration/replication/hdfs.go index ab18cce..0bbeddd 100644 --- a/internal/orchestration/replication/hdfs.go +++ b/internal/orchestration/replication/hdfs.go @@ -30,7 +30,7 @@ func (c *Checker) checkHDFS(ctx context.Context, inventory inventory) Result { if err := expectedMembers(datanodes); err != nil { return result("hdfs", Degraded, err.Error()) } - data, err := c.query(ctx, namenodes[0].pod.Name, "namenode", []string{"bash", "-ec", hdfsQuery}) + data, err := c.query(ctx, namenodes[0].pod.Name, "namenode", []string{bashExecutable, bashExecFlag, hdfsQuery}) if err != nil { return result("hdfs", Unknown, err.Error()) } @@ -85,7 +85,7 @@ func evaluateHDFS(data []byte, expected int) Result { } } if len(problems) > 0 { - return Result{Component: "hdfs", Status: Degraded, Messages: problems} + return Result{Component: hdfsComponent, Status: Degraded, Messages: problems} } return result("hdfs", Healthy, fmt.Sprintf("%d DataNodes live; no missing, corrupt, under-replicated or pending-replication blocks", expected)) } diff --git a/internal/orchestration/replication/hdfs_audit.go b/internal/orchestration/replication/hdfs_audit.go index c56038f..636a453 100644 --- a/internal/orchestration/replication/hdfs_audit.go +++ b/internal/orchestration/replication/hdfs_audit.go @@ -49,7 +49,7 @@ func (c *Checker) Verify(ctx context.Context, previous Report) Report { func (c *Checker) auditHDFS(ctx context.Context, pod string) Result { parser := &fsckParser{} - err := c.kube.ExecTo(ctx, c.options.Namespace, pod, "namenode", []string{"bash", "-ec", hdfsAuditQuery}, parser) + err := c.kube.ExecTo(ctx, c.options.Namespace, pod, "namenode", []string{bashExecutable, bashExecFlag, hdfsAuditQuery}, parser) if ctx.Err() != nil { return result(hdfsComponent, Unknown, fmt.Sprintf("HDFS block audit did not complete: %v", ctx.Err())) } diff --git a/internal/orchestration/replication/hdfs_fsck.go b/internal/orchestration/replication/hdfs_fsck.go index e6ead1d..aa2aa1c 100644 --- a/internal/orchestration/replication/hdfs_fsck.go +++ b/internal/orchestration/replication/hdfs_fsck.go @@ -93,7 +93,7 @@ func (p *fsckParser) result() Result { return result("hdfs", Unknown, "incomplete fsck report; block replication could not be verified") } if p.problems > 0 { - return Result{Component: "hdfs", Status: Degraded, + return Result{Component: hdfsComponent, Status: Degraded, Messages: append([]string{fmt.Sprintf("HDFS block audit found %d replication problems", p.problems)}, p.messages...)} } report := result("hdfs", Healthy, fmt.Sprintf("HDFS audit: %d files and %d completed block entries meet replication targets of at least two (including snapshot references)", p.files, p.completed)) diff --git a/internal/orchestration/replication/kafka.go b/internal/orchestration/replication/kafka.go index 4887404..fcea0d1 100644 --- a/internal/orchestration/replication/kafka.go +++ b/internal/orchestration/replication/kafka.go @@ -39,7 +39,7 @@ func (c *Checker) checkKafka(ctx context.Context, inventory inventory) Result { if err := expectedMembers(members); err != nil { return result("kafka", Degraded, err.Error()) } - command := []string{"bash", "-ec", kafkaQuery, "replication-check", "--bootstrap-server", "localhost:9092", "--describe"} + command := []string{bashExecutable, bashExecFlag, kafkaQuery, replicationScriptName, "--bootstrap-server", "localhost:9092", "--describe"} data, err := c.query(ctx, members[0].pod.Name, "kafka", command) if err != nil { return result("kafka", Unknown, err.Error()) @@ -48,7 +48,7 @@ func (c *Checker) checkKafka(ctx context.Context, inventory inventory) Result { if report.Status == Unknown || hasTransactionTopic(string(data)) { return report } - probe := []string{"bash", "-ec", transactionTopicQuery, "replication-check", "--bootstrap-server", "localhost:9092"} + probe := []string{bashExecutable, bashExecFlag, transactionTopicQuery, replicationScriptName, "--bootstrap-server", "localhost:9092"} presence, err := c.query(ctx, members[0].pod.Name, "kafka", probe) if err != nil || strings.TrimSpace(string(presence)) != "absent" { report.Status = Unknown @@ -144,7 +144,7 @@ func evaluateKafka(output string) Result { } report := result("kafka", Healthy, fmt.Sprintf("all partitions of %d topics have at least two replicas, complete ISR and an in-sync leader", len(counts))) if len(problems) > 0 { - report = Result{Component: "kafka", Status: Degraded, Messages: problems} + report = Result{Component: kafkaComponent, Status: Degraded, Messages: problems} } return report } diff --git a/internal/orchestration/replication/report.go b/internal/orchestration/replication/report.go index c6ee113..50acf89 100644 --- a/internal/orchestration/replication/report.go +++ b/internal/orchestration/replication/report.go @@ -17,7 +17,7 @@ const ( minReplicas = 2 ) -var supportedComponents = []string{"hdfs", "elasticsearch", "kafka", "clickhouse", "zookeeper"} +var supportedComponents = []string{hdfsComponent, elasticsearchComponent, kafkaComponent, clickhouseComponent, zookeeperComponent} // Options identifies the installation and selected databases. type Options struct { diff --git a/internal/orchestration/replication/zookeeper.go b/internal/orchestration/replication/zookeeper.go index f04478d..eecc3bf 100644 --- a/internal/orchestration/replication/zookeeper.go +++ b/internal/orchestration/replication/zookeeper.go @@ -76,7 +76,7 @@ func (c *Checker) checkZooKeeper(ctx context.Context, inventory inventory) Resul } func (c *Checker) queryZooKeeper(ctx context.Context, pod string) (zooKeeperObservation, error) { - data, err := c.query(ctx, pod, "zookeeper", []string{"bash", "-ec", zookeeperQuery}) + data, err := c.query(ctx, pod, "zookeeper", []string{bashExecutable, bashExecFlag, zookeeperQuery}) if err != nil { return zooKeeperObservation{}, err } @@ -163,7 +163,7 @@ func evaluateZooKeeper(observations []zooKeeperObservation) Result { problems = append(problems, fmt.Sprintf("expected one ZooKeeper leader; observed %d", leaders)) } if len(problems) > 0 { - return Result{Component: "zookeeper", Status: Degraded, Messages: problems} + return Result{Component: zookeeperComponent, Status: Degraded, Messages: problems} } return result("zookeeper", Healthy, fmt.Sprintf("%d voting members available; one leader and %d synchronized followers", len(observations), len(observations)-1)) } From 722009b5a4f9ffc84a0b86c0a1940cacc66565cc Mon Sep 17 00:00:00 2001 From: Remco Beckers Date: Fri, 16 Oct 2026 08:25:45 +0200 Subject: [PATCH 3/8] Simplify Go minor update groups --- README.md | 6 +-- renovate.json | 69 +++----------------------------- scripts/renovate-config-test.cjs | 25 ++++++------ 3 files changed, 22 insertions(+), 78 deletions(-) diff --git a/README.md b/README.md index 794d69a..3e72751 100644 --- a/README.md +++ b/README.md @@ -462,9 +462,9 @@ GitHub Actions patches, minors, and digest updates use a separate automerge PR. Major action upgrades require manual review. CI tools use a separate manual group; their major upgrades get individual PRs. Module patches share one library PR, separate from minor updates. -Module minors use separate AWS SDK, Kubernetes, ClickHouse, Elasticsearch, -OpenTelemetry, OpenAPI, and `golang.org/x/*` groups. -Other module minors get individual PRs. +Module minors use three groups: AWS SDK, Kubernetes, and all other Go libraries. +The other-library group includes ClickHouse, Elasticsearch, OpenTelemetry, +OpenAPI, and `golang.org/x/*`. Module patches and minors use native automerge after artifact validation and App approval. Renovate limits concurrent PRs and inherits the shared hourly creation limit. Compiler updates stay within the release line declared in `renovate.json`. diff --git a/renovate.json b/renovate.json index 32cd831..522ef70 100644 --- a/renovate.json +++ b/renovate.json @@ -55,12 +55,15 @@ } }, { - "description": "Automerge individual module minors after required validation and App approval", + "description": "Group and automerge other module minors after required validation and App approval", "matchManagers": ["gomod"], "matchDepTypes": ["require", "indirect", "replace"], "matchUpdateTypes": ["minor"], - "groupName": null, - "groupSlug": null, + "groupName": "Go library minors", + "groupSlug": "go-library-minors", + "group": { + "branchTopic": "go-library-minors" + }, "automerge": true, "automergeType": "pr", "platformAutomerge": true, @@ -90,54 +93,6 @@ "branchTopic": "go-kubernetes" } }, - { - "description": "Group coupled ClickHouse client minor updates", - "matchManagers": ["gomod"], - "matchDepTypes": ["require", "indirect", "replace"], - "matchPackageNames": ["github.com/ClickHouse/**"], - "matchUpdateTypes": ["minor"], - "groupName": "ClickHouse Go clients", - "groupSlug": "go-clickhouse", - "group": { - "branchTopic": "go-clickhouse" - } - }, - { - "description": "Group coupled Elasticsearch client minor updates", - "matchManagers": ["gomod"], - "matchDepTypes": ["require", "indirect", "replace"], - "matchPackageNames": ["github.com/elastic/**"], - "matchUpdateTypes": ["minor"], - "groupName": "Elasticsearch Go clients", - "groupSlug": "go-elasticsearch", - "group": { - "branchTopic": "go-elasticsearch" - } - }, - { - "description": "Group coupled OpenTelemetry module minor updates", - "matchManagers": ["gomod"], - "matchDepTypes": ["require", "indirect", "replace"], - "matchPackageNames": ["go.opentelemetry.io/**"], - "matchUpdateTypes": ["minor"], - "groupName": "OpenTelemetry Go modules", - "groupSlug": "go-opentelemetry", - "group": { - "branchTopic": "go-opentelemetry" - } - }, - { - "description": "Group coupled OpenAPI module minor updates", - "matchManagers": ["gomod"], - "matchDepTypes": ["require", "indirect", "replace"], - "matchPackageNames": ["github.com/go-openapi/**"], - "matchUpdateTypes": ["minor"], - "groupName": "OpenAPI Go modules", - "groupSlug": "go-openapi", - "group": { - "branchTopic": "go-openapi" - } - }, { "description": "Keep module patches separate from minor updates", "matchManagers": ["gomod"], @@ -176,18 +131,6 @@ "automerge": true, "automergeType": "pr", "platformAutomerge": true - }, - { - "description": "Group golang.org/x module minor updates separately from patches", - "matchManagers": ["gomod"], - "matchDepTypes": ["require", "indirect", "replace"], - "matchPackageNames": ["golang.org/x/**"], - "matchUpdateTypes": ["minor"], - "groupName": "Go supplementary modules", - "groupSlug": "go-x", - "group": { - "branchTopic": "go-x" - } } ] } diff --git a/scripts/renovate-config-test.cjs b/scripts/renovate-config-test.cjs index 8a17a4c..f6fca20 100644 --- a/scripts/renovate-config-test.cjs +++ b/scripts/renovate-config-test.cjs @@ -67,6 +67,7 @@ async function main() { assert.equal(patch.automerge, true); assert.equal(patch.rebaseWhen, 'behind-base-branch'); assert.equal(minor.automerge, true); + assert.equal(minor.branchName, 'renovate/go-library-minors'); assert.equal(minor.platformAutomerge, true); assert.equal(minor.rebaseWhen, 'behind-base-branch'); assert.notEqual(patch.branchName, minor.branchName); @@ -86,7 +87,7 @@ async function main() { }); assert.deepEqual( [...new Set(libraries.filter(branch => branch.automerge).map(branch => branch.branchName))], - ['renovate/go-libraries', minor.branchName, 'renovate/go-x'], + ['renovate/go-libraries', 'renovate/go-library-minors'], ); assert.equal(libraries.filter(branch => branch.automerge).length, 3); } @@ -152,21 +153,21 @@ async function main() { ['k8s.io/apimachinery', 'minor', 'go-kubernetes'], ['k8s.io/client-go', 'minor', 'go-kubernetes'], ['sigs.k8s.io/structured-merge-diff/v6', 'minor', 'go-kubernetes'], - ['github.com/ClickHouse/clickhouse-go/v2', 'minor', 'go-clickhouse'], - ['github.com/ClickHouse/ch-go', 'minor', 'go-clickhouse'], - ['github.com/elastic/go-elasticsearch/v8', 'minor', 'go-elasticsearch'], - ['github.com/elastic/elastic-transport-go/v8', 'minor', 'go-elasticsearch'], - ['go.opentelemetry.io/otel', 'minor', 'go-opentelemetry'], - ['go.opentelemetry.io/otel/trace', 'minor', 'go-opentelemetry'], - ['github.com/go-openapi/jsonpointer', 'minor', 'go-openapi'], - ['github.com/go-openapi/swag', 'minor', 'go-openapi'], - ['golang.org/x/net', 'minor', 'go-x'], - ['golang.org/x/sys', 'minor', 'go-x'], + ['github.com/ClickHouse/clickhouse-go/v2', 'minor', 'go-library-minors'], + ['github.com/ClickHouse/ch-go', 'minor', 'go-library-minors'], + ['github.com/elastic/go-elasticsearch/v8', 'minor', 'go-library-minors'], + ['github.com/elastic/elastic-transport-go/v8', 'minor', 'go-library-minors'], + ['go.opentelemetry.io/otel', 'minor', 'go-library-minors'], + ['go.opentelemetry.io/otel/trace', 'minor', 'go-library-minors'], + ['github.com/go-openapi/jsonpointer', 'minor', 'go-library-minors'], + ['github.com/go-openapi/swag', 'minor', 'go-library-minors'], + ['golang.org/x/net', 'minor', 'go-library-minors'], + ['golang.org/x/sys', 'minor', 'go-library-minors'], ['github.com/aws/smithy-go', 'major', null], ['sigs.k8s.io/structured-merge-diff/v6', 'major', null], ['github.com/elastic/go-elasticsearch/v8', 'major', null], ['github.com/go-openapi/jsonpointer', 'major', null], - ['github.com/stretchr/testify', 'minor', null], + ['github.com/stretchr/testify', 'minor', 'go-library-minors'], ]; const moduleBranches = await flattenUpdates(config, { gomod: [{ From fda427deb954feae6ca5ac619a6edcc601ed27bd Mon Sep 17 00:00:00 2001 From: Remco Beckers Date: Fri, 16 Oct 2026 08:50:28 +0200 Subject: [PATCH 4/8] Review Kubernetes minors and age routine updates --- README.md | 6 +++- renovate.json | 12 ++++++-- scripts/renovate-config-test.cjs | 49 ++++++++++++++++++++++++++++---- 3 files changed, 58 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 3e72751..8cf8f79 100644 --- a/README.md +++ b/README.md @@ -458,6 +458,8 @@ This project uses GitHub Actions and GoReleaser for automated releases: ### Dependency updates Renovate runs daily and keeps compiler updates separate from library updates. +Routine updates wait until releases are at least seven days old. +Known vulnerability fixes and compiler patches have no release delay. GitHub Actions patches, minors, and digest updates use a separate automerge PR. Major action upgrades require manual review. CI tools use a separate manual group; their major upgrades get individual PRs. @@ -465,7 +467,9 @@ Module patches share one library PR, separate from minor updates. Module minors use three groups: AWS SDK, Kubernetes, and all other Go libraries. The other-library group includes ClickHouse, Elasticsearch, OpenTelemetry, OpenAPI, and `golang.org/x/*`. -Module patches and minors use native automerge after artifact validation and App approval. +Module patches, AWS SDK minors, and other-library minors use native automerge +after artifact validation and App approval. +Kubernetes minors stay grouped for manual review. Renovate limits concurrent PRs and inherits the shared hourly creation limit. Compiler updates stay within the release line declared in `renovate.json`. Module candidates that declare a newer required Go release line are filtered out. diff --git a/renovate.json b/renovate.json index 522ef70..4cb0a94 100644 --- a/renovate.json +++ b/renovate.json @@ -4,6 +4,7 @@ "github>StackVista/renovate-config", "github>StackVista/renovate-config:go" ], + "minimumReleaseAge": "7 days", "prConcurrentLimit": 5, "rebaseWhen": "behind-base-branch", "packageRules": [ @@ -82,7 +83,7 @@ } }, { - "description": "Group coupled Kubernetes client minor updates", + "description": "Review coupled Kubernetes client minors for API compatibility", "matchManagers": ["gomod"], "matchDepTypes": ["require", "indirect", "replace"], "matchPackageNames": ["k8s.io/**", "sigs.k8s.io/**"], @@ -91,7 +92,9 @@ "groupSlug": "go-kubernetes", "group": { "branchTopic": "go-kubernetes" - } + }, + "automerge": false, + "rebaseWhen": "conflicted" }, { "description": "Keep module patches separate from minor updates", @@ -131,6 +134,11 @@ "automerge": true, "automergeType": "pr", "platformAutomerge": true + }, + { + "description": "Apply known vulnerability fixes without a release delay", + "matchJsonata": ["isVulnerabilityAlert = true or $exists(vulnerabilityFixVersion)"], + "minimumReleaseAge": null } ] } diff --git a/scripts/renovate-config-test.cjs b/scripts/renovate-config-test.cjs index f6fca20..6abc5a7 100644 --- a/scripts/renovate-config-test.cjs +++ b/scripts/renovate-config-test.cjs @@ -7,8 +7,10 @@ const { getConfig } = require(path.join(renovate, 'config/defaults.js')); const { mergeChildConfig } = require(path.join(renovate, 'config/utils.js')); const { resolveConfigPresets } = require(path.join(renovate, 'config/presets/index.js')); const { applyPackageRules } = require(path.join(renovate, 'util/package-rules/index.js')); -const { getDatasourceFor } = require(path.join(renovate, 'modules/datasource/common.js')); +const { getDatasourceFor, getDefaultVersioning } = require(path.join(renovate, 'modules/datasource/common.js')); +const { get: getVersioning } = require(path.join(renovate, 'modules/versioning/index.js')); const { lookupUpdates } = require(path.join(renovate, 'workers/repository/process/lookup/index.js')); +const { filterInternalChecks } = require(path.join(renovate, 'workers/repository/process/lookup/filter-checks.js')); const { flattenUpdates } = require(path.join(renovate, 'workers/repository/updates/flatten.js')); const actions = require(path.join(renovate, 'modules/manager/github-actions/index.js')); const regex = require(path.join(renovate, 'modules/manager/custom/regex/index.js')); @@ -31,6 +33,35 @@ async function main() { }); assert.equal(config.prConcurrentLimit, 5); assert(config.prHourlyLimit > 0); + assert.equal(config.minimumReleaseAge, '7 days'); + assert.equal(config.internalChecksFilter, 'strict'); + const freshReleaseTimestamp = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); + const matureReleaseTimestamp = new Date(Date.now() - 8 * 24 * 60 * 60 * 1000).toISOString(); + for (const [name, manager, depType, datasource, currentVersion, newVersion] of [ + ['github.com/stretchr/testify', 'gomod', 'require', 'go', 'v1.0.0', 'v1.0.1'], + ['actions/checkout', 'github-actions', 'action', 'github-tags', 'v1.0.0', 'v1.0.1'], + ['go', 'gomod', 'golang', 'golang-version', '1.26.8', '1.26.9'], + ]) { + const policy = await applyPackageRules({ + ...config, depName: name, packageName: name, manager, depType, datasource, + currentVersion, currentValue: currentVersion, updateType: 'patch', + }); + const versioning = getVersioning(policy.versioning ?? getDefaultVersioning(datasource)); + for (const releaseTimestamp of [freshReleaseTimestamp, matureReleaseTimestamp, undefined]) { + const result = await filterInternalChecks(policy, versioning, 'patch', [{ version: newVersion, releaseTimestamp }]); + assert.equal(result.pendingChecks, name !== 'go' && releaseTimestamp !== matureReleaseTimestamp, name); + } + for (const security of [{ isVulnerabilityAlert: true }, { vulnerabilityFixVersion: newVersion }]) { + const securityPolicy = await applyPackageRules({ ...policy, ...security }); + assert.equal(securityPolicy.minimumReleaseAge, null, name); + const result = await filterInternalChecks(securityPolicy, versioning, 'patch', [ + { version: newVersion, releaseTimestamp: freshReleaseTimestamp }, + ]); + assert.equal(result.pendingChecks, false, name); + } + const routinePolicy = await applyPackageRules({ ...policy, isVulnerabilityAlert: false }); + assert.equal(routinePolicy.minimumReleaseAge, name === 'go' ? null : '7 days', name); + } getDatasourceFor('go').getReleases = async () => ({ releases: [ ['v1.0.0', '1.18.0'], @@ -152,7 +183,11 @@ async function main() { ['k8s.io/api', 'minor', 'go-kubernetes'], ['k8s.io/apimachinery', 'minor', 'go-kubernetes'], ['k8s.io/client-go', 'minor', 'go-kubernetes'], + ['k8s.io/kops', 'minor', 'go-kubernetes'], + ['sigs.k8s.io/controller-runtime', 'minor', 'go-kubernetes'], ['sigs.k8s.io/structured-merge-diff/v6', 'minor', 'go-kubernetes'], + ['k8s.io/api', 'patch', 'go-libraries'], + ['sigs.k8s.io/controller-runtime', 'patch', 'go-libraries'], ['github.com/ClickHouse/clickhouse-go/v2', 'minor', 'go-library-minors'], ['github.com/ClickHouse/ch-go', 'minor', 'go-library-minors'], ['github.com/elastic/go-elasticsearch/v8', 'minor', 'go-library-minors'], @@ -176,8 +211,8 @@ async function main() { depName: name, packageName: name, depType: 'require', datasource: 'go', currentValue: 'v1.0.0', updates: [{ - updateType: type, newValue: type === 'major' ? 'v2.0.0' : 'v1.1.0', - newVersion: type === 'major' ? 'v2.0.0' : 'v1.1.0', newMajor: type === 'major' ? 2 : 1, + updateType: type, newValue: type === 'major' ? 'v2.0.0' : type === 'patch' ? 'v1.0.1' : 'v1.1.0', + newVersion: type === 'major' ? 'v2.0.0' : type === 'patch' ? 'v1.0.1' : 'v1.1.0', newMajor: type === 'major' ? 2 : 1, }], })), }], @@ -185,8 +220,9 @@ async function main() { for (const branch of moduleBranches) { const group = modules.find(([name, type]) => name === branch.depName && type === branch.updateType)[2]; - assert.equal(branch.automerge, branch.updateType === 'minor'); - assert.equal(branch.rebaseWhen, branch.updateType === 'minor' ? 'behind-base-branch' : 'conflicted'); + const automerge = branch.updateType === 'patch' || (branch.updateType === 'minor' && group !== 'go-kubernetes'); + assert.equal(branch.automerge, automerge); + assert.equal(branch.rebaseWhen, automerge ? 'behind-base-branch' : 'conflicted'); if (branch.automerge) { assert.equal(branch.automergeType, 'pr'); assert.equal(branch.platformAutomerge, true); @@ -218,8 +254,9 @@ async function main() { } console.log('PASS: module patches and minors automerge in separate branches'); console.log('PASS: compatible GitHub Actions updates automerge separately; major actions stay manual'); - console.log('PASS: module minor families are isolated; majors and CI tools stay manual'); + console.log('PASS: Kubernetes minors, module majors and CI tools stay manual; other module minors and all patches automerge'); console.log('PASS: unsupported Go versions are filtered and PR creation is limited'); + console.log('PASS: routine releases wait seven days; known vulnerability fixes and compiler patches remain immediate'); } main().catch(error => { From 6e5bb0530dbe73267f13595e038c2bdc7877880b Mon Sep 17 00:00:00 2001 From: Remco Beckers Date: Fri, 16 Oct 2026 09:10:41 +0200 Subject: [PATCH 5/8] Align Renovate policies --- README.md | 7 +- renovate.json | 174 +++++++++++++++++-------------- scripts/renovate-config-test.cjs | 100 ++++++++++++------ 3 files changed, 170 insertions(+), 111 deletions(-) diff --git a/README.md b/README.md index 8cf8f79..1585539 100644 --- a/README.md +++ b/README.md @@ -460,17 +460,18 @@ This project uses GitHub Actions and GoReleaser for automated releases: Renovate runs daily and keeps compiler updates separate from library updates. Routine updates wait until releases are at least seven days old. Known vulnerability fixes and compiler patches have no release delay. +OSV alerts identify known vulnerable dependencies and trigger fix PRs. GitHub Actions patches, minors, and digest updates use a separate automerge PR. Major action upgrades require manual review. CI tools use a separate manual group; their major upgrades get individual PRs. -Module patches share one library PR, separate from minor updates. -Module minors use three groups: AWS SDK, Kubernetes, and all other Go libraries. +Module patches and minors use three groups: AWS SDK, Kubernetes, and other Go libraries. +Each group keeps patches separate from minors. The other-library group includes ClickHouse, Elasticsearch, OpenTelemetry, OpenAPI, and `golang.org/x/*`. Module patches, AWS SDK minors, and other-library minors use native automerge after artifact validation and App approval. Kubernetes minors stay grouped for manual review. -Renovate limits concurrent PRs and inherits the shared hourly creation limit. +Renovate allows ten concurrent PRs and inherits the shared hourly creation limit. Compiler updates stay within the release line declared in `renovate.json`. Module candidates that declare a newer required Go release line are filtered out. CI rejects a module update that selects a compiler from another release line. diff --git a/renovate.json b/renovate.json index 4cb0a94..4358965 100644 --- a/renovate.json +++ b/renovate.json @@ -4,136 +4,154 @@ "github>StackVista/renovate-config", "github>StackVista/renovate-config:go" ], + "osvVulnerabilityAlerts": true, "minimumReleaseAge": "7 days", - "prConcurrentLimit": 5, - "rebaseWhen": "behind-base-branch", + "prConcurrentLimit": 10, + "rebaseWhen": "conflicted", "packageRules": [ { - "description": "Review major upgrades separately without rebasing for unrelated changes", - "matchUpdateTypes": ["major"], + "description": "Keep CI tools separate from action updates", + "matchManagers": ["github-actions", "custom.regex"], "groupName": null, - "groupSlug": null, - "automerge": false, - "rebaseWhen": "conflicted" + "groupSlug": null }, { - "description": "Automerge compatible action updates after required CI and App approval", - "matchManagers": ["github-actions"], - "matchDepTypes": ["action"], - "matchUpdateTypes": ["patch", "minor", "digest", "pin", "pinDigest"], - "groupName": "GitHub Actions", - "groupSlug": "github-actions", - "automerge": true, - "automergeType": "pr", - "platformAutomerge": true + "description": "Keep Go compiler updates within the approved release line", + "matchManagers": ["gomod"], + "matchDepTypes": ["golang", "toolchain"], + "rangeStrategy": "bump", + "allowedVersions": "/^1\\.26\\./", + "groupName": "Go toolchain", + "groupSlug": "go-toolchain", + "minimumReleaseAge": null }, { - "description": "Review CI tool updates separately from action references", - "matchManagers": ["github-actions", "custom.regex"], - "matchPackageNames": [ - "golangci/golangci-lint", - "goreleaser/goreleaser", - "github.com/google/go-licenses", - "golang.org/x/vuln" - ], - "matchUpdateTypes": ["patch", "minor", "digest", "pin", "pinDigest"], - "groupName": "CI tools", - "groupSlug": "ci-tools", - "automerge": false, - "rebaseWhen": "conflicted" + "description": "Automerge Go compiler patches after required CI and App approval", + "matchManagers": ["gomod"], + "matchDepTypes": ["golang", "toolchain"], + "matchUpdateTypes": ["patch"], + "groupName": "Go toolchain", + "groupSlug": "go-toolchain", + "automerge": true, + "automergeType": "pr", + "platformAutomerge": true, + "rebaseWhen": "behind-base-branch" }, { - "description": "Track Go modules compatible with the approved compiler release line", + "description": "Enable all Go library updates, including indirect dependencies", "matchManagers": ["gomod"], "matchDepTypes": ["require", "indirect", "replace"], "enabled": true, + "separateMinorPatch": true, "constraintsFiltering": "strict", "constraints": { - "%goMod": ">=1.0.0 <1.27.0" + "%goMod": "<1.27.0" }, "constraintsVersioning": { - "%goMod": "semver-coerced" + "%goMod": "npm" } }, { - "description": "Group and automerge other module minors after required validation and App approval", + "description": "Automerge Go library patches and minors after required validation", "matchManagers": ["gomod"], "matchDepTypes": ["require", "indirect", "replace"], - "matchUpdateTypes": ["minor"], - "groupName": "Go library minors", - "groupSlug": "go-library-minors", - "group": { - "branchTopic": "go-library-minors" - }, + "matchUpdateTypes": ["patch", "minor"], + "groupName": "Go libraries", + "groupSlug": "go-libraries", "automerge": true, "automergeType": "pr", "platformAutomerge": true, "rebaseWhen": "behind-base-branch" }, { - "description": "Group coupled AWS SDK minor updates", + "description": "Review unversioned Go module revisions separately from compatible releases", "matchManagers": ["gomod"], "matchDepTypes": ["require", "indirect", "replace"], - "matchPackageNames": ["github.com/aws/aws-sdk-go-v2{,/**}", "github.com/aws/smithy-go"], - "matchUpdateTypes": ["minor"], - "groupName": "AWS Go SDK", - "groupSlug": "go-aws-sdk", - "group": { - "branchTopic": "go-aws-sdk" - } + "matchUpdateTypes": ["digest"], + "groupName": "Go library revisions", + "groupSlug": "go-library-revisions", + "automerge": false, + "rebaseWhen": "conflicted" }, { - "description": "Review coupled Kubernetes client minors for API compatibility", + "description": "Group compatible Kubernetes modules with their coupled dependencies", + "matchManagers": ["gomod"], + "matchDepTypes": ["require", "indirect", "replace"], + "matchPackageNames": ["k8s.io/**", "sigs.k8s.io/**"], + "matchUpdateTypes": ["patch", "minor"], + "groupName": "Kubernetes Go modules", + "groupSlug": "kubernetes-go-modules" + }, + { + "description": "Review Kubernetes module minors for API compatibility", "matchManagers": ["gomod"], "matchDepTypes": ["require", "indirect", "replace"], "matchPackageNames": ["k8s.io/**", "sigs.k8s.io/**"], "matchUpdateTypes": ["minor"], - "groupName": "Kubernetes Go clients", - "groupSlug": "go-kubernetes", - "group": { - "branchTopic": "go-kubernetes" - }, "automerge": false, "rebaseWhen": "conflicted" }, { - "description": "Keep module patches separate from minor updates", + "description": "Group compatible AWS SDK and Smithy modules", "matchManagers": ["gomod"], "matchDepTypes": ["require", "indirect", "replace"], - "separateMinorPatch": true + "matchPackageNames": ["github.com/aws/**"], + "matchUpdateTypes": ["patch", "minor"], + "groupName": "AWS Go modules", + "groupSlug": "aws-go-modules" }, { - "description": "Keep Go compiler updates within the approved release line", + "description": "Group manual Kubernetes module revisions", "matchManagers": ["gomod"], - "matchDepTypes": ["golang", "toolchain"], - "rangeStrategy": "bump", - "allowedVersions": "/^1\\.26\\./", - "groupName": "Go toolchain", - "groupSlug": "go-toolchain", - "minimumReleaseAge": null + "matchDepTypes": ["require", "indirect", "replace"], + "matchPackageNames": ["k8s.io/**", "sigs.k8s.io/**"], + "matchUpdateTypes": ["digest"], + "groupName": "Kubernetes Go module revisions", + "groupSlug": "kubernetes-go-module-revisions" }, { - "description": "Automerge compiler patches after required artifact validation and App approval", + "description": "Group manual AWS module revisions", "matchManagers": ["gomod"], - "matchDepTypes": ["golang", "toolchain"], - "matchUpdateTypes": ["patch"], - "automerge": true, - "automergeType": "pr", - "platformAutomerge": true + "matchDepTypes": ["require", "indirect", "replace"], + "matchPackageNames": ["github.com/aws/**"], + "matchUpdateTypes": ["digest"], + "groupName": "AWS Go module revisions", + "groupSlug": "aws-go-module-revisions" }, { - "description": "Keep module patches separate from compiler updates", - "matchManagers": ["gomod"], - "matchDepTypes": ["require", "indirect", "replace"], - "matchUpdateTypes": ["patch"], - "groupName": "Go library updates", - "groupSlug": "go-libraries", - "group": { - "branchTopic": "go-libraries" - }, + "description": "Automerge compatible GitHub Actions separately from CI tools", + "matchManagers": ["github-actions"], + "matchDepTypes": ["action"], + "matchUpdateTypes": ["patch", "minor", "digest", "pin", "pinDigest"], + "groupName": "GitHub Actions", + "groupSlug": "github-actions", "automerge": true, "automergeType": "pr", - "platformAutomerge": true + "platformAutomerge": true, + "rebaseWhen": "behind-base-branch" + }, + { + "description": "Review CI tool updates separately from action references", + "matchManagers": ["github-actions", "custom.regex"], + "matchPackageNames": [ + "golangci/golangci-lint", + "goreleaser/goreleaser", + "github.com/google/go-licenses", + "golang.org/x/vuln" + ], + "matchUpdateTypes": ["patch", "minor", "digest", "pin", "pinDigest"], + "groupName": "CI tools", + "groupSlug": "ci-tools", + "automerge": false, + "rebaseWhen": "conflicted" + }, + { + "description": "Review major migrations individually", + "matchUpdateTypes": ["major"], + "groupName": null, + "groupSlug": null, + "automerge": false, + "rebaseWhen": "conflicted" }, { "description": "Apply known vulnerability fixes without a release delay", diff --git a/scripts/renovate-config-test.cjs b/scripts/renovate-config-test.cjs index 6abc5a7..4da2d6b 100644 --- a/scripts/renovate-config-test.cjs +++ b/scripts/renovate-config-test.cjs @@ -12,6 +12,7 @@ const { get: getVersioning } = require(path.join(renovate, 'modules/versioning/i const { lookupUpdates } = require(path.join(renovate, 'workers/repository/process/lookup/index.js')); const { filterInternalChecks } = require(path.join(renovate, 'workers/repository/process/lookup/filter-checks.js')); const { flattenUpdates } = require(path.join(renovate, 'workers/repository/updates/flatten.js')); +const { Vulnerabilities } = require(path.join(renovate, 'workers/repository/process/vulnerabilities.js')); const actions = require(path.join(renovate, 'modules/manager/github-actions/index.js')); const regex = require(path.join(renovate, 'modules/manager/custom/regex/index.js')); require(path.join(renovate, 'config/global.js')).GlobalConfig.set({ platform: 'github' }); @@ -31,8 +32,9 @@ async function main() { extends: [], semanticCommits: 'disabled', }); - assert.equal(config.prConcurrentLimit, 5); - assert(config.prHourlyLimit > 0); + assert.equal(config.prConcurrentLimit, 10); + assert.equal(config.prHourlyLimit, 5); + assert.equal(config.osvVulnerabilityAlerts, true); assert.equal(config.minimumReleaseAge, '7 days'); assert.equal(config.internalChecksFilter, 'strict'); const freshReleaseTimestamp = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); @@ -94,11 +96,11 @@ async function main() { }); const patch = branches.find(branch => branch.updateType === 'patch'); const minor = branches.find(branch => branch.updateType === 'minor'); - assert.equal(patch.branchName, 'renovate/go-libraries'); + assert.equal(patch.branchName, 'renovate/patch-go-libraries'); assert.equal(patch.automerge, true); assert.equal(patch.rebaseWhen, 'behind-base-branch'); assert.equal(minor.automerge, true); - assert.equal(minor.branchName, 'renovate/go-library-minors'); + assert.equal(minor.branchName, 'renovate/go-libraries'); assert.equal(minor.platformAutomerge, true); assert.equal(minor.rebaseWhen, 'behind-base-branch'); assert.notEqual(patch.branchName, minor.branchName); @@ -118,7 +120,7 @@ async function main() { }); assert.deepEqual( [...new Set(libraries.filter(branch => branch.automerge).map(branch => branch.branchName))], - ['renovate/go-libraries', 'renovate/go-library-minors'], + ['renovate/patch-go-libraries', 'renovate/go-libraries'], ); assert.equal(libraries.filter(branch => branch.automerge).length, 3); } @@ -169,7 +171,7 @@ async function main() { } } assert.equal(actionBranches.filter(branch => branch.branchName === 'renovate/github-actions').length, 5); - assert.equal(actionBranches.find(branch => branch.manager === 'gomod').branchName, 'renovate/go-libraries'); + assert.equal(actionBranches.find(branch => branch.manager === 'gomod').branchName, 'renovate/patch-go-libraries'); for (const depType of ['github-runner', 'docker', 'uses-with']) { const policy = await applyPackageRules({ ...config, manager: 'github-actions', depType, updateType: 'patch', @@ -177,32 +179,33 @@ async function main() { assert.equal(policy.automerge, false); } const modules = [ - ['github.com/aws/aws-sdk-go-v2', 'minor', 'go-aws-sdk'], - ['github.com/aws/aws-sdk-go-v2/config', 'minor', 'go-aws-sdk'], - ['github.com/aws/smithy-go', 'minor', 'go-aws-sdk'], - ['k8s.io/api', 'minor', 'go-kubernetes'], - ['k8s.io/apimachinery', 'minor', 'go-kubernetes'], - ['k8s.io/client-go', 'minor', 'go-kubernetes'], - ['k8s.io/kops', 'minor', 'go-kubernetes'], - ['sigs.k8s.io/controller-runtime', 'minor', 'go-kubernetes'], - ['sigs.k8s.io/structured-merge-diff/v6', 'minor', 'go-kubernetes'], - ['k8s.io/api', 'patch', 'go-libraries'], - ['sigs.k8s.io/controller-runtime', 'patch', 'go-libraries'], - ['github.com/ClickHouse/clickhouse-go/v2', 'minor', 'go-library-minors'], - ['github.com/ClickHouse/ch-go', 'minor', 'go-library-minors'], - ['github.com/elastic/go-elasticsearch/v8', 'minor', 'go-library-minors'], - ['github.com/elastic/elastic-transport-go/v8', 'minor', 'go-library-minors'], - ['go.opentelemetry.io/otel', 'minor', 'go-library-minors'], - ['go.opentelemetry.io/otel/trace', 'minor', 'go-library-minors'], - ['github.com/go-openapi/jsonpointer', 'minor', 'go-library-minors'], - ['github.com/go-openapi/swag', 'minor', 'go-library-minors'], - ['golang.org/x/net', 'minor', 'go-library-minors'], - ['golang.org/x/sys', 'minor', 'go-library-minors'], + ['github.com/aws/aws-sdk-go-v2', 'minor', 'aws-go-modules'], + ['github.com/aws/aws-sdk-go-v2/config', 'minor', 'aws-go-modules'], + ['github.com/aws/smithy-go', 'minor', 'aws-go-modules'], + ['github.com/aws/smithy-go', 'patch', 'aws-go-modules'], + ['k8s.io/api', 'minor', 'kubernetes-go-modules'], + ['k8s.io/apimachinery', 'minor', 'kubernetes-go-modules'], + ['k8s.io/client-go', 'minor', 'kubernetes-go-modules'], + ['k8s.io/kops', 'minor', 'kubernetes-go-modules'], + ['sigs.k8s.io/controller-runtime', 'minor', 'kubernetes-go-modules'], + ['sigs.k8s.io/structured-merge-diff/v6', 'minor', 'kubernetes-go-modules'], + ['k8s.io/api', 'patch', 'kubernetes-go-modules'], + ['sigs.k8s.io/controller-runtime', 'patch', 'kubernetes-go-modules'], + ['github.com/ClickHouse/clickhouse-go/v2', 'minor', 'go-libraries'], + ['github.com/ClickHouse/ch-go', 'minor', 'go-libraries'], + ['github.com/elastic/go-elasticsearch/v8', 'minor', 'go-libraries'], + ['github.com/elastic/elastic-transport-go/v8', 'minor', 'go-libraries'], + ['go.opentelemetry.io/otel', 'minor', 'go-libraries'], + ['go.opentelemetry.io/otel/trace', 'minor', 'go-libraries'], + ['github.com/go-openapi/jsonpointer', 'minor', 'go-libraries'], + ['github.com/go-openapi/swag', 'minor', 'go-libraries'], + ['golang.org/x/net', 'minor', 'go-libraries'], + ['golang.org/x/sys', 'minor', 'go-libraries'], ['github.com/aws/smithy-go', 'major', null], ['sigs.k8s.io/structured-merge-diff/v6', 'major', null], ['github.com/elastic/go-elasticsearch/v8', 'major', null], ['github.com/go-openapi/jsonpointer', 'major', null], - ['github.com/stretchr/testify', 'minor', 'go-library-minors'], + ['github.com/stretchr/testify', 'minor', 'go-libraries'], ]; const moduleBranches = await flattenUpdates(config, { gomod: [{ @@ -220,16 +223,52 @@ async function main() { for (const branch of moduleBranches) { const group = modules.find(([name, type]) => name === branch.depName && type === branch.updateType)[2]; - const automerge = branch.updateType === 'patch' || (branch.updateType === 'minor' && group !== 'go-kubernetes'); + const automerge = branch.updateType === 'patch' || (branch.updateType === 'minor' && group !== 'kubernetes-go-modules'); assert.equal(branch.automerge, automerge); assert.equal(branch.rebaseWhen, automerge ? 'behind-base-branch' : 'conflicted'); if (branch.automerge) { assert.equal(branch.automergeType, 'pr'); assert.equal(branch.platformAutomerge, true); } - if (group) assert.equal(branch.branchName, `renovate/${group}`); + if (group) assert.equal(branch.branchName, `renovate/${branch.updateType === 'patch' ? 'patch-' : ''}${group}`); else assert.equal(branch.groupName, null); } + const osv = new Vulnerabilities({ + getVulnerabilities: async (ecosystem, name) => [{ + id: 'GO-2099-0001', + affected: [{ + package: { ecosystem, name }, + ranges: [{ type: 'SEMVER', events: [{ introduced: '0' }, { fixed: '1.0.1' }] }], + }], + }], + }); + for (const name of ['github.com/stretchr/testify', 'github.com/aws/smithy-go', 'k8s.io/api']) { + const securityConfig = { ...config, packageRules: [...config.packageRules] }; + const packageFiles = { + gomod: [{ + packageFile: 'go.mod', + deps: [{ + depName: name, packageName: name, depType: 'require', datasource: 'go', currentValue: 'v1.0.0', + updates: [ + { updateType: 'patch', newValue: 'v1.0.1', newVersion: 'v1.0.1' }, + { updateType: 'minor', newValue: 'v1.1.0', newVersion: 'v1.1.0' }, + { updateType: 'major', newValue: 'v2.0.0', newVersion: 'v2.0.0', newMajor: 2 }, + ], + }], + }], + }; + await osv.appendVulnerabilityPackageRules(securityConfig, packageFiles); + assert(securityConfig.packageRules.length > config.packageRules.length, name); + const securityBranches = await flattenUpdates(securityConfig, packageFiles); + assert.equal(securityBranches.length, 3, name); + for (const branch of securityBranches) { + assert.equal(branch.isVulnerabilityAlert, true, name); + assert.equal(branch.minimumReleaseAge, null, name); + assert.equal(branch.groupName, null, name); + assert.equal(branch.automerge, branch.updateType === 'patch' + || (branch.updateType === 'minor' && name !== 'k8s.io/api'), name); + } + } const workflow = fs.readFileSync(path.join(repo, '.github/workflows/ci.yml'), 'utf8'); const extracted = [ ...actions.extractPackageFile(workflow, '.github/workflows/ci.yml', config).deps, @@ -257,6 +296,7 @@ async function main() { console.log('PASS: Kubernetes minors, module majors and CI tools stay manual; other module minors and all patches automerge'); console.log('PASS: unsupported Go versions are filtered and PR creation is limited'); console.log('PASS: routine releases wait seven days; known vulnerability fixes and compiler patches remain immediate'); + console.log('PASS: OSV rules bypass the release delay without automerging Kubernetes minors or majors'); } main().catch(error => { From 9f94afa279d88e3544245f54a991165dbd03f44f Mon Sep 17 00:00:00 2001 From: Remco Beckers Date: Fri, 16 Oct 2026 09:14:47 +0200 Subject: [PATCH 6/8] Group GoReleaser and compiler patches --- README.md | 4 ++- renovate.json | 17 ++++++++++-- scripts/renovate-config-test.cjs | 46 +++++++++++++++++++++++++++----- 3 files changed, 57 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 1585539..7093cdf 100644 --- a/README.md +++ b/README.md @@ -459,11 +459,13 @@ This project uses GitHub Actions and GoReleaser for automated releases: Renovate runs daily and keeps compiler updates separate from library updates. Routine updates wait until releases are at least seven days old. -Known vulnerability fixes and compiler patches have no release delay. +Known vulnerability fixes and build-environment patches have no release delay. OSV alerts identify known vulnerable dependencies and trigger fix PRs. GitHub Actions patches, minors, and digest updates use a separate automerge PR. Major action upgrades require manual review. CI tools use a separate manual group; their major upgrades get individual PRs. +GoReleaser patches share the compiler-patch group and use native automerge. +GoReleaser minors remain in the manual CI tool group. Module patches and minors use three groups: AWS SDK, Kubernetes, and other Go libraries. Each group keeps patches separate from minors. The other-library group includes ClickHouse, Elasticsearch, OpenTelemetry, diff --git a/renovate.json b/renovate.json index 4358965..6b3486a 100644 --- a/renovate.json +++ b/renovate.json @@ -30,8 +30,8 @@ "matchManagers": ["gomod"], "matchDepTypes": ["golang", "toolchain"], "matchUpdateTypes": ["patch"], - "groupName": "Go toolchain", - "groupSlug": "go-toolchain", + "groupName": "Go build environment", + "groupSlug": "go-build-environment", "automerge": true, "automergeType": "pr", "platformAutomerge": true, @@ -145,6 +145,19 @@ "automerge": false, "rebaseWhen": "conflicted" }, + { + "description": "Automerge GoReleaser patches with Go compiler patches", + "matchManagers": ["github-actions", "custom.regex"], + "matchPackageNames": ["goreleaser/goreleaser"], + "matchUpdateTypes": ["patch"], + "groupName": "Go build environment", + "groupSlug": "go-build-environment", + "automerge": true, + "automergeType": "pr", + "platformAutomerge": true, + "minimumReleaseAge": null, + "rebaseWhen": "behind-base-branch" + }, { "description": "Review major migrations individually", "matchUpdateTypes": ["major"], diff --git a/scripts/renovate-config-test.cjs b/scripts/renovate-config-test.cjs index 4da2d6b..741d0e6 100644 --- a/scripts/renovate-config-test.cjs +++ b/scripts/renovate-config-test.cjs @@ -43,15 +43,17 @@ async function main() { ['github.com/stretchr/testify', 'gomod', 'require', 'go', 'v1.0.0', 'v1.0.1'], ['actions/checkout', 'github-actions', 'action', 'github-tags', 'v1.0.0', 'v1.0.1'], ['go', 'gomod', 'golang', 'golang-version', '1.26.8', '1.26.9'], + ['goreleaser/goreleaser', 'custom.regex', undefined, 'github-releases', 'v1.25.1', 'v1.25.2'], ]) { const policy = await applyPackageRules({ ...config, depName: name, packageName: name, manager, depType, datasource, currentVersion, currentValue: currentVersion, updateType: 'patch', }); + const immediate = name === 'go' || name === 'goreleaser/goreleaser'; const versioning = getVersioning(policy.versioning ?? getDefaultVersioning(datasource)); for (const releaseTimestamp of [freshReleaseTimestamp, matureReleaseTimestamp, undefined]) { const result = await filterInternalChecks(policy, versioning, 'patch', [{ version: newVersion, releaseTimestamp }]); - assert.equal(result.pendingChecks, name !== 'go' && releaseTimestamp !== matureReleaseTimestamp, name); + assert.equal(result.pendingChecks, !immediate && releaseTimestamp !== matureReleaseTimestamp, name); } for (const security of [{ isVulnerabilityAlert: true }, { vulnerabilityFixVersion: newVersion }]) { const securityPolicy = await applyPackageRules({ ...policy, ...security }); @@ -62,7 +64,7 @@ async function main() { assert.equal(result.pendingChecks, false, name); } const routinePolicy = await applyPackageRules({ ...policy, isVulnerabilityAlert: false }); - assert.equal(routinePolicy.minimumReleaseAge, name === 'go' ? null : '7 days', name); + assert.equal(routinePolicy.minimumReleaseAge, immediate ? null : '7 days', name); } getDatasourceFor('go').getReleases = async () => ({ releases: [ @@ -286,17 +288,47 @@ async function main() { const policy = await applyPackageRules({ ...config, ...dep, packageName: name, manager, updateType: type, }); - assert.equal(policy.automerge, false, name); - assert.equal(policy.rebaseWhen, 'conflicted', name); - assert.equal(policy.groupName, type === 'major' ? null : 'CI tools', name); + const builderPatch = name === 'goreleaser/goreleaser' && type === 'patch'; + assert.equal(policy.automerge, builderPatch, name); + assert.equal(policy.rebaseWhen, builderPatch ? 'behind-base-branch' : 'conflicted', name); + assert.equal(policy.groupName, type === 'major' ? null : builderPatch ? 'Go build environment' : 'CI tools', name); + assert.equal(policy.minimumReleaseAge, builderPatch ? null : '7 days', name); } } + const builder = extracted.find(dep => (dep.packageName ?? dep.depName) === 'goreleaser/goreleaser'); + const buildBranches = await flattenUpdates(config, { + 'custom.regex': [{ + packageFile: '.github/workflows/ci.yml', + deps: [{ + ...builder, + packageName: builder.packageName ?? builder.depName, + updates: [{ updateType: 'patch', newValue: 'v1.25.2', newVersion: 'v1.25.2' }], + }], + }], + gomod: [{ + packageFile: 'go.mod', + deps: ['golang', 'toolchain'].map(depType => ({ + depName: 'go', packageName: 'go', depType, datasource: 'golang-version', currentValue: '1.26.8', + updates: [{ updateType: 'patch', newValue: '1.26.9', newVersion: '1.26.9' }], + })), + }], + }); + assert.equal(buildBranches.length, 3); + assert.deepEqual([...new Set(buildBranches.map(branch => branch.branchName))], ['renovate/go-build-environment']); + for (const branch of buildBranches) { + assert.equal(branch.automerge, true); + assert.equal(branch.platformAutomerge, true); + assert.equal(branch.automergeType, 'pr'); + assert.equal(branch.minimumReleaseAge, null); + assert.equal(branch.rebaseWhen, 'behind-base-branch'); + } console.log('PASS: module patches and minors automerge in separate branches'); console.log('PASS: compatible GitHub Actions updates automerge separately; major actions stay manual'); - console.log('PASS: Kubernetes minors, module majors and CI tools stay manual; other module minors and all patches automerge'); + console.log('PASS: Kubernetes minors, module majors and non-builder CI tools stay manual; other module minors and all patches automerge'); console.log('PASS: unsupported Go versions are filtered and PR creation is limited'); - console.log('PASS: routine releases wait seven days; known vulnerability fixes and compiler patches remain immediate'); + console.log('PASS: routine releases wait seven days; known vulnerability fixes and build-environment patches remain immediate'); console.log('PASS: OSV rules bypass the release delay without automerging Kubernetes minors or majors'); + console.log('PASS: extracted GoReleaser patches and both Go compiler directives share one automerge branch'); } main().catch(error => { From 524e29d5c12320db2963a66018484f03288430af Mon Sep 17 00:00:00 2001 From: Remco Beckers Date: Fri, 16 Oct 2026 09:19:56 +0200 Subject: [PATCH 7/8] Let Kubernetes upgrades select kube-openapi --- README.md | 1 + renovate.json | 6 ++++++ scripts/renovate-config-test.cjs | 12 ++++++++++++ 3 files changed, 19 insertions(+) diff --git a/README.md b/README.md index 7093cdf..0476693 100644 --- a/README.md +++ b/README.md @@ -473,6 +473,7 @@ OpenAPI, and `golang.org/x/*`. Module patches, AWS SDK minors, and other-library minors use native automerge after artifact validation and App approval. Kubernetes minors stay grouped for manual review. +Kubernetes upgrades select compatible `kube-openapi` revisions instead of independent Renovate updates. Renovate allows ten concurrent PRs and inherits the shared hourly creation limit. Compiler updates stay within the release line declared in `renovate.json`. Module candidates that declare a newer required Go release line are filtered out. diff --git a/renovate.json b/renovate.json index 6b3486a..181cc9f 100644 --- a/renovate.json +++ b/renovate.json @@ -166,6 +166,12 @@ "automerge": false, "rebaseWhen": "conflicted" }, + { + "description": "Let Kubernetes consumer upgrades select a compatible kube-openapi revision", + "matchManagers": ["gomod"], + "matchPackageNames": ["k8s.io/kube-openapi"], + "enabled": false + }, { "description": "Apply known vulnerability fixes without a release delay", "matchJsonata": ["isVulnerabilityAlert = true or $exists(vulnerabilityFixVersion)"], diff --git a/scripts/renovate-config-test.cjs b/scripts/renovate-config-test.cjs index 741d0e6..eb7831c 100644 --- a/scripts/renovate-config-test.cjs +++ b/scripts/renovate-config-test.cjs @@ -235,6 +235,17 @@ async function main() { if (group) assert.equal(branch.branchName, `renovate/${branch.updateType === 'patch' ? 'patch-' : ''}${group}`); else assert.equal(branch.groupName, null); } + const openapiBranches = await flattenUpdates(config, { + gomod: [{ + packageFile: 'go.mod', + deps: [{ + depName: 'k8s.io/kube-openapi', packageName: 'k8s.io/kube-openapi', depType: 'indirect', datasource: 'go', + currentValue: 'v0.0.0-20250710124328-f3f2b991d03b', + updates: [{ updateType: 'digest', newValue: 'v0.0.0-20260721132016-d427ff9ee9ad' }], + }], + }], + }); + assert.equal(openapiBranches.length, 0); const osv = new Vulnerabilities({ getVulnerabilities: async (ecosystem, name) => [{ id: 'GO-2099-0001', @@ -329,6 +340,7 @@ async function main() { console.log('PASS: routine releases wait seven days; known vulnerability fixes and build-environment patches remain immediate'); console.log('PASS: OSV rules bypass the release delay without automerging Kubernetes minors or majors'); console.log('PASS: extracted GoReleaser patches and both Go compiler directives share one automerge branch'); + console.log('PASS: standalone kube-openapi revision updates are disabled without blocking Kubernetes updates'); } main().catch(error => { From 443431c97edde25b25ff9f633ac28918f8ae6bef Mon Sep 17 00:00:00 2001 From: Remco Beckers Date: Fri, 16 Oct 2026 10:23:27 +0200 Subject: [PATCH 8/8] Prioritize maintenance and group major upgrades --- renovate.json | 58 +++++++++++++++++++++++------ scripts/renovate-config-test.cjs | 63 +++++++++++++++++++++++++++----- 2 files changed, 100 insertions(+), 21 deletions(-) diff --git a/renovate.json b/renovate.json index 181cc9f..b27a0dc 100644 --- a/renovate.json +++ b/renovate.json @@ -6,7 +6,7 @@ ], "osvVulnerabilityAlerts": true, "minimumReleaseAge": "7 days", - "prConcurrentLimit": 10, + "prConcurrentLimit": 0, "rebaseWhen": "conflicted", "packageRules": [ { @@ -51,6 +51,15 @@ "%goMod": "npm" } }, + { + "description": "Review major migrations after compatible updates", + "matchUpdateTypes": ["major"], + "groupName": null, + "groupSlug": null, + "automerge": false, + "rebaseWhen": "conflicted", + "prPriority": -1 + }, { "description": "Automerge Go library patches and minors after required validation", "matchManagers": ["gomod"], @@ -74,11 +83,11 @@ "rebaseWhen": "conflicted" }, { - "description": "Group compatible Kubernetes modules with their coupled dependencies", + "description": "Group Kubernetes modules with their coupled dependencies", "matchManagers": ["gomod"], "matchDepTypes": ["require", "indirect", "replace"], "matchPackageNames": ["k8s.io/**", "sigs.k8s.io/**"], - "matchUpdateTypes": ["patch", "minor"], + "matchUpdateTypes": ["patch", "minor", "major"], "groupName": "Kubernetes Go modules", "groupSlug": "kubernetes-go-modules" }, @@ -92,14 +101,41 @@ "rebaseWhen": "conflicted" }, { - "description": "Group compatible AWS SDK and Smithy modules", + "description": "Group AWS SDK and Smithy modules", "matchManagers": ["gomod"], "matchDepTypes": ["require", "indirect", "replace"], "matchPackageNames": ["github.com/aws/**"], - "matchUpdateTypes": ["patch", "minor"], + "matchUpdateTypes": ["patch", "minor", "major"], "groupName": "AWS Go modules", "groupSlug": "aws-go-modules" }, + { + "description": "Group Azure module major migrations", + "matchManagers": ["gomod"], + "matchDepTypes": ["require", "indirect", "replace"], + "matchPackageNames": ["github.com/Azure/**"], + "matchUpdateTypes": ["major"], + "groupName": "Azure Go modules", + "groupSlug": "azure-go-modules" + }, + { + "description": "Group OpenTelemetry module major migrations", + "matchManagers": ["gomod"], + "matchDepTypes": ["require", "indirect", "replace"], + "matchPackageNames": ["go.opentelemetry.io/**"], + "matchUpdateTypes": ["major"], + "groupName": "OpenTelemetry Go modules", + "groupSlug": "opentelemetry-go-modules" + }, + { + "description": "Group golang.org/x module major migrations", + "matchManagers": ["gomod"], + "matchDepTypes": ["require", "indirect", "replace"], + "matchPackageNames": ["golang.org/x/**"], + "matchUpdateTypes": ["major"], + "groupName": "golang.org/x Go modules", + "groupSlug": "golang-x-go-modules" + }, { "description": "Group manual Kubernetes module revisions", "matchManagers": ["gomod"], @@ -159,12 +195,9 @@ "rebaseWhen": "behind-base-branch" }, { - "description": "Review major migrations individually", - "matchUpdateTypes": ["major"], - "groupName": null, - "groupSlug": null, - "automerge": false, - "rebaseWhen": "conflicted" + "description": "Prioritize patch updates before minors and major migrations", + "matchUpdateTypes": ["patch"], + "prPriority": 10 }, { "description": "Let Kubernetes consumer upgrades select a compatible kube-openapi revision", @@ -175,7 +208,8 @@ { "description": "Apply known vulnerability fixes without a release delay", "matchJsonata": ["isVulnerabilityAlert = true or $exists(vulnerabilityFixVersion)"], - "minimumReleaseAge": null + "minimumReleaseAge": null, + "prPriority": 20 } ] } diff --git a/scripts/renovate-config-test.cjs b/scripts/renovate-config-test.cjs index eb7831c..2bff0dc 100644 --- a/scripts/renovate-config-test.cjs +++ b/scripts/renovate-config-test.cjs @@ -12,10 +12,16 @@ const { get: getVersioning } = require(path.join(renovate, 'modules/versioning/i const { lookupUpdates } = require(path.join(renovate, 'workers/repository/process/lookup/index.js')); const { filterInternalChecks } = require(path.join(renovate, 'workers/repository/process/lookup/filter-checks.js')); const { flattenUpdates } = require(path.join(renovate, 'workers/repository/updates/flatten.js')); +const { branchifyUpgrades } = require(path.join(renovate, 'workers/repository/updates/branchify.js')); +const { sortBranches } = require(path.join(renovate, 'workers/repository/process/sort.js')); +const { setCount, isLimitReached } = require(path.join(renovate, 'workers/global/limits.js')); const { Vulnerabilities } = require(path.join(renovate, 'workers/repository/process/vulnerabilities.js')); const actions = require(path.join(renovate, 'modules/manager/github-actions/index.js')); const regex = require(path.join(renovate, 'modules/manager/custom/regex/index.js')); -require(path.join(renovate, 'config/global.js')).GlobalConfig.set({ platform: 'github' }); +require(path.join(renovate, 'config/global.js')).GlobalConfig.set({ + platform: 'github', + localDir: path.resolve(__dirname, '..'), +}); async function main() { const repo = path.resolve(__dirname, '..'); @@ -32,7 +38,7 @@ async function main() { extends: [], semanticCommits: 'disabled', }); - assert.equal(config.prConcurrentLimit, 10); + assert.equal(config.prConcurrentLimit, 0); assert.equal(config.prHourlyLimit, 5); assert.equal(config.osvVulnerabilityAlerts, true); assert.equal(config.minimumReleaseAge, '7 days'); @@ -58,6 +64,7 @@ async function main() { for (const security of [{ isVulnerabilityAlert: true }, { vulnerabilityFixVersion: newVersion }]) { const securityPolicy = await applyPackageRules({ ...policy, ...security }); assert.equal(securityPolicy.minimumReleaseAge, null, name); + assert.equal(securityPolicy.prPriority, 20, name); const result = await filterInternalChecks(securityPolicy, versioning, 'patch', [ { version: newVersion, releaseTimestamp: freshReleaseTimestamp }, ]); @@ -65,6 +72,7 @@ async function main() { } const routinePolicy = await applyPackageRules({ ...policy, isVulnerabilityAlert: false }); assert.equal(routinePolicy.minimumReleaseAge, immediate ? null : '7 days', name); + assert.equal(routinePolicy.prPriority, 10, name); } getDatasourceFor('go').getReleases = async () => ({ releases: [ @@ -203,13 +211,21 @@ async function main() { ['github.com/go-openapi/swag', 'minor', 'go-libraries'], ['golang.org/x/net', 'minor', 'go-libraries'], ['golang.org/x/sys', 'minor', 'go-libraries'], - ['github.com/aws/smithy-go', 'major', null], - ['sigs.k8s.io/structured-merge-diff/v6', 'major', null], + ['github.com/aws/aws-sdk-go-v2', 'major', 'aws-go-modules'], + ['github.com/aws/smithy-go', 'major', 'aws-go-modules'], + ['k8s.io/api', 'major', 'kubernetes-go-modules'], + ['sigs.k8s.io/structured-merge-diff/v6', 'major', 'kubernetes-go-modules'], + ['github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute', 'major', 'azure-go-modules'], + ['github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork', 'major', 'azure-go-modules'], + ['go.opentelemetry.io/otel', 'major', 'opentelemetry-go-modules'], + ['go.opentelemetry.io/otel/trace', 'major', 'opentelemetry-go-modules'], + ['golang.org/x/net', 'major', 'golang-x-go-modules'], + ['golang.org/x/sys', 'major', 'golang-x-go-modules'], ['github.com/elastic/go-elasticsearch/v8', 'major', null], ['github.com/go-openapi/jsonpointer', 'major', null], ['github.com/stretchr/testify', 'minor', 'go-libraries'], ]; - const moduleBranches = await flattenUpdates(config, { + const modulePackageFiles = { gomod: [{ packageFile: 'go.mod', deps: modules.map(([name, type]) => ({ @@ -221,20 +237,41 @@ async function main() { }], })), }], - }); + }; + const moduleBranches = await flattenUpdates(config, modulePackageFiles); for (const branch of moduleBranches) { const group = modules.find(([name, type]) => name === branch.depName && type === branch.updateType)[2]; const automerge = branch.updateType === 'patch' || (branch.updateType === 'minor' && group !== 'kubernetes-go-modules'); assert.equal(branch.automerge, automerge); assert.equal(branch.rebaseWhen, automerge ? 'behind-base-branch' : 'conflicted'); + assert.equal(branch.prPriority ?? 0, branch.updateType === 'patch' ? 10 : branch.updateType === 'major' ? -1 : 0); if (branch.automerge) { assert.equal(branch.automergeType, 'pr'); assert.equal(branch.platformAutomerge, true); } - if (group) assert.equal(branch.branchName, `renovate/${branch.updateType === 'patch' ? 'patch-' : ''}${group}`); + if (group) assert.equal(branch.branchName, `renovate/${branch.updateType === 'patch' ? 'patch-' : branch.updateType === 'major' ? 'major-' : ''}${group}`); else assert.equal(branch.groupName, null); } + const { branches: orderedModules } = await branchifyUpgrades({ ...config, errors: [], warnings: [] }, modulePackageFiles); + sortBranches(orderedModules); + assert.equal(orderedModules[0].updateType, 'patch'); + assert.equal(orderedModules.at(-1).updateType, 'major'); + for (const group of ['aws-go-modules', 'kubernetes-go-modules', 'azure-go-modules', 'opentelemetry-go-modules', 'golang-x-go-modules']) { + const majors = orderedModules.filter(branch => branch.branchName === `renovate/major-${group}`); + assert.equal(majors.length, 1, group); + assert.equal(majors[0].upgrades.length, 2, group); + } + const patch = moduleBranches.find(branch => branch.updateType === 'patch'); + setCount('ConcurrentPRs', 100); + setCount('Branches', 100); + setCount('HourlyPRs', 0); + for (const limit of ['ConcurrentPRs', 'Branches']) { + assert.equal(isLimitReached(limit, { upgrades: [patch] }), false, limit); + } + setCount('HourlyPRs', 5); + assert.equal(isLimitReached('ConcurrentPRs', { upgrades: [patch] }), true); + setCount('HourlyPRs', 0); const openapiBranches = await flattenUpdates(config, { gomod: [{ packageFile: 'go.mod', @@ -277,14 +314,21 @@ async function main() { for (const branch of securityBranches) { assert.equal(branch.isVulnerabilityAlert, true, name); assert.equal(branch.minimumReleaseAge, null, name); + assert.equal(branch.prPriority, 20, name); assert.equal(branch.groupName, null, name); assert.equal(branch.automerge, branch.updateType === 'patch' || (branch.updateType === 'minor' && name !== 'k8s.io/api'), name); } + const { branches: alerts } = await branchifyUpgrades({ ...securityConfig, errors: [], warnings: [] }, packageFiles); + assert.equal(alerts.length, 1, name); + const ordered = [...orderedModules, ...alerts]; + sortBranches(ordered); + assert.equal(ordered[0].isVulnerabilityAlert, true, name); + assert.equal(ordered[1].updateType, 'patch', name); } const workflow = fs.readFileSync(path.join(repo, '.github/workflows/ci.yml'), 'utf8'); const extracted = [ - ...actions.extractPackageFile(workflow, '.github/workflows/ci.yml', config).deps, + ...(await actions.extractPackageFile(workflow, '.github/workflows/ci.yml', config)).deps, ...config.customManagers.flatMap(manager => regex.extractPackageFile(workflow, '.github/workflows/ci.yml', manager)?.deps ?? []), ]; @@ -336,7 +380,8 @@ async function main() { console.log('PASS: module patches and minors automerge in separate branches'); console.log('PASS: compatible GitHub Actions updates automerge separately; major actions stay manual'); console.log('PASS: Kubernetes minors, module majors and non-builder CI tools stay manual; other module minors and all patches automerge'); - console.log('PASS: unsupported Go versions are filtered and PR creation is limited'); + console.log('PASS: unsupported Go versions are filtered; PR creation is paced without a concurrent cap'); + console.log('PASS: security and patch updates precede minors and manual family-grouped majors'); console.log('PASS: routine releases wait seven days; known vulnerability fixes and build-environment patches remain immediate'); console.log('PASS: OSV rules bypass the release delay without automerging Kubernetes minors or majors'); console.log('PASS: extracted GoReleaser patches and both Go compiler directives share one automerge branch');