Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions .github/actions/artifactory-oidc/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: "Artifactory OIDC Auth"
description: "Exchange GitHub OIDC token for Artifactory access token and configure uv/pip"

inputs:
artifactory-url:
description: "JFrog platform base URL for OIDC token exchange. Falls back to ARTIFACTORY_URL env var."
required: false
default: ""
pypi-index-url:
description: "Full PyPI index base URL (without /simple). Falls back to ARTIFACTORY_PYPI_URL env var, then derived from artifactory-url."
required: false
default: ""
oidc-provider-name:
description: "OIDC provider name configured in Artifactory"
required: false
default: "github-actions"
pypi-repo:
description: "Artifactory virtual PyPI repository name (used only when deriving index URL from artifactory-url)"
required: false
default: "virtual-pypi-thirdparty"

runs:
using: "composite"
steps:
- name: Exchange GitHub OIDC token for Artifactory token
shell: bash
env:
INPUT_ARTIFACTORY_URL: ${{ inputs.artifactory-url }}
INPUT_PYPI_INDEX_URL: ${{ inputs.pypi-index-url }}
OIDC_PROVIDER_NAME: ${{ inputs.oidc-provider-name }}
PYPI_REPO: ${{ inputs.pypi-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="<redacted>" else . end' 2>/dev/null || echo "$RESP"
exit 1
fi
echo "::add-mask::$ART_TOKEN"

# Determine the PyPI index base URL:
# 1. explicit input, 2. ARTIFACTORY_PYPI_URL env var, 3. derived from ARTIFACTORY_URL
PYPI_BASE="${INPUT_PYPI_INDEX_URL:-${ARTIFACTORY_PYPI_URL:-}}"
if [ -z "$PYPI_BASE" ]; then
HOST=$(echo "${ARTIFACTORY_URL}" | sed -E 's#^https?://##')
PYPI_BASE="https://${HOST}/artifactory/api/pypi/${PYPI_REPO}"
fi
PYPI_BASE="${PYPI_BASE%/}" # strip trailing slash

INDEX_URL="https://:${ART_TOKEN}@$(echo "${PYPI_BASE}" | sed -E 's#^https?://##')/simple/"

echo "::add-mask::${INDEX_URL}"
echo "UV_INDEX_URL=${INDEX_URL}" >> "$GITHUB_ENV"
echo "PIP_INDEX_URL=${INDEX_URL}" >> "$GITHUB_ENV"
echo "Configured PyPI index through Artifactory"
86 changes: 86 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
name: CI

on:
push:
branches: [master]
pull_request:
workflow_dispatch:

permissions:
id-token: write
contents: read

env:
ARTIFACTORY_URL: ${{ vars.ARTIFACTORY_URL }}

jobs:
lint:
name: Lint
runs-on: ${{ github.event.pull_request.head.repo.fork && 'ubuntu-latest' || 'ubuntu-x64' }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Authenticate with Artifactory
if: ${{ !github.event.pull_request.head.repo.fork }}
uses: ./.github/actions/artifactory-oidc
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.11"
- run: pip install uv==0.9.26
- run: uv sync --frozen --all-extras
- run: uv run ruff check .
- run: uv run ruff format --check .

test:
name: Test - Python ${{ matrix.python-version }}
runs-on: ${{ github.event.pull_request.head.repo.fork && 'ubuntu-latest' || 'ubuntu-x64' }}
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Authenticate with Artifactory
if: ${{ !github.event.pull_request.head.repo.fork }}
uses: ./.github/actions/artifactory-oidc
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: ${{ matrix.python-version }}
- run: pip install uv==0.9.26
- run: uv sync --frozen --all-extras
- run: uv run pytest

build:
name: Build Package
runs-on: ${{ github.event.pull_request.head.repo.fork && 'ubuntu-latest' || 'ubuntu-x64' }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Authenticate with Artifactory
if: ${{ !github.event.pull_request.head.repo.fork }}
uses: ./.github/actions/artifactory-oidc
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.11"
- run: pip install uv==0.9.26
- name: Build package
run: uv build
- name: Verify installable
run: |
python -m venv test-env
. test-env/bin/activate
pip install dist/*.whl
python -c "import segment.analytics; print(f'OK: {segment.analytics.__version__}')"

all-checks:
name: All Checks Passed
needs: [lint, test, build]
runs-on: ubuntu-latest
if: always()
steps:
- name: Check all job statuses
run: |
if [ "${{ needs.lint.result }}" != "success" ] || \
[ "${{ needs.test.result }}" != "success" ] || \
[ "${{ needs.build.result }}" != "success" ]; then
echo "One or more checks failed"
exit 1
fi
80 changes: 80 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
name: Deploy

on:
release:
types: [published]

env:
ARTIFACTORY_URL: ${{ vars.ARTIFACTORY_URL }}

jobs:
test:
name: Test - Python ${{ matrix.python-version }}
runs-on: ${{ github.repository_owner == 'twilio' && 'ubuntu-x64' || 'ubuntu-latest' }}
if: github.repository_owner == 'twilio'
permissions:
contents: read
id-token: write
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Authenticate with Artifactory
uses: ./.github/actions/artifactory-oidc
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: ${{ matrix.python-version }}
- run: pip install uv==0.9.26
- run: uv sync --frozen --all-extras
- run: uv run pytest

deploy:
name: Publish to PyPI
needs: [test]
runs-on: ${{ github.repository_owner == 'twilio' && 'ubuntu-x64' || 'ubuntu-latest' }}
if: github.repository_owner == 'twilio'
environment: pypi
permissions:
contents: read
id-token: write
attestations: write
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Authenticate with Artifactory
uses: ./.github/actions/artifactory-oidc
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.12"
- run: pip install uv==0.9.26

- name: Validate tag format and version match
run: |
TAG="${GITHUB_REF#refs/tags/}"
if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+ ]]; then
echo "::error::Release tag must be in the form v1.2.3 (got '$TAG')"
exit 1
fi
VERSION="${TAG#v}"
PKG_VERSION=$(python -c "
import tomllib
with open('pyproject.toml', 'rb') as f:
print(tomllib.load(f)['project']['version'])
")
if [ "$VERSION" != "$PKG_VERSION" ]; then
echo "::error::Tag $TAG does not match pyproject.toml version $PKG_VERSION"
exit 1
fi

- name: Build package
run: uv build

- name: Publish to PyPI
run: |
pip install twine
twine upload --repository pypi dist/*
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
38 changes: 16 additions & 22 deletions .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
@@ -1,52 +1,46 @@
# E2E Tests for analytics-python
# Copy this file to: analytics-python/.github/workflows/e2e-tests.yml
#
# This workflow:
# 1. Checks out the SDK and sdk-e2e-tests repos
# 2. Installs the SDK and e2e-cli dependencies
# 3. Runs the e2e test suite

name: E2E Tests

on:
push:
branches: [main, master]
branches: [master]
pull_request:
branches: [main, master]
branches: [master]
workflow_dispatch:
inputs:
e2e_tests_ref:
description: 'Branch or ref of sdk-e2e-tests to use'
required: false
default: 'main'

permissions:
id-token: write
contents: read

env:
ARTIFACTORY_URL: ${{ vars.ARTIFACTORY_URL }}

jobs:
e2e-tests:
# Skip on fork PRs where repo secrets aren't available
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
runs-on: ubuntu-latest

runs-on: ${{ github.event.pull_request.head.repo.fork && 'ubuntu-latest' || 'ubuntu-x64' }}
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork }}
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 Python
uses: actions/setup-python@v5
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: '3.11'

- name: Setup Node.js
uses: actions/setup-node@v4
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
with:
node-version: '20'

Expand All @@ -67,7 +61,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/
Expand Down
57 changes: 0 additions & 57 deletions .github/workflows/main.yml

This file was deleted.

Loading
Loading