Skip to content

v3.7.3

v3.7.3 #23

Workflow file for this run

# Publish workflow.
# Purpose: build and publish artifacts when a GitHub release is published.
# This workflow does not compute versions or create tags.
name: "Publish"
on:
release:
types:
- published
concurrency:
# Preserve release order on PyPI: publish runs execute one-by-one.
group: publish-pypi
cancel-in-progress: false
permissions:
contents: read
id-token: write
env:
PYTHON_VERSION: "3.11"
POETRY_VERSION: "2.4.1"
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
TRUSTED_RELEASE_ACTORS: "vstack-release-bot[bot],eschaar"
HOMEBREW_TAP_ENABLED: "false"
HOMEBREW_TAP_NAME: "eschaar/vstack"
HOMEBREW_TAP_REPOSITORY: "eschaar/homebrew-vstack"
HOMEBREW_TAP_REPOSITORY_OWNER: "eschaar"
HOMEBREW_TAP_REPOSITORY_NAME: "homebrew-vstack"
HOMEBREW_FORMULA_NAME: "vstack"
HOMEBREW_FULLY_QUALIFIED_FORMULA: "eschaar/vstack/vstack"
jobs:
publish:
name: Build and Publish to PyPI
if: github.event.release.prerelease == false
runs-on: ubuntu-latest
environment: pypi
env:
PYPI_API_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
steps:
- name: Validate release tag format
# Fail fast before checkout if the tag is not a plain SemVer X.Y.Z.
# Prevents non-versioned or pre-release tags from reaching the publish step.
shell: bash
run: |
TAG="${{ github.event.release.tag_name }}"
if [[ ! "$TAG" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "ERROR: release tag '$TAG' is not in expected SemVer X.Y.Z format."
echo "Only plain version tags (e.g. 1.2.3) are permitted to trigger publish."
exit 1
fi
echo "tag=$TAG format is valid."
- name: Validate release actor
# Only allow trusted actors to trigger a PyPI publish.
# vstack-release-bot[bot] covers release-please GitHub App automation.
# eschaar covers direct maintainer releases.
shell: bash
run: |
ACTOR="${{ github.event.release.author.login }}"
IFS=',' read -r -a ALLOWED <<< "$TRUSTED_RELEASE_ACTORS"
TRUSTED=false
for allowed_actor in "${ALLOWED[@]}"; do
if [[ "$ACTOR" == "$allowed_actor" ]]; then
TRUSTED=true
break
fi
done
if [[ "$TRUSTED" != "true" ]]; then
echo "ERROR: release created by '$ACTOR' which is not in the trusted actor list."
echo "Allowed: $TRUSTED_RELEASE_ACTORS"
echo "Releases must be created by vstack-release-bot automation or a trusted maintainer."
exit 1
fi
echo "release_actor='$ACTOR' is trusted."
- name: Checkout release tag
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
with:
ref: refs/tags/${{ github.event.release.tag_name }}
fetch-depth: 0
- name: Install Poetry
run: pipx install "poetry==${POETRY_VERSION}"
- name: Install poetry-dynamic-versioning plugin
run: pipx inject poetry "poetry-dynamic-versioning[plugin]>=1.10.0,<2.0.0"
- name: Setup Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Validate release tag checkout
run: |
EXPECTED="${{ github.event.release.tag_name }}"
ACTUAL_TAG="$(git tag --points-at HEAD | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -1)"
echo "expected_tag=$EXPECTED"
echo "head_tag=$ACTUAL_TAG"
if [[ "$ACTUAL_TAG" != "$EXPECTED" ]]; then
echo "ERROR: build checkout is not pinned to expected release tag."
exit 1
fi
- name: Build distributions
run: poetry build
- name: Smoke test built wheel
run: |
python -m pip install --upgrade pip
python -m pip install dist/*.whl
vstack --help >/dev/null
- name: Validate built artifact version
run: |
EXPECTED="${{ github.event.release.tag_name }}"
shopt -s nullglob
MATCHES=(dist/*"$EXPECTED"*.whl dist/*"$EXPECTED"*.tar.gz)
echo "expected_version=$EXPECTED"
ls -1 dist/
if [[ ${#MATCHES[@]} -eq 0 ]]; then
echo "ERROR: no built artifacts contain expected version '$EXPECTED'."
exit 1
fi
- name: Publish to PyPI (trusted publishing)
id: publish_trusted
continue-on-error: true
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b
- name: Publish to PyPI (API token fallback)
if: steps.publish_trusted.outcome == 'failure' && env.PYPI_API_TOKEN != ''
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b
with:
user: __token__
password: ${{ env.PYPI_API_TOKEN }}
- name: Fail when trusted publishing fails and no fallback token exists
if: steps.publish_trusted.outcome == 'failure' && env.PYPI_API_TOKEN == ''
shell: bash
run: |
echo "ERROR: trusted publishing failed and secret PYPI_API_TOKEN is not configured."
echo "Either fix PyPI trusted publisher mapping or add PYPI_API_TOKEN as a fallback."
exit 1
publish-homebrew:
name: Update Homebrew Tap
needs: publish
# NOTE: job-level `if` cannot use the `env` context. Toggle via repository variable
# HOMEBREW_TAP_ENABLED (value: "true" to enable). Default/absent keeps this disabled.
if: needs.publish.result == 'success' && github.event.release.prerelease == false && vars.HOMEBREW_TAP_ENABLED == 'true'
runs-on: ubuntu-latest
environment: homebrew
env:
HOMEBREW_TAP_APP_CLIENT_ID: ${{ secrets.HOMEBREW_TAP_APP_CLIENT_ID }}
HOMEBREW_TAP_APP_PRIVATE_KEY: ${{ secrets.HOMEBREW_TAP_APP_PRIVATE_KEY }}
HOMEBREW_TAP_DISPATCH_SECRET: ${{ secrets.HOMEBREW_TAP_DISPATCH_SECRET }}
steps:
- name: Validate release actor
# Restrict dispatch to trusted release automation/maintainers only.
shell: bash
run: |
ACTOR="${{ github.event.release.author.login }}"
IFS=',' read -r -a ALLOWED <<< "$TRUSTED_RELEASE_ACTORS"
TRUSTED=false
for allowed_actor in "${ALLOWED[@]}"; do
if [[ "$ACTOR" == "$allowed_actor" ]]; then
TRUSTED=true
break
fi
done
if [[ "$TRUSTED" != "true" ]]; then
echo "ERROR: release created by '$ACTOR' which is not in the trusted actor list."
echo "Allowed: $TRUSTED_RELEASE_ACTORS"
exit 1
fi
- name: Verify Homebrew tap dispatch configuration
shell: bash
run: |
if [[ -z "$HOMEBREW_TAP_APP_CLIENT_ID" ]]; then
echo "ERROR: HOMEBREW_TAP_APP_CLIENT_ID is not configured in the pypi environment."
exit 1
fi
if [[ -z "$HOMEBREW_TAP_APP_PRIVATE_KEY" ]]; then
echo "ERROR: HOMEBREW_TAP_APP_PRIVATE_KEY is not configured in the pypi environment."
exit 1
fi
if [[ -z "$HOMEBREW_TAP_DISPATCH_SECRET" ]]; then
echo "ERROR: HOMEBREW_TAP_DISPATCH_SECRET is not configured in the pypi environment."
exit 1
fi
- name: Create Homebrew tap app token
id: create_tap_token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1
with:
client-id: ${{ env.HOMEBREW_TAP_APP_CLIENT_ID }}
private-key: ${{ env.HOMEBREW_TAP_APP_PRIVATE_KEY }}
owner: ${{ env.HOMEBREW_TAP_REPOSITORY_OWNER }}
repositories: ${{ env.HOMEBREW_TAP_REPOSITORY_NAME }}
- name: Fetch sdist metadata and verify checksum
id: verify_sdist
shell: bash
run: |
python - <<'PY'
import hashlib
import json
import os
import sys
import urllib.request
version = os.environ["GITHUB_REF_NAME"]
pypi_json_url = f"https://pypi.org/pypi/vstack/{version}/json"
with urllib.request.urlopen(pypi_json_url) as response:
metadata = json.load(response)
sdist = next((item for item in metadata.get("urls", []) if item.get("packagetype") == "sdist"), None)
if sdist is None:
print("ERROR: no sdist artifact found in PyPI metadata for the release version.", file=sys.stderr)
sys.exit(1)
sdist_url = sdist["url"]
pypi_sha = sdist.get("digests", {}).get("sha256", "")
if not pypi_sha:
print("ERROR: PyPI metadata did not provide an sdist sha256 digest.", file=sys.stderr)
sys.exit(1)
with urllib.request.urlopen(sdist_url) as response:
content = response.read()
local_sha = hashlib.sha256(content).hexdigest()
if local_sha != pypi_sha:
print("ERROR: sha256 mismatch between PyPI metadata and downloaded tarball.", file=sys.stderr)
sys.exit(1)
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output:
output.write(f"sdist_url={sdist_url}\n")
output.write(f"sdist_sha256={local_sha}\n")
PY
env:
GITHUB_REF_NAME: ${{ github.event.release.tag_name }}
- name: Dispatch formula update to Homebrew tap
shell: bash
env:
RELEASE_VERSION: ${{ github.event.release.tag_name }}
SDIST_URL: ${{ steps.verify_sdist.outputs.sdist_url }}
SDIST_SHA256: ${{ steps.verify_sdist.outputs.sdist_sha256 }}
run: |
python - <<'PY'
import hmac
import json
import os
from hashlib import sha256
payload = {
"version": os.environ["RELEASE_VERSION"],
"sdist_url": os.environ["SDIST_URL"],
"sha256": os.environ["SDIST_SHA256"],
}
secret = os.environ.get("HOMEBREW_TAP_DISPATCH_SECRET", "")
if secret:
canonical = json.dumps(payload, separators=(",", ":"), sort_keys=True)
payload["signature"] = hmac.new(secret.encode("utf-8"), canonical.encode("utf-8"), sha256).hexdigest()
request_body = {
"event_type": "update-formula",
"client_payload": payload,
}
with open("dispatch-body.json", "w", encoding="utf-8") as output:
json.dump(request_body, output, separators=(",", ":"))
PY
curl -fsS -X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${{ steps.create_tap_token.outputs.token }}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"https://api.github.com/repos/${HOMEBREW_TAP_REPOSITORY}/dispatches" \
--data-binary @dispatch-body.json
- name: Publish Homebrew install UX summary
shell: bash
run: |
{
echo "## Homebrew install UX"
echo
echo "- First-time users (private tap): \`brew tap ${HOMEBREW_TAP_NAME} && brew install ${HOMEBREW_FORMULA_NAME}\`"
echo "- Returning users (tap already configured): \`brew install ${HOMEBREW_FORMULA_NAME}\`"
echo "- Fully-qualified one-liner (no prior tap): \`brew install ${HOMEBREW_FULLY_QUALIFIED_FORMULA}\`"
echo
echo "Plain \`brew install ${HOMEBREW_FORMULA_NAME}\` without tapping cannot be guaranteed while distribution is private-tap only."
echo "That UX becomes universal only after acceptance into Homebrew/homebrew-core."
} >> "$GITHUB_STEP_SUMMARY"