diff --git a/.github/helper-bot/.gitignore b/.github/helper-bot/.gitignore index eec56d637..7c1100e4d 100644 --- a/.github/helper-bot/.gitignore +++ b/.github/helper-bot/.gitignore @@ -1,3 +1,4 @@ node_modules package-lock.json -artifacts \ No newline at end of file +artifacts +.nyc_output/ \ No newline at end of file diff --git a/.github/helper-bot/index.js b/.github/helper-bot/index.js index 8f2501ee8..9769c6bd8 100644 --- a/.github/helper-bot/index.js +++ b/.github/helper-bot/index.js @@ -1,15 +1,66 @@ const fs = require('fs') const cp = require('child_process') -const github = require('gh-helpers')() const pcManifestURL = 'https://launchermeta.mojang.com/mc/game/version_manifest.json' const changelogURL = 'https://feedback.minecraft.net/hc/en-us/sections/360001186971-Release-Changelogs' +// Initialize github helper - can be mocked for testing +let github +try { + github = require('gh-helpers')() +} catch (e) { + // For testing environment, create mock + github = { + mock: true, + createPullRequest: () => {}, + sendWorkflowDispatch: () => {}, + findIssue: () => {}, + close: () => {}, + createIssue: () => {} + } +} + +// Testable functions function exec (file, args, options = {}) { const opts = { stdio: 'inherit', ...options } console.log('> ', file, args.join(' '), options.cwd ? `(cwd: ${options.cwd})` : '') return github.mock ? undefined : cp.execFileSync(file, args, opts) } -const download = (url, dest) => exec('curl', ['-L', url, '-o', dest]) + +function download(url, dest) { + return exec('curl', ['-L', url, '-o', dest]) +} + +function sanitizeVersion(version) { + return version?.replace(/[^a-zA-Z0-9_.]/g, '_') +} + +function generateBranchName(edition, version) { + const branchNameVersion = version.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase() + return `${edition}-${branchNameVersion}` +} + +function createPRBody(edition, version, issueUrl, protocolVersion, branchName) { + return ` +This automated PR sets up the relevant boilerplate for Minecraft ${edition} version ${version}. Fixes ${issueUrl}. + +Related: +- Issue: ${issueUrl} +- Protocol Version: ${protocolVersion} + + +* You can help contribute to this PR by opening a PR against this ${branchName} branch instead of master. +` +} + +function createWorkflowDispatch(repo, workflow, inputs) { + return { + owner: 'PrismarineJS', + repo, + workflow, + branch: repo === 'minecraft-data-generator' ? 'main' : 'master', + inputs + } +} function buildFirstIssue (title, result, jarData) { const protocolVersion = jarData?.protocol_version || 'Failed to obtain from JAR' @@ -43,8 +94,7 @@ async function createInitialPull (edition, issueUrl, { version, protocolVersion exec('npm', ['install'], { cwd: 'tools/js' }) exec('npm', ['run', 'version', edition, version, protocolVersion], { cwd: 'tools/js' }) exec('npm', ['run', 'build'], { cwd: 'tools/js' }) - const branchNameVersion = version.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase() - const branchName = `${edition}-${branchNameVersion}` + const branchName = generateBranchName(edition, version) const title = `🎈 Add Minecraft ${edition} ${version} data` // First, delete any existing branch try { @@ -58,16 +108,7 @@ async function createInitialPull (edition, issueUrl, { version, protocolVersion exec('git', ['add', '--all']) exec('git', ['commit', '-m', title]) exec('git', ['push', 'origin', branchName, '--force']) - const body = ` -This automated PR sets up the relevant boilerplate for Minecraft ${edition} version ${version}. Fixes ${issueUrl}. - -Related: -- Issue: ${issueUrl} -- Protocol Version: ${protocolVersion} - - -* You can help contribute to this PR by opening a PR against this ${branchName} branch instead of master. -` + const body = createPRBody(edition, version, issueUrl, protocolVersion, branchName) const pr = await github.createPullRequest(title, body, branchName, 'master') pr.branchName = branchName return pr @@ -151,31 +192,19 @@ async function updateManifestPC () { }) console.log('Created PR', pr) // Ask minecraft-data-generator to handle new update - const dispatchPayload = { - owner: 'PrismarineJS', - repo: 'minecraft-data-generator', - workflow: 'handle-mcdata-update.yml', - branch: 'main', - inputs: { - version: latestVersion, - issue_number: issue?.number, - pr_number: pr?.number - } - } + const dispatchPayload = createWorkflowDispatch('minecraft-data-generator', 'handle-mcdata-update.yml', { + version: latestVersion, + issue_number: issue?.number, + pr_number: pr?.number + }) console.log('Sending workflow dispatch', dispatchPayload) await github.sendWorkflowDispatch(dispatchPayload) // Ask node-minecraft-protocol to handle new update - const nodeDispatchPayload = { - owner: 'PrismarineJS', - repo: 'node-minecraft-protocol', - workflow: 'update-from-minecraft-data.yml', - branch: 'master', - inputs: { - new_mc_version: latestVersion, - mcdata_branch: pr.branchName, - mcdata_pr_url: pr.url - } - } + const nodeDispatchPayload = createWorkflowDispatch('node-minecraft-protocol', 'update-from-minecraft-data.yml', { + new_mc_version: latestVersion, + mcdata_branch: pr.branchName, + mcdata_pr_url: pr.url + }) console.log('Sending workflow dispatch', nodeDispatchPayload) await github.sendWorkflowDispatch(nodeDispatchPayload) // node-minecraft-protocol would then dispatch to mineflayer @@ -233,4 +262,19 @@ async function updateManifestPC () { } } -updateManifestPC() +// Export for testing +module.exports = { + sanitizeVersion, + buildFirstIssue, + generateBranchName, + createPRBody, + createWorkflowDispatch, + createInitialPull, + updateManifestPC, + setGithub: (mockGithub) => { github = mockGithub } +} + +// Run main function if called directly +if (require.main === module) { + updateManifestPC() +} diff --git a/.github/helper-bot/package.json b/.github/helper-bot/package.json index ee4b17df1..778af5d5f 100644 --- a/.github/helper-bot/package.json +++ b/.github/helper-bot/package.json @@ -1,11 +1,25 @@ { + "name": "minecraft-data-helper-bot", + "version": "1.0.0", + "description": "Helper bot for minecraft-data automation", + "main": "index.js", "scripts": { - "fix": "standard --fix" + "test": "mocha test/**/*.test.js", + "test:watch": "mocha test/**/*.test.js --watch", + "test:coverage": "nyc mocha test/**/*.test.js" + }, + "devDependencies": { + "mocha": "^10.2.0", + "sinon": "^17.0.1", + "nyc": "^15.1.0" }, "dependencies": { - "gh-helpers": "^1.0.0" + "gh-helpers": "*" }, - "devDependencies": { - "standard": "^17.1.2" - } + "directories": { + "test": "test" + }, + "keywords": [], + "author": "", + "license": "ISC" } diff --git a/.github/helper-bot/test/index.test.js b/.github/helper-bot/test/index.test.js new file mode 100644 index 000000000..756d53a25 --- /dev/null +++ b/.github/helper-bot/test/index.test.js @@ -0,0 +1,199 @@ +const sinon = require('sinon') +const fs = require('fs') +const assert = require('assert') +const path = require('path') + +// Import the actual implementation +const helperBot = require('../index') + +describe('Minecraft Data Helper Bot', function() { + let fsStub + + beforeEach(function() { + fsStub = sinon.stub(fs, 'readFileSync') + }) + + afterEach(function() { + sinon.restore() + }) + + describe('sanitizeVersion', function() { + it('should sanitize version strings correctly', function() { + const testCases = [ + { input: '1.21.9', expected: '1.21.9' }, + { input: '1.21.9-test', expected: '1.21.9_test' }, + { input: '24w01a', expected: '24w01a' }, + { input: 'invalid!@#$%^&*()', expected: 'invalid__________' }, + { input: undefined, expected: undefined } + ] + + testCases.forEach(({ input, expected }) => { + const result = helperBot.sanitizeVersion(input) + assert.strictEqual(result, expected, `sanitizeVersion('${input}') should return '${expected}'`) + }) + }) + }) + + describe('generateBranchName', function() { + it('should create correct branch names', function() { + const testCases = [ + { edition: 'pc', version: '1.21.9', expected: 'pc-1_21_9' }, + { edition: 'bedrock', version: '1.21.9-test', expected: 'bedrock-1_21_9_test' }, + { edition: 'pc', version: '24w01a', expected: 'pc-24w01a' }, + { edition: 'pc', version: 'test!@#', expected: 'pc-test___' } + ] + + testCases.forEach(({ edition, version, expected }) => { + const result = helperBot.generateBranchName(edition, version) + assert.strictEqual(result, expected, `generateBranchName('${edition}', '${version}') should return '${expected}'`) + }) + }) + }) + + describe('buildFirstIssue', function() { + it('should create correct issue format', function() { + const title = 'Support Minecraft PC 1.21.9' + const result = { + id: '1.21.9', + type: 'release', + releaseTime: '2024-01-01T00:00:00Z' + } + const jarData = { + protocol_version: 767, + name: 'Minecraft 1.21.9', + world_version: 3955, + java_version: 21 + } + + const issue = helperBot.buildFirstIssue(title, result, jarData) + + assert.strictEqual(issue.title, title) + assert(issue.body.includes('version **1.21.9**'), 'Should contain version') + assert(issue.body.includes('Protocol ID767'), 'Should contain protocol version') + assert(issue.body.includes('Data Version3955'), 'Should contain data version') + assert(issue.body.includes('Java Version21'), 'Should contain Java version') + assert(issue.body.includes('2024-01-01T00:00:00Z'), 'Should contain release date') + }) + + it('should handle missing jar data', function() { + const title = 'Support Minecraft PC 1.21.9' + const result = { + id: '1.21.9', + type: 'release', + releaseTime: '2024-01-01T00:00:00Z' + } + + const issue = helperBot.buildFirstIssue(title, result) + + assert(issue.body.includes('Failed to obtain from JAR'), 'Should handle missing protocol version') + assert(issue.body.includes('1.21.9'), 'Should use result.id as name') + }) + }) + + describe('createPRBody', function() { + it('should create correct PR body format', function() { + const edition = 'pc' + const version = '1.21.9' + const issueUrl = 'https://github.com/test/issues/123' + const protocolVersion = 767 + const branchName = 'pc-1_21_9' + + const body = helperBot.createPRBody(edition, version, issueUrl, protocolVersion, branchName) + + assert(body.includes(`Minecraft ${edition} version ${version}`), 'Should contain edition and version') + assert(body.includes(`Fixes ${issueUrl}`), 'Should contain issue URL') + assert(body.includes(`Protocol Version: ${protocolVersion}`), 'Should contain protocol version') + assert(body.includes(`${branchName} branch`), 'Should contain branch name') + assert(body.includes('master'), 'Should reference master branch') + }) + }) + + describe('createWorkflowDispatch', function() { + it('should create correct workflow dispatch payload for minecraft-data-generator', function() { + const repo = 'minecraft-data-generator' + const workflow = 'handle-mcdata-update.yml' + const inputs = { + version: '1.21.9', + issue_number: 123, + pr_number: 456 + } + + const result = helperBot.createWorkflowDispatch(repo, workflow, inputs) + + const expected = { + owner: 'PrismarineJS', + repo: 'minecraft-data-generator', + workflow: 'handle-mcdata-update.yml', + branch: 'main', + inputs: { + version: '1.21.9', + issue_number: 123, + pr_number: 456 + } + } + + assert.deepStrictEqual(result, expected, 'Should create correct dispatch payload') + }) + + it('should create correct workflow dispatch payload for node-minecraft-protocol', function() { + const repo = 'node-minecraft-protocol' + const workflow = 'update-from-minecraft-data.yml' + const inputs = { + new_mc_version: '1.21.9', + mcdata_branch: 'pc-1_21_9', + mcdata_pr_url: 'https://github.com/test/pr/456' + } + + const result = helperBot.createWorkflowDispatch(repo, workflow, inputs) + + const expected = { + owner: 'PrismarineJS', + repo: 'node-minecraft-protocol', + workflow: 'update-from-minecraft-data.yml', + branch: 'master', + inputs: { + new_mc_version: '1.21.9', + mcdata_branch: 'pc-1_21_9', + mcdata_pr_url: 'https://github.com/test/pr/456' + } + } + + assert.deepStrictEqual(result, expected, 'Should create correct dispatch payload with master branch') + }) + }) + + describe('Integration Tests', function() { + beforeEach(function() { + sinon.restore() + // Stub all file operations + sinon.stub(fs, 'readFileSync') + sinon.stub(fs, 'writeFileSync') + }) + + it('should handle version sanitization in full process', function() { + const result = helperBot.sanitizeVersion('1.21.9-test!@#') + assert.strictEqual(result, '1.21.9_test___', 'sanitizeVersion preserves dots and underscores') + + const branchName = helperBot.generateBranchName('pc', '1.21.9-test!@#') + assert.strictEqual(branchName, 'pc-1_21_9_test___', 'generateBranchName replaces non-alphanumeric') + }) + + it('should create consistent branch names and PR bodies', function() { + const edition = 'pc' + const version = '1.21.9' + const branchName = helperBot.generateBranchName(edition, version) + const prBody = helperBot.createPRBody(edition, version, 'test-url', 767, branchName) + + assert(prBody.includes(branchName), 'PR body should reference the generated branch name') + assert.strictEqual(branchName, 'pc-1_21_9', 'Branch name should be consistent') + }) + + it('should create workflow dispatch payloads with correct branch names', function() { + const generatorDispatch = helperBot.createWorkflowDispatch('minecraft-data-generator', 'test.yml', {}) + const protocolDispatch = helperBot.createWorkflowDispatch('node-minecraft-protocol', 'test.yml', {}) + + assert.strictEqual(generatorDispatch.branch, 'main', 'minecraft-data-generator should use main branch') + assert.strictEqual(protocolDispatch.branch, 'master', 'node-minecraft-protocol should use master branch') + }) + }) +}) \ No newline at end of file diff --git a/.github/workflows/update-helper.yml b/.github/workflows/update-helper.yml index 914a40428..f40f8a6fd 100644 --- a/.github/workflows/update-helper.yml +++ b/.github/workflows/update-helper.yml @@ -10,11 +10,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@master + uses: actions/checkout@v4 - name: Set up Node.js - uses: actions/setup-node@master + uses: actions/setup-node@v4 with: - node-version: 22.0.0 + node-version: 22.x - name: Install Github Actions toolkit run: npm i working-directory: .github/helper-bot