diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 40190d6bca..95d08bfcce 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,69 @@ 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 + 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 + 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 +99,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,161 +115,317 @@ 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 }} + - 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 "#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 + 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 }} review: runs-on: ubuntu-latest needs: [get_version, run_faucet_test, run_tests, pre_release] + permissions: + pull-requests: write name: Review test and security scan result 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: 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: 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" + + # executor = the person who triggered the pipeline + EXECUTOR="${GITHUB_TRIGGERING_ACTOR:-$GITHUB_ACTOR}" + + # Fetch environment and extract required 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")" + + 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 + + # RC detection: skip step 2 if RC (dist-tag starts with rc OR version contains -rc) + INCLUDE_PR_LINE=false + if [ -z "${NPMJS_DIST_TAG:-}" ] || [ "${NPMJS_DIST_TAG:-}" = "latest" ]; then + INCLUDE_PR_LINE=true + fi + + if [ "$INCLUDE_PR_LINE" = true ]; then + STEP2_LINE="2. Review the package update PR and provide two approvals. DO NOT MERGE - ${EXECUTOR} will verify the package on npm registry and merge this approved PR. ${PR_URL}" + printf -v MESSAGE '%s is releasing %s@%s. At least two approvers from (%s) need to take following actions:\n1. Review the release artifacts and approve/reject the release. (%s)\n%s' \ + "$EXECUTOR" "$PACKAGE_NAME" "$PACKAGE_VERSION" "$REVIEWERS" "$RUN_URL" "$STEP2_LINE" + else + printf -v MESSAGE '%s is releasing %s@%s. At least two approvers from (%s) need to take following actions:\nReview the release artifacts and approve/reject the release. (%s)' \ + "$EXECUTOR" "$PACKAGE_NAME" "$PACKAGE_VERSION" "$REVIEWERS" "$RUN_URL" + fi + + echo "$MESSAGE" + + # Post to Slack (channel can be changed as needed) + 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 "$MESSAGE" \ + '{channel: $channel, text: $text}')" release: runs-on: ubuntu-latest permissions: - id-token: write - contents: write + id-token: write + contents: write needs: [get_version, run_faucet_test, run_tests, pre_release, review] name: Release Pipeline for ${{ needs.get_version.outputs.package_version }} env: @@ -260,94 +435,97 @@ 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: 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 + 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 + make_latest: true + + - 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="❌ 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}')" 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.