GitHub Actions¶
Install private packages in a job¶
When: a workflow step runs pip/uv install from your feed.
Use OIDC — no az, no token marshalling. Give the job id-token: write and set
AZURE_CLIENT_ID (+ AZURE_TENANT_ID) for the federated identity:
jobs:
build:
permissions:
id-token: write # required for OIDC
contents: read
runs-on: ubuntu-latest
env:
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v6
- run: uv tool install keyring --with artifacts-keyring-nofuss
- name: Install private package
run: |
uv pip install my-package \
--index-url https://__token__@pkgs.dev.azure.com/{org}/_packaging/{feed}/pypi/simple/
The workload-identity flow mints the token straight from the GitHub OIDC
endpoint — it works whether or not azure/login ran.
The same with official artifacts-keyring
There's no OIDC path, so after azure/login@v2 you mint a token and marshal
it into per-endpoint JSON yourself:
- uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
allow-no-subscriptions: true
- run: |
TOKEN=$(az account get-access-token \
--resource 499b84ac-1321-427f-aa17-267ca6975798 \
--query accessToken -o tsv)
echo "VSS_NUGET_EXTERNAL_FEED_ENDPOINTS={\"endpointCredentials\":[{\"endpoint\":\"https://pkgs.dev.azure.com/{org}/_packaging/{feed}/pypi/simple/\",\"username\":\"AzureDevOps\",\"password\":\"$TOKEN\"}]}" >> "$GITHUB_ENV"
Build a Docker image in a job¶
When: a workflow builds an image that pulls private packages. The OIDC material lives on the runner and can't reach the isolated build, so mint a token on the runner and pass it in as a BuildKit secret.
Use the bundled composite action — pure-Python, no uv and no Azure CLI. It
masks the token and exposes it as step outputs (token and a ready-to-splice
secret-arg); the token is not written to $GITHUB_ENV, so it stays scoped
to the build step:
jobs:
build:
permissions:
id-token: write
contents: read
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Mint feed token
id: mint
uses: microsoft/artifacts-keyring-nofuss@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant: ${{ secrets.AZURE_TENANT_ID }}
- name: Build image
env:
ARTIFACTS_KEYRING_NOFUSS_TOKEN: ${{ steps.mint.outputs.token }}
run: |
DOCKER_BUILDKIT=1 docker buildx build \
${{ steps.mint.outputs.secret-arg }} \
-t myorg/my-image .
See Docker builds for the matching Dockerfile. The action fetches
the OIDC token itself, so azure/login is not required — add it only if other
steps need an authenticated az (its exported AZURE_CLIENT_ID /
AZURE_TENANT_ID are then picked up and the with: inputs can be omitted).
Action inputs & outputs¶
| Input | Required | Description |
|---|---|---|
client-id |
No | Federated identity's client ID. Defaults to AZURE_CLIENT_ID. |
tenant |
No | Azure AD tenant ID. Defaults to AZURE_TENANT_ID. |
resource |
No | Resource to scope the token to. Defaults to the Azure DevOps resource. |
| Output | Description |
|---|---|
token |
The minted bearer token (masked in logs). |
secret-arg |
A ready-to-splice --secret id=…,env=… argument for docker buildx build. |
Prefer to mint inline?¶
If you'd rather not use the action (needs uv on the runner):
- uses: astral-sh/setup-uv@v6
- name: Mint feed token
env:
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
run: |
TOKEN=$(uvx --from artifacts-keyring-nofuss ak-nofuss mint-token)
echo "::add-mask::$TOKEN"
{
echo "ARTIFACTS_KEYRING_NOFUSS_TOKEN<<EOF"
echo "$TOKEN"
echo "EOF"
} >> "$GITHUB_ENV"
Always ::add-mask:: the token (the action does this for you). This form writes
$GITHUB_ENV, exposing the token to every later step in the job — prefer the
action's scoped env: pattern unless you need the job-wide value.