Workflows
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.

