-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
14 changed files
with
500 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
**/*.d.ts |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,181 @@ | ||
# 「精确类型」配套自动发布工具 | ||
|
||
本工具用以给「精确类型」代码仓库自动发布。 | ||
|
||
## 主要包(类型工具)发布流程 | ||
|
||
「精确类型」代码仓库中的类型工具包们应该是这么被发布的: | ||
|
||
### 发布开发版本 | ||
|
||
开发过程由功能进行细分,不同功能在不同的 Git 分支上开发,产生不同的开发版本。 | ||
|
||
Git 分支的命名格式类似 `dev-<feature>` 。 | ||
|
||
开发版本带 `dev-<feature>` 标签,版本号形如 `<x>.<y>.<z>-dev.<featrue>.<milestone>.<number>` 。 | ||
其中: | ||
|
||
- `<x>.<y>.<z>` 表示作为此功能开发基础的非开发版本的版本号; | ||
- `<featrue>` 表示功能的名字,遵循驼峰命名,比如 `compare` 、 `tailRecursion` 等; | ||
- `<milestone>` 表示目前的开发阶段,比如 `alpha` 、 `beta` 、 `release` 等; | ||
- `<number>` 表示在当前开发阶段迭代的次数。 | ||
|
||
比如 `2.0.3-dev.compare.beta.3` 表示是一个在 `2.0.3` 这个非开发版本的基础上,专注于 `compare` 这项功能,到达 `beta` 这个开发阶段后,又递增了 `3` 次的开发版本。 | ||
|
||
- 如果又加了一些功能,这个版本号再递增一次就是 `2.0.3-dev.compare.beta.4` ; | ||
- 如果达到下一个开发阶段了,就变成 `2.0.3-dev.compare.release.0` ; | ||
- 如果开发中 `2.0.3` 修复了一个 bug ,变成了 `2.0.4` 版本。 | ||
那么采用这个新版本来继续开发 `compare` 功能的开发版本就是 `2.0.4-dev.compare.beta.3` 。 | ||
|
||
一般情况下到达 `release` 阶段之后,开发版本的发布频率就可以大大降低了。 | ||
为了避免标签太多,在一个特功能达 `release` 阶段之后,可以把对应的标签给删了。 | ||
|
||
### 发布前瞻版本 | ||
|
||
前瞻版本用来汇总开发中的功能。 | ||
当某个功能开发得比较成熟时,可以把这个功能的开发版本汇总到前瞻版本给大家用。 | ||
最好是开发阶段达到 `beta` 以上,才被认为是功能开发得比较成熟。 | ||
|
||
前瞻版本应该在名叫 `next` 的 Git 分支上发布,这个分支应该只能合并其他分支,不能进行提交。 | ||
将开发分支合并到 `next` 分支来汇总功能。 | ||
|
||
前瞻版本带 `next` 标签,版本号形如 `<x>.<y>.<z>` 。 | ||
其中: | ||
|
||
- 版本号中 `<x>` 应该是偶数。 | ||
- 如果一个已被汇总功能的新的开发版本被汇总进来了,那递增一下 `<z>` ; | ||
- 如果一个新功能被汇总到了前瞻版本,那么递增一下 `<y>` ,同时 `<z>` 重置为 `0`; | ||
- 如果某次汇总包含一些破坏性更新,版本号可以从更高的 `<x>.0.0` 版本开始。 | ||
比如在 `2.3.9` 的前瞻版本上汇总了一个叫 `deleteAllAndGiveEverythingUp` 的包含破坏性更新的功能,那这个前瞻版本可以叫 `4.0.0` 。 | ||
|
||
### 发布正式版本 | ||
|
||
与前瞻版本类似,正式版本用来汇总成熟的功能。 | ||
如果一个功能到达了 `release` 开发阶段,那就可以把这个功能合并到正式版本发布。 | ||
|
||
前瞻版本应该在 Git 仓库的主分支上发布,比如 `master` 分支。 | ||
将开发分支合并到主分支来汇总功能。 | ||
|
||
正式版本的标签是 `latest` ,形如 `<x>.<y>.<z>` 。 | ||
其中: | ||
|
||
- 版本号中 `<x>` 应该是奇数。 | ||
- 如果一个已被汇总功能的新的开发版本被汇总进来了,那递增一下 `<z>` ; | ||
- 如果一个新功能被汇总到了正式版本,那么递增一下 `<y>` ,同时 `<z>` 重置为 `0`。; | ||
- 如果某次汇总包含一些破坏性更新,版本号可以从更高的 `<x>.0.0` 版本开始。 | ||
比如在 `1.2.4` 的正式版本上汇总了一个叫 `deprecateAllApisAndKillThePackage` 的包含破坏性更新的功能,那这个正式版本可以叫 `3.0.0` 。 | ||
|
||
### 更新正式版本 | ||
|
||
当正式版本中出现 bug 时,不再通过迭代开发版本来修复,而是直接在正式版本的基础上递增版本。 | ||
此时应该在 Git 的 `fix` 分支上进行修复。 | ||
修复完后合并到 `master` 分支并发布。 | ||
|
||
如果一个 bug 没法很快修好,那么把它当成一个功能来开发。 | ||
这个功能最好由 `fix` 开头命名,比如 `fixTooManyUsefulfunction` 。 | ||
|
||
## 其他包发布流程 | ||
|
||
对于其他包,对应的发布流程各有不同。 | ||
|
||
### 直接在正式版本上开发 | ||
|
||
有些包由于代码简单,可以直接在正式版本上开发、迭代。 | ||
比如 `eslint-plugin-accurtype-style` 这个包。 | ||
|
||
这些包的开发通常也是在开发分支上进行的,但他们直接产生正式版本。 | ||
在开发分支上修改了包的内容后,直接修改包的版本为正式版本,再通过合并到主分支上来发布。 | ||
|
||
### 不包含前瞻版本的开发流程 | ||
|
||
有些包本身就比较具有实验性,或者此时还为到达稳定版本,可以在开发时跳过前瞻版本。 | ||
|
||
这种包的开发流程实际上就是主要包的流程除去前瞻版本。 | ||
其他包括开发版本、正式版本的发布等都相同。 | ||
|
||
## 工具需要实现的特性 | ||
|
||
需要实现的发布流程有如下几种: | ||
|
||
### 发布开发版本流程 | ||
|
||
使用伪代码表示开发版本的发布流程: | ||
|
||
```text | ||
循环一 { | ||
对于仓库里的每个包 { | ||
如果 ( | ||
公开性不为私有 | ||
且 ( | ||
package.json 是本次提交新建的 | ||
或 ( | ||
本次提交变更了版本号 | ||
且 当前版本与npm不同 | ||
) | ||
) | ||
) 则 { | ||
标记为需要发布 | ||
} | ||
} | ||
如果 ( | ||
当前提交是分支第一个提交 | ||
或 当前所有包的版本都与npm相同 | ||
) 则 { | ||
退出循环一 | ||
} | ||
回退到上一个提交 | ||
} | ||
对于每个被标记的包 { | ||
带着标签发布 | ||
} | ||
``` | ||
|
||
### 前瞻版本和正式版本发布流程 | ||
|
||
使用 ASCII 流程图表示: | ||
|
||
```text | ||
/----------------\ | ||
| 开发过程 | | ||
| V | ||
| |----------------| | ||
\--------| | | ||
| 发布开发版本 | | ||
/----------| |-----------\ | ||
| |----------------| | | ||
V V | ||
|------------------| |------------------| | ||
| 汇总到前瞻分支 | | 汇总到正式分支 | | ||
|------------------| |------------------| | ||
| | | ||
V V | ||
/------------\ /------------\ | ||
/ 是否存在开发 \ 是 |-------------| 是 / 是否存在开发 \ | ||
/ 阶段小于 beta / ---->| PR 不通过 |<---- / 阶段小于 / | ||
\ 的包 / |-------------| \ release 的包 / | ||
\------------/ \------------/ | ||
| | | ||
否 | 否 | | ||
| | | ||
\---------------------------------------/ | ||
| | ||
V | ||
|----------------| | ||
| 处理 PR 冲突 | | ||
|----------------| | ||
| | ||
V | ||
|----------------------------| | ||
| 根据分支元数据递增版本号 | | ||
|----------------------------| | ||
| | ||
V | ||
|----------------------| | ||
| 测试、打标签、发布 | | ||
|----------------------| | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { getDirname } from 'esm-entry'; | ||
import * as fsp from 'fs/promises'; | ||
import * as path from 'path'; | ||
import safe from './safe-do-fn.js'; | ||
|
||
export const myDir = path.join(getDirname(import.meta.url), '..'); | ||
export const myPackageJSON = JSON.parse(`${await safe.myPackageJSON(fsp.readFile, [path.join(myDir, 'package.json')])}`); | ||
/** | ||
* 把需要回调的函数包装成 Promise | ||
* @template {any[]} P | ||
* @template T | ||
* @param {(...args: [...P, (err: any, data: T) => void]) => any} fn 要被包装的函数 | ||
* @returns {(...args: P) => Promise<T>} | ||
*/ | ||
export function promisify(fn) { | ||
return (...args) => { | ||
return new Promise((res, rej) => { | ||
/**@type {[...P, (err: any, data: T) => void]} */ | ||
const argAll = [...args, (err, data) => (err ? rej(err) : res(data))]; | ||
fn(...argAll); | ||
}); | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import * as fsp from 'fs/promises'; | ||
import simpleGit from 'simple-git'; | ||
|
||
class ExtendedGit { | ||
/**@param {import('simple-git').SimpleGit} git Git 操作对象 */ | ||
constructor(git) { | ||
this.git = git; | ||
} | ||
|
||
/** | ||
* 获取提交的差异索引 | ||
* @param {string} commit 提交 | ||
* @param {string} file 文件 | ||
*/ | ||
diffIndex = async (commit, file = '') => { | ||
return await this.git.raw('diff-index', ...file ? [commit, file] : [commit]); | ||
}; | ||
/** | ||
* 查看一个文件本次提交是否是新建或修改过 | ||
* @param {string} file 文件路径 | ||
*/ | ||
isChanged = async file => { | ||
return await this.diffIndex('HEAD^', file) !== ''; | ||
}; | ||
/** | ||
* 查看一个文件本次提交是否是新建 | ||
* @param {string} file 文件路径 | ||
*/ | ||
isNewed = async file => { | ||
return (await this.diffIndex('HEAD^', file)).slice(1, 7) === '000000'; | ||
}; | ||
/** | ||
* 读取特定提交的文件 | ||
* @param {string} commit 提交 | ||
* @param {string} file 文件 | ||
*/ | ||
checkoutFile = async (commit, file) => { | ||
await this.git.checkout(commit, ['--', file]); | ||
const content = await fsp.readFile(file); | ||
await this.git.checkout('HEAD', ['--', file]); | ||
return content; | ||
}; | ||
} | ||
|
||
/** | ||
* 获得拓展过的 Git 操作对象 | ||
* @param {string} root Git 仓库地址 | ||
*/ | ||
export default function getGit(root) { | ||
const git = simpleGit(root); | ||
return Object.assign(git, new ExtendedGit(git)); | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
/** | ||
* 精确类型自动构建脚本 | ||
* @license MIT | ||
* @packageDocumentation @package | ||
*/ | ||
|
||
import { getPackages } from '@manypkg/get-packages'; | ||
import filenamify from 'filenamify'; | ||
import * as fsp from 'fs/promises'; | ||
import * as os from 'os'; | ||
import * as path from 'path'; | ||
import { myPackageJSON } from './constant.js'; | ||
import getGit from './git.js'; | ||
import safe from './safe-do-fn.js'; | ||
import * as Types from './types.js'; | ||
import { view } from './npm.js'; | ||
|
||
if (!Types.imported) throw Error('Cannot load type definitions'); | ||
|
||
export default class Releaser { | ||
/**@param {string} rootDir 项目的根目录 */ | ||
constructor(rootDir) { | ||
this.initer = this.init(rootDir); | ||
} | ||
|
||
/** | ||
* 初始化 | ||
* @param {string} rootDir 项目的根目录 | ||
* @private | ||
*/ | ||
async init(rootDir) { | ||
const osNotRealTempDir = await safe.osNotRealTempDir(os.tmpdir); | ||
const osTempDir = `${await safe.osTempDir(fsp.realpath, [osNotRealTempDir])}`; | ||
const tempDir = `${await safe.tempDir(fsp.mkdtemp, [path.join(osTempDir, `${filenamify(myPackageJSON.name)}-`)])}`; | ||
await safe.moveGit(fsp.cp, [path.join(rootDir, '.git'), path.join(tempDir, '.git'), { recursive: true }]); | ||
const git = getGit(tempDir); | ||
await safe.recoverGit(() => git.raw('restore', '.')); | ||
const packages = await safe.packages(getPackages, [tempDir]); | ||
return { osNotRealTempDir, osTempDir, tempDir, git, packages, rootDir }; | ||
} | ||
/** | ||
* 清理临时文件 | ||
* | ||
* 请在实例被使用完后再调用此方法! | ||
*/ | ||
async cleanTemp() { | ||
const { tempDir } = await this.initer; | ||
await safe.cleanTemp(fsp.rm, [tempDir, { recursive: true, force: true }]); | ||
} | ||
|
||
[Symbol.dispose] = () => this.cleanTemp(); | ||
[Symbol.asyncDispose] = () => this.cleanTemp(); | ||
|
||
/** | ||
* 获得上次提交的包描述 | ||
* @param {string} dir 包地址 | ||
* @returns {Promise<Types.PackageJSON>} | ||
* @protected | ||
*/ | ||
async checkoutPackageJson(dir) { | ||
const { git } = await this.initer; | ||
return JSON.parse(`${await safe.checkoutPackage(() => git.checkoutFile('HEAD^', `${dir}/package.json`))}`); | ||
} | ||
/** | ||
* 检查当前提交的版本是否需要被发布 | ||
* @type {(info: Types.Package) => Promise<boolean>} packageInfo 包信息 | ||
*/ | ||
async isUniqueVersion({ dir, packageJson: { version, name } }) { | ||
if ((await this.checkoutPackageJson(dir)).version === version) return false; | ||
if ((await safe.getNpmInfo(view, [name])).versions.include(version)) return false; | ||
return true; | ||
} | ||
/** | ||
* 获得本提交里被标记的包 | ||
*/ | ||
async getSignedPackages() { | ||
const { packages, git } = await this.initer; | ||
/**@type {Set<Types.Package>} */ | ||
const signeds = new Set(); | ||
for (const packageInfo of packages.packages) { | ||
if (packageInfo.packageJson.private ?? false) continue; | ||
if ( | ||
await safe.checkPackage(() => git.isNewed(`${packageInfo.dir}/package.json`)) | ||
|| await this.isUniqueVersion(packageInfo) | ||
) signeds.add(packageInfo); | ||
} | ||
return signeds; | ||
} | ||
|
||
// /** | ||
// * 发布开发版本 | ||
// */ | ||
// releaseDev() { | ||
// } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { exec as execCb } from 'child_process'; | ||
import { promisify } from 'util'; | ||
|
||
const exec = promisify(execCb); | ||
|
||
/** | ||
* 获取包信息 | ||
* @param {string} name 包名 | ||
*/ | ||
export async function view(name) { | ||
const rslt = await exec(`npm view --json ${name}`); | ||
return JSON.parse(rslt.stdout); | ||
} |
Oops, something went wrong.