From 36ee80b554dff5d950ff2f2bdea1883e40f574f1 Mon Sep 17 00:00:00 2001 From: Daniel Volz Date: Fri, 13 Feb 2026 18:45:51 +0100 Subject: [PATCH] chore: add workflow to auto-move project items to Done on close/merge (#165) - New workflow project-auto-done.yml triggers on issue close and PR merge - Uses GraphQL to find the project item and update Status to Done - Handles both issues and pull requests with proper type detection - Skips gracefully if item is not on the board or already Done - Update release-manager.agent.md to reflect automation (manual is now fallback) --- .github/agents/release-manager.agent.md | 38 ++------- .github/workflows/project-auto-done.yml | 105 ++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 33 deletions(-) create mode 100644 .github/workflows/project-auto-done.yml diff --git a/.github/agents/release-manager.agent.md b/.github/agents/release-manager.agent.md index aabda61..00bba59 100644 --- a/.github/agents/release-manager.agent.md +++ b/.github/agents/release-manager.agent.md @@ -17,7 +17,7 @@ You are the release manager for **MedAssist-ng**. Your job is to guide code from - **NEVER skip CI checks.** Wait for all status checks to pass before merging. - **Testing ownership belongs to `@testing-manager`**. Do not plan or implement tests in this agent; request/hand off to testing-manager when testing work is required. - **Track all work in the GitHub Project board.** Every PR should reference an issue. Move issues through the board as work progresses. -- **ALWAYS verify Project board status after merge.** GitHub Projects V2 does NOT auto-move items to "Done" when issues are closed. After every PR merge, check the linked issue's project status and move it to "Done" manually via GraphQL if needed (see Task 6). +- **ALWAYS verify Project board status after merge.** The `project-auto-done.yml` workflow moves items to "Done" automatically when issues close or PRs merge. Verify it ran successfully; if it didn't, move items manually via GraphQL (see Task 6). ## CI/CD Ownership (Authoritative) @@ -37,6 +37,7 @@ This repository intentionally uses only two operational agents for CI/CD handoff | `.github/workflows/docker-build.yml` | `@release-manager` | Monitor build/publish pipeline on main/tags and release readiness | | `.github/workflows/update-test-badges.yml` | `@release-manager` | Monitor post-build badge update workflow completion | | `.github/workflows/add-to-project.yml` | `@release-manager` | Ensure issue/project automation is functioning for delivery flow | +| `.github/workflows/project-auto-done.yml` | `@release-manager` | Auto-move project items to "Done" when issues close or PRs merge | ### Monitoring Rule (Must Follow) @@ -415,41 +416,12 @@ All work is tracked in the [GitHub Project board](https://github.com/users/Danie 2. **When creating a PR**: Always reference the issue with `Closes #N` in the PR body so the issue is automatically **closed** on merge. Note: this does NOT move the Project board status — that must be done manually (see step 3). -3. **After merge (MANDATORY)**: GitHub Projects V2 does NOT auto-move items to "Done" when issues close. You MUST verify and update the board status after every merge: - - **Step 1 — Check current status:** +3. **After merge — verify automation**: The `project-auto-done.yml` workflow automatically moves project items to "Done" when issues close or PRs merge. After merge, verify it ran: ```bash GH_PAGER=cat gh issue view --json state,projectItems --jq '{state, projects: [.projectItems[] | {title: .title, status: .status.name}]}' ``` - **Step 2 — If status is not "Done", move it via GraphQL:** - - First, get the item ID and field IDs: - ```bash - GH_PAGER=cat gh api graphql -f query='query { - user(login: "DanielVolz") { - projectV2(number: 1) { - id - items(first: 100) { - nodes { - id - content { ... on Issue { number } } - fieldValues(first: 10) { - nodes { - ... on ProjectV2ItemFieldSingleSelectValue { - name - field { ... on ProjectV2SingleSelectField { id options { id name } } } - } - } - } - } - } - } - } - }' --jq '.data.user.projectV2.items.nodes[] | select(.content.number == )' - ``` - - Then update the status field to "Done" (`ca45af98`): + **Manual fallback** — if the workflow fails or the item wasn't moved, use GraphQL: ```bash GH_PAGER=cat gh api graphql -f query='mutation { updateProjectV2ItemFieldValue(input: { @@ -493,7 +465,7 @@ Code complete & validated by testing-manager 3. Commit, push, create PR (with "Closes #N" in body) 4. Wait for CI (all required checks) 5. Merge PR to main (squash + delete branch) -6. Verify issue moved to "Done" on Project board (it WON'T auto-move — use GraphQL, see Task 6) +6. Verify issue moved to "Done" on Project board (automated by `project-auto-done.yml`; fallback: GraphQL, see Task 6) ↓ Ready for release? ↓ diff --git a/.github/workflows/project-auto-done.yml b/.github/workflows/project-auto-done.yml new file mode 100644 index 0000000..1f0b523 --- /dev/null +++ b/.github/workflows/project-auto-done.yml @@ -0,0 +1,105 @@ +name: Move Done in Project + +on: + issues: + types: [closed] + pull_request: + types: [closed] + +permissions: {} + +jobs: + move-to-done: + name: Move to Done + runs-on: ubuntu-latest + if: >- + (github.event_name == 'issues' && github.event.issue.state_reason == 'completed') || + (github.event_name == 'pull_request' && github.event.pull_request.merged == true) + steps: + - name: Move project item to Done + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} + script: | + const projectId = 'PVT_kwHOADH82s4BO2OT'; + const statusFieldId = 'PVTSSF_lAHOADH82s4BO2OTzg9bdkE'; + const doneOptionId = 'ca45af98'; + + // Determine content ID (issue or PR node ID) + const nodeId = context.payload.issue?.node_id || context.payload.pull_request?.node_id; + const number = context.payload.issue?.number || context.payload.pull_request?.number; + const type = context.payload.issue ? 'issue' : 'pull_request'; + + console.log(`Processing ${type} #${number} (${nodeId})`); + + // Find the project item by content node ID + const result = await github.graphql(` + query($nodeId: ID!) { + node(id: $nodeId) { + ... on Issue { + projectItems(first: 10) { + nodes { + id + project { id } + fieldValueByName(name: "Status") { + ... on ProjectV2ItemFieldSingleSelectValue { + name + optionId + } + } + } + } + } + ... on PullRequest { + projectItems(first: 10) { + nodes { + id + project { id } + fieldValueByName(name: "Status") { + ... on ProjectV2ItemFieldSingleSelectValue { + name + optionId + } + } + } + } + } + } + } + `, { nodeId }); + + const items = result.node?.projectItems?.nodes || []; + const projectItem = items.find(item => item.project.id === projectId); + + if (!projectItem) { + console.log(`${type} #${number} is not in the project board — skipping.`); + return; + } + + const currentStatus = projectItem.fieldValueByName?.name || 'unknown'; + if (currentStatus === 'Done') { + console.log(`${type} #${number} is already "Done" — skipping.`); + return; + } + + console.log(`Moving ${type} #${number} from "${currentStatus}" to "Done"...`); + + 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, + itemId: projectItem.id, + fieldId: statusFieldId, + optionId: doneOptionId + }); + + console.log(`Successfully moved ${type} #${number} to "Done".`);