diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 40190d6bca..d51a15e646 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,19 +7,15 @@ on: workflow_dispatch: inputs: package_name: - description: 'The package name to release (e.g., xrpl, ripple-address-codec)' + description: 'Package folder (Name of the package directory under packages/ folder. e.g., xrpl, ripple-address-codec)' required: true - dry-run: - description: 'Perform dry run (create draft release and npm publish with dry-run)' - required: false - default: 'true' - type: choice - options: - - 'true' - - 'false' release_branch: description: 'Release branch the release is generated from' 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 @@ -31,45 +27,88 @@ jobs: outputs: package_version: ${{ steps.get_version.outputs.version }} steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - ref: ${{ github.event.inputs.release_branch }} - fetch-depth: 0 - - name: Validate inputs - run: | - set -euo pipefail - if git ls-remote --exit-code origin "refs/heads/${{ github.event.inputs.release_branch }}" > /dev/null; then - echo "✅ Found release branch: ${{ github.event.inputs.release_branch }}" - else - echo "❌ Release branch ${{ github.event.inputs.release_branch }} not found in remote. Failing workflow." - 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 - - - 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 - echo "PACKAGE_VERSION=$VERSION" >> "$GITHUB_ENV" - echo "version=$VERSION" >> "$GITHUB_OUTPUT" + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ github.event.inputs.release_branch }} + fetch-depth: 0 + + - name: Validate inputs + run: | + set -euo pipefail + # 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 git ls-remote --exit-code origin "refs/heads/${{ github.event.inputs.release_branch }}" > /dev/null; then + echo "✅ Found release branch: ${{ github.event.inputs.release_branch }}" + else + echo "❌ Release branch ${{ github.event.inputs.release_branch }} not found in remote. Failing workflow." + 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 }} @@ -79,7 +118,6 @@ jobs: git_ref: ${{ github.event.inputs.release_branch }} secrets: inherit - run_tests: name: Run unit/integration tests ${{ needs.get_version.outputs.package_version }} permissions: @@ -96,162 +134,389 @@ jobs: 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: - ref: ${{ github.event.inputs.release_branch }} - 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 --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 - - - 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 "project=7c40c8ea-ea0f-4a5f-9b9f-368e53232397" \ - -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 - uses: actions/upload-artifact@v4 - with: - name: vulnerability-report - path: vuln-report.txt - - - 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 - TARBALL=$(npx lerna exec --scope "${{ env.PACKAGE_NAME }}" -- npm pack --json | jq -r '.[0].filename') - echo "TARBALL=packages/${{ env.PACKAGE_NAME }}/${TARBALL}" >> "$GITHUB_ENV" - env: - NPM_CONFIG_USERCONFIG: ${{ runner.temp }}/.npmrc - - - name: Upload tarball as artifact - uses: actions/upload-artifact@v4 - with: - name: npm-package-tarball - path: ${{ env.TARBALL }} - - review: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ github.event.inputs.release_branch }} + 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 "#test-alert" \ + --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 "project=7c40c8ea-ea0f-4a5f-9b9f-368e53232397" \ + -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.event.inputs.release_branch }} + 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] - name: Review test and security scan result + permissions: + pull-requests: write + name: Review test and security scan result -- Dev team env: PACKAGE_VERSION: "${{ needs.get_version.outputs.package_version }}" PACKAGE_NAME: "${{ github.event.inputs.package_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: - ref: ${{ github.event.inputs.release_branch }} - fetch-depth: 0 - - name: Release summary for review - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - REPO: ${{ github.repository }} - RUN_ID: ${{ github.run_id }} - run: | - ARTIFACT_NAME="vulnerability-report" - RELEASE_BRANCH="${{ github.event.inputs.release_branch }}" - 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 Branc: $RELEASE_BRANCH" - echo "🔢 Commit SHA: $COMMIT_SHA" - echo "🔗 Please review Vulnerabilities detected: https://github.com/$REPO/actions/runs/${{ github.run_id }}/artifacts/$ARTIFACT_ID" + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ github.event.inputs.release_branch }} + 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.event.inputs.release_branch }} + 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" + RELEASE_BRANCH="${{ github.event.inputs.release_branch }}" + 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: "#test-alert" + 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: Review test and security scan result — Sec team + steps: + - name: Send Sec team review request to Slack + shell: bash + env: + SLACK_TOKEN: ${{ secrets.SLACK_TOKEN }} + CHANNEL: "#test-alert" + + 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, review] + 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 Pipeline for ${{ needs.get_version.outputs.package_version }} env: PACKAGE_VERSION: "${{ needs.get_version.outputs.package_version }}" @@ -260,94 +525,108 @@ jobs: name: official-release url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - ref: ${{ github.event.inputs.release_branch }} - fetch-depth: 0 - - - name: Ensure Git tag exists - id: create_tag - run: | - set -euo pipefail - BASE_TAG="${{ env.PACKAGE_NAME }}@${{ env.PACKAGE_VERSION }}" - DRY_RUN="${{ github.event.inputs.dry-run }}" - TAG="$BASE_TAG" - - if [ "$DRY_RUN" = "true" ]; then - TAG="draft-$BASE_TAG" - fi - - git fetch --tags origin - - if git rev-parse "$TAG" >/dev/null 2>&1 && [ "$DRY_RUN" != "true" ]; 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: ${{ github.event.inputs.dry-run == 'true' }} - generate_release_notes: true - make_latest: ${{ github.event.inputs.dry-run != 'true' }} - - - 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 - if [[ "${{ github.event.inputs.dry-run }}" == "true" ]]; then - npm publish "$PKG" --provenance --access public --registry=https://registry.npmjs.org/ --dry-run - else - npm publish "$PKG" --provenance --access public --registry=https://registry.npmjs.org/ - fi - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - - - name: Notify Slack success - if: success() - env: - SLACK_TOKEN: ${{ secrets.SLACK_TOKEN }} - run: | - MESSAGE="✅ Released xrpl.js v${{ env.PACKAGE_VERSION }}. Published to npm and GitHub successfully." - 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: Notify Slack if tests fail - if: failure() - env: - SLACK_TOKEN: ${{ secrets.SLACK_TOKEN }} - run: | - MESSAGE="❌ Tests 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: 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: + ref: ${{ github.event.inputs.release_branch }} + 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 "#test-alert" --arg text "$text" '{channel:$channel, text:$text}')" + + - name: Notify Slack if tests fail + if: failure() + env: + SLACK_TOKEN: ${{ secrets.SLACK_TOKEN }} + run: | + MESSAGE="❌ Tests 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 "#test-alert" \ + --arg text "$MESSAGE" \ + '{channel: $channel, text: $text}')" diff --git a/RELEASE.md b/RELEASE.md index d3ad7072c8..5a40eadde3 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -23,19 +23,26 @@ You can manually trigger the release workflow from the [GitHub Actions UI](https 1. Go to **GitHub → Actions → Release Pipeline → Run workflow** 2. Fill in these fields: - - **package_name:** The folder name under `packages/`, e.g., `xrpl` or `ripple-address-codec`. - - **release_branch:** The Git branch to release from (e.g., `release/xrpl@4.3.8`). + - **package_name** → The folder name under `packages/`, e.g., `xrpl` or `ripple-address-codec`. + - **release_branch** → The Git branch the release is generated from, e.g., `release/xrpl@4.3.8`. + - **npmjs_dist_tag** → The npm distribution tag to publish under. Defaults to `latest`. + - Examples: + - `latest` → Standard production release + - `beta` → Pre-release for testing + - `rc` → Release candidate ➡️ Example: -| Field | Example | -|---------------|------------------------| -| package_name | xrpl | -| git_ref | release/xrpl@4.3.8 | +| Field | Example | +|------------------|-----------------------| +| package_name | xrpl | +| release_branch | release/xrpl@4.3.8 | +| npmjs_dist_tag | latest | + ### **Reviewing the release details and scan result** -1. The pipeline will pause at the "Review test and security scan result" step, at least 1 approver is required to review and approve the release. +1. The pipeline will pause at the "Review test and security scan result" step, at least 2 approvers are required to review and approve the release. --- @@ -61,6 +68,7 @@ You can manually trigger the release workflow from the [GitHub Actions UI](https - Uploads the SBOM to OWASP Dependency-Track for tracking vulnerabilities. - Packages the module with Lerna and uploads the tarball as an artifact. - Posts failure notifications to Slack.. +- Create a Github issue for detected vulnerabilities. --- @@ -101,5 +109,3 @@ xrpl@2.3.1 - The release workflow does not overwrite existing tags. If the same version tag already exists, the workflow will fail. - Vulnerability scanning does not block the release, but it is the approvers' responsibility to review the scan results in the Review stage. - -- The final release step performs an npm publish --dry-run. We can remove --dry-run when ready for production release. diff --git a/package-lock.json b/package-lock.json index 3e6a7b2cda..8c3f7c0916 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4530,6 +4530,10 @@ "node": "*" } }, + "node_modules/cheng-xrpl": { + "resolved": "packages/xrpl", + "link": true + }, "node_modules/chokidar": { "version": "3.6.0", "dev": true, @@ -15346,7 +15350,8 @@ } }, "packages/xrpl": { - "version": "4.4.1", + "name": "cheng-xrpl", + "version": "4.4.71", "license": "ISC", "dependencies": { "@scure/bip32": "^1.3.1", diff --git a/packages/xrpl/package.json b/packages/xrpl/package.json index cbffc09387..6ac614b413 100644 --- a/packages/xrpl/package.json +++ b/packages/xrpl/package.json @@ -1,6 +1,6 @@ { - "name": "xrpl", - "version": "4.4.1", + "name": "cheng-xrpl", + "version": "4.4.71", "license": "ISC", "description": "A TypeScript/JavaScript API for interacting with the XRP Ledger in Node.js and the browser", "files": [ @@ -69,7 +69,7 @@ "prettier": "@xrplf/prettier-config", "repository": { "type": "git", - "url": "git@github.com:XRPLF/xrpl.js.git" + "url": "git@github.com:xpring-eng/xrpl.js.git" }, "readmeFilename": "README.md", "keywords": [ diff --git a/packages/xrpl/src/utils/index.ts b/packages/xrpl/src/utils/index.ts index 2304384e90..6a1c5c48d3 100644 --- a/packages/xrpl/src/utils/index.ts +++ b/packages/xrpl/src/utils/index.ts @@ -68,7 +68,7 @@ import verifyPaymentChannelClaim from './verifyPaymentChannelClaim' import { xrpToDrops, dropsToXrp } from './xrpConversion' /** - * Check if a secret is valid. + * Check if a secret is really valid. * * @param secret - Secret to test for validity. * @returns True if secret can be derived into a keypair.