ESPROFILER Handbook
Github

Workflows

Reusable and centralised GitHub Actions workflows used across ESPROFILER repositories.

Centralised workflows

We can define reusable GitHub Actions workflows in a central repository and call them from multiple repositories using workflow_call.

At ESPROFILER, these shared workflows live in ES-Profiler/.github-private and are consumed from project repositories to keep automation consistent and avoid duplicated workflow logic.

Calling a Shared Workflow

In any repository within the ESProfiler organisation, you can call a centrally defined workflow. These workflows must be stored in the ES-Profiler/.github-private repository. This example shows how to call the mark-released-items-done workflow:

name: Mark released issues as 'Done'

on:
  release:
    types: [published]

jobs:
  call-shared-workflow:
    uses: ES-Profiler/.github-private/.github/workflows/mark-released-items-done.yml@main
    secrets: inherit
    permissions:
      contents: read
      pull-requests: read
      issues: write
      repository-projects: write

Example Workflows

Marking released items as "Done"

This workflow finds pull requests referenced in release notes, discovers the issues those PRs close, updates their project status to Done, and comments on each issue with a link back to the published release.

name: Mark released issues as 'Done'

on:
  workflow_call:

jobs:
  update-issues:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: read
      issues: write
      repository-projects: write
    steps:
      - name: Update project status for issues in this release
        uses: actions/github-script@v8
        with:
          github-token: ${{ secrets.PROJECT_V2_TOKEN || secrets.GITHUB_TOKEN }}
          script: |
            const owner = context.repo.owner;
            const repo = context.repo.repo;
            const release = context.payload.release;

            // Your IDs
            const PROJECT_ID = '<insert ID>';
            const STATUS_FIELD_ID = '<insert ID>';
            const DONE_OPTION_ID = '<insert ID>';

            const releaseUrl = release.html_url;
            const body = release.body || '';

            // Collect PR numbers mentioned in the release body: "#123" or "pull/123"
            const prNumbers = [...body.matchAll(/(?:#|pull\/)(\d+)/g)]
              .map(m => parseInt(m[1], 10))
              .filter((n, i, arr) => arr.indexOf(n) === i);

            console.log('PRs found in release notes:', prNumbers);

            const gql = (query, variables) => github.graphql(query, variables);

            for (const prNumber of prNumbers) {
              console.log(`Processing PR #${prNumber}`);

              // 1) Get issues that PR closes
              const prData = await gql(
                `
                query($owner: String!, $repo: String!, $pr: Int!) {
                  repository(owner: $owner, name: $repo) {
                    pullRequest(number: $pr) {
                      closingIssuesReferences(first: 50) {
                        nodes {
                          id
                          number
                          title
                        }
                      }
                    }
                  }
                }
                `,
                { owner, repo, pr: prNumber }
              );

              const issues =
                prData.repository.pullRequest?.closingIssuesReferences?.nodes || [];

              if (!issues.length) {
                console.log(`No closing issues for PR #${prNumber}`);
                continue;
              }

              for (const issue of issues) {
                console.log(`Updating project status for issue #${issue.number}`);

                // 2) Find the ProjectV2 item for this issue on your project
                const itemData = await gql(
                  `
                  query($projectId: ID!, $issueSearch: String!) {
                    node(id: $projectId) {
                      ... on ProjectV2 {
                        items(first: 50, query: $issueSearch) {
                          nodes {
                            id
                            content {
                              ... on Issue {
                                id
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                  `,
                  { projectId: PROJECT_ID, issueSearch: `${issue.number}` }
                );

                const items = itemData.node?.items?.nodes || [];
                const projectItem = items.find(
                  (it) => it.content && it.content.id === issue.id
                );

                if (!projectItem) {
                  console.log(`No project item found for issue #${issue.number}`);
                  continue;
                }

                const itemId = projectItem.id;

                // 3) Set Status = Done
                await gql(
                  `
                  mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
                    updateProjectV2ItemFieldValue(
                      input: {
                        projectId: $projectId
                        itemId: $itemId
                        fieldId: $fieldId
                        value: { singleSelectOptionId: $optionId }
                      }
                    ) {
                      projectV2Item { id }
                    }
                  }
                  `,
                  {
                    projectId: PROJECT_ID,
                    itemId,
                    fieldId: STATUS_FIELD_ID,
                    optionId: DONE_OPTION_ID,
                  }
                );

                console.log(
                  `Issue #${issue.number} status moved to Done (release ${release.tag_name})`
                );

                // 4) Add a comment linking back to this release
                await github.rest.issues.createComment({
                  owner,
                  repo,
                  issue_number: issue.number,
                  body: `Marked as **Done** when release [${release.tag_name}](${releaseUrl}) was published (includes PR #${prNumber}).`
                });
              }
            }

Handling new issues added to a repo

This workflow applies the Needs Triage label to newly opened issues and adds them to the Development Board.

name: Process New Issues

on:
  workflow_call:

jobs:
  label_issues:
    runs-on: ubuntu-latest
    permissions:
      issues: write
    steps:
      - name: Add 'Needs Triage' label
        run: |
          curl -X POST \
            -H "Accept: application/vnd.github+json" \
            -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
            -H "X-GitHub-Api-Version: 2022-11-28" \
            https://api.github.com/repos/${{ github.repository }}/issues/${{ github.event.issue.number }}/labels \
            -d '{"labels":["Needs Triage"]}'
  add-to-dev-project:
    name: Add issue to dev project
    runs-on: ubuntu-latest
    steps:
      - uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
        with:
          project-url: https://github.com/orgs/ES-Profiler/projects/2
          github-token: ${{ secrets.ADD_TO_PROJECT_PAT }}


Important: token requirements for project access

For GitHub Project operations in centralised workflows, you will typically need a PAT secret (for example, PROJECT_V2_TOKEN or ADD_TO_PROJECT_PAT) because GITHUB_TOKEN may not have the required project-level access across repositories.

Use a fine-grained PAT with the minimum required permissions and store it in repository or organisation secrets. For setup instructions, see How to create a fine-grained token.

Copyright © 2026