diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..8b3e24d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,82 @@ +name: Build and Release + +on: + workflow_dispatch: + push: + tags: + - 'v*' + - 'V*' + release: + types: [published] + +jobs: + build: + name: Build Release Package + runs-on: ubuntu-latest + permissions: + contents: write + strategy: + matrix: + include: + - goos: linux + goarch: amd64 + archive_ext: tar.gz + - goos: linux + goarch: arm64 + archive_ext: tar.gz + - goos: windows + goarch: amd64 + archive_ext: zip + steps: + - name: Checkout Source + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 24 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.26.x' + cache-dependency-path: backend/go.sum + + - name: Determine Version + id: version_step + run: | + if [ -n "${{ github.ref_name }}" ] && [[ "${{ github.ref_name }}" =~ ^[vV][0-9] ]]; then + echo "VERSION=${{ github.ref_name }}" >> $GITHUB_ENV + else + PKG_VER=$(node -p "require('./frontend/package.json').version") + echo "VERSION=v${PKG_VER#v}" >> $GITHUB_ENV + fi + + - name: Build Frontend + run: | + cd frontend + npm ci + npm run build + + - name: Package Release + run: | + node deploy/package.mjs --target=${{ matrix.goos }}_${{ matrix.goarch }} --version=${{ env.VERSION }} + echo "PACKAGE_FILE=Scert_${{ env.VERSION }}_${{ matrix.goos }}_${{ matrix.goarch }}.${{ matrix.archive_ext }}" >> $GITHUB_ENV + + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: Scert_${{ env.VERSION }}_${{ matrix.goos }}_${{ matrix.goarch }} + path: | + out/${{ env.PACKAGE_FILE }} + out/${{ env.PACKAGE_FILE }}.sha256 + + - name: Upload to Release (Tag or Release event) + if: github.event_name == 'release' || startsWith(github.ref, 'refs/tags/') + uses: softprops/action-gh-release@v2 + with: + files: | + out/${{ env.PACKAGE_FILE }} + out/${{ env.PACKAGE_FILE }}.sha256 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/deploy/package.mjs b/deploy/package.mjs new file mode 100644 index 0000000..8ca8133 --- /dev/null +++ b/deploy/package.mjs @@ -0,0 +1,132 @@ +import fs from "node:fs"; +import path from "node:path"; +import crypto from "node:crypto"; +import { execFileSync } from "node:child_process"; + +const rootDir = process.cwd(); +const backendDir = path.join(rootDir, "backend"); +const deployDir = path.join(rootDir, "deploy"); +const frontendDist = path.join(rootDir, "frontend", "dist"); +const outDir = path.join(rootDir, "out"); + +const args = process.argv.slice(2); +let targetArg = args.find(a => a.startsWith("--target="))?.replace("--target=", ""); +let versionArg = args.find(a => a.startsWith("--version="))?.replace("--version=", ""); + +if (!versionArg) { + const pkg = JSON.parse(fs.readFileSync(path.join(rootDir, "frontend", "package.json"), "utf8")); + versionArg = pkg.version.startsWith("v") || pkg.version.startsWith("V") ? pkg.version : `v${pkg.version}`; +} else { + if (!versionArg.startsWith("v") && !versionArg.startsWith("V")) { + versionArg = `v${versionArg}`; + } +} + +const targets = targetArg ? [targetArg] : ["linux_amd64", "linux_arm64", "windows_amd64", "darwin_amd64", "darwin_arm64"]; + +fs.mkdirSync(outDir, { recursive: true }); + +for (const target of targets) { + const [goos, goarch] = target.split("_"); + if (!goos || !goarch) continue; + + const pkgName = `Scert_${versionArg}_${goos}_${goarch}`; + const stageDir = path.join(outDir, pkgName); + fs.rmSync(stageDir, { recursive: true, force: true }); + fs.mkdirSync(stageDir, { recursive: true }); + + const ext = goos === "windows" ? ".exe" : ""; + const binaries = [ + { name: `scert${ext}`, pkg: "./cmd/scert" }, + { name: `scert-admin${ext}`, pkg: "./cmd/scert-admin" } + ]; + + if (goos === "linux") { + binaries.push({ name: "scert-nginx", pkg: "./cmd/scert-nginx" }); + binaries.push({ name: "scert-storage", pkg: "./cmd/scert-storage" }); + } + + console.log(`Building Go binaries for ${goos}/${goarch}...`); + const ldflags = `-s -w -X 'git.pcux.cn/xuans/Scert/backend/internal/buildinfo.Version=${versionArg.replace(/^v/i, "")}'`; + + for (const bin of binaries) { + const outBin = path.join(stageDir, bin.name); + execFileSync("go", ["build", "-trimpath", "-ldflags", ldflags, "-o", outBin, bin.pkg], { + cwd: backendDir, + env: { + ...process.env, + GOOS: goos, + GOARCH: goarch, + CGO_ENABLED: "0" + }, + stdio: "inherit" + }); + } + + // Copy Web frontend dist + const webDir = path.join(stageDir, "web"); + fs.mkdirSync(webDir, { recursive: true }); + fs.cpSync(frontendDist, webDir, { recursive: true }); + + // Copy Deploy configurations + if (fs.existsSync(path.join(deployDir, "README.md"))) { + fs.copyFileSync(path.join(deployDir, "README.md"), path.join(stageDir, "部署说明.md")); + } + + if (goos === "linux") { + if (fs.existsSync(path.join(deployDir, "install-linux.sh"))) { + fs.copyFileSync(path.join(deployDir, "install-linux.sh"), path.join(stageDir, "install-linux.sh")); + } + if (fs.existsSync(path.join(deployDir, "systemd"))) { + fs.cpSync(path.join(deployDir, "systemd"), path.join(stageDir, "systemd"), { recursive: true }); + } + if (fs.existsSync(path.join(deployDir, "nginx"))) { + fs.cpSync(path.join(deployDir, "nginx"), path.join(stageDir, "nginx"), { recursive: true }); + } + } else if (goos === "windows") { + if (fs.existsSync(path.join(deployDir, "install-windows.ps1"))) { + fs.copyFileSync(path.join(deployDir, "install-windows.ps1"), path.join(stageDir, "install-windows.ps1")); + } + } + + // Generate SHA256SUMS + const allFiles = []; + function collect(dir, base) { + for (const item of fs.readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, item.name); + const rel = path.join(base, item.name).replace(/\\/g, "/"); + if (item.isDirectory()) { + collect(full, rel); + } else if (item.isFile() && item.name !== "SHA256SUMS") { + const hash = crypto.createHash("sha256").update(fs.readFileSync(full)).digest("hex"); + allFiles.push(`${hash} ${rel}`); + } + } + } + collect(stageDir, ""); + allFiles.sort(); + fs.writeFileSync(path.join(stageDir, "SHA256SUMS"), allFiles.join("\n") + "\n", "utf8"); + + // Archive + const archiveExt = goos === "windows" ? "zip" : "tar.gz"; + const archiveName = `${pkgName}.${archiveExt}`; + const archivePath = path.join(outDir, archiveName); + fs.rmSync(archivePath, { force: true }); + + console.log(`Packaging ${archiveName}...`); + if (archiveExt === "tar.gz") { + execFileSync("tar", ["-czf", archivePath, "-C", outDir, pkgName], { stdio: "inherit" }); + } else { + // PowerShell Compress-Archive or tar + try { + execFileSync("tar", ["-a", "-c", "-f", archivePath, "-C", outDir, pkgName], { stdio: "inherit" }); + } catch { + execFileSync("powershell", ["-Command", `Compress-Archive -Path '${stageDir}' -DestinationPath '${archivePath}' -Force`], { stdio: "inherit" }); + } + } + + // Compute archive sha256 + const archiveHash = crypto.createHash("sha256").update(fs.readFileSync(archivePath)).digest("hex"); + fs.writeFileSync(`${archivePath}.sha256`, `${archiveHash} ${archiveName}\n`, "utf8"); + console.log(`Finished ${archiveName} (${archiveHash})`); +} \ No newline at end of file diff --git a/doc/Scert整改任务执行清单.md b/doc/Scert整改任务执行清单.md index d57eee5..e465fee 100644 --- a/doc/Scert整改任务执行清单.md +++ b/doc/Scert整改任务执行清单.md @@ -3674,8 +3674,8 @@ P0-05 从 `[!]` 调整为 `[~]`,并未标记完成;P0-04 剩余门禁和全 ## 2026年10月18日 双仓库发布工作流与按规范编译包生成 -- [~] 需求确认:为 GitHub 与 git.pcux.cn 双仓库创建统一的 CI/CD Release 工作流,实现多架构自动编译打包与发布,产物严格按照“项目名+版本+架构”(如 Scert_v1.52.1_linux_amd64.tar.gz / Scert_v1.52.1_windows_amd64.zip)进行命名。 -- [ ] 编写跨平台打包辅助能力,支持包含可执行程序、Web资源、服务部署模板配置及哈希清单。 -- [ ] 创建 .github/workflows/release.yml 自动化工作流,覆盖 linux/amd64、linux/arm64、windows/amd64 等平台矩阵,支持 tag 触发与 workflow_dispatch 手动触发。 -- [ ] 执行前端单元测试(357项)与构建检查,确保工具链与代码健全。 -- [ ] 提交代码并双重推送到 Gitea (git.pcux.cn) 与 GitHub (github.com)。 \ No newline at end of file +- [✓] 需求确认:为 GitHub 与 git.pcux.cn 双仓库创建统一的 CI/CD Release 工作流,实现多架构自动编译打包与发布,产物严格按照“项目名+版本+架构”(如 Scert_v1.52.1_linux_amd64.tar.gz / Scert_v1.52.1_windows_amd64.zip)进行命名。 +- [✓] 编写跨平台打包辅助脚本(deploy/package.mjs),支持针对特定平台及架构进行编译,并整合前端构建产物(web/)、部署脚本与配置文件,自动计算 SHA256SUMS,生成对应的 tar.gz / zip 压缩包及校验文件。 +- [✓] 创建 .github/workflows/release.yml 自动化工作流,构建矩阵覆盖 linux/amd64、linux/arm64、windows/amd64 等平台,支持推送 tag、发布 release 以及 workflow_dispatch 手动触发,并自动上传 Release 资产与 Artifacts。 +- [✓] 执行前端单元测试(357 项测试全绿通过,357 pass, 0 fail)与生产打包构建检查。 +- [✓] 提交代码并双重推送到 Gitea (git.pcux.cn) 与 GitHub (github.com)。 \ No newline at end of file