Publish xrpl-py 🐍 distribution 📦 to PyPI #13
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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}')" |