You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
meshcore-client/.github/workflows/sync-labels-to-board.yml

200 lines
7.6 KiB

name: Sync Labels to Project Board
# Fires when a board: or priority: label is added/removed (or an issue closes).
# Maps labels -> Project V2 board fields (OffbandMesh org project #2) via GraphQL,
# so agents never call `gh project` commands directly.
# Adapted from the DifferentWire/Unfocused canonical (GH #59 board backfill).
#
# Requires repo secret PROJECT_PAT: a classic PAT with `project` + `repo` scope
# (the default GITHUB_TOKEN cannot write an org-level project).
on:
issues:
types: [labeled, unlabeled, closed]
jobs:
# -- Auto-label closed issues board:done (mutual-exclusion cleanup) --
auto-label-closed:
runs-on: ubuntu-latest
permissions:
issues: write
if: github.event.action == 'closed'
steps:
- name: Auto-label closed issues
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
run: |
labels=$(gh issue view ${{ github.event.issue.number }} --json labels -q '.labels[].name')
if ! echo "$labels" | grep -q "board:done"; then
gh issue edit ${{ github.event.issue.number }} --add-label "board:done"
else
stale=$(echo "$labels" | grep '^board:' | grep -v '^board:done$' || true)
if [ -n "$stale" ]; then
for lbl in $stale; do
gh issue edit ${{ github.event.issue.number }} --remove-label "$lbl"
done
gh issue edit ${{ github.event.issue.number }} --remove-label "board:done"
gh issue edit ${{ github.event.issue.number }} --add-label "board:done"
fi
fi
# -- Sync board/priority labels to project fields --
sync:
runs-on: ubuntu-latest
if: >-
(github.event.action == 'labeled' || github.event.action == 'unlabeled') && (
startsWith(github.event.label.name, 'board:') ||
startsWith(github.event.label.name, 'priority:')
)
steps:
- name: Sync label to project board
uses: actions/github-script@v7
with:
github-token: ${{ secrets.PROJECT_PAT }}
script: |
// -- Project field IDs (OffbandMesh org project #2) --
const PROJECT_ID = 'PVT_kwDOEXsS3c4BaleY';
const STATUS_FIELD_ID = 'PVTSSF_lADOEXsS3c4BaleYzhVcBOA';
const STATUS_OPTIONS = {
'board:backlog': '74e6ff08',
'board:todo': 'd85ffaa6',
'board:ready': 'e23e8854',
'board:in-progress': 'a406c68f',
'board:testing': 'ead78e5a',
'board:deferred': '0bb62da8',
'board:done': '72944195',
};
const PRIORITY_FIELD_ID = 'PVTSSF_lADOEXsS3c4BaleYzhVcBUc';
const PRIORITY_OPTIONS = {
'priority:P0': '59562f1e',
'priority:P1': 'a76f6d06',
'priority:P2': '2b16020d',
'priority:P3': 'd5740bf3',
};
const action = context.payload.action;
const triggerLabel = context.payload.label.name;
const issueNumber = context.payload.issue.number;
const issueNodeId = context.payload.issue.node_id;
const issueLabels = context.payload.issue.labels.map(l => l.name);
core.info(`Action "${action}" label "${triggerLabel}" on #${issueNumber}`);
let triggerPrefix;
if (triggerLabel.startsWith('board:')) triggerPrefix = 'board:';
else if (triggerLabel.startsWith('priority:')) triggerPrefix = 'priority:';
else {
core.info('Not a board/priority label — skipping');
return;
}
if (action === 'unlabeled') {
const options = triggerPrefix === 'board:' ? STATUS_OPTIONS : PRIORITY_OPTIONS;
const surviving = issueLabels.find(l => options[l]);
if (!surviving) {
core.info(`No remaining ${triggerPrefix} label — nothing to sync`);
return;
}
core.info(`Unlabeled "${triggerLabel}", surviving: "${surviving}" — syncing`);
}
if (action === 'labeled') {
const conflicting = issueLabels.filter(
l => l.startsWith(triggerPrefix) && l !== triggerLabel
);
for (const oldLabel of conflicting) {
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
name: oldLabel,
});
core.info(`Removed conflicting label: ${oldLabel}`);
} catch (e) {
core.warning(`Could not remove label "${oldLabel}": ${e.message}`);
}
}
}
const addMutation = `
mutation($projectId: ID!, $contentId: ID!) {
addProjectV2ItemById(input: {
projectId: $projectId
contentId: $contentId
}) {
item { id }
}
}
`;
let itemId;
try {
const addResult = await github.graphql(addMutation, {
projectId: PROJECT_ID,
contentId: issueNodeId,
});
itemId = addResult.addProjectV2ItemById.item.id;
core.info(`Project item: ${itemId}`);
} catch (e) {
core.setFailed(`Failed to add #${issueNumber} to project: ${e.message}`);
return;
}
const updateMutation = `
mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
updateProjectV2ItemFieldValue(input: {
projectId: $projectId
itemId: $itemId
fieldId: $fieldId
value: { singleSelectOptionId: $optionId }
}) {
projectV2Item { id }
}
}
`;
const boardLabel = triggerPrefix === 'board:'
? (action === 'labeled'
? triggerLabel
: issueLabels.find(l => STATUS_OPTIONS[l]))
: issueLabels.find(l => l.startsWith('board:') && STATUS_OPTIONS[l]);
const priorityLabel = triggerPrefix === 'priority:'
? (action === 'labeled'
? triggerLabel
: issueLabels.find(l => PRIORITY_OPTIONS[l]))
: issueLabels.find(l => l.startsWith('priority:') && PRIORITY_OPTIONS[l]);
if (boardLabel) {
try {
await github.graphql(updateMutation, {
projectId: PROJECT_ID,
itemId: itemId,
fieldId: STATUS_FIELD_ID,
optionId: STATUS_OPTIONS[boardLabel],
});
core.info(`Status -> ${boardLabel}`);
} catch (e) {
core.setFailed(`Failed to set status: ${e.message}`);
}
}
if (priorityLabel) {
try {
await github.graphql(updateMutation, {
projectId: PROJECT_ID,
itemId: itemId,
fieldId: PRIORITY_FIELD_ID,
optionId: PRIORITY_OPTIONS[priorityLabel],
});
core.info(`Priority -> ${priorityLabel}`);
} catch (e) {
core.setFailed(`Failed to set priority: ${e.message}`);
}
}
core.info(`Done syncing #${issueNumber}`);

Powered by TurnKey Linux.