After PR #244 (96c35e7 — "Remove dependency on the runner's volume") replaced the shared PersistentVolumeClaim with exec-based file copying, workflows that use local/composite actions (e.g. .github/actions/...) fail with:
Error: Can't find 'action.yml', 'action.yaml' or 'Dockerfile' under '/home/runner/_work/<repo>/<repo>/.github/actions/<action-path>'.
Did you forget to run actions/checkout before running your local action?
This affects versions 0.8.0+. The same workflows work correctly on 0.7.0, which uses a shared volume between the runner and job pod.
Root cause
In runScriptStep, only _temp is synced back from the job pod to the runner host after each step. When actions/checkout runs inside the job pod, the repo (including .github/actions/) is written to /__w/<repo>/<repo> inside the pod only. The runner host filesystem never receives the checked-out files.
The runner resolves local actions by reading action.yml from its own filesystem before invoking the hook for that step. Since the workspace was never copied back, the runner can't find the action definition.
runContainerStep does not have this problem — it copies the full /__w back from the job pod at https://github.com/actions/runner-container-hooks/blob/main/packages/k8s/src/hooks/run-container-step.ts#L90 because container action steps run in a separate pod. runScriptStep was intentionally limited to _temp only to minimize exec API traffic, but this breaks local action resolution.
Reproduction
- Use runner-container-hooks v0.8.0+
- Create a workflow with a job container that:
- Runs actions/checkout
- Then runs a local action (e.g. uses: ./.github/actions/my-action)
- The local action step fails with the error above
Commenting out the hooks (falling back to v0.7.0 bundled with ghcr.io/actions/actions-runner:latest) resolves the issue.
Suggested fix
After each runScriptStep execution, copy the .github directory back from the job pod to the runner host. This is the minimum needed for local action resolution and stays consistent with PR #244's goal of minimizing exec API data transfer:
const githubWorkspace = process.env.GITHUB_WORKSPACE as string
const parts = githubWorkspace.split('/').slice(-2)
if (parts.length === 2) {
const repoPath = `/__w/${parts.join('/')}/.github`
try {
await execCpFromPod(
getJobPodName(),
repoPath,
`${workdir}/${parts.join('/')}`
)
} catch {
core.debug('No .github directory found in pod workspace, skipping sync')
}
}
This keeps the _temp-only approach for the bulk of the sync while adding a small targeted copy (~KB) for action resolution.
Additional finding: /github/workflow/event.json missing in pods without user mount volumes
The same PR (#244) introduced a second regression. In v0.7.0, /github/home and /github/workflow were set up as volume subPath mounts on every job container. PR #244 replaced those mounts with a shell script (prepareJobScript) that copies these directories into place — but gated that script behind a userMountVolumes check:
// prepare-job.ts
if (args.container?.userMountVolumes?.length) {
prepareScript = prepareJobScript(args.container.userMountVolumes || [])
}
If the workflow has no custom volume mounts, the script never runs and /github/workflow/event.json is never created. This causes actions that read the event payload (e.g. dorny/paths-filter, tj-actions/changed-files, or any action reading GITHUB_EVENT_PATH) to fail with:
GITHUB_EVENT_PATH /github/workflow/event.json does not exist
Fix: Always run prepareJobScript, and only conditionally run the mkdir -p for user mount directories.
After PR #244 (96c35e7 — "Remove dependency on the runner's volume") replaced the shared PersistentVolumeClaim with exec-based file copying, workflows that use local/composite actions (e.g. .github/actions/...) fail with:
This affects versions 0.8.0+. The same workflows work correctly on 0.7.0, which uses a shared volume between the runner and job pod.
Root cause
In runScriptStep, only _temp is synced back from the job pod to the runner host after each step. When actions/checkout runs inside the job pod, the repo (including .github/actions/) is written to
/__w/<repo>/<repo>inside the pod only. The runner host filesystem never receives the checked-out files.The runner resolves local actions by reading action.yml from its own filesystem before invoking the hook for that step. Since the workspace was never copied back, the runner can't find the action definition.
runContainerStep does not have this problem — it copies the full /__w back from the job pod at https://github.com/actions/runner-container-hooks/blob/main/packages/k8s/src/hooks/run-container-step.ts#L90 because container action steps run in a separate pod. runScriptStep was intentionally limited to _temp only to minimize exec API traffic, but this breaks local action resolution.
Reproduction
- Runs actions/checkout
- Then runs a local action (e.g. uses: ./.github/actions/my-action)
Commenting out the hooks (falling back to v0.7.0 bundled with ghcr.io/actions/actions-runner:latest) resolves the issue.
Suggested fix
After each runScriptStep execution, copy the .github directory back from the job pod to the runner host. This is the minimum needed for local action resolution and stays consistent with PR #244's goal of minimizing exec API data transfer:
This keeps the _temp-only approach for the bulk of the sync while adding a small targeted copy (~KB) for action resolution.
Additional finding: /github/workflow/event.json missing in pods without user mount volumes
The same PR (#244) introduced a second regression. In v0.7.0, /github/home and /github/workflow were set up as volume subPath mounts on every job container. PR #244 replaced those mounts with a shell script (prepareJobScript) that copies these directories into place — but gated that script behind a userMountVolumes check:
If the workflow has no custom volume mounts, the script never runs and /github/workflow/event.json is never created. This causes actions that read the event payload (e.g. dorny/paths-filter, tj-actions/changed-files, or any action reading GITHUB_EVENT_PATH) to fail with:
Fix: Always run prepareJobScript, and only conditionally run the mkdir -p for user mount directories.