diff --git a/.github/bench-v3.patch b/.github/bench-v3.patch new file mode 100644 index 000000000..4c4619a85 --- /dev/null +++ b/.github/bench-v3.patch @@ -0,0 +1,9 @@ +@@ +@@ +-"cel.dev/cel-go/cel" ++"github.com/google/cel-go/cel" + +@@ +@@ +-"cel.dev/cel-go/common/types/ref" ++"github.com/google/cel-go/common/types/ref" diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 596532368..3f425482c 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -14,15 +14,15 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 60 steps: - - name: Install Go - uses: buildjet/setup-go@555ce355a95ff01018ffcf8fbbd9c44654db8374 # v5.0.2 - with: - go-version: 1.26.x - cache: false - name: Checkout code uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: fetch-depth: 0 + - name: Install Go + uses: buildjet/setup-go@555ce355a95ff01018ffcf8fbbd9c44654db8374 # v5.0.2 + with: + go-version-file: go.mod + cache: false - name: Post "benchmark running" comment uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 with: @@ -78,12 +78,16 @@ jobs: # the same benchmark suite — only the implementation under test differs. cp serialize_bench_test.go .tmp/bench/base/serialize_bench_test.go cp run_expression_bench_test.go .tmp/bench/base/run_expression_bench_test.go + if rg -q '^module github.com/flanksource/gomplate/v3$' .tmp/bench/base/go.mod + then + go run github.com/uber-go/gopatch@v0.4.0 -p .github/bench-v3.patch .tmp/bench/base/run_expression_bench_test.go + fi - name: Compile benchmark binaries run: | cd .tmp/bench/base - go test -c -o "$GITHUB_WORKSPACE/.tmp/bench/base.test" github.com/flanksource/gomplate/v3 + go test -c -o "$GITHUB_WORKSPACE/.tmp/bench/base.test" . cd "$GITHUB_WORKSPACE" - go test -c -o .tmp/bench/head.test github.com/flanksource/gomplate/v3 + go test -c -o .tmp/bench/head.test . - name: Benchmark interleaved base and head run: | : > bench-base.txt diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 36b32dc07..e7dedb628 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -13,11 +13,11 @@ name: "CodeQL" on: push: - branches: [ main ] + branches: [ main, 3.x ] tags: [ 'v*' ] pull_request: # The branches below must be a subset of the branches above - branches: [ main ] + branches: [ main, 3.x ] schedule: - cron: '42 18 * * 0' diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 08465ffd8..b845b5584 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -3,6 +3,7 @@ on: push: branches: - main + - 3.x pull_request: permissions: @@ -13,10 +14,10 @@ jobs: name: lint runs-on: ubuntu-latest steps: + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 - uses: buildjet/setup-go@555ce355a95ff01018ffcf8fbbd9c44654db8374 # v5.0.2 with: - go-version: 1.22.x - - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + go-version-file: go.mod - name: golangci-lint uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0 with: diff --git a/.github/workflows/release-policy.yml b/.github/workflows/release-policy.yml new file mode 100644 index 000000000..1cf3f8ede --- /dev/null +++ b/.github/workflows/release-policy.yml @@ -0,0 +1,27 @@ +name: Release policy + +on: + pull_request: + branches: [main, 3.x] + push: + branches: [main, 3.x] + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + - uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 + with: + node-version: 24 + - name: Install release verification dependencies + run: npm install --prefix .tmp/release-check --package-lock=false --no-audit --no-fund semantic-release@25.0.9 @semantic-release/commit-analyzer@13.0.1 yaml@2.8.1 semver@7.7.2 + - name: Verify candidate release and branch range + env: + RELEASE_BRANCH: ${{ github.base_ref || github.ref_name }} + run: node hack/verify-release.mjs "$RELEASE_BRANCH" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3a333c273..dddc357c5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,6 +3,7 @@ on: push: branches: - main + - 3.x permissions: contents: read @@ -18,8 +19,12 @@ jobs: egress-policy: audit - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 - uses: cycjimmy/semantic-release-action@b12c8f6015dc215fe37bc154d4ad456dd3833c90 # v6.0.0 id: semantic + with: + semantic_version: 25.0.9 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} outputs: @@ -29,7 +34,7 @@ jobs: bump-clients: runs-on: ubuntu-latest needs: [semantic-release] - if: needs.semantic-release.outputs.new-release-published == 'true' + if: needs.semantic-release.outputs.new-release-published == 'true' && startsWith(needs.semantic-release.outputs.release-version, '3.') strategy: matrix: repos: ["mission-control", "canary-checker", "commons", "config-db", "duty"] @@ -42,7 +47,7 @@ jobs: - name: Setup Go uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 with: - go-version: v1.22.x + go-version-file: go.mod - name: Create commits run: | @@ -51,7 +56,8 @@ jobs: go get github.com/flanksource/gomplate/v3@v${{ needs.semantic-release.outputs.release-version }} go mod tidy if [ -d "hack/generate-schemas" ]; then - cd hack/generate-schemas && go mod tidy + (cd hack/generate-schemas + go mod tidy) fi - name: Create Pull Request diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 000000000..4dd709774 --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,35 @@ +name: Dependency security +on: + push: + branches: [main, 3.x] + pull_request: + schedule: + - cron: '0 6 * * *' + workflow_dispatch: +permissions: + contents: read +jobs: + scan: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + ref: ${{ (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && fromJSON('["main", "3.x"]') || fromJSON(format('[{0}]', toJSON(github.ref))) }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + ref: ${{ matrix.ref }} + - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 + with: + go-version-file: go.mod + - name: Scan Go dependencies + run: go run golang.org/x/vuln/cmd/govulncheck@v1.9.0 ./... + - uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d + with: + package_json_file: web/package.json + - uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b + with: + node-version: 24 + - run: pnpm -C web install --frozen-lockfile + - name: Scan production web dependencies + run: pnpm -C web audit --prod --audit-level low diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 844212094..a13c753bb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,6 +3,7 @@ on: push: branches: - main + - 3.x pull_request: permissions: @@ -12,11 +13,11 @@ jobs: test: runs-on: ubuntu-latest steps: + - name: Checkout code + uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.7.0 - name: Install Go uses: buildjet/setup-go@555ce355a95ff01018ffcf8fbbd9c44654db8374 # v5.0.2 with: - go-version: 1.22.x - - name: Checkout code - uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.7.0 + go-version-file: go.mod - name: Test run: go test ./... diff --git a/.github/workflows/web.yml b/.github/workflows/web.yml index 0a175c8a2..2b3c9f086 100644 --- a/.github/workflows/web.yml +++ b/.github/workflows/web.yml @@ -3,6 +3,7 @@ on: push: branches: - main + - 3.x pull_request: permissions: @@ -17,12 +18,12 @@ jobs: name: generated definitions are current runs-on: ubuntu-latest steps: + - name: Checkout code + uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.7.0 - name: Install Go uses: buildjet/setup-go@555ce355a95ff01018ffcf8fbbd9c44654db8374 # v5.0.2 with: - go-version: 1.22.x - - name: Checkout code - uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.7.0 + go-version-file: go.mod - name: Check for drift run: make monarch-check @@ -35,11 +36,11 @@ jobs: - name: Install pnpm uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d # v3.0.0 with: - version: 10 + package_json_file: web/package.json - name: Install Node uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: - node-version: 22 + node-version: 24 cache: pnpm cache-dependency-path: web/pnpm-lock.yaml - name: Install dependencies diff --git a/.releaserc b/.releaserc index 347c0348d..017ad214c 100644 --- a/.releaserc +++ b/.releaserc @@ -1,8 +1,12 @@ branches: + - name: 3.x + range: 3.x + channel: 3.x - name: main plugins: - - "@semantic-release/commit-analyzer" - releaseRules: + - { breaking: true, release: major } - { type: doc, scope: README, release: patch } - { type: fix, release: patch } - { type: chore, release: patch } @@ -13,8 +17,12 @@ plugins: parserOpts: noteKeywords: - MAJOR RELEASE - - "@semantic-release/release-notes-generator" + - BREAKING CHANGE + - BREAKING CHANGES + - - "@semantic-release/release-notes-generator" + - parserOpts: + noteKeywords: ["MAJOR RELEASE", "BREAKING CHANGE", "BREAKING CHANGES"] - - "@semantic-release/github" # From: https://github.com/semantic-release/github/pull/487#issuecomment-1486298997 - successComment: false - failTitle: false + failTitle: false diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..96df45ff3 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,33 @@ +# Supported versions and security maintenance + +| Branch | Go module | CEL module | Support | +| --- | --- | --- | --- | +| main (before v4 publication) | github.com/flanksource/gomplate/v3 | github.com/google/cel-go | Active v3 publication | +| main (after v4 publication) | github.com/flanksource/gomplate/v4 | cel.dev/cel-go | Active development | +| 3.x | github.com/flanksource/gomplate/v3 | github.com/google/cel-go | Security fixes only | + +The v3 maintenance line starts at v3.24.90. There is no announced end-of-support date. Security patches preserve the v3 API, expression behavior, and CEL import path. Each patch must identify its advisory and use the lowest compatible fixed dependency version. Ordinary feature and dependency refreshes belong on main. Security fixes should be applied to every affected supported branch. + +Renovate processes both branches. Routine updates are disabled on 3.x, while vulnerability fixes require review. OSV checks direct dependencies; daily Go vulnerability and production web dependency scans supplement them, including transitive dependencies. A scheduled scan runs from main and explicitly checks out both supported branches. Failed scans require investigation; scanner errors are not passing results. + +Until main publishes v4.0.0, publish v3 security fixes from main and keep the 3.x branch synchronized with that published history. Semantic-release does not allow a maintenance branch to publish a version newer than the active main release line. After main publishes v4, v3.x patch releases are published from 3.x. Existing downstream v3 dependency-update PR automation runs only for v3 releases. Consumers must migrate explicitly to v4. + +## Migrating to v4 + +The initial security maintenance patch updates Go to 1.26.10 (including GO-2026-6809), x/net to 0.62.0 (GO-2026-6860 and GO-2026-6866), OpenTelemetry to 1.42.0 (GO-2026-5158), and their required dependency graph. This includes fixed x/text and x/mod versions. The web lockfile resolves DOMPurify 3.4.17, addressing the five advisories reported for 3.4.13. CEL remains at 0.31.0 on 3.x. + +Change Go imports from github.com/flanksource/gomplate/v3 to github.com/flanksource/gomplate/v4. When supplying CEL environment options or using AST, evaluation, or value types, change CEL imports from github.com/google/cel-go to cel.dev/cel-go. Do not mix the old and new CEL types. + +The initial v4 migration must use a Conventional Commit with a BREAKING CHANGE footer (or the existing MAJOR RELEASE footer) so semantic-release selects v4.0.0. The release analyzer and notes generator recognize both. Local preparation does not publish a release; publishing happens only after the reviewed migration is merged to main. + +## Verify release configuration without publishing + +Run from the repository root: + +```sh +npm install --prefix .tmp/release-check --package-lock=false --no-audit --no-fund semantic-release@25.0.9 @semantic-release/commit-analyzer@13.0.1 yaml@2.8.1 semver@7.7.2 +node hack/verify-release.mjs main +go test ./internal/releasepolicy +``` + +This checks the actual unreleased commits against the target branch's normalized semantic-release range and verifies that the candidate version matches the Go module major. Pass 3.x instead of main when validating a maintenance release. It also checks synthetic breaking and security-fix commits, workflow syntax, and the v3 downstream-update guard. The Release policy workflow runs this check before PRs are merged. It does not create commits, tags, releases, or external PRs. diff --git a/go.mod b/go.mod index 8cb12cfd5..6b3259ac8 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/flanksource/gomplate/v3 -go 1.26.0 +go 1.26.10 require ( github.com/Masterminds/goutils v1.1.1 @@ -28,8 +28,8 @@ require ( github.com/stretchr/testify v1.11.1 github.com/timberio/go-datemath v0.1.0 github.com/ugorji/go/codec v1.3.1 - golang.org/x/text v0.37.0 - golang.org/x/tools v0.44.0 + golang.org/x/text v0.43.0 + golang.org/x/tools v0.51.0 google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af gopkg.in/yaml.v3 v3.0.1 gotest.tools/v3 v3.5.1 @@ -96,15 +96,15 @@ require ( github.com/tidwall/sjson v1.2.5 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/yuin/gopher-lua v1.1.1 // indirect - go.opentelemetry.io/otel v1.41.0 // indirect - go.opentelemetry.io/otel/trace v1.41.0 // indirect + go.opentelemetry.io/otel v1.42.0 // indirect + go.opentelemetry.io/otel/trace v1.42.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect - golang.org/x/mod v0.35.0 // indirect - golang.org/x/net v0.55.0 // indirect - golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.45.0 // indirect + golang.org/x/mod v0.41.0 // indirect + golang.org/x/net v0.62.0 // indirect + golang.org/x/sync v0.24.0 // indirect + golang.org/x/sys v0.49.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/go.sum b/go.sum index f0c21eecc..7bfef942a 100644 --- a/go.sum +++ b/go.sum @@ -196,16 +196,16 @@ github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= -go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= +go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= +go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0 h1:T0Ec2E+3YZf5bgTNQVet8iTDW7oIk03tXHq+wkwIDnE= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0/go.mod h1:30v2gqH+vYGJsesLWFov8u47EpYTcIQcBjKpI6pJThg= -go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= -go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= +go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= +go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= -go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= -go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= +go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= +go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= @@ -225,8 +225,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= -golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= +golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c= +golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -236,8 +236,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= -golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/net v0.62.0 h1:18ELalfM5+XNRwzM2nYqXsZQ9ByS1uhj2O/U7+5jqRY= +golang.org/x/net v0.62.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -245,8 +245,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA= +golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -259,8 +259,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= -golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA= +golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -279,16 +279,16 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc= +golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= -golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= +golang.org/x/tools v0.51.0 h1:k4Xc/1Om9jwkBJBo4NVLMSARBoWtK10mx+W5BnXCeAI= +golang.org/x/tools v0.51.0/go.mod h1:9eEncMayCV6zRMGhR5eZEC2iBx98qWcF1HZ9Z7wJOoA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 h1:tu/dtnW1o3wfaxCOjSLn5IRX4YDcJrtlpzYkhHhGaC4= google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171/go.mod h1:M5krXqk4GhBKvB596udGL3UyjL4I1+cTbK0orROM9ng= diff --git a/hack/verify-release.mjs b/hack/verify-release.mjs new file mode 100644 index 000000000..5f6aef1c0 --- /dev/null +++ b/hack/verify-release.mjs @@ -0,0 +1,89 @@ +import assert from "node:assert/strict"; +import { readFileSync, readdirSync } from "node:fs"; +import { execFileSync } from "node:child_process"; +import { createRequire } from "node:module"; +const require = createRequire(new URL("../.tmp/release-check/package.json", import.meta.url)); +const { parse } = require("yaml"); +const semver = require("semver"); +const { analyzeCommits } = await import(require.resolve("@semantic-release/commit-analyzer")); +const normalize = await import(require.resolve("semantic-release/lib/branches/normalize.js")); +const definitions = await import(require.resolve("semantic-release/lib/definitions/branches.js")); +const config = parse(readFileSync(".releaserc", "utf8")); +const options = config.plugins[0][1]; +const logger = { log() {} }; +const git = (...args) => execFileSync("git", args, { encoding: "utf8" }).trim(); +const target = process.argv[2] ?? "main"; +const revision = process.argv[3] ?? "HEAD"; +const branches = config.branches.map((branch) => { + const options = typeof branch === "string" ? { name: branch } : branch; + const tags = git("tag", "--merged", `origin/${options.name}`).split("\n") + .map((gitTag) => ({ gitTag, version: semver.valid(gitTag), channels: [null] })) + .filter(({ version }) => version && !semver.prerelease(version)); + return { ...options, tags }; +}); +function normalizeBranches(branches) { + const groups = { + maintenance: branches.filter(definitions.maintenance.filter), + release: branches.filter(definitions.release.filter), + }; + return [...normalize.maintenance(groups), ...normalize.release(groups)]; +} +const normalized = normalizeBranches(branches); +const fixtureTags = (versions) => versions.map((version) => ({ version, gitTag: `v${version}`, channels: [null] })); +for (const scenario of [ + { main: ["3.24.90"], maintenance: ["3.24.90"], target: "main", candidate: "3.24.91", allowed: true }, + { main: ["3.24.90"], maintenance: ["3.24.90"], target: "3.x", candidate: "3.24.91", allowed: false }, + { main: ["3.24.90"], maintenance: ["3.24.90"], target: "main", candidate: "4.0.0", allowed: true }, + { main: ["3.24.90", "3.24.91", "4.0.0"], maintenance: ["3.24.90", "3.24.91"], target: "3.x", candidate: "3.24.92", allowed: true }, +]) { + const fixtureBranches = branches.map((branch) => ({ ...branch, + tags: fixtureTags(branch.name === "main" ? scenario.main : scenario.maintenance), + })); + const branch = normalizeBranches(fixtureBranches).find(({ name }) => name === scenario.target); + assert.equal(semver.satisfies(scenario.candidate, branch.range), scenario.allowed, JSON.stringify(scenario)); +} +const branch = normalized.find(({ name }) => name === target); +assert(branch, `Unknown release branch: ${target}`); +const lastVersion = semver.rsort(branch.tags.map(({ version }) => version))[0]; +assert(lastVersion, `No published release found on ${target}`); +const lastTag = branch.tags.find(({ version }) => version === lastVersion).gitTag; +const commits = git("log", "--format=%B%x00", `${lastTag}..${revision}`).split("\0") + .map((message, index) => ({ hash: String(index), message: message.trim() })) + .filter(({ message }) => message); +const kind = await analyzeCommits(options, { commits, logger, cwd: process.cwd() }); +if (kind) { + const candidate = semver.inc(lastVersion, kind); + assert(semver.satisfies(candidate, branch.range), + `${target} cannot publish ${candidate}: allowed range is ${branch.range}. Publish v3 fixes from main until main publishes v4.`); + const moduleMajor = readFileSync("go.mod", "utf8").match(/^module github.com\/flanksource\/gomplate\/v(\d+)$/m); + assert(moduleMajor, "Cannot determine gomplate module major"); + assert.equal(semver.major(candidate), Number(moduleMajor[1]), "Release major must match the Go module"); + process.stdout.write(`${target} can publish ${candidate} (${branch.range})\n`); +} else { + process.stdout.write(`${target}: no unreleased conventional commits\n`); +} +for (const [message, expected] of [ + ["feat!: migrate gomplate and CEL module paths\n\nBREAKING CHANGE: use /v4 and cel.dev/cel-go imports", "major"], + ["fix(security): remediate dependency advisory", "patch"], +]) { + const kind = await analyzeCommits(options, { commits: [{hash:"example",message}], logger, cwd:process.cwd() }); + assert.equal(kind, expected); + assert.equal(semver.inc("3.24.90", kind), kind === "major" ? "4.0.0" : "3.24.91"); +} +for (const file of ["test.yml", "lint.yml", "web.yml", "release.yml", "security.yml"]) { + const workflow = parse(readFileSync(".github/workflows/" + file, "utf8")); + assert(workflow.on.push.branches.includes("3.x"), file); +} +const release = parse(readFileSync(".github/workflows/release.yml", "utf8")); +assert.equal(release.jobs["semantic-release"].steps.find(({ id }) => id === "semantic").with.semantic_version, + require("semantic-release/package.json").version, "Verification and publishing must use the same semantic-release version"); +for (const file of readdirSync(".github/workflows").filter(file => file.endsWith(".yml"))) { + const workflow = parse(readFileSync(".github/workflows/" + file, "utf8")); + for (const job of Object.values(workflow.jobs)) { + for (const step of job.steps ?? []) { + if (step.run) execFileSync("bash", ["-n"], { input: step.run }); + } + } +} +assert(release.jobs["bump-clients"].if.includes("startsWith(needs.semantic-release.outputs.release-version, '3.')")); +process.stdout.write("Release versions, workflow YAML, and downstream guard passed.\n"); diff --git a/internal/releasepolicy/policy_test.go b/internal/releasepolicy/policy_test.go new file mode 100644 index 000000000..6b86c6f3c --- /dev/null +++ b/internal/releasepolicy/policy_test.go @@ -0,0 +1,56 @@ +package releasepolicy + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "gopkg.in/yaml.v3" +) + +func TestReleasePolicy(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Release policy") +} + +var _ = Describe("major version release policy", func() { + root := filepath.Join("..", "..") + It("keeps maintenance releases in v3 and recognizes breaking releases", func() { + content, err := os.ReadFile(filepath.Join(root, ".releaserc")) + Expect(err).NotTo(HaveOccurred()) + var config struct { + Branches []struct { + Name string + Range string + } + Plugins []any + } + Expect(yaml.Unmarshal(content, &config)).To(Succeed()) + Expect(config.Branches).To(ContainElement(struct { + Name string + Range string + }{"3.x", "3.x"})) + analyzer := config.Plugins[0].([]any)[1].(map[string]any) + Expect(analyzer["releaseRules"]).To(ContainElement(map[string]any{"breaking": true, "release": "major"})) + }) + It("disables routine maintenance updates while retaining reviewed security updates", func() { + content, err := os.ReadFile(filepath.Join(root, "renovate.json")) + Expect(err).NotTo(HaveOccurred()) + var config map[string]any + Expect(json.Unmarshal(content, &config)).To(Succeed()) + Expect(config["baseBranchPatterns"]).To(ConsistOf("main", "3.x")) + Expect(config["osvVulnerabilityAlerts"]).To(Equal(true)) + Expect(config["packageRules"]).To(ContainElement(SatisfyAll( + HaveKeyWithValue("matchBaseBranches", []any{"3.x"}), + HaveKeyWithValue("matchPackageNames", []any{"*"}), + HaveKeyWithValue("enabled", false), + ))) + alerts := config["vulnerabilityAlerts"].(map[string]any) + Expect(alerts).To(HaveKeyWithValue("enabled", true)) + Expect(alerts).To(HaveKeyWithValue("automerge", false)) + Expect(alerts).To(HaveKeyWithValue("vulnerabilityFixStrategy", "lowest")) + }) +}) diff --git a/renovate.json b/renovate.json index 8bd3e334e..67b1bb9d9 100644 --- a/renovate.json +++ b/renovate.json @@ -1,5 +1,7 @@ { - "extends": ["schedule:weekends"], + "extends": [ + "schedule:weekends" + ], "separatePatchReleases": false, "prCreation": "immediate", "branchPrefix": "renovate/", @@ -16,7 +18,9 @@ "automerge": true }, "docker": { - "fileMatch": ["(^|/)Dockerfile.*$"], + "fileMatch": [ + "(^|/)Dockerfile.*$" + ], "major": { "enabled": true } @@ -30,5 +34,41 @@ "prHourlyLimit": 4, "labels": [ "dependencies" + ], + "baseBranchPatterns": [ + "main", + "3.x" + ], + "osvVulnerabilityAlerts": true, + "vulnerabilityAlerts": { + "enabled": true, + "automerge": false, + "vulnerabilityFixStrategy": "lowest", + "addLabels": [ + "security" + ] + }, + "packageRules": [ + { + "description": "Maintain v3 with reviewed security fixes only", + "matchBaseBranches": [ + "3.x" + ], + "matchPackageNames": [ + "*" + ], + "enabled": false, + "automerge": false + }, + { + "description": "Preserve the CEL module path on v3", + "matchBaseBranches": [ + "3.x" + ], + "matchPackageNames": [ + "github.com/google/cel-go" + ], + "allowedVersions": "<0.32.0" + } ] } diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 96f252c03..82886082f 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -1032,8 +1032,8 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} - dompurify@3.4.13: - resolution: {integrity: sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==} + dompurify@3.4.17: + resolution: {integrity: sha512-B/pSlYVHQ58kqISD/yDjuJ1yfzI34m1gF4lAod2bwSRhiN4K5Cl3H8Af7hqB2PmTL911tdEudnnXn5FnhRoz0w==} electron-to-chromium@1.5.405: resolution: {integrity: sha512-bNglH7lPH5l+yHOes7Zr4VqxhOy4BQ9ZBUX4VdoFgxMpzJk7W1ZoO3Vgd9Pxa9PyjQ76sfm2aKH/nzEcCNRlew==} @@ -1922,7 +1922,7 @@ snapshots: '@tanstack/react-query': 5.101.4(react@19.2.8) class-variance-authority: 0.7.1 clsx: 2.1.1 - dompurify: 3.4.13 + dompurify: 3.4.17 jotai: 2.20.2(@babel/core@7.29.7)(@babel/template@7.29.7)(@types/react@19.2.18)(react@19.2.8) monaco-editor: 0.48.0 monaco-yaml: 5.1.1(monaco-editor@0.48.0) @@ -2425,7 +2425,7 @@ snapshots: detect-libc@2.1.2: {} - dompurify@3.4.13: + dompurify@3.4.17: optionalDependencies: '@types/trusted-types': 2.0.7