From 2416cdaa2fe458444da598aa2734d730531d8a55 Mon Sep 17 00:00:00 2001 From: Michael Grosse Huelsewiesche Date: Thu, 9 Jul 2026 18:04:04 -0400 Subject: [PATCH 1/5] Harden CI: Artifactory OIDC, fork-aware workflows, SHA-pinned actions - Add Artifactory OIDC composite action (sets GOPROXY to virtual-go-thirdparty) - Replace ci.yml: fork-aware runner selection, Go 1.22-1.24 matrix, SHA-pinned - Update e2e-tests.yml + publish-e2e-cli.yml: remove E2E_TESTS_TOKEN, fork-aware --- .github/actions/artifactory-oidc/action.yml | 65 ++++++++++++++++++ .github/workflows/ci.yml | 76 ++++++++++++++++++--- .github/workflows/e2e-tests.yml | 21 ++++-- .github/workflows/publish-e2e-cli.yml | 16 ++++- 4 files changed, 160 insertions(+), 18 deletions(-) create mode 100644 .github/actions/artifactory-oidc/action.yml diff --git a/.github/actions/artifactory-oidc/action.yml b/.github/actions/artifactory-oidc/action.yml new file mode 100644 index 0000000..342fc9f --- /dev/null +++ b/.github/actions/artifactory-oidc/action.yml @@ -0,0 +1,65 @@ +name: "Artifactory OIDC Auth" +description: "Exchange GitHub OIDC token for Artifactory access token and configure GOPROXY" + +inputs: + artifactory-url: + description: "Base Artifactory platform URL. Falls back to ARTIFACTORY_URL env var." + required: false + default: "" + oidc-provider-name: + description: "OIDC provider name configured in Artifactory" + required: false + default: "github-actions" + go-repo: + description: "Artifactory virtual Go repository name" + required: false + default: "virtual-go-thirdparty" + +runs: + using: "composite" + steps: + - name: Exchange GitHub OIDC token for Artifactory token + shell: bash + env: + INPUT_ARTIFACTORY_URL: ${{ inputs.artifactory-url }} + OIDC_PROVIDER_NAME: ${{ inputs.oidc-provider-name }} + GO_REPO: ${{ inputs.go-repo }} + run: | + set -euo pipefail + ARTIFACTORY_URL="${INPUT_ARTIFACTORY_URL:-${ARTIFACTORY_URL:-}}" + if [ -z "${ARTIFACTORY_URL}" ]; then + echo "::error::ARTIFACTORY_URL is not set (pass as input or set as env var)"; exit 1 + fi + + OIDC_JWT=$(curl -sS \ + "${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=${ARTIFACTORY_URL}" \ + -H "Authorization: Bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" | jq -r '.value') + + if [ -z "$OIDC_JWT" ] || [ "$OIDC_JWT" = "null" ]; then + echo "::error::Failed to obtain GitHub OIDC token"; exit 1 + fi + + RESP=$(curl -sS "${ARTIFACTORY_URL}/access/api/v1/oidc/token" \ + -H 'Content-Type: application/json' \ + -d "{\"grant_type\":\"urn:ietf:params:oauth:grant-type:token-exchange\", + \"subject_token_type\":\"urn:ietf:params:oauth:token-type:id_token\", + \"subject_token\":\"${OIDC_JWT}\", + \"provider_name\":\"${OIDC_PROVIDER_NAME}\"}") + + ART_TOKEN=$(echo "$RESP" | jq -r '.access_token // empty') + + if [ -z "$ART_TOKEN" ]; then + echo "::error::OIDC token exchange failed." + echo "$RESP" | jq 'if .access_token then .access_token="" else . end' 2>/dev/null || echo "$RESP" + exit 1 + fi + echo "::add-mask::$ART_TOKEN" + + HOST=$(echo "${ARTIFACTORY_URL}" | sed -E 's#^https?://##') + GOPROXY_URL="https://:${ART_TOKEN}@${HOST}/artifactory/api/go/${GO_REPO},direct" + + echo "::add-mask::${GOPROXY_URL}" + echo "GOPROXY=${GOPROXY_URL}" >> "$GITHUB_ENV" + echo "GONOSUMDB=*" >> "$GITHUB_ENV" + echo "GONOSUMCHECK=*" >> "$GITHUB_ENV" + echo "Configured GOPROXY to resolve through Artifactory (${GO_REPO})" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5854c87..2d86ab2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,31 +6,63 @@ on: pull_request: branches: [v3.0] +permissions: + id-token: write + contents: read + +env: + ARTIFACTORY_URL: ${{ vars.ARTIFACTORY_URL }} + jobs: + vet: + name: Vet & Lint + runs-on: ${{ github.event.pull_request.head.repo.fork && 'ubuntu-latest' || 'ubuntu-x64' }} + + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + + - name: Setup Go + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5 + with: + go-version: "1.24" + + - name: Authenticate to Artifactory + if: ${{ !github.event.pull_request.head.repo.fork }} + uses: ./.github/actions/artifactory-oidc + + - name: Download dependencies + run: go mod download + + - name: Vet + run: go vet ./... + test: - runs-on: ubuntu-latest + name: Test (Go ${{ matrix.go-version }}) + runs-on: ${{ github.event.pull_request.head.repo.fork && 'ubuntu-latest' || 'ubuntu-x64' }} strategy: fail-fast: false matrix: - go-version: ['1.21', '1.22', '1.23'] + go-version: ["1.22", "1.23", "1.24"] steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - name: Setup Go ${{ matrix.go-version }} - uses: actions/setup-go@v5 + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5 with: go-version: ${{ matrix.go-version }} - - name: Download dependencies - run: go get -v -t ./... + - name: Authenticate to Artifactory + if: ${{ !github.event.pull_request.head.repo.fork }} + uses: ./.github/actions/artifactory-oidc - - name: Vet - run: go vet ./... + - name: Download dependencies + run: go mod download - name: Test - run: go test -race -coverprofile=cover.out . + run: go test -race -coverprofile=cover.out ./... - name: Upload coverage uses: actions/upload-artifact@v4 @@ -38,3 +70,29 @@ jobs: name: coverage-go${{ matrix.go-version }} path: cover.out retention-days: 7 + + build: + name: Build Verification + runs-on: ${{ github.event.pull_request.head.repo.fork && 'ubuntu-latest' || 'ubuntu-x64' }} + + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + + - name: Setup Go + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5 + with: + go-version: "1.24" + + - name: Authenticate to Artifactory + if: ${{ !github.event.pull_request.head.repo.fork }} + uses: ./.github/actions/artifactory-oidc + + - name: Download dependencies + run: go mod download + + - name: Verify module + run: go mod verify + + - name: Build + run: go build ./... diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index d65d77b..6a7bdea 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -12,27 +12,33 @@ on: required: false default: 'main' +permissions: + id-token: write + contents: read + +env: + ARTIFACTORY_URL: ${{ vars.ARTIFACTORY_URL }} + jobs: e2e-tests: - if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} - runs-on: ubuntu-latest + if: ${{ !github.event.pull_request.head.repo.fork }} + runs-on: ubuntu-x64 steps: - name: Checkout SDK - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 with: path: sdk - name: Checkout sdk-e2e-tests - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 with: repository: segmentio/sdk-e2e-tests ref: ${{ inputs.e2e_tests_ref || 'main' }} - token: ${{ secrets.E2E_TESTS_TOKEN }} path: sdk-e2e-tests - name: Setup Go - uses: actions/setup-go@v5 + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5 with: go-version-file: sdk/go.mod @@ -41,6 +47,9 @@ jobs: with: node-version: '20' + - name: Authenticate to Artifactory + uses: ./sdk/.github/actions/artifactory-oidc + - name: Build e2e-cli working-directory: sdk/e2e-cli run: go build -o e2e-cli-bin ./... diff --git a/.github/workflows/publish-e2e-cli.yml b/.github/workflows/publish-e2e-cli.yml index 9e9e590..11635ba 100644 --- a/.github/workflows/publish-e2e-cli.yml +++ b/.github/workflows/publish-e2e-cli.yml @@ -12,18 +12,28 @@ on: - cron: '0 0 1 * *' workflow_dispatch: +permissions: + id-token: write + contents: read + +env: + ARTIFACTORY_URL: ${{ vars.ARTIFACTORY_URL }} + jobs: publish: - runs-on: ubuntu-latest + runs-on: ubuntu-x64 steps: - name: Checkout SDK - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - name: Setup Go - uses: actions/setup-go@v5 + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5 with: go-version-file: go.mod + - name: Authenticate to Artifactory + uses: ./.github/actions/artifactory-oidc + - name: Build e2e-cli working-directory: e2e-cli run: go build -o e2e-cli-bin ./... From 06ede7c2fb9f2315402ddcc0b3bec9ef170423f0 Mon Sep 17 00:00:00 2001 From: Michael Grosse Huelsewiesche Date: Mon, 13 Jul 2026 11:32:16 -0400 Subject: [PATCH 2/5] fix(ci): correct Artifactory OIDC provider name default and add JWT logging Change default oidc-provider-name from github-actions to github-actions-segmentio (segmentio org) and add JWT claim logging for debugging OIDC exchange failures. --- .github/actions/artifactory-oidc/action.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/actions/artifactory-oidc/action.yml b/.github/actions/artifactory-oidc/action.yml index 342fc9f..1821d71 100644 --- a/.github/actions/artifactory-oidc/action.yml +++ b/.github/actions/artifactory-oidc/action.yml @@ -9,7 +9,7 @@ inputs: oidc-provider-name: description: "OIDC provider name configured in Artifactory" required: false - default: "github-actions" + default: "github-actions-segmentio" go-repo: description: "Artifactory virtual Go repository name" required: false @@ -39,6 +39,13 @@ runs: echo "::error::Failed to obtain GitHub OIDC token"; exit 1 fi + decode_seg() { local s="${1}"; local m=$(( ${#s} % 4 )); [ $m -ne 0 ] && s="${s}$(printf '=%.0s' $(seq 1 $((4-m))))"; echo "$s" | tr '_-' '/+' | base64 -d 2>/dev/null; } + PAYLOAD=$(decode_seg "$(echo "$OIDC_JWT" | cut -d. -f2)") + echo "OIDC token claims:" + echo " sub = $(echo "$PAYLOAD" | jq -r '.sub')" + echo " aud = $(echo "$PAYLOAD" | jq -r '.aud')" + echo " iss = $(echo "$PAYLOAD" | jq -r '.iss')" + RESP=$(curl -sS "${ARTIFACTORY_URL}/access/api/v1/oidc/token" \ -H 'Content-Type: application/json' \ -d "{\"grant_type\":\"urn:ietf:params:oauth:grant-type:token-exchange\", From bd1e9a28c76009d583c4685e27c12c60f5932486 Mon Sep 17 00:00:00 2001 From: Michael Grosse Huelsewiesche Date: Wed, 22 Jul 2026 20:07:58 -0400 Subject: [PATCH 3/5] fix(ci): harden OIDC error redaction to cover all token fields --- .github/actions/artifactory-oidc/action.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/actions/artifactory-oidc/action.yml b/.github/actions/artifactory-oidc/action.yml index 1821d71..176ae17 100644 --- a/.github/actions/artifactory-oidc/action.yml +++ b/.github/actions/artifactory-oidc/action.yml @@ -57,7 +57,8 @@ runs: if [ -z "$ART_TOKEN" ]; then echo "::error::OIDC token exchange failed." - echo "$RESP" | jq 'if .access_token then .access_token="" else . end' 2>/dev/null || echo "$RESP" + echo "$RESP" | jq 'walk(if type == "object" then with_entries(if (.key | test("token"; "i")) then .value = "" else . end) else . end)' 2>/dev/null \ + || echo "::error::(response withheld — not valid JSON)" exit 1 fi echo "::add-mask::$ART_TOKEN" From f1d23030b1013bd55d53ef120aa5902aac924c84 Mon Sep 17 00:00:00 2001 From: Michael Grosse Huelsewiesche Date: Fri, 25 Sep 2026 21:00:07 -0400 Subject: [PATCH 4/5] fix(ci): SHA-pin remaining actions so workflows pass org sha_pinning_required MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The segmentio org enforces sha_pinning_required, so any tag-pinned uses: line fails the whole workflow at startup — no jobs run and no check is reported, which reads as 'no CI' rather than a failure. --- .github/workflows/ci.yml | 2 +- .github/workflows/e2e-tests.yml | 4 ++-- .github/workflows/publish-e2e-cli.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2d86ab2..2417cbb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,7 +65,7 @@ jobs: run: go test -race -coverprofile=cover.out ./... - name: Upload coverage - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: coverage-go${{ matrix.go-version }} path: cover.out diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 6a7bdea..f32bb5d 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -43,7 +43,7 @@ jobs: go-version-file: sdk/go.mod - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: '20' @@ -63,7 +63,7 @@ jobs: - name: Upload test results if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: e2e-test-results path: sdk-e2e-tests/test-results/ diff --git a/.github/workflows/publish-e2e-cli.yml b/.github/workflows/publish-e2e-cli.yml index 11635ba..56d5c5d 100644 --- a/.github/workflows/publish-e2e-cli.yml +++ b/.github/workflows/publish-e2e-cli.yml @@ -39,7 +39,7 @@ jobs: run: go build -o e2e-cli-bin ./... - name: Upload CLI artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: e2e-cli-go path: e2e-cli/e2e-cli-bin From a050b516c8b35bb40cd1706d5d7d8b8449da92d8 Mon Sep 17 00:00:00 2001 From: Michael Grosse Huelsewiesche Date: Fri, 25 Sep 2026 22:56:35 -0400 Subject: [PATCH 5/5] refactor(ci): use twilio/sdk-actions for Artifactory OIDC Replaces the repo-local copy with the shared first-party action, pinned. Each SDK had its own drifting copy of the same exchange; the shared one covers every ecosystem we use, so fixes land once instead of six times. --- .github/actions/artifactory-oidc/action.yml | 73 --------------------- .github/workflows/ci.yml | 15 ++++- .github/workflows/e2e-tests.yml | 5 +- .github/workflows/publish-e2e-cli.yml | 5 +- 4 files changed, 20 insertions(+), 78 deletions(-) delete mode 100644 .github/actions/artifactory-oidc/action.yml diff --git a/.github/actions/artifactory-oidc/action.yml b/.github/actions/artifactory-oidc/action.yml deleted file mode 100644 index 176ae17..0000000 --- a/.github/actions/artifactory-oidc/action.yml +++ /dev/null @@ -1,73 +0,0 @@ -name: "Artifactory OIDC Auth" -description: "Exchange GitHub OIDC token for Artifactory access token and configure GOPROXY" - -inputs: - artifactory-url: - description: "Base Artifactory platform URL. Falls back to ARTIFACTORY_URL env var." - required: false - default: "" - oidc-provider-name: - description: "OIDC provider name configured in Artifactory" - required: false - default: "github-actions-segmentio" - go-repo: - description: "Artifactory virtual Go repository name" - required: false - default: "virtual-go-thirdparty" - -runs: - using: "composite" - steps: - - name: Exchange GitHub OIDC token for Artifactory token - shell: bash - env: - INPUT_ARTIFACTORY_URL: ${{ inputs.artifactory-url }} - OIDC_PROVIDER_NAME: ${{ inputs.oidc-provider-name }} - GO_REPO: ${{ inputs.go-repo }} - run: | - set -euo pipefail - ARTIFACTORY_URL="${INPUT_ARTIFACTORY_URL:-${ARTIFACTORY_URL:-}}" - if [ -z "${ARTIFACTORY_URL}" ]; then - echo "::error::ARTIFACTORY_URL is not set (pass as input or set as env var)"; exit 1 - fi - - OIDC_JWT=$(curl -sS \ - "${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=${ARTIFACTORY_URL}" \ - -H "Authorization: Bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" | jq -r '.value') - - if [ -z "$OIDC_JWT" ] || [ "$OIDC_JWT" = "null" ]; then - echo "::error::Failed to obtain GitHub OIDC token"; exit 1 - fi - - decode_seg() { local s="${1}"; local m=$(( ${#s} % 4 )); [ $m -ne 0 ] && s="${s}$(printf '=%.0s' $(seq 1 $((4-m))))"; echo "$s" | tr '_-' '/+' | base64 -d 2>/dev/null; } - PAYLOAD=$(decode_seg "$(echo "$OIDC_JWT" | cut -d. -f2)") - echo "OIDC token claims:" - echo " sub = $(echo "$PAYLOAD" | jq -r '.sub')" - echo " aud = $(echo "$PAYLOAD" | jq -r '.aud')" - echo " iss = $(echo "$PAYLOAD" | jq -r '.iss')" - - RESP=$(curl -sS "${ARTIFACTORY_URL}/access/api/v1/oidc/token" \ - -H 'Content-Type: application/json' \ - -d "{\"grant_type\":\"urn:ietf:params:oauth:grant-type:token-exchange\", - \"subject_token_type\":\"urn:ietf:params:oauth:token-type:id_token\", - \"subject_token\":\"${OIDC_JWT}\", - \"provider_name\":\"${OIDC_PROVIDER_NAME}\"}") - - ART_TOKEN=$(echo "$RESP" | jq -r '.access_token // empty') - - if [ -z "$ART_TOKEN" ]; then - echo "::error::OIDC token exchange failed." - echo "$RESP" | jq 'walk(if type == "object" then with_entries(if (.key | test("token"; "i")) then .value = "" else . end) else . end)' 2>/dev/null \ - || echo "::error::(response withheld — not valid JSON)" - exit 1 - fi - echo "::add-mask::$ART_TOKEN" - - HOST=$(echo "${ARTIFACTORY_URL}" | sed -E 's#^https?://##') - GOPROXY_URL="https://:${ART_TOKEN}@${HOST}/artifactory/api/go/${GO_REPO},direct" - - echo "::add-mask::${GOPROXY_URL}" - echo "GOPROXY=${GOPROXY_URL}" >> "$GITHUB_ENV" - echo "GONOSUMDB=*" >> "$GITHUB_ENV" - echo "GONOSUMCHECK=*" >> "$GITHUB_ENV" - echo "Configured GOPROXY to resolve through Artifactory (${GO_REPO})" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2417cbb..7adec40 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,10 @@ jobs: - name: Authenticate to Artifactory if: ${{ !github.event.pull_request.head.repo.fork }} - uses: ./.github/actions/artifactory-oidc + uses: twilio/sdk-actions/artifactory-oidc@c94e420aa64ea686ff25bb03d4c66cdaf8e523e4 # main + with: + ecosystem: go + provider-name: github-actions-segmentio - name: Download dependencies run: go mod download @@ -56,7 +59,10 @@ jobs: - name: Authenticate to Artifactory if: ${{ !github.event.pull_request.head.repo.fork }} - uses: ./.github/actions/artifactory-oidc + uses: twilio/sdk-actions/artifactory-oidc@c94e420aa64ea686ff25bb03d4c66cdaf8e523e4 # main + with: + ecosystem: go + provider-name: github-actions-segmentio - name: Download dependencies run: go mod download @@ -86,7 +92,10 @@ jobs: - name: Authenticate to Artifactory if: ${{ !github.event.pull_request.head.repo.fork }} - uses: ./.github/actions/artifactory-oidc + uses: twilio/sdk-actions/artifactory-oidc@c94e420aa64ea686ff25bb03d4c66cdaf8e523e4 # main + with: + ecosystem: go + provider-name: github-actions-segmentio - name: Download dependencies run: go mod download diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index f32bb5d..7c7e3e5 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -48,7 +48,10 @@ jobs: node-version: '20' - name: Authenticate to Artifactory - uses: ./sdk/.github/actions/artifactory-oidc + uses: twilio/sdk-actions/artifactory-oidc@c94e420aa64ea686ff25bb03d4c66cdaf8e523e4 # main + with: + ecosystem: go + provider-name: github-actions-segmentio - name: Build e2e-cli working-directory: sdk/e2e-cli diff --git a/.github/workflows/publish-e2e-cli.yml b/.github/workflows/publish-e2e-cli.yml index 56d5c5d..1a3dff4 100644 --- a/.github/workflows/publish-e2e-cli.yml +++ b/.github/workflows/publish-e2e-cli.yml @@ -32,7 +32,10 @@ jobs: go-version-file: go.mod - name: Authenticate to Artifactory - uses: ./.github/actions/artifactory-oidc + uses: twilio/sdk-actions/artifactory-oidc@c94e420aa64ea686ff25bb03d4c66cdaf8e523e4 # main + with: + ecosystem: go + provider-name: github-actions-segmentio - name: Build e2e-cli working-directory: e2e-cli