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
1 change: 1 addition & 0 deletions .github/workflows/changesets-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ jobs:
libs/**
apps/**
workflows/**
!actions/invoke-gati-v2/**
!actions/signed-commits/__fixtures__/**
!**/node_modules/**

Expand Down
173 changes: 173 additions & 0 deletions .github/workflows/invoke-gati-v2-validate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
# Exercises actions/invoke-gati-v2 against the v1 migration profiles.
#
# This repository is public, so these workflow logs are public. Keep the checks
# below to assertions, counts, and permission flags — never print team member
# logins or the names of repositories a token happens to reach.
name: invoke-gati-v2 validate

on:
pull_request:
paths:
- "actions/invoke-gati-v2/**"
- ".github/workflows/invoke-gati-v2-validate.yml"
workflow_dispatch:

permissions: {}

jobs:
global-read-only:
name: Global read-only role
# Repository secrets are unavailable to pull requests from forks.
if:
github.event_name == 'workflow_dispatch' ||
github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- name: Checkout repo (needed to reference local action)
uses: actions/checkout@v7
with:
persist-credentials: false

- name: Get a GitHub token
id: gati
uses: ./actions/invoke-gati-v2
with:
profile:
v1/${{ secrets.AWS_OIDC_GLOBAL_READ_ONLY_TOKEN_ISSUER_ROLE_ARN }}

- name: Check a token was issued
env:
ACCESS_TOKEN: ${{ steps.gati.outputs.access-token }}
run: |
set -euo pipefail

if [[ -z "${ACCESS_TOKEN}" ]]; then
echo "No access token was returned."
exit 1
fi
echo "An access token was returned."

repository-contents-write:
name: Repository contents:write role
if:
github.event_name == 'workflow_dispatch' ||
github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
env:
TEST_BRANCH: gati-v2-validate/${{ github.run_id }}-${{ github.run_attempt }}
steps:
- name: Checkout repo (needed to reference local action)
uses: actions/checkout@v7
with:
persist-credentials: false
# Full history, so pushing the checked-out commit below is a plain
# ref create rather than a shallow update, which servers may refuse.
fetch-depth: 0

- name: Get a GitHub token
id: gati
uses: ./actions/invoke-gati-v2
with:
profile: v1/${{ secrets.AWS_OIDC_IAM_ROLE_ARN_GATI }}

- name: Configure git to authenticate with the token
env:
GH_TOKEN: ${{ steps.gati.outputs.access-token }}
run: |
git config --global \
url."https://x-access-token:${GH_TOKEN}@github.com/".insteadOf \
"https://github.com/"

- name: Check the token reaches this repository
env:
GH_TOKEN: ${{ steps.gati.outputs.access-token }}
run: |
set -euo pipefail

repositories=$(gh api --paginate /installation/repositories --jq '.repositories[].full_name')
if [[ -z "${repositories}" ]]; then
echo "Token reaches no repositories."
exit 1
fi
echo "Token reaches $(grep -c '^' <<<"${repositories}") repositories."

if ! grep -qxF "${GITHUB_REPOSITORY}" <<<"${repositories}"; then
echo "Token cannot reach ${GITHUB_REPOSITORY}."
exit 1
fi
echo "Token reaches ${GITHUB_REPOSITORY}."

# An installation token's permissions are not readable back from the API,
# so prove contents:write by exercising it. The branch points at the
# commit under test, which is already on the remote, so no new content is
# published, and no workflow here triggers on pushes outside main.
- name: Check the token can push a branch to this repository
run: |
set -euo pipefail

git switch --create "${TEST_BRANCH}"
git push origin "${TEST_BRANCH}"
echo "Token pushed ${TEST_BRANCH} to ${GITHUB_REPOSITORY}."

# Always runs so the branch is never left behind when the push above
# succeeded but a later step failed.
- name: Check the token can delete the branch
if: always()
run: |
set -euo pipefail

pushed=$(git ls-remote --heads origin "refs/heads/${TEST_BRANCH}")
if [[ -z "${pushed}" ]]; then
echo "No branch was pushed, so there is nothing to delete."
exit 0
fi

git push origin --delete "${TEST_BRANCH}"
echo "Token deleted ${TEST_BRANCH} from ${GITHUB_REPOSITORY}."

codeowners-team-read:
name: CODEOWNERS team role
if:
github.event_name == 'workflow_dispatch' ||
github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
env:
TEAM_ORG: smartcontractkit
TEAM_SLUG: devex-cicd
steps:
- name: Checkout repo (needed to reference local action)
uses: actions/checkout@v7
with:
persist-credentials: false

- name: Get a GitHub token
id: gati
uses: ./actions/invoke-gati-v2
with:
profile: v1/${{ secrets.GATI_CODEOWNERS_IAM_ARN }}

- name: Check the token can read the team
env:
GH_TOKEN: ${{ steps.gati.outputs.access-token }}
run: |
set -euo pipefail

slug=$(gh api "/orgs/${TEAM_ORG}/teams/${TEAM_SLUG}" --jq '.slug')
if [[ "${slug}" != "${TEAM_SLUG}" ]]; then
echo "Requested ${TEAM_ORG}/${TEAM_SLUG} but read back '${slug}'."
exit 1
fi
echo "Read team ${TEAM_ORG}/${slug}."

# Count only: member logins would be published in this repo's logs.
members=$(gh api --paginate "/orgs/${TEAM_ORG}/teams/${TEAM_SLUG}/members" --jq '.[].login' | grep -c '^' || true)
echo "Team ${TEAM_ORG}/${TEAM_SLUG} has ${members} members."
6 changes: 3 additions & 3 deletions .github/workflows/pull-request-main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ jobs:
# Disable this job until it's updated to be:
# 1. More generalizable for all workspaces requiring build artifacts
# 2. Have better attribution for the commits generated from this workflow
if: false
if: ${{ github.event_name == 'disabled'}}
permissions:
contents: write
pull-requests: write
Expand Down Expand Up @@ -149,9 +149,9 @@ jobs:

- name: Check for diff in built artifacts
run: |
if [[ $(git status --porcelain "**/index.js") ]]; then
if [[ $(git status --porcelain "**/index.js" "**/post.js") ]]; then
echo "Built artifacts are out of date. Please run 'pnpm build:artifacts' and commit the changes."
git status --porcelain "**/index.js"
git status --porcelain "**/index.js" "**/post.js"
exit 1
else
echo "Built artifacts are up to date."
Expand Down
94 changes: 94 additions & 0 deletions actions/invoke-gati-v2/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# invoke-gati-v2

Mints a scoped GitHub installation access token from the GATI v2 token issuer,
authenticated with a GitHub OIDC token. The token is revoked automatically when
the job finishes.

Supersedes
[`invoke-gati`](https://github.com/smartcontractkit/.github/tree/main/actions/invoke-gati)
(v1), which required assuming an AWS role first. This action needs no AWS
credentials.

## Requirements

The calling job must be able to mint OIDC tokens:

```yaml
permissions:
id-token: write
```

Without it the action fails immediately, before making any network call.

## Inputs

| Name | Description | Required | Defaulted |
| --------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ----------------------------- |
| `profile` | The token profile to request. See [Profiles](#profiles). | ✅ | |
| `url` | Overrides the endpoint to request the token from. | ❌ | ✅ |
| `retries` | Additional attempts when the request fails with a network error, `429`, or a `5xx` response, with exponential backoff. `4xx` fails immediately. | ❌ | ✅ - `"0"` (a single attempt) |

## Outputs

| Name | Description |
| -------------- | -------------------------------------------------------------------------------- |
| `access-token` | The scoped GitHub installation access token. Masked in logs, revoked on cleanup. |

## Profiles

A profile determines which repositories the returned token can reach and what it
can do there. Profiles are configured outside this repository — ask the team
that owns GATI which profiles are available to yours.

There are 4 profile types:

| Profile | Refers to |
| ------------------- | -------------------------------------------- |
| `<name>` | A profile defined for the calling repository |
| `presets/<name>` | A shared preset |
| `globals/<name>` | A shared global |
| `v1/<iam-role-arn>` | The GATI v1 entry for an IAM role ARN |

Names in the first three forms must be non-empty and must not contain `/`. A v1
role ARN is used as given, including its colons and any role path.

Malformed profiles fail the step with a message naming the rule that was
violated, before any token is requested.

## Token lifetime

The action stores the token in step state and revokes it via
[`DELETE /installation/token`](https://docs.github.com/en/rest/apps/installations#revoke-an-installation-access-token)
in its post step, which GitHub runs at the end of the job. Revocation cannot be
disabled. If it fails, the action emits a warning rather than failing the job —
the token expires on its own regardless, and failing a green job would not
un-issue it.

Because revocation is scoped to the job, do not pass this token to another job
via job outputs; request a new one in each job that needs it.

## Example Usage

```yaml
name: example

on:
workflow_dispatch:

jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write
steps:
- name: Get a GitHub token
id: gati
uses: smartcontractkit/.github/actions/invoke-gati-v2@<tag>
with:
profile: my-profile

- name: Use the token
env:
GH_TOKEN: ${{ steps.gati.outputs.access-token }}
run: gh repo view smartcontractkit/.github
```
40 changes: 40 additions & 0 deletions actions/invoke-gati-v2/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: invoke-gati-v2
description:
"Mint a scoped GitHub installation access token from GATI v2, authenticated
with a GitHub OIDC token"

inputs:
profile:
description: |
The GATI token profile to request. One of:
<name> a profile defined for the calling repository
presets/<name> a shared preset
globals/<name> a shared global
v1/<iam-role-arn> the GATI v1 entry for an IAM role ARN
Names in the first three forms must be non-empty and must not contain "/".
required: true

url:
description:
"Overrides the GATI v2 endpoint to request the token from. Defaults to the
endpoint the action is built against."
required: false

retries:
description: |
Additional attempts when the request fails with a network error, 429, or a
5xx response, with exponential backoff between attempts. 4xx responses fail
immediately. Defaults to "0" — a single attempt, no retry.
required: false
default: "0"

outputs:
access-token:
description:
The GitHub installation access token scoped by the requested profile. It
is revoked automatically in this action's post step.

runs:
using: "node24"
main: "dist/index.js"
post: "dist/post.js"
Loading
Loading