summaryrefslogtreecommitdiff
path: root/ci/github-script/reviews.js
diff options
context:
space:
mode:
authorMehmet Samet Duman <yongdohyun@projecttick.org>2026-04-04 19:47:58 +0300
committerMehmet Samet Duman <yongdohyun@projecttick.org>2026-04-04 19:47:58 +0300
commit8d0d919fbf43230148da7533519ed0ffdfaa4197 (patch)
tree27e352d6ca09910e577ec27a10659814e88b15b9 /ci/github-script/reviews.js
parentfce202465d4fede9e19d4d057eebbaa702291652 (diff)
downloadProject-Tick-8d0d919fbf43230148da7533519ed0ffdfaa4197.tar.gz
Project-Tick-8d0d919fbf43230148da7533519ed0ffdfaa4197.zip
NOISSUE add GitHub Actions scripts for PR preparation and review management
- Introduced `prepare.js` to validate PR mergeability and branch targeting. - Added `reviews.js` for automated review dismissal and posting. - Created `run` script to execute actions with GitHub context. - Implemented rate limiting in `withRateLimit.js` to manage API requests. - Added `supportedBranches.js` for branch classification logic. - Created `update-pinned.sh` for updating pinned dependencies. - Added `pinned.json` to manage pinned Nix dependencies. - Updated `libnbtplusplus` version from 2.3 to 3.0 and adjusted README accordingly. Signed-off-by: Mehmet Samet Duman <yongdohyun@projecttick.org>
Diffstat (limited to 'ci/github-script/reviews.js')
-rw-r--r--ci/github-script/reviews.js197
1 files changed, 197 insertions, 0 deletions
diff --git a/ci/github-script/reviews.js b/ci/github-script/reviews.js
new file mode 100644
index 0000000000..b26615fae2
--- /dev/null
+++ b/ci/github-script/reviews.js
@@ -0,0 +1,197 @@
+// @ts-check
+
+const eventToState = {
+ COMMENT: 'COMMENTED',
+ REQUEST_CHANGES: 'CHANGES_REQUESTED',
+}
+
+/**
+ * @param {{
+ * github: InstanceType<import('@actions/github/lib/utils').GitHub>,
+ * context: import('@actions/github/lib/context').Context,
+ * core: import('@actions/core'),
+ * dry: boolean,
+ * reviewKey?: string,
+ * }} DismissReviewsProps
+ */
+async function dismissReviews({ github, context, core, dry, reviewKey }) {
+ const pull_number = context.payload.pull_request?.number
+ if (!pull_number) {
+ core.warning('dismissReviews called outside of pull_request context')
+ return
+ }
+
+ if (dry) return
+
+ const reviews = (
+ await github.paginate(github.rest.pulls.listReviews, {
+ ...context.repo,
+ pull_number,
+ })
+ ).filter(
+ (review) =>
+ review.user?.login === 'github-actions[bot]' &&
+ review.state !== 'DISMISSED',
+ )
+ const changesRequestedReviews = reviews.filter(
+ (review) => review.state === 'CHANGES_REQUESTED',
+ )
+
+ const commentRegex = /<!-- projt review key: (.*)(?:; resolved: .*)? -->/
+ const reviewKeyRegex = new RegExp(
+ `<!-- (projt review key: ${reviewKey})(?:; resolved: .*)? -->`,
+ )
+ const commentResolvedRegex =
+ /<!-- projt review key: .*; resolved: true -->/
+
+ let reviewsToMinimize = reviews
+ let /** @type {typeof reviews} */ reviewsToDismiss = []
+ let /** @type {typeof reviews} */ reviewsToResolve = []
+
+ if (reviewKey && reviews.every((review) => commentRegex.test(review.body))) {
+ reviewsToMinimize = reviews.filter((review) =>
+ reviewKeyRegex.test(review.body),
+ )
+ }
+
+ if (
+ changesRequestedReviews.every(
+ (review) =>
+ commentResolvedRegex.test(review.body) ||
+ (reviewKey && reviewKeyRegex.test(review.body)),
+ )
+ ) {
+ reviewsToDismiss = changesRequestedReviews
+ } else if (reviewsToMinimize.length) {
+ reviewsToResolve = reviewsToMinimize.filter(
+ (review) =>
+ review.state === 'CHANGES_REQUESTED' &&
+ !commentResolvedRegex.test(review.body),
+ )
+ }
+
+ await Promise.all([
+ ...reviewsToMinimize.map(async (review) =>
+ github.graphql(
+ `mutation($node_id:ID!) {
+ minimizeComment(input: {
+ classifier: OUTDATED,
+ subjectId: $node_id
+ })
+ { clientMutationId }
+ }`,
+ { node_id: review.node_id },
+ ),
+ ),
+ ...reviewsToDismiss.map(async (review) =>
+ github.rest.pulls.dismissReview({
+ ...context.repo,
+ pull_number,
+ review_id: review.id,
+ message: 'Review dismissed automatically',
+ }),
+ ),
+ ...reviewsToResolve.map(async (review) =>
+ github.rest.pulls.updateReview({
+ ...context.repo,
+ pull_number,
+ review_id: review.id,
+ body: review.body.replace(
+ reviewKeyRegex,
+ `<!-- projt review key: ${reviewKey}; resolved: true -->`,
+ ),
+ }),
+ ),
+ ])
+}
+
+/**
+ * @param {{
+ * github: InstanceType<import('@actions/github/lib/utils').GitHub>,
+ * context: import('@actions/github/lib/context').Context
+ * core: import('@actions/core'),
+ * dry: boolean,
+ * body: string,
+ * event?: keyof eventToState,
+ * reviewKey: string,
+ * }} PostReviewProps
+ */
+async function postReview({
+ github,
+ context,
+ core,
+ dry,
+ body,
+ event = 'REQUEST_CHANGES',
+ reviewKey,
+}) {
+ const pull_number = context.payload.pull_request?.number
+ if (!pull_number) {
+ core.warning('postReview called outside of pull_request context')
+ return
+ }
+
+ const reviewKeyRegex = new RegExp(
+ `<!-- (projt review key: ${reviewKey})(?:; resolved: .*)? -->`,
+ )
+ const reviewKeyComment = `<!-- projt review key: ${reviewKey}; resolved: false -->`
+ body = body + '\n\n' + reviewKeyComment
+
+ const reviews = (
+ await github.paginate(github.rest.pulls.listReviews, {
+ ...context.repo,
+ pull_number,
+ })
+ ).filter(
+ (review) =>
+ review.user?.login === 'github-actions[bot]' &&
+ review.state !== 'DISMISSED',
+ )
+
+ /** @type {null | typeof reviews[number]} */
+ let pendingReview
+ const matchingReviews = reviews.filter((review) =>
+ reviewKeyRegex.test(review.body),
+ )
+
+ if (matchingReviews.length === 0) {
+ pendingReview = null
+ } else if (
+ matchingReviews.length === 1 &&
+ matchingReviews[0].state === eventToState[event]
+ ) {
+ pendingReview = matchingReviews[0]
+ } else {
+ await dismissReviews({ github, context, core, dry, reviewKey })
+ pendingReview = null
+ }
+
+ if (pendingReview !== null) {
+ if (pendingReview.body === body) {
+ core.info('Review is already up to date')
+ return
+ }
+
+ core.info('Updating existing review')
+ if (!dry) {
+ await github.rest.pulls.updateReview({
+ ...context.repo,
+ pull_number,
+ review_id: pendingReview.id,
+ body,
+ })
+ }
+ } else {
+ core.info(`Posting review (event: ${event})`)
+ if (!dry) {
+ await github.rest.pulls.createReview({
+ ...context.repo,
+ pull_number,
+ event,
+ body,
+ })
+ }
+ }
+}
+
+module.exports = { postReview, dismissReviews }