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/README.md b/README.md index 531f8aa..0476693 100644 --- a/README.md +++ b/README.md @@ -458,21 +458,32 @@ 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 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. -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. +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, +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. 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/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)) } diff --git a/renovate.json b/renovate.json index 7c5280b..b27a0dc 100644 --- a/renovate.json +++ b/renovate.json @@ -4,143 +4,212 @@ "github>StackVista/renovate-config", "github>StackVista/renovate-config:go" ], + "osvVulnerabilityAlerts": true, + "minimumReleaseAge": "7 days", "prConcurrentLimit": 0, - "prHourlyLimit": 0, - "rebaseWhen": "behind-base-branch", + "rebaseWhen": "conflicted", "packageRules": [ { - "description": "Review major upgrades separately without rebasing for unrelated changes", + "description": "Keep CI tools separate from action updates", + "matchManagers": ["github-actions", "custom.regex"], + "groupName": null, + "groupSlug": null + }, + { + "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": "Automerge Go compiler patches after required CI and App approval", + "matchManagers": ["gomod"], + "matchDepTypes": ["golang", "toolchain"], + "matchUpdateTypes": ["patch"], + "groupName": "Go build environment", + "groupSlug": "go-build-environment", + "automerge": true, + "automergeType": "pr", + "platformAutomerge": true, + "rebaseWhen": "behind-base-branch" + }, + { + "description": "Enable all Go library updates, including indirect dependencies", + "matchManagers": ["gomod"], + "matchDepTypes": ["require", "indirect", "replace"], + "enabled": true, + "separateMinorPatch": true, + "constraintsFiltering": "strict", + "constraints": { + "%goMod": "<1.27.0" + }, + "constraintsVersioning": { + "%goMod": "npm" + } + }, + { + "description": "Review major migrations after compatible updates", "matchUpdateTypes": ["major"], "groupName": null, "groupSlug": null, "automerge": false, - "rebaseWhen": "conflicted" + "rebaseWhen": "conflicted", + "prPriority": -1 }, { - "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", + "description": "Automerge Go library patches and minors after required validation", + "matchManagers": ["gomod"], + "matchDepTypes": ["require", "indirect", "replace"], + "matchUpdateTypes": ["patch", "minor"], + "groupName": "Go libraries", + "groupSlug": "go-libraries", "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", + "description": "Review unversioned Go module revisions separately from compatible releases", + "matchManagers": ["gomod"], + "matchDepTypes": ["require", "indirect", "replace"], + "matchUpdateTypes": ["digest"], + "groupName": "Go library revisions", + "groupSlug": "go-library-revisions", "automerge": false, "rebaseWhen": "conflicted" }, { - "description": "Track Go modules compatible with the approved compiler release line", + "description": "Group Kubernetes modules with their coupled dependencies", "matchManagers": ["gomod"], "matchDepTypes": ["require", "indirect", "replace"], - "enabled": true, - "constraintsFiltering": "strict", - "constraints": { - "%goMod": ">=1.0.0 <1.27.0" - }, - "constraintsVersioning": { - "%goMod": "semver-coerced" - } + "matchPackageNames": ["k8s.io/**", "sigs.k8s.io/**"], + "matchUpdateTypes": ["patch", "minor", "major"], + "groupName": "Kubernetes Go modules", + "groupSlug": "kubernetes-go-modules" }, { - "description": "Review other module minors individually without rebasing for unrelated changes", + "description": "Review Kubernetes module minors for API compatibility", "matchManagers": ["gomod"], "matchDepTypes": ["require", "indirect", "replace"], + "matchPackageNames": ["k8s.io/**", "sigs.k8s.io/**"], "matchUpdateTypes": ["minor"], - "groupName": null, - "groupSlug": null, "automerge": false, "rebaseWhen": "conflicted" }, { - "description": "Review coupled AWS SDK updates together", + "description": "Group AWS SDK and Smithy modules", "matchManagers": ["gomod"], "matchDepTypes": ["require", "indirect", "replace"], - "matchPackageNames": ["github.com/aws/aws-sdk-go-v2{,/**}", "github.com/aws/smithy-go"], - "matchUpdateTypes": ["minor", "major"], - "groupName": "AWS Go SDK", - "groupSlug": "go-aws-sdk", - "group": { - "branchTopic": "go-aws-sdk" - } + "matchPackageNames": ["github.com/aws/**"], + "matchUpdateTypes": ["patch", "minor", "major"], + "groupName": "AWS Go modules", + "groupSlug": "aws-go-modules" }, { - "description": "Review coupled Kubernetes clients and schema migrations together", + "description": "Group Azure module major migrations", "matchManagers": ["gomod"], "matchDepTypes": ["require", "indirect", "replace"], - "matchPackageNames": ["k8s.io/**", "sigs.k8s.io/**"], - "matchUpdateTypes": ["minor", "major"], - "groupName": "Kubernetes Go clients", - "groupSlug": "go-kubernetes", - "group": { - "branchTopic": "go-kubernetes" - } + "matchPackageNames": ["github.com/Azure/**"], + "matchUpdateTypes": ["major"], + "groupName": "Azure Go modules", + "groupSlug": "azure-go-modules" }, { - "description": "Keep automerged patches available alongside manual module minors", + "description": "Group OpenTelemetry module major migrations", "matchManagers": ["gomod"], "matchDepTypes": ["require", "indirect", "replace"], - "matchPackageNames": ["!golang.org/x/**"], - "separateMinorPatch": true + "matchPackageNames": ["go.opentelemetry.io/**"], + "matchUpdateTypes": ["major"], + "groupName": "OpenTelemetry Go modules", + "groupSlug": "opentelemetry-go-modules" }, { - "description": "Keep Go compiler updates within the approved release line", + "description": "Group golang.org/x module major migrations", "matchManagers": ["gomod"], - "matchDepTypes": ["golang", "toolchain"], - "rangeStrategy": "bump", - "allowedVersions": "/^1\\.26\\./", - "groupName": "Go toolchain", - "groupSlug": "go-toolchain", - "minimumReleaseAge": null + "matchDepTypes": ["require", "indirect", "replace"], + "matchPackageNames": ["golang.org/x/**"], + "matchUpdateTypes": ["major"], + "groupName": "golang.org/x Go modules", + "groupSlug": "golang-x-go-modules" }, { - "description": "Automerge compiler patches after required artifact validation and App approval", + "description": "Group manual Kubernetes module revisions", "matchManagers": ["gomod"], - "matchDepTypes": ["golang", "toolchain"], - "matchUpdateTypes": ["patch"], - "automerge": true, - "automergeType": "pr", - "platformAutomerge": true + "matchDepTypes": ["require", "indirect", "replace"], + "matchPackageNames": ["k8s.io/**", "sigs.k8s.io/**"], + "matchUpdateTypes": ["digest"], + "groupName": "Kubernetes Go module revisions", + "groupSlug": "kubernetes-go-module-revisions" }, { - "description": "Keep module patches separate from compiler updates", + "description": "Group manual AWS module revisions", "matchManagers": ["gomod"], "matchDepTypes": ["require", "indirect", "replace"], - "matchUpdateTypes": ["patch"], - "groupName": "Go library updates", - "groupSlug": "go-libraries", - "group": { - "branchTopic": "go-libraries" - }, + "matchPackageNames": ["github.com/aws/**"], + "matchUpdateTypes": ["digest"], + "groupName": "AWS Go module revisions", + "groupSlug": "aws-go-module-revisions" + }, + { + "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": "Include golang.org/x module minors in the library group", - "matchManagers": ["gomod"], - "matchDepTypes": ["require", "indirect", "replace"], - "matchPackageNames": ["golang.org/x/**"], - "matchUpdateTypes": ["minor"], - "groupName": "Go library updates", - "groupSlug": "go-libraries", + "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 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": "Prioritize patch updates before minors and major migrations", + "matchUpdateTypes": ["patch"], + "prPriority": 10 + }, + { + "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)"], + "minimumReleaseAge": null, + "prPriority": 20 } ] } diff --git a/scripts/renovate-config-test.cjs b/scripts/renovate-config-test.cjs index 1428ff7..2bff0dc 100644 --- a/scripts/renovate-config-test.cjs +++ b/scripts/renovate-config-test.cjs @@ -7,12 +7,21 @@ 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 { 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, '..'); @@ -29,6 +38,42 @@ async function main() { extends: [], semanticCommits: 'disabled', }); + assert.equal(config.prConcurrentLimit, 0); + 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(); + 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'], + ['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, !immediate && releaseTimestamp !== matureReleaseTimestamp, name); + } + 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 }, + ]); + assert.equal(result.pendingChecks, false, name); + } + 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: [ ['v1.0.0', '1.18.0'], @@ -61,11 +106,13 @@ 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, false); - assert.equal(minor.rebaseWhen, 'conflicted'); + assert.equal(minor.automerge, true); + 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); const libraries = await flattenUpdates(config, { gomod: [{ @@ -83,9 +130,9 @@ async function main() { }); assert.deepEqual( [...new Set(libraries.filter(branch => branch.automerge).map(branch => branch.branchName))], - ['renovate/go-libraries'], + ['renovate/patch-go-libraries', 'renovate/go-libraries'], ); - 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': [{ @@ -134,7 +181,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', @@ -142,39 +189,146 @@ 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', 'major', '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'], + ['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/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/stretchr/testify', 'minor', null], + ['github.com/go-openapi/jsonpointer', 'major', null], + ['github.com/stretchr/testify', 'minor', 'go-libraries'], ]; - const manual = await flattenUpdates(config, { + const modulePackageFiles = { gomod: [{ packageFile: 'go.mod', deps: modules.map(([name, type]) => ({ 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, }], })), }], - }); - for (const branch of manual) { - const group = modules.find(([name]) => name === branch.depName)[2]; - assert.equal(branch.automerge, false); - assert.equal(branch.rebaseWhen, 'conflicted'); - if (group) assert.equal(branch.branchName, `renovate/${group}`); + }; + 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-' : 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', + 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', + 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.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 ?? []), ]; @@ -189,14 +343,49 @@ 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); } } - console.log('PASS: available module patches automerge independently of manual minors'); + 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: unsupported Go versions are filtered; manual families and CI tools stay separate'); + 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; 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'); + console.log('PASS: standalone kube-openapi revision updates are disabled without blocking Kubernetes updates'); } main().catch(error => {