Compare commits

..

No commits in common. "main" and "v2.1.2" have entirely different histories.

27 changed files with 943 additions and 1102 deletions

15
.eslintrc.js Normal file
View file

@ -0,0 +1,15 @@
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
rules: {
'@typescript-eslint/no-explicit-any': 'off',
},
};

View file

@ -1,5 +0,0 @@
paths:
'**/*.yml':
ignore:
# https://github.com/rhysd/actionlint/issues/559
- 'invalid runner name "node24"'

View file

@ -1,3 +1,17 @@
# Copyright 2023 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
name: 'Draft release'
on:
@ -15,11 +29,10 @@ on:
jobs:
draft-release:
uses: 'google-github-actions/.github/.github/workflows/draft-release.yml@v3' # ratchet:exclude
permissions:
contents: 'read'
pull-requests: 'write'
name: 'Draft release'
uses: 'google-github-actions/.github/.github/workflows/draft-release.yml@v0'
with:
version_strategy: '${{ github.event.inputs.version_strategy }}'
# secrets must be explicitly passed to reusable workflows https://docs.github.com/en/enterprise-cloud@latest/actions/using-workflows/reusing-workflows#using-inputs-and-secrets-in-a-reusable-workflow
secrets:
ACTIONS_BOT_TOKEN: '${{ secrets.ACTIONS_BOT_TOKEN }}'

View file

@ -1,25 +0,0 @@
name: 'Publish immutable action version'
on:
workflow_dispatch:
release:
types:
- 'published'
jobs:
publish:
runs-on: 'ubuntu-latest'
permissions:
contents: 'read'
id-token: 'write'
packages: 'write'
steps:
- name: 'Checkout'
uses: 'actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683' # ratchet:actions/checkout@v4
- name: 'Publish'
id: 'publish'
uses: 'actions/publish-immutable-action@4bc8754ffc40f27910afb20287dbbbb675a4e978' # ratchet:actions/publish-immutable-action@v0.0.4
with:
github-token: '${{ secrets.GITHUB_TOKEN }}'

View file

@ -1,3 +1,17 @@
# Copyright 2023 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
name: 'Release'
on:
@ -8,10 +22,6 @@ on:
jobs:
release:
uses: 'google-github-actions/.github/.github/workflows/release.yml@v3' # ratchet:exclude
permissions:
attestations: 'write'
contents: 'write'
packages: 'write'
secrets:
ACTIONS_BOT_TOKEN: '${{ secrets.ACTIONS_BOT_TOKEN }}'
if: "startsWith(github.event.head_commit.message, 'Release: v')"
name: 'Release'
uses: 'google-github-actions/.github/.github/workflows/release.yml@v0'

View file

@ -29,10 +29,6 @@ concurrency:
group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}'
cancel-in-progress: true
permissions:
contents: 'read'
statuses: 'write'
defaults:
run:
shell: 'bash'
@ -43,15 +39,18 @@ jobs:
runs-on: 'ubuntu-latest'
steps:
- uses: 'actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683' # ratchet:actions/checkout@v4
- uses: 'actions/checkout@v4'
- uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
- uses: 'actions/setup-node@v4'
with:
node-version-file: 'package.json'
node-version: '20.x'
- name: 'npm build'
run: 'npm ci && npm run build'
- name: 'npm lint'
run: 'npm run lint'
- name: 'npm test'
run: 'npm run test'
@ -60,8 +59,7 @@ jobs:
# Direct Workload Identity Federation
#
direct_workload_identity_federation:
if: |-
${{ github.event_name == 'push' || github.repository == github.event.pull_request.head.repo.full_name }}
if: ${{ github.event_name == 'push' || github.repository == github.event.pull_request.head.repo.full_name }}
name: 'direct_workload_identity_federation'
runs-on: '${{ matrix.os }}'
strategy:
@ -76,11 +74,11 @@ jobs:
id-token: 'write'
steps:
- uses: 'actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683' # ratchet:actions/checkout@v4
- uses: 'actions/checkout@v4'
- uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
- uses: 'actions/setup-node@v4'
with:
node-version-file: 'package.json'
node-version: '20.x'
- name: 'npm build'
run: 'npm ci && npm run build'
@ -101,7 +99,7 @@ jobs:
--fail \
--header "Authorization: Bearer ${{ steps.auth-default.outputs.auth_token }}"
- uses: 'google-github-actions/setup-gcloud@main' # ratchet:exclude
- uses: 'google-github-actions/setup-gcloud@v2'
with:
version: '>= 363.0.0'
@ -114,8 +112,7 @@ jobs:
# Workload Identity Federation through a Service Account
#
workload_identity_federation_through_service_account:
if: |-
${{ github.event_name == 'push' || github.repository == github.event.pull_request.head.repo.full_name }}
if: ${{ github.event_name == 'push' || github.repository == github.event.pull_request.head.repo.full_name }}
name: 'workload_identity_federation_through_service_account'
runs-on: '${{ matrix.os }}'
strategy:
@ -130,11 +127,11 @@ jobs:
id-token: 'write'
steps:
- uses: 'actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683' # ratchet:actions/checkout@v4
- uses: 'actions/checkout@v4'
- uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
- uses: 'actions/setup-node@v4'
with:
node-version-file: 'package.json'
node-version: '20.x'
- name: 'npm build'
run: 'npm ci && npm run build'
@ -146,7 +143,7 @@ jobs:
workload_identity_provider: '${{ vars.WIF_PROVIDER_NAME }}'
service_account: '${{ vars.SERVICE_ACCOUNT_EMAIL }}'
- uses: 'google-github-actions/setup-gcloud@main' # ratchet:exclude
- uses: 'google-github-actions/setup-gcloud@v2'
with:
version: '>= 363.0.0'
@ -186,8 +183,7 @@ jobs:
# Service Account Key JSON
#
credentials_json:
if: |-
${{ github.event_name == 'push' || github.repository == github.event.pull_request.head.repo.full_name }}
if: ${{ github.event_name == 'push' || github.repository == github.event.pull_request.head.repo.full_name }}
name: 'credentials_json'
runs-on: '${{ matrix.os }}'
strategy:
@ -199,11 +195,11 @@ jobs:
- 'macos-latest'
steps:
- uses: 'actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683' # ratchet:actions/checkout@v4
- uses: 'actions/checkout@v4'
- uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
- uses: 'actions/setup-node@v4'
with:
node-version-file: 'package.json'
node-version: '20.x'
- name: 'npm build'
run: 'npm ci && npm run build'
@ -214,7 +210,7 @@ jobs:
with:
credentials_json: '${{ secrets.SERVICE_ACCOUNT_KEY_JSON }}'
- uses: 'google-github-actions/setup-gcloud@main' # ratchet:exclude
- uses: 'google-github-actions/setup-gcloud@v2'
with:
version: '>= 363.0.0'
@ -254,18 +250,17 @@ jobs:
# has permissions to read the file.
#
docker:
if: |-
${{ github.event_name == 'push' || github.repository == github.event.pull_request.head.repo.full_name }}
if: ${{ github.event_name == 'push' || github.repository == github.event.pull_request.head.repo.full_name }}
name: 'docker'
runs-on: 'ubuntu-latest'
strategy:
fail-fast: false
steps:
- uses: 'actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683' # ratchet:actions/checkout@v4
- uses: 'actions/checkout@v4'
- uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
- uses: 'actions/setup-node@v4'
with:
node-version-file: 'package.json'
node-version: '20.x'
- name: 'npm build'
run: 'npm ci && npm run build'
@ -276,7 +271,7 @@ jobs:
credentials_json: '${{ secrets.SERVICE_ACCOUNT_KEY_JSON }}'
- name: 'docker'
uses: 'docker://index.docker.io/library/alpine@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1' # ratchet:docker://alpine:3
uses: 'docker://alpine:3'
with:
entrypoint: '/bin/sh'
args: '-euc "test -n "${GOOGLE_APPLICATION_CREDENTIALS}" && test -r "${GOOGLE_APPLICATION_CREDENTIALS}"'

View file

@ -27,7 +27,7 @@ jobs:
runs-on: 'ubuntu-latest'
steps:
- uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea' # ratchet:actions/github-script@v7
- uses: 'actions/github-script@v6'
with:
script: |-
const msg =

View file

@ -14,10 +14,6 @@ Action to authenticate to Google Cloud:
1. [Workload Identity Federation through a Service Account](#indirect-wif)
1. [Service Account Key JSON](#sake)
> [!IMPORTANT]
> The `gsutil` command will **not** use the credentials exported by this GitHub
> Action. Customers should use `gcloud storage` instead.
**This is not an officially supported Google product, and it is not covered by a
Google Cloud support contract. To report bugs or request features in a Google
Cloud product, please contact [Google Cloud
@ -39,7 +35,10 @@ support](https://cloud.google.com/support).**
gha-creds-*.json
```
- This action runs using Node 24. Use a [runner
- To use the `bq` or `gsutil` tools, use the Google Cloud SDK version 390.0.0
or newer.
- This action runs using Node 20. Use a [runner
version](https://github.com/actions/virtual-environments) that supports this
version of Node or newer.
@ -49,9 +48,6 @@ support](https://cloud.google.com/support).**
```yaml
jobs:
job_id:
# Any runner supporting Node 20 or newer
runs-on: ubuntu-latest
# Add "id-token" with the intended permissions.
permissions:
contents: 'read'
@ -60,7 +56,7 @@ jobs:
steps:
- uses: 'actions/checkout@v4'
- uses: 'google-github-actions/auth@v3'
- uses: 'google-github-actions/auth@v2'
with:
project_id: 'my-project'
workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider'
@ -84,12 +80,6 @@ For more usage options, see the [examples](docs/EXAMPLES.md).
> SDK](https://github.com/firebase/firebase-admin-node/issues/1377). Use Service
> Account Key JSON authentication instead.
> [!WARNING]
>
> As of the time of this writing, the GitHub OIDC token expires in 5 minutes,
> which means any derived credentials also expire in 5 minutes.
The following inputs are for _authenticating_ to Google Cloud via Workload
Identity Federation.
@ -201,10 +191,6 @@ Cloud as an output for use in future steps in the workflow. These options only
apply to ID tokens generated by this action. By default, this action does not
generate any tokens.
> [!CAUTION]
>
> ID Tokens have a maximum lifetime of 10 minutes. This value cannot be changed.
- `service_account`: (Required) Email address or unique identifier of the
Google Cloud service account for which to generate the ID token. For
example:
@ -249,7 +235,7 @@ regardless of the authentication mechanism.
job_id:
steps:
- uses: 'actions/checkout@v4' # Must come first!
- uses: 'google-github-actions/auth@v3'
- uses: 'google-github-actions/auth@v2'
```
- `export_environment_variables`: (Optional) If true, the action will export
@ -283,22 +269,13 @@ regardless of the authentication mechanism.
https://cloud.google.com. Trusted Partner Cloud and Google Distributed
Hosted Cloud should set this to their universe address.
You can also override individual API endpoints by setting the environment
variable `GHA_ENDPOINT_OVERRIDE_<endpoint>` where endpoint is the API
endpoint to override. This only applies to the `auth` action and does not
persist to other steps. For example:
You can also override individual API endpoints by setting the environment variable `GHA_ENDPOINT_OVERRIDE_<endpoint>` where endpoint is the API endpoint to override. This only applies to the `auth` action and does not persist to other steps. For example:
```yaml
env:
GHA_ENDPOINT_OVERRIDE_oauth2: 'https://oauth2.myapi.endpoint/v1'
```
- `request_reason`: (Optional) An optional Reason Request [System
Parameter](https://cloud.google.com/apis/docs/system-parameters) for each
API call made by the GitHub Action. This will inject the
"X-Goog-Request-Reason" HTTP header, which will provide user-supplied
information in Google Cloud audit logs.
- `cleanup_credentials`: (Optional) If true, the action will remove any
created credentials from the filesystem upon completion. This only applies
if "create_credentials_file" is true. The default is true.
@ -322,6 +299,7 @@ regardless of the authentication mechanism.
"token_format" is "id_token".
<a id="setup"></a>
## Setup
@ -346,8 +324,8 @@ In this setup, the Workload Identity Pool has direct IAM permissions on Google
Cloud resources; there are no intermediate service accounts or keys. This is
preferred since it directly authenticates GitHub Actions to Google Cloud without
a proxy resource. However, not all Google Cloud resources support `principalSet`
identities, and the resulting token has a maximum lifetime of 10 minutes. Please
see the documentation for your Google Cloud service for more information.
identities. Please see the documentation for your Google Cloud service for more
information.
[![Authenticate to Google Cloud from GitHub Actions with Direct Workload Identity Federation](docs/google-github-actions-auth-direct-workload-identity-federation.svg)](docs/google-github-actions-auth-direct-workload-identity-federation.svg)
@ -434,7 +412,7 @@ These instructions use the [gcloud][gcloud] command-line tool.
Actions YAML:
```yaml
- uses: 'google-github-actions/auth@v3'
- uses: 'google-github-actions/auth@v2'
with:
project_id: 'my-project'
workload_identity_provider: '...' # "projects/123456789/locations/global/workloadIdentityPools/github/providers/my-repo"
@ -598,7 +576,7 @@ These instructions use the [gcloud][gcloud] command-line tool.
Actions YAML:
```yaml
- uses: 'google-github-actions/auth@v3'
- uses: 'google-github-actions/auth@v2'
with:
service_account: '...' # my-service-account@my-project.iam.gserviceaccount.com
workload_identity_provider: '...' # "projects/123456789/locations/global/workloadIdentityPools/github/providers/my-repo"
@ -631,7 +609,7 @@ as a secret.
> [!CAUTION]
>
> Google Cloud Service Account Key JSON files must be secured
> and treated like a password. Anyone with access to the JSON key can
> and treated like a password. Anyone with acess to the JSON key can
> authenticate to Google Cloud as the underlying Service Account. By default,
> these credentials never expire, which is why the former authentication options
> are much preferred.
@ -663,11 +641,11 @@ These instructions use the [gcloud][gcloud] command-line tool.
1. Upload the contents of this file as a [GitHub Actions
Secret](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions).
Use the name of the GitHub Actions secret as the `credentials_json` value in
Use the name of the GitHub Actios secret as the `credentials_json` value in
the GitHub Actions YAML:
```yaml
- uses: 'google-github-actions/auth@v3'
- uses: 'google-github-actions/auth@v2'
with:
credentials_json: '${{ secrets.GOOGLE_CREDENTIALS }}' # Replace with the name of your GitHub Actions secret
```

View file

@ -56,7 +56,7 @@ inputs:
description: |-
If true, the action will securely generate a credentials file which can be
used for authentication via gcloud and Google Cloud SDKs.
default: 'true'
default: true
required: false
export_environment_variables:
description: |-
@ -79,7 +79,7 @@ inputs:
If false, the action will not export any environment variables, meaning
future steps are unlikely to be automatically authenticated to Google
Cloud.
default: 'true'
default: true
required: false
token_format:
description: |-
@ -102,18 +102,12 @@ inputs:
Hosted Cloud should set this to their universe address.
required: false
default: 'googleapis.com'
request_reason:
description: |-
An optional Reason Request System Parameter for each API call made by the
GitHub Action. This will inject the "X-Goog-Request-Reason" HTTP header,
which will provide user-supplied information in Google Cloud audit logs.
required: false
cleanup_credentials:
description: |-
If true, the action will remove any created credentials from the
filesystem upon completion. This only applies if "create_credentials_file"
is true.
default: 'true'
default: true
required: false
# access token params
@ -138,6 +132,30 @@ inputs:
default: ''
required: false
# retries - TODO - remove in v3.0
retries:
description: |-
Number of times to retry a failed authentication attempt. This is useful
for automated pipelines that may execute before IAM permissions are fully
propogated.
deprecationMessage: |-
This field is no longer used and will be removed in a future release.
required: false
backoff:
description: |-
Delay time before trying another authentication attempt. This is
implemented using a fibonacci backoff method (e.g. 1-1-2-3-5). The default
value is 250 milliseconds.
deprecationMessage: |-
This field is no longer used and will be removed in a future release.
required: false
backoff_limit:
description: |-
Limits the retry backoff to the specified value.
deprecationMessage: |-
This field is no longer used and will be removed in a future release.
required: false
# id token params
id_token_audience:
description: |-
@ -151,7 +169,7 @@ inputs:
generated token. If true, the token will contain "email" and
"email_verified" claims. This is only valid when "token_format" is
"id_token".
default: 'false'
default: false
required: false
outputs:
@ -180,6 +198,6 @@ branding:
color: 'blue'
runs:
using: 'node24'
using: 'node20'
main: 'dist/main/index.js'
post: 'dist/post/index.js'

19
bin/runTests.sh Normal file
View file

@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -eEuo pipefail
#
# As of Node 20, the --test parameter does not support globbing, and it does not
# support variable Windows paths. We also cannot invoke the test runner
# directly, because while it has an API, there's no way to force it to transpile
# the Typescript into JavaScript before passing it to the runner.
#
# So we're left with this solution, which shells out to Node to list all files
# that end in *.test.ts (excluding node_modules/), and then execs out to that
# process. We have to exec so the stderr/stdout and exit code is appropriately
# fed to the caller.
#
FILES="$(node -e "process.stdout.write(require('node:fs').readdirSync('./', { recursive: true }).filter((e) => {return e.endsWith('.test.ts') && !e.startsWith('node_modules');}).sort().join(' '));")"
set -x
exec node --require ts-node/register --test-reporter spec --test ${FILES}

6
dist/main/index.js vendored

File diff suppressed because one or more lines are too long

6
dist/post/index.js vendored

File diff suppressed because one or more lines are too long

View file

@ -20,7 +20,7 @@ jobs:
id-token: 'write'
- id: 'auth'
uses: 'google-github-actions/auth@v3'
uses: 'google-github-actions/auth@v2'
with:
project_id: 'my-project'
workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider'
@ -45,7 +45,7 @@ jobs:
contents: 'read'
id-token: 'write'
- uses: 'google-github-actions/auth@v3'
- uses: 'google-github-actions/auth@v2'
with:
project_id: 'my-project'
workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider'
@ -56,7 +56,7 @@ jobs:
# the service account, specify the 'token_format' parameter and use the
# 'accesss_token' output.
#
# - uses: 'google-github-actions/auth@v3'
# - uses: 'google-github-actions/auth@v2'
# with:
# workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider'
# service_account: 'my-service-account@my-project.iam.gserviceaccount.com'
@ -79,7 +79,7 @@ jobs:
steps:
- uses: 'actions/checkout@v4'
- uses: 'google-github-actions/auth@v3'
- uses: 'google-github-actions/auth@v2'
with:
credentials_json: '${{ secrets.GOOGLE_CREDENTIALS }}'
```
@ -100,7 +100,7 @@ jobs:
- uses: 'actions/checkout@v4'
- id: 'auth'
uses: 'google-github-actions/auth@v3'
uses: 'google-github-actions/auth@v2'
with:
project_id: 'my-project'
workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider'
@ -136,7 +136,7 @@ jobs:
- uses: 'actions/checkout@v4'
- id: 'auth'
uses: 'google-github-actions/auth@v3'
uses: 'google-github-actions/auth@v2'
with:
token_format: 'access_token' # <--
workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider'
@ -173,7 +173,7 @@ jobs:
- uses: 'actions/checkout@v4'
- id: 'auth'
uses: 'google-github-actions/auth@v3'
uses: 'google-github-actions/auth@v2'
with:
token_format: 'id_token' # <--
workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider'
@ -187,69 +187,6 @@ jobs:
run: |-
curl https://myapp-uvehjacqzq.a.run.app \
--header "Authorization: Bearer ${{ steps.auth.outputs.id_token }}"
# Example of using ID token in Python code
- id: 'python-example'
run: |-
python -c "
import os
import requests
# ID token is available as environment variable
id_token = os.environ.get('GOOGLE_ID_TOKEN', '${{ steps.auth.outputs.id_token }}')
# Use the token to invoke a Cloud Run service
response = requests.get(
'https://myapp-uvehjacqzq.a.run.app',
headers={'Authorization': f'Bearer {id_token}'}
)
print(response.text)
"
```
### Using Default Credentials with Scopes in Python
When using Workload Identity Federation with Python libraries, you may need to
add scopes before refreshing credentials:
```yaml
jobs:
job_id:
permissions:
contents: 'read'
id-token: 'write'
steps:
- uses: 'actions/checkout@v4'
- id: 'auth'
uses: 'google-github-actions/auth@v3'
with:
workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider'
service_account: 'my-service-account@my-project.iam.gserviceaccount.com'
- id: 'python-auth'
run: |-
python -c "
from google.auth import default
from google.auth.transport.requests import Request
# Get default credentials
credentials, project = default()
# Add scopes before refreshing for impersonation
credentials = credentials.with_scopes(
['https://www.googleapis.com/auth/cloud-platform']
)
# Refresh to get the token
credentials.refresh(request=Request())
# Now you can use the credentials
print(f'Access token: {credentials.token}')
if hasattr(credentials, 'id_token'):
print(f'ID token: {credentials.id_token}')
"
```
[github-markdown-toc]: https://github.blog/changelog/2021-04-13-table-of-contents-support-in-markdown-files/

View file

@ -40,7 +40,7 @@ curl -sfL -H "Accept: application/json" "https://api.github.com/repos/${REPO}" |
These can be used in an Attribute Condition:
```cel
assertion.repository_owner_id == '1342004' && assertion.repository_id == '260064828'
assertion.repository_owner_id == 1342004 && assertion.repository_id == 260064828
```
[cybersquatting]: https://en.wikipedia.org/wiki/Cybersquatting

View file

@ -6,8 +6,10 @@
see exactly which step is failing. Ensure you are using the latest version
of the GitHub Action.
> **⚠️ WARNING!** Enabling debug logging increases the chances of a secret
> being accidentally logged. While GitHub Actions will scrub secrets,
> [!CAUTION]
>
> Enabling debug logging increases the chances of a secret
> being accidentially logged. While GitHub Actions will scrub secrets,
> please take extra caution when sharing these debug logs in publicly
> accessible places like GitHub issues.
>
@ -27,7 +29,7 @@
```yaml
steps:
- uses: 'actions/checkout@v4'
- uses: 'google-github-actions/auth@v3'
- uses: 'google-github-actions/auth@v2'
```
1. Ensure the value for `workload_identity_provider` is the full _Provider_
@ -44,7 +46,7 @@
```diff
- projects/my-project/locations/global/workloadIdentityPools/my-pool/providers/my-provider
+ projects/1234567890/locations/global/workloadIdentityPools/my-pool/providers/my-provider
+ projects/1234567890/locations/global/workloadIdentityPools/my-pool/providers/
```
1. Ensure that you have the correct `permissions:` for the job in your
@ -62,9 +64,11 @@
GitHub OIDC token. You cannot grant permissions on an attribute unless you
map that value from the incoming GitHub OIDC token.
> ** TIP!** Use the [GitHub Actions OIDC Debugger][oidc-debugger] to print
> the list of token claims and compare them to your Attribute Mappings and
> Attribute Conditions.
> [!TIP]
>
> Use the [GitHub Actions OIDC Debugger][oidc-debugger] to print the list of
> token claims and compare them to your Attribute Mappings and Attribute
> Conditions.
1. Ensure you have the correct character casing and capitalization. GitHub does
not distinguish between "foobar" and "FooBar", but Google Cloud does. Ensure
@ -77,7 +81,7 @@
token", it means admission into the Workload Identity Pool failed. Check
your [**Attribute Conditions**][attribute-conditions].
- If the error message includes "Failed to generate OAuth 2.0 Access
- If the error message inclues "Failed to generate OAuth 2.0 Access
Token", it means Service Account Impersonation failed. Check your
[**Service Account Impersonation**][sa-impersonation] settings and
ensure the principalSet is correct.
@ -85,8 +89,10 @@
1. Enable `Admin Read`, `Data Read`, and `Data Write` [Audit Logging][cal] for
Identity and Access Management (IAM) in your Google Cloud project.
> **⚠️ WARNING!** This will increase log volume which may increase costs.
> You can disable this audit logging after you have debugged the issue.
> [!WARNING]
>
> This will increase log volume which may increase costs. You can disable
> this audit logging after you have debugged the issue.
Try to authenticate again, and then explore the logs for your Workload
Identity Provider and Workload Identity Pool. Sometimes these error messages
@ -230,56 +236,11 @@ tool like `jq`:
cat credentials.json | jq -r tostring
```
<a name="cannot-refresh"></a>
## Cannot refresh credentials to retrieve an ID token
If you get an error like:
```text
google.auth.exceptions.RefreshError: ('Unable to acquire impersonated credentials', '{"error": {"code": 400, "message": "Request contains an invalid argument.", "status": "INVALID_ARGUMENT"}}')
```
when trying to refresh credentials in Python code to get an ID token, this is
usually because the credentials are missing required scopes. The Google Auth
library requires scopes to be set when refreshing credentials for impersonation.
To fix this issue, add the required scopes before refreshing:
```python
from google.auth import default
from google.auth.transport.requests import Request
credentials, project = default()
# Add scopes before refreshing
credentials = credentials.with_scopes(
["https://www.googleapis.com/auth/cloud-platform"]
)
credentials.refresh(request=Request())
# Now you can access the ID token
print(credentials.id_token)
```
Alternatively, you can use the `token_format` parameter of this action to
generate an ID token directly:
```yaml
- uses: 'google-github-actions/auth@v3'
with:
workload_identity_provider: ${{ secrets.WIF_PROVIDER }}
service_account: ${{ secrets.WIF_SERVICE_ACCOUNT }}
token_format: 'id_token'
id_token_audience: 'https://example.com'
```
This will export the ID token as an environment variable that you can use in
your Python code.
## Organizational Policy Constraints
> ** NOTE!** Your Google Cloud organization administrator controls these
> [!NOTE]
>
> Your Google Cloud organization administrator controls these
> policies. You must work with your internal IT department to resolve OrgPolicy
> violations and constraints.

View file

@ -1,18 +0,0 @@
import js from '@eslint/js';
import ts from 'typescript-eslint';
import tsParser from '@typescript-eslint/parser';
import prettierRecommended from 'eslint-plugin-prettier/recommended';
export default ts.config(
js.configs.recommended,
ts.configs.eslintRecommended,
{
files: ['**/*.ts', '**/*.tsx'],
languageOptions: {
parser: tsParser,
},
},
{ ignores: ['dist/', '**/*.js'] },
prettierRecommended,
);

1436
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,17 +1,13 @@
{
"name": "@google-github-actions/auth",
"version": "3.0.0",
"version": "2.1.2",
"description": "Authenticate to Google Cloud using OIDC tokens or JSON service account keys.",
"main": "dist/main/index.js",
"scripts": {
"build": "ncc build -m src/main.ts -o dist/main && ncc build -m src/post.ts -o dist/post",
"lint": "eslint .",
"format": "eslint . --fix",
"test": "node --require ts-node/register --test-reporter spec --test tests/**/*.test.ts"
},
"engines": {
"node": ">= 24.x",
"npm": ">= 11.x"
"lint": "eslint . --ext .ts,.tsx",
"format": "prettier --write **/*.ts",
"test": "bash ./bin/runTests.sh"
},
"repository": {
"type": "git",
@ -27,22 +23,20 @@
"author": "GoogleCloudPlatform",
"license": "Apache-2.0",
"dependencies": {
"@actions/core": "^1.11.1",
"@actions/http-client": "^2.2.3",
"@google-github-actions/actions-utils": "^1.0.1"
"@actions/core": "^1.10.1",
"@actions/http-client": "^2.2.0",
"@google-github-actions/actions-utils": "^0.7.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.34.0",
"@types/node": "^24.3.0",
"@vercel/ncc": "^0.38.3",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.4",
"eslint": "^9.34.0",
"prettier": "^3.6.2",
"@types/node": "^20.11.5",
"@typescript-eslint/eslint-plugin": "^6.19.1",
"@typescript-eslint/parser": "^6.19.1",
"@vercel/ncc": "^0.38.1",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"prettier": "^3.2.4",
"ts-node": "^10.9.2",
"typescript-eslint": "^8.41.0",
"@typescript-eslint/eslint-plugin": "^8.41.0",
"typescript": "^5.9.2"
"typescript": "^5.3.3"
}
}

View file

@ -14,8 +14,6 @@
import { HttpClient } from '@actions/http-client';
import { expandUniverseEndpoints } from '@google-github-actions/actions-utils';
import { Logger } from '../logger';
import { userAgent } from '../utils';
@ -34,24 +32,23 @@ export interface AuthClient {
* other Google Cloud tools) that instructs the tool how to perform identity
* federation.
*/
createCredentialsFile(outputPath: string): Promise<string>; // eslint-disable-line no-unused-vars
createCredentialsFile(outputPath: string): Promise<string>;
/**
* signJWT signs a JWT using the auth provider.
*/
signJWT(claims: any): Promise<string>; // eslint-disable-line no-unused-vars
signJWT(claims: any): Promise<string>;
}
export interface ClientParameters {
logger: Logger;
universe: string;
requestReason?: string;
child: string;
}
export abstract class Client {
export class Client {
protected readonly _logger: Logger;
protected readonly _httpClient: HttpClient;
private readonly _requestReason: string | undefined;
protected readonly _endpoints = {
iam: 'https://iam.{universe}/v1',
@ -60,9 +57,10 @@ export abstract class Client {
sts: 'https://sts.{universe}/v1',
www: 'https://www.{universe}',
};
protected readonly _universe;
constructor(child: string, opts: ClientParameters) {
this._logger = opts.logger.withNamespace(child);
constructor(opts: ClientParameters) {
this._logger = opts.logger.withNamespace(opts.child);
// Create the http client with our user agent.
this._httpClient = new HttpClient(userAgent, undefined, {
@ -73,19 +71,26 @@ export abstract class Client {
maxRetries: 3,
});
this._endpoints = expandUniverseEndpoints(this._endpoints, opts.universe);
this._requestReason = opts.requestReason;
// Expand universe to support TPC and custom endpoints.
this._universe = opts.universe;
for (const key of Object.keys(this._endpoints) as Array<keyof typeof this._endpoints>) {
this._endpoints[key] = this.expandEndpoint(key);
}
this._logger.debug(`Computed endpoints for universe ${this._universe}`, this._endpoints);
}
/**
* _headers returns any added headers to apply to HTTP API calls.
*/
protected _headers(): Record<string, string> {
const headers: Record<string, string> = {};
if (this._requestReason) {
headers['X-Goog-Request-Reason'] = this._requestReason;
expandEndpoint(key: keyof typeof this._endpoints): string {
const envOverrideKey = `GHA_ENDPOINT_OVERRIDE_${key}`;
const envOverrideValue = process.env[envOverrideKey];
if (envOverrideValue && envOverrideValue !== '') {
this._logger.debug(
`Overriding API endpoint for ${key} because ${envOverrideKey} is set`,
envOverrideValue,
);
return envOverrideValue.replace(/\/+$/, '');
}
return headers;
return (this._endpoints[key] || '').replace(/{universe}/g, this._universe).replace(/\/+$/, '');
}
}
export { IAMCredentialsClient, IAMCredentialsClientParameters } from './iamcredentials';

View file

@ -16,7 +16,8 @@ import { URLSearchParams } from 'url';
import { errorMessage } from '@google-github-actions/actions-utils';
import { Client, ClientParameters } from './client';
import { Client } from './client';
import { Logger } from '../logger';
/**
* GenerateAccessTokenParameters are the inputs to the generateAccessToken call.
@ -41,7 +42,10 @@ export interface GenerateIDTokenParameters {
/**
* IAMCredentialsClientParameters are the inputs to the IAM client.
*/
export interface IAMCredentialsClientParameters extends ClientParameters {
export interface IAMCredentialsClientParameters {
readonly logger: Logger;
readonly universe: string;
readonly authToken: string;
}
@ -53,7 +57,11 @@ export class IAMCredentialsClient extends Client {
readonly #authToken: string;
constructor(opts: IAMCredentialsClientParameters) {
super('IAMCredentialsClient', opts);
super({
logger: opts.logger,
universe: opts.universe,
child: `IAMCredentialsClient`,
});
this.#authToken = opts.authToken;
}
@ -72,9 +80,7 @@ export class IAMCredentialsClient extends Client {
const pth = `${this._endpoints.iamcredentials}/projects/-/serviceAccounts/${serviceAccount}:generateAccessToken`;
const headers = Object.assign(this._headers(), {
Authorization: `Bearer ${this.#authToken}`,
});
const headers = { Authorization: `Bearer ${this.#authToken}` };
const body: Record<string, string | Array<string>> = {};
if (delegates && delegates.length > 0) {
@ -120,10 +126,10 @@ export class IAMCredentialsClient extends Client {
const pth = `${this._endpoints.oauth2}/token`;
const headers = Object.assign(this._headers(), {
const headers = {
'Accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded',
});
};
const body = new URLSearchParams();
body.append('grant_type', 'urn:ietf:params:oauth:grant-type:jwt-bearer');
@ -167,9 +173,7 @@ export class IAMCredentialsClient extends Client {
const pth = `${this._endpoints.iamcredentials}/projects/-/serviceAccounts/${serviceAccount}:generateIdToken`;
const headers = Object.assign(this._headers(), {
Authorization: `Bearer ${this.#authToken}`,
});
const headers = { Authorization: `Bearer ${this.#authToken}` };
const body: Record<string, string | string[] | boolean> = {
audience: audience,

View file

@ -23,13 +23,17 @@ import {
writeSecureFile,
} from '@google-github-actions/actions-utils';
import { AuthClient, Client, ClientParameters } from './client';
import { AuthClient, Client } from './client';
import { Logger } from '../logger';
/**
* ServiceAccountKeyClientParameters is used as input to the
* ServiceAccountKeyClient.
*/
export interface ServiceAccountKeyClientParameters extends ClientParameters {
export interface ServiceAccountKeyClientParameters {
readonly logger: Logger;
readonly universe: string;
readonly serviceAccountKey: string;
}
@ -42,7 +46,11 @@ export class ServiceAccountKeyClient extends Client implements AuthClient {
readonly #audience: string;
constructor(opts: ServiceAccountKeyClientParameters) {
super('ServiceAccountKeyClient', opts);
super({
logger: opts.logger,
universe: opts.universe,
child: `ServiceAccountKeyClient`,
});
const serviceAccountKey = parseCredential(opts.serviceAccountKey);
if (!isServiceAccountKey(serviceAccountKey)) {

View file

@ -14,13 +14,17 @@
import { errorMessage, writeSecureFile } from '@google-github-actions/actions-utils';
import { AuthClient, Client, ClientParameters } from './client';
import { AuthClient, Client } from './client';
import { Logger } from '../logger';
/**
* WorkloadIdentityFederationClientParameters is used as input to the
* WorkloadIdentityFederationClient.
*/
export interface WorkloadIdentityFederationClientParameters extends ClientParameters {
export interface WorkloadIdentityFederationClientParameters {
readonly logger: Logger;
readonly universe: string;
readonly githubOIDCToken: string;
readonly githubOIDCTokenRequestURL: string;
readonly githubOIDCTokenRequestToken: string;
@ -47,7 +51,11 @@ export class WorkloadIdentityFederationClient extends Client implements AuthClie
#cachedAt?: number;
constructor(opts: WorkloadIdentityFederationClientParameters) {
super('WorkloadIdentityFederationClient', opts);
super({
logger: opts.logger,
universe: opts.universe,
child: `WorkloadIdentityFederationClient`,
});
this.#githubOIDCToken = opts.githubOIDCToken;
this.#githubOIDCTokenRequestURL = opts.githubOIDCTokenRequestURL;
@ -82,8 +90,6 @@ export class WorkloadIdentityFederationClient extends Client implements AuthClie
const pth = `${this._endpoints.sts}/token`;
const headers = Object.assign(this._headers(), {});
const body = {
audience: this.#audience,
grantType: `urn:ietf:params:oauth:grant-type:token-exchange`,
@ -96,12 +102,11 @@ export class WorkloadIdentityFederationClient extends Client implements AuthClie
logger.debug(`Built request`, {
method: `POST`,
path: pth,
headers: headers,
body: body,
});
try {
const resp = await this._httpClient.postJson<{ access_token: string }>(pth, body, headers);
const resp = await this._httpClient.postJson<{ access_token: string }>(pth, body);
const statusCode = resp.statusCode || 500;
if (statusCode < 200 || statusCode > 299) {
throw new Error(`Failed to call ${pth}: HTTP ${statusCode}: ${resp.result || '[no body]'}`);
@ -135,9 +140,9 @@ export class WorkloadIdentityFederationClient extends Client implements AuthClie
const pth = `${this._endpoints.iamcredentials}/projects/-/serviceAccounts/${this.#serviceAccount}:signJwt`;
const headers = Object.assign(this._headers(), {
const headers = {
Authorization: `Bearer ${await this.getToken()}`,
});
};
const body = {
payload: claims,

View file

@ -25,7 +25,6 @@ import {
* LoggerFunction is the type signature of a log function for the GitHub Actions
* SDK.
*/
// eslint-disable-next-line no-unused-vars
type LoggerFunction = (message: string, properties?: AnnotationProperties) => void;
/**

View file

@ -16,6 +16,7 @@ import { join as pathjoin } from 'path';
import {
exportVariable,
getBooleanInput,
getIDToken,
getInput,
setFailed,
@ -28,10 +29,8 @@ import {
isEmptyDir,
isPinnedToHead,
parseMultilineCSV,
parseBoolean,
parseDuration,
pinnedToHeadWarning,
withRetries,
} from '@google-github-actions/actions-utils';
import {
@ -80,12 +79,11 @@ export async function run(logger: Logger) {
const oidcTokenAudience =
getInput(`audience`) || `https://iam.googleapis.com/${workloadIdentityProvider}`;
const credentialsJSON = getInput(`credentials_json`);
const createCredentialsFile = parseBoolean(getInput(`create_credentials_file`));
const exportEnvironmentVariables = parseBoolean(getInput(`export_environment_variables`));
const createCredentialsFile = getBooleanInput(`create_credentials_file`);
const exportEnvironmentVariables = getBooleanInput(`export_environment_variables`);
const tokenFormat = getInput(`token_format`);
const delegates = parseMultilineCSV(getInput(`delegates`));
const universe = getInput(`universe`);
const requestReason = getInput(`request_reason`);
// Ensure exactly one of workload_identity_provider and credentials_json was
// provided.
@ -111,16 +109,10 @@ export async function run(logger: Logger) {
throw new Error(oidcWarning);
}
const oidcToken = await withRetries(
async (): Promise<string> => {
return await getIDToken(oidcTokenAudience);
},
{ retries: 3 },
)();
const oidcToken = await getIDToken(oidcTokenAudience);
client = new WorkloadIdentityFederationClient({
logger: logger,
universe: universe,
requestReason: requestReason,
githubOIDCToken: oidcToken,
githubOIDCTokenRequestURL: oidcTokenRequestURL,
@ -134,7 +126,6 @@ export async function run(logger: Logger) {
client = new ServiceAccountKeyClient({
logger: logger,
universe: universe,
requestReason: requestReason,
serviceAccountKey: credentialsJSON,
});
@ -208,7 +199,7 @@ export async function run(logger: Logger) {
// Set the project ID environment variables to the computed values.
if (!projectID) {
logger.info(
`⚠️ Failed to compute a project ID from the given inputs. Neither the ` +
`⚠️ Failed to a project ID from the given inputs. Neither the ` +
`"project_id" output nor any environment variables will be ` +
`exported. If you require these values in other steps, specify the ` +
`"project_id" input directly.`,
@ -307,7 +298,7 @@ export async function run(logger: Logger) {
logger.debug(`Creating id token`);
const idTokenAudience = getInput('id_token_audience', { required: true });
const idTokenIncludeEmail = parseBoolean(getInput('id_token_include_email'));
const idTokenIncludeEmail = getBooleanInput('id_token_include_email');
// Ensure a service_account was provided if using WIF.
if (!serviceAccount) {

View file

@ -12,21 +12,21 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { getInput, setFailed } from '@actions/core';
import { getBooleanInput, setFailed } from '@actions/core';
import { errorMessage, forceRemove, parseBoolean } from '@google-github-actions/actions-utils';
import { errorMessage, forceRemove } from '@google-github-actions/actions-utils';
import { Logger } from './logger';
export async function run(logger: Logger) {
try {
const createCredentials = parseBoolean(getInput('create_credentials_file'));
const createCredentials = getBooleanInput('create_credentials_file');
if (!createCredentials) {
logger.info(`Skipping credential cleanup - "create_credentials_file" is false.`);
return;
}
const cleanupCredentials = parseBoolean(getInput('cleanup_credentials'));
const cleanupCredentials = getBooleanInput('cleanup_credentials');
if (!cleanupCredentials) {
logger.info(`Skipping credential cleanup - "cleanup_credentials" is false.`);
return;
@ -34,7 +34,7 @@ export async function run(logger: Logger) {
// Look up the credentials path, if one exists. Note that we only check the
// environment variable set by our action, since we don't want to
// accidentally clean up if someone set GOOGLE_APPLICATION_CREDENTIALS or
// accidentially clean up if someone set GOOGLE_APPLICATION_CREDENTIALS or
// another environment variable manually.
const credentialsPath = process.env['GOOGLE_GHA_CREDS_PATH'];
if (!credentialsPath) {

View file

@ -19,6 +19,7 @@ import {
} from '@google-github-actions/actions-utils';
// Do not listen to the linter - this can NOT be rewritten as an ES6 import statement.
// eslint-disable-next-line @typescript-eslint/no-var-requires
export const { version: appVersion } = require('../package.json');
// userAgent is the default user agent.

View file

@ -1,9 +1,11 @@
{
"compilerOptions": {
"alwaysStrict": true,
"target": "es2022",
"target": "es6",
"module": "commonjs",
"lib": ["es2022"],
"lib": [
"es6"
],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,