mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-10-12 06:26:15 -08:00
ci: use lightweight PR checks and conditional nightly builds (#2677)
* ci: use lightweight PR checks and conditional nightly builds Check three Core platforms, Linux GUI, and Android ARM64 on PRs and ordinary branch pushes. Keep formatting, clippy, WASI, and all Linux tests; reserve the per-feature sweep for full builds. Run complete platform matrices on releases/** pushes and manual dispatch. At 03:17 Asia/Shanghai, check existing main and develop branches and dispatch only workflows whose last successful full build used a different commit, retrying failed components on the next night. Cache compilation outputs by target and build mode. Build only the GUI frontend dependencies and reuse the dashboard artifact for Core builds. Separate full-build concurrency so ordinary pushes cannot cancel them. Keep the required result checks and propagate failures and cancellations. Validate successful full-build origins and matching Core, GUI, and Mobile commits before preparing release assets. Validation: actionlint 1.7.12, 10 Node tests, read-only GitHub nightly dry run, GUI frontend build, Linux GUI check, Android ARM64 check, and Linux musl Core/Web checks including embedded dashboard assets. * docs: describe CI behavior in English and Chinese Document PR checks, nightly dispatch, manual builds, release validation, artifacts, and independent workflows. Keep both versions aligned with current workflow definitions. * ci: decouple PR checks from Web dashboard builds Run PR dashboard builds only when frontend sources, Core configuration sources, protobuf schemas, or build configuration change. Keep embedded Web builds in full mode and let ordinary Rust checks start without frontend artifacts. Upload dashboard artifacts only for full builds. Skip frontend setup for ordinary lint runs and remove GUI/frontend setup from test archives. Update both CI documents to match the new behavior. Validation: actionlint; frontend dependency and workflow routing cases; Linux Core cargo check and workspace clippy with frontend outputs absent.
This commit is contained in:
1 parent
7592951b80
commit
f8a45ccc20
11 files changed
+519
-56
No files matched your search
@@ -0,0 +1,56 @@
|
||||
# CI
|
||||
|
||||
[中文](CI.zh-CN.md)
|
||||
|
||||
| Trigger | Core | GUI | Mobile |
|
||||
| --- | --- | --- | --- |
|
||||
| PR targeting `main`/`develop`, or push to either branch | Core/Web `cargo check`: Linux x64, Windows x64, macOS ARM64 | Linux frontend build and Rust check | Android ARM64 frontend build and Rust check |
|
||||
| Push to `releases/**`, or manual dispatch | 16 targets and Magisk artifacts | Installers for 7 targets | APKs for 4 architectures |
|
||||
|
||||
Test runs CI script tests, fmt, clippy, WASI, Cargo.lock validation, and
|
||||
Linux tests for `easytier`/`easytier-core`, including `three_node`. Full mode
|
||||
also checks each `easytier` feature except `macos-ne`.
|
||||
|
||||
These four workflows skip build/test jobs for draft PRs. Path and duplicate
|
||||
content checks can skip ordinary builds and Rust jobs. The result jobs are
|
||||
`core-result`, `gui-result`, `mobile-result`, and `test`.
|
||||
|
||||
PR dashboard builds run only for frontend, Core configuration, schema, or
|
||||
build configuration changes. Ordinary branch pushes build the dashboard.
|
||||
Core checks run independently; Web embedding is compiled only in full mode.
|
||||
Test skips the frontend build in ordinary mode; its test archive builds
|
||||
only Rust packages.
|
||||
|
||||
PR runs can upload the test archive for reuse within CI. The dashboard is
|
||||
uploaded only in full mode.
|
||||
Core binaries, GUI installers, APKs, and Magisk artifacts require full mode.
|
||||
Compilation outputs are cached. Ordinary pushes do not cancel full builds.
|
||||
|
||||
## Nightly
|
||||
|
||||
Scheduled at 03:17 Asia/Shanghai. Check Core, GUI, Mobile, and Test separately
|
||||
on each existing `main`/`develop` branch. Compare HEAD with that workflow's
|
||||
latest successful `workflow_dispatch` run: skip matching commits; dispatch
|
||||
otherwise. Commits without a successful run are retried on the next check.
|
||||
|
||||
Nightly only dispatches jobs; results appear in the corresponding workflows.
|
||||
Core's `sign_windows` is set to `false` for nightly builds.
|
||||
|
||||
## Manual builds and releases
|
||||
|
||||
Repository Write access is required. In Actions, run Core, GUI, and Mobile
|
||||
separately on the PR branch to obtain full artifacts. The branch must contain
|
||||
these workflow definitions. Fork PRs need a temporary branch in this repository.
|
||||
|
||||
Manual Core builds default to SignPath test signing (`sign_windows: true`).
|
||||
Full macOS GUI builds are signed and notarized.
|
||||
|
||||
Release is a separate manual workflow. Its Core/GUI/Mobile run IDs must refer
|
||||
to successful full builds from their respective workflows at the same commit.
|
||||
It downloads their artifacts and creates a draft release.
|
||||
|
||||
## Other workflows
|
||||
|
||||
- OHOS runs independently; relevant PRs can still upload HAR artifacts.
|
||||
- Go, JavaScript Hosts, and Nix use their own path filters.
|
||||
- Docker is manual; it downloads a specified Core run's artifacts and pushes images.
|
||||
@@ -0,0 +1,54 @@
|
||||
# CI
|
||||
|
||||
[English](CI.md)
|
||||
|
||||
| 触发方式 | Core | GUI | Mobile |
|
||||
| --- | --- | --- | --- |
|
||||
| 目标为 `main`/`develop` 的 PR,或 push 到这两个分支 | Linux x64、Windows x64、macOS ARM64 的 Core/Web `cargo check` | Linux 前端构建和 Rust 检查 | Android ARM64 前端构建和 Rust 检查 |
|
||||
| Push 到 `releases/**`,或手动触发 | 16 个目标和 Magisk 产物 | 7 个目标的安装包 | 4 个架构的 APK |
|
||||
|
||||
Test 运行 CI 脚本测试、fmt、clippy、WASI、Cargo.lock 检查,以及
|
||||
`easytier`/`easytier-core` 的 Linux 测试(含 `three_node`)。全量模式还逐个
|
||||
检查 `easytier` 的 feature,排除 `macos-ne`。
|
||||
|
||||
这四个 workflow 的构建和测试任务跳过 draft PR。普通构建和 Rust 任务可因
|
||||
无关路径或重复内容而跳过。汇总检查为 `core-result`、`gui-result`、
|
||||
`mobile-result` 和 `test`。
|
||||
|
||||
PR 仅在前端、Core 配置源码、schema 或构建配置变化时构建 Web 前端。
|
||||
普通分支 push 仍构建 Web 前端。Core 检查独立运行;Web 的嵌入模式仅在
|
||||
全量构建中编译。
|
||||
Test 在普通模式下跳过前端构建,测试归档仅构建 Rust 包。
|
||||
|
||||
PR 可上传供 CI 内部复用的测试归档。Web 前端仅在全量模式上传。
|
||||
Core 二进制、GUI 安装包、APK 和 Magisk 产物仅在全量模式上传。编译产物
|
||||
启用缓存。普通 push 不会取消全量构建。
|
||||
|
||||
## 每日构建
|
||||
|
||||
定时为北京时间 03:17。对实际存在的 `main`/`develop` 分支,分别检查 Core、
|
||||
GUI、Mobile、Test:比较分支 HEAD 与该 workflow 最近一次成功的
|
||||
`workflow_dispatch` 提交,相同则跳过,不同或无成功记录则触发该 workflow。
|
||||
尚未成功构建的提交在下一轮继续触发。
|
||||
|
||||
Nightly 只负责触发,结果见对应 workflow。每日 Core 构建设置
|
||||
`sign_windows: false`。
|
||||
|
||||
## 手动构建与发布
|
||||
|
||||
需要仓库 Write 权限。在 Actions 分别运行 Core、GUI、Mobile,选择 PR 分支
|
||||
即可生成全量产物。该分支必须包含这些 workflow 定义。Fork PR 需要先在本
|
||||
仓库建立临时分支。
|
||||
|
||||
手动 Core 构建默认开启 SignPath 测试签名(`sign_windows: true`)。
|
||||
全量 macOS GUI 构建会签名和公证。
|
||||
|
||||
Release 是独立的手动 workflow。输入的 Core/GUI/Mobile run ID 必须分别来自
|
||||
正确的 workflow、全量构建成功,且提交相同。它下载这些产物并创建 draft
|
||||
release。
|
||||
|
||||
## 其他 workflow
|
||||
|
||||
- OHOS 独立运行,相关 PR 仍可上传 HAR。
|
||||
- Go、JavaScript Hosts、Nix 使用各自的路径过滤。
|
||||
- Docker 手动触发,下载指定 Core 构建的产物并推送镜像。
|
||||
@@ -0,0 +1,133 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const { test } = require('node:test');
|
||||
const dispatch = require('./nightly-build.js');
|
||||
const validate = require('./validate-release-builds.js');
|
||||
|
||||
const context = { repo: { owner: 'EasyTier', repo: 'EasyTier' } };
|
||||
const core = { info() {} };
|
||||
const workflows = ['core.yml', 'gui.yml', 'mobile.yml', 'test.yml'];
|
||||
|
||||
async function nightly(branches, successfulRuns = {}) {
|
||||
const dispatched = [];
|
||||
const github = {
|
||||
paginate: async () => branches,
|
||||
rest: {
|
||||
repos: { listBranches() {} },
|
||||
actions: {
|
||||
async listWorkflowRuns(request) {
|
||||
assert.equal(request.event, 'workflow_dispatch');
|
||||
assert.equal(request.status, 'success');
|
||||
const sha = successfulRuns[`${request.branch}/${request.workflow_id}`];
|
||||
return { data: { workflow_runs: sha ? [{ head_sha: sha }] : [] } };
|
||||
},
|
||||
async createWorkflowDispatch(request) {
|
||||
dispatched.push(request);
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
await dispatch({ github, context, core });
|
||||
return dispatched;
|
||||
}
|
||||
|
||||
test('first nightly builds every workflow on an existing main branch', async () => {
|
||||
const runs = await nightly([{ name: 'main', commit: { sha: 'new' } }]);
|
||||
assert.deepEqual(runs.map(run => run.workflow_id), workflows);
|
||||
assert.ok(runs.every(run => run.ref === 'main'));
|
||||
assert.deepEqual(runs[0].inputs, { sign_windows: false });
|
||||
assert.ok(runs.slice(1).every(run => !run.inputs));
|
||||
});
|
||||
|
||||
test('unchanged branches skip builds and unmonitored branches are ignored', async () => {
|
||||
const successfulRuns = Object.fromEntries(workflows.map(workflow => [`main/${workflow}`, 'same']));
|
||||
const runs = await nightly([
|
||||
{ name: 'main', commit: { sha: 'same' } },
|
||||
{ name: 'feature', commit: { sha: 'new' } },
|
||||
], successfulRuns);
|
||||
assert.deepEqual(runs, []);
|
||||
});
|
||||
|
||||
test('nightly retries a failed component using its last successful commit', async () => {
|
||||
const successfulRuns = Object.fromEntries(workflows.map(workflow => [`main/${workflow}`, 'new']));
|
||||
successfulRuns['main/gui.yml'] = 'old';
|
||||
const runs = await nightly([{ name: 'main', commit: { sha: 'new' } }], successfulRuns);
|
||||
assert.deepEqual(runs.map(run => run.workflow_id), ['gui.yml']);
|
||||
});
|
||||
|
||||
test('main and develop are checked independently', async () => {
|
||||
const successfulRuns = Object.fromEntries(workflows.map(workflow => [`main/${workflow}`, 'same']));
|
||||
const runs = await nightly([
|
||||
{ name: 'main', commit: { sha: 'same' } },
|
||||
{ name: 'develop', commit: { sha: 'new' } },
|
||||
], successfulRuns);
|
||||
assert.deepEqual(runs.map(run => run.workflow_id), workflows);
|
||||
assert.ok(runs.every(run => run.ref === 'develop'));
|
||||
});
|
||||
|
||||
process.env.CORE_RUN_ID = '1';
|
||||
process.env.GUI_RUN_ID = '2';
|
||||
process.env.MOBILE_RUN_ID = '3';
|
||||
|
||||
function releaseRuns() {
|
||||
return ['core.yml', 'gui.yml', 'mobile.yml'].map(workflow => ({
|
||||
path: `.github/workflows/${workflow}`,
|
||||
conclusion: 'success',
|
||||
event: 'workflow_dispatch',
|
||||
head_branch: 'main',
|
||||
head_sha: 'same',
|
||||
}));
|
||||
}
|
||||
|
||||
async function release(runs) {
|
||||
const github = {
|
||||
rest: {
|
||||
actions: {
|
||||
async getWorkflowRun({ run_id }) {
|
||||
return { data: runs[run_id - 1] };
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
return validate({ github, context, core });
|
||||
}
|
||||
|
||||
test('release accepts successful full builds from one commit', async () => {
|
||||
await release(releaseRuns());
|
||||
});
|
||||
|
||||
test('release accepts full builds triggered by a release branch push', async () => {
|
||||
const runs = releaseRuns();
|
||||
runs[0].event = 'push';
|
||||
runs[0].head_branch = 'releases/2.7';
|
||||
runs[1].event = 'push';
|
||||
runs[1].head_branch = 'releases/2.7';
|
||||
await release(runs);
|
||||
});
|
||||
|
||||
test('release rejects PR builds and regular branch pushes', async () => {
|
||||
for (const event of ['pull_request', 'push']) {
|
||||
const runs = releaseRuns();
|
||||
runs[0].event = event;
|
||||
await assert.rejects(release(runs), /successful full build/);
|
||||
}
|
||||
});
|
||||
|
||||
test('release rejects failed and cancelled builds', async () => {
|
||||
for (const conclusion of ['failure', 'cancelled']) {
|
||||
const runs = releaseRuns();
|
||||
runs[1].conclusion = conclusion;
|
||||
await assert.rejects(release(runs), /successful full build/);
|
||||
}
|
||||
});
|
||||
|
||||
test('release rejects run IDs from the wrong workflow', async () => {
|
||||
const runs = releaseRuns();
|
||||
runs[1].path = runs[0].path;
|
||||
await assert.rejects(release(runs), /successful full build/);
|
||||
});
|
||||
|
||||
test('release rejects builds from different commits', async () => {
|
||||
const runs = releaseRuns();
|
||||
runs[2].head_sha = 'different';
|
||||
await assert.rejects(release(runs), /same commit/);
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
module.exports = async ({ github, context, core }) => {
|
||||
const repository = context.repo;
|
||||
const branches = await github.paginate(github.rest.repos.listBranches, {
|
||||
...repository,
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
for (const branch of branches.filter(({ name }) => ['main', 'develop'].includes(name))) {
|
||||
for (const workflow of ['core.yml', 'gui.yml', 'mobile.yml', 'test.yml']) {
|
||||
const { data } = await github.rest.actions.listWorkflowRuns({
|
||||
...repository,
|
||||
workflow_id: workflow,
|
||||
branch: branch.name,
|
||||
event: 'workflow_dispatch',
|
||||
status: 'success',
|
||||
per_page: 1,
|
||||
});
|
||||
if (data.workflow_runs[0]?.head_sha === branch.commit.sha) {
|
||||
core.info(`${workflow}: ${branch.name} is already built successfully.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
await github.rest.actions.createWorkflowDispatch({
|
||||
...repository,
|
||||
workflow_id: workflow,
|
||||
ref: branch.name,
|
||||
...(workflow === 'core.yml' ? { inputs: { sign_windows: false } } : {}),
|
||||
});
|
||||
core.info(`${workflow}: dispatched a full build for ${branch.name}.`);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
module.exports = async ({ github, context, core }) => {
|
||||
const builds = [
|
||||
['core.yml', process.env.CORE_RUN_ID],
|
||||
['gui.yml', process.env.GUI_RUN_ID],
|
||||
['mobile.yml', process.env.MOBILE_RUN_ID],
|
||||
];
|
||||
let commit;
|
||||
|
||||
for (const [workflow, runId] of builds) {
|
||||
const { data: run } = await github.rest.actions.getWorkflowRun({
|
||||
...context.repo,
|
||||
run_id: Number(runId),
|
||||
});
|
||||
const fullBuild = run.event === 'workflow_dispatch' ||
|
||||
(run.event === 'push' && run.head_branch.startsWith('releases/'));
|
||||
if (run.path !== `.github/workflows/${workflow}` || run.conclusion !== 'success' || !fullBuild) {
|
||||
throw new Error(`${workflow}: run ${runId} must be a successful full build.`);
|
||||
}
|
||||
if (commit && run.head_sha !== commit) {
|
||||
throw new Error('Core, GUI and Mobile must be built from the same commit.');
|
||||
}
|
||||
commit = run.head_sha;
|
||||
core.info(`${workflow}: validated full build ${runId} at ${commit}.`);
|
||||
}
|
||||
};
|
||||
+78
-16
@@ -2,6 +2,11 @@ name: EasyTier Core
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
sign_windows:
|
||||
description: Submit Windows artifacts for SignPath test signing
|
||||
type: boolean
|
||||
default: true
|
||||
push:
|
||||
branches: [ "develop", "main", "releases/**" ]
|
||||
pull_request:
|
||||
@@ -9,7 +14,8 @@ on:
|
||||
types: [ opened, synchronize, reopened, ready_for_review ]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
# A new PR or branch push must not cancel a full build.
|
||||
group: ${{ github.workflow }}-${{ (github.event_name == 'workflow_dispatch' || startsWith(github.ref_name, 'releases/')) && 'full' || 'check' }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
@@ -31,7 +37,17 @@ jobs:
|
||||
"easytier-core/**", "easytier-proto/**", "easytier-rpc-build/**",
|
||||
"easytier-web/**", "easytier-contrib/easytier-magisk/**",
|
||||
".github/workflows/core.yml", ".github/actions/**"]
|
||||
FRONTEND_PATHS: >-
|
||||
["easytier-web/frontend/**", "easytier-web/frontend-lib/**",
|
||||
"easytier-web/config-generator/**", "easytier-proto/proto/**",
|
||||
"easytier-core/src/config/**", "easytier-core/src/lib.rs",
|
||||
"pnpm-lock.yaml", "pnpm-workspace.yaml", "package.json",
|
||||
"Cargo.toml", "Cargo.lock", "rust-toolchain.toml", ".cargo/**",
|
||||
"easytier-core/Cargo.toml", "easytier-proto/Cargo.toml",
|
||||
".github/workflows/core.yml", ".github/actions/prepare-pnpm/**"]
|
||||
outputs:
|
||||
full_build: ${{ github.event_name == 'workflow_dispatch' || startsWith(github.ref_name, 'releases/') }}
|
||||
frontend_changed: ${{ steps.path_filter.outputs.frontend }}
|
||||
# Release builds must run even if they only change release metadata.
|
||||
should_skip: >-
|
||||
${{
|
||||
@@ -42,15 +58,16 @@ jobs:
|
||||
steps.path_filter.outputs.relevant != 'true'
|
||||
)
|
||||
) &&
|
||||
github.event_name != 'workflow_dispatch' &&
|
||||
!startsWith(github.ref_name, 'releases/')
|
||||
}}
|
||||
steps:
|
||||
- id: skip_check
|
||||
if: github.event_name != 'workflow_dispatch' && !startsWith(github.ref_name, 'releases/')
|
||||
uses: fkirc/skip-duplicate-actions@v5
|
||||
with:
|
||||
concurrent_skipping: 'same_content_newer'
|
||||
skip_after_successful_duplicate: 'true'
|
||||
cancel_others: 'true'
|
||||
paths: ${{ env.RELEVANT_PATHS }}
|
||||
|
||||
- id: path_filter
|
||||
@@ -61,10 +78,54 @@ jobs:
|
||||
with:
|
||||
filters: |
|
||||
relevant: ${{ env.RELEVANT_PATHS }}
|
||||
frontend: ${{ env.FRONTEND_PATHS }}
|
||||
|
||||
check:
|
||||
name: Check (${{ matrix.TARGET }})
|
||||
needs: pre_job
|
||||
if: needs.pre_job.outputs.should_skip != 'true' && needs.pre_job.outputs.full_build != 'true'
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
include:
|
||||
- TARGET: x86_64-unknown-linux-musl
|
||||
OS: ubuntu-24.04
|
||||
FEATURES: jemalloc
|
||||
- TARGET: aarch64-apple-darwin
|
||||
OS: macos-latest
|
||||
FEATURES: jemalloc
|
||||
- TARGET: x86_64-pc-windows-msvc
|
||||
OS: windows-latest
|
||||
FEATURES: mimalloc
|
||||
runs-on: ${{ matrix.OS }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- uses: ./.github/actions/prepare-build
|
||||
with:
|
||||
target: ${{ matrix.TARGET }}
|
||||
gui: false
|
||||
pnpm: false
|
||||
token: ${{ github.token }}
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
shared-key: core-check-${{ matrix.TARGET }}
|
||||
save-if: ${{ github.event_name != 'pull_request' }}
|
||||
|
||||
- name: Check Core and Web
|
||||
run: cargo check --locked --all-targets --target ${{ matrix.TARGET }} --features ${{ matrix.FEATURES }}
|
||||
|
||||
build_web:
|
||||
runs-on: ubuntu-latest
|
||||
needs: pre_job
|
||||
if: needs.pre_job.outputs.should_skip != 'true'
|
||||
if: >-
|
||||
needs.pre_job.outputs.should_skip != 'true' &&
|
||||
(
|
||||
needs.pre_job.outputs.full_build == 'true' ||
|
||||
github.event_name != 'pull_request' ||
|
||||
needs.pre_job.outputs.frontend_changed == 'true'
|
||||
)
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
@@ -75,6 +136,7 @@ jobs:
|
||||
build-filter: './easytier-web/*'
|
||||
|
||||
- name: Archive artifact
|
||||
if: needs.pre_job.outputs.full_build == 'true'
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: easytier-web-dashboard
|
||||
@@ -150,7 +212,7 @@ jobs:
|
||||
needs:
|
||||
- pre_job
|
||||
- build_web
|
||||
if: needs.pre_job.outputs.should_skip != 'true'
|
||||
if: needs.pre_job.outputs.should_skip != 'true' && needs.pre_job.outputs.full_build == 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
@@ -168,8 +230,8 @@ jobs:
|
||||
uses: ./.github/actions/prepare-build
|
||||
with:
|
||||
target: ${{ matrix.TARGET }}
|
||||
gui: true
|
||||
pnpm: true
|
||||
gui: false
|
||||
pnpm: false
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
@@ -177,8 +239,8 @@ jobs:
|
||||
# The prefix cache key, this can be changed to start a new cache manually.
|
||||
# default: "v0-rust"
|
||||
prefix-key: ""
|
||||
shared-key: "core-registry"
|
||||
cache-targets: "false"
|
||||
shared-key: core-release-${{ matrix.TARGET }}
|
||||
cache-targets: "true"
|
||||
|
||||
- uses: mlugg/setup-zig@v2
|
||||
if: ${{ contains(matrix.OS, 'ubuntu') }}
|
||||
@@ -286,7 +348,7 @@ jobs:
|
||||
./artifacts/*
|
||||
|
||||
- name: Prepare SignPath test artifact
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && contains(matrix.TARGET, 'windows') }}
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && inputs.sign_windows && contains(matrix.TARGET, 'windows') }}
|
||||
run: |
|
||||
mkdir -p ./signpath-artifacts
|
||||
cp \
|
||||
@@ -297,7 +359,7 @@ jobs:
|
||||
./signpath-artifacts/
|
||||
|
||||
- name: Upload unsigned SignPath test artifact
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && contains(matrix.TARGET, 'windows') }}
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && inputs.sign_windows && contains(matrix.TARGET, 'windows') }}
|
||||
id: signpath-upload
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
@@ -306,7 +368,7 @@ jobs:
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Submit SignPath test-signing request
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && contains(matrix.TARGET, 'windows') }}
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && inputs.sign_windows && contains(matrix.TARGET, 'windows') }}
|
||||
uses: signpath/github-action-submit-signing-request@v2
|
||||
with:
|
||||
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
|
||||
@@ -319,7 +381,7 @@ jobs:
|
||||
output-artifact-directory: ./signpath-signed-artifacts
|
||||
|
||||
- name: Upload signed SignPath test artifact
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && contains(matrix.TARGET, 'windows') }}
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && inputs.sign_windows && contains(matrix.TARGET, 'windows') }}
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: signpath-signed-${{ matrix.ARTIFACT_NAME }}
|
||||
@@ -329,7 +391,7 @@ jobs:
|
||||
build_magisk:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ pre_job, build_web, build ]
|
||||
if: needs.pre_job.result == 'success' && needs.pre_job.outputs.should_skip != 'true' && !cancelled()
|
||||
if: needs.pre_job.result == 'success' && needs.pre_job.outputs.should_skip != 'true' && needs.pre_job.outputs.full_build == 'true' && !cancelled()
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v5 # 必须先检出代码才能获取模块配置
|
||||
@@ -371,9 +433,9 @@ jobs:
|
||||
|
||||
core-result:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ pre_job, build_web, build, build_magisk ]
|
||||
if: needs.pre_job.result == 'success' && needs.pre_job.outputs.should_skip != 'true' && !cancelled()
|
||||
needs: [ pre_job, check, build_web, build, build_magisk ]
|
||||
if: ${{ always() }}
|
||||
steps:
|
||||
- name: Mark result as failed
|
||||
if: contains(needs.*.result, 'failure')
|
||||
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')
|
||||
run: exit 1
|
||||
@@ -1,6 +1,7 @@
|
||||
name: EasyTier GUI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: ["develop", "main", "releases/**"]
|
||||
pull_request:
|
||||
@@ -8,7 +9,8 @@ on:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
# A new PR or branch push must not cancel a full build.
|
||||
group: ${{ github.workflow }}-${{ (github.event_name == 'workflow_dispatch' || startsWith(github.ref_name, 'releases/')) && 'full' || 'check' }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
@@ -32,6 +34,7 @@ jobs:
|
||||
"easytier-web/frontend-lib/**", ".github/workflows/gui.yml",
|
||||
".github/actions/**"]
|
||||
outputs:
|
||||
full_build: ${{ github.event_name == 'workflow_dispatch' || startsWith(github.ref_name, 'releases/') }}
|
||||
# Release builds must run even if they only change release metadata.
|
||||
should_skip: >-
|
||||
${{
|
||||
@@ -42,15 +45,16 @@ jobs:
|
||||
steps.path_filter.outputs.relevant != 'true'
|
||||
)
|
||||
) &&
|
||||
github.event_name != 'workflow_dispatch' &&
|
||||
!startsWith(github.ref_name, 'releases/')
|
||||
}}
|
||||
steps:
|
||||
- id: skip_check
|
||||
if: github.event_name != 'workflow_dispatch' && !startsWith(github.ref_name, 'releases/')
|
||||
uses: fkirc/skip-duplicate-actions@v5
|
||||
with:
|
||||
concurrent_skipping: 'same_content_newer'
|
||||
skip_after_successful_duplicate: 'true'
|
||||
cancel_others: 'true'
|
||||
paths: ${{ env.RELEVANT_PATHS }}
|
||||
|
||||
- id: path_filter
|
||||
@@ -62,6 +66,29 @@ jobs:
|
||||
filters: |
|
||||
relevant: ${{ env.RELEVANT_PATHS }}
|
||||
|
||||
check:
|
||||
name: Check GUI (Linux)
|
||||
needs: pre_job
|
||||
if: needs.pre_job.outputs.should_skip != 'true' && needs.pre_job.outputs.full_build != 'true'
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- uses: ./.github/actions/prepare-build
|
||||
with:
|
||||
gui: true
|
||||
pnpm: true
|
||||
pnpm-build-filter: easytier-gui...
|
||||
token: ${{ github.token }}
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
shared-key: gui-check-x86_64-unknown-linux-gnu
|
||||
save-if: ${{ github.event_name != 'pull_request' }}
|
||||
|
||||
- name: Check GUI
|
||||
run: cargo check --locked --all-targets --package easytier-gui --features custom-protocol
|
||||
|
||||
build-gui:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
@@ -106,7 +133,7 @@ jobs:
|
||||
GUI_TARGET: ${{ matrix.GUI_TARGET }}
|
||||
OSS_BUCKET: ${{ secrets.ALIYUN_OSS_BUCKET }}
|
||||
needs: pre_job
|
||||
if: needs.pre_job.outputs.should_skip != 'true'
|
||||
if: needs.pre_job.outputs.should_skip != 'true' && needs.pre_job.outputs.full_build == 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
@@ -120,7 +147,7 @@ jobs:
|
||||
target: ${{ matrix.TARGET }}
|
||||
gui: true
|
||||
pnpm: true
|
||||
pnpm-build-filter: ''
|
||||
pnpm-build-filter: easytier-gui...
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
@@ -128,8 +155,8 @@ jobs:
|
||||
# The prefix cache key, this can be changed to start a new cache manually.
|
||||
# default: "v0-rust"
|
||||
prefix-key: ""
|
||||
shared-key: "gui-registry"
|
||||
cache-targets: "false"
|
||||
shared-key: gui-release-${{ matrix.GUI_TARGET }}
|
||||
cache-targets: "true"
|
||||
|
||||
- name: copy correct DLLs
|
||||
if: ${{ contains(matrix.GUI_TARGET, 'windows') }}
|
||||
@@ -262,9 +289,9 @@ jobs:
|
||||
|
||||
gui-result:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ pre_job, build-gui ]
|
||||
if: needs.pre_job.result == 'success' && needs.pre_job.outputs.should_skip != 'true' && !cancelled()
|
||||
needs: [ pre_job, check, build-gui ]
|
||||
if: ${{ always() }}
|
||||
steps:
|
||||
- name: Mark result as failed
|
||||
if: contains(needs.*.result, 'failure')
|
||||
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')
|
||||
run: exit 1
|
||||
@@ -1,6 +1,7 @@
|
||||
name: EasyTier Mobile
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: ["develop", "main", "releases/**"]
|
||||
pull_request:
|
||||
@@ -8,7 +9,8 @@ on:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
# A new PR or branch push must not cancel a full build.
|
||||
group: ${{ github.workflow }}-${{ (github.event_name == 'workflow_dispatch' || startsWith(github.ref_name, 'releases/')) && 'full' || 'check' }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
@@ -32,6 +34,7 @@ jobs:
|
||||
"easytier-web/frontend-lib/**", ".github/workflows/mobile.yml",
|
||||
".github/actions/**"]
|
||||
outputs:
|
||||
full_build: ${{ github.event_name == 'workflow_dispatch' || startsWith(github.ref_name, 'releases/') }}
|
||||
# Release builds must run even if they only change release metadata.
|
||||
should_skip: >-
|
||||
${{
|
||||
@@ -42,15 +45,16 @@ jobs:
|
||||
steps.path_filter.outputs.relevant != 'true'
|
||||
)
|
||||
) &&
|
||||
github.event_name != 'workflow_dispatch' &&
|
||||
!startsWith(github.ref_name, 'releases/')
|
||||
}}
|
||||
steps:
|
||||
- id: skip_check
|
||||
if: github.event_name != 'workflow_dispatch' && !startsWith(github.ref_name, 'releases/')
|
||||
uses: fkirc/skip-duplicate-actions@v5
|
||||
with:
|
||||
concurrent_skipping: 'same_content_newer'
|
||||
skip_after_successful_duplicate: 'true'
|
||||
cancel_others: 'true'
|
||||
paths: ${{ env.RELEVANT_PATHS }}
|
||||
|
||||
- id: path_filter
|
||||
@@ -66,19 +70,11 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
include:
|
||||
- TARGET: aarch64-linux-android
|
||||
ARCH: aarch64
|
||||
- TARGET: armv7-linux-androideabi
|
||||
ARCH: armv7
|
||||
- TARGET: i686-linux-android
|
||||
ARCH: i686
|
||||
- TARGET: x86_64-linux-android
|
||||
ARCH: x86_64
|
||||
ARCH: ${{ fromJSON(needs.pre_job.outputs.full_build == 'true' && '["aarch64", "armv7", "i686", "x86_64"]' || '["aarch64"]') }}
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NAME: easytier
|
||||
TARGET: ${{ matrix.TARGET }}
|
||||
TARGET: ${{ matrix.ARCH == 'armv7' && 'armv7-linux-androideabi' || format('{0}-linux-android', matrix.ARCH) }}
|
||||
ARCH: ${{ matrix.ARCH }}
|
||||
OSS_BUCKET: ${{ secrets.ALIYUN_OSS_BUCKET }}
|
||||
needs: pre_job
|
||||
@@ -110,10 +106,10 @@ jobs:
|
||||
- name: Prepare build environment
|
||||
uses: ./.github/actions/prepare-build
|
||||
with:
|
||||
target: ${{ matrix.TARGET }}
|
||||
target: ${{ env.TARGET }}
|
||||
gui: false
|
||||
pnpm: true
|
||||
pnpm-build-filter: ''
|
||||
pnpm-build-filter: easytier-gui...
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
@@ -121,15 +117,22 @@ jobs:
|
||||
# The prefix cache key, this can be changed to start a new cache manually.
|
||||
# default: "v0-rust"
|
||||
prefix-key: ""
|
||||
shared-key: "gui-registry"
|
||||
cache-targets: "false"
|
||||
shared-key: mobile-${{ env.TARGET }}-${{ needs.pre_job.outputs.full_build }}
|
||||
cache-targets: "true"
|
||||
save-if: ${{ github.event_name != 'pull_request' }}
|
||||
|
||||
- name: Check Android ARM64
|
||||
if: needs.pre_job.outputs.full_build != 'true'
|
||||
run: cargo check --locked --lib --target "$TARGET" --package easytier-gui --features custom-protocol
|
||||
|
||||
- name: Build
|
||||
if: needs.pre_job.outputs.full_build == 'true'
|
||||
run: |
|
||||
cd easytier-gui
|
||||
pnpm tauri android build --apk --target "$ARCH" --split-per-abi
|
||||
|
||||
- name: Collect artifact
|
||||
if: needs.pre_job.outputs.full_build == 'true'
|
||||
run: |
|
||||
mkdir -p ./artifacts/objects/
|
||||
mv easytier-gui/src-tauri/gen/android/app/build/outputs/apk/*/release/*.apk ./artifacts/objects/
|
||||
@@ -144,6 +147,7 @@ jobs:
|
||||
rm -rf ./artifacts/objects/
|
||||
|
||||
- name: Archive artifact
|
||||
if: needs.pre_job.outputs.full_build == 'true'
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: easytier-mobile-android-${{ matrix.ARCH }}
|
||||
@@ -153,8 +157,8 @@ jobs:
|
||||
mobile-result:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ pre_job, build-mobile ]
|
||||
if: needs.pre_job.result == 'success' && needs.pre_job.outputs.should_skip != 'true' && !cancelled()
|
||||
if: ${{ always() }}
|
||||
steps:
|
||||
- name: Mark result as failed
|
||||
if: contains(needs.*.result, 'failure')
|
||||
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')
|
||||
run: exit 1
|
||||
@@ -0,0 +1,28 @@
|
||||
name: EasyTier Nightly
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# 03:17 Asia/Shanghai; avoid the busy start of the hour.
|
||||
- cron: '17 19 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: easytier-nightly
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
actions: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
dispatch:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Build branches with new commits
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const dispatch = require('./.github/scripts/nightly-build.js');
|
||||
await dispatch({ github, context, core });
|
||||
@@ -27,6 +27,7 @@ on:
|
||||
required: true
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
@@ -37,6 +38,17 @@ jobs:
|
||||
name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Validate complete builds
|
||||
uses: actions/github-script@v8
|
||||
env:
|
||||
CORE_RUN_ID: ${{ inputs.core_run_id }}
|
||||
GUI_RUN_ID: ${{ inputs.gui_run_id }}
|
||||
MOBILE_RUN_ID: ${{ inputs.mobile_run_id }}
|
||||
with:
|
||||
script: |
|
||||
const validate = require('./.github/scripts/validate-release-builds.js');
|
||||
await validate({ github, context, core });
|
||||
|
||||
- name: Download Core Artifact
|
||||
uses: dawidd6/action-download-artifact@v11
|
||||
with:
|
||||
|
||||
+43
-13
@@ -1,13 +1,16 @@
|
||||
name: EasyTier Test
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [ "develop", "main" ]
|
||||
branches: [ "develop", "main", "releases/**" ]
|
||||
pull_request:
|
||||
branches: [ "develop", "main" ]
|
||||
types: [ opened, synchronize, reopened, ready_for_review ]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
# A new PR or branch push must not cancel a full build.
|
||||
group: ${{ github.workflow }}-${{ (github.event_name == 'workflow_dispatch' || startsWith(github.ref_name, 'releases/')) && 'full' || 'check' }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
@@ -21,8 +24,22 @@ defaults:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
ci-scripts:
|
||||
name: Test CI scripts
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- run: node --test .github/scripts/*.test.js
|
||||
|
||||
pre_job:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
|
||||
env:
|
||||
RELEVANT_PATHS: >-
|
||||
["Cargo.toml", "Cargo.lock", "rust-toolchain.toml", ".cargo/**",
|
||||
@@ -32,16 +49,22 @@ jobs:
|
||||
"tauri-plugin-vpnservice/**", "easytier-contrib/**",
|
||||
".github/workflows/test.yml", ".github/actions/**"]
|
||||
outputs:
|
||||
full_build: ${{ github.event_name == 'workflow_dispatch' || startsWith(github.ref_name, 'releases/') }}
|
||||
should_skip: >-
|
||||
${{
|
||||
steps.skip_check.outputs.should_skip == 'true' ||
|
||||
(
|
||||
github.event_name == 'pull_request' &&
|
||||
steps.path_filter.outputs.relevant != 'true'
|
||||
)
|
||||
steps.skip_check.outputs.should_skip == 'true' ||
|
||||
(
|
||||
github.event_name == 'pull_request' &&
|
||||
steps.path_filter.outputs.relevant != 'true'
|
||||
)
|
||||
) &&
|
||||
github.event_name != 'workflow_dispatch' &&
|
||||
!startsWith(github.ref_name, 'releases/')
|
||||
}}
|
||||
steps:
|
||||
- id: skip_check
|
||||
if: github.event_name != 'workflow_dispatch' && !startsWith(github.ref_name, 'releases/')
|
||||
uses: fkirc/skip-duplicate-actions@v5
|
||||
with:
|
||||
concurrent_skipping: 'never'
|
||||
@@ -69,16 +92,23 @@ jobs:
|
||||
uses: ./.github/actions/prepare-build
|
||||
with:
|
||||
gui: true
|
||||
pnpm: true
|
||||
pnpm: ${{ needs.pre_job.outputs.full_build == 'true' }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
components: rustfmt,clippy
|
||||
target: wasm32-wasip1
|
||||
cache: false
|
||||
rustflags: ''
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
shared-key: test-check
|
||||
save-if: ${{ github.event_name != 'pull_request' }}
|
||||
|
||||
- uses: taiki-e/install-action@cargo-hack
|
||||
if: needs.pre_job.outputs.full_build == 'true'
|
||||
|
||||
- name: Check formatting
|
||||
if: ${{ !cancelled() }}
|
||||
@@ -89,7 +119,7 @@ jobs:
|
||||
run: cargo clippy --all-targets --features full --all -- -D warnings
|
||||
|
||||
- name: Check features
|
||||
if: ${{ !cancelled() }}
|
||||
if: ${{ !cancelled() && needs.pre_job.outputs.full_build == 'true' }}
|
||||
run: cargo hack check --package easytier --each-feature --exclude-features macos-ne --verbose
|
||||
|
||||
- name: Check WASI
|
||||
@@ -117,8 +147,8 @@ jobs:
|
||||
- name: Prepare build environment
|
||||
uses: ./.github/actions/prepare-build
|
||||
with:
|
||||
gui: true
|
||||
pnpm: true
|
||||
gui: false
|
||||
pnpm: false
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
@@ -191,9 +221,9 @@ jobs:
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ pre_job, check, test_matrix ]
|
||||
if: needs.pre_job.result == 'success' && needs.pre_job.outputs.should_skip != 'true' && !cancelled()
|
||||
needs: [ pre_job, ci-scripts, check, pre-test, test_matrix ]
|
||||
if: ${{ always() }}
|
||||
steps:
|
||||
- name: Mark result as failed
|
||||
if: contains(needs.*.result, 'failure')
|
||||
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')
|
||||
run: exit 1
|
||||
Reference in new issue
Block a user