Skip to content

Publish xrpl-py 🐍 distribution 📦 to PyPI #12

Publish xrpl-py 🐍 distribution 📦 to PyPI

Publish xrpl-py 🐍 distribution 📦 to PyPI #12

Workflow file for this run

name: Publish xrpl-py 🐍 distribution 📦 to PyPI
on:
workflow_dispatch:
inputs:
release_branch:
description: "Release branch to package (e.g., release/1.0.0)"
required: true
type: string
jobs:
input-validate:
name: Validate release inputs
runs-on: ubuntu-latest
outputs:
package_version: ${{ steps.package_version.outputs.version }}
is_beta_release: ${{ steps.detect_release_kind.outputs.is_beta_release }}
# release_branch output no longer needed; reference github.event.inputs.release_branch directly
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.event.inputs.release_branch }}
- name: Install toml-cli
run: |
set -euo pipefail
python3 -m venv /tmp/tomlcli
/tmp/tomlcli/bin/pip install --upgrade pip
/tmp/tomlcli/bin/pip install 'toml-cli==0.8.2'
echo "/tmp/tomlcli/bin" >> "${GITHUB_PATH}"
- name: Extract package version
id: package_version
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
rm -f /tmp/toml_err
if ! VERSION="$(toml get project.version --toml-path pyproject.toml 2>/tmp/toml_err)"; then
cat /tmp/toml_err >&2 || true
echo "Unable to retrieve version from pyproject.toml using toml-cli" >&2
exit 1
fi
rm -f /tmp/toml_err
if [[ -z "${VERSION}" ]]; then
echo "Version value is empty in pyproject.toml" >&2
exit 1
fi
# Ensure no existing remote git tag matches this version (protect against re-releases)
if gh api -X GET "repos/$REPO/git/ref/tags/${VERSION}" >/dev/null 2>&1 || \
gh api -X GET "repos/$REPO/git/ref/tags/v${VERSION}" >/dev/null 2>&1; then
echo "❌ A remote git tag matching the version already exists: '${VERSION}' or 'v${VERSION}'." >&2
echo "Please bump the version in pyproject.toml or remove the existing git tag before releasing." >&2
exit 1
fi
echo "version=${VERSION}" >> "${GITHUB_OUTPUT}"
echo "Detected package version: ${VERSION}"
- name: Determine release type
id: detect_release_kind
run: |
set -euo pipefail
VERSION="${{ steps.package_version.outputs.version }}"
if [[ "$VERSION" =~ (a|b|rc) ]]; then
echo "is_beta_release=true" >> "$GITHUB_OUTPUT"
else
echo "is_beta_release=false" >> "$GITHUB_OUTPUT"
fi
- name: Validate inputs
id: validate_inputs
env:
TRIGGER_BRANCH: ${{ github.ref_name }}
IS_BETA_RELEASE: ${{ steps.detect_release_kind.outputs.is_beta_release }}
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
RELEASE_BRANCH: ${{ github.event.inputs.release_branch }}
run: |
set -euo pipefail
if [[ "${TRIGGER_BRANCH}" != "main" ]]; then
echo "❌ This workflow must be dispatched from the main branch (current: ${TRIGGER_BRANCH})." >&2
exit 1
fi
RELEASE_BRANCH="${RELEASE_BRANCH}"
if [[ -z "$RELEASE_BRANCH" ]]; then
echo "❌ Unable to determine branch name." >&2
exit 1
fi
if [[ "${IS_BETA_RELEASE}" != "true" ]] && [[ ! "${RELEASE_BRANCH,,}" =~ ^release[-/] ]]; then
echo "❌ Release branch '$RELEASE_BRANCH' must start with 'release-' or 'release/' for stable releases." >&2
exit 1
fi
if grep -R --exclude-dir=.git --exclude-dir=.github "artifactory.ops.ripple.com" .; then
echo "❌ Internal Artifactory URL found"
exit 1
else
echo "✅ No Internal Artifactory URL found"
fi
echo "release_branch=${RELEASE_BRANCH}" >> "$GITHUB_OUTPUT"
faucet-tests:
name: Run faucet tests matrix (${{ needs.input-validate.outputs.package_version }})
needs:
- input-validate
if: ${{ needs.input-validate.outputs.is_beta_release != 'true' }}
uses: ./.github/workflows/faucet_test.yml
with:
git_ref: ${{ github.event.inputs.release_branch }}
secrets: inherit
integration-tests:
name: Run integration tests matrix (${{ needs.input-validate.outputs.package_version }})
needs:
- input-validate
uses: ./.github/workflows/integration_test.yml
with:
git_ref: ${{ github.event.inputs.release_branch }}
secrets: inherit
unit-tests:
name: Run unit tests matrix (${{ needs.input-validate.outputs.package_version }})
needs:
- input-validate
uses: ./.github/workflows/unit_test.yml
with:
git_ref: ${{ github.event.inputs.release_branch }}
secrets: inherit
pre-release:
name: Pre-release distribution 📦 (${{ needs.input-validate.outputs.package_version }})
needs:
- input-validate
- faucet-tests
- integration-tests
- unit-tests
if: ${{ always()
&& needs.input-validate.result == 'success'
&& (needs['faucet-tests'].result == 'success' || needs.input-validate.outputs.is_beta_release == 'true')
&& (needs['integration-tests'].result == 'success' || needs.input-validate.outputs.is_beta_release == 'true')
&& needs['unit-tests'].result == 'success' }}
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
attestations: write
issues: write
pull-requests: write
env:
POETRY_VERSION: 2.1.1
CYCLONEDX_BOM_VERSION: 7.2.0
PACKAGE_VERSION: ${{ needs.input-validate.outputs.package_version }}
outputs:
package_version: ${{ needs.input-validate.outputs.package_version }}
vuln_art_url: ${{ steps.vuln_art.outputs.art_url }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.event.inputs.release_branch }}
- name: Create PR from release branch to main (skips for rc/beta)
id: ensure_pr
if: ${{ needs.input-validate.outputs.is_beta_release != 'true' }}
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
RELEASE_BRANCH: ${{ github.event.inputs.release_branch }}
VERSION: ${{ needs.input-validate.outputs.package_version }}
RUN_ID: ${{ github.run_id }}
run: |
set -euo pipefail
echo "🔎 Checking if a PR already exists for $RELEASE_BRANCH → main…"
OWNER="${REPO%%/*}"
PRS_JSON="$(gh api -H 'Accept: application/vnd.github+json' \
"/repos/$REPO/pulls?state=open&base=main&head=${OWNER}:${RELEASE_BRANCH}")"
PR_NUMBER="$(printf '%s' "$PRS_JSON" | jq -r '.[0].number // empty')"
PR_URL="$(printf '%s' "$PRS_JSON" | jq -r '.[0].html_url // empty')"
if [ -n "${PR_NUMBER:-}" ]; then
echo "ℹ️ Found existing PR: #$PR_NUMBER ($PR_URL)"
echo "pr_url=$PR_URL" >> "$GITHUB_OUTPUT"
echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "📝 Creating PR for release $VERSION from $RELEASE_BRANCH → main (as draft)"
CREATE_JSON="$(jq -n \
--arg title "Release $VERSION: $RELEASE_BRANCH → main" \
--arg head "$RELEASE_BRANCH" \
--arg base "main" \
--arg body "Automated PR for release **$VERSION** from **$RELEASE_BRANCH** → **main**. Workflow Run: https://github.com/$REPO/actions/runs/$RUN_ID" \
'{title:$title, head:$head, base:$base, body:$body, draft:true}')"
RESP="$(gh api -H 'Accept: application/vnd.github+json' \
--method POST /repos/$REPO/pulls --input <(printf '%s' "$CREATE_JSON"))"
PR_NUMBER="$(printf '%s' "$RESP" | jq -r '.number')"
PR_URL="$(printf '%s' "$RESP" | jq -r '.html_url')"
echo "pr_url=$PR_URL" >> "$GITHUB_OUTPUT"
echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT"
- name: Load cached .local
id: cache-poetry
uses: actions/cache@v4
with:
path: /home/runner/.local
key: dotlocal-${{ env.POETRY_VERSION }}
- name: Ensure Poetry on PATH
run: echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- name: Install poetry
if: steps.cache-poetry.outputs.cache-hit != 'true'
run: |
python --version
curl -sSL https://install.python-poetry.org/ | python - --version ${{ env.POETRY_VERSION }}
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Install Python + Retrieve Poetry dependencies from cache
uses: actions/setup-python@v5
with:
python-version: "3.8"
cache: "poetry"
- name: Build a binary wheel and a source tarball
run: poetry build
- name: Store the distribution packages
uses: actions/upload-artifact@v4
with:
name: python-package-distributions
path: dist/
- name: Generate build provenance attestation
id: provenance
uses: actions/attest-build-provenance@v1
with:
subject-path: "dist/*"
- name: Store provenance attestation
if: steps.provenance.outputs.bundle-path != ''
uses: actions/upload-artifact@v4
with:
name: python-package-provenance
path: ${{ steps.provenance.outputs.bundle-path }}
- name: Prepare vulnerability scan
uses: actions/setup-python@v5
with:
python-version: "3.9"
- name: Install CycloneDX Python tool
run: |
set -euo pipefail
python -m pip install --upgrade "cyclonedx-bom==${CYCLONEDX_BOM_VERSION}"
- name: Generate CycloneDX SBOM
run: |
set -euo pipefail
cyclonedx-py poetry > sbom.json
if [[ ! -s sbom.json ]]; then
echo "Generated SBOM is empty" >&2
exit 1
fi
- name: Scan SBOM for vulnerabilities using Trivy
uses: aquasecurity/trivy-action@0.33.1
with:
scan-type: sbom
scan-ref: sbom.json
format: table
exit-code: 0
output: vuln-report.txt
severity: CRITICAL,HIGH
- name: Upload sbom to OWASP
run: |
set -euo pipefail
curl -X POST \
-H "X-Api-Key: ${{ secrets.OWASP_TOKEN }}" \
-F "autoCreate=true" \
-F "projectName=xrpl-py" \
-F "projectVersion=${{ env.PACKAGE_VERSION }}" \
-F "bom=@sbom.json" \
https://owasp-dt-api.prod.ripplex.io/api/v1/bom
- name: Upload SBOM artifact
uses: actions/upload-artifact@v4
with:
name: sbom
path: sbom.json
- name: Show scan report
id: show_scan_report
run: |
set -euo pipefail
SUMMARY_LINE="$(grep -E '^Total:' vuln-report.txt || true)"
if printf '%s' "$SUMMARY_LINE" | grep -Eq '^Total:\s*0\s*\(HIGH:\s*0,\s*CRITICAL:\s*0\)$'; then
printf '\n%s\n' "✅ No CRITICAL or HIGH vulnerabilities detected for xrpl-py." >> vuln-report.txt
echo "found_vulnerability=false" >> "$GITHUB_OUTPUT"
else
echo "found_vulnerability=true" >> "$GITHUB_OUTPUT"
fi
cat vuln-report.txt
- name: Upload vulnerability report artifact
id: upload_vuln
uses: actions/upload-artifact@v4
with:
name: vulnerability-report
path: vuln-report.txt
- name: Build vuln artifact URL
id: vuln_art
env:
REPO: ${{ github.repository }}
RUN_ID: ${{ github.run_id }}
ARTIFACT_ID: ${{ steps.upload_vuln.outputs.artifact-id }}
run: |
set -euo pipefail
echo "art_url=https://github.com/${REPO}/actions/runs/${RUN_ID}/artifacts/${ARTIFACT_ID}" >> "$GITHUB_OUTPUT"
- name: Create GitHub Issue for vulnerabilities
if: steps.show_scan_report.outputs.found_vulnerability == 'true'
shell: bash
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
PKG_VER: ${{ env.PACKAGE_VERSION }}
REL_BRANCH: ${{ github.event.inputs.release_branch }}
VULN_ART_URL: ${{ steps.vuln_art.outputs.art_url }}
LABELS: security
run: |
set -euo pipefail
TITLE="🔒 Security vulnerabilities in xrpl-py@${PKG_VER}"
: > issue_body.md
{
echo "The vulnerability scan has detected **CRITICAL/HIGH** vulnerabilities for \`xrpl-py@${PKG_VER}\` on branch \`${REL_BRANCH}\`."
echo ""
echo "**Release Branch:** \`${REL_BRANCH}\`"
echo "**Package Version:** \`${PKG_VER}\`"
echo ""
echo "**Full vulnerability report:** ${VULN_ART_URL}"
echo ""
echo "Please review the report and take necessary action."
echo ""
echo "---"
echo "_This issue was automatically generated by the Publish to PyPI workflow._"
} >> issue_body.md
gh issue create --title "$TITLE" --body-file issue_body.md --label "$LABELS"
ask_for_dev_team_review:
name: Summarize release and request Dev review
runs-on: ubuntu-latest
needs:
- input-validate
- faucet-tests
- integration-tests
- unit-tests
- pre-release
if: ${{ always()
&& needs.input-validate.result == 'success'
&& needs.pre-release.result == 'success'
&& (needs['faucet-tests'].result == 'success' || needs.input-validate.outputs.is_beta_release == 'true')
&& (needs['integration-tests'].result == 'success' || needs.input-validate.outputs.is_beta_release == 'true')
&& needs['unit-tests'].result == 'success' }}
permissions:
pull-requests: write
outputs:
reviewers_dev: ${{ steps.get_reviewers.outputs.reviewers_dev }}
reviewers_sec: ${{ steps.get_reviewers.outputs.reviewers_sec }}
env:
PACKAGE_VERSION: ${{ needs.input-validate.outputs.package_version }}
IS_BETA_RELEASE: ${{ needs.input-validate.outputs.is_beta_release }}
RELEASE_BRANCH: ${{ github.event.inputs.release_branch }}
SLACK_TOKEN: ${{ secrets.SLACK_TOKEN }}
ENV_DEV_NAME: 'first-review'
ENV_SEC_NAME: ${{ needs.input-validate.outputs.is_beta_release == 'true' && 'beta-release' || 'official-release' }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ env.RELEASE_BRANCH }}
- name: Get reviewers
id: get_reviewers
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
RUN_ID: ${{ github.run_id }}
RUN_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
PACKAGE_VERSION: ${{ env.PACKAGE_VERSION }}
GITHUB_ACTOR: ${{ github.actor }}
GITHUB_TRIGGERING_ACTOR: ${{ github.triggering_actor }}
run: |
set -euo pipefail
fetch_reviewers() {
local env_name="$1"
local env_json reviewers
env_json="$(curl -sSf \
-H "Authorization: Bearer $GH_TOKEN" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/$REPO/environments/$env_name")" || true
reviewers="$(printf '%s' "$env_json" | jq -r '
(.protection_rules // [])
| map(select(.type=="required_reviewers") | .reviewers // [])
| add // []
| map(
if .type=="User" then (.reviewer.login)
elif .type=="Team" then (.reviewer.slug)
else (.reviewer.login // .reviewer.slug // "unknown")
end
)
| unique
| join(", ")
')"
if [ -z "$reviewers" ] || [ "$reviewers" = "null" ]; then
reviewers="(no required reviewers configured)"
fi
printf '%s' "$reviewers"
}
# Get reviewer lists
REVIEWERS_DEV="$(fetch_reviewers "$ENV_DEV_NAME")"
REVIEWERS_SEC="$(fetch_reviewers "$ENV_SEC_NAME")"
# Output messages
echo "reviewers_dev=$REVIEWERS_DEV" >> "$GITHUB_OUTPUT"
echo "reviewers_sec=$REVIEWERS_SEC" >> "$GITHUB_OUTPUT"
- name: Release summary for review
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
RUN_ID: ${{ github.run_id }}
PACKAGE_VERSION: ${{ env.PACKAGE_VERSION }}
RELEASE_BRANCH: ${{ env.RELEASE_BRANCH }}
run: |
set -euo pipefail
ARTIFACT_NAME="vulnerability-report"
ARTIFACTS=$(curl -s -H "Authorization: Bearer $GH_TOKEN" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/$REPO/actions/runs/$RUN_ID/artifacts")
ARTIFACT_ID=$(echo "$ARTIFACTS" | jq -r ".artifacts[]? | select(.name == \"$ARTIFACT_NAME\") | .id")
echo "📦 Package version: $PACKAGE_VERSION"
echo "🌿 Release branch: $RELEASE_BRANCH"
if [ -n "${ARTIFACT_ID:-}" ]; then
echo "🛡️ Vulnerability report: https://github.com/$REPO/actions/runs/$RUN_ID/artifacts/$ARTIFACT_ID"
else
echo "⚠️ Vulnerability report artifact not found"
fi
- name: Send Dev review message to Slack
if: always()
shell: bash
env:
SLACK_TOKEN: ${{ secrets.SLACK_TOKEN }}
CHANNEL: "#xrpl-py"
EXECUTOR: ${{ github.triggering_actor || github.actor }}
RELEASE_BRANCH: ${{ env.RELEASE_BRANCH }}
PACKAGE_VERSION: ${{ env.PACKAGE_VERSION }}
RUN_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
DEV_REVIEWERS: ${{ steps.get_reviewers.outputs.reviewers_dev }}
run: |
set -euo pipefail
MSG="${EXECUTOR} is releasing xrpl-py ${PACKAGE_VERSION} from ${RELEASE_BRANCH}. A member from the dev team (${DEV_REVIEWERS}) needs to review the release artifacts and approve/reject the release. (${RUN_URL})"
MSG=$(printf '%b' "$MSG")
# Post once
curl -sS -X POST https://slack.com/api/chat.postMessage \
-H "Authorization: Bearer $SLACK_TOKEN" \
-H "Content-Type: application/json; charset=utf-8" \
-d "$(jq -n --arg channel "$CHANNEL" --arg text "$MSG" '{channel:$channel, text:$text}')" \
| jq -er '.ok' >/dev/null
first_review:
name: First approval (dev team)
runs-on: ubuntu-latest
needs:
- input-validate
- faucet-tests
- integration-tests
- unit-tests
- pre-release
- ask_for_dev_team_review
if: ${{ always()
&& needs.pre-release.result == 'success'
&& needs.ask_for_dev_team_review.result == 'success'
&& (needs['faucet-tests'].result == 'success' || needs.input-validate.outputs.is_beta_release == 'true')
&& (needs['integration-tests'].result == 'success' || needs.input-validate.outputs.is_beta_release == 'true')
&& needs['unit-tests'].result == 'success' }}
environment:
name: first-review
url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
steps:
- name: Awaiting approval
run: echo "Awaiting Dev team approval"
ask_for_sec_team_review:
name: Request security team review
runs-on: ubuntu-latest
needs:
- input-validate
- faucet-tests
- integration-tests
- unit-tests
- pre-release
- ask_for_dev_team_review
- first_review
if: ${{ always()
&& needs.pre-release.result == 'success'
&& needs.ask_for_dev_team_review.result == 'success'
&& needs.first_review.result == 'success'
&& (needs['faucet-tests'].result == 'success' || needs.input-validate.outputs.is_beta_release == 'true')
&& (needs['integration-tests'].result == 'success' || needs.input-validate.outputs.is_beta_release == 'true')
&& needs['unit-tests'].result == 'success' }}
env:
PACKAGE_VERSION: ${{ needs.input-validate.outputs.package_version }}
SLACK_TOKEN: ${{ secrets.SLACK_TOKEN }}
EXECUTOR: ${{ github.triggering_actor || github.actor }}
RELEASE_BRANCH: ${{ github.event.inputs.release_branch }}
RUN_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
SEC_REVIEWERS: ${{ needs.ask_for_dev_team_review.outputs.reviewers_sec }}
VULN_ART_URL: ${{ needs.pre-release.outputs.vuln_art_url }}
steps:
- name: Notify security reviewers on Slack
env:
RUN_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
PACKAGE_VERSION: ${{ env.PACKAGE_VERSION }}
VULN_ART_URL: ${{ env.VULN_ART_URL }}
run: |
set -euo pipefail
MSG="${EXECUTOR} is releasing xrpl-py ${PACKAGE_VERSION} from ${RELEASE_BRANCH}. A member from the sec reviewer team (${SEC_REVIEWERS}) needs to take the following action:\nReview the vulnerabilities ${VULN_ART_URL} and approve/reject the release. (${RUN_URL})"
MSG=$(printf '%b' "$MSG")
curl -sS -X POST https://slack.com/api/chat.postMessage \
-H "Authorization: Bearer $SLACK_TOKEN" \
-H "Content-Type: application/json; charset=utf-8" \
-d "$(jq -n --arg channel "#ripplex-security" --arg text "$MSG" '{channel:$channel, text:$text}')" \
| jq -er '.ok' >/dev/null
- name: Awaiting security approval
run: echo "Waiting for security team review"
publish-to-pypi:
name: >-
Publish Python 🐍 distribution 📦 to PyPI (${{ needs.pre-release.outputs.package_version }})
needs:
- input-validate
- faucet-tests
- integration-tests
- pre-release
- ask_for_dev_team_review
- first_review
- ask_for_sec_team_review
- unit-tests
if: ${{ always()
&& needs.pre-release.result == 'success'
&& needs.ask_for_dev_team_review.result == 'success'
&& needs.first_review.result == 'success'
&& needs.ask_for_sec_team_review.result == 'success'
&& (needs['faucet-tests'].result == 'success' || needs.input-validate.outputs.is_beta_release == 'true')
&& (needs['integration-tests'].result == 'success' || needs.input-validate.outputs.is_beta_release == 'true')
&& needs['unit-tests'].result == 'success' }}
runs-on: ubuntu-latest
timeout-minutes: 10 # Adjust based on typical publishing time
environment:
name: ${{ needs.input-validate.outputs.is_beta_release == 'true' && 'beta-release' || 'official-release' }}
url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
env:
PACKAGE_VERSION: ${{ needs.pre-release.outputs.package_version }}
permissions:
# More information about Trusted Publishing and OpenID Connect: https://blog.pypi.org/posts/2023-04-20-introducing-trusted-publishers/
contents: read
id-token: write # IMPORTANT: mandatory for trusted publishing
steps:
- name: Prevent second attempt
run: |
if (( ${GITHUB_RUN_ATTEMPT:-1} > 1 )); then
echo "❌ Workflow rerun (attempt ${GITHUB_RUN_ATTEMPT}). Second attempts are not allowed."
exit 1
fi
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.event.inputs.release_branch }}
- name: Download all the dists
uses: actions/download-artifact@v4
with:
name: python-package-distributions
path: dist/
- name: Verify downloaded artifacts
run: |
ls dist/*.whl dist/*.tar.gz || exit 1
- name: Publish distribution 📦 to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
verbose: true
verify-metadata: true
attestations: true
github-release:
name: Github Release (${{ needs.pre-release.outputs.package_version }})
needs:
- faucet-tests
- integration-tests
- input-validate
- pre-release
- ask_for_dev_team_review
- first_review
- ask_for_sec_team_review
- publish-to-pypi
- unit-tests
if: ${{ always()
&& needs.pre-release.result == 'success'
&& needs.ask_for_dev_team_review.result == 'success'
&& needs.first_review.result == 'success'
&& needs.ask_for_sec_team_review.result == 'success'
&& needs['publish-to-pypi'].result == 'success'
&& (needs['faucet-tests'].result == 'success' || needs.input-validate.outputs.is_beta_release == 'true')
&& (needs['integration-tests'].result == 'success' || needs.input-validate.outputs.is_beta_release == 'true')
&& needs['unit-tests'].result == 'success' }}
runs-on: ubuntu-latest
timeout-minutes: 15 # Adjust based on typical signing and release time
permissions:
contents: write # IMPORTANT: mandatory for making GitHub Releases
id-token: write # IMPORTANT: mandatory for sigstore
env:
PACKAGE_VERSION: ${{ needs.pre-release.outputs.package_version }}
IS_BETA_RELEASE: ${{ needs.input-validate.outputs.is_beta_release }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.event.inputs.release_branch }}
- name: Download all the dists
uses: actions/download-artifact@v4
with:
name: python-package-distributions
path: dist/
- name: Download provenance attestations
uses: actions/download-artifact@v4
with:
name: python-package-provenance
path: provenance/
- name: Sign the dists with Sigstore
uses: sigstore/gh-action-sigstore-python@v3.0.1
with:
inputs: >-
./dist/*.tar.gz
./dist/*.whl
- name: Create GitHub Release and upload assets
uses: softprops/action-gh-release@v2
env:
GITHUB_TOKEN: ${{ github.token }}
with:
tag_name: v${{ env.PACKAGE_VERSION }}
target_commitish: ${{ github.event.inputs.release_branch }}
generate_release_notes: true
prerelease: ${{ env.IS_BETA_RELEASE == 'true' }}
make_latest: ${{ env.IS_BETA_RELEASE != 'true' }}
files: |
dist/**
provenance/**
- name: Notify Slack success (single-line)
if: success()
env:
SLACK_TOKEN: ${{ secrets.SLACK_TOKEN }}
REPO: ${{ github.repository }}
PACKAGE_VERSION: ${{ env.PACKAGE_VERSION }}
TAG: ${{ env.PACKAGE_VERSION }}
run: |
set -euo pipefail
# Build release URL from tag (URL-encoded to handle '@' etc.)
TAG="${TAG:-${PACKAGE_VERSION}}"
RELEASE_URL="https://github.com/$REPO/releases/tag/v$TAG"
text="xrpl-py ${PACKAGE_VERSION} has been successfully released and published to pypi. Release URL: ${RELEASE_URL}"
text="${text//\\n/ }"
curl -sS -X POST https://slack.com/api/chat.postMessage \
-H "Authorization: Bearer $SLACK_TOKEN" \
-H "Content-Type: application/json; charset=utf-8" \
-d "$(jq -n --arg channel "#xrpl-py" --arg text "$text" '{channel:$channel, text:$text}')"
notify-failure:
name: Notify Slack if release fails
runs-on: ubuntu-latest
needs:
- input-validate
- faucet-tests
- integration-tests
- unit-tests
- pre-release
- ask_for_dev_team_review
- first_review
- ask_for_sec_team_review
- publish-to-pypi
- github-release
if: ${{ failure() }}
env:
SLACK_TOKEN: ${{ secrets.SLACK_TOKEN }}
PACKAGE_VERSION: ${{ needs.input-validate.outputs.package_version }}
REPO: ${{ github.repository }}
RUN_ID: ${{ github.run_id }}
steps:
- name: Notify Slack if release fails
run: |
MESSAGE="❌ Release failed for xrpl-py ${PACKAGE_VERSION}. Check the logs: https://github.com/${REPO}/actions/runs/${RUN_ID}"
curl -X POST https://slack.com/api/chat.postMessage \
-H "Authorization: Bearer $SLACK_TOKEN" \
-H "Content-Type: application/json" \
-d "$(jq -n \
--arg channel "#xrpl-py" \
--arg text "$MESSAGE" \
'{channel: $channel, text: $text}')"