Skip to content

v0.10.0-prepare-validate-publish-batch-5 #5

v0.10.0-prepare-validate-publish-batch-5

v0.10.0-prepare-validate-publish-batch-5 #5

name: v0.10.0-Mono-Prepare-Validate-Publish-Batch
run-name: v0.10.0-prepare-validate-publish-batch-${{ github.event.inputs.batch_index||'na' }}
on:
workflow_dispatch:
inputs:
members_json:
description: "JSON array of workspace members to process"
required: true
batch_index:
description: "Batch index for logging"
required: false
batch_label:
description: "Batch label for logging"
required: false
bump_type:
description: "Bump type {major,minor,patch,finalize}"
required: false
default: "patch"
set_version:
description: "Version (e.g. 0.2.0 or 0.2.0.dev1)"
required: false
concurrency:
group: v0.10.0-mono-prepare-validate-publish-batch-${{ github.run_id }}
cancel-in-progress: false
permissions:
contents: write
deployments: write
jobs:
set-matrix:
if: ${{ !cancelled() }}
runs-on: ubuntu-latest
timeout-minutes: 15
outputs:
matrix: ${{ steps.get-matrix.outputs.matrix }}
approval_matrix: ${{ steps.get-matrix.outputs.approval_matrix }}
approval_required: ${{ steps.get-matrix.outputs.approval_required }}
has_members: ${{ steps.get-matrix.outputs.has_members }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Install dependencies for matrix generation
run: pip install toml
- name: Get workspace members
id: get-matrix
env:
MEMBERS_JSON: ${{ github.event.inputs.members_json }}
run: |
cd pkgs
python - <<'PY'
import json
import os
import toml
import urllib.error
import urllib.request
members = json.loads(os.environ["MEMBERS_JSON"])
if not isinstance(members, list):
raise RuntimeError("members_json must be a JSON list.")
members = list(dict.fromkeys(members))
matrix_entries = []
approval_entries = []
for member in members:
project_path = os.path.join(member, "pyproject.toml")
with open(project_path, "r") as member_file:
member_config = toml.load(member_file)
package_name = member_config.get("project", {}).get("name")
if not package_name:
raise RuntimeError(f"Unable to determine package name for member '{member}'.")
requires_approval = True
try:
with urllib.request.urlopen(f"https://pypi.org/pypi/{package_name}/json") as response:
requires_approval = response.status != 200
except urllib.error.HTTPError as exc:
if exc.code == 404:
requires_approval = True
else:
raise
entry = {
"member": member,
"package_name": package_name,
"requires_approval": "true" if requires_approval else "false",
}
matrix_entries.append(entry)
if requires_approval:
approval_entries.append(entry)
matrix = {"include": matrix_entries}
approval_matrix = {"include": approval_entries}
approval_required = "true" if approval_entries else "false"
has_members = "true" if members else "false"
with open(os.environ["GITHUB_OUTPUT"], "a") as fh:
print(f"matrix={json.dumps(matrix)}", file=fh)
print(f"approval_matrix={json.dumps(approval_matrix)}", file=fh)
print(f"approval_required={approval_required}", file=fh)
print(f"has_members={has_members}", file=fh)
print(f"Prepared matrix for {len(members)} members.")
PY
test:
needs: set-matrix
if: ${{ !cancelled() && needs.set-matrix.outputs.has_members == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
matrix: ${{ fromJson(needs.set-matrix.outputs.matrix) }}
fail-fast: false
continue-on-error: true
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Install dependencies for testing
run: pip install uv pytest toml
- name: Bump Version Placeholder
run: |
MEMBER="${{ matrix.member }}"
echo "Running tests for workspace member: $MEMBER"
PACKAGE_NAME="${{ matrix.package_name }}"
echo "Package name: $PACKAGE_NAME"
if [ -n "${{ github.event.inputs.set_version }}" ]; then
VERSION_ARG="--set ${{ github.event.inputs.set_version }}"
else
VERSION_ARG="--bump ${{ github.event.inputs.bump_type }}"
fi
uv run --active scripts/bump_pyproject_version.py $VERSION_ARG ${{ github.workspace }}/pkgs/${MEMBER}/pyproject.toml
- name: Ruff format
run: |
cd pkgs
MEMBER="${{ matrix.member }}"
echo "Running tests for workspace member: $MEMBER"
PACKAGE_NAME="${{ matrix.package_name }}"
echo "Package name: $PACKAGE_NAME"
uv run --directory "$MEMBER" --package "$PACKAGE_NAME" --isolated --active ruff format .
- name: Ruff lint check & fix
run: |
cd pkgs
MEMBER="${{ matrix.member }}"
echo "Running tests for workspace member: $MEMBER"
PACKAGE_NAME="${{ matrix.package_name }}"
echo "Package name: $PACKAGE_NAME"
uv run --directory "$MEMBER" --package "$PACKAGE_NAME" --isolated --active ruff check . --fix
- name: Run tests for member ${{ matrix.member }}
run: |
cd pkgs
MEMBER="${{ matrix.member }}"
echo "Running tests for workspace member: $MEMBER"
PACKAGE_NAME="${{ matrix.package_name }}"
echo "Package name: $PACKAGE_NAME"
uv run --directory "$MEMBER" --package "$PACKAGE_NAME" --isolated --active pytest -vvv
- name: Create patch for changes
if: always()
run: |
mkdir -p patches
MEMBER="${{ matrix.member }}"
SAFE_MEMBER=$(echo "$MEMBER" | tr '/' '-')
git diff HEAD -- pkgs/"$MEMBER" > patches/patch_${SAFE_MEMBER}.patch
- name: Set safe member variable
if: always()
id: set_safe_member
run: |
echo "SAFE_MEMBER=$(echo '${{ matrix.member }}' | tr '/' '-')" >> $GITHUB_OUTPUT
- name: Upload patch artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: patch-${{ steps.set_safe_member.outputs.SAFE_MEMBER }}
path: patches/patch_${{ steps.set_safe_member.outputs.SAFE_MEMBER }}.patch
if-no-files-found: warn
commit:
name: Commit Changes
needs: [set-matrix, test]
runs-on: ubuntu-latest
if: ${{ !cancelled() && always() && needs.set-matrix.outputs.has_members == 'true' }}
timeout-minutes: 15
outputs:
checkout_ref: ${{ steps.commit_and_push.outputs.checkout_ref }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ github.ref }}
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Download patch artifacts
uses: actions/download-artifact@v4
with:
path: patches
- name: List downloaded artifacts
run: ls -R patches || true
- name: Apply patches
run: |
find patches -type f -name "*.patch" | while read patch; do
echo "Applying patch $patch"
git apply -p1 "$patch" || echo "Warning: Patch $patch failed, continuing..."
done
rm -rf patches
- name: Configure SSH for deploy key
run: |
mkdir -p ~/.ssh
echo "${{ secrets.DEPLOY_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan github.com >> ~/.ssh/known_hosts
git remote set-url origin git@github.com:${{ github.repository }}.git
- name: Git Commit and Push
id: commit_and_push
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add .
DEFAULT_REF="${{ github.ref }}"
BRANCH_NAME="automation/batch-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.event.inputs.batch_index || 'na' }}"
if ! git diff-index --quiet HEAD; then
git checkout -B "$BRANCH_NAME"
git commit -m "chore: apply automatic formatting and lint fixes"
git push origin "HEAD:refs/heads/$BRANCH_NAME" --force
echo "checkout_ref=refs/heads/$BRANCH_NAME" >> $GITHUB_OUTPUT
else
echo "No changes to commit."
echo "checkout_ref=$DEFAULT_REF" >> $GITHUB_OUTPUT
fi
pypi_approval_gate:
name: PyPI approval gate (${{ matrix.package_name }})
needs: set-matrix
if: ${{ !cancelled() && needs.set-matrix.outputs.has_members == 'true' && needs.set-matrix.outputs.approval_required == 'true' }}
runs-on: ubuntu-latest
strategy:
matrix: ${{ fromJson(needs.set-matrix.outputs.approval_matrix) }}
fail-fast: false
max-parallel: 1
timeout-minutes: 15
environment:
# Use the shared protected environment so required reviewers are always enforced.
# Package-specific context is still visible via the job name and deployment URL.
name: pypi-new-package-approval
url: https://pypi.org/project/${{ matrix.package_name }}/
steps:
- name: Await manual approval in the environment gate
run: |
echo "::notice::Approval gate for package '${{ matrix.package_name }}' (directory: '${{ matrix.member }}'). Use the 'Review deployments' banner to Approve and deploy."
release:
needs: [commit, set-matrix, pypi_approval_gate]
runs-on: ubuntu-latest
if: ${{ !cancelled() && always() && needs.set-matrix.outputs.has_members == 'true' && (needs.set-matrix.outputs.approval_required != 'true' || needs.pypi_approval_gate.result == 'success') }}
timeout-minutes: 15
strategy:
matrix: ${{ fromJson(needs.set-matrix.outputs.matrix) }}
fail-fast: false
max-parallel: 5
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ needs.commit.outputs.checkout_ref }}
fetch-depth: 0
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Install build tooling
run: pip install uv pytest toml
- name: Build
run: |
cd pkgs
MEMBER="${{ matrix.member }}"
echo "Building workspace member: $MEMBER"
PACKAGE_NAME="${{ matrix.package_name }}"
echo "Package name: $PACKAGE_NAME"
uv build --directory "$MEMBER" --package "$PACKAGE_NAME" -o distout
- name: Publish
run: |
cd pkgs
MEMBER="${{ matrix.member }}"
echo "Publishing workspace member: $MEMBER"
PACKAGE_NAME="${{ matrix.package_name }}"
echo "Package name: $PACKAGE_NAME"
uv publish --directory "$MEMBER" distout/* --token "${{ secrets.DANGER_MASTER_PYPI_API_TOKEN }}"
- name: Prepare release variables
id: prepare_release
run: |
cd pkgs/${{ matrix.member }}
PACKAGE_NAME="${{ matrix.package_name }}"
VERSION=$(python -c "import toml; print(toml.load('pyproject.toml')['project']['version'])")
TAG="${PACKAGE_NAME}==${VERSION}"
if [ "${{ github.event.inputs.bump_type }}" = "finalize" ] || [[ "$VERSION" != *".dev"* ]]; then
PRERELEASE_FLAG=false
else
PRERELEASE_FLAG=true
fi
echo "tag=$TAG" >> $GITHUB_OUTPUT
echo "prerelease=$PRERELEASE_FLAG" >> $GITHUB_OUTPUT
echo "package_name=$PACKAGE_NAME" >> $GITHUB_OUTPUT
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Computed tag: $TAG, prerelease: $PRERELEASE_FLAG"
- name: Create Git Tag
run: |
cd pkgs/${{ matrix.member }}
git tag "${{ steps.prepare_release.outputs.tag }}"
git push origin "${{ steps.prepare_release.outputs.tag }}"
- name: Generate Release Notes
id: generate_release_notes
run: |
cd pkgs/${{ matrix.member }}
CURRENT_TAG="${{ steps.prepare_release.outputs.tag }}"
PREVIOUS_TAG=$(git tag --list "${{ steps.prepare_release.outputs.package_name }}==*" --sort=-v:refname | grep -v "^${CURRENT_TAG}$" | head -n1)
echo "Previous tag: $PREVIOUS_TAG"
if [ -n "$PREVIOUS_TAG" ]; then
RELEASE_NOTES=$(git log "${PREVIOUS_TAG}..${CURRENT_TAG}" -- . --pretty=format:"* %s" --reverse)
else
RELEASE_NOTES="Initial release."
fi
echo "release_notes<<EOF" >> $GITHUB_OUTPUT
echo "$RELEASE_NOTES" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Zip distribution artifacts
run: |
cd pkgs/${{ matrix.member }}
mkdir -p release_artifacts
ZIP_PATH=release_artifacts/${{ steps.prepare_release.outputs.package_name }}-${{ steps.prepare_release.outputs.version }}.zip
zip -j "$ZIP_PATH" distout/*.whl distout/*.tar.gz
echo "Artifacts zipped to: $ZIP_PATH"
- name: Create GitHub Release
id: create_release
uses: softprops/action-gh-release@v2.2.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ steps.prepare_release.outputs.tag }}
name: ${{ steps.prepare_release.outputs.tag }}
body: ${{ steps.generate_release_notes.outputs.release_notes }}
prerelease: ${{ steps.prepare_release.outputs.prerelease }}
files: |
pkgs/${{ matrix.member }}/release_artifacts/${{ steps.prepare_release.outputs.package_name }}-${{ steps.prepare_release.outputs.version }}.zip