Release Pipeline #19
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 | |
| 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 | |
| jobs: | |
| get_version: | |
| runs-on: ubuntu-latest | |
| name: Get release version from package.json | |
| outputs: | |
| package_version: ${{ steps.get_version.outputs.version }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Validate inputs | |
| run: | | |
| set -euo pipefail | |
| RELEASE_BRANCH="$(git branch --show-current || true)" | |
| if [[ -z "$RELEASE_BRANCH" ]]; then | |
| RELEASE_BRANCH="${{ github.ref_name }}" | |
| fi | |
| if [[ -z "$RELEASE_BRANCH" ]]; then | |
| echo "❌ Unable to determine branch name." >&2 | |
| exit 1 | |
| fi | |
| # Validate package_name | |
| PKG_NAME="${{ github.event.inputs.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 [[ ! "${RELEASE_BRANCH,,}" =~ ^release[-/] ]]; then | |
| echo "❌ Release branch '$RELEASE_BRANCH' must start with 'release-' or 'release/'." >&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 | |
| # validate dist tag | |
| NPM_DIST_TAG="${{ github.event.inputs.npmjs_dist_tag }}" | |
| # Empty → default to 'latest' | |
| if [ -z "$NPM_DIST_TAG" ]; then | |
| NPM_DIST_TAG="latest" | |
| echo "ℹ️ npmjs_dist_tag empty → defaulting to 'latest'." | |
| fi | |
| # Must start with a lowercase letter; then [a-z0-9._-]; max 128 chars | |
| 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 | |
| # Disallow version-like prefixes (avoid semver/range confusion) | |
| 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 | |
| echo "✅ npmjs_dist_tag '$NPM_DIST_TAG' is valid." | |
| - name: Get package version from package.json | |
| id: get_version | |
| run: | | |
| set -euo pipefail | |
| PACKAGE_NAME="${{ github.event.inputs.package_name }}" | |
| PKG_JSON="packages/${PACKAGE_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 | |
| NPM_DIST_TAG="${{ github.event.inputs.npmjs_dist_tag }}" | |
| if [ -z "$NPM_DIST_TAG" ]; then | |
| NPM_DIST_TAG="latest" | |
| fi | |
| if [[ "$NPM_DIST_TAG" == "latest" ]] && ! [[ "$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_ENV" | |
| echo "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: ${{ github.ref }} | |
| secrets: inherit | |
| run_tests: | |
| name: Run unit/integration tests ${{ needs.get_version.outputs.package_version }} | |
| permissions: | |
| contents: read | |
| id-token: write | |
| pages: write | |
| needs: [get_version] | |
| uses: ./.github/workflows/nodejs.yml | |
| with: | |
| git_ref: ${{ github.ref }} | |
| secrets: inherit | |
| pre_release: | |
| runs-on: ubuntu-latest | |
| needs: [get_version, run_faucet_test, run_tests] | |
| name: Pre Release Pipeline for ${{ needs.get_version.outputs.package_version }} | |
| permissions: | |
| issues: write | |
| env: | |
| PACKAGE_VERSION: "${{ needs.get_version.outputs.package_version }}" | |
| PACKAGE_NAME: "${{ github.event.inputs.package_name }}" | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| registry-url: 'https://registry.npmjs.org' | |
| - name: Build package | |
| run: | | |
| # dubugging 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 }} | |
| run: | | |
| MESSAGE="❌ Build failed for xrpl.js ${{ env.PACKAGE_VERSION }}. Check the logs: https://github.com/${{ github.repository }}/actions/runs/${{ github.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 | |
| run: | | |
| curl -X POST \ | |
| -H "X-Api-Key: ${{ secrets.OWASP_TOKEN }}" \ | |
| -F "autoCreate=true" \ | |
| -F "projectName=xrpl-js" \ | |
| -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: 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 | |
| run: | | |
| echo "art_url=https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts/${{ steps.upload_vuln.outputs.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 }} | |
| PKG_NAME: ${{ env.PACKAGE_NAME }} | |
| PKG_VER: ${{ env.PACKAGE_VERSION }} | |
| 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_VER}" | |
| : > issue_body.md | |
| echo "The vulnerability scan has detected **CRITICAL/HIGH** vulnerabilities for \`${PKG_NAME}@${PKG_VER}\` on branch \`${REL_BRANCH}\`." >> issue_body.md | |
| echo "" >> issue_body.md | |
| echo "**Release Branch:** \`${REL_BRANCH}\`" >> issue_body.md | |
| echo "**Package Version:** \`${PKG_VER}\`" >> 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/${{ env.PACKAGE_NAME }}" | |
| # Use jq to update the packages field safely | |
| jq --arg pkg "packages/${{ env.PACKAGE_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 ${{ env.PACKAGE_NAME }}" | |
| find "packages/${{ env.PACKAGE_NAME }}" -maxdepth 1 -name '*.tgz' -delete || true | |
| FULL_PACKAGE_NAME="$(jq -er '.name' packages/${{ env.PACKAGE_NAME }}/package.json)" | |
| TARBALL=$(npx lerna exec --scope "$FULL_PACKAGE_NAME" -- npm pack --json | jq -r '.[0].filename') | |
| echo "TARBALL=packages/${{ env.PACKAGE_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 | |
| 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: | |
| PACKAGE_VERSION: "${{ needs.get_version.outputs.package_version }}" | |
| PACKAGE_NAME: "${{ github.event.inputs.package_name }}" | |
| RELEASE_BRANCH: "${{ github.ref_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: 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: ${{ github.ref_name }} | |
| VERSION: ${{ needs.get_version.outputs.package_version }} | |
| 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)" | |
| else | |
| echo "📝 Creating PR for release $VERSION from $RELEASE_BRANCH → main" | |
| 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/${{ github.run_id }}" \ | |
| '{title:$title, head:$head, base:$base, body:$body}')" | |
| 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')" | |
| fi | |
| # Expose as step outputs (use these in later steps) | |
| echo "pr_url=$PR_URL" >> "$GITHUB_OUTPUT" | |
| echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT" | |
| - 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 }} | |
| ENV_DEV_NAME: first-review | |
| ENV_SEC_NAME: official-release | |
| PACKAGE_NAME: ${{ env.PACKAGE_NAME }} | |
| PACKAGE_VERSION: ${{ env.PACKAGE_VERSION }} | |
| NPMJS_DIST_TAG: ${{ github.event.inputs.npmjs_dist_tag }} | |
| PR_URL: ${{ steps.ensure_pr.outputs.pr_url }} | |
| 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 | |
| PACKAGE_NAME: ${{ env.PACKAGE_NAME }} | |
| PACKAGE_VERSION: ${{ env.PACKAGE_VERSION }} | |
| NPMJS_DIST_TAG: ${{ github.event.inputs.npmjs_dist_tag }} | |
| GITHUB_ACTOR: ${{ github.actor }} | |
| GITHUB_TRIGGERING_ACTOR: ${{ github.triggering_actor }} | |
| RUN_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| PR_URL: ${{ steps.ensure_pr.outputs.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: $PACKAGE_NAME" | |
| echo "🔖 Package Version: $PACKAGE_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 }} | |
| PACKAGE_NAME: ${{ env.PACKAGE_NAME }} | |
| 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 }} | |
| PR_URL: ${{ steps.ensure_pr.outputs.pr_url }} | |
| run: | | |
| set -euo pipefail | |
| MSG="${EXECUTOR} is releasing ${PACKAGE_NAME}@${PACKAGE_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 | |
| 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 | |
| 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 }} | |
| PACKAGE_NAME: ${{ needs.get_version.outputs.package_version && github.event.inputs.package_name }} | |
| PACKAGE_VERSION: ${{ needs.get_version.outputs.package_version }} | |
| RUN_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| SEC_REVIEWERS: ${{ needs.ask_for_dev_team_review.outputs.reviewers_sec }} | |
| run: | | |
| set -euo pipefail | |
| MSG="${EXECUTOR} is releasing ${PACKAGE_NAME}@${PACKAGE_VERSION}. A member from the infosec team (${SEC_REVIEWERS}) needs to take the following action:\n Review 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 | |
| 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: | |
| PACKAGE_VERSION: "${{ needs.get_version.outputs.package_version }}" | |
| PACKAGE_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: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Download artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: npm-package-tarball | |
| path: dist | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| registry-url: 'https://registry.npmjs.org/' | |
| - name: Publish to npm | |
| run: | | |
| cd dist | |
| PKG=$(ls *.tgz) | |
| echo $PKG | |
| NPM_DIST_TAG="${{ github.event.inputs.npmjs_dist_tag }}" | |
| if [ -z "$NPM_DIST_TAG" ]; then | |
| NPM_DIST_TAG="latest" | |
| fi | |
| if [[ "$NPM_DIST_TAG" == "latest" ]] && ! [[ "${{ env.PACKAGE_VERSION }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then | |
| echo "With npmjs_dist_tag 'latest', version must be of the form x.y.z. Found '${{ env.PACKAGE_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="${{ env.PACKAGE_NAME }}@${{ env.PACKAGE_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: ${{ github.event.inputs.npmjs_dist_tag != '' && github.event.inputs.npmjs_dist_tag != 'latest' }} | |
| make_latest: ${{ github.event.inputs.npmjs_dist_tag == '' || github.event.inputs.npmjs_dist_tag == 'latest' }} | |
| - name: Notify Slack success (single-line) | |
| if: success() | |
| env: | |
| SLACK_TOKEN: ${{ secrets.SLACK_TOKEN }} | |
| REPO: ${{ github.repository }} | |
| PACKAGE_NAME: ${{ env.PACKAGE_NAME }} | |
| PACKAGE_VERSION: ${{ env.PACKAGE_VERSION }} | |
| 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="${PACKAGE_NAME} ${PACKAGE_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}')" | |
| - name: Notify Slack if tests fail | |
| if: failure() | |
| env: | |
| SLACK_TOKEN: ${{ secrets.SLACK_TOKEN }} | |
| run: | | |
| MESSAGE="❌ Release failed for ${{ env.PACKAGE_NAME }}@${{ env.PACKAGE_VERSION }}. Check the logs: https://github.com/${{ github.repository }}/actions/runs/${{ github.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}')" | |
| generate-documentation: | |
| name: Generate and Publish documentation for ${{ needs.get_version.outputs.package_version }} | |
| if: ${{ github.event.inputs.npmjs_dist_tag == 'latest' }} | |
| uses: ./.github/workflows/generate-documentation.yml | |
| needs: [get_version, release] | |
| with: | |
| git_ref: ${{ github.ref_name }} | |
| permissions: | |
| contents: read | |
| id-token: write | |
| pages: write |