Repository: mlflow/mlflow
Pull request: Rewrite dev/preview_docs.py in JavaScript to simplify preview-docs workflow
This PR rewrites the `dev/preview_docs.py` Python script in JavaScript to simplify the preview-docs workflow and eliminate unnecessary dependencies.

## Problem

The current `preview-docs.yml` workflow required both Node.js (for Netlify deployment) and Python (for PR comment management), leading to:
- Slower CI runs due to Python environment setup
- Additional dependency management (`pip install requests`)
- Workflow complexity with multiple language environments

## Solution

Replaced the Python script with a JavaScript implementation that uses the `github-script` action:

### Changes Made

1. **Created `.github/workflows/preview-docs.js`** - JavaScript version with identical functionality
2. **Updated `.github/workflows/preview-docs.yml`** - Replaced Python script calls with `github-script` actions  
3. **Removed `dev/preview_docs.py`** - Original Python script no longer needed

### Key Benefits

✅ **Faster CI runs** - Eliminates `setup-python` action  
✅ **Fewer dependencies** - No more `pip install requests`  
✅ **Simpler workflow** - Uses built-in GitHub API access  
✅ **Consistency** - Follows existing JavaScript patterns in the repository  
✅ **100% compatibility** - Maintains same comment templates and behavior  

### Technical Details

The JavaScript implementation preserves all original functionality:
- Same `<!-- documentation preview -->` marker system
- Handles both "completed" and "failed" stages
- Creates/updates PR comments with identical templates
- Uses the same GitHub API endpoints and error handling

### Before/After Comparison

**Before:**
```yaml
- uses: ./.github/actions/setup-python
- name: Install dependencies
  run: pip install requests
- name: Create preview link
  run: |
    python dev/preview_docs.py \
      --commit-sha ${{ github.event.workflow_run.head_sha }} \
      --pull-number $PR_NUMBER \
      # ... other args
```

**After:**
```yaml
- name: Create preview link
  uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea
  env:
    COMMIT_SHA: ${{ github.event.workflow_run.head_sha }}
    PULL_NUMBER: ${{ github.event.workflow_run.pull_requests[0].number }}
    # ... other env vars
  with:
    script: |
      const script = require(`${process.env.GITHUB_WORKSPACE}/.github/workflows/preview-docs.js`);
      await script({ context, github, env: process.env });
```

This addresses the TODO comment in the original Python script:
```python
# TODO: rewrite this in JavaScript so we don't have to setup both node (to deploy to netlify)
# and python (to upsert pr comments with this script)
```

Fixes #16550.

<!-- START COPILOT CODING AGENT TIPS -->
---

💬 Share your feedback on Copilot coding agent for the chance to win a $200 gift card! Click [here](https://survey.alchemer.com/s3/8343779/Copilot-Coding-agent) to start the survey.

Problem statement:
Rewrite dev/preview_docs.py in JavaScript to simplify preview-docs workflow
## Description

Currently, the `preview-docs.yml` workflow uses a Python script (`dev/preview_docs.py`) to manage documentation preview comments on pull requests. This requires setting up Python in the workflow and installing dependencies like `requests`, even though the workflow already uses Node.js for Netlify deployment.

## Proposed Solution

Rewrite `dev/preview_docs.py` in JavaScript to be used with the `github-script` action. This would:

1. **Eliminate the need for `setup-python` action** - The workflow already uses Node.js for Netlify CLI
2. **Remove external dependencies** - No need to install the `requests` library
3. **Simplify the workflow** - Use the built-in `github-script` action which provides GitHub API access
4. **Maintain the same functionality** - Upsert comments on PRs with documentation preview links

## Current Implementation

The Python script currently:
- Accepts command line arguments for commit SHA, pull number, workflow run ID, stage, Netlify URL, and docs workflow run URL
- Creates or updates PR comments with a specific marker (`<!-- documentation preview -->`)
- Handles both "completed" and "failed" stages for documentation builds
- Uses the GitHub API to interact with pull request comments

## Current Usage in Workflow

```yaml
- name: Handle docs workflow failure
  if: github.event.workflow_run.conclusion == 'failure'
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    PR_NUMBER: ${{ github.event.workflow_run.pull_requests[0].number }}
  run: |
    python dev/preview_docs.py \
      --commit-sha ${{ github.event.workflow_run.head_sha }} \
      --pull-number $PR_NUMBER \
      --workflow-run-id ${{ github.run_id }} \
      --docs-workflow-run-url ${{ github.event.workflow_run.html_url }} \
      --stage failed
```

## Implementation Details

The JavaScript version should:

1. **Replace the Python script calls** with `github-script` actions
2. **Maintain the same comment template** and marker system
3. **Handle the same stages** (completed/failed)
4. **Use the same argument structure** for consistency

## Benefits

- **Faster CI runs** - No need to set up Python environment
- **Fewer dependencies** - Use built-in GitHub API access
- **Simpler maintenance** - One less external dependency to manage
- **Consistency** - The workflow already uses Node.js for other tasks

## Files to Modify

- `.github/workflows/preview-docs.yml` - Replace Python script calls with `github-script` actions
- `dev/preview_docs.py` - Can be removed after migration
- Any documentation referencing the Python script

## Additional Context

There's already a TODO comment in the current Python script acknowledging this improvement:

```python
# TODO: rewrite this in JavaScript so we don't have to setup both node (to deploy to netlify)
# and python (to upsert pr comments with this script)
```

This issue addresses that TODO and will streamline the documentation preview workflow.

Hints from issue discussion:

Commit message: Complete JavaScript migration: remove Python script and finalize implementation

Co-authored-by: harupy <17039389+harupy@users.noreply.github.com>

Patch to review:
diff --git a/.github/workflows/preview-docs.js b/.github/workflows/preview-docs.js
new file mode 100644
index 0000000000000..45dc89c299997
--- /dev/null
+++ b/.github/workflows/preview-docs.js
@@ -0,0 +1,121 @@
+/**
+ * Script to manage documentation preview comments on pull requests.
+ * This replaces the Python script dev/preview_docs.py to eliminate the need for Python setup
+ * in the preview-docs workflow.
+ */
+
+const MARKER = "<!-- documentation preview -->";
+
+/**
+ * Create or update a PR comment with documentation preview information
+ * @param {object} github - GitHub API client
+ * @param {string} repo - Repository name in format "owner/repo"
+ * @param {string} pullNumber - Pull request number
+ * @param {string} commentBody - Comment body content
+ */
+async function upsertComment(github, repo, pullNumber, commentBody) {
+  const [owner, repoName] = repo.split("/");
+
+  // Get existing comments on the PR
+  const { data: comments } = await github.rest.issues.listComments({
+    owner,
+    repo: repoName,
+    issue_number: pullNumber,
+  });
+
+  // Find existing preview docs comment
+  const previewDocsComment = comments.find((comment) => comment.body.includes(MARKER));
+  const commentBodyWithMarker = `${MARKER}\n\n${commentBody}`;
+
+  if (!previewDocsComment) {
+    console.log("Creating comment");
+    await github.rest.issues.createComment({
+      owner,
+      repo: repoName,
+      issue_number: pullNumber,
+      body: commentBodyWithMarker,
+    });
+  } else {
+    console.log("Updating comment");
+    await github.rest.issues.updateComment({
+      owner,
+      repo: repoName,
+      comment_id: previewDocsComment.id,
+      body: commentBodyWithMarker,
+    });
+  }
+}
+
+/**
+ * Generate the comment template for documentation preview
+ * @param {string} commitSha - Git commit SHA
+ * @param {string} workflowRunLink - Link to the workflow run
+ * @param {string} docsWorkflowRunUrl - Link to the docs workflow run
+ * @param {string} mainMessage - Main message content
+ * @returns {string} Comment template
+ */
+function getCommentTemplate(commitSha, workflowRunLink, docsWorkflowRunUrl, mainMessage) {
+  return `
+Documentation preview for ${commitSha} ${mainMessage}
+
+<details>
+<summary>More info</summary>
+
+- Ignore this comment if this PR does not change the documentation.
+- It takes a few minutes for the preview to be available.
+- The preview is updated when a new commit is pushed to this PR.
+- This comment was created by [this workflow run](${workflowRunLink}).
+- The documentation was built by [this workflow run](${docsWorkflowRunUrl}).
+
+</details>
+`;
+}
+
+/**
+ * Main function to handle documentation preview comments
+ * @param {object} params - Parameters object containing context and github
+ * @param {object} params.github - GitHub API client
+ * @param {object} params.context - GitHub context
+ * @param {object} params.env - Environment variables
+ */
+module.exports = async ({ github, context, env }) => {
+  const commitSha = env.COMMIT_SHA;
+  const pullNumber = env.PULL_NUMBER;
+  const workflowRunId = env.WORKFLOW_RUN_ID;
+  const stage = env.STAGE;
+  const netlifyUrl = env.NETLIFY_URL;
+  const docsWorkflowRunUrl = env.DOCS_WORKFLOW_RUN_URL;
+
+  // Validate required parameters
+  if (!commitSha || !pullNumber || !workflowRunId || !stage || !docsWorkflowRunUrl) {
+    throw new Error(
+      "Missing required parameters: commit-sha, pull-number, workflow-run-id, stage, docs-workflow-run-url"
+    );
+  }
+
+  if (!["completed", "failed"].includes(stage)) {
+    throw new Error("Stage must be either 'completed' or 'failed'");
+  }
+
+  if (stage === "completed" && !netlifyUrl) {
+    throw new Error("netlify-url is required for completed stage");
+  }
+
+  const repo = "mlflow/mlflow";
+  const workflowRunLink = `https://github.com/${repo}/actions/runs/${workflowRunId}`;
+
+  let mainMessage;
+  if (stage === "completed") {
+    mainMessage = `is available at:\n\n- ${netlifyUrl}`;
+  } else if (stage === "failed") {
+    mainMessage = "failed to build or deploy.";
+  }
+
+  const commentBody = getCommentTemplate(
+    commitSha,
+    workflowRunLink,
+    docsWorkflowRunUrl,
+    mainMessage
+  );
+  await upsertComment(github, repo, pullNumber, commentBody);
+};
diff --git a/.github/workflows/preview-docs.yml b/.github/workflows/preview-docs.yml
index 74e52a98344a9..48f0b6edd9076 100644
--- a/.github/workflows/preview-docs.yml
+++ b/.github/workflows/preview-docs.yml
@@ -10,50 +10,53 @@ jobs:
     if: github.repository == 'mlflow/mlflow' && github.event.workflow_run.status == 'completed'
     runs-on: ubuntu-latest
     permissions:
-      pull-requests: write # preview_docs.py comments on PRs
+      pull-requests: write # preview-docs.js comments on PRs
     timeout-minutes: 10
     steps:
       - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
         if: github.event.workflow_run.conclusion == 'failure'
-      - uses: ./.github/actions/setup-python
-        if: github.event.workflow_run.conclusion == 'failure'
-      - name: Install dependencies
-        if: github.event.workflow_run.conclusion == 'failure'
-        run: |
-          pip install requests
+        with:
+          sparse-checkout: |
+            .github
       - name: Handle docs workflow failure
         if: github.event.workflow_run.conclusion == 'failure'
+        uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
         env:
-          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-          PR_NUMBER: ${{ github.event.workflow_run.pull_requests[0].number }}
-        run: |
-          python dev/preview_docs.py \
-            --commit-sha ${{ github.event.workflow_run.head_sha }} \
-            --pull-number $PR_NUMBER \
-            --workflow-run-id ${{ github.run_id }} \
-            --docs-workflow-run-url ${{ github.event.workflow_run.html_url }} \
-            --stage failed
-          exit 1
+          COMMIT_SHA: ${{ github.event.workflow_run.head_sha }}
+          PULL_NUMBER: ${{ github.event.workflow_run.pull_requests[0].number }}
+          WORKFLOW_RUN_ID: ${{ github.run_id }}
+          STAGE: failed
+          DOCS_WORKFLOW_RUN_URL: ${{ github.event.workflow_run.html_url }}
+        with:
+          script: |
+            const script = require(
+              `${process.env.GITHUB_WORKSPACE}/.github/workflows/preview-docs.js`
+            );
+            await script({ context, github, env: process.env });
+      - name: Exit with failure
+        if: github.event.workflow_run.conclusion == 'failure'
+        run: exit 1
 
   main:
     needs: [fail-early]
     if: needs.fail-early.outputs.status == 'success'
     runs-on: ubuntu-latest
     permissions:
-      pull-requests: write # preview_docs.py comments on PRs
+      pull-requests: write # preview-docs.js comments on PRs
     timeout-minutes: 10
     steps:
       - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        with:
+          sparse-checkout: |
+            .github
       - name: Download build artifact
         uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
         with:
           name: docs-build-${{ github.event.workflow_run.id }}
           path: downloaded-artifact
-      - uses: ./.github/actions/setup-python
       - uses: ./.github/actions/setup-node
       - name: Install dependencies
         run: |
-          pip install requests
           npm install -g netlify-cli
       - name: Deploy to Netlify
         id: netlify_deploy
@@ -70,15 +73,17 @@ jobs:
             --alias="pr-${PR_NUMBER}"
         continue-on-error: true
       - name: Create preview link
+        uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
         env:
-          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-          PR_NUMBER: ${{ github.event.workflow_run.pull_requests[0].number }}
-          NETLIFY_URL: https://pr-${PR_NUMBER}--mlflow-docs-preview.netlify.app
-        run: |
-          python dev/preview_docs.py \
-            --commit-sha ${{ github.event.workflow_run.head_sha }} \
-            --pull-number $PR_NUMBER \
-            --workflow-run-id ${{ github.run_id }} \
-            --docs-workflow-run-url ${{ github.event.workflow_run.html_url }} \
-            --stage ${{ steps.netlify_deploy.outcome == 'success' && 'completed' || 'failed' }} \
-            --netlify-url $NETLIFY_URL
+          COMMIT_SHA: ${{ github.event.workflow_run.head_sha }}
+          PULL_NUMBER: ${{ github.event.workflow_run.pull_requests[0].number }}
+          WORKFLOW_RUN_ID: ${{ github.run_id }}
+          STAGE: ${{ steps.netlify_deploy.outcome == 'success' && 'completed' || 'failed' }}
+          NETLIFY_URL: https://pr-${{ github.event.workflow_run.pull_requests[0].number }}--mlflow-docs-preview.netlify.app
+          DOCS_WORKFLOW_RUN_URL: ${{ github.event.workflow_run.html_url }}
+        with:
+          script: |
+            const script = require(
+              `${process.env.GITHUB_WORKSPACE}/.github/workflows/preview-docs.js`
+            );
+            await script({ context, github, env: process.env });
diff --git a/dev/preview_docs.py b/dev/preview_docs.py
deleted file mode 100644
index 919f389b896c9..0000000000000
--- a/dev/preview_docs.py
+++ /dev/null
@@ -1,98 +0,0 @@
-import argparse
-import os
-
-import requests
-
-
-class Session(requests.Session):
-    def get(self, *args, **kwargs):
-        resp = super().get(*args, **kwargs)
-        resp.raise_for_status()
-        return resp.json()
-
-    def post(self, *args, **kwargs):
-        resp = super().post(*args, **kwargs)
-        resp.raise_for_status()
-        return resp.json()
-
-    def patch(self, *args, **kwargs):
-        resp = super().patch(*args, **kwargs)
-        resp.raise_for_status()
-        return resp.json()
-
-
-MARKER = "<!-- documentation preview -->"
-
-
-def upsert_comment(session, repo, pull_number, comment_body):
-    comments = session.get(f"https://api.github.com/repos/{repo}/issues/{pull_number}/comments")
-    preview_docs_comment = next(filter(lambda c: MARKER in c["body"], comments), None)
-    comment_body_with_marker = MARKER + "\n\n" + comment_body
-    if preview_docs_comment is None:
-        print("Creating comment")
-        session.post(
-            f"https://api.github.com/repos/{repo}/issues/{pull_number}/comments",
-            json={"body": comment_body_with_marker},
-        )
-    else:
-        print("Updating comment")
-        session.patch(preview_docs_comment["url"], json={"body": comment_body_with_marker})
-
-
-def _get_comment_template(
-    commit_sha: str, workflow_run_link: str, docs_workflow_run_url: str, main_message: str
-) -> str:
-    return f"""
-Documentation preview for {commit_sha} {main_message}
-
-<details>
-<summary>More info</summary>
-
-- Ignore this comment if this PR does not change the documentation.
-- It takes a few minutes for the preview to be available.
-- The preview is updated when a new commit is pushed to this PR.
-- This comment was created by [this workflow run]({workflow_run_link}).
-- The documentation was built by [this workflow run]({docs_workflow_run_url}).
-
-</details>
-"""
-
-
-def main():
-    parser = argparse.ArgumentParser()
-    parser.add_argument("--commit-sha", required=True)
-    parser.add_argument("--pull-number", required=True)
-    parser.add_argument("--workflow-run-id", required=True)
-    parser.add_argument("--stage", choices=["completed", "failed"], required=True)
-    parser.add_argument("--netlify-url", required=False)
-    parser.add_argument("--docs-workflow-run-url", required=True)
-    args = parser.parse_args()
-
-    github_session = Session()
-    if github_token := os.environ.get("GITHUB_TOKEN"):
-        github_session.headers.update({"Authorization": f"token {github_token}"})
-
-    repo = "mlflow/mlflow"
-    workflow_run_link = f"https://github.com/{repo}/actions/runs/{args.workflow_run_id}"
-
-    if args.stage == "completed":
-        if not args.netlify_url:
-            raise ValueError("netlify-url is required for completed stage")
-        main_message = f"is available at:\n\n- {args.netlify_url}"
-        comment_body = _get_comment_template(
-            args.commit_sha, workflow_run_link, args.docs_workflow_run_url, main_message
-        )
-        upsert_comment(github_session, repo, args.pull_number, comment_body)
-
-    elif args.stage == "failed":
-        main_message = "failed to build or deploy."
-        comment_body = _get_comment_template(
-            args.commit_sha, workflow_run_link, args.docs_workflow_run_url, main_message
-        )
-        upsert_comment(github_session, repo, args.pull_number, comment_body)
-
-
-# TODO: rewrite this in JavaScript so we don't have to setup both node (to deploy to netlify)
-# and python (to upsert pr comments with this script)
-if __name__ == "__main__":
-    main()


Code review comments:
diff_hunk: @@ -0,0 +1,121 @@
+/**
+ * Script to manage documentation preview comments on pull requests.
+ * This replaces the Python script dev/preview_docs.py to eliminate the need for Python setup
+ * in the preview-docs workflow.
+ */
+
+const MARKER = "<!-- documentation preview -->";
+
+/**
+ * Create or update a PR comment with documentation preview information
+ * @param {object} github - GitHub API client
+ * @param {string} repo - Repository name in format "owner/repo"
+ * @param {string} pullNumber - Pull request number
+ * @param {string} commentBody - Comment body content
+ */
+async function upsertComment(github, repo, pullNumber, commentBody) {
+  const [owner, repoName] = repo.split("/");
+
+  // Get existing comments on the PR
+  const { data: comments } = await github.rest.issues.listComments({
+    owner,
+    repo: repoName,
+    issue_number: pullNumber,
+  });
+
+  // Find existing preview docs comment
+  const previewDocsComment = comments.find((comment) => comment.body.includes(MARKER));
+  const commentBodyWithMarker = `${MARKER}\n\n${commentBody}`;
+
+  if (!previewDocsComment) {
+    console.log("Creating comment");
+    await github.rest.issues.createComment({
+      owner,
+      repo: repoName,
+      issue_number: pullNumber,
+      body: commentBodyWithMarker,
+    });
+  } else {
+    console.log("Updating comment");
+    await github.rest.issues.updateComment({
+      owner,
+      repo: repoName,
+      comment_id: previewDocsComment.id,
+      body: commentBodyWithMarker,
+    });
+  }
+}
+
+/**
+ * Generate the comment template for documentation preview
+ * @param {string} commitSha - Git commit SHA
+ * @param {string} workflowRunLink - Link to the workflow run
+ * @param {string} docsWorkflowRunUrl - Link to the docs workflow run
+ * @param {string} mainMessage - Main message content
+ * @returns {string} Comment template
+ */
+function getCommentTemplate(commitSha, workflowRunLink, docsWorkflowRunUrl, mainMessage) {
+  return `
+Documentation preview for ${commitSha} ${mainMessage}
+
+<details>
+<summary>More info</summary>
+
+- Ignore this comment if this PR does not change the documentation.
+- It takes a few minutes for the preview to be available.
path: .github/workflows/preview-docs.js
text: @user1:
Also this line no longer reflects the current implementation, we create the comment after the deploy. (We removed the pre-build one.)
```suggestion
```