From a6d3c9d111d37c9b01490701e164f93bc6fa455f Mon Sep 17 00:00:00 2001 From: jonathan keinan Date: Sun, 16 Aug 2026 15:41:39 +0300 Subject: [PATCH] checkout-isolated: add a pre-checkout git-config isolation action MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hardening a checkout has to happen before that checkout runs, which rules out a local (`./`) action in the consuming repo: GitHub resolves those from the workspace, and the workspace is empty until something checks out — a consumer that tried it got "Can't find action.yml" on a fresh runner. Downloaded from here, the runner needs no workspace for it. The step itself is the block consumers already run inline ahead of a checkout on self-hosted runners, with the checkout's ref, depth and credential persistence as inputs. persist-credentials defaults off, unlike actions/checkout. Co-Authored-By: Claude Opus 5 (1M context) --- .github/actions/checkout-isolated/action.yml | 56 ++++++++++++++++++++ README.md | 1 + 2 files changed, 57 insertions(+) create mode 100644 .github/actions/checkout-isolated/action.yml diff --git a/.github/actions/checkout-isolated/action.yml b/.github/actions/checkout-isolated/action.yml new file mode 100644 index 0000000..79ca119 --- /dev/null +++ b/.github/actions/checkout-isolated/action.yml @@ -0,0 +1,56 @@ +name: 'Checkout with an isolated git config' +description: | + Neutralise a persistent self-hosted runner's leftover git state, then check the repo out. + + On a self-hosted runner both the workspace and the runner user's global git config + outlive the job that created them. A config key that maps an event or a path to an + executable (hooks, fsmonitor, attributesFile filters, templateDir, url.insteadOf, + credential.helper, sshCommand, ...) would be honoured by the checkout below, and a + reused `.git` carries the same state locally. This points git at a clean, + workflow-owned global config for every later step in the job, ignores system config, + and keeps only safe.directory so a non-runner-owned tree still checks out. + + Use it as a job's FIRST step — hardening a checkout is worth nothing after that + checkout has run. This is also why it cannot live in the consuming repo as a local + (`./`) action: GitHub resolves those from the workspace, which is empty until the + checkout this action performs. + +inputs: + ref: + description: "Ref to check out. Defaults to the ref the workflow runs from." + required: false + default: "" + fetch-depth: + description: "Commits to fetch. 0 for the full history." + required: false + default: "1" + persist-credentials: + description: > + Leave the token in .git/config as an extraheader. Defaults off, unlike + actions/checkout: a job that pushes should mint a scoped token and pass it + explicitly rather than inherit whatever checked the repo out. + required: false + default: "false" + +runs: + using: 'composite' + steps: + - name: Isolate git config + drop reused checkout (self-hosted safety) + shell: bash + run: | + set -euo pipefail + cfg="${RUNNER_TEMP}/ci-gitconfig" + : > "${cfg}" + GIT_CONFIG_GLOBAL="${cfg}" GIT_CONFIG_NOSYSTEM=1 git config --global --add safe.directory '*' + { + echo "GIT_CONFIG_GLOBAL=${cfg}" + echo "GIT_CONFIG_SYSTEM=/dev/null" + echo "GIT_CONFIG_NOSYSTEM=1" + } >> "$GITHUB_ENV" + rm -rf .git + + - uses: actions/checkout@v6 + with: + ref: ${{ inputs.ref }} + fetch-depth: ${{ inputs.fetch-depth }} + persist-credentials: ${{ inputs.persist-credentials }} diff --git a/README.md b/README.md index b6cd339..315bcce 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ plumbing. | Unit | Path | Purpose | |---|---|---| +| `checkout-isolated` composite action | [`.github/actions/checkout-isolated`](.github/actions/checkout-isolated/action.yml) | A job's **first** step: bypass a persistent self-hosted runner's leftover global/system git config and drop a reused `.git`, then check out. Has to be consumed from here — a local `./` copy is resolved from the workspace, which is empty before the checkout it performs. | | `slack-notify` composite action | [`.github/actions/slack-notify`](.github/actions/slack-notify/action.yml) | Post a payload to a Slack webhook — replaces every repo's hand-rolled "notify failure" step. Webhook is a caller secret. | | `pr-size-label` reusable workflow | [`.github/workflows/pr-size-label.yml`](.github/workflows/pr-size-label.yml) | Label a PR by diff size. Add a thin `on: pull_request` caller. | | `spellcheck` reusable workflow | [`.github/workflows/spellcheck.yml`](.github/workflows/spellcheck.yml) | codespell over a PR's changed files. The repo supplies its own `.codespell/` config. |