Skip to content
Merged
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
14 changes: 9 additions & 5 deletions .github/workflows/github-pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,20 @@ concurrency:

jobs:
deploy:
if: github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@v7
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
with:
python-version: "3.12"

Expand All @@ -39,13 +43,13 @@ jobs:
run: mkdocs build

- name: Setup Pages
uses: actions/configure-pages@v6
uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6

- name: Upload artifact
uses: actions/upload-pages-artifact@v5
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5
with:
path: "./site"

- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v5
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5
26 changes: 17 additions & 9 deletions .github/workflows/live-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ name: Live API Tests
# secret. Runs unconditionally on every push/PR, so route health and
# envelope-shape coverage can never silently skip (this tier caught
# the 442->436 catalog change keyless).
# 2. Keyed live tests — auth path + gated endpoints, only when the
# OILPRICEAPI_TEST_KEY secret is available (skips loudly on forks).
# 2. Keyed live tests — auth path + gated endpoints, required for exact
# default-branch code and never exposed to pull requests or other refs.

on:
push:
Expand All @@ -16,16 +16,22 @@ on:
branches: [main]
workflow_dispatch: {}

permissions:
contents: read

jobs:
live-tests:
name: Live API tests
runs-on: ubuntu-latest
timeout-minutes: 15

steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@v7
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
with:
python-version: "3.12"

Expand All @@ -38,24 +44,26 @@ jobs:
- name: Keyless demo smoke (always runs)
run: pytest tests/integration/test_demo_contract.py -m live --no-cov -v

# Tier 2: full live suite, gated on the repo secret (forks skip loudly).
# Tier 2: full live suite is required only for protected default-branch code.
- name: Keyed live tests
if: github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
env:
OILPRICEAPI_TEST_KEY: ${{ secrets.OILPRICEAPI_TEST_KEY }}
run: |
if [ -z "$OILPRICEAPI_TEST_KEY" ]; then
echo "::warning::OILPRICEAPI_TEST_KEY not available (fork?); keyed live tests skipped. Keyless demo smoke above still ran."
exit 0
echo "::error::OILPRICEAPI_TEST_KEY is required for default-branch live tests"
exit 1
fi
pytest tests/integration -m live --no-cov -v

- name: Run canonical success snippets against production
if: github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
env:
OILPRICEAPI_KEY: ${{ secrets.OILPRICEAPI_TEST_KEY }}
run: |
if [ -z "$OILPRICEAPI_KEY" ]; then
echo "::warning::OILPRICEAPI_TEST_KEY not available; canonical snippet smoke skipped."
exit 0
echo "::error::OILPRICEAPI_TEST_KEY is required for default-branch snippet smoke"
exit 1
fi
python examples/snippets/latest_price.py
python examples/snippets/history.py
211 changes: 178 additions & 33 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,33 +8,52 @@ permissions:
contents: read

jobs:
test:
name: Run Tests Before Publish
verify:
name: Verify release candidate
if: github.event.release.prerelease == false
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read

steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@v7
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
with:
python-version: "3.12"

- name: Install dependencies
- name: Install verification dependencies
run: |
python -m pip install --upgrade pip
pip install -e '.[dev]' pip-audit
python -m pip install -e '.[dev]' pip-audit build 'jsonschema>=4.17,<4.24'

- name: Verify release tag matches package version
- name: Verify release tag matches package version and protected main
env:
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: |
PACKAGE_VERSION="$(python -c 'from oilpriceapi.version import SDK_VERSION; print(SDK_VERSION)')"
set -euo pipefail
PACKAGE_VERSION="$(python scripts/package_version.py)"
if [ "$RELEASE_TAG" != "v$PACKAGE_VERSION" ]; then
echo "::error::Release tag $RELEASE_TAG does not match package version $PACKAGE_VERSION"
exit 1
fi

git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
RELEASE_COMMIT="$(git rev-parse "$RELEASE_TAG^{commit}")"
if [ "$RELEASE_COMMIT" != "$(git rev-parse HEAD)" ]; then
echo "::error::Checked-out commit does not match $RELEASE_TAG"
exit 1
fi
if ! git merge-base --is-ancestor "$RELEASE_COMMIT" origin/main; then
echo "::error::$RELEASE_TAG is not reachable from protected main"
exit 1
fi

- name: Lint source with ruff
run: ruff check oilpriceapi/

Expand All @@ -44,45 +63,171 @@ jobs:
- name: Audit installed dependencies
run: pip-audit

- name: Validate public storefront claims
run: python scripts/validate_storefront_claims.py

- name: Build package
run: python -m build

- name: Install and import the exact built wheel
run: ./scripts/clean-wheel-smoke.sh

- name: Build signed snippet manifest
run: |
python scripts/generate_snippet_manifest.py \
--source-commit "$(git rev-parse HEAD)" \
--output artifacts/snippets/oilpriceapi-python-snippets-v1.json

- name: Prepare checksummed release artifact
run: |
set -euo pipefail
ARTIFACT_DIR="$RUNNER_TEMP/release-artifact"
mkdir -p "$ARTIFACT_DIR/dist" "$ARTIFACT_DIR/snippets"
cp dist/* "$ARTIFACT_DIR/dist/"
cp artifacts/snippets/* "$ARTIFACT_DIR/snippets/"
PACKAGE_VERSION="$(python scripts/package_version.py)"
printf 'PACKAGE_VERSION=%s\n' "$PACKAGE_VERSION" > "$ARTIFACT_DIR/release.env"
(
cd "$ARTIFACT_DIR"
find dist snippets -type f -print0 \
| sort -z \
| xargs -0 sha256sum > artifact.sha256
sha256sum release.env >> artifact.sha256
)

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: verified-pypi-package
path: ${{ runner.temp }}/release-artifact/
if-no-files-found: error
retention-days: 1

publish:
name: Publish to PyPI
needs: test
name: Publish verified package to PyPI
needs: verify
if: github.event.release.prerelease == false
runs-on: ubuntu-latest
timeout-minutes: 15
environment: pypi
permissions:
id-token: write # Required for trusted publishing
contents: write
contents: read
id-token: write

steps:
- uses: actions/checkout@v7

- name: Set up Python
uses: actions/setup-python@v7
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
python-version: "3.12"
name: verified-pypi-package
path: ${{ runner.temp }}/release-artifact

- name: Install build dependencies
- name: Verify exact artifact checksums
working-directory: ${{ runner.temp }}/release-artifact
run: |
python -m pip install --upgrade pip
pip install build 'jsonschema>=4.17,<4.24'
set -euo pipefail
manifest_files="$RUNNER_TEMP/manifest-files"
actual_files="$RUNNER_TEMP/actual-files"
sed -n 's/^[0-9a-f]\{64\} //p' artifact.sha256 \
| LC_ALL=C sort > "$manifest_files"
{
find dist snippets -type f -print
printf '%s\n' release.env
} | LC_ALL=C sort > "$actual_files"
if [ -n "$(find dist snippets -type l -print -quit)" ]; then
echo "::error::Verified release artifact contains a symlink"
exit 1
fi
if ! cmp -s "$manifest_files" "$actual_files"; then
echo "::error::Checksum manifest does not cover the exact release files"
exit 1
fi
sha256sum -c artifact.sha256

- name: Build package
run: python -m build
- name: Publish exact verified distributions
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1 2026-07-28
with:
packages-dir: ${{ runner.temp }}/release-artifact/dist/
skip-existing: true
Comment thread
coderabbitai[bot] marked this conversation as resolved.

- name: Install and import the exact built wheel
run: ./scripts/clean-wheel-smoke.sh
readback:
name: Verify public PyPI artifact hashes
needs: publish
if: github.event.release.prerelease == false
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read

- name: Build signed snippet manifest
steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: verified-pypi-package
path: ${{ runner.temp }}/release-artifact

- name: Verify exact public PyPI hashes
working-directory: ${{ runner.temp }}/release-artifact
run: |
python scripts/generate_snippet_manifest.py \
--source-commit "$GITHUB_SHA" \
--output artifacts/snippets/oilpriceapi-python-snippets-v1.json
set -euo pipefail
sha256sum -c artifact.sha256
PACKAGE_VERSION="$(sed -n 's/^PACKAGE_VERSION=//p' release.env)"
if ! printf '%s' "$PACKAGE_VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "::error::Invalid package version in verified artifact"
exit 1
fi

for attempt in $(seq 1 24); do
curl --fail --silent --show-error --max-time 10 \
"https://pypi.org/pypi/oilpriceapi/$PACKAGE_VERSION/json" \
> "$RUNNER_TEMP/pypi.json" || true
all_present=true
for file in dist/*; do
filename="$(basename "$file")"
expected="$(sha256sum "$file" | cut -d' ' -f1)"
actual="$(jq -r --arg filename "$filename" \
'[.urls[]? | select(.filename == $filename) | .digests.sha256][0] // empty' \
"$RUNNER_TEMP/pypi.json" 2>/dev/null || true)"
if [ -z "$actual" ]; then
all_present=false
elif [ "$actual" != "$expected" ]; then
echo "::error::PyPI $filename has an unexpected immutable hash"
exit 1
fi
done
if [ "$all_present" = true ]; then
echo "Verified every public oilpriceapi $PACKAGE_VERSION distribution hash."
exit 0
fi
if [ "$attempt" -lt 6 ]; then
sleep_seconds=$((attempt * 2))
else
sleep_seconds=10
fi
sleep "$sleep_seconds"
done

echo "::error::PyPI public readback did not expose every verified distribution"
exit 1

release_assets:
name: Attach verified release assets
needs: readback
if: github.event.release.prerelease == false
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write

steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: verified-pypi-package
path: ${{ runner.temp }}/release-artifact

- name: Attach snippet manifest to release
- name: Attach checksummed snippet manifest
working-directory: ${{ runner.temp }}/release-artifact
env:
GH_TOKEN: ${{ github.token }}
run: gh release upload "${{ github.event.release.tag_name }}" artifacts/snippets/* --clobber

- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
GH_REPO: ${{ github.repository }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: |
set -euo pipefail
sha256sum -c artifact.sha256
gh release upload "$RELEASE_TAG" snippets/* artifact.sha256 --clobber
Loading