Welcome to mirror list, hosted at ThFree Co, Russian Federation.

release-manager.js « scripts - github.com/npm/cli.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 8313692adfdc44ffe9c7694cc4a5f5956661d4e6 (plain)
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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
#!/usr/bin/env node

const { basename, relative } = require('path')
const cp = require('child_process')

const usage = () => `
  node ${relative(process.cwd(), __filename)} [flags]

  Copies the release process checklist to a GitHub issue, optionally updating the
  version and date of the instructions.
  
  Flags: [--create] [--update[=<issue-num>]] [--date=<YYYY-MM-DD>] [--version=X.Y.Z]

  [--create] (default: true)
  By default this will create a new issue in the repo.

  [--update[=<issue-num>]]
  Update a specific issue number, or if set without a value it will update the most
  recent issue created with the default tag.

  [--tag=<tag>] (default: "release-manager")
  Issues will be created and looked up with this tag.

  [--version=X.Y.Z]
  This script can be run before the next version number is known and then rerun
  with this flag to update the checklist with the correct version number.
  
  [--date=<YYYY-MM-DD>] (default: ${date()})
  Set the date of the release in the release process checklist.
`

const spawnSync = (cmd, args, options) => {
  const res = cp.spawnSync(cmd, args, { ...options, encoding: 'utf-8' })
  if (res.status !== 0) {
    throw new Error(res.stderr)
  }
  return res.stdout.trim()
}

const get = url =>
  new Promise((resolve, reject) => {
    require('https')
      .get(url, resp => {
        let d = ''
        resp.on('data', c => (d += c))
        resp.on('end', () => resolve(d))
      })
      .on('error', reject)
  })

const date = () => {
  const d = new Date()
  const pad = s => s.toString().padStart(2, '0')
  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
}

const replaceAll = (str, rep) =>
  Object.entries(rep).reduce(
    (a, [k, v]) => a.replace(new RegExp(k, 'g'), v),
    str
  )

const ghIssue = args => {
  const label = ['-l', args.label]
  const assignee = ['-a', args.assignee]
  const title = ['-t', args.title]
  const json = ['--json', 'body,title,number']
  const body = ['--body-file', '-']

  const issue = (cmd, a, options) =>
    spawnSync('gh', ['issue', cmd, '-R', args.repo, ...a.flat()], options)

  const listIssues = () => {
    const issues = JSON.parse(issue('list', [label, json]))
    const ids = issues.map(i => '#' + i.number)
    const msg = `Found existing label:${args.label} issues: ${ids.join(', ')}.`
    return { issues, msg }
  }

  switch (args.command) {
    case 'list': {
      // get the first matching issue
      const { issues, msg } = listIssues()
      if (issues.length > 1) {
        throw new Error(`${msg} Rerun with --update=<id> to target a specific issue.`)
      }
      return issues[0]
    }
    case 'view':
      // get an issue by id
      return JSON.parse(issue('view', [args.number, json]))
    case 'create': {
      const { issues, msg } = listIssues()
      if (issues.length) {
        throw new Error(`${msg} Close before creating a new one.`)
      }
      // create an issue
      return issue('create', [assignee, label, title, body], { input: args.body })
    }
    case 'edit':
      // edit title and body of an issue
      return issue('edit', [args.number, title, body], { input: args.body })
    default:
      throw new Error(`Unknown command: ${JSON.stringify(args.command)}`)
  }
}

const getSection = (content, args) => {
  const [, heading, section] = args.section.match(/^(#+)\s(.*)/)

  // remove the title since we are making a new one
  const [title, ...lines] = content
    .split(`${heading} `)
    .find(s => s.split('\n')[0].match(new RegExp(section, 'i')))
    .trim()
    .split('\n')

  // first task is to run this script, so thats done
  const body = lines.join('\n').replace('- [ ] **0', '- [x] **0')
  const created = `${basename(args.release)}${heading}${title}`

  return {
    title: `Release Manager: v${args.version} (${args.date})`,
    body: [
      `**Target Version**: v${args.version}`,
      `**Target Date**: ${args.date}`,
      // github markdown: 2x backticks + space will escape backticks within title
      `**Created From:** [\`\` ${created} \`\`](${args.release})`,
      body,
    ]
      .join('\n')
      .trim(),
  }
}

const main = async args => {
  const replace = s => replaceAll(s, args.replacements)

  const { body, title, number } = args.create
    // get a section of the release process wiki doc
    ? getSection(await get(args.release), args)
    // get the contents of an existing gh issue by id
    // or it will default to the most recent one by label
    // this is so it will preserve state of checked todo items
    : await ghIssue({
      ...args,
      command: typeof args.update === 'string' ? 'view' : 'list',
      number: args.update,
    })

  return ghIssue({
    ...args,
    command: number ? 'edit' : 'create',
    number,
    body: replace(body),
    title: replace(title),
  })
}

const parseArgs = raw => {
  const result = {
    create: false,
    update: null,
    repo: 'npm/cli',
    label: 'release: manager',
    assignee: '@me',
    date: date(),
    version: 'X.Y.Z',
    // look for that heading level with a match for the portion after
    section: '### .*cli.*',
    release:
      'https://raw.githubusercontent.com/wiki/npm/cli/Release-Process-(v8).md',
  }

  const replacements = {}

  const clean = {
    // this script will not work correctly with the tag style
    // of the version (prefixed with a v) so strip it out
    version: v => v.replace(/^v/g, ''),
  }

  const shorts = {
    R: 'repo',
    l: 'label',
    a: 'assignee',
    d: 'date',
    v: 'version',
    c: 'create',
    u: 'update',
  }

  const camel = k => k.replace(/-([a-z])/g, a => a[1].toUpperCase())

  // parse argv into array of [k,v] pairs
  // works with --x=1 --x 1 --x -x
  const argv = raw
    .join(' ') // join to a string
    .split(/(?:^|\s+)-/g) // split on starting dashses
    .map(x => x.trim().replace(/\s+/g, ' ')) // collapse spaces
    .filter(Boolean) // remove empties
    .map(x => x.split(/[=\s]/)) // split on equal or space
    .map(([k, v]) => [
      // we split on the initial dash previously so now
      // 1 dash means 2 and 0 means 1
      ...(k.startsWith('-') ? ['--', k.slice(1)] : ['-', k]),
      v ?? true, // default to true for no value
    ])
    .map(([dash, key, value]) => ({ dash, key: camel(key), value }))

  for (const { dash, key, value } of argv) {
    const k = dash.length < 2 ? shorts[key] : key
    if (Object.hasOwn(result, k)) {
      result[k] = clean[k] ? clean[k](value) : value
    } else {
      // any unknown arg is a replacement value
      replacements[k] = value
    }
  }

  if (!result.create && !result.update) {
    // set default to create if no command is specified
    result.create = true
  } else if (result.create && result.update) {
    throw new Error('Cannot set both create and update')
  }

  if (result.help) {
    console.error(usage())
    return process.exit(0)
  }

  return {
    ...result,
    replacements: {
      '(\\d+\\.\\d+\\.\\d+|X\\.Y\\.Z)': result.version,
      '(\\d{4}-\\d{2}-\\d{2}|YYYY-MM-DD)': result.date,
      ...replacements,
    },
  }
}

main(parseArgs(process.argv.slice(2)))
  .then(d => console.log(d))
  .catch(err => {
    console.error(err)
    process.exitCode = 1
  })