|
| 1 | +importtype{ResolvedChangelogConfig}from'changelogen' |
| 2 | + |
| 3 | +import{execSync}from'node:child_process' |
| 4 | +import{promisesasfsp}from'node:fs' |
| 5 | +import{join,resolve}from'node:path' |
| 6 | +importprocessfrom'node:process' |
| 7 | + |
| 8 | +import{determineSemverChange,generateMarkDown,getCurrentGitBranch,getGitDiff,loadChangelogConfig,parseCommits}from'changelogen' |
| 9 | +import{inc}from'semver' |
| 10 | + |
| 11 | +constrepo=`nuxt/cli` |
| 12 | +constcorePackage='nuxi' |
| 13 | +constignoredPackages=['create-nuxt-app'] |
| 14 | +constuser={ |
| 15 | +name: 'Daniel Roe', |
| 16 | +email: 'daniel@roe.dev', |
| 17 | +} |
| 18 | + |
| 19 | +asyncfunctionmain(){ |
| 20 | +constreleaseBranch=getCurrentGitBranch() |
| 21 | +constworkspace=awaitloadWorkspace(process.cwd()) |
| 22 | +constconfig=awaitloadChangelogConfig(process.cwd(),{}) |
| 23 | + |
| 24 | +constcommits=awaitgetLatestCommits(config).then(commits=>commits.filter(c=>config.types[c.type]&&!(c.type==='chore'&&c.scope==='deps'&&!c.isBreaking))) |
| 25 | +constbumpType=(awaitdetermineBumpType(config))||'patch' |
| 26 | + |
| 27 | +constnewVersion=inc(workspace.find(corePackage).data.version,bumpType) |
| 28 | +constchangelog=awaitgenerateMarkDown(commits,config) |
| 29 | + |
| 30 | +// Create and push a branch with bumped versions if it has not already been created |
| 31 | +constbranchExists=execSync(`git ls-remote --heads origin v${newVersion}`).toString().trim().length>0 |
| 32 | +if(!branchExists){ |
| 33 | +for(const[key,value]ofObject.entries(user)){ |
| 34 | +execSync(`git config --global user.${key} "${value}"`) |
| 35 | +execSync(`git config --global user.${key} "${value}"`) |
| 36 | +} |
| 37 | +execSync(`git checkout -b v${newVersion}`) |
| 38 | + |
| 39 | +for(constpkgofworkspace.packages.filter(p=>!p.data.private)){ |
| 40 | +workspace.setVersion(pkg.data.name,newVersion!) |
| 41 | +} |
| 42 | +awaitworkspace.save() |
| 43 | + |
| 44 | +execSync(`git commit -am v${newVersion}`) |
| 45 | +execSync(`git push -u origin v${newVersion}`) |
| 46 | +} |
| 47 | + |
| 48 | +// Get the current PR for this release, if it exists |
| 49 | +const[currentPR]=awaitfetch(`https://api.github.com/repos/${repo}/pulls?head=nuxt:v${newVersion}`).then(r=>r.json()) |
| 50 | +constcontributors=awaitgetContributors() |
| 51 | + |
| 52 | +constreleaseNotes=[ |
| 53 | +currentPR?.body.replace(/##👉Changelog[\s\S]*$/,'')||`> ${newVersion} is the next ${bumpType} release.\n>\n> **Timetable**: to be announced.`, |
| 54 | +'## 👉 Changelog', |
| 55 | +changelog |
| 56 | +.replace(/^##v.*\n/,'') |
| 57 | +.replace(`...${releaseBranch}`,`...v${newVersion}`) |
| 58 | +.replace(/###❤️Contributors[\s\S]*$/,''), |
| 59 | +'### ❤️ Contributors', |
| 60 | +contributors.map(c=>`- ${c.name} (@${c.username})`).join('\n'), |
| 61 | +].join('\n') |
| 62 | + |
| 63 | +// Create a PR with release notes if none exists |
| 64 | +if(!currentPR){ |
| 65 | +returnawaitfetch(`https://api.github.com/repos/${repo}/pulls`,{ |
| 66 | +method: 'POST', |
| 67 | +headers: { |
| 68 | +'Authorization': `token ${process.env.GITHUB_TOKEN}`, |
| 69 | +'content-type': 'application/json', |
| 70 | +}, |
| 71 | +body: JSON.stringify({ |
| 72 | +title: `v${newVersion}`, |
| 73 | +head: `v${newVersion}`, |
| 74 | +base: releaseBranch, |
| 75 | +body: releaseNotes, |
| 76 | +draft: true, |
| 77 | +}), |
| 78 | +}) |
| 79 | +} |
| 80 | + |
| 81 | +// Update release notes if the pull request does exist |
| 82 | +awaitfetch(`https://api.github.com/repos/${repo}/pulls/${currentPR.number}`,{ |
| 83 | +method: 'PATCH', |
| 84 | +headers: { |
| 85 | +'Authorization': `token ${process.env.GITHUB_TOKEN}`, |
| 86 | +'content-type': 'application/json', |
| 87 | +}, |
| 88 | +body: JSON.stringify({ |
| 89 | +body: releaseNotes, |
| 90 | +}), |
| 91 | +}) |
| 92 | +} |
| 93 | + |
| 94 | +main().catch((err)=>{ |
| 95 | +console.error(err) |
| 96 | +process.exit(1) |
| 97 | +}) |
| 98 | + |
| 99 | +exportinterfaceDep{ |
| 100 | +name: string |
| 101 | +range: string |
| 102 | +type: string |
| 103 | +} |
| 104 | + |
| 105 | +typeThenArg<T>=TextendsPromiseLike<infer U> ? U : T |
| 106 | +exporttypePackage=ThenArg<ReturnType<typeofloadPackage>> |
| 107 | + |
| 108 | +exportasyncfunctionloadPackage(dir: string){ |
| 109 | +constpkgPath=resolve(dir,'package.json') |
| 110 | +constdata=JSON.parse(awaitfsp.readFile(pkgPath,'utf-8').catch(()=>'{}')) |
| 111 | +constsave=()=>fsp.writeFile(pkgPath,`${JSON.stringify(data,null,2)}\n`) |
| 112 | + |
| 113 | +constupdateDeps=(reviver: (dep: Dep)=>Dep|void)=>{ |
| 114 | +for(consttypeof['dependencies','devDependencies','optionalDependencies','peerDependencies']){ |
| 115 | +if(!data[type]){ |
| 116 | +continue |
| 117 | +} |
| 118 | +for(consteofObject.entries(data[type])){ |
| 119 | +constdep: Dep={name: e[0],range: e[1]asstring, type } |
| 120 | +deletedata[type][dep.name] |
| 121 | +constupdated=reviver(dep)||dep |
| 122 | +data[updated.type]=data[updated.type]||{} |
| 123 | +data[updated.type][updated.name]=updated.range |
| 124 | +} |
| 125 | +} |
| 126 | +} |
| 127 | + |
| 128 | +return{ |
| 129 | + dir, |
| 130 | + data, |
| 131 | + save, |
| 132 | + updateDeps, |
| 133 | +} |
| 134 | +} |
| 135 | + |
| 136 | +exportasyncfunctionloadWorkspace(dir: string){ |
| 137 | +constworkspacePkg=awaitloadPackage(dir) |
| 138 | + |
| 139 | +constpackages: Package[]=[] |
| 140 | + |
| 141 | +forawait(constpkgDiroffsp.glob(['packages/*'],{withFileTypes: true})){ |
| 142 | +if(!pkgDir.isDirectory()){ |
| 143 | +continue |
| 144 | +} |
| 145 | +constpkg=awaitloadPackage(join(pkgDir.parentPath,pkgDir.name)) |
| 146 | +if(!pkg.data.name||ignoredPackages.includes(pkg.data.name)){ |
| 147 | +continue |
| 148 | +} |
| 149 | +console.log(pkg.data.name) |
| 150 | +packages.push(pkg) |
| 151 | +} |
| 152 | + |
| 153 | +constfind=(name: string)=>{ |
| 154 | +constpkg=packages.find(pkg=>pkg.data.name===name) |
| 155 | +if(!pkg){ |
| 156 | +thrownewError(`Workspace package not found: ${name}`) |
| 157 | +} |
| 158 | +returnpkg |
| 159 | +} |
| 160 | + |
| 161 | +constrename=(from: string,to: string)=>{ |
| 162 | +find(from).data._name=find(from).data.name |
| 163 | +find(from).data.name=to |
| 164 | +for(constpkgofpackages){ |
| 165 | +pkg.updateDeps((dep)=>{ |
| 166 | +if(dep.name===from&&!dep.range.startsWith('npm:')){ |
| 167 | +dep.range=`npm:${to}@${dep.range}` |
| 168 | +} |
| 169 | +}) |
| 170 | +} |
| 171 | +} |
| 172 | + |
| 173 | +constsetVersion=(name: string,newVersion: string,opts: {updateDeps?: boolean}={})=>{ |
| 174 | +find(name).data.version=newVersion |
| 175 | +if(!opts.updateDeps){ |
| 176 | +return |
| 177 | +} |
| 178 | + |
| 179 | +for(constpkgofpackages){ |
| 180 | +pkg.updateDeps((dep)=>{ |
| 181 | +if(dep.name===name){ |
| 182 | +dep.range=newVersion |
| 183 | +} |
| 184 | +}) |
| 185 | +} |
| 186 | +} |
| 187 | + |
| 188 | +constsave=()=>Promise.all(packages.map(pkg=>pkg.save())) |
| 189 | + |
| 190 | +return{ |
| 191 | + dir, |
| 192 | + workspacePkg, |
| 193 | + packages, |
| 194 | + save, |
| 195 | + find, |
| 196 | + rename, |
| 197 | + setVersion, |
| 198 | +} |
| 199 | +} |
| 200 | + |
| 201 | +exportasyncfunctiondetermineBumpType(config: ResolvedChangelogConfig){ |
| 202 | +constcommits=awaitgetLatestCommits(config) |
| 203 | + |
| 204 | +constbumpType=determineSemverChange(commits,config) |
| 205 | + |
| 206 | +returnbumpType==='major' ? 'minor' : bumpType |
| 207 | +} |
| 208 | + |
| 209 | +exportasyncfunctiongetLatestCommits(config: ResolvedChangelogConfig){ |
| 210 | +constlatestTag=execSync('git describe --tags --abbrev=0').toString().trim() |
| 211 | + |
| 212 | +returnparseCommits(awaitgetGitDiff(latestTag),config) |
| 213 | +} |
| 214 | + |
| 215 | +exportasyncfunctiongetContributors(){ |
| 216 | +constcontributors=[]asArray<{name: string,username: string}> |
| 217 | +constemails=newSet<string>() |
| 218 | +constlatestTag=execSync('git describe --tags --abbrev=0').toString().trim() |
| 219 | +constrawCommits=awaitgetGitDiff(latestTag) |
| 220 | +for(constcommitofrawCommits){ |
| 221 | +if(emails.has(commit.author.email)||commit.author.name==='renovate[bot]'){ |
| 222 | +continue |
| 223 | +} |
| 224 | +const{ author }=awaitfetch(`https://api.github.com/repos/${repo}/commits/${commit.shortHash}`,{ |
| 225 | +headers: { |
| 226 | +'User-Agent': `${repo} github action automation`, |
| 227 | +'Accept': 'application/vnd.github.v3+json', |
| 228 | +'Authorization': `token ${process.env.GITHUB_TOKEN}`, |
| 229 | +}, |
| 230 | +}).then(r=>r.json()asPromise<{author: {login: string,email: string}}>) |
| 231 | +if(!contributors.some(c=>c.username===author.login)){ |
| 232 | +contributors.push({name: commit.author.name,username: author.login}) |
| 233 | +} |
| 234 | +emails.add(author.email) |
| 235 | +} |
| 236 | +returncontributors |
| 237 | +} |
0 commit comments