diff --git a/.github/actions/create-github-issue/action.yml b/.github/actions/create-github-issue/action.yml new file mode 100644 index 0000000000..c18fcb53b7 --- /dev/null +++ b/.github/actions/create-github-issue/action.yml @@ -0,0 +1,64 @@ +name: "Create Vulnerability Issue" +description: "Creates a GitHub issue if CRITICAL/HIGH vulnerabilities are found in the scan report" + +inputs: + report_path: + description: "Path to the vulnerability report file" + required: true + package_name: + description: "Name of the package being released" + required: true + package_version: + description: "Version of the package being released" + required: true + release_branch: + description: "Release branch name" + required: true + labels: + description: "Labels to apply to the created issue" + required: false + default: "security" + vuln_art_url: + description: "URL to the vulnerability report artifact" + required: false + +runs: + using: "composite" + steps: + - name: Check vulnerabilities in report + id: check_vulns + shell: bash + run: | + set -euo pipefail + REPORT="${{ inputs.report_path }}" + + if grep -qE "CRITICAL|HIGH" "$REPORT"; then + echo "found=true" >> "$GITHUB_OUTPUT" + else + echo "found=false" >> "$GITHUB_OUTPUT" + fi + + - name: Create GitHub Issue + if: steps.check_vulns.outputs.found == 'true' + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: | + TITLE="πŸ”’ Security vulnerabilities in ${{ inputs.package_name }}@${{ inputs.package_version }}" + BODY=$(cat <> "$GITHUB_OUTPUT" + echo "❎ Report not found at $FILE. Skipping issue creation." + exit 0 + fi + + if ! [[ -s "$FILE" ]]; then + echo "has_findings=false" >> "$GITHUB_OUTPUT" + echo "βœ… Report exists but is empty. Skipping issue creation." + exit 0 + fi + + # Look for CRITICAL or HIGH (case-insensitive, whole word) + if grep -Eqi '\b(CRITICAL|HIGH)\b' "$FILE"; then + echo "has_findings=true" >> "$GITHUB_OUTPUT" + + # Escape backticks so the code block renders correctly in Markdown + REPORT_ESCAPED=$(sed 's/`/\\`/g' "$FILE") + + { + echo "VULN_BODY<<'EOF_VULN'" + echo "$REPORT_ESCAPED" + echo "EOF_VULN" + } >> "$GITHUB_ENV" + else + echo "has_findings=false" >> "$GITHUB_OUTPUT" + echo "βœ… No CRITICAL/HIGH findings in $FILE. Skipping issue creation." + fi + + - name: Create GitHub issue with inline report + if: steps.scan.outputs.has_findings == 'true' + uses: actions/github-script@v7 + with: + github-token: ${{ github.token }} + script: | + const labelsCsv = `${{ inputs.labels }}`.trim(); + const labels = labelsCsv ? labelsCsv.split(',').map(s => s.trim()).filter(Boolean) : []; + + const pkg = `${{ inputs.package_name }}` || '(unknown package)'; + const version = `${{ inputs.package_version }}` || '(unknown version)'; + const branch = `${{ inputs.release_branch }}` || '(unknown branch)'; + const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${process.env.GITHUB_RUN_ID}`; + const report = process.env.VULN_BODY || '(no report content)'; + + const title = `${{ inputs.title_prefix }} in ${pkg}@${ver}`; + const body = + `Automated scan detected **CRITICAL/HIGH** vulnerabilities.\n\n` + + `**Package:** ${pkg}\n` + + `**Version:** ${version}\n` + + `**Release branch:** ${branch}\n` + + `**Workflow run:** ${runUrl}\n\n` + + `
Trivy Report (table)\n\n` + + '```\n' + report + '\n```\n\n' + + `
\n`; + + const { data: issue } = await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title, + body, + labels + }); + + core.info(`βœ… Issue created: ${issue.html_url}`); diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 40190d6bca..e91667927e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,17 +9,14 @@ on: package_name: description: 'The package name to release (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: '' + required: true + concurrency: group: release cancel-in-progress: true @@ -31,45 +28,46 @@ 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 + + - 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 +77,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 +93,329 @@ 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 --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 + + - 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 + TARBALL=$(npx lerna exec --scope "@shichengsh001/${{ env.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) + if: | + !contains(needs.get_version.outputs.package_version, '-rc') && + !contains(needs.get_version.outputs.package_version, '-beta') + 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%%/*}" + + # List open PRs with base=main and 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 "ℹ️ PR already exists: #$PR_NUMBER" + 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')" + + # (Optional) add a label to the PR (labels are an Issues API) + gh api \ + -H 'Accept: application/vnd.github+json' \ + --method POST \ + "/repos/$REPO/issues/$PR_NUMBER/labels" \ + --input <(jq -n --arg l "release" '{labels:[$l]}') >/dev/null || true + fi + + echo "PR_URL=$PR_URL" >> "$GITHUB_ENV" + echo "βœ… PR URL: $PR_URL" + + - 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 }} + + 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) + IS_RC="false" + [[ "${NPMJS_DIST_TAG:-}" =~ ^rc ]] || [[ "${PACKAGE_VERSION:-}" =~ -rc ]] && IS_RC="true" + + # Build Step 2 line with PR URL (if available), unless RC + if [ "$IS_RC" = "true" ]; then + STEP2_LINE="2. Review the package update PR – **SKIPPED for release candidates**." + else + if [ -n "${PR_URL:-}" ]; 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})" + else + 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." + fi + fi + + # Compose message robustly + printf -v MESSAGE '%s is releasing %s@%s. 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" + + 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 "#test-alert" \ + --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 +425,156 @@ 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 "$TAG" ]; then + TAG="latest" + fi + npm install -g npm@latest + 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: Fetch release notes + id: fetch_notes + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + TAG: ${{ steps.create_tag.outputs.tag_name }} + run: | + set -euo pipefail + + # URL-encode the tag (handles '@' -> %40, etc.) + enc_tag="$(printf '%s' "$TAG" | jq -sRr @uri)" + endpoint="https://api.github.com/repos/$REPO/releases/tags/$enc_tag" + + tries=8 + sleep_sec=2 + resp_file="$(mktemp)" + + # Retry to tolerate eventual consistency after release creation + for i in $(seq 1 "$tries"); do + code=$(curl -sS -o "$resp_file" -w '%{http_code}' \ + -H "Authorization: Bearer $GH_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + "$endpoint") || code=$? + + if [ "$code" = "200" ]; then + break + fi + + if [ "$i" -lt "$tries" ]; then + sleep "$sleep_sec" + else + # fallback: give a valid URL and empty notes + url="https://github.com/$REPO/releases/tag/$enc_tag" + notes="(No release notes were generated.)" + + esc_notes="${notes//'%'/'%25'}" + esc_notes="${esc_notes//$'\n'/'%0A'}" + esc_notes="${esc_notes//$'\r'/'%0D'}" + esc_url="${url//'%'/'%25'}" + + echo "notes=$esc_notes" >> "$GITHUB_OUTPUT" + echo "release_url=$esc_url" >> "$GITHUB_OUTPUT" + exit 0 + fi + done + + body="$(jq -r '.body // ""' < "$resp_file")" + url="$(jq -r '.html_url // ""' < "$resp_file")" + + # Safety: trim very long notes for Slack + [ -z "$body" ] && body="(No release notes were generated.)" + max=38000 + [ "${#body}" -gt "$max" ] && body="${body:0:$max}\n…(truncated)" + [ -z "$url" ] && url="https://github.com/$REPO/releases/tag/$enc_tag" + + # Escape for GITHUB_OUTPUT + esc_notes="${body//'%'/'%25'}" + esc_notes="${esc_notes//$'\n'/'%0A'}" + esc_notes="${esc_notes//$'\r'/'%0D'}" + esc_url="${url//'%'/'%25'}" + + echo "notes=$esc_notes" >> "$GITHUB_OUTPUT" + echo "release_url=$esc_url" >> "$GITHUB_OUTPUT" + + - name: Notify Slack success + if: success() + env: + SLACK_TOKEN: ${{ secrets.SLACK_TOKEN }} + NOTES: ${{ steps.fetch_notes.outputs.notes }} + URL: ${{ steps.fetch_notes.outputs.release_url }} + TAG: ${{ steps.create_tag.outputs.tag_name }} + run: | + set -euo pipefail + notes="${NOTES//'%0A'/$'\n'}" + notes="${notes//'%0D'/}" + text="πŸ“£ New release: ${TAG}\n${URL}\n\n${notes}" + + 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/package-lock.json b/package-lock.json index 3f8ca81cb3..60ea215cbe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2689,6 +2689,10 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@shichengsh001/xrpl": { + "resolved": "packages/xrpl", + "link": true + }, "node_modules/@shikijs/engine-oniguruma": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.4.2.tgz", @@ -14888,7 +14892,8 @@ } }, "packages/xrpl": { - "version": "4.4.1", + "name": "@shichengsh001/xrpl", + "version": "4.6.9", "license": "ISC", "dependencies": { "@scure/bip32": "^1.3.1", diff --git a/packages/xrpl/package.json b/packages/xrpl/package.json index f9990fd0e0..8466ff3af5 100644 --- a/packages/xrpl/package.json +++ b/packages/xrpl/package.json @@ -1,6 +1,6 @@ { - "name": "xrpl", - "version": "4.4.1", + "name": "@shichengsh001/xrpl", + "version": "4.6.9", "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": [