From 38187d91f1fdb6164459566aa2a83def3e960f8f Mon Sep 17 00:00:00 2001 From: Piotr Zaniewski Date: Wed, 4 Nov 2026 16:19:52 +0100 Subject: [PATCH 1/8] chore(ci): route backports through verified pr metadata Public backport automation loses its default event allowance on 2 December. A read-only metadata relay preserves fork and conflicted PR events while the shared backport workflow consumes a validated payload. --- .github/workflows/backport.yaml | 29 +++++++++++++++++++--- .github/workflows/pull-request-events.yaml | 24 ++++++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/pull-request-events.yaml diff --git a/.github/workflows/backport.yaml b/.github/workflows/backport.yaml index 25866a5da2..4653213f76 100644 --- a/.github/workflows/backport.yaml +++ b/.github/workflows/backport.yaml @@ -1,15 +1,38 @@ name: Automatic backport action on: - pull_request_target: - types: ["labeled", "closed"] + workflow_run: # zizmor: ignore[dangerous-triggers] -- only metadata from the verified read-only relay is consumed + workflows: ["Pull request events"] + types: [completed] permissions: contents: read jobs: + event: + if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request_target' + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + actions: read + contents: read + pull-requests: read + outputs: + eligible: ${{ steps.event.outputs.eligible }} + pr-event: ${{ steps.event.outputs.event-json }} + steps: + - name: Resolve pull request from the metadata relay + id: event + uses: loft-sh/github-actions/.github/actions/resolve-pr-event@resolve-pr-event/v1 # zizmor: ignore[unpinned-uses] -- internal versioned action + with: + source-workflow: .github/workflows/pull-request-events.yaml + backport: - uses: loft-sh/github-actions/.github/workflows/backport.yaml@backport/v1 + needs: event + if: needs.event.outputs.eligible == 'true' + uses: loft-sh/github-actions/.github/workflows/backport.yaml@backport/v1 # zizmor: ignore[unpinned-uses] -- internal rolling workflow tag + with: + pr-event: ${{ needs.event.outputs.pr-event }} secrets: gh-access-token: ${{ secrets.GH_ACCESS_TOKEN }} linear-token: ${{ secrets.LINEAR_TOKEN }} diff --git a/.github/workflows/pull-request-events.yaml b/.github/workflows/pull-request-events.yaml new file mode 100644 index 0000000000..0869db492d --- /dev/null +++ b/.github/workflows/pull-request-events.yaml @@ -0,0 +1,24 @@ +name: Pull request events + +on: + pull_request_target: # zizmor: ignore[dangerous-triggers] -- metadata only; no checkout, business secrets, or write token + types: [labeled, closed] + +permissions: {} + +jobs: + relay: + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + steps: + - name: Record event + run: >- + jq '{number,action,label: (if .action == "labeled" then .label.name else "" end)}' "$GITHUB_EVENT_PATH" > "$RUNNER_TEMP/event.json" + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: pull-request-event-${{ github.run_attempt }} + path: ${{ runner.temp }}/event.json + retention-days: 1 + if-no-files-found: error From 072522650dfa668c2e6032d626db22f39db7f8f4 Mon Sep 17 00:00:00 2001 From: Piotr Zaniewski Date: Wed, 4 Nov 2026 16:28:28 +0100 Subject: [PATCH 2/8] fix(ci): disable cache access in the backport caller The new workflow_run caller must explicitly cap cache access for its reusable workflow. CI rejected the inherited cache mode because the callee could request writes beyond the low-trust trigger default. --- .github/workflows/backport.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/backport.yaml b/.github/workflows/backport.yaml index 4653213f76..8393fceaf9 100644 --- a/.github/workflows/backport.yaml +++ b/.github/workflows/backport.yaml @@ -8,6 +8,8 @@ on: permissions: contents: read +cache-mode: none + jobs: event: if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request_target' From c85eed2b8fc7a1ad8a05601120e676f88fbc80f0 Mon Sep 17 00:00:00 2001 From: Piotr Zaniewski Date: Thu, 5 Nov 2026 13:58:01 +0100 Subject: [PATCH 3/8] fix(ci): remove privileged pr triggers and isolate fork jobs Public PR automation must keep working without an exception for privileged PR triggers. The metadata relay still required that exception and CI jobs relied on repository settings to strip fork privileges. Use a no-op label signal and API-verified trusted push or workflow-run consumers. Restrict secret and write jobs to same-repository PRs, keep fork code jobs read-only, and test those trust boundaries. --- .github/tests/test-pr-event-workflows.py | 102 ++++++++++++++++++++ .github/workflows/actionlint.yml | 19 +++- .github/workflows/auto-approve-bot-prs.yaml | 17 ++-- .github/workflows/backport.yaml | 34 +++++-- .github/workflows/clean-github-cache.yaml | 4 +- .github/workflows/conflict-check.yaml | 5 +- .github/workflows/e2e-ginkgo.yaml | 7 +- .github/workflows/e2e.yaml | 13 +++ .github/workflows/lint.yaml | 5 + .github/workflows/pull-request-events.yaml | 22 ++--- .github/workflows/unit-tests.yaml | 7 ++ 11 files changed, 196 insertions(+), 39 deletions(-) create mode 100644 .github/tests/test-pr-event-workflows.py diff --git a/.github/tests/test-pr-event-workflows.py b/.github/tests/test-pr-event-workflows.py new file mode 100644 index 0000000000..085a5b8d82 --- /dev/null +++ b/.github/tests/test-pr-event-workflows.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +"""Regression checks for PR workflow trust boundaries.""" +from pathlib import Path +import unittest + +import yaml + +ROOT = Path(__file__).resolve().parents[2] +WORKFLOWS = ROOT / '.github' / 'workflows' +SOURCE = '.github/workflows/pull-request-events.yaml' +HEAD_GUARD = 'github.event.pull_request.head.repo.full_name == github.repository' +FORBIDDEN_TRIGGER = 'pull_request' + '_target' + + +def workflow(name): + return yaml.load((WORKFLOWS / name).read_text(), Loader=yaml.BaseLoader) + + +class WorkflowTrustTests(unittest.TestCase): + def test_no_target_triggers(self): + for path in WORKFLOWS.iterdir(): + if path.suffix not in ('.yaml', '.yml'): + continue + data = yaml.load(path.read_text(), Loader=yaml.BaseLoader) + with self.subTest(path=path.name): + self.assertNotIn(FORBIDDEN_TRIGGER, data.get('on', {})) + self.assertNotIn(FORBIDDEN_TRIGGER, path.read_text()) + + def test_signal_has_no_privilege_or_untrusted_outputs(self): + signal = workflow('pull-request-events.yaml') + self.assertEqual(set(signal['on']), {'pull_request'}) + self.assertEqual(set(signal['on']['pull_request']['types']), {'labeled'}) + self.assertEqual(signal['permissions'], {}) + self.assertEqual(set(signal['jobs']), {'relay'}) + relay = signal['jobs']['relay'] + self.assertEqual(relay['permissions'], {}) + self.assertEqual(len(relay['steps']), 1) + self.assertEqual(relay['steps'][0]['run'], ':') + self.assertNotIn('uses', relay['steps'][0]) + self.assertNotIn('secrets.', (WORKFLOWS / 'pull-request-events.yaml').read_text()) + + def test_trusted_backport_resolves_and_forwards_only_verified_events(self): + backport = workflow('backport.yaml') + self.assertEqual(backport['permissions'], {}) + self.assertEqual(backport['on']['push']['branches'], ['main']) + self.assertEqual(backport['on']['workflow_run']['workflows'], ['Pull request events']) + self.assertEqual(backport['on']['workflow_run']['types'], ['completed']) + event, call = backport['jobs']['event'], backport['jobs']['backport'] + condition = event['if'] + for clause in ("github.ref == format('refs/heads/{0}', github.event.repository.default_branch)", + "endsWith(github.workflow_ref, format('@refs/heads/{0}', github.event.repository.default_branch))", + "github.event_name == 'push'", "github.event.workflow_run.conclusion == 'success'", + "github.event.workflow_run.event == 'pull_request'"): + self.assertIn(clause, condition) + readonly = {'actions': 'read', 'contents': 'read', 'pull-requests': 'read'} + self.assertEqual(event['permissions'], readonly) + self.assertEqual(call['permissions'], readonly) + step = event['steps'][0] + self.assertEqual(step['with']['source-workflow'], SOURCE) + self.assertEqual(call['with']['source-workflow'], SOURCE) + self.assertEqual(event['outputs']['events-json'], '${{ steps.event.outputs.events-json }}') + self.assertEqual(call['needs'], 'event') + self.assertEqual(call['if'], "needs.event.outputs.eligible == 'true'") + self.assertEqual(call['strategy']['matrix']['event'], '${{ fromJSON(needs.event.outputs.events-json) }}') + self.assertEqual(call['with']['pr-event'], '${{ toJSON(matrix.event) }}') + resolver = step['uses'].rsplit('@', 1)[1] + reusable = call['uses'].rsplit('@', 1)[1] + self.assertRegex(resolver, r'^[0-9a-f]{40}$') + self.assertEqual(resolver, reusable) + + def test_pr_code_jobs_are_read_only_and_drop_checkout_credentials(self): + for name in ('e2e.yaml', 'e2e-ginkgo.yaml', 'unit-tests.yaml', 'lint.yaml'): + data = workflow(name) + with self.subTest(workflow=name): + self.assertEqual(data['permissions'], {'contents': 'read'}) + for job_name, job in data['jobs'].items(): + for step in job.get('steps', []): + if step.get('uses', '').startswith('actions/checkout@'): + self.assertEqual(step.get('with', {}).get('persist-credentials'), 'false', job_name) + + def test_privileged_pr_jobs_require_same_repository(self): + for name, job in (('auto-approve-bot-prs.yaml', 'auto-approve'), + ('conflict-check.yaml', 'conflict-check'), + ('clean-github-cache.yaml', 'clean-github-cache'), + ('actionlint.yml', 'actionlint')): + data = workflow(name) + with self.subTest(workflow=name): + self.assertEqual(data['permissions'], {}) + self.assertIn(HEAD_GUARD, data['jobs'][job]['if']) + ginkgo = workflow('e2e-ginkgo.yaml')['jobs']['ginkgo-e2e-tests'] + self.assertIn(HEAD_GUARD, ginkgo['if']) + self.assertIn("github.event_name != 'pull_request'", ginkgo['if']) + self.assertIn("github.event_name == 'workflow_dispatch'", ginkgo['if']) + lint = workflow('lint.yaml')['jobs']['golangci'] + secret_steps = [s for s in lint['steps'] if 'secrets.' in str(s)] + self.assertTrue(secret_steps) + for step in secret_steps: + self.assertIn(HEAD_GUARD, step['if']) + + +if __name__ == '__main__': + unittest.main() diff --git a/.github/workflows/actionlint.yml b/.github/workflows/actionlint.yml index 16a887a32c..b3121bfbf2 100644 --- a/.github/workflows/actionlint.yml +++ b/.github/workflows/actionlint.yml @@ -4,16 +4,29 @@ on: pull_request: paths: - ".github/workflows/**" + - ".github/tests/test-pr-event-workflows.py" -permissions: - contents: read - pull-requests: write +permissions: {} jobs: actionlint: + if: github.event.pull_request.head.repo.full_name == github.repository permissions: contents: read pull-requests: write uses: loft-sh/github-actions/.github/workflows/actionlint.yaml@actionlint/v1 with: reporter: github-pr-check + + workflow-security-tests: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + with: + persist-credentials: false + - name: Install YAML parser + run: python3 -m pip install PyYAML==6.0.1 + - name: Test PR workflow trust boundaries + run: python3 .github/tests/test-pr-event-workflows.py diff --git a/.github/workflows/auto-approve-bot-prs.yaml b/.github/workflows/auto-approve-bot-prs.yaml index c178930ccc..f8f2192176 100644 --- a/.github/workflows/auto-approve-bot-prs.yaml +++ b/.github/workflows/auto-approve-bot-prs.yaml @@ -5,20 +5,21 @@ on: types: [opened, synchronize] # checks/statuses are what the shared workflow's CI poll reads. They must be -# granted here, not there: a reusable workflow can only downgrade the caller's +# granted by the caller job, not just the shared workflow: a reusable workflow can only downgrade the caller's # GITHUB_TOKEN permissions, never elevate them, and anything omitted defaults to -# none. GITHUB_TOKEN is scoped strictly by this block whatever the repository's +# none. GITHUB_TOKEN is scoped strictly by the job permissions block whatever the repository's # visibility, so omitting them makes the poll fail and the bot silently never # approve, while both the check and the job still report success. -permissions: - contents: read - pull-requests: write - checks: read - statuses: read +permissions: {} jobs: auto-approve: - if: startsWith(github.head_ref, 'backport/') + if: github.event.pull_request.head.repo.full_name == github.repository && startsWith(github.head_ref, 'backport/') + permissions: + contents: read + pull-requests: write + checks: read + statuses: read uses: loft-sh/github-actions/.github/workflows/auto-approve-bot-prs.yaml@auto-approve-bot-prs/v1 # a889ee9 with: trusted-authors: 'loft-bot' diff --git a/.github/workflows/backport.yaml b/.github/workflows/backport.yaml index 8393fceaf9..41b0579eeb 100644 --- a/.github/workflows/backport.yaml +++ b/.github/workflows/backport.yaml @@ -1,18 +1,25 @@ name: Automatic backport action on: - workflow_run: # zizmor: ignore[dangerous-triggers] -- only metadata from the verified read-only relay is consumed + # Merges still reach backporting when the PR signal cannot run, for example + # while the PR has conflicts. The resolver verifies merged default-branch PRs. + push: + branches: [main] + workflow_run: # zizmor: ignore[dangerous-triggers] -- API-verified merged PRs only; no PR code or artifacts are executed workflows: ["Pull request events"] types: [completed] -permissions: - contents: read +permissions: {} cache-mode: none jobs: event: - if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request_target' + if: >- + github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && + endsWith(github.workflow_ref, format('@refs/heads/{0}', github.event.repository.default_branch)) && + (github.event_name == 'push' || + (github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request')) runs-on: ubuntu-latest timeout-minutes: 5 permissions: @@ -21,20 +28,29 @@ jobs: pull-requests: read outputs: eligible: ${{ steps.event.outputs.eligible }} - pr-event: ${{ steps.event.outputs.event-json }} + events-json: ${{ steps.event.outputs.events-json }} steps: - - name: Resolve pull request from the metadata relay + - name: Resolve merged pull requests from GitHub id: event - uses: loft-sh/github-actions/.github/actions/resolve-pr-event@resolve-pr-event/v1 # zizmor: ignore[unpinned-uses] -- internal versioned action + uses: loft-sh/github-actions/.github/actions/resolve-pr-event@2a987c344d7c30cf3003666e6e1963bb32c8c5e1 # DEVOPS-1803: shared PR #349, pending release with: source-workflow: .github/workflows/pull-request-events.yaml backport: needs: event if: needs.event.outputs.eligible == 'true' - uses: loft-sh/github-actions/.github/workflows/backport.yaml@backport/v1 # zizmor: ignore[unpinned-uses] -- internal rolling workflow tag + permissions: + actions: read + contents: read + pull-requests: read + strategy: + fail-fast: false + matrix: + event: ${{ fromJSON(needs.event.outputs.events-json) }} + uses: loft-sh/github-actions/.github/workflows/backport.yaml@2a987c344d7c30cf3003666e6e1963bb32c8c5e1 # DEVOPS-1803: shared PR #349, pending release with: - pr-event: ${{ needs.event.outputs.pr-event }} + pr-event: ${{ toJSON(matrix.event) }} + source-workflow: .github/workflows/pull-request-events.yaml secrets: gh-access-token: ${{ secrets.GH_ACCESS_TOKEN }} linear-token: ${{ secrets.LINEAR_TOKEN }} diff --git a/.github/workflows/clean-github-cache.yaml b/.github/workflows/clean-github-cache.yaml index 28fa90e7e3..6cdd24ae80 100644 --- a/.github/workflows/clean-github-cache.yaml +++ b/.github/workflows/clean-github-cache.yaml @@ -5,11 +5,11 @@ on: types: - closed -permissions: - actions: write +permissions: {} jobs: clean-github-cache: + if: github.event.pull_request.head.repo.full_name == github.repository permissions: actions: write uses: loft-sh/github-actions/.github/workflows/clean-github-cache.yaml@clean-github-cache/v1 diff --git a/.github/workflows/conflict-check.yaml b/.github/workflows/conflict-check.yaml index 37792d9e3a..6091fd9222 100644 --- a/.github/workflows/conflict-check.yaml +++ b/.github/workflows/conflict-check.yaml @@ -5,12 +5,11 @@ on: branches: - 'v**' -permissions: - contents: read - pull-requests: write +permissions: {} jobs: conflict-check: + if: github.event.pull_request.head.repo.full_name == github.repository permissions: contents: read pull-requests: write diff --git a/.github/workflows/e2e-ginkgo.yaml b/.github/workflows/e2e-ginkgo.yaml index 24041aee69..595af8e52e 100644 --- a/.github/workflows/e2e-ginkgo.yaml +++ b/.github/workflows/e2e-ginkgo.yaml @@ -2,7 +2,6 @@ name: vCluster E2E CI (Ginkgo) permissions: contents: read - pull-requests: write on: workflow_dispatch: @@ -72,6 +71,9 @@ jobs: label-filter-input: ${{ inputs.ginkgo-label }} detect_changes: + permissions: + contents: read + pull-requests: read # PR-only: the release path is a workflow_dispatch --ref dispatch that # always validates and bypasses change detection; entry jobs break # skip-propagation with !cancelled(). @@ -106,6 +108,7 @@ jobs: uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 with: fetch-depth: 0 + persist-credentials: false - run: git fetch --force --tags @@ -174,6 +177,7 @@ jobs: - detect_changes if: >- !cancelled() && needs.build.result == 'success' && + (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && needs.parse_label-filter.outputs.skip-edited != 'true' && ( (github.event_name == 'pull_request' && needs.detect_changes.outputs.has_changed == 'true') || @@ -190,6 +194,7 @@ jobs: uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 with: fetch-depth: 0 + persist-credentials: false - run: git fetch --force --tags diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 127c59a942..8f3f589548 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -1,5 +1,8 @@ name: E2E CI +permissions: + contents: read + on: pull_request: branches: @@ -20,6 +23,9 @@ env: jobs: detect_changes: + permissions: + contents: read + pull-requests: read # PR-only: the release path is a workflow_dispatch --ref dispatch that # always validates, so it bypasses change detection. Entry jobs break the # skip-propagation with !cancelled(). @@ -51,6 +57,7 @@ jobs: - name: Checkout repository uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 with: + persist-credentials: false fetch-depth: 0 - run: git fetch --force --tags @@ -102,6 +109,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + with: + persist-credentials: false - id: set-paths-matrix run: | set -x @@ -134,6 +143,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + with: + persist-credentials: false - uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4 name: Setup Helm @@ -225,6 +236,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + with: + persist-credentials: false - uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4 name: Setup Helm diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index caa168cc64..93dcdb665f 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -1,5 +1,8 @@ name: Lint +permissions: + contents: read + on: pull_request: branches: @@ -32,6 +35,8 @@ jobs: steps: - name: Checkout code uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + with: + persist-credentials: false - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 with: diff --git a/.github/workflows/pull-request-events.yaml b/.github/workflows/pull-request-events.yaml index 0869db492d..3fb65fd6fb 100644 --- a/.github/workflows/pull-request-events.yaml +++ b/.github/workflows/pull-request-events.yaml @@ -1,8 +1,10 @@ name: Pull request events on: - pull_request_target: # zizmor: ignore[dangerous-triggers] -- metadata only; no checkout, business secrets, or write token - types: [labeled, closed] + pull_request: + # Merges use the trusted push consumer; signaling close as well would repeat + # the same backport. Labels added after merge still need their own signal. + types: [labeled] permissions: {} @@ -10,15 +12,9 @@ jobs: relay: runs-on: ubuntu-latest timeout-minutes: 5 - permissions: - contents: read + permissions: {} steps: - - name: Record event - run: >- - jq '{number,action,label: (if .action == "labeled" then .label.name else "" end)}' "$GITHUB_EVENT_PATH" > "$RUNNER_TEMP/event.json" - - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 - with: - name: pull-request-event-${{ github.run_attempt }} - path: ${{ runner.temp }}/event.json - retention-days: 1 - if-no-files-found: error + # The trusted consumer resolves PR identity through GitHub's API. Nothing + # supplied by this PR revision (code, artifacts, or event JSON) is consumed. + - name: Signal pull request activity + run: ':' diff --git a/.github/workflows/unit-tests.yaml b/.github/workflows/unit-tests.yaml index 06b4f60dc3..d3a2fe35f6 100644 --- a/.github/workflows/unit-tests.yaml +++ b/.github/workflows/unit-tests.yaml @@ -1,5 +1,8 @@ name: Unit tests +permissions: + contents: read + on: workflow_dispatch: pull_request: @@ -28,6 +31,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + with: + persist-credentials: false - name: Install Helm Unit Test Plugin run: | helm plugin install https://github.com/helm-unittest/helm-unittest --version v0.4.4 @@ -43,6 +48,8 @@ jobs: steps: - name: Check out code into the Go module directory uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + with: + persist-credentials: false - name: Set up Go uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 with: From 6cdf5ffe81911e55de504ab38d3bd6d59d4a6bc3 Mon Sep 17 00:00:00 2001 From: Piotr Zaniewski Date: Thu, 5 Nov 2026 14:00:33 +0100 Subject: [PATCH 4/8] fix(ci): pin helm for the chart unit test plugin The runner Helm now requires plugin verification, so the existing helm-unittest v0.4.4 install fails before any chart tests run. Keep chart verification on the same Helm 3 version already used by the e2e workflows. --- .github/workflows/unit-tests.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/unit-tests.yaml b/.github/workflows/unit-tests.yaml index d3a2fe35f6..57c11d5a60 100644 --- a/.github/workflows/unit-tests.yaml +++ b/.github/workflows/unit-tests.yaml @@ -33,6 +33,10 @@ jobs: uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 with: persist-credentials: false + - name: Set up Helm + uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4 + with: + version: v3.22.1 - name: Install Helm Unit Test Plugin run: | helm plugin install https://github.com/helm-unittest/helm-unittest --version v0.4.4 From 00cb72802d2fd23e0f33c9a5d849a880931bae64 Mon Sep 17 00:00:00 2001 From: Piotr Zaniewski Date: Thu, 5 Nov 2026 14:06:32 +0100 Subject: [PATCH 5/8] fix(ci): retain workspace metadata generated by go vendor Go 1.26.10 copies four upstream OpenAPI workspace files into vendor. Their omission makes the repository consistency check fail before lint can run, even though the module graph and checksums stay unchanged. --- vendor/github.com/go-openapi/analysis/go.work | 6 ++++++ vendor/github.com/go-openapi/strfmt/go.work | 7 +++++++ vendor/github.com/go-openapi/swag/go.work | 20 +++++++++++++++++++ vendor/github.com/go-openapi/swag/go.work.sum | 7 +++++++ 4 files changed, 40 insertions(+) create mode 100644 vendor/github.com/go-openapi/analysis/go.work create mode 100644 vendor/github.com/go-openapi/strfmt/go.work create mode 100644 vendor/github.com/go-openapi/swag/go.work create mode 100644 vendor/github.com/go-openapi/swag/go.work.sum diff --git a/vendor/github.com/go-openapi/analysis/go.work b/vendor/github.com/go-openapi/analysis/go.work new file mode 100644 index 0000000000..c0f02a78f6 --- /dev/null +++ b/vendor/github.com/go-openapi/analysis/go.work @@ -0,0 +1,6 @@ +go 1.25.0 + +use ( + . + ./internal/testintegration +) diff --git a/vendor/github.com/go-openapi/strfmt/go.work b/vendor/github.com/go-openapi/strfmt/go.work new file mode 100644 index 0000000000..f233e74cfd --- /dev/null +++ b/vendor/github.com/go-openapi/strfmt/go.work @@ -0,0 +1,7 @@ +use ( + . + ./enable/mongodb + ./internal/testintegration +) + +go 1.25.0 diff --git a/vendor/github.com/go-openapi/swag/go.work b/vendor/github.com/go-openapi/swag/go.work new file mode 100644 index 0000000000..8537cb2a76 --- /dev/null +++ b/vendor/github.com/go-openapi/swag/go.work @@ -0,0 +1,20 @@ +use ( + . + ./cmdutils + ./conv + ./fileutils + ./jsonname + ./jsonutils + ./jsonutils/adapters/easyjson + ./jsonutils/adapters/testintegration + ./jsonutils/adapters/testintegration/benchmarks + ./jsonutils/fixtures_test + ./loading + ./mangling + ./netutils + ./stringutils + ./typeutils + ./yamlutils +) + +go 1.25.0 diff --git a/vendor/github.com/go-openapi/swag/go.work.sum b/vendor/github.com/go-openapi/swag/go.work.sum new file mode 100644 index 0000000000..c1308cafa6 --- /dev/null +++ b/vendor/github.com/go-openapi/swag/go.work.sum @@ -0,0 +1,7 @@ +github.com/go-openapi/testify/v2 v2.0.1/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= +golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= +golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= From d301683b295d745cc7dab873a061a5b6fd9dba2a Mon Sep 17 00:00:00 2001 From: Piotr Zaniewski Date: Thu, 5 Nov 2026 14:08:59 +0100 Subject: [PATCH 6/8] fix(ci): pin the completed pr event resolver fixes The shared resolver now binds signal runs to the original fork repository and avoids repeated push-range discovery when trusted jobs restore an already selected event. Both entry points need the same reviewed revision until the shared release is published. --- .github/workflows/backport.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/backport.yaml b/.github/workflows/backport.yaml index 41b0579eeb..df89932017 100644 --- a/.github/workflows/backport.yaml +++ b/.github/workflows/backport.yaml @@ -32,7 +32,7 @@ jobs: steps: - name: Resolve merged pull requests from GitHub id: event - uses: loft-sh/github-actions/.github/actions/resolve-pr-event@2a987c344d7c30cf3003666e6e1963bb32c8c5e1 # DEVOPS-1803: shared PR #349, pending release + uses: loft-sh/github-actions/.github/actions/resolve-pr-event@a770a9dcb3a1c6c72dd2f48098f9d7c20ec15253 # DEVOPS-1803: shared PR #349, pending release with: source-workflow: .github/workflows/pull-request-events.yaml @@ -47,7 +47,7 @@ jobs: fail-fast: false matrix: event: ${{ fromJSON(needs.event.outputs.events-json) }} - uses: loft-sh/github-actions/.github/workflows/backport.yaml@2a987c344d7c30cf3003666e6e1963bb32c8c5e1 # DEVOPS-1803: shared PR #349, pending release + uses: loft-sh/github-actions/.github/workflows/backport.yaml@a770a9dcb3a1c6c72dd2f48098f9d7c20ec15253 # DEVOPS-1803: shared PR #349, pending release with: pr-event: ${{ toJSON(matrix.event) }} source-workflow: .github/workflows/pull-request-events.yaml From 73279ec1db29db168fb4f984d116d4e67e894149 Mon Sep 17 00:00:00 2001 From: Piotr Zaniewski Date: Thu, 5 Nov 2026 14:13:09 +0100 Subject: [PATCH 7/8] fix(ci): pin the final shared pr event release candidate The shared prerequisite completed its final review and test round. Keep this consumer on that exact revision until the resolver tag is published so rollout cannot pick up an earlier candidate. --- .github/workflows/backport.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/backport.yaml b/.github/workflows/backport.yaml index df89932017..775f61516c 100644 --- a/.github/workflows/backport.yaml +++ b/.github/workflows/backport.yaml @@ -32,7 +32,7 @@ jobs: steps: - name: Resolve merged pull requests from GitHub id: event - uses: loft-sh/github-actions/.github/actions/resolve-pr-event@a770a9dcb3a1c6c72dd2f48098f9d7c20ec15253 # DEVOPS-1803: shared PR #349, pending release + uses: loft-sh/github-actions/.github/actions/resolve-pr-event@28f0433fb8dca3eb19918224e5bdcd44f69be28c # DEVOPS-1803: shared PR #349, pending release with: source-workflow: .github/workflows/pull-request-events.yaml @@ -47,7 +47,7 @@ jobs: fail-fast: false matrix: event: ${{ fromJSON(needs.event.outputs.events-json) }} - uses: loft-sh/github-actions/.github/workflows/backport.yaml@a770a9dcb3a1c6c72dd2f48098f9d7c20ec15253 # DEVOPS-1803: shared PR #349, pending release + uses: loft-sh/github-actions/.github/workflows/backport.yaml@28f0433fb8dca3eb19918224e5bdcd44f69be28c # DEVOPS-1803: shared PR #349, pending release with: pr-event: ${{ toJSON(matrix.event) }} source-workflow: .github/workflows/pull-request-events.yaml From 19631173e9eb54b6d45a57b76178e0627ade30ee Mon Sep 17 00:00:00 2001 From: Piotr Zaniewski Date: Wed, 11 Nov 2026 11:38:44 +0100 Subject: [PATCH 8/8] fix(ci): use published backport workflow references Backport automation depends on a release candidate that is no longer the published integration point. The published tags allow the supported resolver and reusable workflow to receive coordinated updates. --- .github/tests/test-pr-event-workflows.py | 4 ++-- .github/workflows/backport.yaml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/tests/test-pr-event-workflows.py b/.github/tests/test-pr-event-workflows.py index 085a5b8d82..d4d97afc4e 100644 --- a/.github/tests/test-pr-event-workflows.py +++ b/.github/tests/test-pr-event-workflows.py @@ -65,8 +65,8 @@ def test_trusted_backport_resolves_and_forwards_only_verified_events(self): self.assertEqual(call['with']['pr-event'], '${{ toJSON(matrix.event) }}') resolver = step['uses'].rsplit('@', 1)[1] reusable = call['uses'].rsplit('@', 1)[1] - self.assertRegex(resolver, r'^[0-9a-f]{40}$') - self.assertEqual(resolver, reusable) + self.assertEqual(resolver, 'resolve-pr-event/v1') + self.assertEqual(reusable, 'backport/v2') def test_pr_code_jobs_are_read_only_and_drop_checkout_credentials(self): for name in ('e2e.yaml', 'e2e-ginkgo.yaml', 'unit-tests.yaml', 'lint.yaml'): diff --git a/.github/workflows/backport.yaml b/.github/workflows/backport.yaml index 775f61516c..325cec7522 100644 --- a/.github/workflows/backport.yaml +++ b/.github/workflows/backport.yaml @@ -32,7 +32,7 @@ jobs: steps: - name: Resolve merged pull requests from GitHub id: event - uses: loft-sh/github-actions/.github/actions/resolve-pr-event@28f0433fb8dca3eb19918224e5bdcd44f69be28c # DEVOPS-1803: shared PR #349, pending release + uses: loft-sh/github-actions/.github/actions/resolve-pr-event@resolve-pr-event/v1 with: source-workflow: .github/workflows/pull-request-events.yaml @@ -47,7 +47,7 @@ jobs: fail-fast: false matrix: event: ${{ fromJSON(needs.event.outputs.events-json) }} - uses: loft-sh/github-actions/.github/workflows/backport.yaml@28f0433fb8dca3eb19918224e5bdcd44f69be28c # DEVOPS-1803: shared PR #349, pending release + uses: loft-sh/github-actions/.github/workflows/backport.yaml@backport/v2 with: pr-event: ${{ toJSON(matrix.event) }} source-workflow: .github/workflows/pull-request-events.yaml