Release Pipeline #30
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: Release Pipeline | |
| permissions: | |
| contents: read | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| package_name: | |
| description: "Package folder (Name of the package directory under packages/ folder. e.g., xrpl, ripple-address-codec)" | |
| required: true | |
| release_branch_name: | |
| description: 'Name of the release branch to be used' | |
| required: true | |
| npmjs_dist_tag: | |
| description: "npm distribution tag(Read more https://docs.npmjs.com/adding-dist-tags-to-packages)" | |
| default: "latest" | |
| concurrency: | |
| group: release | |
| cancel-in-progress: true | |
| defaults: | |
| run: | |
| shell: bash | |
| jobs: | |
| get_version: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| name: Get release version from package.json | |
| outputs: | |
| package_version: ${{ steps.get_version.outputs.package_version }} | |
| dist_tag: ${{ steps.validate_inputs.outputs.dist_tag }} | |
| release_branch: ${{ steps.validate_inputs.outputs.release_branch }} | |
| is_beta: ${{ steps.validate_inputs.outputs.is_beta }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| ref: ${{ github.event.inputs.release_branch_name }} | |
| - name: Validate inputs | |
| id: validate_inputs | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| PKG_NAME: ${{ github.event.inputs.package_name }} | |
| REPO: ${{ github.repository }} | |
| RELEASE_BRANCH: ${{ github.event.inputs.release_branch_name }} | |
| TRIGGER_BRANCH: ${{ github.ref_name }} | |
| NPM_DIST_TAG: ${{ github.event.inputs.npmjs_dist_tag }} | |
| run: | | |
| set -euo pipefail | |
| if [[ -z "$RELEASE_BRANCH" ]]; then | |
| echo "❌ Unable to determine branch name." >&2 | |
| exit 1 | |
| fi | |
| # Validate package_name | |
| if ! [[ "${PKG_NAME}" =~ ^[a-z0-9][a-z0-9-]*$ ]]; then | |
| echo "❌ Invalid package_name '${PKG_NAME}' (allowed: [a-z0-9-], must start with alnum)." >&2 | |
| exit 1 | |
| fi | |
| # Guard against path traversal | |
| if [[ "${PKG_NAME}" == *".."* || "${PKG_NAME}" == *"/"* ]]; then | |
| echo "❌ package_name must be a single directory under packages/." >&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 | |
| if [ -z "$NPM_DIST_TAG" ]; then | |
| NPM_DIST_TAG="latest" | |
| echo "ℹ️ npmjs_dist_tag empty → defaulting to 'latest'." | |
| else | |
| NPM_DIST_TAG="$(printf '%s' "$NPM_DIST_TAG" | tr -d '[:space:]')" | |
| fi | |
| if ! [[ "$NPM_DIST_TAG" =~ ^[a-z][a-z0-9._-]{0,127}$ ]]; then | |
| echo "❌ Invalid npm dist-tag '$NPM_DIST_TAG'. Must start with a lowercase letter and contain only [a-z0-9._-], max 128 chars." >&2 | |
| exit 1 | |
| fi | |
| if [[ "$NPM_DIST_TAG" =~ ^v[0-9] || "$NPM_DIST_TAG" =~ ^[0-9] ]]; then | |
| echo "❌ Invalid npm dist-tag '$NPM_DIST_TAG'. Must not start with 'v' + digit or a digit (e.g., 'v1', '1.2.3')." >&2 | |
| exit 1 | |
| fi | |
| if [ "$NPM_DIST_TAG" = "latest" ]; then | |
| IS_BETA="false" | |
| else | |
| IS_BETA="true" | |
| NPM_DIST_TAG="${NPM_DIST_TAG}-experimental" | |
| fi | |
| if [ "$IS_BETA" != "true" ] && [[ ! "${RELEASE_BRANCH}" =~ ^[Rr][Ee][Ll][Ee][Aa][Ss][Ee][-/] ]]; then | |
| echo "❌ Release branch '$RELEASE_BRANCH' must start with 'release-' or 'release/' for stable releases." >&2 | |
| exit 1 | |
| fi | |
| if [[ "$TRIGGER_BRANCH" != "main" ]]; then | |
| echo "❌ Release pipeline can only be triggered from the 'main' branch. Current branch: '$TRIGGER_BRANCH'." >&2 | |
| exit 1 | |
| fi | |
| { | |
| echo "NPM_DIST_TAG=$NPM_DIST_TAG" | |
| echo "RELEASE_BRANCH=$RELEASE_BRANCH" | |
| } >> "$GITHUB_ENV" | |
| PR_NUMBER="" | |
| PR_URL="" | |
| { | |
| echo "release_branch=$RELEASE_BRANCH" | |
| echo "is_beta=$IS_BETA" | |
| echo "dist_tag=$NPM_DIST_TAG" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Get package version from package.json | |
| id: get_version | |
| env: | |
| IS_BETA: ${{ steps.validate_inputs.outputs.is_beta }} | |
| PKG_NAME: ${{ github.event.inputs.package_name }} | |
| run: | | |
| set -euo pipefail | |
| PKG_JSON="packages/${PKG_NAME}/package.json" | |
| if [[ ! -f "${PKG_JSON}" ]]; then | |
| echo "package.json not found at ${PKG_JSON}. Check 'package_name' input." >&2 | |
| exit 1 | |
| fi | |
| VERSION=$(jq -er .version "${PKG_JSON}") | |
| if [[ -z "${VERSION}" || "${VERSION}" == "null" ]]; then | |
| echo "Version is empty or missing in ${PKG_JSON}" >&2 | |
| exit 1 | |
| fi | |
| if [[ "${IS_BETA:-false}" != "true" ]] && ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then | |
| echo "With npmjs_dist_tag 'latest', version must be of the form x.y.z. Found '$VERSION'." >&2 | |
| exit 1 | |
| fi | |
| echo "package_version=$VERSION" >> "$GITHUB_OUTPUT" | |
| run_faucet_test: | |
| name: Run faucet tests ${{ needs.get_version.outputs.package_version }} | |
| needs: [get_version] | |
| uses: ./.github/workflows/faucet_test.yml | |
| with: | |
| git_ref: ${{ needs.get_version.outputs.release_branch }} | |
| run_faucet_tests: ${{ needs.get_version.outputs.is_beta != 'true' }} | |
| secrets: inherit | |
| run_tests: | |
| name: Run unit/integration tests ${{ needs.get_version.outputs.package_version }} | |
| needs: [get_version] | |
| uses: ./.github/workflows/nodejs.yml | |
| with: | |
| git_ref: ${{ needs.get_version.outputs.release_branch }} | |
| run_unit_tests: true | |
| run_integration_tests: ${{ needs.get_version.outputs.is_beta != 'true' }} | |
| run_browser_tests: ${{ needs.get_version.outputs.is_beta != 'true' }} | |
| secrets: inherit | |
| pre_release: | |
| runs-on: ubuntu-latest | |
| if: ${{ always() && needs.get_version.result == 'success' && (needs.run_faucet_test.result == 'success' || needs.run_faucet_test.result == 'skipped') && needs.run_tests.result == 'success' }} | |
| needs: [get_version, run_faucet_test, run_tests] | |
| name: Pre Release Pipeline for ${{ needs.get_version.outputs.package_version }} | |
| permissions: | |
| issues: write | |
| pull-requests: write | |
| env: | |
| PKG_VERSION: "${{ needs.get_version.outputs.package_version }}" | |
| PKG_NAME: "${{ github.event.inputs.package_name }}" | |
| outputs: | |
| release_pr_number: ${{ steps.ensure_pr.outputs.pr_number }} | |
| release_pr_url: ${{ steps.ensure_pr.outputs.pr_url }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| ref: ${{ needs.get_version.outputs.release_branch }} | |
| - name: Create PR from release branch to main (skips for rc/beta) | |
| id: ensure_pr | |
| if: ${{ github.event.inputs.npmjs_dist_tag == '' || github.event.inputs.npmjs_dist_tag == 'latest' }} | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| REPO: ${{ github.repository }} | |
| RELEASE_BRANCH: ${{ needs.get_version.outputs.release_branch }} | |
| VERSION: ${{ needs.get_version.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%%/*}" | |
| # Find existing OPEN PR: base=main, head=OWNER:RELEASE_BRANCH | |
| 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 "🛑 Closing existing PR #$PR_NUMBER before opening a draft…" | |
| CLOSE_JSON="$(jq -n --arg state "closed" '{state:$state}')" | |
| if ! gh api -H 'Accept: application/vnd.github+json' \ | |
| --method PATCH \ | |
| "/repos/$REPO/pulls/$PR_NUMBER" \ | |
| --input <(printf '%s' "$CLOSE_JSON"); then | |
| echo "⚠️ Unable to close PR #$PR_NUMBER automatically. You may need to close it manually." >&2 | |
| fi | |
| 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')" | |
| # Expose as step outputs (use these in later steps) | |
| echo "pr_url=$PR_URL" >> "$GITHUB_OUTPUT" | |
| echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT" | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| registry-url: "https://registry.npmjs.org" | |
| - name: Build package | |
| run: | | |
| # debugging info | |
| npm i -g npm@11.6.0 | |
| npm --version | |
| node --version | |
| ls -l | |
| pwd | |
| # build | |
| npm ci | |
| npm run build | |
| - name: Notify Slack if tests fail | |
| if: failure() | |
| env: | |
| SLACK_TOKEN: ${{ secrets.SLACK_TOKEN }} | |
| REPO: ${{ github.repository }} | |
| RUN_ID: ${{ github.run_id }} | |
| run: | | |
| MESSAGE="❌ Build failed for xrpl.js ${PKG_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-js" \ | |
| --arg text "${MESSAGE}" \ | |
| '{channel: $channel, text: $text}')" | |
| - name: Install cyclonedx-npm | |
| run: npm install -g @cyclonedx/cyclonedx-npm@4.0.2 | |
| - name: Generate CycloneDX SBOM | |
| run: cyclonedx-npm --output-format json --output-file sbom.json | |
| - name: Scan SBOM for vulnerabilities using Trivy | |
| uses: aquasecurity/trivy-action@0.28.0 | |
| 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 | |
| env: | |
| OWASP_TOKEN: ${{ secrets.OWASP_TOKEN }} | |
| run: | | |
| curl -X POST \ | |
| -H "X-Api-Key: ${OWASP_TOKEN}" \ | |
| -F "autoCreate=true" \ | |
| -F "projectName=xrpl-js" \ | |
| -F "projectVersion=${PKG_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: Print scan report | |
| run: 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: | | |
| echo "art_url=https://github.com/${REPO}/actions/runs/${RUN_ID}/artifacts/${ARTIFACT_ID}" >> "$GITHUB_OUTPUT" | |
| - name: Check vulnerabilities in report | |
| id: check_vulns | |
| shell: bash | |
| env: | |
| REPORT_PATH: vuln-report.txt # change if different | |
| run: | | |
| set -euo pipefail | |
| if grep -qE "CRITICAL|HIGH" "$REPORT_PATH"; then | |
| echo "found=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "found=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Create GitHub Issue (links to report artifact) | |
| if: steps.check_vulns.outputs.found == 'true' | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| REPO: ${{ github.repository }} | |
| REL_BRANCH: ${{ github.ref_name }} | |
| VULN_ART_URL: ${{ steps.vuln_art.outputs.art_url }} | |
| LABELS: security | |
| run: | | |
| set -euo pipefail | |
| TITLE="🔒 Security vulnerabilities in ${PKG_NAME}@${PKG_VERSION}" | |
| : > issue_body.md | |
| echo "The vulnerability scan has detected **CRITICAL/HIGH** vulnerabilities for \`${PKG_NAME}@${PKG_VERSION}\` on branch \`${REL_BRANCH}\`." >> issue_body.md | |
| echo "" >> issue_body.md | |
| echo "**Release Branch:** \`${REL_BRANCH}\`" >> issue_body.md | |
| echo "**Package Version:** \`${PKG_VERSION}\`" >> issue_body.md | |
| echo "" >> issue_body.md | |
| echo "**Full vulnerability report:** ${VULN_ART_URL}" >> issue_body.md | |
| echo "" >> issue_body.md | |
| echo "Please review the report and take necessary action." >> issue_body.md | |
| echo "" >> issue_body.md | |
| echo "---" >> issue_body.md | |
| echo "_This issue was automatically generated by the Release Pipeline._" >> issue_body.md | |
| gh issue create --title "${TITLE}" --body-file issue_body.md --label "${LABELS}" | |
| - name: Generate lerna.json for choosen the package | |
| run: | | |
| echo "🔧 Updating lerna.json to include only packages/${PKG_NAME}" | |
| # Use jq to update the packages field safely | |
| jq --arg pkg "packages/${PKG_NAME}" '.packages = [$pkg]' lerna.json > lerna.tmp.json && mv lerna.tmp.json lerna.json | |
| echo "✅ lerna.json updated:" | |
| cat lerna.json | |
| - name: Pack tarball | |
| run: | | |
| set -euo pipefail | |
| echo "Packaging ${PKG_NAME}" | |
| find "packages/${PKG_NAME}" -maxdepth 1 -name '*.tgz' -delete || true | |
| FULL_PKG_NAME="$(jq -er '.name' packages/${PKG_NAME}/package.json)" | |
| TARBALL=$(npx lerna exec --scope "${FULL_PKG_NAME}" -- npm pack --json | jq -r '.[0].filename') | |
| echo "TARBALL=packages/${PKG_NAME}/${TARBALL}" >> "$GITHUB_ENV" | |
| - name: Upload tarball as artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: npm-package-tarball | |
| path: ${{ env.TARBALL }} | |
| ask_for_dev_team_review: | |
| runs-on: ubuntu-latest | |
| if: ${{ always() && needs.pre_release.result == 'success' && needs.run_tests.result == 'success' && (needs.run_faucet_test.result == 'success' || needs.run_faucet_test.result == 'skipped') }} | |
| needs: [get_version, run_faucet_test, run_tests, pre_release] | |
| permissions: | |
| pull-requests: write | |
| name: Print Test/Security scan result and invite Dev team to review | |
| env: | |
| PKG_VERSION: "${{ needs.get_version.outputs.package_version }}" | |
| PKG_NAME: "${{ github.event.inputs.package_name }}" | |
| RELEASE_BRANCH: "${{ github.event.inputs.release_branch_name }}" | |
| outputs: | |
| reviewers_dev: ${{ steps.get_reviewers.outputs.reviewers_dev }} | |
| reviewers_sec: ${{ steps.get_reviewers.outputs.reviewers_sec }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Get reviewers | |
| id: get_reviewers | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| REPO: ${{ github.repository }} | |
| RUN_ID: ${{ github.run_id }} | |
| ENV_DEV_NAME: first-review | |
| ENV_SEC_NAME: official-release | |
| 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 }} | |
| SLACK_TOKEN: ${{ secrets.SLACK_TOKEN }} | |
| REPO: ${{ github.repository }} | |
| RUN_ID: ${{ github.run_id }} | |
| ENV_NAME: official-release | |
| GITHUB_ACTOR: ${{ github.actor }} | |
| GITHUB_TRIGGERING_ACTOR: ${{ github.triggering_actor }} | |
| RUN_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| PR_URL: ${{ needs.pre_release.outputs.release_pr_url }} | |
| run: | | |
| set -euo pipefail | |
| ARTIFACT_NAME="vulnerability-report" | |
| COMMIT_SHA="$(git rev-parse --short HEAD)" | |
| echo "Fetching artifact ID for ${ARTIFACT_NAME}..." | |
| 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") | |
| if [ -z "${ARTIFACT_ID:-}" ]; then | |
| echo "❌ Artifact not found." | |
| exit 1 | |
| fi | |
| echo "🔍 Please review the following details before proceeding:" | |
| echo "📦 Package Name: ${PKG_NAME}" | |
| echo "🔖 Package Version: ${PKG_VERSION}" | |
| echo "🌿 Release Branch: ${RELEASE_BRANCH}" | |
| echo "🔢 Commit SHA: ${COMMIT_SHA}" | |
| echo "🔗 Vulnerabilities: https://github.com/${REPO}/actions/runs/${RUN_ID}/artifacts/${ARTIFACT_ID}" | |
| - name: Send Dev review message to Slack | |
| if: always() | |
| shell: bash | |
| env: | |
| SLACK_TOKEN: ${{ secrets.SLACK_TOKEN }} | |
| CHANNEL: "#xrpl-js" | |
| EXECUTOR: ${{ github.triggering_actor || github.actor }} | |
| RUN_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| REPO: ${{ github.repository }} | |
| RUN_ID: ${{ github.run_id }} | |
| DEV_REVIEWERS: ${{ steps.get_reviewers.outputs.reviewers_dev }} | |
| PR_URL: ${{ needs.pre_release.outputs.release_pr_url }} | |
| run: | | |
| set -euo pipefail | |
| RUN_URL="https://github.com/${REPO}/actions/runs/${RUN_ID}" | |
| MSG="${EXECUTOR} is releasing ${PKG_NAME}@${PKG_VERSION}. A member from the dev team (${DEV_REVIEWERS}) needs to take the following actions: \n1) Review the release artifacts and approve/reject the release. (${RUN_URL})" | |
| if [ -n "${PR_URL}" ]; then | |
| MSG="${MSG} \n2) Review the package update PR and provide two approvals. DO NOT MERGE — ${EXECUTOR} will verify the package on npm and merge the approved PR. (${PR_URL})" | |
| fi | |
| 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: | |
| runs-on: ubuntu-latest | |
| if: ${{ always() && needs.ask_for_dev_team_review.result == 'success' && needs.run_tests.result == 'success' && (needs.run_faucet_test.result == 'success' || needs.run_faucet_test.result == 'skipped') }} | |
| needs: | |
| [ | |
| get_version, | |
| run_faucet_test, | |
| run_tests, | |
| pre_release, | |
| ask_for_dev_team_review | |
| ] | |
| name: First approval (dev team) | |
| 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: | |
| runs-on: ubuntu-latest | |
| if: ${{ always() && needs.first_review.result == 'success' && needs.run_tests.result == 'success' && (needs.run_faucet_test.result == 'success' || needs.run_faucet_test.result == 'skipped') }} | |
| needs: | |
| [ | |
| get_version, | |
| run_faucet_test, | |
| run_tests, | |
| pre_release, | |
| ask_for_dev_team_review, | |
| first_review | |
| ] | |
| name: Invite sec team to review | |
| steps: | |
| - name: Send Sec team review request to Slack | |
| shell: bash | |
| env: | |
| SLACK_TOKEN: ${{ secrets.SLACK_TOKEN }} | |
| CHANNEL: "#ripplex-security" | |
| EXECUTOR: ${{ github.triggering_actor || github.actor }} | |
| PKG_NAME: ${{ github.event.inputs.package_name }} | |
| PKG_VERSION: ${{ needs.get_version.outputs.package_version }} | |
| REPO: ${{ github.repository }} | |
| RUN_ID: ${{ github.run_id }} | |
| SEC_REVIEWERS: ${{ needs.ask_for_dev_team_review.outputs.reviewers_sec }} | |
| run: | | |
| set -euo pipefail | |
| RUN_URL="https://github.com/${REPO}/actions/runs/${RUN_ID}" | |
| MSG="${EXECUTOR} is releasing ${PKG_NAME}@${PKG_VERSION}. A sec reviewer from (${SEC_REVIEWERS}) needs to take the following action:\nReview the release artifacts 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 "${CHANNEL}" --arg text "${MSG}" '{channel:$channel, text:$text}')" \ | |
| | jq -er '.ok' >/dev/null | |
| release: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| id-token: write | |
| contents: write | |
| if: ${{ always() && needs.ask_for_sec_team_review.result == 'success' && needs.run_tests.result == 'success' && (needs.run_faucet_test.result == 'success' || needs.run_faucet_test.result == 'skipped') }} | |
| needs: | |
| [ | |
| get_version, | |
| run_faucet_test, | |
| run_tests, | |
| pre_release, | |
| ask_for_dev_team_review, | |
| first_review, | |
| ask_for_sec_team_review | |
| ] | |
| name: Release for ${{ needs.get_version.outputs.package_version }} | |
| env: | |
| PKG_VERSION: "${{ needs.get_version.outputs.package_version }}" | |
| PKG_NAME: "${{ github.event.inputs.package_name }}" | |
| environment: | |
| name: official-release | |
| url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| 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: Set up Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| registry-url: 'https://registry.npmjs.org/' | |
| - name: Checkout release branch | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| ref: ${{ needs.get_version.outputs.release_branch }} | |
| - name: Download artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: npm-package-tarball | |
| path: dist | |
| - name: Publish to npm | |
| env: | |
| NPM_DIST_TAG: ${{ needs.get_version.outputs.dist_tag }} | |
| IS_BETA: ${{ needs.get_version.outputs.is_beta }} | |
| run: | | |
| set -euo pipefail | |
| REPO_ROOT="$PWD" | |
| PACKAGE_JSON_PATH="$REPO_ROOT/packages/${PKG_NAME}/package.json" | |
| if [ ! -f "$PACKAGE_JSON_PATH" ]; then | |
| echo "❌ package.json not found at $PACKAGE_JSON_PATH" >&2 | |
| exit 1 | |
| fi | |
| FULL_PACKAGE_NAME=$(jq -er '.name' "$PACKAGE_JSON_PATH") | |
| cd dist | |
| PKG=$(ls *.tgz) | |
| echo "$PKG" | |
| if [ -z "${NPM_DIST_TAG:-}" ]; then | |
| echo "❌ Primary npm dist-tag is not set." >&2 | |
| exit 1 | |
| fi | |
| if [[ "${IS_BETA}" != "true" ]] && ! [[ "${PKG_VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then | |
| echo "Stable releases (tagged with 'latest') must use x.y.z SemVer. Found '${PKG_VERSION}'." >&2 | |
| exit 1 | |
| fi | |
| npm i -g npm@11.6.0 | |
| npm publish "${PKG}" --provenance --access public --registry=https://registry.npmjs.org/ --tag "$NPM_DIST_TAG" | |
| - name: Ensure Git tag exists | |
| id: create_tag | |
| run: | | |
| set -euo pipefail | |
| TAG="${PKG_NAME}@${PKG_VERSION}" | |
| git fetch --tags origin | |
| if git rev-parse "${TAG}" >/dev/null 2>&1 ; then | |
| echo "❌ Tag ${TAG} already exists (not a draft). Failing." | |
| exit 1 | |
| fi | |
| echo "🔖 Tagging ${TAG}" | |
| git tag -f "${TAG}" | |
| git push origin -f "${TAG}" | |
| echo "tag_name=${TAG}" >> "$GITHUB_OUTPUT" | |
| - name: Create GitHub release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: "${{ steps.create_tag.outputs.tag_name }}" | |
| name: "${{ steps.create_tag.outputs.tag_name }}" | |
| draft: false | |
| generate_release_notes: true | |
| prerelease: ${{ needs.get_version.outputs.is_beta == 'true' }} | |
| make_latest: ${{ needs.get_version.outputs.is_beta != 'true' }} | |
| - name: Notify Slack success (single-line) | |
| if: success() | |
| env: | |
| SLACK_TOKEN: ${{ secrets.SLACK_TOKEN }} | |
| REPO: ${{ github.repository }} | |
| TAG: ${{ steps.create_tag.outputs.tag_name }} | |
| run: | | |
| set -euo pipefail | |
| # Build release URL from tag (URL-encoded to handle '@' etc.) | |
| enc_tag="$(printf '%s' "${TAG}" | jq -sRr @uri)" | |
| RELEASE_URL="https://github.com/${REPO}/releases/tag/${enc_tag}" | |
| text="${PKG_NAME} ${PKG_VERSION} has been succesfully released and published to npm.js. 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-js' --arg text "${text}" '{channel:$channel, text:$text}')" | |
| generate-documentation: | |
| name: Generate and Publish documentation for ${{ needs.get_version.outputs.package_version }} | |
| if: ${{ needs.get_version.outputs.is_beta != 'true' }} | |
| uses: ./.github/workflows/generate-documentation.yml | |
| needs: [get_version, release] | |
| permissions: | |
| contents: read | |
| pages: write | |
| id-token: write | |
| with: | |
| git_ref: ${{ needs.get_version.outputs.release_branch }} | |
| notify_failures: | |
| runs-on: ubuntu-latest | |
| needs: | |
| [ | |
| get_version, | |
| run_faucet_test, | |
| run_tests, | |
| pre_release, | |
| ask_for_dev_team_review, | |
| first_review, | |
| ask_for_sec_team_review, | |
| release, | |
| generate-documentation | |
| ] | |
| if: >- | |
| ${{ always() && ( | |
| needs.get_version.result == 'failure' || | |
| (needs.run_faucet_test.result == 'failure' && needs.get_version.outputs.is_beta != 'true') || | |
| (needs.run_tests.result == 'failure' && needs.get_version.outputs.is_beta != 'true') || | |
| needs.pre_release.result == 'failure' || | |
| needs.ask_for_dev_team_review.result == 'failure' || | |
| needs.first_review.result == 'failure' || | |
| needs.ask_for_sec_team_review.result == 'failure' || | |
| needs.release.result == 'failure' || | |
| needs.generate-documentation.result == 'failure' | |
| ) }} | |
| steps: | |
| - name: Notify Slack about workflow failure | |
| env: | |
| SLACK_TOKEN: ${{ secrets.SLACK_TOKEN }} | |
| PKG_NAME: ${{ github.event.inputs.package_name }} | |
| PKG_VERSION: ${{ needs.get_version.outputs.package_version }} | |
| NEEDS_JSON: ${{ toJson(needs) }} | |
| run: | | |
| set -euo pipefail | |
| FAILED_JOBS=$(printf '%s' "$NEEDS_JSON" | jq -r ' | |
| to_entries | |
| | map(select(.value.result=="failure") | .key) | |
| | join(", ") | |
| ') | |
| if [ -z "$FAILED_JOBS" ]; then | |
| echo "No failed jobs detected; skipping notification." | |
| exit 0 | |
| fi | |
| MESSAGE="❌ Workflow failure for ${PKG_NAME}@${PKG_VERSION}. Release failed at ${FAILED_JOBS}. For details: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" | |
| curl -sS -X POST https://slack.com/api/chat.postMessage \ | |
| -H "Authorization: Bearer $SLACK_TOKEN" \ | |
| -H "Content-Type: application/json" \ | |
| -d "$(jq -n \ | |
| --arg channel '#xrpl-js' \ | |
| --arg text "${MESSAGE}" \ | |
| '{channel: $channel, text: $text}')" |