summaryrefslogtreecommitdiff
path: root/ci/github-script/prepare.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/prepare.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/prepare.js')
-rw-r--r--ci/github-script/prepare.js181
1 files changed, 181 insertions, 0 deletions
diff --git a/ci/github-script/prepare.js b/ci/github-script/prepare.js
new file mode 100644
index 0000000000..9f98e8d392
--- /dev/null
+++ b/ci/github-script/prepare.js
@@ -0,0 +1,181 @@
+// @ts-check
+const { classify } = require('../supportedBranches.js')
+const { postReview, dismissReviews } = require('./reviews.js')
+const reviewKey = 'prepare'
+
+/**
+ * Prepares a PR for CI by checking mergeability and branch targeting.
+ *
+ * Outputs:
+ * - base: base branch classification
+ * - head: head branch classification
+ * - mergedSha: the merge commit SHA (or head SHA if conflict)
+ * - targetSha: the target comparison SHA
+ *
+ * @param {{
+ * github: InstanceType<import('@actions/github/lib/utils').GitHub>,
+ * context: import('@actions/github/lib/context').Context,
+ * core: import('@actions/core'),
+ * dry?: boolean,
+ * }} PrepareProps
+ */
+module.exports = async ({ github, context, core, dry }) => {
+ const pull_number = context.payload.pull_request.number
+
+ for (const retryInterval of [5, 10, 20, 40, 80]) {
+ core.info('Checking whether the pull request can be merged...')
+ const prInfo = (
+ await github.rest.pulls.get({
+ ...context.repo,
+ pull_number,
+ })
+ ).data
+
+ if (prInfo.state !== 'open') throw new Error('PR is not open anymore.')
+
+ if (prInfo.mergeable == null) {
+ core.info(
+ `GitHub is still computing mergeability, waiting ${retryInterval}s...`,
+ )
+ await new Promise((resolve) => setTimeout(resolve, retryInterval * 1000))
+ continue
+ }
+
+ const { base, head } = prInfo
+
+ const baseClassification = classify(base.ref)
+ core.setOutput('base', baseClassification)
+ console.log('base classification:', baseClassification)
+
+ const headClassification =
+ base.repo.full_name === head.repo.full_name
+ ? classify(head.ref)
+ : { type: ['wip'] }
+ core.setOutput('head', headClassification)
+ console.log('head classification:', headClassification)
+
+ // Warn if targeting a release branch with a non-backport/fix branch
+ if (
+ baseClassification.stable &&
+ baseClassification.type.includes('primary')
+ ) {
+ const headPrefix = head.ref.split('-')[0]
+ if (!['backport', 'fix', 'revert'].includes(headPrefix)) {
+ core.warning(
+ `This PR targets release branch \`${base.ref}\`. ` +
+ 'New features should typically target \`master\`.',
+ )
+ }
+ }
+
+ // Check base branch targeting
+ if (headClassification.type.includes('wip')) {
+ // Determine the best base branch candidate
+ const branches = (
+ await github.paginate(github.rest.repos.listBranches, {
+ ...context.repo,
+ per_page: 100,
+ })
+ ).map(({ name }) => classify(name))
+
+ const releases = branches
+ .filter(({ stable, type }) => type.includes('primary') && stable)
+ .sort((a, b) => b.version.localeCompare(a.version))
+
+ async function mergeBase({ branch, order, version }) {
+ const { data } = await github.rest.repos.compareCommitsWithBasehead({
+ ...context.repo,
+ basehead: `${branch}...${head.sha}`,
+ per_page: 1,
+ page: 2,
+ })
+ return {
+ branch,
+ order,
+ version,
+ commits: data.total_commits,
+ sha: data.merge_base_commit.sha,
+ }
+ }
+
+ let candidates = [await mergeBase(classify('master'))]
+ for (const release of releases) {
+ const nextCandidate = await mergeBase(release)
+ if (candidates[0].commits === nextCandidate.commits)
+ candidates.push(nextCandidate)
+ if (candidates[0].commits > nextCandidate.commits)
+ candidates = [nextCandidate]
+ if (candidates[0].commits < 10000) break
+ }
+
+ const best = candidates.sort((a, b) => a.order - b.order).at(0)
+
+ core.info(`Best base branch candidate: ${best.branch}`)
+
+ if (best.branch !== base.ref) {
+ const current = await mergeBase(classify(base.ref))
+ const body = [
+ `This PR targets \`${current.branch}\`, but based on the commit history ` +
+ `\`${best.branch}\` appears to be a better fit ` +
+ `(${current.commits - best.commits} fewer commits ahead).`,
+ '',
+ `If this is intentional, you can ignore this message. Otherwise:`,
+ `- [Change the base branch](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/changing-the-base-branch-of-a-pull-request) to \`${best.branch}\`.`,
+ ].join('\n')
+
+ await postReview({ github, context, core, dry, body, reviewKey })
+ core.warning(`PR may target the wrong base branch.`)
+ } else {
+ await dismissReviews({ github, context, core, dry, reviewKey })
+ }
+ } else {
+ await dismissReviews({ github, context, core, dry, reviewKey })
+ }
+
+ let mergedSha, targetSha
+
+ if (prInfo.mergeable) {
+ core.info('The PR can be merged.')
+ mergedSha = prInfo.merge_commit_sha
+ targetSha = (
+ await github.rest.repos.getCommit({
+ ...context.repo,
+ ref: prInfo.merge_commit_sha,
+ })
+ ).data.parents[0].sha
+ } else {
+ core.warning('The PR has a merge conflict.')
+ mergedSha = head.sha
+ targetSha = (
+ await github.rest.repos.compareCommitsWithBasehead({
+ ...context.repo,
+ basehead: `${base.sha}...${head.sha}`,
+ })
+ ).data.merge_base_commit.sha
+ }
+
+ core.info(`merged: ${mergedSha}\ntarget: ${targetSha}`)
+ core.setOutput('mergedSha', mergedSha)
+ core.setOutput('targetSha', targetSha)
+
+ // Detect touched CI-relevant files
+ const files = (
+ await github.paginate(github.rest.pulls.listFiles, {
+ ...context.repo,
+ pull_number,
+ per_page: 100,
+ })
+ ).map((file) => file.filename)
+
+ const touched = []
+ if (files.some((f) => f.startsWith('ci/'))) touched.push('ci')
+ if (files.includes('ci/pinned.json')) touched.push('pinned')
+ if (files.some((f) => f.startsWith('.github/'))) touched.push('github')
+ core.setOutput('touched', touched)
+
+ return
+ }
+ throw new Error(
+ 'Timed out waiting for GitHub to compute mergeability. Check https://www.githubstatus.com.',
+ )
+}