diff --git a/.gitea/workflows/publish.yml b/.gitea/workflows/publish.yml index ba2ec8f..2ac9ac8 100644 --- a/.gitea/workflows/publish.yml +++ b/.gitea/workflows/publish.yml @@ -1,10 +1,16 @@ -name: Build and publish Docker images +name: Build and publish packages and Docker images on: push: tags: - "v*" workflow_dispatch: + inputs: + include_extended_platforms: + description: "同时构建 Windows ARM64 和 macOS x64/ARM64" + required: false + default: false + type: boolean jobs: publish: @@ -23,9 +29,56 @@ jobs: with: dotnet-version: "10.0.x" + - name: Set up Node.js + uses: https://github.com/actions/setup-node@v4 + with: + node-version: "24" + cache: npm + cache-dependency-path: src/Eis.Web/ClientApp/package-lock.json + + - name: Build Vue application + working-directory: src/Eis.Web/ClientApp + run: | + npm ci + npm run build + - name: Run tests run: dotnet test Eis.slnx --configuration Release + - name: Determine package version + id: package_version + shell: bash + env: + INCLUDE_EXTENDED_PLATFORMS: ${{ inputs.include_extended_platforms }} + run: | + set -euo pipefail + if [[ "$GITHUB_REF" == refs/tags/v* ]]; then + version="${GITHUB_REF_NAME#v}" + else + version="0.0.0-dev.${GITHUB_RUN_NUMBER:-0}" + fi + profile=default + if [[ "${INCLUDE_EXTENDED_PLATFORMS:-false}" == "true" ]]; then + profile=all + fi + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "profile=$profile" >> "$GITHUB_OUTPUT" + + - name: Build self-contained platform packages + shell: bash + run: | + bash ./scripts/publish-platform-packages.sh \ + "${{ steps.package_version.outputs.version }}" \ + "artifacts/packages" \ + "${{ steps.package_version.outputs.profile }}" + + - name: Upload platform packages as workflow artifact + uses: https://github.com/actions/upload-artifact@v4 + with: + name: eis-${{ steps.package_version.outputs.version }}-${{ steps.package_version.outputs.profile }}-platform-packages + path: artifacts/packages/* + if-no-files-found: error + - name: Set up QEMU uses: https://github.com/docker/setup-qemu-action@v3 @@ -53,9 +106,8 @@ jobs: images: | docker.io/${{ vars.DOCKERHUB_IMAGE }} git.biss.click/biss/exam-information-system - flavor: latest=false + flavor: latest=auto tags: | - type=raw,value=latest,enable=${{ gitea.ref == 'refs/heads/master' }} type=sha,format=short,prefix=sha- type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} @@ -91,8 +143,18 @@ jobs: tag_name: ${{ gitea.ref_name }} name: ${{ gitea.ref_name }} prerelease: ${{ steps.release_type.outputs.prerelease }} + files: | + artifacts/packages/* body: | - Docker 镜像已发布: + 已发布以下自包含程序包,无需预装 .NET 运行时: + + - Windows x64:下载对应的 `.zip` + - Linux x64 / ARM64:下载对应的 `.tar.gz` + - 使用 `SHA256SUMS` 校验下载文件 + + Windows ARM64 与 macOS Intel / Apple Silicon 包可在 Actions 页面选择扩展平台后手动构建。 + + Docker 多架构镜像(linux/amd64、linux/arm64): ```text docker pull docker.io/${{ vars.DOCKERHUB_IMAGE }}:${{ steps.metadata.outputs.version }} diff --git a/README.md b/README.md index 9a9e456..b8d8865 100644 --- a/README.md +++ b/README.md @@ -173,9 +173,20 @@ docker compose run --rm --entrypoint dotnet app /app/tools/Eis.Tools.dll databas docker compose up --detach app ``` -#### Gitea Actions 自动发布到 Docker Hub 与 Gitea 软件包 +#### Gitea Actions 自动发布跨平台程序包与 Docker 镜像 -工作流位于 `.gitea/workflows/docker-publish.yml`。它会使用 .NET 10 运行测试,然后构建 `linux/amd64`、`linux/arm64` 双架构镜像,并同时推送到 Docker Hub 与 `git.biss.click/biss/exam-information-system`。 +工作流位于 `.gitea/workflows/publish.yml`。它会构建 Vue 前端、使用 .NET 10 运行测试,然后默认生成以下无需预装 .NET 运行时的自包含程序包: + +- Windows:`win-x64`(`.zip`) +- Linux:`linux-x64`、`linux-arm64`(`.tar.gz`) + +每个程序包都把 Web 与数据库工具放在同一个 `app` 目录中,使二者共享 .NET 运行时和公共依赖;根目录还包含 `README.md`、`LICENSE` 和环境变量示例,构建结果同时附带 `SHA256SUMS`。标签构建会把这些文件上传到同名 Gitea Release;手动构建可在 Actions 运行记录中下载归档。普通代码推送不会触发这套耗时的发布构建。 + +解压后,Windows 使用 `.\app\Eis.Web.exe` 启动服务,Linux/macOS 使用 `./app/Eis.Web`;数据库工具分别为 `.\app\Eis.Tools.exe` 和 `./app/Eis.Tools`。 + +如需 Windows ARM64、macOS Intel 或 Apple Silicon 包,从 Gitea Actions 页面手动运行工作流并勾选“同时构建 Windows ARM64 和 macOS x64/ARM64”。扩展构建会在默认三种平台之外增加 `win-arm64`、`osx-x64` 和 `osx-arm64`。 + +同一工作流还会构建 `linux/amd64`、`linux/arm64` 双架构 Docker 镜像,并同时推送到 Docker Hub 与 `git.biss.click/biss/exam-information-system`。 使用前需要完成以下配置: @@ -185,7 +196,7 @@ docker compose up --detach app 4. 使用对 `biss` 组织拥有软件包写权限的 Gitea 账号,在“设置 → 应用 → 生成新令牌”中创建具有 Package Read & Write 权限的个人访问令牌。在仓库 Actions Variables 中添加 `REGISTRY_USERNAME`,值为该令牌所属的用户名;在 Actions Secrets 中添加 `REGISTRY_TOKEN`,值为个人访问令牌。自定义 Secret 不能使用 Gitea 保留的 `GITEA_` 前缀,也不能用工作流内置的 `GITEA_TOKEN` 代替该软件包令牌。 5. 确保 Gitea Actions 与仓库的软件包注册表已启用,并且 `ubuntu-latest` Runner 能访问 Docker daemon、GitHub、Docker Hub 和 `git.biss.click`。双架构构建还需要 Runner 允许 QEMU 注册步骤运行。 -推送到 `master` 后会发布 `latest` 和 `sha-<短提交号>`;推送形如 `v1.2.3` 的 Git 标签后会发布 `1.2.3`、`1.2` 和对应的提交标签,并在镜像推送成功后自动创建同名正式 Gitea Release。例如: +推送形如 `v1.2.3` 的 Git 标签后,会自动构建跨平台程序包,发布镜像标签 `1.2.3`、`1.2`、`latest` 和对应的提交标签,并在构建成功后创建带下载附件的同名正式 Gitea Release。例如: ```powershell git tag v1.2.3 @@ -199,9 +210,9 @@ git tag v1.3.0-rc.1 git push origin v1.3.0-rc.1 ``` -该版本的镜像标签为 `1.3.0-rc.1`,不会覆盖稳定版的 `1.3` 或 `latest` 标签。 +该版本的镜像标签为 `1.3.0-rc.1`,不会覆盖稳定版的 `1.3` 或 `latest` 标签。除版本标签外,也可以从 Gitea Actions 页面手动运行工作流;普通分支推送不会触发发布。 -工作流也支持从 Gitea Actions 页面手动运行。构建缓存保存为同一 Docker Hub 仓库中的 `buildcache` 标签,以加快后续构建。Release 使用工作流内置的 `GITEA_TOKEN` 创建,无需添加额外 Secret;仓库或组织“Actions → General”中的任务令牌最大权限必须允许 Releases Write。 +构建缓存保存为同一 Docker Hub 仓库中的 `buildcache` 标签,以加快后续构建。Release 使用工作流内置的 `GITEA_TOKEN` 创建,无需添加额外 Secret;仓库或组织“Actions → General”中的任务令牌最大权限必须允许 Releases Write。 首次成功推送后,容器镜像会出现在 `biss` 所有者的软件包列表。Gitea 的软件包归属于用户或组织,不会天然归属于某个仓库;打开该软件包的设置页面,将它关联到 `Exam-Information-System`,即可让它显示在此仓库的“软件包”页。之后可使用 `docker pull git.biss.click/biss/exam-information-system:latest` 拉取。 @@ -393,6 +404,7 @@ src/Eis.Web/wwwroot 浏览器入口、基础样式及 CKEditor tests/Eis.Infrastructure.Tests xUnit 自动化测试 scripts/publish.ps1 Web 与数据库工具统一发布脚本 +scripts/publish-platform-packages.sh CI 跨平台自包含程序包构建脚本 scripts/smoke-dotnet-native.ps1 纯 .NET 发布产物烟测 data/exam.sqlite 本地运行后生成的 SQLite 数据库 .env.example 开发与生产环境变量模板 diff --git a/scripts/publish-platform-packages.sh b/scripts/publish-platform-packages.sh new file mode 100644 index 0000000..1fbd678 --- /dev/null +++ b/scripts/publish-platform-packages.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash + +set -euo pipefail + +version="${1:?Usage: publish-platform-packages.sh [output-directory] [default|all]}" +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repository_root="$(cd "${script_dir}/.." && pwd)" +output_directory="${2:-${repository_root}/artifacts/packages}" +platform_profile="${3:-default}" +web_project="${repository_root}/src/Eis.Web/Eis.Web.csproj" +tools_project="${repository_root}/src/Eis.Tools/Eis.Tools.csproj" + +case "${platform_profile}" in + default) + runtime_identifiers=(win-x64 linux-x64 linux-arm64) + ;; + all) + runtime_identifiers=(win-x64 win-arm64 linux-x64 linux-arm64 osx-x64 osx-arm64) + ;; + *) + echo "Unknown platform profile: ${platform_profile}. Expected default or all." >&2 + exit 2 + ;; +esac + +if [[ "${output_directory}" != /* ]]; then + output_directory="${repository_root}/${output_directory}" +fi +mkdir -p "${output_directory}" +output_directory="$(cd "${output_directory}" && pwd)" +temporary_root="$(mktemp -d)" +trap 'rm -rf "${temporary_root}"' EXIT + +for runtime_identifier in "${runtime_identifiers[@]}"; do + package_name="eis-${version}-${runtime_identifier}" + package_root="${temporary_root}/${package_name}" + application_root="${package_root}/app" + + echo "Publishing ${package_name}..." + dotnet publish "${web_project}" \ + --configuration Release \ + --runtime "${runtime_identifier}" \ + --self-contained true \ + --output "${application_root}" \ + -p:Version="${version}" + + dotnet publish "${tools_project}" \ + --configuration Release \ + --runtime "${runtime_identifier}" \ + --self-contained true \ + --output "${application_root}" \ + -p:Version="${version}" + + cp "${repository_root}/README.md" "${package_root}/README.md" + cp "${repository_root}/LICENSE" "${package_root}/LICENSE" + cp "${repository_root}/.env.example" "${package_root}/.env.example" + + if [[ "${runtime_identifier}" == win-* ]]; then + ( + cd "${temporary_root}" + zip -q -r "${output_directory}/${package_name}.zip" "${package_name}" + ) + else + tar -C "${temporary_root}" -czf "${output_directory}/${package_name}.tar.gz" "${package_name}" + fi + + rm -rf "${package_root}" +done + +( + cd "${output_directory}" + find . -maxdepth 1 -type f \ + \( -name "eis-${version}-*.zip" -o -name "eis-${version}-*.tar.gz" \) \ + -printf '%f\n' \ + | sort \ + | xargs sha256sum > SHA256SUMS + sha256sum --check SHA256SUMS +) + +echo "Platform packages were written to ${output_directory}"