Github Actions 101

This blog post walks through how we use GitHub Actions to automate fetching completed issues by label: bug counts, enhancements, and documentation updates.

In this guide, you’ll learn:

  • What GitHub Actions are and how to set them up
  • How to securely reference PAT tokens to access private repo data
  • Breaking down each part of a GitHub Action workflow
  • Why we use GraphQL for fetching data
  • How to output JSON for frontend use

What are GitHub Actions?

GitHub Actions are automation workflows you can trigger directly in your GitHub repositories. Whether you want to run tests, deploy code, or automate administrative tasks (like tracking issues), Actions make it easy to build and deploy code automatically.

How it Works:

  • Event-driven: Actions trigger on events (like pushing to a repo or opening an issue).
  • Flexible: You can run custom scripts or use pre-built actions from the GitHub Marketplace.
  • Scalable: Actions run on virtual environments (Ubuntu).

For our issue tracker, we use GitHub Actions to fetch the latest bug/enhancement data daily and store it in a JSON file that the frontend reads.


Setting Up GitHub Personal Access Tokens (PAT)

Since our repositories are private, we need a secure way to fetch issue data. GitHub Personal Access Tokens (PAT) grant fine-grained access to your repositories.

Steps to Generate a PAT:

  1. Go to Settings > Developer Settings > Personal Access Tokens on GitHub.
  2. Click Generate new token and select the following scopes:
    • repo – Full control of private repositories
    • read:org – Read organization-level information
    • issues – Read issues data
  3. Copy the token – this is your key for API access.

Storing and Referencing the PAT Securely:

  • In your repository, go to Settings > Secrets and Variables > Actions.
  • Click New Repository Secret and name it PAT_TOKEN.
  • In your GitHub Actions workflow, reference the secret like this:
TOKEN=${{ secrets.PAT_TOKEN }}

Breaking Down the GitHub Action Workflow

Now that our token is set up, let’s break down the GitHub Action workflow we use to track issues. Each repository (API, game, and website) has a separate workflow, outputting to its own JSON file.

.github/workflows
    - api-issue-tracker.yml
    - hoa-issue-tracker.yml
    - website-issue-tracker.yml

public
    - api-issue-count.json
    - frontend-issue-count.json
    - hoa-issue-count.json

Example Workflow

name: Track 3ee API Resolved Issues

on:
  schedule:
    - cron: '0 0 * * *'  #Runs daily at midnight
  workflow_dispatch:  #Manual trigger

permissions:
  contents: write
  issues: read

jobs:
  track-issues:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v3
        
      - name: Fetch All Closed Issues (GraphQL)
        run: |
          TOKEN=${{ secrets.PAT_TOKEN }}
          OWNER="3ee-Games"
          REPO="3ee-api"

          fetch_issues() {
            LABEL=$1
            END_CURSOR=$2

            QUERY="{"query": "query {
              repository(owner: \"$OWNER\", name: \"$REPO\") {
                issues(first: 100, labels: [\"$LABEL\"], states: CLOSED, after: $END_CURSOR) {
                  totalCount
                  pageInfo {
                    endCursor
                    hasNextPage
                  }
                }
              }
            }"}"
            
            echo $QUERY
          }

          count_issues_by_label() {
            LABEL=$1
            TOTAL_COUNT=0
            END_CURSOR="null"

            while :; do
              QUERY=$(fetch_issues "$LABEL" "$END_CURSOR")

              RESPONSE=$(curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d "$QUERY" https://api.github.com/graphql)

              BUG_COUNT=$(echo $RESPONSE | jq '.data.repository.issues.totalCount')
              TOTAL_COUNT=$((TOTAL_COUNT + BUG_COUNT))

              HAS_NEXT_PAGE=$(echo $RESPONSE | jq '.data.repository.issues.pageInfo.hasNextPage')
              END_CURSOR=$(echo $RESPONSE | jq -r '.data.repository.issues.pageInfo.endCursor')

              if [ "$HAS_NEXT_PAGE" != "true" ]; then
                break
              fi
            done

            echo "$TOTAL_COUNT"
          }

          BUG_COUNT=$(count_issues_by_label "bug")
          DOC_COUNT=$(count_issues_by_label "documentation")
          ENHANCEMENT_COUNT=$(count_issues_by_label "enhancement")

          mkdir -p public
          echo "{
            "resolved_bugs": $BUG_COUNT,
            "resolved_documentation": $DOC_COUNT,
            "resolved_enhancements": $ENHANCEMENT_COUNT
          }" > public/api-issue-count.json

      - name: Commit and Push Issue Counts
        run: |
          git config user.name "github-actions"
          git config user.email "actions@github.com"
          git add public/api-issue-count.json
          git commit -m "Update issue counts" || echo "No changes to commit"
          git push

Why Use GraphQL?

GitHub’s REST API can be limiting when fetching large amounts of data. GraphQL allows us to request exactly the data we need in a single query, reducing the number of API calls.

  • Efficiency: Fetches all labeled issues in one request.
  • Pagination: Handles pagination gracefully using endCursor and hasNextPage.
  • Customizable: We can adjust queries dynamically by passing labels or states.

In the next section, I’ll illustrate the difference between using GraphQL and REST, from the query to the response and how we can use that response to build a frontend component.

GraphQL returning exactly what we want

GraphQL returns only the requested fields (title, state, and labels). No unnecessary data is retrieved.

Query:


{
  repository(owner: "3ee-Games", name: "3ee-api") {
    issues(first: 10, labels: ["bug"], states: CLOSED) {
      edges {
        node {
          title
          state
          labels(first: 5) {
            edges {
              node {
                name
              }
            }
          }
        }
      }
    }
  }
}

Output:

{
  "data": {
    "repository": {
      "issues": {
        "edges": [
          {
            "node": {
              "title": "Fix login bug",
              "state": "CLOSED",
              "labels": {
                "edges": [
                  { "node": { "name": "bug" } }
                ]
              }
            }
          },
          {
            "node": {
              "title": "Resolve API timeout",
              "state": "CLOSED",
              "labels": {
                "edges": [
                  { "node": { "name": "bug" } },
                  { "node": { "name": "urgent" } }
                ]
              }
            }
          }
        ]
      }
    }
  }
}

REST returning everything

REST returns everything by default (user info, timestamps, full issue body, comments, etc.), even if you don’t need it.

Query:

curl -s -H "Authorization: token $TOKEN" 
  "https://api.github.com/repos/3ee-Games/3ee-api/issues?state=closed&labels=bug"

Output:

[
  {
    "id": 123456789,
    "node_id": "MDU6SXNzdWUxMjM0NTY3ODk=",
    "title": "Fix login bug",
    "state": "closed",
    "labels": [
      {
        "id": 987654321,
        "node_id": "MDU6TGFiZWw5ODc2NTQzMjE=",
        "name": "bug",
        "color": "d73a4a",
        "default": true,
        "description": "Something isn't working"
      }
    ],
    "locked": false,
    "comments": 5,
    "created_at": "2023-01-01T12:00:00Z",
    "updated_at": "2023-01-05T15:30:00Z",
    "closed_at": "2023-01-05T15:30:00Z",
    "author_association": "CONTRIBUTOR",
    "body": "Steps to reproduce the issue..."
  }
]

This is why GraphQL is more efficient for fetching exactly what you need, reducing payload size and improving performance.


Outputting to JSON for Frontend Use

Each time the action runs, it generates a JSON file that looks like this:

{
  "resolved_bugs": 41,
  "resolved_documentation": 9,
  "resolved_enhancements": 57
}

These files live in the public folder and are read by frontend components to display live issue counts: https://github.com/3ee-Games/github-actions/tree/main/public

We can build a simple component that makes a call to those json files the actions created. In this example, we’ll log the response:


async function fetchIssueData() {
    try {
        const response = await fetch('https://raw.githubusercontent.com/3ee-Games/github-actions/refs/heads/main/public/api-issue-count.json');
        
        if (!response.ok) {
            throw new Error(`Failed to fetch: ${response.status}`);
        }

        const data = await response.json();
        console.log('Resolved Bugs:', data.resolved_bugs);
        console.log('Resolved Documentation Issues:', data.resolved_documentation);
        console.log('Resolved Enhancements:', data.resolved_enhancements);
    } 
    catch (error) {
        console.error('Error fetching issue data:', error);
    }
}

fetchIssueData();

📄 You can copy and paste this code in your browser’s console and it will return a json object.

The next step is to build out a frontend component using html and css. Below is the component that was built and used on the homepage of this website:

Issue Tracker

Keeping players informed about the ongoing improvements in our games is important. This tracker offers a behind-the-scenes look at bugs that have been fixed, new features that have been added, and other enhancements in progress. It provides a transparent view of the work being done to create the best experience possible.

Bugs Resolved
Games 0
API Services 0
Website 0
Enhancements
Games 0
API Services 0
Website 0
Documentation
Games 0
API Services 0
Website 0

Final Thoughts

GitHub Actions provide a powerful way to automate issue tracking and boost transparency. By integrating GraphQL and automating with cron jobs, we can stay organized and communicate our progress with players in real-time.

Ready to set up your own tracker? Fork our repo and start automating your project.