From 9f1ade5dde58908433612ef26629ae921bbc53e8 Mon Sep 17 00:00:00 2001 From: Lucas Koontz Date: Thu, 27 Aug 2026 10:28:34 -0700 Subject: [PATCH 1/3] chore(cla-assistant): harden the reusable and drop the runner input (ENG-2017) Ten public repos reached mdb-dev through a hand-rolled copy of this job. An outside account opening a pull request or leaving a comment started every one of those runs with no approval, because pull_request_target and issue_comment both execute in base-repo context. Delete the runs-on input so no caller can point the CLA check at a self-hosted runner. Pin contributor-assistant/github-action to the commit behind v2.6.1. Move the event filter from the step to the job, so an unrelated comment reports as skipped instead of claiming a runner. Reduce actions to read: the only write it buys is a re-run API that refuses a GITHUB_TOKEN, and dropping the scope entirely fails the job on the workflow listing that precedes it. Refs: ENG-2017 --- .github/workflows/cla-assistant.yml | 38 +++++++++++++++++++++-------- README.md | 35 ++++++++++++++++++++++++-- 2 files changed, 61 insertions(+), 12 deletions(-) diff --git a/.github/workflows/cla-assistant.yml b/.github/workflows/cla-assistant.yml index 44bc9f0..fdbbffe 100644 --- a/.github/workflows/cla-assistant.yml +++ b/.github/workflows/cla-assistant.yml @@ -1,5 +1,5 @@ # Reusable workflow: the CLA signature check on pull requests from outside -# contributors, for the public repos (cowork, cowork-server, anton). +# contributors, for the public repos. # # The bot comments on an unsigned PR, records a signature when the contributor # replies with the agreement sentence, and re-checks on `recheck`. Signatures are @@ -11,7 +11,7 @@ # anton/.github/workflows/cla.yml # name: "MindsDB Anton CLA Assistant" # permissions: -# actions: write +# actions: read # contents: write # pull-requests: write # statuses: write @@ -30,6 +30,16 @@ # The caller must declare those four permissions: the action writes the # signature file, comments on the PR, and sets the commit status, and a called # workflow can never hold more than its caller grants. +# +# `actions` is READ, not write. The only write it would buy is +# `pullRerunRunner.ts`'s re-run of a previously failed CLA run, and that API +# refuses a `GITHUB_TOKEN`, so the call fails and the action swallows the error. +# The scope cannot be dropped entirely: the same file lists the repo's workflows +# first, and `main.ts` turns any throw into a failed job. +# +# There is no runner input. This check calls the GitHub API and nothing else, so +# it has no reason to sit on a pod inside our clusters, and leaving a knob here +# is how ten repos ended up pointing it at `mdb-dev`. name: CLA Assistant @@ -52,24 +62,32 @@ on: description: "Branch the signature ledger is committed to" type: string default: 'cla' - runs-on: - description: "Runner label for the check" - type: string - default: ubuntu-latest permissions: - actions: write + actions: read contents: write pull-requests: write statuses: write jobs: CLAssistant: - runs-on: ${{ inputs.runs-on }} + # Gate the JOB, not the step. Every caller also triggers on `issue_comment`, + # which fires on a comment on any issue in the repo, from any account. At + # step level that still starts a runner and then does nothing; here the run + # reports the job as skipped and claims nothing. + # + # Every `pull_request_target` runs, and that is deliberate: the bot has to + # look at each opened, synchronized and closed PR to decide whether its + # author has signed. Narrowing this to comments only is what leaves a repo + # with a CLA check that never asks anybody to sign. + if: >- + github.event_name == 'pull_request_target' + || github.event.comment.body == 'recheck' + || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA' + runs-on: ubuntu-latest steps: - name: "CLA Assistant" - if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target' - uses: contributor-assistant/github-action@v2.6.1 + uses: contributor-assistant/github-action@ca4a40a7d1004f18d9960b404b97e5f30a505a08 # v2.6.1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/README.md b/README.md index e3caa23..15f7bdb 100644 --- a/README.md +++ b/README.md @@ -357,10 +357,17 @@ the group name) and skipping auto-version commits with `cla-assistant.yml` runs the contributor-agreement check on the public repos. The wrapper keeps the `issue_comment` + `pull_request_target` triggers (the action -reads those payloads directly) and the four write permissions, and passes the +reads those payloads directly) and declares the permissions, and passes the per-repo agreement URL and allowlist: ```yaml +permissions: + actions: read + contents: write + pull-requests: write + statuses: write + +jobs: cla: uses: mindsdb/github-actions/.github/workflows/cla-assistant.yml@ # v1 with: @@ -369,7 +376,31 @@ per-repo agreement URL and allowlist: ``` Signatures are committed to the calling repo's own `cla` branch, so each repo -keeps its own ledger. +keeps its own ledger. `path-to-signatures` and `branch` default to that shape; +pass them only where a repo already keeps its ledger somewhere else. + +**There is no runner input, and that is the point.** This check calls the GitHub +API and nothing else, so it never needs a pod in our clusters. Ten public repos +reached `mdb-dev` through a hand-rolled copy of this job, and an outside account +opening a pull request or leaving a comment started every one of those runs with +no approval, because `pull_request_target` and `issue_comment` both execute in +base-repo context. + +**The permission that is read rather than write is `actions`.** The upstream +README asks for `actions: write`. The only write it buys is +`pullRerunRunner.ts` re-running a previously failed CLA run, and that API +refuses a `GITHUB_TOKEN`, so the call fails and the action logs and continues. +Dropping the scope altogether does break it: the same file lists the repo's +workflows first, and `main.ts` turns any throw into a failed job. + +**The job carries the event filter, not the step.** `issue_comment` fires on a +comment on any issue in the repo, from any account. Filtering inside the step +still starts a runner for every one of them. Filtering on the job means the run +reports it as skipped. Every `pull_request_target` still runs, because the bot +has to look at each opened, synchronized and closed pull request to decide +whether its author has signed. Four repos learned that the hard way, gating on +`github.event_name == 'pull_request'` while triggering on events that are not +`pull_request`, so their check quietly never asked anybody to sign. ### Prerequisites (provisioned once, org level, scoped to the release-train repos) From 94cc602fdb19f5f3873001a20adfe1224eab5a02 Mon Sep 17 00:00:00 2001 From: Lucas Koontz Date: Thu, 27 Aug 2026 14:31:43 -0700 Subject: [PATCH 2/3] fix(cla-assistant): create the signature ledger branch when it is missing (ENG-2017) The action writes the ledger through the contents API, and that API answers 404 for a branch that does not exist rather than creating one. Nothing upstream creates it, and upstream is archived, so five repos needed the branch made by hand before their first signature could land. A repo adopting this workflow would have hit the same wall, and the symptom is a red check on a contributor's pull request with nothing they can do about it. Refs: ENG-2017 --- .github/workflows/cla-assistant.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/.github/workflows/cla-assistant.yml b/.github/workflows/cla-assistant.yml index fdbbffe..818830e 100644 --- a/.github/workflows/cla-assistant.yml +++ b/.github/workflows/cla-assistant.yml @@ -40,6 +40,10 @@ # There is no runner input. This check calls the GitHub API and nothing else, so # it has no reason to sit on a pod inside our clusters, and leaving a knob here # is how ten repos ended up pointing it at `mdb-dev`. +# +# The ledger branch is created here rather than assumed. The action writes with +# the contents API, which 404s on a branch that does not exist, so a repo +# adopting this workflow would otherwise fail on its first signature. name: CLA Assistant @@ -86,6 +90,29 @@ jobs: || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA' runs-on: ubuntu-latest steps: + # The action writes the ledger through the contents API, and that API + # cannot create a branch: it answers 404 for a `branch:` that does not + # exist, so the very first signature in a new repo fails and the + # contributor sees a red check with nothing to act on. Upstream never + # creates it and upstream is archived, so it is created here. + - name: Ensure the signature ledger branch exists + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + LEDGER_BRANCH: ${{ inputs.branch }} + REPO: ${{ github.repository }} + run: | + set -euo pipefail + if gh api "/repos/${REPO}/git/ref/heads/${LEDGER_BRANCH}" >/dev/null 2>&1; then + # Expect: this is the path every run after the first one takes + echo "Ledger branch ${LEDGER_BRANCH} already exists." + exit 0 + fi + BASE=$(gh api "/repos/${REPO}" --jq .default_branch) + SHA=$(gh api "/repos/${REPO}/git/ref/heads/${BASE}" --jq .object.sha) + # Expect: refs/heads/, created at the default branch tip + gh api -X POST "/repos/${REPO}/git/refs" \ + -f ref="refs/heads/${LEDGER_BRANCH}" -f sha="${SHA}" --jq .ref + - name: "CLA Assistant" uses: contributor-assistant/github-action@ca4a40a7d1004f18d9960b404b97e5f30a505a08 # v2.6.1 env: From d5135c6d198ae28dfe791b877c65e568e5e79957 Mon Sep 17 00:00:00 2001 From: Lucas Koontz Date: Thu, 27 Aug 2026 15:26:18 -0700 Subject: [PATCH 3/3] fix(cla-assistant): seed the ledger file, not just its branch (ENG-2017) The ledger needs a branch and a file, and the action supplies neither. The branch this workflow already creates. The file it does not, and the action's own handler for it is dead code: setupClaCheck.ts guards the create path with `error.status === "404"`, a string compared strictly against Octokit's numeric RequestError.status, so a missing ledger falls through to "Could not retrieve repository contents. Status: 404" and main.ts fails the job. Read out of dist/index.js at the pinned commit, which is what actually executes. dataprep_ml, mindsdb_evaluator and type_infer each have a cla branch and no ledger file, so all three would have hit this on their first external pull request: a red check with nothing to act on, instead of the bot asking for a signature. That is the case this change set exists to fix. The seeded content is byte-identical to what the action would have written, JSON.stringify(content, null, 3) at 31 bytes, so the first real signature lands as a one-line diff rather than a reformat. Also points the caller example and the README at mindsdb/mindshub rather than the mindsdb/mindsdb redirect, matching the thirteen callers. --- .github/workflows/cla-assistant.yml | 60 +++++++++++++++++++++-------- README.md | 20 +++++++++- 2 files changed, 64 insertions(+), 16 deletions(-) diff --git a/.github/workflows/cla-assistant.yml b/.github/workflows/cla-assistant.yml index 818830e..87ccff7 100644 --- a/.github/workflows/cla-assistant.yml +++ b/.github/workflows/cla-assistant.yml @@ -24,7 +24,7 @@ # cla: # uses: mindsdb/github-actions/.github/workflows/cla-assistant.yml@ # v1 # with: -# path-to-document: 'https://github.com/mindsdb/mindsdb/blob/main/assets/contributions-agreement/individual-contributor.md' +# path-to-document: 'https://github.com/mindsdb/mindshub/blob/main/assets/contributions-agreement/individual-contributor.md' # allowlist: bot*, ZoranPandovski, ... # # The caller must declare those four permissions: the action writes the @@ -41,9 +41,11 @@ # it has no reason to sit on a pod inside our clusters, and leaving a knob here # is how ten repos ended up pointing it at `mdb-dev`. # -# The ledger branch is created here rather than assumed. The action writes with -# the contents API, which 404s on a branch that does not exist, so a repo -# adopting this workflow would otherwise fail on its first signature. +# The ledger is created here rather than assumed, branch and file both. The +# action writes with the contents API, which 404s on a branch that does not +# exist; and its own handler for a missing FILE is dead code, because it +# compares `error.status` against the string `"404"`. A repo adopting this +# workflow would otherwise fail on its first signature either way. name: CLA Assistant @@ -90,28 +92,56 @@ jobs: || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA' runs-on: ubuntu-latest steps: - # The action writes the ledger through the contents API, and that API - # cannot create a branch: it answers 404 for a `branch:` that does not - # exist, so the very first signature in a new repo fails and the - # contributor sees a red check with nothing to act on. Upstream never - # creates it and upstream is archived, so it is created here. - - name: Ensure the signature ledger branch exists + # The ledger needs a branch AND a file, and the action supplies neither. + # + # The branch: it writes through the contents API, and that API answers 404 + # for a `branch:` that does not exist rather than creating one. + # + # The file: the action does try to handle that itself, and the code is + # dead. `setupClaCheck.ts` guards its create path with + # `error.status === "404"`, a string compared strictly against Octokit's + # numeric `RequestError.status`, so the branch never runs. A missing + # ledger file falls through to `Could not retrieve repository contents. + # Status: 404`, which `main.ts` turns into a failed job. Read out of + # `dist/index.js` at the pinned commit, which is what actually executes. + # + # So both halves are created here. Upstream is archived, so waiting for a + # fix upstream is waiting forever, and without this the first contributor + # to a new repo gets a red check with nothing to act on instead of being + # asked to sign. + - name: Ensure the signature ledger exists env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} LEDGER_BRANCH: ${{ inputs.branch }} + LEDGER_PATH: ${{ inputs.path-to-signatures }} REPO: ${{ github.repository }} run: | set -euo pipefail if gh api "/repos/${REPO}/git/ref/heads/${LEDGER_BRANCH}" >/dev/null 2>&1; then # Expect: this is the path every run after the first one takes echo "Ledger branch ${LEDGER_BRANCH} already exists." + else + BASE=$(gh api "/repos/${REPO}" --jq .default_branch) + SHA=$(gh api "/repos/${REPO}/git/ref/heads/${BASE}" --jq .object.sha) + # Expect: refs/heads/, created at the default branch tip + gh api -X POST "/repos/${REPO}/git/refs" \ + -f ref="refs/heads/${LEDGER_BRANCH}" -f sha="${SHA}" --jq .ref + fi + + if gh api "/repos/${REPO}/contents/${LEDGER_PATH}?ref=${LEDGER_BRANCH}" >/dev/null 2>&1; then + # Expect: this is the path every run after the first one takes + echo "Ledger file ${LEDGER_PATH} already exists on ${LEDGER_BRANCH}." exit 0 fi - BASE=$(gh api "/repos/${REPO}" --jq .default_branch) - SHA=$(gh api "/repos/${REPO}/git/ref/heads/${BASE}" --jq .object.sha) - # Expect: refs/heads/, created at the default branch tip - gh api -X POST "/repos/${REPO}/git/refs" \ - -f ref="refs/heads/${LEDGER_BRANCH}" -f sha="${SHA}" --jq .ref + # Three-space indent because that is what the action itself writes + # (`JSON.stringify(content, null, 3)`), so the first real signature + # lands as a one-line diff rather than a reformat of the whole file. + CONTENT=$(printf '{\n "signedContributors": []\n}' | base64 -w0) + # Expect: the blob SHA of the empty ledger, committed on the branch + gh api -X PUT "/repos/${REPO}/contents/${LEDGER_PATH}" \ + -f message="Create the CLA signature ledger" \ + -f branch="${LEDGER_BRANCH}" \ + -f content="${CONTENT}" --jq .content.sha - name: "CLA Assistant" uses: contributor-assistant/github-action@ca4a40a7d1004f18d9960b404b97e5f30a505a08 # v2.6.1 diff --git a/README.md b/README.md index 15f7bdb..d1c3290 100644 --- a/README.md +++ b/README.md @@ -371,14 +371,32 @@ jobs: cla: uses: mindsdb/github-actions/.github/workflows/cla-assistant.yml@ # v1 with: - path-to-document: 'https://github.com/mindsdb/mindsdb/blob/main/assets/contributions-agreement/individual-contributor.md' + path-to-document: 'https://github.com/mindsdb/mindshub/blob/main/assets/contributions-agreement/individual-contributor.md' allowlist: bot*, ZoranPandovski, ... ``` +`mindsdb/mindshub` is the repository that holds the agreement. `mindsdb/mindsdb` +and `mindsdb/minds` both still resolve to it through a rename redirect, and +neither belongs in a new caller: a redirect stops being harmless the moment +somebody creates a repository at the old name, and this is the page a +contributor reads before agreeing to it. + Signatures are committed to the calling repo's own `cla` branch, so each repo keeps its own ledger. `path-to-signatures` and `branch` default to that shape; pass them only where a repo already keeps its ledger somewhere else. +**A new caller needs no setup, because the reusable creates the ledger itself.** +Both halves of it, and the action supplies neither. The branch: the action writes +through the contents API, and that API answers 404 for a `branch:` that does not +exist rather than creating one. The file: the action does try, and the code is +dead — `setupClaCheck.ts` guards its create path with `error.status === "404"`, +a string compared strictly against Octokit's numeric `RequestError.status`, so +the branch never runs and a missing ledger fails the job with `Could not +retrieve repository contents. Status: 404` instead. Upstream is archived, so the +first step creates the branch at the default-branch tip and seeds an empty +ledger, byte-identical to what the action would have written. Every run after +the first short-circuits on two API calls. + **There is no runner input, and that is the point.** This check calls the GitHub API and nothing else, so it never needs a pod in our clusters. Ten public repos reached `mdb-dev` through a hand-rolled copy of this job, and an outside account