diff --git a/.github/renovate-entrypoint.sh b/.github/renovate-entrypoint.sh index a9f48b78..d6587dcf 100755 --- a/.github/renovate-entrypoint.sh +++ b/.github/renovate-entrypoint.sh @@ -2,18 +2,22 @@ set -e -# Install Earthly (for release branches) -echo "Installing Earthly..." -curl -fsSLo /usr/local/bin/earthly https://github.com/earthly/earthly/releases/latest/download/earthly-linux-amd64 -chmod +x /usr/local/bin/earthly -/usr/local/bin/earthly bootstrap - -# Install Nix (for main branch) +# Install Nix. echo "Installing Nix..." apt-get update && apt-get install -y nix-bin # Configure Nix mkdir -p /etc/nix + +# The Renovate container can't run Nix sandboxed, so HOME=/homeless-shelter on +# the real filesystem. Any build that writes to $HOME creates that directory, +# and Nix then refuses to start another build until it's gone. +cat >/usr/local/bin/nix-clean-homeless-shelter <<'EOF' +#!/bin/sh +rm -rf /homeless-shelter +EOF +chmod +x /usr/local/bin/nix-clean-homeless-shelter + cat >/etc/nix/nix.conf <<'EOF' # Enable flakes and the nix command (e.g. nix run, nix build). experimental-features = nix-command flakes @@ -22,14 +26,27 @@ experimental-features = nix-command flakes # needing to create the nixbld group and users in this ephemeral container. build-users-group = -# Build derivations in parallel, one per CPU core. -max-jobs = auto +# One build at a time, so no build starts before we can clean up /homeless-shelter. +max-jobs = 1 + +# Removes /homeless-shelter after each build (see the hook script above). +post-build-hook = /usr/local/bin/nix-clean-homeless-shelter # Use the Crossplane Cachix cache to download pre-built binaries from CI. extra-substituters = https://crossplane.cachix.org extra-trusted-public-keys = crossplane.cachix.org-1:NJluVUN9TX0rY/zAxHYaT19Y5ik4ELH4uFuxje+62d4= EOF -echo "Nix $(nix --version) installed successfully" +# Renovate installs its own Nix when it updates flake.lock. It goes earlier on +# PATH than ours and ignores the config above, so all nix commands we run (e.g. +# postUpgradeTasks) will go through this launcher, which pins both the binary +# and the config it reads. +cat >/usr/local/bin/crossplane-nix <<'EOF' +#!/bin/bash +exec env NIX_CONF_DIR=/etc/nix /usr/bin/nix "$@" +EOF +chmod +x /usr/local/bin/crossplane-nix + +echo "Nix $(crossplane-nix --version) installed successfully" renovate diff --git a/.github/renovate.json5 b/.github/renovate.json5 index c4dc835d..6ff7acdb 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -8,8 +8,13 @@ // We only want renovate to rebase PRs when they have conflicts, default // "auto" mode is not required. rebaseWhen: 'conflicted', - // The maximum number of PRs to be created in parallel - prConcurrentLimit: 5, + // The maximum number of PRs to be created in parallel - branchConcurrentLimit + // inherits this value as well + prConcurrentLimit: 10, + // Disable the default 2 per hour PR creation limit. prConcurrentLimit already + // caps how many PRs can be open at once and our renovate job runs on a daily + // schedule, so having any hourly throttling can actually slow us down. + prHourlyLimit: 0, // The branches renovate should target // PLEASE UPDATE THIS WHEN RELEASING. baseBranches: [ @@ -25,9 +30,46 @@ labels: [ 'automated', ], + // Enable the nix manager to update flake.lock when flake inputs change. + nix: { + enabled: true, + }, + // Allow Renovate to update lock files, so we get new updates in Nix inputs + // (e.g., nixpkgs, nixpkgs-unstable) regularly. + // + // An empty schedule ("[]") means "at any time", so we can get a lock file + // maintenance PR during any run of Renovate. They can be important, so let's + // not throttle them unnecessarily. + lockFileMaintenance: { + enabled: true, + schedule: [], + }, + customManagers: [ + { + customType: 'regex', + description: 'Bump the Renovate version used by the config validator and the bot', + managerFilePatterns: [ + '/^\\.github/workflows/renovate\\.ya?ml$/', + ], + matchStrings: [ + 'RENOVATE_VERSION: "(?.*?)"', + ], + datasourceTemplate: 'npm', + depNameTemplate: 'renovate', + }, + ], // PackageRules disabled below should be enabled in case of vulnerabilities vulnerabilityAlerts: { enabled: true, + automerge: false, + // Security fixes shouldn't have to wait on dependency dashboard approval. + dependencyDashboardApproval: false, + addLabels: [ + 'security', + ], + // Set a static groupName for security fixes so they all get batched into a + // single PR per base branch, instead of one per CVE. + groupName: 'vulnerable dependencies', }, osvVulnerabilityAlerts: true, // Renovate evaluates all packageRules in order, so low priority rules should @@ -113,6 +155,31 @@ ], groupName: 'golang version', }, + { + // Give lock file maintenance PRs higher priority than other non-security + // related updates. + description: 'Sort lock file maintenance ahead of other updates', + matchUpdateTypes: [ + 'lockFileMaintenance', + ], + prPriority: 10, + }, + { + // We disable all non-security updates on release branches, but we still + // want Nix flake.lock to get updated there, since that's how we get newer + // versions of Go, which could have security fixes. + description: 'Refresh flake.lock on release branches too', + matchManagers: [ + 'nix', + ], + matchUpdateTypes: [ + 'lockFileMaintenance', + ], + matchBaseBranches: [ + '/^release-.*/', + ], + enabled: true, + }, { description: 'Regenerate gomod2nix.toml and generated code after upgrading go dependencies', matchDatasources: [ @@ -120,8 +187,8 @@ ], postUpgradeTasks: { commands: [ - 'nix run .#tidy', - 'nix run .#generate', + 'crossplane-nix run .#tidy', + 'crossplane-nix run .#generate', ], fileFilters: [ '**/*', @@ -136,7 +203,7 @@ ], postUpgradeTasks: { commands: [ - 'nix run .#lint', + 'crossplane-nix run .#lint', ], fileFilters: [ '**/*', diff --git a/.github/workflows/renovate.yml b/.github/workflows/renovate.yml index 17254862..a7b663f3 100644 --- a/.github/workflows/renovate.yml +++ b/.github/workflows/renovate.yml @@ -16,6 +16,10 @@ permissions: env: LOG_LEVEL: "info" + # The Renovate version, used by both the config validator and the bot itself + # (see renovate-version below), so validation can't disagree with what runs. + # Renovate keeps this current - see the customManager in renovate.json5. + RENOVATE_VERSION: "44.23.0" jobs: renovate: @@ -29,7 +33,7 @@ jobs: # Don't waste time starting Renovate if JSON is invalid - name: Validate Renovate JSON - run: npx --yes --package renovate -- renovate-config-validator + run: npx --yes --package "renovate@${RENOVATE_VERSION}" -- renovate-config-validator - name: Get token id: get-github-app-token @@ -45,9 +49,10 @@ jobs: # Use GitHub API to create commits RENOVATE_PLATFORM_COMMIT: "true" LOG_LEVEL: ${{ github.event.inputs.logLevel || env.LOG_LEVEL }} - RENOVATE_ALLOWED_COMMANDS: '["^nix .+"]' + RENOVATE_ALLOWED_COMMANDS: '["^crossplane-nix .+"]' with: configurationFile: .github/renovate.json5 + renovate-version: ${{ env.RENOVATE_VERSION }} token: '${{ steps.get-github-app-token.outputs.token }}' mount-docker-socket: true docker-user: root diff --git a/cmd/crossplane/render/engine_docker_test.go b/cmd/crossplane/render/engine_docker_test.go index 696dc1a9..73fcdddb 100644 --- a/cmd/crossplane/render/engine_docker_test.go +++ b/cmd/crossplane/render/engine_docker_test.go @@ -18,6 +18,7 @@ package render import ( "context" + "slices" "strings" "testing" @@ -334,8 +335,8 @@ func TestDockerRenderEngineSetup(t *testing.T) { // Defer-LIFO: all cleanups in this test are no-ops (we pre-seed // e.network, so no call took the create-network branch). Calling // them must not panic. - for i := len(cleanups) - 1; i >= 0; i-- { - cleanups[i]() + for _, cleanup := range slices.Backward(cleanups) { + cleanup() } if tc.engine.network != presetNetwork { diff --git a/cmd/crossplane/trace/internal/printer/default.go b/cmd/crossplane/trace/internal/printer/default.go index 427c07a2..b0412155 100644 --- a/cmd/crossplane/trace/internal/printer/default.go +++ b/cmd/crossplane/trace/internal/printer/default.go @@ -19,6 +19,7 @@ package printer import ( "fmt" "io" + "slices" "strings" "text/tabwriter" @@ -259,9 +260,9 @@ func (p *DefaultPrinter) printResourceTree(tw *tabwriter.Writer, root *resource. // Enqueue the children of the current node in reverse order to ensure // that they are dequeued from the LIFO queue in the same order w.r.t. // the way they are defined by the resources. - for idx := len(item.resource.Children) - 1; idx >= 0; idx-- { + for idx, child := range slices.Backward(item.resource.Children) { isLast := idx == len(item.resource.Children)-1 - queue = append(queue, &queueItem{resource: item.resource.Children[idx], depth: item.depth + 1, isLast: isLast, prefix: childPrefix}) + queue = append(queue, &queueItem{resource: child, depth: item.depth + 1, isLast: isLast, prefix: childPrefix}) } } return nil diff --git a/cmd/crossplane/xrd/generate.go b/cmd/crossplane/xrd/generate.go index dac3f500..ee229628 100644 --- a/cmd/crossplane/xrd/generate.go +++ b/cmd/crossplane/xrd/generate.go @@ -47,6 +47,13 @@ import ( //go:embed help/generate.md var generateHelp string +const ( + schemaTypeObject = "object" + + schemaFieldSpec = "spec" + schemaFieldStatus = "status" +) + type generateCmd struct { File string `arg:"" help:"Path to the XR or XRC YAML file."` From string `default:"xr" enum:"xr,simpleschema" help:"Input format: xr or simpleschema."` @@ -192,7 +199,7 @@ func replaceCELWithPlaceholder(data map[string]any) map[string]any { for key, value := range data { if isCELExpression(value) { - result[key] = "object" + result[key] = schemaTypeObject } else if nestedMap, ok := value.(map[string]any); ok { result[key] = replaceCELWithPlaceholder(nestedMap) } else { @@ -255,7 +262,7 @@ func newXRDFromSimpleSchema(yamlData []byte, customPlural string) (*v2.Composite return nil, errors.Wrap(err, "failed to convert spec to OpenAPI schema") } - statusSchema := &extv1.JSONSchemaProps{Type: "object", Properties: map[string]extv1.JSONSchemaProps{}} + statusSchema := &extv1.JSONSchemaProps{Type: schemaTypeObject, Properties: map[string]extv1.JSONSchemaProps{}} if len(simpleInput.Status) > 0 { celPaths := findCELFields(simpleInput.Status, nil) processedStatus := replaceCELWithPlaceholder(simpleInput.Status) @@ -270,12 +277,12 @@ func newXRDFromSimpleSchema(yamlData []byte, customPlural string) (*v2.Composite openAPIV3Schema := &extv1.JSONSchemaProps{ Description: fmt.Sprintf("%s is the Schema for the %s API.", kind, kind), - Type: "object", + Type: schemaTypeObject, Properties: map[string]extv1.JSONSchemaProps{ - "spec": *specSchema, - "status": *statusSchema, + schemaFieldSpec: *specSchema, + schemaFieldStatus: *statusSchema, }, - Required: []string{"spec"}, + Required: []string{schemaFieldSpec}, } schemaBytes, err := json.Marshal(openAPIV3Schema) @@ -401,20 +408,20 @@ func newXRDFromExample(yamlData []byte, customPlural string) (*v2.CompositeResou openAPIV3Schema := &extv1.JSONSchemaProps{ Description: description, - Type: "object", + Type: schemaTypeObject, Properties: map[string]extv1.JSONSchemaProps{ - "spec": { + schemaFieldSpec: { Description: fmt.Sprintf("%sSpec defines the desired state of %s.", kind, kind), - Type: "object", + Type: schemaTypeObject, Properties: specProps, }, - "status": { + schemaFieldStatus: { Description: fmt.Sprintf("%sStatus defines the observed state of %s.", kind, kind), - Type: "object", + Type: schemaTypeObject, Properties: statusProps, }, }, - Required: []string{"spec"}, + Required: []string{schemaFieldSpec}, } schemaBytes, err := json.Marshal(openAPIV3Schema) diff --git a/flake.lock b/flake.lock index 283dbd07..b7ccc11b 100644 --- a/flake.lock +++ b/flake.lock @@ -42,27 +42,27 @@ }, "nixpkgs": { "locked": { - "lastModified": 1776221942, - "narHash": "sha256-FbQAeVNi7G4v3QCSThrSAAvzQTmrmyDLiHNPvTF2qFM=", + "lastModified": 1786313170, + "narHash": "sha256-9BG7OgUWdu0ONDO5X2q6+K4bsuBITkX/3W4nNJu1Ito=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "1766437c5509f444c1b15331e82b8b6a9b967000", + "rev": "fcb8fcd6bf2d0adecae5bd491afaaaf8311b758d", "type": "github" }, "original": { "owner": "NixOS", - "ref": "nixos-25.11", + "ref": "nixos-26.05", "repo": "nixpkgs", "type": "github" } }, "nixpkgs-unstable": { "locked": { - "lastModified": 1776255774, - "narHash": "sha256-psVTpH6PK3q1htMJpmdz1hLF5pQgEshu7gQWgKO6t6Y=", + "lastModified": 1786098110, + "narHash": "sha256-shi1tjDhCGGd0kIgVPNY03V8NBWSTzhMSFDb7IqSoec=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "566acc07c54dc807f91625bb286cb9b321b5f42a", + "rev": "afb4584a80bbf779ce0f691509ff902d188c2b3d", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index fa802c28..7cd4a05f 100644 --- a/flake.nix +++ b/flake.nix @@ -5,7 +5,7 @@ description = "Crossplane CLI"; inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11"; + nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05"; nixpkgs-unstable.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; # TODO(negz): Unpin once https://github.com/nix-community/gomod2nix/pull/231 is released. @@ -181,7 +181,7 @@ pkgs.kubernetes-controller-tools # Nix - pkgs.nixfmt-rfc-style + pkgs.nixfmt ]; shellHook = '' diff --git a/nix/apps.nix b/nix/apps.nix index 1643e30c..4de643be 100644 --- a/nix/apps.nix +++ b/nix/apps.nix @@ -44,7 +44,7 @@ pkgs.unstable.golangci-lint pkgs.statix pkgs.deadnix - pkgs.nixfmt-rfc-style + pkgs.nixfmt pkgs.shellcheck pkgs.gnupatch pkgs.shfmt diff --git a/nix/checks.nix b/nix/checks.nix index b5cc5239..f2284dd6 100644 --- a/nix/checks.nix +++ b/nix/checks.nix @@ -144,7 +144,7 @@ nativeBuildInputs = [ pkgs.statix pkgs.deadnix - pkgs.nixfmt-rfc-style + pkgs.nixfmt ]; } '' diff --git a/proto/render/v1alpha1/render.pb.go b/proto/render/v1alpha1/render.pb.go index 58aab1f9..10ce2590 100644 --- a/proto/render/v1alpha1/render.pb.go +++ b/proto/render/v1alpha1/render.pb.go @@ -15,7 +15,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.10 +// protoc-gen-go v1.36.11 // protoc (unknown) // source: proto/render/v1alpha1/render.proto