diff --git a/src/cli.js b/src/cli.js index 08a4099..8fe0bcd 100755 --- a/src/cli.js +++ b/src/cli.js @@ -686,6 +686,7 @@ issue .option('-b, --body ', 'issue body (markdown)') .option('--body-file ', 'read the issue body from a file') .option('--assignee ', 'assign the issue to one or more users') + .option('--label ', 'apply one or more labels by name') .action(async (options) => { try { const config = loadConfig(); @@ -697,6 +698,9 @@ issue if (options.assignee && options.assignee.length) { payload.assignees = options.assignee; } + if (options.label && options.label.length) { + payload.labels = await resolveLabelIds(client, options.owner, options.repo, options.label); + } const result = await client.createIssue(options.owner, options.repo, payload); console.log(`Issue created: #${result.number} ${result.title}`); console.log(`URL: ${result.html_url}`); diff --git a/test/cli.test.js b/test/cli.test.js index e08255c..bbe1b43 100644 --- a/test/cli.test.js +++ b/test/cli.test.js @@ -77,6 +77,75 @@ test('issue create --body-file reports unreadable files cleanly', () => { } }); +test('issue create resolves repeated label names into the initial create payload', async () => { + const cfg = path.join(os.tmpdir(), `stoke-cfg-${process.pid}-issue-labels.json`); + const requests = []; + const server = http.createServer((req, res) => { + let data = ''; + req.on('data', (chunk) => { data += chunk; }); + req.on('end', () => { + requests.push({ method: req.method, url: req.url, body: data }); + res.writeHead(req.method === 'POST' ? 201 : 200, { 'Content-Type': 'application/json' }); + if (req.method === 'GET') { + res.end(JSON.stringify([ + { id: 107, name: 'ready', color: '0e8a16' }, + { id: 100, name: 'enhancement', color: '84b6eb' }, + ])); + } else { + res.end(JSON.stringify({ number: 27, title: 'Probe', html_url: 'https://forge.test/o/r/issues/27' })); + } + }); + }); + await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); + const { port } = server.address(); + fs.writeFileSync(cfg, JSON.stringify({ url: `http://127.0.0.1:${port}`, token: 'tok' })); + + try { + const res = await spawnAsync( + ['issue', 'create', '-o', 'o', '-r', 'r', '-t', 'Probe', '--label', 'ready', '--label', 'enhancement'], + { STOKE_CONFIG_FILE: cfg }, + ); + assert.equal(res.status, 0, res.stderr); + assert.deepEqual(requests.map(({ method }) => method), ['GET', 'POST']); + assert.match(requests[0].url, /^\/api\/v1\/repos\/o\/r\/labels\?/); + assert.equal(requests[1].url, '/api/v1/repos/o/r/issues'); + assert.deepEqual(JSON.parse(requests[1].body), { + title: 'Probe', + body: '', + labels: [107, 100], + }); + } finally { + await new Promise((resolve) => server.close(resolve)); + fs.unlinkSync(cfg); + } +}); + +test('issue create rejects an unknown label before creating the issue', async () => { + const cfg = path.join(os.tmpdir(), `stoke-cfg-${process.pid}-unknown-issue-label.json`); + let createRequests = 0; + const server = http.createServer((req, res) => { + if (req.method === 'POST') createRequests += 1; + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify([{ id: 107, name: 'ready', color: '0e8a16' }])); + }); + await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); + const { port } = server.address(); + fs.writeFileSync(cfg, JSON.stringify({ url: `http://127.0.0.1:${port}`, token: 'tok' })); + + try { + const res = await spawnAsync( + ['issue', 'create', '-o', 'o', '-r', 'r', '-t', 'Probe', '--label', 'nonexistent'], + { STOKE_CONFIG_FILE: cfg }, + ); + assert.equal(res.status, 1); + assert.match(res.stderr, /Label not found in o\/r: nonexistent/); + assert.equal(createRequests, 0); + } finally { + await new Promise((resolve) => server.close(resolve)); + fs.unlinkSync(cfg); + } +}); + test('pr show validates --number before any network call', () => { const cfg = path.join(os.tmpdir(), `stoke-cfg-${process.pid}.json`); fs.writeFileSync(cfg, JSON.stringify({ url: 'https://forge.test', token: 'tok' }));