summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/android-merge.js218
-rw-r--r--.github/workflows/android-publish.yml57
2 files changed, 275 insertions, 0 deletions
diff --git a/.github/workflows/android-merge.js b/.github/workflows/android-merge.js
new file mode 100644
index 000000000..7e02dc9e5
--- /dev/null
+++ b/.github/workflows/android-merge.js
@@ -0,0 +1,218 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4// Note: This is a GitHub Actions script
5// It is not meant to be executed directly on your machine without modifications
6
7const fs = require("fs");
8// which label to check for changes
9const CHANGE_LABEL = 'android-merge';
10// how far back in time should we consider the changes are "recent"? (default: 24 hours)
11const DETECTION_TIME_FRAME = (parseInt(process.env.DETECTION_TIME_FRAME)) || (24 * 3600 * 1000);
12
13async function checkBaseChanges(github, context) {
14 // query the commit date of the latest commit on this branch
15 const query = `query($owner:String!, $name:String!, $ref:String!) {
16 repository(name:$name, owner:$owner) {
17 ref(qualifiedName:$ref) {
18 target {
19 ... on Commit { id pushedDate oid }
20 }
21 }
22 }
23 }`;
24 const variables = {
25 owner: context.repo.owner,
26 name: context.repo.repo,
27 ref: 'refs/heads/master',
28 };
29 const result = await github.graphql(query, variables);
30 const pushedAt = result.repository.ref.target.pushedDate;
31 console.log(`Last commit pushed at ${pushedAt}.`);
32 const delta = new Date() - new Date(pushedAt);
33 if (delta <= DETECTION_TIME_FRAME) {
34 console.info('New changes detected, triggering a new build.');
35 return true;
36 }
37 console.info('No new changes detected.');
38 return false;
39}
40
41async function checkAndroidChanges(github, context) {
42 if (checkBaseChanges(github, context)) return true;
43 const query = `query($owner:String!, $name:String!, $label:String!) {
44 repository(name:$name, owner:$owner) {
45 pullRequests(labels: [$label], states: OPEN, first: 100) {
46 nodes { number headRepository { pushedAt } }
47 }
48 }
49 }`;
50 const variables = {
51 owner: context.repo.owner,
52 name: context.repo.repo,
53 label: CHANGE_LABEL,
54 };
55 const result = await github.graphql(query, variables);
56 const pulls = result.repository.pullRequests.nodes;
57 for (let i = 0; i < pulls.length; i++) {
58 let pull = pulls[i];
59 if (new Date() - new Date(pull.headRepository.pushedAt) <= DETECTION_TIME_FRAME) {
60 console.info(`${pull.number} updated at ${pull.headRepository.pushedAt}`);
61 return true;
62 }
63 }
64 console.info("No changes detected in any tagged pull requests.");
65 return false;
66}
67
68async function tagAndPush(github, owner, repo, execa, commit=false) {
69 let altToken = process.env.ALT_GITHUB_TOKEN;
70 if (!altToken) {
71 throw `Please set ALT_GITHUB_TOKEN environment variable. This token should have write access to ${owner}/${repo}.`;
72 }
73 const query = `query ($owner:String!, $name:String!) {
74 repository(name:$name, owner:$owner) {
75 refs(refPrefix: "refs/tags/", orderBy: {field: TAG_COMMIT_DATE, direction: DESC}, first: 10) {
76 nodes { name }
77 }
78 }
79 }`;
80 const variables = {
81 owner: owner,
82 name: repo,
83 };
84 const tags = await github.graphql(query, variables);
85 const tagList = tags.repository.refs.nodes;
86 const lastTag = tagList[0] ? tagList[0].name : 'dummy-0';
87 const tagNumber = /\w+-(\d+)/.exec(lastTag)[1] | 0;
88 const channel = repo.split('-')[1];
89 const newTag = `${channel}-${tagNumber + 1}`;
90 console.log(`New tag: ${newTag}`);
91 if (commit) {
92 let channelName = channel[0].toUpperCase() + channel.slice(1);
93 console.info(`Committing pending commit as ${channelName} #${tagNumber + 1}`);
94 await execa("git", ['commit', '-m', `${channelName} #${tagNumber + 1}`]);
95 }
96 console.info('Pushing tags to GitHub ...');
97 await execa("git", ['tag', newTag]);
98 await execa("git", ['remote', 'add', 'target', `https://${altToken}@github.com/${owner}/${repo}.git`]);
99 await execa("git", ['push', 'target', 'master', '-f']);
100 await execa("git", ['push', 'target', 'master', '--tags']);
101 console.info('Successfully pushed new changes.');
102}
103
104async function generateReadme(pulls, context, mergeResults, execa) {
105 let baseUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/`;
106 let output =
107 "| Pull Request | Commit | Title | Author | Merged? |\n|----|----|----|----|----|\n";
108 for (let pull of pulls) {
109 let pr = pull.number;
110 let result = mergeResults[pr];
111 output += `| [${pr}](${baseUrl}/pull/${pr}) | [\`${result.rev || "N/A"}\`](${baseUrl}/pull/${pr}/files) | ${pull.title} | [${pull.author.login}](https://github.com/${pull.author.login}/) | ${result.success ? "Yes" : "No"} |\n`;
112 }
113 output +=
114 "\n\nEnd of merge log. You can find the original README.md below the break.\n\n-----\n\n";
115 output += fs.readFileSync("./README.md");
116 fs.writeFileSync("./README.md", output);
117 await execa("git", ["add", "README.md"]);
118}
119
120async function fetchPullRequests(pulls, repoUrl, execa) {
121 console.log("::group::Fetch pull requests");
122 for (let pull of pulls) {
123 let pr = pull.number;
124 console.info(`Fetching PR ${pr} ...`);
125 await execa("git", [
126 "fetch",
127 "-f",
128 "--no-recurse-submodules",
129 repoUrl,
130 `pull/${pr}/head:pr-${pr}`,
131 ]);
132 }
133 console.log("::endgroup::");
134}
135
136async function mergePullRequests(pulls, execa) {
137 let mergeResults = {};
138 console.log("::group::Merge pull requests");
139 await execa("git", ["config", "--global", "user.name", "yuzubot"]);
140 await execa("git", [
141 "config",
142 "--global",
143 "user.email",
144 "yuzu\x40yuzu-emu\x2eorg", // prevent email harvesters from scraping the address
145 ]);
146 let hasFailed = false;
147 for (let pull of pulls) {
148 let pr = pull.number;
149 console.info(`Merging PR ${pr} ...`);
150 try {
151 const process1 = execa("git", [
152 "merge",
153 "--squash",
154 "--no-edit",
155 `pr-${pr}`,
156 ]);
157 process1.stdout.pipe(process.stdout);
158 await process1;
159
160 const process2 = execa("git", ["commit", "-m", `Merge PR ${pr}`]);
161 process2.stdout.pipe(process.stdout);
162 await process2;
163
164 const process3 = await execa("git", ["rev-parse", "--short", `pr-${pr}`]);
165 mergeResults[pr] = {
166 success: true,
167 rev: process3.stdout,
168 };
169 } catch (err) {
170 console.log(
171 `::error title=#${pr} not merged::Failed to merge pull request: ${pr}: ${err}`
172 );
173 mergeResults[pr] = { success: false };
174 hasFailed = true;
175 await execa("git", ["reset", "--hard"]);
176 }
177 }
178 console.log("::endgroup::");
179 if (hasFailed) {
180 throw 'There are merge failures. Aborting!';
181 }
182 return mergeResults;
183}
184
185async function mergebot(github, context, execa) {
186 const query = `query ($owner:String!, $name:String!, $label:String!) {
187 repository(name:$name, owner:$owner) {
188 pullRequests(labels: [$label], states: OPEN, first: 100) {
189 nodes {
190 number title author { login }
191 }
192 }
193 }
194 }`;
195 const variables = {
196 owner: context.repo.owner,
197 name: context.repo.repo,
198 label: CHANGE_LABEL,
199 };
200 const result = await github.graphql(query, variables);
201 const pulls = result.repository.pullRequests.nodes;
202 let displayList = [];
203 for (let i = 0; i < pulls.length; i++) {
204 let pull = pulls[i];
205 displayList.push({ PR: pull.number, Title: pull.title });
206 }
207 console.info("The following pull requests will be merged:");
208 console.table(displayList);
209 await fetchPullRequests(pulls, "https://github.com/yuzu-emu/yuzu", execa);
210 const mergeResults = await mergePullRequests(pulls, execa);
211 await generateReadme(pulls, context, mergeResults, execa);
212 await tagAndPush(github, context.repo.owner, `${context.repo.repo}-android`, execa, true);
213}
214
215module.exports.mergebot = mergebot;
216module.exports.checkAndroidChanges = checkAndroidChanges;
217module.exports.tagAndPush = tagAndPush;
218module.exports.checkBaseChanges = checkBaseChanges;
diff --git a/.github/workflows/android-publish.yml b/.github/workflows/android-publish.yml
new file mode 100644
index 000000000..8f46fcf74
--- /dev/null
+++ b/.github/workflows/android-publish.yml
@@ -0,0 +1,57 @@
1# SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2# SPDX-License-Identifier: GPL-2.0-or-later
3
4name: yuzu-android-publish
5
6on:
7 schedule:
8 - cron: '37 0 * * *'
9 workflow_dispatch:
10 inputs:
11 android:
12 description: 'Whether to trigger an Android build (true/false/auto)'
13 required: false
14 default: 'true'
15
16jobs:
17 android:
18 runs-on: ubuntu-latest
19 if: ${{ github.event.inputs.android != 'false' && github.repository == 'yuzu-emu/yuzu' }}
20 steps:
21 # this checkout is required to make sure the GitHub Actions scripts are available
22 - uses: actions/checkout@v3
23 name: Pre-checkout
24 with:
25 submodules: false
26 - uses: actions/github-script@v6
27 id: check-changes
28 name: 'Check for new changes'
29 env:
30 # 24 hours
31 DETECTION_TIME_FRAME: 86400000
32 with:
33 script: |
34 if (context.payload.inputs && context.payload.inputs.android === 'true') return true;
35 const checkAndroidChanges = require('./.github/workflows/android-merge.js').checkAndroidChanges;
36 return checkAndroidChanges(github, context);
37 - run: npm install execa@5
38 if: ${{ steps.check-changes.outputs.result == 'true' }}
39 - uses: actions/checkout@v3
40 name: Checkout
41 if: ${{ steps.check-changes.outputs.result == 'true' }}
42 with:
43 path: 'yuzu-merge'
44 fetch-depth: 0
45 submodules: true
46 token: ${{ secrets.ALT_GITHUB_TOKEN }}
47 - uses: actions/github-script@v5
48 name: 'Check and merge Android changes'
49 if: ${{ steps.check-changes.outputs.result == 'true' }}
50 env:
51 ALT_GITHUB_TOKEN: ${{ secrets.ALT_GITHUB_TOKEN }}
52 with:
53 script: |
54 const execa = require("execa");
55 const mergebot = require('./.github/workflows/android-merge.js').mergebot;
56 process.chdir('${{ github.workspace }}/yuzu-merge');
57 mergebot(github, context, execa);