diff --git a/.github/helper-bot/github-helper.js b/.github/helper-bot/github-helper.js new file mode 100644 index 0000000..08759b2 --- /dev/null +++ b/.github/helper-bot/github-helper.js @@ -0,0 +1,59 @@ +if (!process.env.CI) { + // mock a bunch of things for testing locally -- https://github.com/actions/toolkit/issues/71 + process.env.GITHUB_REPOSITORY = 'PrismarineJS/bedrock-protocol' + process.env.GITHUB_EVENT_NAME = 'issue_comment' + process.env.GITHUB_SHA = 'cb2fd97b6eae9f2c7fee79d5a86eb9c3b4ac80d8' + process.env.GITHUB_REF = 'refs/heads/master' + process.env.GITHUB_WORKFLOW = 'Issue comments' + process.env.GITHUB_ACTION = 'run1' + process.env.GITHUB_ACTOR = 'test-user' + module.exports = { getIssueStatus: () => ({}), updateIssue: () => {}, createIssue: () => {} } + return +} + +// const { Octokit } = require('@octokit/rest') // https://github.com/octokit/rest.js +const github = require('@actions/github') + +const token = process.env.GITHUB_TOKEN +const octokit = github.getOctokit(token) +const context = github.context + +async function getIssueStatus (title) { + // https://docs.github.com/en/rest/reference/search#search-issues-and-pull-requests + const existingIssues = await octokit.rest.search.issuesAndPullRequests({ + q: `is:issue repo:${process.env.GITHUB_REPOSITORY} in:title ${title}` + }) + // console.log('Existing issues', existingIssues) + const existingIssue = existingIssues.data.items.find(issue => issue.title === title) + + if (!existingIssue) return {} + + return { open: existingIssue.state === 'open', closed: existingIssue.state === 'closed', id: existingIssue.number } +} + +async function updateIssue (id, payload) { + const issue = await octokit.rest.issues.update({ + ...context.repo, + issue_number: id, + body: payload.body + }) + console.log(`Updated issue ${issue.data.title}#${issue.data.number}: ${issue.data.html_url}`) +} + +async function createIssue (payload) { + const issue = await octokit.rest.issues.create({ + ...context.repo, + ...payload + }) + console.log(`Created issue ${issue.data.title}#${issue.data.number}: ${issue.data.html_url}`) +} + +async function close (id, reason) { + if (reason) await octokit.rest.issues.createComment({ ...context.repo, issue_number: id, body: reason }) + const issue = await octokit.rest.issues.update({ ...context.repo, issue_number: id, state: 'closed' }) + console.log(`Closed issue ${issue.data.title}#${issue.data.number}: ${issue.data.html_url}`) +} + +if (process.env.CI) { + module.exports = { getIssueStatus, updateIssue, createIssue, close } +} diff --git a/.github/helper-bot/index.js b/.github/helper-bot/index.js new file mode 100644 index 0000000..58d7856 --- /dev/null +++ b/.github/helper-bot/index.js @@ -0,0 +1,137 @@ +// Automatic version update checker for Minecraft bedrock edition. +const fs = require('fs') +const cp = require('child_process') +const helper = require('./github-helper') +const latestVesionEndpoint = 'https://itunes.apple.com/lookup?bundleId=com.mojang.minecraftpe' +const changelogURL = 'https://feedback.minecraft.net/hc/en-us/sections/360001186971-Release-Changelogs' + +// Relevant infomation for us is: +// "version": "1.17.10", +// "currentVersionReleaseDate": "2021-07-13T15:35:49Z", +// "releaseNotes": "What's new in 1.17.10:\nVarious bug fixes", + +function buildFirstIssue (title, result, externalPatches) { + let commitData = '' + let protocolVersion = '?' + const date = new Date(result.currentVersionReleaseDate).toUTCString() + + for (const name in externalPatches) { + commitData += '### ' + name + '\n' + const [patches, diff] = externalPatches[name] + for (const [name, url] of patches) { + commitData += `${name}\n` + } + commitData += `\n**[See the diff between *${result.currentVersionReleaseDate}* and now](${diff})**\n` + } + try { protocolVersion = getProtocolVersion() } catch (e) { console.log(e) } + + return { + title, + body: ` +A new Minecraft Bedrock version is available (as of ${date}), version **${result.version}** + +## Official Changelog +* ${result.releaseNotes} *(via App Store)* +* ${changelogURL} + +## 3rd party protocol patches +${commitData} + +## Protocol Details +(I will close this issue automatically if "${result.version}" is added to index.d.ts on "master" and there are no X's below) + + + + +
Name${result.version}
Protocol ID${protocolVersion}
+ +----- + +🤖 I am a bot, I check for updates every 2 hours without a trigger. You can close this PR to prevent any further updates. + ` + } +} + +function getCommitsInRepo (repo, containing, since) { + const endpoint = `https://api.github.com/repos/${repo}/commits` + console.log('Getting', endpoint) + cp.execSync(`curl -L ${endpoint} -o commits.json`, { stdio: 'inherit', shell: true }) + const commits = JSON.parse(fs.readFileSync('./commits.json', 'utf-8')) + const relevant = [] + for (const commit of commits) { + if (commit.commit.message.includes(containing)) { + console.log('commit url', commit.html_url) + relevant.push([commit.commit.message, commit.html_url]) + } + } + if (since) { + cp.execSync(`curl -L ${endpoint}?since=${since} -o commits.json`, { stdio: 'inherit', shell: true }) + const commits = JSON.parse(fs.readFileSync('./commits.json', 'utf-8')) + const head = commits[0].sha + const tail = commits[commits.length - 1].sha + return [relevant, `https://github.com/${repo}/compare/${tail}..${head}`] + } + return [relevant] +} + +function getProtocolVersion () { + if (!fs.existsSync('./ProtocolInfo.php')) cp.execSync('curl -LO https://raw.githubusercontent.com/pmmp/PocketMine-MP/stable/src/pocketmine/network/mcpe/protocol/ProtocolInfo.php', { stdio: 'inherit', shell: true }) + const currentApi = fs.readFileSync('./ProtocolInfo.php', 'utf-8') + const [, latestProtocolVersion] = currentApi.match(/public const CURRENT_PROTOCOL = (\d+);/) + return latestProtocolVersion +} + +async function fetchLatest () { + if (!fs.existsSync('./results.json')) cp.execSync(`curl -L ${latestVesionEndpoint} -o results.json`, { stdio: 'inherit', shell: true }) + const json = require('./results.json') + const result = json.results[0] + // console.log(json) + + if (!fs.existsSync('./index.d.ts')) cp.execSync('curl -LO https://raw.githubusercontent.com/PrismarineJS/bedrock-protocol/master/index.d.ts', { stdio: 'inherit', shell: true }) + const currentApi = fs.readFileSync('./index.d.ts', 'utf-8') + const supportedVersions = currentApi.match(/type Version = ([^\n]+)/)[1].replace(/\||'/g, ' ').split(' ').map(k => k.trim()).filter(k => k.length) + console.log(supportedVersions) + + let { version, currentVersionReleaseDate, releaseNotes } = result + console.log(version, currentVersionReleaseDate, releaseNotes) + + const title = `Support Minecraft ${result.version}` + + const issueStatus = await helper.getIssueStatus(title) + + if (supportedVersions.includes(version)) { + if (issueStatus.open) { + helper.close(issueStatus.id, `Closing as ${version} is now supported`) + } + console.log('Latest version is supported.') + return + } + + + if (issueStatus.closed) { + // We already made an issue, but someone else already closed it, don't do anything else + console.log('I already made an issue, but it was closed') + return + } + + version = version.replace('.0', '') + const issuePayload = buildFirstIssue(title, result, { + PocketMine: getCommitsInRepo('pmmp/PocketMine-MP', version, currentVersionReleaseDate), + gophertunnel: getCommitsInRepo('Sandertv/gophertunnel', version, currentVersionReleaseDate), + CloudburstMC: getCommitsInRepo('CloudburstMC/Protocol', version, currentVersionReleaseDate) + }) + + if (issueStatus.open) { + helper.updateIssue(issueStatus.id, issuePayload) + } else { + helper.createIssue(issuePayload) + } + + fs.writeFileSync('./issue.md', issuePayload.body) + console.log('OK, wrote to ./issue.md', issuePayload) +} + +fetchLatest() diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0a1ac07..580260c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,8 +14,6 @@ jobs: strategy: matrix: node-version: [14.x] - env: - FORCE_BUILD: true steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} diff --git a/.github/workflows/update-helper.yml b/.github/workflows/update-helper.yml new file mode 100644 index 0000000..83503ac --- /dev/null +++ b/.github/workflows/update-helper.yml @@ -0,0 +1,24 @@ +name: Update Helper +on: + workflow_dispatch: + schedule: + - cron: "0 */2 * * *" + +jobs: + helper: + name: update-checker + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@master + - name: Set up Node.js + uses: actions/setup-node@master + with: + node-version: 16.0.0 + - name: Install Github Actions toolkit + run: npm i @actions/github + # The env vars contain the relevant trigger information, so we don't need to pass it + - name: Runs helper + run: cd .github/helper-bot && node index.js + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/test/internal.test.js b/test/internal.test.js index 32207e3..d6c11d8 100644 --- a/test/internal.test.js +++ b/test/internal.test.js @@ -3,6 +3,7 @@ const { timedTest } = require('./internal') const { proxyTest } = require('./proxy') const { Versions } = require('../src/options') +const { sleep } = require('../src/datatypes/util') describe('internal client/server test', function () { const vcount = Object.keys(Versions).length @@ -12,6 +13,7 @@ describe('internal client/server test', function () { it('connects ' + version, async () => { console.debug(version) await timedTest(version) + await sleep(100) }) } @@ -19,6 +21,8 @@ describe('internal client/server test', function () { it('proxies ' + version, async () => { console.debug(version) await proxyTest(version) + await sleep(5000) + console.debug('Done', version) }) } }) diff --git a/test/vanilla.test.js b/test/vanilla.test.js index 9cea359..3bae596 100644 --- a/test/vanilla.test.js +++ b/test/vanilla.test.js @@ -2,6 +2,7 @@ const { clientTest } = require('./vanilla') const { Versions } = require('../src/options') +const { sleep } = require('../src/datatypes/util') describe('vanilla server test', function () { const vcount = Object.keys(Versions).length @@ -10,6 +11,7 @@ describe('vanilla server test', function () { for (const version in Versions) { it('client spawns ' + version, async () => { await clientTest(version) + await sleep(100) }) } })