From 1926ac5ceb6811643354e7a45d2f86ff11824fd3 Mon Sep 17 00:00:00 2001 From: Natty Linden Date: Mon, 12 Oct 2026 23:30:39 +0000 Subject: [PATCH 1/5] Keep the Action script as a real bash script --- action.sh | 47 +++++++++++++++++++++++++++++++++++++++++++++++ action.yaml | 48 ++---------------------------------------------- 2 files changed, 49 insertions(+), 46 deletions(-) create mode 100755 action.sh diff --git a/action.sh b/action.sh new file mode 100755 index 0000000..8b2abcc --- /dev/null +++ b/action.sh @@ -0,0 +1,47 @@ +#!/bin/bash + +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" 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 From 72525e5f12b74c8cd200e056d29b9003ee62fce1 Mon Sep 17 00:00:00 2001 From: Natty Linden Date: Mon, 12 Oct 2026 23:30:39 +0000 Subject: [PATCH 2/5] Check the Action script with shellcheck --- .pre-commit-config.yaml | 10 ++++++++-- action.sh | 16 ++++++++-------- 2 files changed, 16 insertions(+), 10 deletions(-) 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/action.sh b/action.sh index 8b2abcc..d522101 100755 --- a/action.sh +++ b/action.sh @@ -1,17 +1,17 @@ #!/bin/bash -set -eu +set -euxo pipefail # 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 + 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 + cat "$output_file" + rm "$output_file" } request() { @@ -29,9 +29,9 @@ request() { 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?' + 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" + echo "::error title=Missing \`id-token: write\` permission?::${message}" exit 1 fi From ed55c959756583c5b159c8f247796c712c3a2cf9 Mon Sep 17 00:00:00 2001 From: Natty Linden Date: Mon, 12 Oct 2026 23:33:32 +0000 Subject: [PATCH 3/5] Simplify curling for API responses As long as we have curl v7.76 (released 31 Mar 2021), we can use `--fail-with-body` to still print the response body in fail mode. --- action.sh | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/action.sh b/action.sh index d522101..8c4502e 100755 --- a/action.sh +++ b/action.sh @@ -2,27 +2,12 @@ set -euxo pipefail -# 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 + if ! resp="$(curl --silent --fail-with-body "$@")" + then + curl_exit_code="$?" + echo "$resp" >&2 + exit "$curl_exit_code" fi echo "$resp" } From 5b4a8db70f2b0aae6765c3bcad703c57e64eca08 Mon Sep 17 00:00:00 2001 From: Natty Linden Date: Tue, 13 Oct 2026 16:23:56 +0000 Subject: [PATCH 4/5] Run pre-commit in a separate Lint job --- .github/workflows/test.yaml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 515205d..aae3ffb 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -12,18 +12,22 @@ 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 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - name: Fetch Cloudsmith OIDC token id: cloudsmith-oidc uses: ./ From 89578037d8493030843fc66765a802a8fb74f5e2 Mon Sep 17 00:00:00 2001 From: Natty Linden Date: Mon, 12 Oct 2026 23:34:05 +0000 Subject: [PATCH 5/5] If jq is not available, try using Python to pluck fields out of API responses This can be helpful when running the Action in a container, where perhaps Python is available but jq is not. --- .github/workflows/test.yaml | 27 ++++++++++++++++++++++++++- README.md | 4 ++++ action.sh | 20 ++++++++++++++++++-- 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index aae3ffb..24bd51e 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -24,7 +24,19 @@ jobs: 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 @@ -35,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 @@ -50,3 +74,4 @@ jobs: echo "Token not found in file $FILE" exit 1 fi + shell: bash 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 index 8c4502e..6050460 100755 --- a/action.sh +++ b/action.sh @@ -12,6 +12,22 @@ request() { 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?" @@ -22,9 +38,9 @@ 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') +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 -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') +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)"