Skip to content

Commit

Permalink
Refactor(accureleaser): 从主仓库迁移主包过来
Browse files Browse the repository at this point in the history
  • Loading branch information
E0SelmY4V committed Apr 3, 2024
1 parent 105ff87 commit 8e296ad
Show file tree
Hide file tree
Showing 14 changed files with 500 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/accureleaser/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
**/*.d.ts
181 changes: 181 additions & 0 deletions packages/accureleaser/README.md
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
|----------------------|
| 测试、打标签、发布 |
|----------------------|
```
23 changes: 23 additions & 0 deletions packages/accureleaser/lib/constant.js
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);
});
};
}
53 changes: 53 additions & 0 deletions packages/accureleaser/lib/git.js
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));
}

95 changes: 95 additions & 0 deletions packages/accureleaser/lib/index.js
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() {
// }
}
13 changes: 13 additions & 0 deletions packages/accureleaser/lib/npm.js
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);
}
Loading

0 comments on commit 8e296ad

Please sign in to comment.