Skip to content

Commit d190e10

Browse files
committed
fix: release merged project board items
1 parent 16e50e7 commit d190e10

6 files changed

Lines changed: 393 additions & 109 deletions

File tree

.github/actions/project-board/transition-status/action.yml

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ inputs:
88
default: ''
99
from-status:
1010
description: Source project status name.
11-
required: true
11+
required: false
12+
default: ''
13+
from-statuses:
14+
description: Comma-separated source project status names.
15+
required: false
16+
default: ''
1217
to-status:
1318
description: Destination project status name.
1419
required: true
@@ -17,13 +22,26 @@ inputs:
1722
required: false
1823
default: 'false'
1924

25+
outputs:
26+
moved-count:
27+
description: Number of project items moved to the destination status.
28+
value: ${{ steps.transition.outputs.moved-count }}
29+
skipped-count:
30+
description: Number of project items inspected but not moved.
31+
value: ${{ steps.transition.outputs.skipped-count }}
32+
source-statuses:
33+
description: Comma-separated source statuses used for the transition.
34+
value: ${{ steps.transition.outputs.source-statuses }}
35+
2036
runs:
2137
using: composite
2238
steps:
23-
- uses: actions/github-script@v8
39+
- id: transition
40+
uses: actions/github-script@v8
2441
env:
2542
INPUT_PROJECT: ${{ inputs.project }}
2643
INPUT_FROM_STATUS: ${{ inputs.from-status }}
44+
INPUT_FROM_STATUSES: ${{ inputs.from-statuses }}
2745
INPUT_TO_STATUS: ${{ inputs.to-status }}
2846
INPUT_INCLUDE_CURRENT_PULL_REQUEST: ${{ inputs.include-current-pull-request }}
2947
with:

.github/actions/project-board/transition-status/run.cjs

Lines changed: 109 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,24 @@ const board = require('../shared/project-board-client.cjs');
77
*/
88
module.exports = async function transitionStatus({ github, context, core }) {
99
const includeCurrentPullRequest = 'true' === (process.env.INPUT_INCLUDE_CURRENT_PULL_REQUEST ?? '').toLowerCase();
10-
const fromStatus = process.env.INPUT_FROM_STATUS;
10+
const sourceStatuses = [
11+
...(process.env.INPUT_FROM_STATUSES ?? '').split(','),
12+
process.env.INPUT_FROM_STATUS ?? '',
13+
]
14+
.map((status) => status.trim())
15+
.filter((status, index, statuses) => '' !== status && statuses.indexOf(status) === index);
1116
const toStatus = process.env.INPUT_TO_STATUS;
1217

18+
core.setOutput('source-statuses', sourceStatuses.join(','));
19+
20+
if (0 === sourceStatuses.length) {
21+
core.info('No source project statuses were provided. Skipping status transition.');
22+
core.setOutput('moved-count', '0');
23+
core.setOutput('skipped-count', '0');
24+
25+
return;
26+
}
27+
1328
const project = await board.loadConfiguredProject(
1429
github,
1530
context.repo.owner,
@@ -18,6 +33,8 @@ module.exports = async function transitionStatus({ github, context, core }) {
1833

1934
if (!project) {
2035
core.info('No configured GitHub Project V2 was resolved. Skipping status transition.');
36+
core.setOutput('moved-count', '0');
37+
core.setOutput('skipped-count', '0');
2138

2239
return;
2340
}
@@ -27,107 +44,93 @@ module.exports = async function transitionStatus({ github, context, core }) {
2744

2845
if (!statusField || !targetOption) {
2946
core.info(`Project "${project.title}" does not expose the expected target status "${toStatus}".`);
47+
core.setOutput('moved-count', '0');
48+
core.setOutput('skipped-count', '0');
3049

3150
return;
3251
}
3352

34-
const result = await github.graphql(
35-
`query($owner: String!, $repo: String!, $pullRequestNumber: Int!) {
36-
repository(owner: $owner, name: $repo) {
37-
issues(first: 100, orderBy: {field: UPDATED_AT, direction: DESC}, states: CLOSED) {
38-
nodes {
39-
number
40-
projectItems(first: 20) {
41-
nodes {
42-
id
43-
project {
44-
... on ProjectV2 {
45-
id
46-
}
47-
}
48-
fieldValues(first: 20) {
49-
nodes {
50-
__typename
51-
... on ProjectV2ItemFieldSingleSelectValue {
52-
field {
53-
... on ProjectV2SingleSelectField {
54-
name
55-
}
56-
}
57-
name
53+
const loadProjectItems = async () => {
54+
const items = [];
55+
let cursor = null;
56+
57+
do {
58+
const result = await github.graphql(
59+
`query($project: ID!, $cursor: String) {
60+
node(id: $project) {
61+
... on ProjectV2 {
62+
items(first: 100, after: $cursor) {
63+
pageInfo {
64+
hasNextPage
65+
endCursor
5866
}
59-
}
60-
}
61-
}
62-
}
63-
}
64-
}
65-
pullRequests(first: 100, orderBy: {field: UPDATED_AT, direction: DESC}, states: [MERGED, CLOSED]) {
66-
nodes {
67-
number
68-
projectItems(first: 20) {
69-
nodes {
70-
id
71-
project {
72-
... on ProjectV2 {
73-
id
74-
}
75-
}
76-
fieldValues(first: 20) {
77-
nodes {
78-
__typename
79-
... on ProjectV2ItemFieldSingleSelectValue {
80-
field {
81-
... on ProjectV2SingleSelectField {
82-
name
67+
nodes {
68+
id
69+
content {
70+
__typename
71+
... on Issue {
72+
number
73+
title
74+
url
75+
}
76+
... on PullRequest {
77+
number
78+
title
79+
url
8380
}
8481
}
85-
name
86-
}
87-
}
88-
}
89-
}
90-
}
91-
}
92-
}
93-
pullRequest(number: $pullRequestNumber) {
94-
number
95-
projectItems(first: 20) {
96-
nodes {
97-
id
98-
project {
99-
... on ProjectV2 {
100-
id
101-
}
102-
}
103-
fieldValues(first: 20) {
104-
nodes {
105-
__typename
106-
... on ProjectV2ItemFieldSingleSelectValue {
107-
field {
108-
... on ProjectV2SingleSelectField {
109-
name
82+
fieldValues(first: 20) {
83+
nodes {
84+
__typename
85+
... on ProjectV2ItemFieldSingleSelectValue {
86+
field {
87+
... on ProjectV2SingleSelectField {
88+
name
89+
}
90+
}
91+
name
92+
}
93+
}
11094
}
11195
}
112-
name
11396
}
11497
}
11598
}
116-
}
117-
}
118-
}
119-
}
120-
}`,
121-
{
122-
owner: context.repo.owner,
123-
repo: context.repo.repo,
124-
pullRequestNumber: context.payload.pull_request?.number ?? 0,
125-
},
126-
);
99+
}`,
100+
{
101+
project: project.id,
102+
cursor,
103+
},
104+
);
105+
106+
const page = result.node?.items;
107+
108+
items.push(...(page?.nodes ?? []));
109+
cursor = page?.pageInfo?.hasNextPage ? page.pageInfo.endCursor : null;
110+
} while (null !== cursor);
111+
112+
return items;
113+
};
114+
115+
const formatLabel = (item) => {
116+
const content = item.content;
117+
118+
if ('Issue' === content?.__typename) {
119+
return `Issue #${content.number}`;
120+
}
121+
122+
if ('PullRequest' === content?.__typename) {
123+
return `PR #${content.number}`;
124+
}
125+
126+
return `Project item ${item.id}`;
127+
};
127128

128129
const moveToStatus = async (item, label) => {
129-
if (!item || board.getExistingFieldValue(item, 'Status') !== fromStatus) {
130-
return;
130+
const currentStatus = board.getExistingFieldValue(item, 'Status');
131+
132+
if (!sourceStatuses.includes(currentStatus)) {
133+
return false;
131134
}
132135

133136
await board.updateSingleSelectField(
@@ -138,27 +141,29 @@ module.exports = async function transitionStatus({ github, context, core }) {
138141
targetOption.id,
139142
);
140143

141-
core.info(`${label} moved to ${toStatus}.`);
144+
core.info(`${label} moved from ${currentStatus} to ${toStatus}.`);
145+
146+
return true;
142147
};
143148

144149
if (includeCurrentPullRequest) {
145-
await moveToStatus(
146-
board.findProjectItem(result.repository.pullRequest?.projectItems?.nodes ?? [], project.id),
147-
`Pull request #${context.payload.pull_request.number}`,
148-
);
150+
core.info('The include-current-pull-request input is kept for compatibility; project item pagination already includes the current pull request when it is on the board.');
149151
}
150152

151-
for (const pullRequest of result.repository.pullRequests.nodes) {
152-
await moveToStatus(
153-
board.findProjectItem(pullRequest.projectItems.nodes, project.id),
154-
`PR #${pullRequest.number}`,
155-
);
156-
}
153+
let movedCount = 0;
154+
let skippedCount = 0;
157155

158-
for (const issue of result.repository.issues.nodes) {
159-
await moveToStatus(
160-
board.findProjectItem(issue.projectItems.nodes, project.id),
161-
`Issue #${issue.number}`,
162-
);
156+
for (const item of await loadProjectItems()) {
157+
if (await moveToStatus(item, formatLabel(item))) {
158+
movedCount++;
159+
160+
continue;
161+
}
162+
163+
skippedCount++;
163164
}
165+
166+
core.info(`${movedCount} project item(s) moved to ${toStatus}; ${skippedCount} inspected item(s) skipped.`);
167+
core.setOutput('moved-count', String(movedCount));
168+
core.setOutput('skipped-count', String(skippedCount));
164169
};

0 commit comments

Comments
 (0)