diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 515205d..24bd51e 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -12,18 +12,34 @@ permissions: id-token: write jobs: - test: - name: Test + lint: + name: Lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - - uses: actions/setup-python@v7 with: python-version: 3.12 - - uses: pre-commit/action@v3.0.1 + test: + name: Test + container: + # One or more test cases run in a container, so we can tell that the + # Action works when run in a containerized job. + image: ${{ matrix.container-image }} + runs-on: ubuntu-latest + strategy: + # Finish all test cases even once one of them fails. + fail-fast: false + matrix: + case: [runner, container] + include: + - case: container + container-image: python:3.12 + steps: + - uses: actions/checkout@v7 + - name: Fetch Cloudsmith OIDC token id: cloudsmith-oidc uses: ./ @@ -31,12 +47,24 @@ jobs: account: ${{ secrets.CLOUDSMITH_ACCOUNT }} service: ${{ secrets.CLOUDSMITH_SERVICE }} + - uses: actions/setup-python@v7 + if: '! matrix.container-image' + with: + python-version: 3.12 - name: Verify Cloudsmith OIDC token env: TOKEN: ${{ steps.cloudsmith-oidc.outputs.token }} FILE: ${{ steps.cloudsmith-oidc.outputs.file }} run: | - typ=$(echo "$TOKEN" | cut -d'.' -f1 | base64 -d | jq -r .typ) + set -euxo pipefail + + # jq isn't available in the container case, so check the token with + # Python in both cases. + pluck_field() { + python3 -c 'import json, sys; print(json.load(sys.stdin)[sys.argv[1]])' "$1" + } + + typ=$(echo "$TOKEN" | cut -d'.' -f1 | base64 -d | pluck_field typ) if [[ "$typ" != "JWT" ]]; then echo "Invalid token type: $typ" exit 1 @@ -46,3 +74,4 @@ jobs: echo "Token not found in file $FILE" exit 1 fi + shell: bash diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0d517f8..ccf6826 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,9 +2,15 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: - - id: check-shebang-scripts-are-executable - - id: check-yaml + # Fix the last lines' trailing whitespace before correcting that they exist. - id: trailing-whitespace - id: end-of-file-fixer + + - id: check-shebang-scripts-are-executable + - id: check-yaml - id: fix-byte-order-marker - id: mixed-line-ending +- repo: https://github.com/shellcheck-py/shellcheck-py + rev: v0.11.0.1 + hooks: + - id: shellcheck diff --git a/README.md b/README.md index b0fd79f..6e78002 100644 --- a/README.md +++ b/README.md @@ -26,4 +26,8 @@ jobs: # ... ``` +Either [jq] or Python is required. To use in a [containerized job] or on a custom runner image, ensure Python is available or install jq. + +[containerized job]: https://docs.github.com/en/actions/how-tos/write-workflows/choose-where-workflows-run/run-jobs-in-a-container +[jq]: https://jqlang.org [oidc]: https://help.cloudsmith.io/docs/openid-connect diff --git a/action.sh b/action.sh new file mode 100755 index 0000000..6050460 --- /dev/null +++ b/action.sh @@ -0,0 +1,48 @@ +#!/bin/bash + +set -euxo pipefail + +request() { + if ! resp="$(curl --silent --fail-with-body "$@")" + then + curl_exit_code="$?" + echo "$resp" >&2 + exit "$curl_exit_code" + fi + echo "$resp" +} + +# pluck_field +# Pluck the named field from the JSON content on stdin using jq or Python & +# print it to stdout. +pluck_field() { + field="$1" + + if command -v jq > /dev/null; then + jq -r ".${field}" + elif command -v python3 > /dev/null; then + python3 -c 'import json, sys; print(json.load(sys.stdin)[sys.argv[1]])' "$field" + else + echo "Neither jq nor python3 are installed. One or the other is required to read JSON responses." >&2 + exit 1 + fi +} + +if [[ ! -v ACTIONS_ID_TOKEN_REQUEST_TOKEN ]] +then + message="No \$ACTIONS_ID_TOKEN_REQUEST_TOKEN is set. Does this job have the \`id-token: write\` permission necessary to get a Cloudsmith token?" + echo "$message" >&2 + echo "::error title=Missing \`id-token: write\` permission?::${message}" + exit 1 +fi + +CLOUDSMITH_ACCOUNT=${CLOUDSMITH_ACCOUNT:-$GITHUB_REPOSITORY_OWNER} +echo "Retrieving actions OIDC token" +id_token=$(request -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=api://AzureADTokenExchange" | pluck_field value) +echo "Retrieving Cloudsmith token for account: $CLOUDSMITH_ACCOUNT (service account: $SERVICE_SLUG)" +cloudsmith_token=$(request --json '{"oidc_token":"'"$id_token"'", "service_slug":"'"$SERVICE_SLUG"'"}' "https://api.cloudsmith.io/openid/$CLOUDSMITH_ACCOUNT/" | pluck_field token) +echo "value=$cloudsmith_token" >> "$GITHUB_OUTPUT" + +file="$(mktemp)" +echo "CLOUDSMITH_TOKEN=$cloudsmith_token" >> "$file" +echo "file=$file" >> "$GITHUB_OUTPUT" diff --git a/action.yaml b/action.yaml index a84d553..3bd6d27 100644 --- a/action.yaml +++ b/action.yaml @@ -19,53 +19,9 @@ runs: steps: - name: Get OIDC token id: token - shell: bash env: CLOUDSMITH_ACCOUNT: ${{ inputs.account }} SERVICE_SLUG: ${{ inputs.service }} run: | - set -eu - - # https://superuser.com/a/1641410 - curlf() { - output_file=$(mktemp) - code=$(curl --silent --output $output_file --write-out "%{http_code}" "$@") - if [[ ${code} -lt 200 || ${code} -gt 299 ]]; then - >&2 cat $output_file - return 22 - fi - cat $output_file - rm $output_file - } - - request() { - set +e - resp="$(curlf "$@")" - rc=$? - set -e - # shellcheck disable=SC2181 - if [ $rc -ne 0 ]; then - echo "$resp" >> /dev/stderr - exit 1 - fi - echo "$resp" - } - - if [[ ! -v ACTIONS_ID_TOKEN_REQUEST_TOKEN ]] - then - message='No $ACTIONS_ID_TOKEN_REQUEST_TOKEN is set. Does this job have the `id-token: write` permission necessary to get a Cloudsmith token?' - echo "$message" >&2 - echo '::error title=Missing `id-token: write` permission?::'"$message" - exit 1 - fi - - CLOUDSMITH_ACCOUNT=${CLOUDSMITH_ACCOUNT:-$GITHUB_REPOSITORY_OWNER} - echo "Retrieving actions OIDC token" - id_token=$(request -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=api://AzureADTokenExchange" | jq -r '.value') - echo "Retrieving Cloudsmith token for account: $CLOUDSMITH_ACCOUNT (service account: $SERVICE_SLUG)" - cloudsmith_token=$(request -X POST -H "Content-Type: application/json" -d "{\"oidc_token\":\"$id_token\", \"service_slug\":\"$SERVICE_SLUG\"}" "https://api.cloudsmith.io/openid/$CLOUDSMITH_ACCOUNT/" | jq -r '.token') - echo "value=$cloudsmith_token" >> "$GITHUB_OUTPUT" - - file="$(mktemp)" - echo "CLOUDSMITH_TOKEN=$cloudsmith_token" >> "$file" - echo "file=$file" >> "$GITHUB_OUTPUT" + "$GITHUB_ACTION_PATH"/action.sh + shell: bash