Skip to content

Commit 089fcbc

Browse files
Fix release project-board promotion (#254)
* fix: release merged project board items * Update wiki submodule pointer for PR #254 * fix: scope release project status transitions --------- Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 16e50e7 commit 089fcbc

7 files changed

Lines changed: 399 additions & 110 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: 127 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,105 @@ 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+
repository {
74+
nameWithOwner
75+
}
76+
title
77+
url
78+
}
79+
... on PullRequest {
80+
number
81+
repository {
82+
nameWithOwner
83+
}
84+
title
85+
url
8386
}
8487
}
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
88+
fieldValues(first: 20) {
89+
nodes {
90+
__typename
91+
... on ProjectV2ItemFieldSingleSelectValue {
92+
field {
93+
... on ProjectV2SingleSelectField {
94+
name
95+
}
96+
}
97+
name
98+
}
99+
}
110100
}
111101
}
112-
name
113102
}
114103
}
115104
}
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-
);
105+
}`,
106+
{
107+
project: project.id,
108+
cursor,
109+
},
110+
);
111+
112+
const page = result.node?.items;
113+
114+
items.push(...(page?.nodes ?? []));
115+
cursor = page?.pageInfo?.hasNextPage ? page.pageInfo.endCursor : null;
116+
} while (null !== cursor);
117+
118+
return items;
119+
};
120+
121+
const formatLabel = (item) => {
122+
const content = item.content;
123+
124+
if ('Issue' === content?.__typename) {
125+
return `Issue #${content.number}`;
126+
}
127+
128+
if ('PullRequest' === content?.__typename) {
129+
return `PR #${content.number}`;
130+
}
131+
132+
return `Project item ${item.id}`;
133+
};
134+
135+
const belongsToCurrentRepository = (item) => {
136+
const repository = item.content?.repository?.nameWithOwner;
137+
138+
return `${context.repo.owner}/${context.repo.repo}` === repository;
139+
};
127140

128141
const moveToStatus = async (item, label) => {
129-
if (!item || board.getExistingFieldValue(item, 'Status') !== fromStatus) {
130-
return;
142+
const currentStatus = board.getExistingFieldValue(item, 'Status');
143+
144+
if (!sourceStatuses.includes(currentStatus)) {
145+
return false;
131146
}
132147

133148
await board.updateSingleSelectField(
@@ -138,27 +153,35 @@ module.exports = async function transitionStatus({ github, context, core }) {
138153
targetOption.id,
139154
);
140155

141-
core.info(`${label} moved to ${toStatus}.`);
156+
core.info(`${label} moved from ${currentStatus} to ${toStatus}.`);
157+
158+
return true;
142159
};
143160

144161
if (includeCurrentPullRequest) {
145-
await moveToStatus(
146-
board.findProjectItem(result.repository.pullRequest?.projectItems?.nodes ?? [], project.id),
147-
`Pull request #${context.payload.pull_request.number}`,
148-
);
162+
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.');
149163
}
150164

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-
}
165+
let movedCount = 0;
166+
let skippedCount = 0;
157167

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-
);
168+
for (const item of await loadProjectItems()) {
169+
if (!belongsToCurrentRepository(item)) {
170+
skippedCount++;
171+
172+
continue;
173+
}
174+
175+
if (await moveToStatus(item, formatLabel(item))) {
176+
movedCount++;
177+
178+
continue;
179+
}
180+
181+
skippedCount++;
163182
}
183+
184+
core.info(`${movedCount} project item(s) moved to ${toStatus}; ${skippedCount} inspected item(s) skipped.`);
185+
core.setOutput('moved-count', String(movedCount));
186+
core.setOutput('skipped-count', String(skippedCount));
164187
};

0 commit comments

Comments
 (0)