From fb224e96fe623a9d20943e4e55170f92ee65e75f Mon Sep 17 00:00:00 2001 From: Dominic Couture Date: Tue, 18 Nov 2025 15:17:04 +0000 Subject: [PATCH] chore: Use trusted publishing for PyPi package Speakeasy doesn't support it out of the box, but we can manully mint a short-lived API token using OIDC which we then pass to speakeasy. This allows us to remove the long-lived API token from our CI variables. --- .github/workflows/pypi_mint_token.yaml | 33 ++++++++++++++++++++++++++ .github/workflows/sdk_generation.yaml | 8 +++++-- .github/workflows/sdk_publish.yaml | 8 +++++-- 3 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/pypi_mint_token.yaml diff --git a/.github/workflows/pypi_mint_token.yaml b/.github/workflows/pypi_mint_token.yaml new file mode 100644 index 00000000..3e9c9ef8 --- /dev/null +++ b/.github/workflows/pypi_mint_token.yaml @@ -0,0 +1,33 @@ +name: Mint PyPI Token +on: + workflow_call: + outputs: + pypi_token: + description: Minted PyPI API token via OIDC + value: ${{ jobs.mint.outputs.pypi_token }} +jobs: + mint: + name: Mint PyPI token + runs-on: ubuntu-latest + permissions: + id-token: write + steps: + - name: Mint API token + id: mint-token + run: | + # retrieve the ambient OIDC token + resp=$(curl -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \ + "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=pypi") + oidc_token=$(jq -r '.value' <<< "${resp}") + + # exchange the OIDC token for an API token + resp=$(curl -X POST https://pypi.org/_/oidc/mint-token -d "{\"token\": \"${oidc_token}\"}") + api_token=$(jq -r '.token' <<< "${resp}") + + # mask the newly minted API token, so that we don't accidentally leak it + echo "::add-mask::${api_token}" + + # see the next step in the workflow for an example of using this step output + echo "api-token=${api_token}" >> "${GITHUB_OUTPUT}" + outputs: + pypi_token: ${{ steps.mint-token.outputs.pypi_token }} diff --git a/.github/workflows/sdk_generation.yaml b/.github/workflows/sdk_generation.yaml index b5261716..82e57765 100644 --- a/.github/workflows/sdk_generation.yaml +++ b/.github/workflows/sdk_generation.yaml @@ -4,7 +4,8 @@ permissions: contents: write pull-requests: write statuses: write -"on": + id-token: write +on: workflow_dispatch: inputs: force: @@ -17,7 +18,10 @@ permissions: schedule: - cron: 0 0 * * * jobs: + mint_pypi_token: + uses: ./.github/workflows/pypi_mint_token.yaml generate: + needs: mint_pypi_token uses: speakeasy-api/sdk-generation-action/.github/workflows/workflow-executor.yaml@v15 with: force: ${{ github.event.inputs.force }} @@ -26,5 +30,5 @@ jobs: speakeasy_version: latest secrets: github_access_token: ${{ secrets.GITHUB_TOKEN }} - pypi_token: ${{ secrets.PYPI_TOKEN }} + pypi_token: ${{ needs.mint_pypi_token.outputs.pypi_token }} speakeasy_api_key: ${{ secrets.SPEAKEASY_API_KEY }} diff --git a/.github/workflows/sdk_publish.yaml b/.github/workflows/sdk_publish.yaml index cdfbfa8e..72bdfb7e 100644 --- a/.github/workflows/sdk_publish.yaml +++ b/.github/workflows/sdk_publish.yaml @@ -4,7 +4,8 @@ permissions: contents: write pull-requests: write statuses: write -"on": + id-token: write +on: push: branches: - main @@ -12,9 +13,12 @@ permissions: - RELEASES.md - '*/RELEASES.md' jobs: + mint_pypi_token: + uses: ./.github/workflows/pypi_mint_token.yaml publish: + needs: mint_pypi_token uses: speakeasy-api/sdk-generation-action/.github/workflows/sdk-publish.yaml@v15 secrets: github_access_token: ${{ secrets.GITHUB_TOKEN }} - pypi_token: ${{ secrets.PYPI_TOKEN }} + pypi_token: ${{ needs.mint_pypi_token.outputs.pypi_token }} speakeasy_api_key: ${{ secrets.SPEAKEASY_API_KEY }}