diff --git a/.changeset/setup-github-token-gati-v2.md b/.changeset/setup-github-token-gati-v2.md new file mode 100644 index 000000000..ec1ea701d --- /dev/null +++ b/.changeset/setup-github-token-gati-v2.md @@ -0,0 +1,8 @@ +--- +"setup-github-token": minor +--- + +Support GATI v2 behind a staged rollout. Adds a `profile` input that selects +GATI v2, a `gati-version` input to force either flow, and a `gati-version` +output. `aws-role-arn` and `aws-lambda-url` are now optional, and required only +when `profile` is not set. diff --git a/actions/setup-github-token/README.md b/actions/setup-github-token/README.md index b45ee56f1..e5b758963 100644 --- a/actions/setup-github-token/README.md +++ b/actions/setup-github-token/README.md @@ -1 +1,90 @@ # setup-github-token action + +Gets a GitHub installation access token from GATI, and optionally configures git +to authenticate with it. + +Requires `id-token: write` on the calling job — v1 uses it to assume the AWS +role, v2 to authenticate to GATI directly. + +## Which flow runs + +- **v1** ([`invoke-gati`](../invoke-gati/)) assumes `aws-role-arn`, then calls + the lambda at `aws-lambda-url`. +- **v2** ([`invoke-gati-v2`](../invoke-gati-v2/)) requests a `profile` over + GitHub OIDC, with no AWS credentials. + +Passing `profile` selects v2. Otherwise a staged rollout decides: the repository +name is hashed into a bucket of 0–99, and uses v2 when that bucket is below the +rollout percentage. Since the bucket never changes, a repository moves to v2 +once and stays there. The percentage and the two pin lists that override it are +the `ROLLOUT_PERCENTAGE`, `FORCE_V2_REPOSITORIES`, and `FORCE_V1_REPOSITORIES` +env values on the `Resolve GATI version` step in [`action.yml`](./action.yml); +changing them needs a release. `gati-version` overrides all of the above. + +## Migrated v1 roles + +GATI v1 role configuration was carried into v2 automatically, keyed by the +role's IAM ARN and addressable as the profile `v1/`. So v2 needs +no new configuration to take over from v1: given no `profile`, it derives one +from the `aws-role-arn` the caller already passes and returns a token with the +same permissions v1 would have. A caller only needs a real `profile` to get +scopes its v1 role does not already have. + +## Inputs + +Either `profile` or the v1 inputs must be provided. + +| Name | Description | Defaulted | +| ---------------- | --------------------------------------------------------- | -------------- | +| `set-git-config` | Configure git to use the token for `https://github.com/`. | ✅ - `"false"` | +| `gati-version` | Force `v1` or `v2`, bypassing the rollout. | | + +### v2 + +| Name | Description | +| --------- | ---------------------------------------------- | +| `profile` | Token profile to request. Selects v2 when set. | + +### v1 + +Also read by v2 to derive a profile, see +[Migrated v1 roles](#migrated-v1-roles). Ignored when `profile` is set. + +| Name | Description | Defaulted | +| --------------------------- | ----------------------------------------------- | -------------------------------- | +| `aws-role-arn` | ARN of the role that can get a token from GATI. | | +| `aws-lambda-url` | URL of the GATI lambda function. | | +| `aws-region` | AWS region. | | +| `aws-role-duration-seconds` | Duration of the assumed role, in seconds. | ✅ - `"900"` | +| `role-session-name` | Session name, truncated to 64 characters. | ✅ - run id, run number, and job | + +## Outputs + +| Name | Description | +| -------------- | --------------------------------------------------------------------------------------------------------------- | +| `access-token` | The token. A v2 token is revoked when its job ends, so request one per job rather than passing it between jobs. | +| `gati-version` | The GATI version that issued the token, `v1` or `v2`. | + +## Example + +```yaml +jobs: + example: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - name: Setup GitHub token + id: setup-github-token + uses: smartcontractkit/.github/actions/setup-github-token@setup-github-token/v1 + with: + aws-role-arn: ${{ secrets.AWS_OIDC_IAM_ROLE_ARN_GATI }} + aws-lambda-url: ${{ secrets.AWS_LAMBDA_URL_GATI }} + aws-region: us-west-2 + + - name: Use the token + env: + GH_TOKEN: ${{ steps.setup-github-token.outputs.access-token }} + run: gh repo view smartcontractkit/.github +``` diff --git a/actions/setup-github-token/action.yml b/actions/setup-github-token/action.yml index 269ad47c3..66a1a775b 100644 --- a/actions/setup-github-token/action.yml +++ b/actions/setup-github-token/action.yml @@ -2,15 +2,28 @@ name: Setup GitHub Token description: Setup a GitHub Token from GATI inputs: + profile: + description: + GATI v2 token profile to request. When set, GATI v2 is used and the aws-* + inputs are ignored. + required: false + gati-version: + description: + Force "v1" or "v2", bypassing the staged rollout. Leave unset to let the + rollout decide. + required: false aws-role-arn: - description: ARN of role capable of getting token from GATI - required: true + description: + ARN of role capable of getting token from GATI. Required unless profile is + set. + required: false aws-lambda-url: - description: URL of GATI lambda function - required: true + description: URL of GATI lambda function. Required unless profile is set. + required: false aws-region: - description: AWS region - required: true + description: AWS region. Required unless profile is set. + required: false + default: "us-west-2" aws-role-duration-seconds: description: Duration of role in seconds required: false @@ -26,15 +39,38 @@ inputs: outputs: access-token: - value: ${{ steps.get-gh-token.outputs.access-token }} + value: + ${{ steps.get-gh-token.outputs.access-token || + steps.get-gh-token-v2.outputs.access-token }} description: The github access token that has permissions reflecting the current AWS role value + gati-version: + value: ${{ steps.resolve-gati-version.outputs.version }} + description: The GATI version that issued the token, "v1" or "v2" runs: using: composite steps: + - name: Resolve GATI version + id: resolve-gati-version + shell: bash + env: + PROFILE: ${{ inputs.profile }} + GATI_VERSION: ${{ inputs.gati-version }} + AWS_ROLE_ARN: ${{ inputs.aws-role-arn }} + AWS_LAMBDA_URL: ${{ inputs.aws-lambda-url }} + # Rollout configuration. Repositories not named in either list are + # bucketed by the hash of their name, and use v2 when their bucket is + # below the percentage. + ROLLOUT_PERCENTAGE: "0" + FORCE_V2_REPOSITORIES: | + smartcontractkit/.github + FORCE_V1_REPOSITORIES: "" + run: ${{ github.action_path }}/scripts/resolve-gati-version.sh + - name: Check the role session name lengths and truncate if needed + if: steps.resolve-gati-version.outputs.version == 'v1' shell: bash id: role-session-name env: @@ -47,6 +83,7 @@ runs: fi - name: Assume role capable of getting token from gati + if: steps.resolve-gati-version.outputs.version == 'v1' uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.0 with: aws-region: ${{ inputs.aws-region }} @@ -56,16 +93,28 @@ runs: ${{ steps.role-session-name.outputs.role-session-name }} role-to-assume: ${{ inputs.aws-role-arn }} - - name: Get github token from gati + - name: Get github token from gati (v1) id: get-gh-token + if: steps.resolve-gati-version.outputs.version == 'v1' uses: smartcontractkit/.github/actions/invoke-gati@invoke-gati/0.3.0 with: url: ${{ inputs.aws-lambda-url }} + - name: Get github token from gati (v2) + id: get-gh-token-v2 + if: steps.resolve-gati-version.outputs.version == 'v2' + uses: smartcontractkit/.github/actions/invoke-gati-v2@invoke-gati-v2/0.1.0 + with: + profile: ${{ steps.resolve-gati-version.outputs.profile }} + - name: Configure github token if: inputs.set-git-config == 'true' shell: bash + env: + ACCESS_TOKEN: + ${{ steps.get-gh-token.outputs.access-token || + steps.get-gh-token-v2.outputs.access-token }} run: | git config --global \ - url."https://x-access-token:${{ steps.get-gh-token.outputs.access-token }}@github.com/".insteadOf \ + url."https://x-access-token:${ACCESS_TOKEN}@github.com/".insteadOf \ "https://github.com/" diff --git a/actions/setup-github-token/scripts/resolve-gati-version.sh b/actions/setup-github-token/scripts/resolve-gati-version.sh new file mode 100755 index 000000000..e9961f245 --- /dev/null +++ b/actions/setup-github-token/scripts/resolve-gati-version.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash + +# Selects between GATI v1 (assume an AWS role, then call the v1 lambda) and GATI +# v2 (GitHub OIDC, no AWS credentials), then emits the version and, for v2, the +# profile to request. + +set -euo pipefail + +PROFILE="${PROFILE:-}" +GATI_VERSION="${GATI_VERSION:-}" +AWS_ROLE_ARN="${AWS_ROLE_ARN:-}" +AWS_LAMBDA_URL="${AWS_LAMBDA_URL:-}" + +VERSION="" +REASON="" + +resolve_version() { + if [[ -n "${GATI_VERSION}" ]]; then + if [[ "${GATI_VERSION}" != "v1" && "${GATI_VERSION}" != "v2" ]]; then + echo "::error::gati-version must be 'v1' or 'v2', got '${GATI_VERSION}'." + exit 1 + fi + VERSION="${GATI_VERSION}" + REASON="gati-version input" + return + fi + + if [[ -n "${PROFILE}" ]]; then + VERSION="v2" + REASON="profile input" + return + fi + + # A repository pinned to both lists stays on v1, so that pinning a broken + # repository back to v1 does not require also editing the v2 list. + if grep -qxF "${GITHUB_REPOSITORY}" <<<"${FORCE_V1_REPOSITORIES}"; then + VERSION="v1" + REASON="repository is pinned to v1" + return + fi + + if grep -qxF "${GITHUB_REPOSITORY}" <<<"${FORCE_V2_REPOSITORIES}"; then + VERSION="v2" + REASON="repository is pinned to v2" + return + fi + + local bucket + bucket=$(($(printf '%s' "${GITHUB_REPOSITORY}" | cksum | cut -d ' ' -f 1) % 100)) + REASON="bucket ${bucket}, rollout at ${ROLLOUT_PERCENTAGE}%" + if ((bucket < ROLLOUT_PERCENTAGE)); then + VERSION="v2" + else + VERSION="v1" + fi +} + +resolve_version + +if [[ "${VERSION}" == "v1" && -n "${PROFILE}" ]]; then + echo "::error::profile is only supported by GATI v2, but v1 was selected (${REASON}). Remove the profile input, or set gati-version to 'v2'." + exit 1 +fi + +if [[ -z "${PROFILE}" ]]; then + if [[ -z "${AWS_ROLE_ARN}" || -z "${AWS_LAMBDA_URL}" ]]; then + echo "::error::aws-role-arn and aws-lambda-url are required when profile is not set." + exit 1 + fi + # Only read by the v2 flow, which migrates a v1 role ARN as a v1 profile. + PROFILE="v1/${AWS_ROLE_ARN}" +fi + +echo "Using GATI ${VERSION} (${REASON})." +echo "version=${VERSION}" >>"$GITHUB_OUTPUT" + +if [[ "${VERSION}" == "v2" ]]; then + # Written without tee: the profile can embed an IAM role ARN. + echo "profile=${PROFILE}" >>"$GITHUB_OUTPUT" +fi