Skip to content
Learni
View all tutorials
DevOps

How to Implement Advanced GitHub Projects in 2026

Lire en français

Introduction

GitHub Projects v2, launched in beta and stabilized in 2024, revolutionizes project management with dynamic boards, custom fields, and native automations. Unlike Projects v1's static columns, v2 provides a powerful GraphQL API for programmatic CRUD, iterations like sprints, and seamless GitHub Actions integrations. For experts, the key is scaling: automate updates from issues/PRs, build data-driven dashboards, and connect external tools. This tutorial walks you through it step by step with complete code to turn your repos into project powerhouses. Ideal for tech leads managing 10+ repos at once. (112 words)

Prerequisites

  • GitHub Pro or Enterprise account with Projects v2 access (enable beta if needed).
  • Existing repository with issues and PRs.
  • Advanced knowledge of GitHub Actions and GraphQL.
  • Node.js 20+ to test API scripts.
  • GitHub CLI installed (gh auth login).

Create a Project via GraphQL API

create-project.graphql
mutation CreateProject {
  createProjectV2(input: {
    ownerId: "REPO:your-org/your-repo",
    title: "Mon Projet Expert 2026",
    description: "Projet avancé avec automatisations",
    public: true
  }) {
    projectV2 {
      id
      number
      title
      url
    }
  }
}

This GraphQL mutation creates a Project v2 in a specific repo. Replace ownerId with your repo (format 'REPO:org/repo'). Run it via GitHub CLI (gh api graphql -f queryFile=query.graphql) or curl with a PAT token (scopes: project). Pitfall: without public: true, it stays private and inaccessible to automations.

Add Custom Fields

After creation, set up fields via UI or API: iteration (for sprints), single select (priorities), number (effort). This sets the stage for automations.

Add Custom Field via API

add-custom-field.graphql
mutation AddCustomField {
  projectV2FieldCreate(input: {
    projectId: "PVT_xxxxx",
    name: "Priorité",
    dataType: SINGLE_SELECT,
    singleSelectOptionValues: ["P0", "P1", "P2", "P3"]
  }) {
    field {
      id
      name
      dataType
    }
  }
}

Adds a 'Priorité' single-select field. projectId comes from the creation response (format PVT_). Great for dynamic sorting. Note: max 50 options per field, and dataType must match (SINGLE_SELECT, ITERATION, etc.). Test with gh api graphql.

YAML Workflow for Auto-Adding Issues

.github/workflows/add-to-project.yml
name: Ajouter Issue au Project
on:
  issues:
    types: [opened, labeled]
jobs:
  add-issue:
    runs-on: ubuntu-latest
    permissions:
      issues: write
      pull-requests: write
    steps:
      - uses: actions/add-to-project@v2
        with:
          project-url: https://github.com/orgs/your-org/projects/1
          github-token: ${{ secrets.ADD_TO_PROJECT_PAT }}
          labeled: triage
          label-operator: OR

This workflow automatically adds issues labeled 'triage' to the Project. Use a dedicated PAT (repo:write, project scopes). project-url is the board's URL. Pitfall: missing permissions cause silent failures; check Actions logs.

Link Items to Issues/PRs

Now, map existing issues to the Project and update fields automatically.

Add Item and Update Fields

add-item-update.graphql
mutation AddItemAndUpdate {
  addProjectV2ItemById(input: {
    projectId: "PVT_xxxxx",
    contentId: "I_abc123"
  }) {
    item {
      id
    }
  }
  projectV2ItemFieldSingleSelectValueUpdate(input: {
    projectId: "PVT_xxxxx",
    itemId: "PVTF_xxxxx",
    fieldId: "PVTF_xxxxx",
    singleSelectOptionId: "P0"
  }) {
    singleSelectField {
      name
    }
  }
}

Adds an issue (contentId: I_... for issue, PR_ for PR) to the project, then updates the 'Priorité' field to 'P0'. Fetch IDs via prior queries. Tip: Chain mutations in batches for better performance. Common error: IDs expire after 30 days.

Advanced Workflow: Auto-Update on PR Merge

.github/workflows/auto-update-field.yml
name: Update Project on Merge
on:
  pull_request:
    types: [closed]
jobs:
  update-field:
    if: github.event.pull_request.merged == true
    runs-on: ubuntu-latest
    steps:
      - name: Run field updater
        uses: actions/github-script@v7
        with:
          script: |
            const { data: { project } } = await github.rest.projects.get({ owner: context.repo.owner, project_id: 1 });
            // Logique custom pour update via GraphQL
        env:
          PROJECT_ID: ${{ secrets.PROJECT_ID }}

Triggers on PR merge to update fields (e.g., set to 'Done'). Extend with github-script for GraphQL calls. Secure with PROJECT_ID secret. Limit: API rate limits (5000/hour with PAT).

GraphQL Query for Custom Dashboard

query-dashboard.graphql
query ProjectDashboard($projectId: ID!) {
  node(id: $projectId) {
    ... on ProjectV2 {
      title
      items(first: 20) {
        nodes {
          content {
            ... on Issue {
              title
              labels(first: 5) { nodes { name } }
            }
          }
          fieldValues(first: 10) {
            nodes {
              ... on ProjectV2ItemFieldSingleSelectValue {
                name
              }
            }
          }
        }
      }
    }
  }
}

Paginated query to export Project data as JSON/CSV. Uses $projectId variable. Ideal for external dashboards (e.g., Node script to Google Sheets). Pitfall: first:100 max per page; use after for cursor pagination.

Best Practices

  • Always use dedicated PATs with minimal scopes (project, repo) stored in secrets.
  • Paginate GraphQL queries for >100 items; implement cursors.
  • Version workflows with feature branches for testing.
  • Backup IDs: Store project/field IDs in repo vars/secrets.
  • Monitor rate limits via GitHub API status.

Common Errors to Avoid

  • Stale IDs: Project v2 IDs rarely change, but verify after org migrations.
  • Missing permissions: Actions fail without issues:write; add permissions: block.
  • Uninitialized fields: Updating before addItem causes NULL errors.
  • Rate limiting: >100 mutations/day → 429; add throttling delays.

Next Steps