Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions .github/actions/checkout-isolated/action.yml
Original file line number Diff line number Diff line change
@@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Semgrep identified a blocking 🔴 issue in your code:
GitHub Actions step uses a mutable tag or branch reference. Tags and branch names can be silently repointed by the action owner, enabling supply-chain attacks — as seen in the trivy-action and kics-github-action compromises. Pin the reference to a full 40-character commit SHA instead, e.g. uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608.

To resolve this comment:

🔧 No guidance has been designated for this issue. Fix according to your organization's approved methods.

💬 Ignore this finding

Reply with Semgrep commands to ignore this finding.

  • /fp <comment> for false positive
  • /ar <comment> for acceptable risk
  • /other <comment> for all other reasons

Alternatively, triage in Semgrep AppSec Platform to ignore the finding created by github-actions-mutable-action-tag.

You can view more details about this finding in the Semgrep AppSec Platform.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/ar actions/checkout is GitHub's own first-party action, and every one of the eight other checkout pins in this repo (ci.yml, codex-agent.yml, flaky-*.yml, flaky-filter, flaky-record-results, link-check.yml, spellcheck.yml) is the same @v6 version tag. SHA-pinning only this one would diverge from all of them and then drift, since the repo has no process for bumping SHA pins — the deliberate SHA pin here is openai/codex-action, a third party. The rule's threat model (the action owner silently repointing the tag) is GitHub itself, which the runner and the API already trust wholesale.

with:
ref: ${{ inputs.ref }}
fetch-depth: ${{ inputs.fetch-depth }}
persist-credentials: ${{ inputs.persist-credentials }}
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down