From 3b2dd40f9a6c5ad4eb4e5533e8f5ff372e438c78 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 10 Jul 2025 11:01:51 -0400 Subject: [PATCH] Switch to using /github/home for storing credentials The GitHub Action currently puts generated credentials into $GITHUB_WORKSPACE (/github/workspace). Unfortunately this is also the working directory of the checkout, so it's too easy to accidentally bundle the generated credentials into Docker containers, binaries, or anything that uses `*` or `.` as a build context. In the past, we tried to move the exported credentials into RUNNER_TEMP or other directories, but it always introduced incompatibility with the various community workflows (Docker, self-hosted, etc.): - https://github.com/google-github-actions/setup-gcloud/pull/148 - https://github.com/google-github-actions/setup-gcloud/pull/149 - https://github.com/google-github-actions/setup-gcloud/pull/405 - https://github.com/google-github-actions/setup-gcloud/pull/412 While undocumented, it appears that `/github/home` is an understood path, AND that path is mounted into Docker containers. That means we can export credentials outside of the workspace and still have them available inside the Docker container without users taking manual actions. This comes at three major costs: 1. We have to write the file into two locations. This isn't ideal, but it's also not the end of the world. 2. We would be relying on an undocumented filepath which GitHub could change at any point in the future. Since this is not part of the publicly-documented API, GitHub is within their rights to change this without notice, potentially breaking everyone/everything. 3. Because of the previous point, there are no environment variables that export these paths. We have to dynamically compile them, and it's a bit messy. --- README.md | 29 ++---------------------- src/main.ts | 65 +++++++++++++++++++++-------------------------------- 2 files changed, 28 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index 8816eafb..65758eb6 100644 --- a/README.md +++ b/README.md @@ -26,19 +26,6 @@ support](https://cloud.google.com/support).** ## Prerequisites -- Run the `actions/checkout@v4` step _before_ this action. Omitting the - checkout step or putting it after `auth` will cause future steps to be - unable to authenticate. - -- To create binaries, containers, pull requests, or other releases, add the - following to your `.gitignore`, `.dockerignore` and similar files to prevent - accidentally committing credentials to your release artifact: - - ```text - # Ignore generated credentials from google-github-actions/auth - gha-creds-*.json - ``` - - This action runs using Node 20. Use a [runner version](https://github.com/actions/virtual-environments) that supports this version of Node or newer. @@ -237,20 +224,8 @@ regardless of the authentication mechanism. generate a credentials file which can be used for authentication via gcloud and Google Cloud SDKs in other steps in the workflow. The default is true. - The credentials file is exported into `$GITHUB_WORKSPACE`, which makes it - available to all future steps and filesystems (including Docker-based GitHub - Actions). The file is automatically removed at the end of the job via a post - action. In order to use exported credentials, you **must** add the - `actions/checkout` step before calling `auth`. This is due to how GitHub - Actions creates `$GITHUB_WORKSPACE`: - - ```yaml - jobs: - job_id: - steps: - - uses: 'actions/checkout@v4' # Must come first! - - uses: 'google-github-actions/auth@v2' - ``` + The credentials file is exported into the GitHub Actions temp directory, + outside of the current workspace. - `export_environment_variables`: (Optional) If true, the action will export common environment variables which are known to be consumed by popular diff --git a/src/main.ts b/src/main.ts index 54822ea8..b89abd6b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -12,7 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { join as pathjoin } from 'path'; +import { join as pathjoin } from 'node:path'; +import { mkdir } from 'node:fs/promises'; import { exportVariable, @@ -25,11 +26,10 @@ import { import { errorMessage, exactlyOneOf, - isEmptyDir, isPinnedToHead, - parseMultilineCSV, parseBoolean, parseDuration, + parseMultilineCSV, pinnedToHeadWarning, } from '@google-github-actions/actions-utils'; @@ -141,44 +141,31 @@ export async function run(logger: Logger) { if (createCredentialsFile) { logger.debug(`Creating credentials file`); - // Note: We explicitly and intentionally export to GITHUB_WORKSPACE - // instead of RUNNER_TEMP, because RUNNER_TEMP is not shared with - // Docker-based actions on the filesystem. Exporting to GITHUB_WORKSPACE - // ensures that the exported credentials are automatically available to - // Docker-based actions without user modification. - // - // This has the unintended side-effect of leaking credentials over time, - // because GITHUB_WORKSPACE is not automatically cleaned up on self-hosted - // runners. To mitigate this issue, this action defines a post step to - // remove any created credentials. - const githubWorkspace = process.env.GITHUB_WORKSPACE; - if (!githubWorkspace) { - throw new Error('$GITHUB_WORKSPACE is not set'); + // Get the runner's temporary directory. This is cleaned up between runs + // automatically, but also subsets of this directory are shared with + // Docker-based GitHub Actions. + const runnerTempDir = process.env.RUNNER_TEMP; + if (!runnerTempDir) { + throw new Error('$RUNNER_TEMP is not set'); } - // There have been a number of issues where users have not used the - // "actions/checkout" step before our action. Our action relies on the - // creation of that directory; worse, if a user puts "actions/checkout" - // after our action, it will delete the exported credential. This - // following code does a small check to see if there are any files in the - // directory. It emits a warning if there are no files, since there may be - // legitimate use cases for authenticating without checking out the - // repository. - const githubWorkspaceIsEmpty = await isEmptyDir(githubWorkspace); - if (githubWorkspaceIsEmpty) { - logger.info( - `⚠️ The "create_credentials_file" option is true, but the current ` + - `GitHub workspace is empty. Did you forget to use ` + - `"actions/checkout" before this step? If you do not intend to ` + - `share authentication with future steps in this job, set ` + - `"create_credentials_file" to false.`, - ); - } - - // Create credentials file. - const outputFile = generateCredentialsFilename(); - const outputPath = pathjoin(githubWorkspace, outputFile); - const credentialsPath = await client.createCredentialsFile(outputPath); + // This is an undocumented path that is shared with Docker containers as a + // volume and has path remapping. + // + // https://github.com/actions/runner/blob/0d24afa114c2ee4b6451e35f2ba2cb9b96955789/src/Runner.Worker/Handlers/ContainerActionHandler.cs#L193-L202 + const githubHomeDir = pathjoin(runnerTempDir, '_github_home'); + logger.debug(`Computed home directory: "${githubHomeDir}"`); + + // Create the directory. Unlike $GITHUB_WORKSPACE, this directory may not + // yet exist. + await mkdir(githubHomeDir, { recursive: true }); + logger.debug(`Created home directory: "${githubHomeDir}"`); + + // Generate an output file that is unique, but still coupled to the run + // and run attempt. + const credentialsPath = await client.createCredentialsFile( + pathjoin(githubHomeDir, generateCredentialsFilename()), + ); logger.info(`Created credentials file at "${credentialsPath}"`); // Output to be available to future steps.