feat: automate tag release pipeline
Some checks failed
continuous-integration/drone/tag Build is failing
Some checks failed
continuous-integration/drone/tag Build is failing
This commit is contained in:
parent
57e4dc72aa
commit
d302614b99
34
.drone.yml
Normal file
34
.drone.yml
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
name: release-tag
|
||||||
|
|
||||||
|
trigger:
|
||||||
|
event:
|
||||||
|
- tag
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: install
|
||||||
|
image: node:20-alpine
|
||||||
|
commands:
|
||||||
|
- npm ci
|
||||||
|
|
||||||
|
- name: test
|
||||||
|
image: node:20-alpine
|
||||||
|
depends_on:
|
||||||
|
- install
|
||||||
|
commands:
|
||||||
|
- npm test
|
||||||
|
|
||||||
|
- name: release
|
||||||
|
image: node:20-alpine
|
||||||
|
depends_on:
|
||||||
|
- test
|
||||||
|
environment:
|
||||||
|
COS_BUCKET: wksgx-1343191620
|
||||||
|
COS_REGION: ap-nanjing
|
||||||
|
COS_SECRET_ID:
|
||||||
|
from_secret: cos_secret_id
|
||||||
|
COS_SECRET_KEY:
|
||||||
|
from_secret: cos_secret_key
|
||||||
|
commands:
|
||||||
|
- npm run release:tag
|
||||||
@ -94,6 +94,7 @@ npm run write:latest
|
|||||||
- 它是发给公司内部同事使用的交付包
|
- 它是发给公司内部同事使用的交付包
|
||||||
- 同事收到后需要解压,再到 `chrome://extensions` 中 `Load unpacked`
|
- 同事收到后需要解压,再到 `chrome://extensions` 中 `Load unpacked`
|
||||||
- COS 发布时,`latest.json` 放在 `star-chart-search-enhancer/latest.json`,ZIP 和 PDF 放在对应版本目录下
|
- COS 发布时,`latest.json` 放在 `star-chart-search-enhancer/latest.json`,ZIP 和 PDF 放在对应版本目录下
|
||||||
|
- 打 tag 后会触发 Drone 发布,推荐格式:`0.MMDD.N`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -36,7 +36,6 @@
|
|||||||
"identity",
|
"identity",
|
||||||
"storage"
|
"storage"
|
||||||
],
|
],
|
||||||
"version": "0.2.0421.2",
|
|
||||||
"web_accessible_resources": [
|
"web_accessible_resources": [
|
||||||
{
|
{
|
||||||
"matches": [
|
"matches": [
|
||||||
@ -48,6 +47,7 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"version": "0.2.0421.2",
|
||||||
"host_permissions": [
|
"host_permissions": [
|
||||||
"https://xingtu.cn/ad/creator/market*",
|
"https://xingtu.cn/ad/creator/market*",
|
||||||
"https://*.xingtu.cn/ad/creator/market*",
|
"https://*.xingtu.cn/ad/creator/market*",
|
||||||
|
|||||||
@ -42,6 +42,31 @@ Quick access check:
|
|||||||
curl -I https://wksgx-1343191620.cos.ap-nanjing.myqcloud.com/star-chart-search-enhancer/latest.json
|
curl -I https://wksgx-1343191620.cos.ap-nanjing.myqcloud.com/star-chart-search-enhancer/latest.json
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Drone Release Flow
|
||||||
|
|
||||||
|
Tag the repo to trigger the release pipeline:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git tag 0.0525.1
|
||||||
|
git push origin 0.0525.1
|
||||||
|
```
|
||||||
|
|
||||||
|
The Drone job will:
|
||||||
|
|
||||||
|
1. Run `npm ci`.
|
||||||
|
2. Run `npm test`.
|
||||||
|
3. Run `npm run release:tag`.
|
||||||
|
4. Build the release bundle.
|
||||||
|
5. Write `release/latest.json`.
|
||||||
|
6. Upload `latest.json`, the ZIP, and the PDF to COS.
|
||||||
|
|
||||||
|
Drone secrets required:
|
||||||
|
|
||||||
|
- `cos_secret_id`
|
||||||
|
- `cos_secret_key`
|
||||||
|
|
||||||
|
The pipeline uses the tag as the release version. Recommended format: `0.MMDD.N`.
|
||||||
|
|
||||||
## Coworker Install Steps
|
## Coworker Install Steps
|
||||||
|
|
||||||
1. Unzip `star-chart-search-enhancer-internal.zip`.
|
1. Unzip `star-chart-search-enhancer-internal.zip`.
|
||||||
|
|||||||
871
package-lock.json
generated
871
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -7,6 +7,7 @@
|
|||||||
"build": "node scripts/build.mjs",
|
"build": "node scripts/build.mjs",
|
||||||
"build:release": "BUILD_TARGET=release node scripts/build.mjs",
|
"build:release": "BUILD_TARGET=release node scripts/build.mjs",
|
||||||
"mock:protected-api": "node scripts/mock-protected-api.mjs",
|
"mock:protected-api": "node scripts/mock-protected-api.mjs",
|
||||||
|
"release:tag": "node scripts/ci/release-tag.mjs",
|
||||||
"package:internal": "npm run build:release && node scripts/package-release.mjs",
|
"package:internal": "npm run build:release && node scripts/package-release.mjs",
|
||||||
"package:release": "npm run build:release && node scripts/package-release.mjs",
|
"package:release": "npm run build:release && node scripts/package-release.mjs",
|
||||||
"write:latest": "node scripts/write-latest-manifest.mjs",
|
"write:latest": "node scripts/write-latest-manifest.mjs",
|
||||||
@ -19,6 +20,7 @@
|
|||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.59.1",
|
"@playwright/test": "^1.59.1",
|
||||||
|
"cos-nodejs-sdk-v5": "^2.15.4",
|
||||||
"jsdom": "^29.0.2",
|
"jsdom": "^29.0.2",
|
||||||
"tsup": "^8.5.1",
|
"tsup": "^8.5.1",
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.3",
|
||||||
|
|||||||
Binary file not shown.
71
scripts/ci/release-tag.mjs
Normal file
71
scripts/ci/release-tag.mjs
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import { execFile } from "node:child_process";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { promisify } from "node:util";
|
||||||
|
|
||||||
|
import { resolveReleaseVersion } from "../release-version.mjs";
|
||||||
|
import { uploadReleaseAssets } from "./upload-release-assets.mjs";
|
||||||
|
|
||||||
|
const execFileAsync = promisify(execFile);
|
||||||
|
|
||||||
|
export async function runReleaseTagPipeline(env = process.env) {
|
||||||
|
const projectRoot = resolveProjectRoot();
|
||||||
|
const releaseVersion = resolveReleaseVersion(env);
|
||||||
|
const publicBaseUrl =
|
||||||
|
env.UPDATE_PUBLIC_BASE_URL ??
|
||||||
|
`https://wksgx-1343191620.cos.ap-nanjing.myqcloud.com/star-chart-search-enhancer/releases/${releaseVersion}`;
|
||||||
|
|
||||||
|
console.log(`release version: ${releaseVersion}`);
|
||||||
|
console.log("running build:release");
|
||||||
|
await runNpmScript("build:release", projectRoot, {
|
||||||
|
...env,
|
||||||
|
EXTENSION_VERSION: releaseVersion
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("running package-release");
|
||||||
|
await runNodeScript("scripts/package-release.mjs", projectRoot, {
|
||||||
|
...env,
|
||||||
|
EXTENSION_VERSION: releaseVersion
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("writing latest manifest");
|
||||||
|
await runNpmScript("write:latest", projectRoot, {
|
||||||
|
...env,
|
||||||
|
EXTENSION_VERSION: releaseVersion,
|
||||||
|
UPDATE_PUBLIC_BASE_URL: publicBaseUrl
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("uploading release assets to COS");
|
||||||
|
await uploadReleaseAssets({
|
||||||
|
env: {
|
||||||
|
...env,
|
||||||
|
EXTENSION_VERSION: releaseVersion
|
||||||
|
},
|
||||||
|
projectRoot,
|
||||||
|
releaseVersion
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runNpmScript(scriptName, cwd, env) {
|
||||||
|
await execFileAsync("npm", ["run", scriptName], {
|
||||||
|
cwd,
|
||||||
|
env,
|
||||||
|
stdio: "inherit"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runNodeScript(scriptPath, cwd, env) {
|
||||||
|
await execFileAsync("node", [scriptPath], {
|
||||||
|
cwd,
|
||||||
|
env,
|
||||||
|
stdio: "inherit"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveProjectRoot() {
|
||||||
|
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||||
|
await runReleaseTagPipeline();
|
||||||
|
}
|
||||||
83
scripts/ci/upload-release-assets.mjs
Normal file
83
scripts/ci/upload-release-assets.mjs
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import COS from "cos-nodejs-sdk-v5";
|
||||||
|
import { readFile } from "node:fs/promises";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import path from "node:path";
|
||||||
|
import { buildReleaseUploadTargets } from "../release-assets.mjs";
|
||||||
|
|
||||||
|
export async function uploadReleaseAssets(options = {}) {
|
||||||
|
const env = options.env ?? process.env;
|
||||||
|
const projectRoot = options.projectRoot ?? resolveProjectRoot();
|
||||||
|
const releaseVersion = options.releaseVersion ?? env.EXTENSION_VERSION ?? env.DRONE_TAG;
|
||||||
|
|
||||||
|
if (!releaseVersion) {
|
||||||
|
throw new Error("release version is required for COS upload");
|
||||||
|
}
|
||||||
|
|
||||||
|
const cos = options.cosClient ?? createCosClient(env);
|
||||||
|
const targets =
|
||||||
|
options.targets ??
|
||||||
|
buildReleaseUploadTargets({
|
||||||
|
projectRoot,
|
||||||
|
releaseVersion
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const target of targets) {
|
||||||
|
const body = await readFile(target.localPath);
|
||||||
|
await putObjectAsync(cos, {
|
||||||
|
Bucket: getRequiredEnv(env, "COS_BUCKET"),
|
||||||
|
Body: body,
|
||||||
|
ContentType: getContentType(target.cosKey),
|
||||||
|
Key: target.cosKey,
|
||||||
|
Region: getRequiredEnv(env, "COS_REGION")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function putObjectAsync(client, params) {
|
||||||
|
return await new Promise((resolve, reject) => {
|
||||||
|
client.putObject(params, (error, data) => {
|
||||||
|
if (error) {
|
||||||
|
reject(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCosClient(env) {
|
||||||
|
return new COS({
|
||||||
|
SecretId: getRequiredEnv(env, "COS_SECRET_ID"),
|
||||||
|
SecretKey: getRequiredEnv(env, "COS_SECRET_KEY")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getContentType(key) {
|
||||||
|
if (key.endsWith(".json")) {
|
||||||
|
return "application/json";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key.endsWith(".pdf")) {
|
||||||
|
return "application/pdf";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key.endsWith(".zip")) {
|
||||||
|
return "application/zip";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "application/octet-stream";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRequiredEnv(env, name) {
|
||||||
|
const value = env[name];
|
||||||
|
if (!value) {
|
||||||
|
throw new Error(`${name} is required`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveProjectRoot() {
|
||||||
|
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||||
|
}
|
||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { resolveReleaseVersion } from "./release-version.mjs";
|
||||||
|
|
||||||
const sharedIcons = {
|
const sharedIcons = {
|
||||||
16: "assets/icons/icon-16.png",
|
16: "assets/icons/icon-16.png",
|
||||||
32: "assets/icons/icon-32.png",
|
32: "assets/icons/icon-32.png",
|
||||||
@ -34,7 +36,6 @@ const sharedManifest = {
|
|||||||
manifest_version: 3,
|
manifest_version: 3,
|
||||||
name: "Star Chart Search Enhancer",
|
name: "Star Chart Search Enhancer",
|
||||||
permissions: ["downloads", "identity", "storage"],
|
permissions: ["downloads", "identity", "storage"],
|
||||||
version: "0.2.0421.2",
|
|
||||||
web_accessible_resources: [
|
web_accessible_resources: [
|
||||||
{
|
{
|
||||||
matches: [
|
matches: [
|
||||||
@ -72,6 +73,7 @@ export function createManifest(options = {}) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...sharedManifest,
|
...sharedManifest,
|
||||||
|
version: resolveReleaseVersion(),
|
||||||
host_permissions: hostPermissions
|
host_permissions: hostPermissions
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
25
scripts/release-assets.mjs
Normal file
25
scripts/release-assets.mjs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
export function buildReleaseUploadTargets({
|
||||||
|
projectRoot,
|
||||||
|
releaseVersion
|
||||||
|
}) {
|
||||||
|
const releaseDir = path.join(projectRoot, "release");
|
||||||
|
const releasePrefix = "star-chart-search-enhancer";
|
||||||
|
const releaseVersionPrefix = `${releasePrefix}/releases/${releaseVersion}`;
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
cosKey: `${releasePrefix}/latest.json`,
|
||||||
|
localPath: path.join(releaseDir, "latest.json")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cosKey: `${releaseVersionPrefix}/star-chart-search-enhancer-internal.zip`,
|
||||||
|
localPath: path.join(releaseDir, "star-chart-search-enhancer-internal.zip")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cosKey: `${releaseVersionPrefix}/星图增强插件-超简单安装使用指南.pdf`,
|
||||||
|
localPath: path.join(releaseDir, "星图增强插件-超简单安装使用指南.pdf")
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
30
scripts/release-version.mjs
Normal file
30
scripts/release-version.mjs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
const RELEASE_VERSION_PATTERN = /^\d+(?:\.\d+)*$/;
|
||||||
|
|
||||||
|
export function normalizeReleaseVersionTag(value) {
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = value.trim().replace(/^v/i, "");
|
||||||
|
if (!RELEASE_VERSION_PATTERN.test(normalized)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveReleaseVersion(
|
||||||
|
env = process.env,
|
||||||
|
fallbackVersion = "0.2.0421.2"
|
||||||
|
) {
|
||||||
|
const candidates = [env.EXTENSION_VERSION, env.DRONE_TAG, fallbackVersion];
|
||||||
|
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
const normalized = normalizeReleaseVersionTag(candidate);
|
||||||
|
if (normalized) {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("unable to resolve a valid release version");
|
||||||
|
}
|
||||||
@ -3,13 +3,15 @@ import path from "node:path";
|
|||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import { createManifest } from "./manifest.mjs";
|
import { createManifest } from "./manifest.mjs";
|
||||||
import { createLatestManifest } from "./write-latest-manifest-data.mjs";
|
import { createLatestManifest } from "./write-latest-manifest-data.mjs";
|
||||||
|
import { resolveReleaseVersion } from "./release-version.mjs";
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
const projectRoot = path.resolve(__dirname, "..");
|
const projectRoot = path.resolve(__dirname, "..");
|
||||||
const releaseDir = path.join(projectRoot, "release");
|
const releaseDir = path.join(projectRoot, "release");
|
||||||
const releaseManifest = createManifest({ target: "release" });
|
const releaseManifest = createManifest({ target: "release" });
|
||||||
const latestVersion = process.env.LATEST_VERSION ?? releaseManifest.version;
|
const latestVersion =
|
||||||
|
process.env.LATEST_VERSION ?? resolveReleaseVersion(process.env, releaseManifest.version);
|
||||||
const publicBaseUrl =
|
const publicBaseUrl =
|
||||||
process.env.UPDATE_PUBLIC_BASE_URL ??
|
process.env.UPDATE_PUBLIC_BASE_URL ??
|
||||||
`https://wksgx-1343191620.cos.ap-nanjing.myqcloud.com/star-chart-search-enhancer/releases/${latestVersion}`;
|
`https://wksgx-1343191620.cos.ap-nanjing.myqcloud.com/star-chart-search-enhancer/releases/${latestVersion}`;
|
||||||
|
|||||||
@ -66,4 +66,20 @@ describe("manifest", () => {
|
|||||||
"32": "assets/icons/icon-32.png"
|
"32": "assets/icons/icon-32.png"
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("uses EXTENSION_VERSION for release builds", () => {
|
||||||
|
const previousVersion = process.env.EXTENSION_VERSION;
|
||||||
|
process.env.EXTENSION_VERSION = "v0.0525.1";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const releaseManifest = createManifest({ target: "release" });
|
||||||
|
expect(releaseManifest.version).toBe("0.0525.1");
|
||||||
|
} finally {
|
||||||
|
if (previousVersion === undefined) {
|
||||||
|
delete process.env.EXTENSION_VERSION;
|
||||||
|
} else {
|
||||||
|
process.env.EXTENSION_VERSION = previousVersion;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
30
tests/release-assets.test.ts
Normal file
30
tests/release-assets.test.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
import { describe, expect, test } from "vitest";
|
||||||
|
|
||||||
|
import { buildReleaseUploadTargets } from "../scripts/release-assets.mjs";
|
||||||
|
|
||||||
|
describe("release-assets", () => {
|
||||||
|
test("maps release files to the COS object keys", () => {
|
||||||
|
expect(
|
||||||
|
buildReleaseUploadTargets({
|
||||||
|
projectRoot: "/repo",
|
||||||
|
releaseVersion: "0.0525.1"
|
||||||
|
})
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
|
cosKey: "star-chart-search-enhancer/latest.json",
|
||||||
|
localPath: path.join("/repo", "release", "latest.json")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cosKey:
|
||||||
|
"star-chart-search-enhancer/releases/0.0525.1/star-chart-search-enhancer-internal.zip",
|
||||||
|
localPath: path.join("/repo", "release", "star-chart-search-enhancer-internal.zip")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cosKey:
|
||||||
|
"star-chart-search-enhancer/releases/0.0525.1/星图增强插件-超简单安装使用指南.pdf",
|
||||||
|
localPath: path.join("/repo", "release", "星图增强插件-超简单安装使用指南.pdf")
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
25
tests/release-version.test.ts
Normal file
25
tests/release-version.test.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { describe, expect, test } from "vitest";
|
||||||
|
|
||||||
|
import {
|
||||||
|
normalizeReleaseVersionTag,
|
||||||
|
resolveReleaseVersion
|
||||||
|
} from "../scripts/release-version.mjs";
|
||||||
|
|
||||||
|
describe("release-version", () => {
|
||||||
|
test("normalizes a tag by stripping a leading v", () => {
|
||||||
|
expect(normalizeReleaseVersionTag("v0.0525.1")).toBe("0.0525.1");
|
||||||
|
expect(normalizeReleaseVersionTag("0.0525.1")).toBe("0.0525.1");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("prefers EXTENSION_VERSION over DRONE_TAG and fallback", () => {
|
||||||
|
expect(
|
||||||
|
resolveReleaseVersion(
|
||||||
|
{
|
||||||
|
DRONE_TAG: "0.0525.2",
|
||||||
|
EXTENSION_VERSION: "v0.0525.3"
|
||||||
|
},
|
||||||
|
"0.2.0421.2"
|
||||||
|
)
|
||||||
|
).toBe("0.0525.3");
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user