From 55f22b8824ab27ca46e63b6fc92935202e42d035 Mon Sep 17 00:00:00 2001 From: Lukas Audzevicius Date: Mon, 26 Oct 2026 15:23:44 +0000 Subject: [PATCH 1/2] Add manual fork-PR CI dispatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fork PRs don't get secrets (e.g. PUPPET_FORGE_TOKEN, CODECOV_TOKEN) via the normal pull_request trigger, so CI silently skips secret-dependent steps for external contributors. Add pr_number/ref inputs to the existing workflow_dispatch trigger and a resolve-ref job that resolves either into a ref, threaded into both the spec job (gem_ci.yml@main, which gained a ref input in puppetlabs/cat-github-actions#191) and the acceptance job (the repo-local custom_acceptance.yml, which needed the same ref input added since it's not one of the shared cat-github-actions workflows). workflow_dispatch is already gated to users with repo write access, which is the trust boundary — no label/approval system is layered on top, intentionally lightweight for this low-traffic tool repo. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/ci.yml | 34 ++++++++++++++++++++++++- .github/workflows/custom_acceptance.yml | 7 +++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e815b4d9..649bf0f1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,12 +8,42 @@ on: branches: - "main" workflow_dispatch: + inputs: + pr_number: + description: "PR number to test (e.g. a fork PR, which gets no secrets via the normal pull_request trigger)" + required: false + type: string + ref: + description: "Branch or SHA to test (ignored if pr_number is set)" + required: false + type: string env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} jobs: + resolve-ref: + runs-on: ubuntu-latest + outputs: + ref: ${{ steps.resolve.outputs.ref }} + steps: + - id: resolve + env: + PR_NUMBER: ${{ inputs.pr_number }} + INPUT_REF: ${{ inputs.ref }} + run: | + if [ -n "$PR_NUMBER" ]; then + ref="refs/pull/${PR_NUMBER}/merge" + else + ref="$INPUT_REF" + fi + echo "ref=$ref" >> "$GITHUB_OUTPUT" + if [ -n "$ref" ]; then + echo "Testing ref: $ref" >> "$GITHUB_STEP_SUMMARY" + fi + spec: + needs: resolve-ref strategy: fail-fast: false matrix: @@ -32,9 +62,10 @@ jobs: rake_task: "spec:coverage" ruby_version: ${{ matrix.ruby_version }} puppet_gem_version: ${{ matrix.puppet_version }} + ref: ${{ needs.resolve-ref.outputs.ref }} acceptance: - needs: "spec" + needs: ["resolve-ref", "spec"] strategy: fail-fast: false matrix: @@ -55,3 +86,4 @@ jobs: ruby_version: ${{ matrix.ruby_version }} puppet_version: ${{ matrix.puppet_version }} runs_on: ${{ matrix.runs_on }} + ref: ${{ needs.resolve-ref.outputs.ref }} diff --git a/.github/workflows/custom_acceptance.yml b/.github/workflows/custom_acceptance.yml index e4d1d344..05d36512 100644 --- a/.github/workflows/custom_acceptance.yml +++ b/.github/workflows/custom_acceptance.yml @@ -30,6 +30,11 @@ on: required: false default: "ubuntu-latest" type: "string" + ref: + description: "Git ref to check out. Defaults to the ref that triggered the workflow run if unset." + required: false + default: "" + type: "string" env: PUPPET_FORGE_TOKEN: ${{ secrets.PUPPET_FORGE_TOKEN || secrets.PUPPET_FORGE_TOKEN_PUBLIC }} @@ -44,6 +49,8 @@ jobs: - name: "checkout" uses: "actions/checkout@v4" + with: + ref: ${{ inputs.ref }} - name: "export environment" shell: pwsh From 3f749172e0015ea6c56b67d5fb96272ca8bb8e33 Mon Sep 17 00:00:00 2001 From: Lukas Audzevicius Date: Mon, 26 Oct 2026 15:23:55 +0000 Subject: [PATCH 2/2] Pin resolve-ref to the PR's merge commit SHA MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refs/pull//merge is mutable: GitHub repoints it on every new push to the PR, and spec/acceptance check it out independently, with acceptance only starting after spec finishes. A fork author could push a malicious commit in that window and have acceptance silently execute it with PUPPET_FORGE_TOKEN in scope, instead of the commit a maintainer actually reviewed before dispatching. Resolve pr_number/ref to a concrete, immutable commit SHA once via the GitHub API instead — specifically the PR's merge commit (matching the module-track fork-PR precedent and the semantics of the normal pull_request trigger, which also tests the merge result rather than the raw head), failing explicitly if the PR isn't mergeable yet or has conflicts rather than silently testing the wrong commit. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/ci.yml | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 649bf0f1..f525f35a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,17 +29,28 @@ jobs: steps: - id: resolve env: + GH_TOKEN: ${{ github.token }} PR_NUMBER: ${{ inputs.pr_number }} INPUT_REF: ${{ inputs.ref }} run: | if [ -n "$PR_NUMBER" ]; then - ref="refs/pull/${PR_NUMBER}/merge" + IFS=$'\t' read -r mergeable sha <<< "$(gh api "repos/${{ github.repository }}/pulls/${PR_NUMBER}" --jq '[(.mergeable | tostring), (.merge_commit_sha // "")] | @tsv')" + if [ "$mergeable" != "true" ]; then + echo "::error::PR #${PR_NUMBER} is not mergeable right now (mergeable=${mergeable}). It may be closed, have conflicts with the base branch, or GitHub may not have finished computing mergeability yet — wait a few seconds and retry." >&2 + exit 1 + fi + if [ -z "$sha" ]; then + echo "::error::GitHub did not return a merge commit SHA for PR #${PR_NUMBER}." >&2 + exit 1 + fi + elif [ -n "$INPUT_REF" ]; then + sha="$(gh api "repos/${{ github.repository }}/commits/${INPUT_REF}" --jq '.sha')" else - ref="$INPUT_REF" + sha="" fi - echo "ref=$ref" >> "$GITHUB_OUTPUT" - if [ -n "$ref" ]; then - echo "Testing ref: $ref" >> "$GITHUB_STEP_SUMMARY" + echo "ref=$sha" >> "$GITHUB_OUTPUT" + if [ -n "$sha" ]; then + echo "Testing commit: $sha" >> "$GITHUB_STEP_SUMMARY" fi spec: