feat: automate tag release pipeline
Some checks failed
continuous-integration/drone/tag Build is failing

This commit is contained in:
admin123 2026-05-25 11:26:02 +08:00
parent 57e4dc72aa
commit d302614b99
16 changed files with 1220 additions and 3 deletions

34
.drone.yml Normal file
View 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

View File

@ -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`
--- ---

View File

@ -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*",

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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",

View 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();
}

View 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)), "../..");
}

View File

@ -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
}; };
} }

View 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")
}
];
}

View 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");
}

View File

@ -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}`;

View File

@ -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;
}
}
});
}); });

View 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")
}
]);
});
});

View 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");
});
});