Skip to content

fix(plugin): fail closed when Workflow skill context is unavailable #344

fix(plugin): fail closed when Workflow skill context is unavailable

fix(plugin): fail closed when Workflow skill context is unavailable #344

name: Public Workflow Policy

Check warning on line 1 in .github/workflows/public-workflow-policy.yml

View workflow run for this annotation

GitHub Actions / Public Workflow Policy

Workflow execution policy warning (evaluate mode)

On December 2, 2026, GitHub will restrict `pull_request_target` on public repositories by default. To continue allowing the event trigger, configure an Actions policy. Learn more: https://gh.io/securely-using-pull_request_target#default-policy-for-pull_request_target
# Bootstrap note: the PR that first introduces this workflow cannot be guarded
# by a workflow absent from its base branch. Once merged, pull_request_target
# always runs the trusted base copy and treats the candidate archive as data.
on:
pull_request_target:
branches: [main]
types: [opened, synchronize, reopened, ready_for_review, edited]
push:
branches: [main]
permissions:
contents: read
defaults:
run:
shell: bash
jobs:
policy:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Check out trusted base policy
id: trusted-checkout
continue-on-error: true
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ github.event_name == 'push' && github.event.before || github.event.pull_request.base.sha }}
path: trusted
persist-credentials: false
- name: Fetch candidate as inert data
id: candidate-fetch
env:
EVENT_NAME: ${{ github.event_name }}
BASE_REF: ${{ github.base_ref }}
EVENT_REF: ${{ github.ref }}
CANDIDATE_REPOSITORY: ${{ github.event_name == 'push' && github.repository || github.event.pull_request.head.repo.full_name }}
CANDIDATE_SHA: ${{ github.event_name == 'push' && github.sha || github.event.pull_request.head.sha }}
GIT_ASKPASS: /bin/false
GIT_CONFIG_GLOBAL: /dev/null
GIT_CONFIG_NOSYSTEM: 1
GIT_TERMINAL_PROMPT: 0
run: |
case "${EVENT_NAME}:${BASE_REF}:${EVENT_REF}" in
pull_request_target:main:refs/heads/main|push::refs/heads/main) ;;
*)
echo "public workflow policy accepts only push or pull_request_target for main" >&2
exit 1
;;
esac
if [[ ! "$CANDIDATE_REPOSITORY" =~ ^[A-Za-z0-9][A-Za-z0-9-]{0,38}/[A-Za-z0-9._-]{1,100}$ ]]; then
echo "candidate repository must be an exact GitHub owner/name" >&2
exit 1
fi
if [[ ! "$CANDIDATE_SHA" =~ ^[0-9a-f]{40}$ ]]; then
echo "candidate SHA must be exactly 40 lowercase hexadecimal characters" >&2
exit 1
fi
origin="https://github.com/${CANDIDATE_REPOSITORY}.git"
mkdir candidate-source candidate
git -C candidate-source init --quiet
git -C candidate-source \
-c credential.helper= \
-c core.askPass=/bin/false \
-c credential.interactive=never \
-c http.https://github.com/.extraheader= \
fetch --no-tags --depth=1 "$origin" "$CANDIDATE_SHA"
fetched_sha="$(git -C candidate-source rev-parse 'FETCH_HEAD^{commit}')"
if [[ "$fetched_sha" != "$CANDIDATE_SHA" ]]; then
echo "fetched candidate does not match the requested SHA" >&2
exit 1
fi
git -C candidate-source ls-tree -r --format='%(objectmode)' FETCH_HEAD > candidate-tree-modes
while read -r object_mode; do
case "$object_mode" in
100644|100755) ;;
*)
echo "candidate tree contains forbidden object mode: $object_mode" >&2
exit 1
;;
esac
done < candidate-tree-modes
git -C candidate-source archive --worktree-attributes --format=tar FETCH_HEAD > candidate.tar
if ! LC_ALL=C tar -tvf candidate.tar > candidate.tar.list; then
echo "candidate archive listing failed" >&2
exit 1
fi
while read -r archive_entry; do
case "${archive_entry:0:1}" in
-|d) ;;
*)
echo "candidate archive contains a forbidden entry type" >&2
exit 1
;;
esac
done < candidate.tar.list
tar --extract --file=candidate.tar --directory=candidate --no-same-owner --no-same-permissions
symlink_path="$(find candidate -type l -print -quit)"
if [[ -n "$symlink_path" ]]; then
echo "candidate archive contains a forbidden symlink" >&2
exit 1
fi
- name: Select exact trusted policy root
id: policy-root
env:
BEFORE_SHA: ${{ github.event.before }}
EVENT_NAME: ${{ github.event_name }}
run: |
root=trusted
if [ ! -x trusted/scripts/check-public-workflow-policy.sh ]; then
# This one-time push executes newly merged main, never PR-head data.
if [ "$EVENT_NAME" = push ] && [ "$BEFORE_SHA" = 9c364dd4e6dad83808f8a87c1ba990d0132f0372 ]; then
root=candidate
else
echo "trusted workflow policy is missing or incomplete" >&2
exit 1
fi
fi
echo "root=$root" >> "$GITHUB_OUTPUT"
- name: Set up Go for the selected trusted policy
id: trusted-toolchain
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version: '1.27.2'
cache: false
- name: Scan candidate workflows with trusted base policy
id: trusted-policy
continue-on-error: true
run: |
if [ "${{ steps.policy-root.outputs.root }}" = candidate ]; then
cd candidate && ./scripts/check-public-workflow-policy.sh --scan-root "${GITHUB_WORKSPACE}/candidate" --bootstrap-authority
else
cd trusted && ./scripts/check-public-workflow-policy.sh --scan-root "${GITHUB_WORKSPACE}/candidate"
fi
- name: Fail a rejected one-time bootstrap
id: bootstrap-rejection
if: steps.policy-root.outputs.root == 'candidate' && steps.trusted-policy.outcome == 'failure'
run: |
echo "one-time public workflow policy bootstrap was rejected" >&2
exit 1
- name: Authorize exact staged policy adoption
id: adoption-guard
if: steps.policy-root.outputs.root == 'trusted' && steps.trusted-policy.outcome == 'failure'
run: |
authorization="$(
cd trusted
./.github/workflows/policytool/adoptionguard/run.sh \
--base "${GITHUB_WORKSPACE}/trusted" \
--candidate "${GITHUB_WORKSPACE}/candidate"
)"
if [ "$authorization" != "candidate_policy_authorized=true" ]; then
echo "trusted adoption guard did not authorize candidate policy execution" >&2
exit 1
fi
printf '%s\n' "$authorization" >> "$GITHUB_OUTPUT"
- name: Set up Go for the authorized candidate analyzer
id: candidate-toolchain
if: steps.adoption-guard.outputs.candidate_policy_authorized == 'true'
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version: '1.27.2'
cache: false
- name: Scan candidate workflows with authorized candidate policy
id: candidate-policy
if: steps.adoption-guard.outputs.candidate_policy_authorized == 'true'
run: |
cd candidate
./scripts/check-public-workflow-policy.sh --scan-root "${GITHUB_WORKSPACE}/candidate"