Skip to content

v1.4.10

v1.4.10 #27

Workflow file for this run

name: release
# Automated PyPI publishing for django-admin-react.
#
# WHY: every release was a manual local run of `scripts/build.sh` +
# `poetry publish` with the long-lived PyPI token in `./.env` on the
# maintainer's laptop — so cadence depended on a human remembering. This
# workflow makes a release a single GitHub click: publish a Release and the
# matching wheel/sdist ship to PyPI automatically.
#
# SECURITY POSTURE:
# - **No long-lived tokens stored anywhere.** PyPI auth uses
# **Trusted Publishing (OIDC)** — PyPI verifies the workflow's
# short-lived OIDC identity at upload time. The maintainer's `./.env`
# `POETRY_PYPI_TOKEN_PYPI` stays purely local for the manual fallback
# path; it is never copied into GitHub Secrets and never echoed.
# - Trigger is `release: published` — a human still authorises every
# publish (preserves the Tier-6 "human triggers the release" rule); the
# Release notes double as the changelog entry.
# - **Idempotent publish.** Before uploading, the job queries PyPI's JSON
# API for the current version. If the wheel + sdist for that version
# already exist (e.g. the maintainer published manually first, or a
# previous workflow run partially succeeded), the upload step is
# skipped and the deployment is still marked **green**. Releases never
# fail just because the artifact is already where it should be — and
# the idempotency check itself requires NO auth, so the widget goes
# green for already-published versions even before #564 is set up.
# - Least-privilege: top-level `contents: read`; only the publish job
# gets `id-token: write`, scoped to the `pypi` environment.
# - All third-party actions are pinned to a full commit SHA (a tag can be
# moved, a SHA cannot) per the supply-chain hardening in issue #331.
#
# ONE-TIME OWNER SETUP (required before the workflow can perform a FRESH
# upload — already-published versions go green without it via the
# idempotency guard above):
# 1. PyPI → project `django-admin-react` → Settings → Publishing → add a
# "Trusted Publisher":
# Owner: MartinCastroAlvarez
# Repository: django-admin-react
# Workflow: release.yml
# Environment: pypi
# 2. GitHub → repo Settings → Environments → create `pypi` (optionally
# add required reviewers so a release is approval-gated), and
# `testpypi`.
# See SECURITY.md §7 for the rationale. Tracked in #564.
on:
release:
types: [published]
workflow_dispatch:
inputs:
target:
description: "Publish target"
type: choice
default: testpypi
options:
- testpypi
- pypi
permissions:
contents: read
jobs:
build:
name: Build SPA + wheel/sdist
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up pnpm
uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0
with:
version: 9
- name: Set up Node
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: 22
cache: pnpm
cache-dependency-path: frontend/pnpm-lock.yaml
- name: Set up Python
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
with:
python-version: "3.12"
- name: Install Poetry
run: pipx install poetry
- name: Build (Vite SPA bundle, then wheel + sdist)
run: bash scripts/build.sh
- name: Upload distributions
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: dist
path: dist/
if-no-files-found: error
publish-pypi:
name: Publish to PyPI (Trusted Publishing)
needs: build
runs-on: ubuntu-latest
# GitHub Release publish, or an explicit manual run targeting pypi.
if: github.event_name == 'release' || inputs.target == 'pypi'
environment:
name: pypi
url: https://pypi.org/project/django-admin-react/
permissions:
id-token: write # OIDC for Trusted Publishing — no stored token
steps:
- name: Checkout (for pyproject version read)
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Download distributions
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with:
name: dist
path: dist/
# Idempotency guard — never fail just because the artifact is already
# on PyPI. The maintainer's manual `.env` publish path occasionally
# ships first; in that case this workflow is a no-op that still marks
# the GitHub Deployment green so the repo widget reflects reality.
# This step calls only the public PyPI JSON API — no auth, no token.
- name: Is this version already on PyPI?
id: already
run: |
set -euo pipefail
VERSION=$(grep -E '^version = ' pyproject.toml | head -1 | sed -E 's/version = "(.*)"/\1/')
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
if curl -fsS "https://pypi.org/pypi/django-admin-react/${VERSION}/json" >/dev/null 2>&1; then
echo "Found django-admin-react ${VERSION} on PyPI — skipping upload."
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "django-admin-react ${VERSION} not on PyPI yet — proceeding with OIDC upload."
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
- name: Publish to PyPI (Trusted Publishing)
if: steps.already.outputs.skip != 'true'
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
with:
packages-dir: dist/
publish-testpypi:
name: Publish to TestPyPI (Trusted Publishing)
needs: build
runs-on: ubuntu-latest
# Manual dry-runs only — keeps a safe rehearsal path off PyPI.
if: github.event_name == 'workflow_dispatch' && inputs.target == 'testpypi'
environment:
name: testpypi
url: https://test.pypi.org/project/django-admin-react/
permissions:
id-token: write
steps:
- name: Download distributions
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with:
name: dist
path: dist/
- name: Publish to TestPyPI (Trusted Publishing)
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
with:
packages-dir: dist/
repository-url: https://test.pypi.org/legacy/