Skip to content

feat: add script to export GitHub Projects V2 board to Markdown#167

Merged
joshjohanning merged 3 commits intomainfrom
add-export-project-to-markdown-script
Apr 22, 2026
Merged

feat: add script to export GitHub Projects V2 board to Markdown#167
joshjohanning merged 3 commits intomainfrom
add-export-project-to-markdown-script

Conversation

@joshjohanning
Copy link
Copy Markdown
Owner

This pull request adds a new script for exporting GitHub Project Boards (Projects V2) to a clean, shareable Markdown file. The script fetches all board items, preserves their Markdown formatting, and generates a well-structured document with a clickable table of contents and detailed metadata. Documentation has also been updated to describe this new tool.

Copilot AI review requested due to automatic review settings April 22, 2026 01:37
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new gh-cli shell script to export a GitHub Projects V2 board into a shareable Markdown document (TOC + per-item sections), and documents the new tool in the gh-cli README.

Changes:

  • Introduces export-project-board-to-markdown.sh to fetch Projects V2 items via GraphQL and render them as structured Markdown
  • Generates a Markdown table of contents plus per-item metadata tables, descriptions, and comments
  • Updates gh-cli/README.md with usage and feature notes for the new script
Show a summary per file
File Description
gh-cli/export-project-board-to-markdown.sh New exporter script that queries Projects V2 via GraphQL and writes a Markdown report
gh-cli/README.md Adds documentation entry for the new exporter script

Copilot's findings

Comments suppressed due to low confidence (4)

gh-cli/export-project-board-to-markdown.sh:310

  • Markdown table rows are built by directly interpolating field values. If any value contains | or newlines (common for free-text project fields), the table will render incorrectly. Escape | (e.g., \|) and convert newlines to <br> (or otherwise sanitize) for all values emitted inside the table.
        echo "| Field | Value |"
        echo "| --- | --- |"
        echo "| Type | $icon |"
        if [ -n "$repo_owner" ] && [ -n "$repo_name" ]; then
            echo "| Repository | \`$repo_owner/$repo_name\` |"
        fi
        if [ -n "$number" ] && [ -n "$url" ]; then
            echo "| Link | [#$number]($url) |"
        elif [ -n "$url" ]; then
            echo "| Link | [$url]($url) |"
        fi
        if [ -n "$state" ]; then
            if [ "$type" = "PullRequest" ] && [ "$merged" = "true" ]; then
                echo "| State | MERGED |"
            else
                echo "| State | $state |"
            fi
        fi

        # Pull out Status and Day custom fields up front so they appear in a consistent slot.
        status_value=$(echo "$item" | jq -r '[.fieldValues.nodes[]? | select(.field.name == "Status") | (.text // .name // .title // .date // (.number | tostring?))] | first // empty')
        day_value=$(echo "$item" | jq -r '[.fieldValues.nodes[]? | select(.field.name == "Day") | (.text // .name // .title // .date // (.number | tostring?))] | first // empty')
        if [ -n "$status_value" ]; then
            echo "| Status | $status_value |"
        fi
        if [ -n "$day_value" ]; then
            echo "| Day | $day_value |"
        fi

        if [ -n "$author" ]; then
            echo "| Author | @$author |"
        fi
        if [ -n "$created" ]; then
            created_fmt=$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "$created" "+%Y-%m-%d" 2>/dev/null || echo "$created")
            echo "| Created | $created_fmt |"
        fi

        # Assignees
        assignees=$(echo "$item" | jq -r '[.content.assignees.nodes[]?.login] | map("@" + .) | join(", ")')
        if [ -n "$assignees" ]; then
            echo "| Assignees | $assignees |"
        fi

        # Labels
        labels=$(echo "$item" | jq -r '[.content.labels.nodes[]?.name] | map("`" + . + "`") | join(", ")')
        if [ -n "$labels" ]; then
            echo "| Labels | $labels |"
        fi

        # Remaining custom field values (Title/Description/Body excluded; Status/Day already emitted)
        echo "$item" | jq -r '
          .fieldValues.nodes[]?
          | select(.field.name != null
                   and (.field.name | IN("Title", "Description", "Body", "Status", "Day") | not))
          | "| " + .field.name + " | " + (
              (.text // .name // .title // .date // (.number | tostring?)) // ""
            ) + " |"
        '

gh-cli/export-project-board-to-markdown.sh:221

  • Inside the per-item loop, the script invokes jq many times against the same JSON object (title/number/url/repo/state/author/etc.). For boards with many items, repeatedly parsing JSON can make the export noticeably slow. Consider extracting all needed fields in a single jq call per item (or emitting the full Markdown via one jq template) to reduce overhead.
        item=$(echo "$items_json" | jq -c ".[$i]")
        idx=$((i + 1))

        type=$(echo "$item" | jq -r '.content.__typename // "ProjectItem"')

        # Resolve title
        title=$(echo "$item" | jq -r '.content.title // empty')
        if [ -z "$title" ]; then
            title=$(echo "$item" | jq -r '[.fieldValues.nodes[]? | select(.field.name == "Title") | .text] | first // "Untitled"')
        fi

        number=$(echo "$item" | jq -r '.content.number // empty')
        url=$(echo "$item" | jq -r '.content.url // empty')
        repo_owner=$(echo "$item" | jq -r '.content.repository.owner.login // empty')
        repo_name=$(echo "$item" | jq -r '.content.repository.name // empty')
        state=$(echo "$item" | jq -r '.content.state // empty')
        merged=$(echo "$item" | jq -r '.content.merged // empty')
        author=$(echo "$item" | jq -r '.content.author.login // .content.creator.login // empty')
        created=$(echo "$item" | jq -r '.content.createdAt // empty')
        body=$(echo "$item" | jq -r '.content.body // empty')

gh-cli/export-project-board-to-markdown.sh:36

  • The GraphQL call is missing the X-Github-Next-Global-ID: 1 header. Add this header to the gh api graphql request so results use the new global node ID format (per GitHub’s GraphQL global ID migration guidance).
response=$(gh api graphql --paginate -f org="$org" -F projectNumber="$project_number" -f query='
  query($org: String!, $projectNumber: Int!, $endCursor: String) {

gh-cli/export-project-board-to-markdown.sh:170

  • jq’s group_by(.) requires the input array to be sorted by the grouping key; otherwise identical Status values that aren’t adjacent will be counted in separate groups. Sort the Status list (e.g., sort/sort_by(.)) before group_by(.) so the status breakdown counts are correct.
    status_summary=$(echo "$items_json" | jq -r '
      [ .[] | .fieldValues.nodes[]? | select(.field.name == "Status") | .name ]
      | group_by(.)
      | map({status: .[0], count: length})
      | sort_by(-.count)
      | map("\(.status): \(.count)")
      | join(" - ")
    ')
  • Files reviewed: 2/2 changed files
  • Comments generated: 2

Comment thread gh-cli/README.md Outdated
Comment thread gh-cli/export-project-board-to-markdown.sh Outdated
joshjohanning and others added 2 commits April 22, 2026 15:30
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…ename behavior

Agent-Logs-Url: https://github.com/joshjohanning/github-misc-scripts/sessions/00189b49-0cc0-4114-96f4-9345a93653b7

Co-authored-by: joshjohanning <19912012+joshjohanning@users.noreply.github.com>
@joshjohanning joshjohanning merged commit 704b0c5 into main Apr 22, 2026
6 checks passed
@joshjohanning joshjohanning deleted the add-export-project-to-markdown-script branch April 22, 2026 20:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants