From c2ec5628a85f11c727b5a7c6308697f352698203 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Tue, 6 Jan 2026 11:57:38 +0100 Subject: [PATCH 01/10] Add cooldown configuration to Dependabot updates Adds 7-day cooldown period to all Dependabot package ecosystem entries to prevent excessive update frequency and potential supply chain attacks through rapid dependency updates. Detected by: zizmor v1.20.0 (dependabot-cooldown rule) --- .github/dependabot.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 73ce535..ce92872 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,22 +4,32 @@ updates: directory: /canary-release schedule: interval: weekly + cooldown: + default-days: 7 - package-ecosystem: github-actions directory: /check-cla schedule: interval: weekly + cooldown: + default-days: 7 - package-ecosystem: github-actions directory: /read-yaml schedule: interval: weekly + cooldown: + default-days: 7 - package-ecosystem: github-actions directory: /set-commit-status schedule: interval: weekly + cooldown: + default-days: 7 - package-ecosystem: pip directory: / schedule: interval: weekly + cooldown: + default-days: 7 - package-ecosystem: github-actions directory: /.github/workflows schedule: @@ -28,3 +38,5 @@ updates: workflows: patterns: - '*' + cooldown: + default-days: 7 From a9eb0d1fedf79e1ebf85260510fc4e5ec5f30706 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Tue, 6 Jan 2026 11:59:04 +0100 Subject: [PATCH 02/10] Add persist-credentials: false to checkout actions Prevents Git credentials from being persisted in the workflow environment after checkout, reducing the risk of credential exposure through artifacts or subsequent steps. Affected files: - .github/workflows/labels.yml - .github/workflows/tests.yml (4 checkouts) - .github/workflows/update.yml - check-cla/action.yml Detected by: zizmor v1.20.0 (artipacked rule) --- .github/workflows/labels.yml | 2 ++ .github/workflows/tests.yml | 8 ++++++++ .github/workflows/update.yml | 1 + check-cla/action.yml | 1 + 4 files changed, 12 insertions(+) diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml index b34a5da..7507be5 100644 --- a/.github/workflows/labels.yml +++ b/.github/workflows/labels.yml @@ -24,6 +24,8 @@ jobs: LOCAL: .github/labels.yml steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false - id: has_local uses: andstor/file-existence-action@076e0072799f4942c8bc574a82233e1e4d13e9d6 # v3.0.0 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3bbc074..f862bdc 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -23,6 +23,8 @@ jobs: steps: - name: Checkout Source uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false - name: Cache Pip uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 @@ -52,6 +54,8 @@ jobs: steps: - name: Checkout Source uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false - name: Read Remote JSON id: json @@ -89,6 +93,8 @@ jobs: steps: - name: Checkout Source uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false - name: Template Success id: templates-success @@ -146,6 +152,8 @@ jobs: - name: Checkout our source if: always() && github.event_name != 'pull_request' && steps.alls-green.outputs.result == 'failure' uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false - name: Report failures if: always() && github.event_name != 'pull_request' && steps.alls-green.outputs.result == 'failure' diff --git a/.github/workflows/update.yml b/.github/workflows/update.yml index 3ee42eb..f190c30 100644 --- a/.github/workflows/update.yml +++ b/.github/workflows/update.yml @@ -49,6 +49,7 @@ jobs: repository: ${{ env.REPOSITORY || github.repository }} ref: ${{ env.REF || '' }} token: ${{ secrets.SYNC_TOKEN }} + persist-credentials: false - name: Configure git user run: | diff --git a/check-cla/action.yml b/check-cla/action.yml index 0c153b7..9ea0553 100644 --- a/check-cla/action.yml +++ b/check-cla/action.yml @@ -120,6 +120,7 @@ runs: if: steps.metadata.outputs.has_signed == 'false' with: repository: ${{ inputs.cla_repo }} + persist-credentials: false # if unsigned, update cla_path - name: Add contributor as a CLA signee From 4520650fc672fcc03273c5e2ff9d1820f203d983 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Tue, 6 Jan 2026 12:00:28 +0100 Subject: [PATCH 03/10] Add explicit permissions to workflow files Apply least privilege principle by declaring minimal required permissions for each workflow instead of using default write-all. Detected by: zizmor v1.20.0 (excessive-permissions rule) --- .github/workflows/cla.yml | 5 +++++ .github/workflows/issues.yml | 4 ++++ .github/workflows/labels.yml | 3 +++ .github/workflows/project.yml | 3 +++ .github/workflows/tests.yml | 5 +++++ .github/workflows/update.yml | 5 +++++ 6 files changed, 25 insertions(+) diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml index 2749eb2..883a293 100644 --- a/.github/workflows/cla.yml +++ b/.github/workflows/cla.yml @@ -6,6 +6,11 @@ on: - created pull_request_target: +permissions: + contents: read + pull-requests: write + statuses: write + jobs: check: if: >- diff --git a/.github/workflows/issues.yml b/.github/workflows/issues.yml index 634bf13..d3d7c4d 100644 --- a/.github/workflows/issues.yml +++ b/.github/workflows/issues.yml @@ -6,6 +6,10 @@ on: issue_comment: types: [created] +permissions: + contents: read + issues: write + env: FEEDBACK_LBL: pending::feedback SUPPORT_LBL: pending::support diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml index 7507be5..88e15cd 100644 --- a/.github/workflows/labels.yml +++ b/.github/workflows/labels.yml @@ -15,6 +15,9 @@ on: default: false type: boolean +permissions: + contents: read + jobs: sync: if: '!github.event.repository.fork' diff --git a/.github/workflows/project.yml b/.github/workflows/project.yml index 4eda798..a73cb03 100644 --- a/.github/workflows/project.yml +++ b/.github/workflows/project.yml @@ -8,6 +8,9 @@ on: types: - opened +permissions: + contents: read + jobs: add_to_project: if: '!github.event.repository.fork' diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f862bdc..13b7fea 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,6 +13,11 @@ on: # https://crontab.guru/#15_14_*_*_* - cron: 15 14 * * * +permissions: + contents: read + issues: write + pull-requests: write + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true diff --git a/.github/workflows/update.yml b/.github/workflows/update.yml index f190c30..9b2603d 100644 --- a/.github/workflows/update.yml +++ b/.github/workflows/update.yml @@ -12,6 +12,11 @@ on: types: - created +permissions: + contents: read + pull-requests: write + issues: write + jobs: update: if: >- From 3f8c449db6cece0cf0a75f3c0fcad98f4551b651 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Tue, 6 Jan 2026 12:02:12 +0100 Subject: [PATCH 04/10] Fix template injection vulnerabilities in shell commands Replace direct template expansions with environment variables to prevent potential code injection attacks. Inputs are now passed via env vars which are properly escaped by the shell. Affected files: - canary-release/action.yml - combine-durations/action.yml - create-fork/action.yml - read-file/action.yml - template-files/action.yml - .github/workflows/update.yml Detected by: zizmor v1.20.0 (template-injection rule) --- .github/workflows/update.yml | 6 ++++-- canary-release/action.yml | 23 +++++++++++++++-------- combine-durations/action.yml | 18 ++++++++++++------ create-fork/action.yml | 6 ++++-- read-file/action.yml | 9 ++++++--- template-files/action.yml | 6 ++++-- 6 files changed, 45 insertions(+), 23 deletions(-) diff --git a/.github/workflows/update.yml b/.github/workflows/update.yml index 9b2603d..9ec4df8 100644 --- a/.github/workflows/update.yml +++ b/.github/workflows/update.yml @@ -46,8 +46,10 @@ jobs: - if: github.event.comment.body == '@conda-bot render' name: Configure git origin run: | - echo REPOSITORY=$(curl --silent ${{ github.event.issue.pull_request.url }} | jq --raw-output '.head.repo.full_name') >> $GITHUB_ENV - echo REF=$(curl --silent ${{ github.event.issue.pull_request.url }} | jq --raw-output '.head.ref') >> $GITHUB_ENV + echo REPOSITORY=$(curl --silent "${PR_URL}" | jq --raw-output '.head.repo.full_name') >> $GITHUB_ENV + echo REF=$(curl --silent "${PR_URL}" | jq --raw-output '.head.ref') >> $GITHUB_ENV + env: + PR_URL: ${{ github.event.issue.pull_request.url }} - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: diff --git a/canary-release/action.yml b/canary-release/action.yml index f253329..addb482 100644 --- a/canary-release/action.yml +++ b/canary-release/action.yml @@ -65,6 +65,13 @@ runs: shell: bash -l {0} env: BINSTAR_API_TOKEN: ${{ inputs.anaconda-org-token }} + INPUT_CONDA_BUILD_ARGUMENTS: ${{ inputs.conda-build-arguments }} + INPUT_CONDA_BUILD_PATH: ${{ inputs.conda-build-path }} + INPUT_SUBDIR: ${{ inputs.subdir }} + INPUT_PACKAGE_NAME: ${{ inputs.package-name }} + INPUT_UPLOAD: ${{ inputs.upload }} + INPUT_ANACONDA_ORG_CHANNEL: ${{ inputs.anaconda-org-channel }} + INPUT_ANACONDA_ORG_LABEL: ${{ inputs.anaconda-org-label }} run: | echo "::group::Setting up environment" set -euo pipefail @@ -89,36 +96,36 @@ runs: echo "::endgroup::" echo "::group::Building package" - conda build --croot=./pkgs ${{ inputs.conda-build-arguments }} ${{ inputs.conda-build-path }} + conda build --croot=./pkgs ${INPUT_CONDA_BUILD_ARGUMENTS} ${INPUT_CONDA_BUILD_PATH} echo "::endgroup::" echo "::group::Find packages" PACKAGES=( $( - find "./pkgs/${{ inputs.subdir }}" -type f \ + find "./pkgs/${INPUT_SUBDIR}" -type f \ \( \ - -name "${{ inputs.package-name }}-*.tar.bz2" -o \ - -name "${{ inputs.package-name }}-*.conda" \ + -name "${INPUT_PACKAGE_NAME}-*.tar.bz2" -o \ + -name "${INPUT_PACKAGE_NAME}-*.conda" \ \) ) ) echo "::endgroup::" - if [[ "${{ inputs.upload }}" == "true" ]]; then + if [[ "${INPUT_UPLOAD}" == "true" ]]; then echo "::group::Uploading package" anaconda \ upload \ --force \ --register \ --no-progress \ - --user="${{ inputs.anaconda-org-channel }}" \ - --label="${{ inputs.anaconda-org-label }}" \ + --user="${INPUT_ANACONDA_ORG_CHANNEL}" \ + --label="${INPUT_ANACONDA_ORG_LABEL}" \ "${PACKAGES[@]}" echo "Uploaded the following files:" basename -a "${PACKAGES[@]}" echo "::endgroup::" echo "Use this command to try out the build:" - echo "conda install -c ${{ inputs.anaconda-org-channel }}/label/${{ inputs.anaconda-org-label }} ${{ inputs.package-name }}" + echo "conda install -c ${INPUT_ANACONDA_ORG_CHANNEL}/label/${INPUT_ANACONDA_ORG_LABEL} ${INPUT_PACKAGE_NAME}" else echo "Skipping upload because 'upload != true'." fi diff --git a/combine-durations/action.yml b/combine-durations/action.yml index d01d9f8..9198284 100644 --- a/combine-durations/action.yml +++ b/combine-durations/action.yml @@ -38,20 +38,24 @@ runs: shell: bash run: > gh run list - --repo ${{ inputs.repository }} - --branch ${{ inputs.branch }} - --workflow ${{ inputs.workflow }} + --repo "${INPUT_REPOSITORY}" + --branch "${INPUT_BRANCH}" + --workflow "${INPUT_WORKFLOW}" --limit 10 --json databaseId --jq '.[].databaseId' | xargs -n 1 gh run download - --repo ${{ inputs.repository }} + --repo "${INPUT_REPOSITORY}" --dir ${{ runner.temp }}/artifacts/ - --pattern '${{ inputs.pattern }}' + --pattern "${INPUT_PATTERN}" || true env: GITHUB_TOKEN: ${{ github.token }} + INPUT_REPOSITORY: ${{ inputs.repository }} + INPUT_BRANCH: ${{ inputs.branch }} + INPUT_WORKFLOW: ${{ inputs.workflow }} + INPUT_PATTERN: ${{ inputs.pattern }} # `hashFiles` only works on files within the working directory, since `requirements.txt` # is not in the working directory we need to manually compute the SHA256 hash @@ -84,5 +88,7 @@ runs: shell: bash run: > python ${{ github.action_path }}/combine_durations.py - --durations-dir=${{ inputs.durations-dir }} + --durations-dir="${INPUT_DURATIONS_DIR}" --artifacts-dir=${{ runner.temp }}/artifacts/ + env: + INPUT_DURATIONS_DIR: ${{ inputs.durations-dir }} diff --git a/create-fork/action.yml b/create-fork/action.yml index 98f9689..7d7a64c 100644 --- a/create-fork/action.yml +++ b/create-fork/action.yml @@ -33,7 +33,7 @@ runs: RESPONSE=$(gh api \ -X POST \ -H "Accept: application/vnd.github+json" \ - "/repos/${{ inputs.repository }}/forks" \ + "/repos/${INPUT_REPOSITORY}/forks" \ -f default_branch_only=true) # extract values with jq @@ -43,9 +43,11 @@ runs: # wait a minute to ensure the fork is ready TIMESTAMP="$(date -d "${CREATED_AT}" +%s)" CURRENT="$(date +%s)" - [ $((CURRENT - TIMESTAMP)) -gt 60 ] || sleep ${{ inputs.timeout }} + [ $((CURRENT - TIMESTAMP)) -gt 60 ] || sleep "${INPUT_TIMEOUT}" # store values for subsequent usage echo fork="${FULL_NAME}" >> $GITHUB_OUTPUT env: GH_TOKEN: ${{ inputs.token }} + INPUT_REPOSITORY: ${{ inputs.repository }} + INPUT_TIMEOUT: ${{ inputs.timeout }} diff --git a/read-file/action.yml b/read-file/action.yml index 5398909..f0ea255 100644 --- a/read-file/action.yml +++ b/read-file/action.yml @@ -52,8 +52,11 @@ runs: shell: bash run: > python ${{ github.action_path }}/read_file.py - ${{ inputs.path }} - ${{ inputs.parser && format('"--parser={0}"', inputs.parser) || '' }} - ${{ inputs.default && format('"--default={0}"', inputs.default) || '' }} + "${INPUT_PATH}" + ${INPUT_PARSER:+"--parser=${INPUT_PARSER}"} + ${INPUT_DEFAULT:+"--default=${INPUT_DEFAULT}"} env: GITHUB_TOKEN: ${{ github.token }} + INPUT_PATH: ${{ inputs.path }} + INPUT_PARSER: ${{ inputs.parser }} + INPUT_DEFAULT: ${{ inputs.default }} diff --git a/template-files/action.yml b/template-files/action.yml index 73ca89a..4b71602 100644 --- a/template-files/action.yml +++ b/template-files/action.yml @@ -56,7 +56,9 @@ runs: shell: bash run: > python ${{ github.action_path }}/template_files.py - --config ${{ inputs.config }} - --stubs ${{ inputs.stubs }} + --config "${INPUT_CONFIG}" + --stubs "${INPUT_STUBS}" env: GITHUB_TOKEN: ${{ github.token }} + INPUT_CONFIG: ${{ inputs.config }} + INPUT_STUBS: ${{ inputs.stubs }} From 8ccee0db2e6a7483c7cdf1d7ba14e35a9e311fbc Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Tue, 6 Jan 2026 12:03:32 +0100 Subject: [PATCH 05/10] Fix template injection in github-script and Python steps Use process.env and os.environ to access inputs instead of direct template expansion in JavaScript and Python code blocks. Affected files: - set-commit-status/action.yml - read-yaml/action.yml - check-cla/action.yml Detected by: zizmor v1.20.0 (template-injection rule) --- check-cla/action.yml | 18 +++++++++++++----- read-yaml/action.yml | 7 +++++-- set-commit-status/action.yml | 13 +++++++++---- 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/check-cla/action.yml b/check-cla/action.yml index 9ea0553..03b8cca 100644 --- a/check-cla/action.yml +++ b/check-cla/action.yml @@ -53,6 +53,10 @@ runs: - name: Collect PR metadata uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd id: metadata + env: + INPUT_LABEL: ${{ inputs.label }} + INPUT_CLA_REPO: ${{ inputs.cla_repo }} + INPUT_CLA_PATH: ${{ inputs.cla_path }} with: github-token: ${{ inputs.token }} script: | @@ -70,15 +74,15 @@ runs: const labels = raw.data.labels.map(label => label.name); core.debug(`labels: ${labels}`); - const has_label = labels.includes('${{ inputs.label }}'); + const has_label = labels.includes(process.env.INPUT_LABEL); core.setOutput('has_label', has_label); core.debug(`has_label: ${has_label}`); - const cla_repo = '${{ inputs.cla_repo }}'.split('/', 2); + const cla_repo = process.env.INPUT_CLA_REPO.split('/', 2); const { content, encoding } = (await github.rest.repos.getContent({ owner: cla_repo[0], repo: cla_repo[1], - path: '${{ inputs.cla_path }}' + path: process.env.INPUT_CLA_PATH })).data; const contributors = JSON.parse( Buffer.from(content, encoding).toString('utf-8') @@ -126,13 +130,17 @@ runs: - name: Add contributor as a CLA signee shell: python if: steps.metadata.outputs.has_signed == 'false' + env: + INPUT_CLA_PATH: ${{ inputs.cla_path }} + CONTRIBUTOR: ${{ steps.metadata.outputs.contributor }} run: | import json + import os from pathlib import Path - path = Path("${{ inputs.cla_path }}") + path = Path(os.environ["INPUT_CLA_PATH"]) signees = json.loads(path.read_text()) - signees["contributors"].append("${{ steps.metadata.outputs.contributor }}") + signees["contributors"].append(os.environ["CONTRIBUTOR"]) signees["contributors"].sort(key=str.lower) path.write_text(json.dumps(signees, indent=2) + "\n") diff --git a/read-yaml/action.yml b/read-yaml/action.yml index edb953b..3f1c994 100644 --- a/read-yaml/action.yml +++ b/read-yaml/action.yml @@ -22,6 +22,9 @@ runs: shell: bash -l {0} - id: read_yaml uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd + env: + INPUT_PATH: ${{ inputs.path }} + INPUT_KEY: ${{ inputs.key }} with: script: | const yaml = require('js-yaml'); @@ -90,8 +93,8 @@ runs: } async function main() { - const path = "${{ inputs.path }}"; - const key = `${{ inputs.key }}`.trim(); + const path = process.env.INPUT_PATH; + const key = (process.env.INPUT_KEY || '').trim(); let value = await readYaml(path); if (key) { diff --git a/set-commit-status/action.yml b/set-commit-status/action.yml index 4b1e561..6c41ccc 100644 --- a/set-commit-status/action.yml +++ b/set-commit-status/action.yml @@ -25,6 +25,11 @@ runs: using: composite steps: - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd + env: + INPUT_CONTEXT: ${{ inputs.context }} + INPUT_DESCRIPTION: ${{ inputs.description }} + INPUT_STATE: ${{ inputs.state }} + INPUT_TARGET_URL: ${{ inputs.target_url }} with: github-token: ${{ inputs.token }} script: | @@ -39,12 +44,12 @@ runs: core.debug(`sha: ${sha}`); const { context: name, state } = (await github.rest.repos.createCommitStatus({ - context: '${{ inputs.context }}', - description: '${{ inputs.description }}', + context: process.env.INPUT_CONTEXT, + description: process.env.INPUT_DESCRIPTION, owner: owner, repo: repo, sha: sha, - state: '${{ inputs.state }}', - target_url: '${{ inputs.target_url }}' + state: process.env.INPUT_STATE, + target_url: process.env.INPUT_TARGET_URL })).data; core.info(`${name} is ${state}`); From 241676ff5d5d30111a71f6f41d2210232ebd8096 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Tue, 6 Jan 2026 12:16:37 +0100 Subject: [PATCH 06/10] Refactor CLA workflow to use safer workflow_run pattern Split the CLA workflow into two parts for improved security: 1. cla-trigger.yml: Runs on pull_request_target but only saves PR metadata (number, author, url, sha) to artifacts. No secrets used. 2. cla.yml: Runs on workflow_run after trigger completes. Downloads PR metadata from artifacts and uses secrets safely. This follows the recommended pattern for secure pull_request_target usage by separating untrusted PR context from secret access. Also adds zizmor.yml configuration file with documented suppressions for intentional dangerous trigger usage in: - cla-trigger.yml (saves metadata only, no secrets) - cla.yml (workflow_run with artifact-based PR context) - project.yml (no code checkout, only adds to project) --- .github/workflows/cla-trigger.yml | 35 +++++++++++++++++++++++ .github/workflows/cla.yml | 47 ++++++++++++++++++++++++++++--- check-cla/action.yml | 31 +++++++++++++++++--- zizmor.yml | 23 +++++++++++++++ 4 files changed, 128 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/cla-trigger.yml create mode 100644 zizmor.yml diff --git a/.github/workflows/cla-trigger.yml b/.github/workflows/cla-trigger.yml new file mode 100644 index 0000000..ad45969 --- /dev/null +++ b/.github/workflows/cla-trigger.yml @@ -0,0 +1,35 @@ +name: CLA Trigger + +on: + pull_request_target: + types: + - opened + - synchronize + - reopened + +permissions: + contents: read + +jobs: + save-pr-info: + runs-on: ubuntu-latest + steps: + - name: Save PR metadata + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_AUTHOR: ${{ github.event.pull_request.user.login }} + PR_URL: ${{ github.event.pull_request.html_url }} + PR_SHA: ${{ github.event.pull_request.head.sha }} + run: | + mkdir -p pr-info + echo "${PR_NUMBER}" > pr-info/number + echo "${PR_AUTHOR}" > pr-info/author + echo "${PR_URL}" > pr-info/url + echo "${PR_SHA}" > pr-info/sha + + - name: Upload PR info + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: pr-info + path: pr-info/ + retention-days: 1 diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml index 883a293..a10bf0c 100644 --- a/.github/workflows/cla.yml +++ b/.github/workflows/cla.yml @@ -1,27 +1,59 @@ name: CLA on: + # Triggered by comment to re-check CLA status issue_comment: types: - created - pull_request_target: + + # Triggered after CLA Trigger workflow completes (safer than pull_request_target) + workflow_run: + workflows: ["CLA Trigger"] + types: + - completed permissions: contents: read pull-requests: write statuses: write + actions: read # Required to download artifacts from workflow_run jobs: check: if: >- !github.event.repository.fork && ( - github.event.issue.pull_request - && github.event.comment.body == '@conda-bot check' - || github.event_name == 'pull_request_target' + ( + github.event_name == 'issue_comment' + && github.event.issue.pull_request + && github.event.comment.body == '@conda-bot check' + ) + || ( + github.event_name == 'workflow_run' + && github.event.workflow_run.conclusion == 'success' + ) ) runs-on: ubuntu-latest steps: + # For workflow_run events, download PR info from artifact + - name: Download PR info + if: github.event_name == 'workflow_run' + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: pr-info + path: pr-info/ + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Set PR context from artifact + if: github.event_name == 'workflow_run' + id: pr-context + run: | + echo "number=$(cat pr-info/number)" >> $GITHUB_OUTPUT + echo "author=$(cat pr-info/author)" >> $GITHUB_OUTPUT + echo "url=$(cat pr-info/url)" >> $GITHUB_OUTPUT + echo "sha=$(cat pr-info/sha)" >> $GITHUB_OUTPUT + - name: Check CLA uses: conda/actions/check-cla@f05161c6e6e37a49b17c8e0b436197b53830318a # v25.9.2 with: @@ -38,3 +70,10 @@ jobs: # Token for opening signee PR in the provided `cla_repo` # (`pull_request: write` for fine-grained PAT; `repo` and `workflow` for classic PAT) cla_token: ${{ secrets.CLA_FORK_TOKEN }} + + # PR context from workflow_run artifact (if applicable) + pr_number: ${{ steps.pr-context.outputs.number }} + pr_author: ${{ steps.pr-context.outputs.author }} + pr_url: ${{ steps.pr-context.outputs.url }} + pr_sha: ${{ steps.pr-context.outputs.sha }} + diff --git a/check-cla/action.yml b/check-cla/action.yml index 03b8cca..2ff15fe 100644 --- a/check-cla/action.yml +++ b/check-cla/action.yml @@ -27,6 +27,19 @@ inputs: cla_author: description: Git-format author/committer to use for pull request commits default: Conda Bot <18747875+conda-bot@users.noreply.github.com> + # Optional inputs for workflow_run trigger (when context.issue is not available) + pr_number: + description: PR number (optional, for workflow_run triggers) + required: false + pr_author: + description: PR author username (optional, for workflow_run triggers) + required: false + pr_url: + description: PR URL (optional, for workflow_run triggers) + required: false + pr_sha: + description: PR head SHA (optional, for workflow_run triggers) + required: false runs: using: composite @@ -57,10 +70,19 @@ runs: INPUT_LABEL: ${{ inputs.label }} INPUT_CLA_REPO: ${{ inputs.cla_repo }} INPUT_CLA_PATH: ${{ inputs.cla_path }} + INPUT_PR_NUMBER: ${{ inputs.pr_number }} + INPUT_PR_AUTHOR: ${{ inputs.pr_author }} + INPUT_PR_URL: ${{ inputs.pr_url }} with: github-token: ${{ inputs.token }} script: | - const { owner, repo, number } = context.issue; + const { owner, repo } = context.repo; + + // Use provided inputs or fall back to context + const number = process.env.INPUT_PR_NUMBER + ? parseInt(process.env.INPUT_PR_NUMBER, 10) + : context.issue.number; + core.debug(`owner: ${owner}`); core.debug(`repo: ${repo}`); core.setOutput('number', number); @@ -89,12 +111,13 @@ runs: ).contributors; core.debug(`contributors: ${contributors}`); - const payload = context.payload.issue || context.payload.pull_request || context.payload; - const contributor = payload.user.login; + // Use provided author or get from PR data + const contributor = process.env.INPUT_PR_AUTHOR || raw.data.user.login; core.setOutput('contributor', contributor); core.debug(`contributor: ${contributor}`); - const url = payload.html_url; + // Use provided URL or get from PR data + const url = process.env.INPUT_PR_URL || raw.data.html_url; core.setOutput('url', url); core.debug(`url: ${url}`); diff --git a/zizmor.yml b/zizmor.yml new file mode 100644 index 0000000..2f7aefc --- /dev/null +++ b/zizmor.yml @@ -0,0 +1,23 @@ +# zizmor configuration file +# https://docs.zizmor.sh/configuration/ + +rules: + dangerous-triggers: + ignore: + # cla-trigger.yml uses pull_request_target but is safe because: + # - It does NOT checkout any code from the PR + # - It only saves PR metadata (number, author, url, sha) to artifacts + # - No secrets are used in this workflow + - cla-trigger.yml + + # cla.yml uses workflow_run which is the SAFE alternative to pull_request_target + # - Secrets are only accessed after the trigger workflow completes + # - PR context is passed via artifacts, not from untrusted event payload + # - No code from the PR is ever checked out or executed + - cla.yml + + # project.yml uses pull_request_target but is safe because: + # - It does NOT checkout any code from the PR + # - It only adds PRs to a GitHub project using actions/add-to-project + # - The only dynamic value used is github.event_name which cannot be spoofed + - project.yml From c037ba06713af1e910237f9fb6a992d5805f0667 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Tue, 6 Jan 2026 12:16:47 +0100 Subject: [PATCH 07/10] Move write permissions to job level in tests.yml Permissions for issues:write and pull-requests:write are now defined at the job level (template-files and analyze jobs) rather than workflow level, following least privilege principle. --- .github/workflows/tests.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 13b7fea..582ef10 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,8 +15,6 @@ on: permissions: contents: read - issues: write - pull-requests: write concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -95,6 +93,9 @@ jobs: template-files: runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write steps: - name: Checkout Source uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 @@ -147,6 +148,9 @@ jobs: needs: [pytest, read-file, template-files] if: '!cancelled()' runs-on: ubuntu-latest + permissions: + contents: read + issues: write steps: - name: Determine Success uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # v1.2.2 From 6cd1c664fc9039afc01cb6ce267263b2b56bedd7 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Tue, 6 Jan 2026 12:19:01 +0100 Subject: [PATCH 08/10] Fix remaining template injection findings Use environment variables instead of direct template expansion: - stale.yml: Pass stale action outputs via STALE_OUTPUTS env var - tests.yml: Pass JSON/YAML content via env vars to Python script --- .github/workflows/stale.yml | 4 +++- .github/workflows/tests.yml | 10 ++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 28c8576..902b1d6 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -95,4 +95,6 @@ jobs: exempt-assignees: mingwandroid - name: Print outputs - run: echo ${{ join(steps.stale.outputs.*, ',') }} + env: + STALE_OUTPUTS: ${{ join(steps.stale.outputs.*, ',') }} + run: echo "${STALE_OUTPUTS}" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 582ef10..7379a67 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -87,9 +87,15 @@ jobs: - name: Run Tests shell: python + env: + JSON_CONTENT: ${{ steps.json.outputs.content }} + YAML_CONTENT: ${{ steps.yaml.outputs.content }} + JSON_FOO: ${{ fromJSON(steps.json.outputs.content)['foo'] }} + YAML_FOO: ${{ fromJSON(steps.yaml.outputs.content)['foo'] }} run: | - assert '''${{ steps.json.outputs.content }}''' == '''${{ steps.yaml.outputs.content }}''' - assert '''${{ fromJSON(steps.json.outputs.content)['foo'] }}''' == '''${{ fromJSON(steps.yaml.outputs.content)['foo'] }}''' + import os + assert os.environ['JSON_CONTENT'] == os.environ['YAML_CONTENT'] + assert os.environ['JSON_FOO'] == os.environ['YAML_FOO'] template-files: runs-on: ubuntu-latest From ea2827ac1551b2dbd1da9ce40c0e4aad32fcbebd Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 6 Jan 2026 12:18:08 +0000 Subject: [PATCH 09/10] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .github/workflows/cla.yml | 3 +-- check-cla/action.yml | 6 +++--- zizmor.yml | 4 ++-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml index a10bf0c..735a22b 100644 --- a/.github/workflows/cla.yml +++ b/.github/workflows/cla.yml @@ -8,7 +8,7 @@ on: # Triggered after CLA Trigger workflow completes (safer than pull_request_target) workflow_run: - workflows: ["CLA Trigger"] + workflows: [CLA Trigger] types: - completed @@ -76,4 +76,3 @@ jobs: pr_author: ${{ steps.pr-context.outputs.author }} pr_url: ${{ steps.pr-context.outputs.url }} pr_sha: ${{ steps.pr-context.outputs.sha }} - diff --git a/check-cla/action.yml b/check-cla/action.yml index 2ff15fe..a39c249 100644 --- a/check-cla/action.yml +++ b/check-cla/action.yml @@ -77,12 +77,12 @@ runs: github-token: ${{ inputs.token }} script: | const { owner, repo } = context.repo; - + // Use provided inputs or fall back to context - const number = process.env.INPUT_PR_NUMBER + const number = process.env.INPUT_PR_NUMBER ? parseInt(process.env.INPUT_PR_NUMBER, 10) : context.issue.number; - + core.debug(`owner: ${owner}`); core.debug(`repo: ${repo}`); core.setOutput('number', number); diff --git a/zizmor.yml b/zizmor.yml index 2f7aefc..922d8f2 100644 --- a/zizmor.yml +++ b/zizmor.yml @@ -9,13 +9,13 @@ rules: # - It only saves PR metadata (number, author, url, sha) to artifacts # - No secrets are used in this workflow - cla-trigger.yml - + # cla.yml uses workflow_run which is the SAFE alternative to pull_request_target # - Secrets are only accessed after the trigger workflow completes # - PR context is passed via artifacts, not from untrusted event payload # - No code from the PR is ever checked out or executed - cla.yml - + # project.yml uses pull_request_target but is safe because: # - It does NOT checkout any code from the PR # - It only adds PRs to a GitHub project using actions/add-to-project From 208d3c8247a4a02f2255133c95cbb422c73d94bc Mon Sep 17 00:00:00 2001 From: Daniel Bast <2790401+dbast@users.noreply.github.com> Date: Fri, 16 Jan 2026 12:31:50 +0100 Subject: [PATCH 10/10] More hooks (#362) --- .github/workflows/cla.yml | 10 ++++++---- .github/workflows/stale.yml | 2 +- .github/workflows/update.yml | 6 +++--- .pre-commit-config.yaml | 8 ++++++++ 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml index 735a22b..d0c26ea 100644 --- a/.github/workflows/cla.yml +++ b/.github/workflows/cla.yml @@ -49,10 +49,12 @@ jobs: if: github.event_name == 'workflow_run' id: pr-context run: | - echo "number=$(cat pr-info/number)" >> $GITHUB_OUTPUT - echo "author=$(cat pr-info/author)" >> $GITHUB_OUTPUT - echo "url=$(cat pr-info/url)" >> $GITHUB_OUTPUT - echo "sha=$(cat pr-info/sha)" >> $GITHUB_OUTPUT + { + echo "number=$(cat pr-info/number)" + echo "author=$(cat pr-info/author)" + echo "url=$(cat pr-info/url)" + echo "sha=$(cat pr-info/sha)" + } >> "$GITHUB_OUTPUT" - name: Check CLA uses: conda/actions/check-cla@f05161c6e6e37a49b17c8e0b436197b53830318a # v25.9.2 diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 902b1d6..2e2067c 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -96,5 +96,5 @@ jobs: - name: Print outputs env: - STALE_OUTPUTS: ${{ join(steps.stale.outputs.*, ',') }} + STALE_OUTPUTS: ${{ toJSON(steps.stale.outputs) }} run: echo "${STALE_OUTPUTS}" diff --git a/.github/workflows/update.yml b/.github/workflows/update.yml index 9ec4df8..e0b68cb 100644 --- a/.github/workflows/update.yml +++ b/.github/workflows/update.yml @@ -46,8 +46,8 @@ jobs: - if: github.event.comment.body == '@conda-bot render' name: Configure git origin run: | - echo REPOSITORY=$(curl --silent "${PR_URL}" | jq --raw-output '.head.repo.full_name') >> $GITHUB_ENV - echo REF=$(curl --silent "${PR_URL}" | jq --raw-output '.head.ref') >> $GITHUB_ENV + echo "REPOSITORY=$(curl --silent "${PR_URL}" | jq --raw-output '.head.repo.full_name')" >> "$GITHUB_ENV" + echo "REF=$(curl --silent "${PR_URL}" | jq --raw-output '.head.ref')" >> "$GITHUB_ENV" env: PR_URL: ${{ github.event.issue.pull_request.url }} @@ -81,7 +81,7 @@ jobs: - if: github.event.comment.body != '@conda-bot render' name: Create fork # no-op if the repository is already forked - run: echo FORK=$(gh repo fork --clone=false --default-branch-only 2>&1 | awk '{print $1}') >> $GITHUB_ENV + run: echo "FORK=$(gh repo fork --clone=false --default-branch-only 2>&1 | awk '{print $1}')" >> "$GITHUB_ENV" env: GH_TOKEN: ${{ secrets.SYNC_TOKEN }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 135634b..0814355 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -45,6 +45,14 @@ repos: files: .*/action.(yml|yaml)$ - id: check-github-workflows - id: check-dependabot + - repo: https://github.com/zizmorcore/zizmor-pre-commit + rev: v1.20.0 + hooks: + - id: zizmor + - repo: https://github.com/rhysd/actionlint + rev: v1.7.9 + hooks: + - id: actionlint - repo: https://github.com/codespell-project/codespell # see setup.cfg rev: v2.4.1