Skip to content

Initial announcement #717

Initial announcement

Initial announcement #717

name: LFX Proposal Board Sync
on:
issues:
types: [opened, labeled, unlabeled, closed, reopened]
permissions:
contents: read
jobs:
sync-board:
if: >-
contains(github.event.issue.labels.*.name, 'lfx mentorship')
&& contains(github.event.issue.labels.*.name, 'Proposal')
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Sync issue to project board
uses: actions/github-script@v7
env:
PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }}
with:
github-token: ${{ secrets.PROJECT_TOKEN }}
script: |
// ── Resolve this repo's board from board.json (dev vs prod) ──
// Only the projectId is stored per repo; the Status field ID and
// option IDs are resolved live from the board so the fork (dev) and
// cncf/mentoring (prod) run identical code. The board's Status
// column names must match the lifecycle stages used below.
const fs = require('fs');
const path = require('path');
const _lib = (m) => require(path.join(process.env.GITHUB_WORKSPACE || process.cwd(), 'programs/lfx-mentorship/automation/lib', m));
const { determineStatus, shouldSkipSync, readStatusWithRetry } = _lib('board.js');
const _board = JSON.parse(fs.readFileSync('programs/lfx-mentorship/automation/board.json', 'utf8'));
const _envCfg = (_board.environments || {})[process.env.GITHUB_REPOSITORY];
if (!_envCfg || !_envCfg.projectId || String(_envCfg.projectId).startsWith('REPLACE_')) {
core.warning(`No board configured for ${process.env.GITHUB_REPOSITORY} in board.json; skipping board sync.`);
return;
}
const PROJECT_ID = _envCfg.projectId;
const _statusField = (await github.graphql(`
query($projectId: ID!) {
node(id: $projectId) { ... on ProjectV2 {
field(name: "Status") { ... on ProjectV2SingleSelectField { id options { id name } } }
} }
}
`, { projectId: PROJECT_ID })).node.field;
if (!_statusField) {
core.warning(`Project ${PROJECT_ID} has no "Status" field; skipping board sync.`);
return;
}
const STATUS_FIELD_ID = _statusField.id;
const STATUS_OPTIONS = Object.fromEntries(_statusField.options.map(o => [o.name, o.id]));
const issue = context.payload.issue;
const labels = (issue.labels || []).map(l => l.name);
const action = context.payload.action;
// ── Determine target status from labels + event (see lib/board.js) ──
const targetStatus = determineStatus(labels, action);
console.log(`Target status: ${targetStatus}`);
const optionId = STATUS_OPTIONS[targetStatus];
if (!optionId) {
core.warning(`Board has no "${targetStatus}" status option; skipping board sync.`);
return;
}
// ── Ensure issue is added to the project ──
const addResult = await github.graphql(`
mutation($projectId: ID!, $contentId: ID!) {
addProjectV2ItemById(input: {
projectId: $projectId
contentId: $contentId
}) {
item { id }
}
}
`, {
projectId: PROJECT_ID,
contentId: issue.node_id,
});
const itemId = addResult.addProjectV2ItemById.item.id;
console.log(`Project item: ${itemId}`);
// ── Respect manual admin moves (guard logic in lib/board.js) ──
// Automation owns the lifecycle only through "Exported"; the
// post-export columns are advanced by hand by the administrator, so
// never overwrite a card already sitting in one (except on close),
// and fail closed if the status read fails while setting "Exported".
const { currentStatus, readFailed } = await readStatusWithRetry(
() => github.graphql(`
query($itemId: ID!) {
node(id: $itemId) { ... on ProjectV2Item {
fieldValueByName(name: "Status") {
... on ProjectV2ItemFieldSingleSelectValue { name }
}
} }
}
`, { itemId }),
{ onError: (attempt, e) => core.warning(`Could not read current board status (attempt ${attempt}/2): ${e.message}`) },
);
if (shouldSkipSync(currentStatus, targetStatus, readFailed)) {
if (currentStatus) {
console.log(`Card is in admin-owned column "${currentStatus}"; leaving placement to the administrator.`);
} else {
core.warning('Status read failed while about to set "Exported"; skipping to avoid clobbering a manual placement.');
}
return;
}
// ── Set the Status field ──
await github.graphql(`
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: itemId,
fieldId: STATUS_FIELD_ID,
optionId: optionId,
});
console.log(`Status set to: ${targetStatus}`);