Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | #!/usr/bin/env node
'use strict'
/**
* create-pr
*
* Creates the release branch, commits the icon changes, pushes, and opens a
* PR (release notes become the PR body). It makes NO recovery decisions: any
* git/gh failure stops the pipeline and surfaces the error verbatim. It never
* rebases, force-pushes, or retries.
*
* --branch is REQUIRED (no auto-naming) so the human always confirms it.
*
* Usage:
* node .claude/skills/icons-release/scripts/create-pr.js --release R264 --branch release/10.17.0 [--base develop] [--dry-run]
*
* Output: { success, step, data: { branch, base, title, prUrl } }
*/
const fs = require('fs')
const path = require('path')
const { spawnSync } = require('child_process')
const { succeed, failWith, run, log } = require('./lib/output')
const { parseArgs } = require('./lib/args')
const { loadWorkset } = require('./lib/workset')
const STEP = 'create-pr'
// Commands are [executable, ...args] and run without a shell, so user-provided
// values (branch, base) are passed to git/gh literally and never shell-parsed.
function formatCmd([file, ...args]) {
return [file, ...args.map(a => (/^[\w./@:=-]+$/.test(a) ? a : JSON.stringify(a)))].join(' ')
}
function exec([file, ...args]) {
const result = spawnSync(file, args, { encoding: 'utf8' })
if (result.error) log(result.error.message)
return { status: result.status === null ? 1 : result.status, stdout: (result.stdout || '').trim(), stderr: (result.stderr || '').trim() }
}
function runCmd(command, dryRun) {
log(`${dryRun ? '[dry-run] ' : ''}> ${formatCmd(command)}`)
if (dryRun) return { status: 0, stdout: '' }
const result = exec(command)
if (result.stdout) log(result.stdout)
if (result.stderr) log(result.stderr)
return result
}
function main() {
const args = parseArgs(process.argv.slice(2))
const dryRun = !!args['dry-run']
if (!args.release) failWith(STEP, 'missingArg', '--release is required (e.g. --release R264)')
if (!args.branch) {
failWith(STEP, 'missingBranch', '--branch is required (no auto-naming; confirm with the user, e.g. release/10.17.0)')
}
const base = String(args.base || 'develop')
const branch = String(args.branch)
for (const [name, value] of [['branch', branch], ['base', base]]) {
if (exec(['git', 'check-ref-format', '--branch', value]).status !== 0 || value.startsWith('-')) {
failWith(STEP, 'invalidBranch', `--${name} "${value}" is not a valid git branch name.`)
}
}
// `git add svg model/metadata` doesn't drop entries that are already staged,
// so anything staged beforehand would ride along in the release commit.
const staged = exec(['git', 'diff', '--cached', '--name-only'])
if (staged.status !== 0) failWith(STEP, 'commandFailed', `"git diff --cached" exited with code ${staged.status}`)
if (staged.stdout) {
failWith(
STEP,
'stagedChanges',
`Unrelated changes are already staged. Unstage them before creating the release PR:\n${staged.stdout}`
)
}
const { release, location, icons } = loadWorkset(args.release)
const releaseNotesPath = path.join(location, 'releasenotes.md')
if (!fs.existsSync(releaseNotesPath)) {
failWith(STEP, 'noReleaseNotes', `${releaseNotesPath} not found. Run copy-files first.`)
}
const title = `Release ${release} icons: ${icons.length} added`
const commands = [
['git', 'checkout', '-b', branch],
['git', 'add', 'svg', 'model/metadata'],
['git', 'commit', '-m', title],
['git', 'push', '-u', 'origin', branch],
['gh', 'pr', 'create', '--base', base, '--head', branch, '--title', title, '--body-file', releaseNotesPath]
]
let prUrl = null
for (const command of commands) {
const result = runCmd(command, dryRun)
if (result.status !== 0) {
failWith(STEP, 'commandFailed', `"${formatCmd(command)}" exited with code ${result.status}`)
}
if (command[0] === 'gh' && result.stdout) {
const match = result.stdout.match(/https?:\/\/\S+/)
prUrl = match ? match[0] : result.stdout
}
}
succeed(STEP, { release, branch, base, title, prUrl, dryRun })
}
run(STEP, main)
|