Skip to content

Release Pipeline

Release Pipeline #30

Workflow file for this run

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}')"