Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save 0xdevalias/7807fa0a8febe648a5e6f70a3a4ac0b6 to your computer and use it in GitHub Desktop.
Save 0xdevalias/7807fa0a8febe648a5e6f70a3a4ac0b6 to your computer and use it in GitHub Desktop.
Proof of concept GitHub Actions workflow that converts top-level PR comments into file-attached review comments for better thread organization.

Convert Top-Level PR Comments to File-Attached Review Comments via GitHub Actions

Table of Contents

Overview

Proof of concept GitHub Actions workflow that converts top-level PR comments into file-attached review comments for better thread organization. It moves comments to a dedicated placeholder file (subject_type: "file") and deletes the original, enabling structured discussions without external tools. 🚀

Originally explored in this discussion and re-shared in this discussion.

Features

  • Detects new top-level PR comments
  • Moves them to a placeholder file as review comments (subject_type: "file")
  • Deletes the original comment to keep discussions clean
  • Ensures the placeholder file exists before posting
  • Uses an ADMIN_TOKEN for proper API permissions

This approach enables structured PR conversations without third-party tools like Pullpo. 🚀

Code

name: Convert PR Top-Level Comment to File Comment

on:
  issue_comment:
    types: [created]

env:
  COMMITTER_NAME: "github-actions"
  COMMITTER_EMAIL: "github-actions@github.com"

jobs:
  convert-comment:
    runs-on: ubuntu-latest
    if: ${{ github.event.issue.pull_request != null }}
    env:
      PLACEHOLDER_FILE: "thread-comments/pr_${{ github.event.issue.number }}_threads.txt"
    steps:
      - name: Get PR details
        id: pr_details
        uses: actions/github-script@v6
        with:
          script: |
            const pull_number = context.payload.issue.number;
            const { data: pr } = await github.rest.pulls.get({
              owner: context.repo.owner,
              repo: context.repo.repo,
              pull_number: pull_number
            });
            core.setOutput("head_ref", pr.head.ref);
            core.setOutput("head_sha", pr.head.sha);
          token: ${{ secrets.ADMIN_TOKEN }}

      - name: Checkout PR branch
        uses: actions/checkout@v4
        with:
          ref: ${{ steps.pr_details.outputs.head_ref }}
          token: ${{ secrets.ADMIN_TOKEN }}
          fetch-depth: 0

      - name: Ensure placeholder file exists
        id: ensure_placeholder
        run: |
          if [ -f "${PLACEHOLDER_FILE}" ]; then
            echo "Placeholder file exists. Skipping creation."
          else
            mkdir -p "$(dirname "${PLACEHOLDER_FILE}")"
            cat <<EOF > "${PLACEHOLDER_FILE}"
# Threaded Discussion Placeholder

This file is used to anchor threaded comments for this pull request.
Do not modify this file manually.

<!-- Auto-generated by GitHub Action -->
EOF
            git config user.name "${COMMITTER_NAME}"
            git config user.email "${COMMITTER_EMAIL}"
            git add "${PLACEHOLDER_FILE}"
            git commit -m "Add placeholder file for threaded discussions"
            git push origin ${{ steps.pr_details.outputs.head_ref }}
          fi

      - name: Create file-level review comment on placeholder file
        id: create_review_comment
        uses: actions/github-script@v6
        with:
          script: |
            const owner = context.repo.owner;
            const repo = context.repo.repo;
            const pull_number = context.payload.issue.number;
            const commit_id = "${{ steps.pr_details.outputs.head_sha }}";
            const placeholderPath = "${PLACEHOLDER_FILE}";
            const body = `**Threaded comment by @${context.payload.comment.user.login}:**\n\n${context.payload.comment.body}`;
            // Using subject_type "file" creates a file-level review comment.
            const { data: reviewComment } = await github.rest.pulls.createReviewComment({
              owner,
              repo,
              pull_number,
              commit_id,
              path: placeholderPath,
              body: body,
              subject_type: "file"
            });
            core.info("Created review comment with ID: " + reviewComment.id);
          token: ${{ secrets.ADMIN_TOKEN }}

      - name: Delete original top-level comment
        uses: actions/github-script@v6
        with:
          script: |
            await github.rest.issues.deleteComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              comment_id: context.payload.comment.id
            });
            core.info("Deleted original top-level comment");
          token: ${{ secrets.ADMIN_TOKEN }}

Original Discussion Comment

The following is the original discussion comment, for context/reference:

Looking at the setup docs for this, it seems the threading feature is enabled by a GitHub action that they include inline on the docs page

.github/workflows/pullpo.yaml
## .github/workflows/pullpo.yaml

    name: Pullpo PR Threads

    on:
    pull_request:
        types: [opened]

    jobs:
    pullpo_threads:
        runs-on: ubuntu-latest
        permissions:
        contents: write
        steps:
        - uses: actions/checkout@v4
            with:
            ref: ${{ github.event.pull_request.head.ref }}
            fetch-depth: 0
        - name: Create Pullpo thread file with PR SHA
            run: |
            mkdir -p 一pullpo一
            rm -rf 一pullpo一/*
            echo "# Pullpo Threaded Conversations on GitHub
            ## What is the 一pullpo一 directory I see on my project?
            The `一pullpo一` directory allows your team to respond to comments that do not reference code in a
            dedicated thread, instead of having to \"quote reply\" which makes following a conversation more
            complex than it needs to be.

            **DO NOT REMOVE OR .GITIGNORE THIS DIRECTORY OR THE AUTO-GENERATED FILES IT CONTAINS**. Doing so will
            prevent you, your reviewers and ohter contributors from being able to have threaded messages in GitHub.
            ## What files does it contain?
            The files inside this directory are created by a workflow (which you can probably find in `.github/workflows`)
            that is executed when a new pull request is open in this repository. This file is added in a new commit to
            your pull request. The **Pullpo GitHub App** transforms your normal comments into threaded comments by
            commenting on this uniquely named file.
            ## Will this directory keep getting bigger and bigger?
            No, the workflow removes all previous files in the directory. Since files have unique names within a
            repository, this avoids merge conflicts and also prevents an infinetly increasing file list. If the 
            `一pullpo一` directory contains more than one file, it is likely because multiple pull requests were open at
            the same time at one point.
            ## How will this appear on my Pull Request `Conversation` or `Files Changed` tabs?
            The `Conversation` tab will have the new threaded comments. Ideally you'll have 
            [logged into Pullpo](https://pullpo.io/login/github) with your GitHub account so the Pullpo App will be able
            to replace the new comments as authored by you. Otherwise they will show up as an action that the bot made and
            you'll be tagged in the new threaded comment. 

            As for the `Files Changed` tab, the `一pullpo一` directory has been named by prefixing a
            special character that sorts it to the bottom of the page, so all comment threads will sit at the bottom and
            won't disturb anyone trying to read your changes.
            ## So, what now...?
            Keep contributing as you're used to; your pull requests will receive an additional commit that enables 
            threaded comments. Other than that everything stays as usual.
            " > 一pullpo一/README.md
            touch 一pullpo一/pr_${{ github.event.pull_request.number }}_threads.txt
            git config user.name github-actions
            git config user.email github-actions@github.com
            git add .
            git commit -m "add-pullpo-pr-threads"
            git push

Summarizing that GitHub action with ChatGPT 4o:

This GitHub Action is designed to integrate Pullpo's threaded conversation feature into pull requests by automatically creating a special directory and files when a new PR is opened. Here's a high-level breakdown of what it does:

  1. Triggered on New PRs

    • The action runs when a new pull request is opened.
  2. Checks Out the PR Branch

    • It checks out the PR branch with full history (fetch-depth: 0) to allow proper commit history handling.
  3. Creates a Special Directory (一pullpo一)

    • The action ensures the existence of a directory named 一pullpo一.
    • Any previous files in this directory are removed to keep it clean.
  4. Adds a README File Explaining Pullpo

    • It creates a README.md inside the directory, explaining that Pullpo enables threaded discussions for PR comments.
  5. Generates a Unique Thread File

    • A placeholder file is created (pr_<PR_NUMBER>_threads.txt), which will be used by Pullpo to track threaded conversations.
  6. Commits & Pushes the Changes

    • The workflow sets up Git with github-actions as the user.
    • The new directory and files are committed and pushed to the PR branch.

Purpose

This automation ensures that every new pull request includes the necessary files for Pullpo to enable threaded discussions on GitHub, allowing more structured conversations instead of relying on manual quoting.

Looking at the demo PR's 'How does it work?' section:

When a pull request is opened, it creates a special folder called 一pullpo一 if it doesn't already exist. We named it this way so that it always appears at the end of the files changed tab. Inside this folder, a simple text file is created to track discussions related to this specific pull request. Based on how it works, these files will never create merge conflicts.

Then when someone creates an issue comment on GitHub (or in a PR Slack channel), the Pullpo app is going to take that message and transform it into a file comment message referencing this newly created file.

Basically, this action sets up a spot for threaded conversations linked to pull requests. It helps the Pullpo app to organize comments more neatly.


Based on the above:

This requires external tool subscription? and unclear if it's free or not.

@romanr From a quick skim, the GitHub action seems to do very little by itself, only creating a the placeholder file that the main app uses. It seems the bulk of this functionality will be handled by the Pullpo app itself; and therefore based on what I can determine from a skim of their pricing page, sounds like it will require a paid subscription.

Based on the description of how this feature works, at a high level, it seems like the Pullpo app will just notice when someone has made a comment on the main PR, convert that to a new file comment on the 一pullpo一/pr_<PR_NUMBER>_threads.txt placeholder file (creating the comment as the user if it has permission to; and falling back to posting as the app bot if it doesn't have permissions to post as that user), and then deletes the old top-level comment.

Based on that functionality.. you could probably implement something similar purely within a GitHub action alone if you really wanted to... though personally, I don't really like the idea of littering my PR's with an unrelated file just to serve as a placeholder for where to create these new comment threads on.

I haven't deeply reviewed/tested the following GitHub Action, but here's a quick proof-of-concept I knocked up for this using ChatGPT o3-mini-high. It can't post the new comment as the user themselves.. but aside from that I think it pretty much implements the same basic workflow/etc:

name: Convert PR Top-Level Comment to File Comment

on:
  issue_comment:
    types: [created]

env:
  COMMITTER_NAME: "github-actions"
  COMMITTER_EMAIL: "github-actions@github.com"

jobs:
  convert-comment:
    runs-on: ubuntu-latest
    if: ${{ github.event.issue.pull_request != null }}
    env:
      PLACEHOLDER_FILE: "thread-comments/pr_${{ github.event.issue.number }}_threads.txt"
    steps:
      - name: Get PR details
        id: pr_details
        uses: actions/github-script@v6
        with:
          script: |
            const pull_number = context.payload.issue.number;
            const { data: pr } = await github.rest.pulls.get({
              owner: context.repo.owner,
              repo: context.repo.repo,
              pull_number: pull_number
            });
            core.setOutput("head_ref", pr.head.ref);
            core.setOutput("head_sha", pr.head.sha);
          token: ${{ secrets.ADMIN_TOKEN }}

      - name: Checkout PR branch
        uses: actions/checkout@v4
        with:
          ref: ${{ steps.pr_details.outputs.head_ref }}
          token: ${{ secrets.ADMIN_TOKEN }}
          fetch-depth: 0

      - name: Ensure placeholder file exists
        id: ensure_placeholder
        run: |
          if [ -f "${PLACEHOLDER_FILE}" ]; then
            echo "Placeholder file exists. Skipping creation."
          else
            mkdir -p "$(dirname "${PLACEHOLDER_FILE}")"
            cat <<EOF > "${PLACEHOLDER_FILE}"
# Threaded Discussion Placeholder

This file is used to anchor threaded comments for this pull request.
Do not modify this file manually.

<!-- Auto-generated by GitHub Action -->
EOF
            git config user.name "${COMMITTER_NAME}"
            git config user.email "${COMMITTER_EMAIL}"
            git add "${PLACEHOLDER_FILE}"
            git commit -m "Add placeholder file for threaded discussions"
            git push origin ${{ steps.pr_details.outputs.head_ref }}
          fi

      - name: Create file-level review comment on placeholder file
        id: create_review_comment
        uses: actions/github-script@v6
        with:
          script: |
            const owner = context.repo.owner;
            const repo = context.repo.repo;
            const pull_number = context.payload.issue.number;
            const commit_id = "${{ steps.pr_details.outputs.head_sha }}";
            const placeholderPath = "${PLACEHOLDER_FILE}";
            const body = `**Threaded comment by @${context.payload.comment.user.login}:**\n\n${context.payload.comment.body}`;
            // Using subject_type "file" creates a file-level review comment.
            const { data: reviewComment } = await github.rest.pulls.createReviewComment({
              owner,
              repo,
              pull_number,
              commit_id,
              path: placeholderPath,
              body: body,
              subject_type: "file"
            });
            core.info("Created review comment with ID: " + reviewComment.id);
          token: ${{ secrets.ADMIN_TOKEN }}

      - name: Delete original top-level comment
        uses: actions/github-script@v6
        with:
          script: |
            await github.rest.issues.deleteComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              comment_id: context.payload.comment.id
            });
            core.info("Deleted original top-level comment");
          token: ${{ secrets.ADMIN_TOKEN }}

Originally posted by @0xdevalias in https://github.com/orgs/community/discussions/5633#discussioncomment-12088921

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment