From 34fce5d37600dc4802363da9639be1408167b56a Mon Sep 17 00:00:00 2001 From: Parth Bansal Date: Sun, 19 Apr 2026 18:42:06 +0000 Subject: [PATCH 1/2] changelog and tagging --- .github/workflows/next-changelog.yml | 183 +++++++++++++++++---------- 1 file changed, 119 insertions(+), 64 deletions(-) diff --git a/.github/workflows/next-changelog.yml b/.github/workflows/next-changelog.yml index 475157d1..ddd0829d 100755 --- a/.github/workflows/next-changelog.yml +++ b/.github/workflows/next-changelog.yml @@ -23,86 +23,141 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - # Use the GitHub API to fetch changed files - files=$(gh pr view ${{ github.event.pull_request.number }} --json files -q '.files[].path') - - # Sanitize to avoid code injection - sanitized_files=$(echo "$files" | sed 's/[^a-zA-Z0-9._/-]/_/g') - - # Store the sanitized list of files in a temporary file to avoid env variable issues - echo "$sanitized_files" > modified_files.txt - - - name: Fetch PR message + # Use the GitHub API to fetch the list of changed files, one per + # line. Stored as a file so the verify step can read it without + # passing PR-controlled strings through a shell command. + gh pr view ${{ github.event.pull_request.number }} --json files -q '.files[].path' > modified_files.txt + echo "Changed files:" + cat modified_files.txt + + - name: Fetch PR description id: pr-message env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - # Use the GitHub API to fetch the PR message - pr_message=$(gh pr view ${{ github.event.pull_request.number }} --json body -q '.body') - - # Sanitize the PR message to avoid code injection, keeping the equal sign - sanitized_pr_message=$(echo "$pr_message" | sed 's/[^a-zA-Z0-9._/-=]/_/g') - - # Store the sanitized PR message - echo "$sanitized_pr_message" > pr_message.txt - - - name: Verify NEXT_CHANGELOG.md was modified or PR message contains NO_CHANGELOG=true + # Fetch the PR body; the verify step reads this as a literal string + # in Python, so shell escape rules never apply to its content. + gh pr view ${{ github.event.pull_request.number }} --json body -q '.body' > pr_body.txt + + - name: Verify NEXT_CHANGELOG.md was modified for each touched package run: | - # Read the sanitized files and PR message from the temporary files - modified_files=$(cat modified_files.txt) - pr_message=$(cat pr_message.txt) - - # Check if NEXT_CHANGELOG.md exists in the list of changed files - echo "Changed files: $modified_files" - if ! echo "$modified_files" | grep -q "NEXT_CHANGELOG.md"; then - echo "NEXT_CHANGELOG.md not modified." - - # Check if PR message contains NO_CHANGELOG=true - if echo "$pr_message" | grep -q "NO_CHANGELOG=true"; then - echo "NO_CHANGELOG=true found in PR message. Skipping changelog check." - exit 0 - else - echo "WARNING: file NEXT_CHANGELOG.md not changed. If this is expected, add NO_CHANGELOG=true to the PR message." - exit 1 - fi - fi + python3 <<'PYEOF' + import os + import re + import sys + + # Read inputs written by the previous steps. Reading as literal + # strings keeps PR-controlled content safely inert. + modified = {l for l in open('modified_files.txt').read().splitlines() if l} + pr_body = open('pr_body.txt').read() + + # Parse NO_CHANGELOG directive from the PR body. + # NO_CHANGELOG=true -> skip every package. + # NO_CHANGELOG=,,... -> skip only the listed packages. + skip_all = False + skip_packages = set() + m = re.search(r'NO_CHANGELOG=(\S+)', pr_body) + if m: + val = m.group(1).strip() + if val == 'true': + skip_all = True + else: + skip_packages = {p.strip().rstrip('/') for p in val.split(',') if p.strip()} + + if skip_all: + print("NO_CHANGELOG=true found in PR body. Skipping check.") + sys.exit(0) + + # Discover packages the same way tagging.py does: any directory that + # contains a .package.json is a release unit with its own changelog. + packages = [] + for dirpath, dirnames, filenames in os.walk('.'): + dirnames[:] = [d for d in dirnames if d not in ('.git', 'node_modules', 'dist')] + if '.package.json' in filenames: + rel = os.path.relpath(dirpath, '.') + packages.append('' if rel == '.' else rel) + + # Sub-package prefixes so a root-level package doesn't double-count + # files that live under a nested package. + sub_prefixes = tuple(p + '/' for p in packages if p) - - name: Comment on PR with instructions if needed - if: failure() # This step will only run if the previous step fails (i.e., if NEXT_CHANGELOG.md was not modified and NO_CHANGELOG=true was not in the PR message) + # For each package with modified files (excluding its own bookkeeping + # files), require that its NEXT_CHANGELOG.md was also modified. + missing = [] + for pkg in packages: + prefix = pkg + '/' if pkg else '' + next_changelog = prefix + 'NEXT_CHANGELOG.md' + changelog = prefix + 'CHANGELOG.md' + package_file = prefix + '.package.json' + in_pkg = [ + f for f in modified + if (not pkg or f.startswith(prefix)) + and f not in (changelog, package_file) + ] + if not pkg: + in_pkg = [f for f in in_pkg if not f.startswith(sub_prefixes)] + if not in_pkg: + continue + if next_changelog not in modified: + missing.append((pkg or '.', next_changelog)) + + # Honor per-package NO_CHANGELOG overrides. + missing = [(pkg, chg) for pkg, chg in missing if pkg not in skip_packages] + + if missing: + # Build the PR comment body and also emit it to the workflow log + # so failures are debuggable without clicking into the PR. + lines = [ + "", + "One or more packages were modified without updating their `NEXT_CHANGELOG.md`:", + "", + ] + for pkg, chg in missing: + lines.append(f"- `{pkg}` — expected a change to `{chg}`") + lines.append("") + lines.append("If this is intentional, add one of the following to the PR description and rerun this job:") + lines.append("") + lines.append("- `NO_CHANGELOG=true` — skip the check for every package.") + suggested = ",".join(pkg for pkg, _ in missing) + lines.append(f"- `NO_CHANGELOG={suggested}` — skip only the packages listed above.") + body = "\n".join(lines) + "\n" + with open('pr_comment.md', 'w') as f: + f.write(body) + sys.stdout.write(body) + sys.exit(1) + + print("All modified packages have NEXT_CHANGELOG.md entries.") + PYEOF + + - name: Comment on PR with the list of missing packages + if: failure() # Only runs if the verify step failed. env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - # Check if a comment exists with the instructions + # Replace any previous instructions comment so the list reflects the + # current PR state instead of accumulating stale entries. previous_comment_ids=$(gh api "repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments" \ --jq '.[] | select(.body | startswith("")) | .id') echo "Previous comment IDs: $previous_comment_ids" - - # If no previous comment exists, add one with instructions - if [ -z "$previous_comment_ids" ]; then - echo "Adding instructions comment." - gh pr comment ${{ github.event.pull_request.number }} --body \ - " - Please ensure that the NEXT_CHANGELOG.md file is updated with any relevant changes. - If this is not necessary for your PR, please include the following in your PR description: - NO_CHANGELOG=true - and rerun the job." - fi + for id in $previous_comment_ids; do + gh api "repos/${{ github.repository }}/issues/comments/$id" --method DELETE + done + gh pr comment ${{ github.event.pull_request.number }} --body-file pr_comment.md - name: Delete instructions comment on success - if: success() # This step will only run if the previous check passed (i.e., if NEXT_CHANGELOG.md was modified or NO_CHANGELOG=true is in the PR message) + if: success() # Only runs if the verify step passed. env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - # Check if there is a previous instructions comment + # Clean up any previous instructions comment now that the check is + # green. previous_comment_ids=$(gh api "repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments" \ --jq '.[] | select(.body | startswith("")) | .id') - - # If a comment exists, delete it - if [ -n "$previous_comment_ids" ]; then - echo "Deleting previous instructions comment." - for comment_id in $previous_comment_ids; do - gh api "repos/${{ github.repository }}/issues/comments/$comment_id" --method DELETE - done - else - echo "No instructions comment found to delete." - fi + if [ -n "$previous_comment_ids" ]; then + echo "Deleting previous instructions comments: $previous_comment_ids" + for id in $previous_comment_ids; do + gh api "repos/${{ github.repository }}/issues/comments/$id" --method DELETE + done + else + echo "No instructions comment found to delete." + fi From a3c4c6e3e551e1e30dd9fdad10f14af0ce08aaa5 Mon Sep 17 00:00:00 2001 From: Parth Bansal Date: Sun, 19 Apr 2026 21:57:40 +0000 Subject: [PATCH 2/2] add prerelease workflow --- .github/workflows/prepare-release.yml | 115 ++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 .github/workflows/prepare-release.yml diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml new file mode 100644 index 00000000..d2f17cf9 --- /dev/null +++ b/.github/workflows/prepare-release.yml @@ -0,0 +1,115 @@ +name: Prepare Release + +on: + push: + # Per-package tags produced by tagging.py, e.g. "settings/v0.2.0". + # Each tag push fires an independent prepare-release run, producing one + # tarball ready for release. + tags: + - "*/v*" + workflow_dispatch: + inputs: + tag: + description: "Tag to prepare (e.g. settings/v0.2.0)" + required: true + type: string + +concurrency: + group: prepare-release-${{ github.ref_name || inputs.tag }} + cancel-in-progress: false + +permissions: + contents: read + id-token: write + +jobs: + prepare: + name: Prepare ${{ github.ref_name || inputs.tag }} + runs-on: + group: databricks-protected-runner-group + labels: linux-ubuntu-latest + timeout-minutes: 20 + + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + # Fetch tags + full history so git for-each-ref can read the + # annotated tag body (where tagging.py stores release notes). + fetch-depth: 0 + ref: ${{ inputs.tag || github.ref }} + + - name: Parse tag into package and version + id: parse + run: | + tag="${{ inputs.tag || github.ref_name }}" + package="${tag%/v*}" + version="${tag##*/v}" + echo "tag=$tag" >> "$GITHUB_OUTPUT" + echo "package=$package" >> "$GITHUB_OUTPUT" + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "Parsed tag=$tag package=$package version=$version" + + - name: Validate package directory exists + run: | + if [ ! -f "packages/${{ steps.parse.outputs.package }}/package.json" ]; then + echo "::error::No package.json at packages/${{ steps.parse.outputs.package }}/" + echo "Tag ${{ steps.parse.outputs.tag }} does not correspond to a workspace package." + exit 1 + fi + + - name: Setup JFrog CLI + uses: jfrog/setup-jfrog-cli@279b1f629f43dd5bc658d8361ac4802a7ef8d2d5 # v4.9.1 + env: + JF_URL: https://databricks.jfrog.io + with: + oidc-provider-name: github-actions + + - name: Setup Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: "22" + cache: "npm" + + - name: Configure npm for JFrog + run: jf npmc --repo-resolve=db-npm + + - name: Install dependencies + run: jf npm ci + + - name: Build workspace + run: npm run build + + - name: Pack package tarball + id: pack + working-directory: packages/${{ steps.parse.outputs.package }} + run: | + # npm pack emits the filename on stdout; capture it so release can + # match it without guessing the scope's underscore-encoding. + tarball=$(npm pack --silent) + echo "tarball=packages/${{ steps.parse.outputs.package }}/${tarball}" >> "$GITHUB_OUTPUT" + echo "Packed $tarball" + + - name: Stage release artifacts + id: stage + run: | + mkdir -p release-artifacts + cp "${{ steps.pack.outputs.tarball }}" release-artifacts/ + echo "${{ steps.parse.outputs.version }}" > release-artifacts/VERSION + echo "${{ steps.parse.outputs.package }}" > release-artifacts/PACKAGE + echo "${{ steps.parse.outputs.tag }}" > release-artifacts/TAG + # tagging.py embeds the NEXT_CHANGELOG section into the annotated + # tag body; extract it verbatim for release to attach to the GitHub + # release. + git for-each-ref --format='%(body)' "refs/tags/${{ steps.parse.outputs.tag }}" > release-artifacts/changelog-diff.md + (cd release-artifacts && sha256sum *.tgz VERSION PACKAGE TAG changelog-diff.md > SHA256SUMS) + echo "Artifacts staged:" + ls -la release-artifacts/ + + - name: Upload release artifacts + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: sdk-js-release-${{ github.run_number }} + retention-days: 7 + if-no-files-found: error + path: release-artifacts/