mirror of
https://github.com/whyour/qinglong.git
synced 2026-11-03 16:19:11 +08:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5f22f8d44 | ||
|
|
f2c2f5e4f8 |
@@ -405,151 +405,6 @@ jobs:
|
||||
cache-from: type=registry,ref=whyour/qinglong:cache-debian
|
||||
cache-to: type=registry,ref=whyour/qinglong:cache-debian,mode=max
|
||||
|
||||
build-alpine310:
|
||||
if: ${{ github.ref_name == 'master' }}
|
||||
needs: build-static
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: qinglong-static-${{ github.sha }}
|
||||
path: static/
|
||||
- uses: pnpm/action-setup@v6
|
||||
with:
|
||||
version: "8.3.1"
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: pnpm-lock.yaml
|
||||
|
||||
- name: Read version from version.yaml
|
||||
id: version
|
||||
run: |
|
||||
VERSION=$(grep '^version:' version.yaml | awk '{print $2}')
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Setup timezone
|
||||
run: sudo timedatectl set-timezone Asia/Shanghai
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v4
|
||||
with:
|
||||
cache-image: false
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Build and push (Alpine Python 3.10)
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
build-args: |
|
||||
QL_MAINTAINER=${{ github.repository_owner }}
|
||||
QL_BRANCH=${{ github.ref_name }}
|
||||
SOURCE_COMMIT=${{ github.sha }}
|
||||
network: host
|
||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x,linux/386
|
||||
context: .
|
||||
file: ./docker/Dockerfile.310
|
||||
push: true
|
||||
provenance: mode=max
|
||||
sbom: true
|
||||
tags: |
|
||||
whyour/qinglong:python3.10
|
||||
whyour/qinglong:${{ steps.version.outputs.version }}-python3.10
|
||||
cache-from: type=registry,ref=whyour/qinglong:cache-alpine-python3.10
|
||||
cache-to: type=registry,ref=whyour/qinglong:cache-alpine-python3.10,mode=max
|
||||
|
||||
build-debian310:
|
||||
if: ${{ github.ref_name == 'master' }}
|
||||
needs: build-static
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: qinglong-static-${{ github.sha }}
|
||||
path: static/
|
||||
- uses: pnpm/action-setup@v6
|
||||
with:
|
||||
version: "8.3.1"
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: pnpm-lock.yaml
|
||||
|
||||
- name: Read version from version.yaml
|
||||
id: version
|
||||
run: |
|
||||
VERSION=$(grep '^version:' version.yaml | awk '{print $2}')
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Setup timezone
|
||||
run: sudo timedatectl set-timezone Asia/Shanghai
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v4
|
||||
with:
|
||||
cache-image: false
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Build and push (Debian Python 3.10)
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
build-args: |
|
||||
QL_MAINTAINER=${{ github.repository_owner }}
|
||||
QL_BRANCH=${{ github.ref_name }}
|
||||
SOURCE_COMMIT=${{ github.sha }}
|
||||
network: host
|
||||
# Keep s390x on Debian: npm can hang under QEMU with Alpine (nodejs/docker-node#1973).
|
||||
platforms: linux/amd64,linux/arm64,linux/ppc64le,linux/s390x
|
||||
context: .
|
||||
file: ./docker/Dockerfile.debian310
|
||||
push: true
|
||||
provenance: mode=max
|
||||
sbom: true
|
||||
tags: |
|
||||
whyour/qinglong:debian-python3.10
|
||||
whyour/qinglong:${{ steps.version.outputs.version }}-debian-python3.10
|
||||
cache-from: type=registry,ref=whyour/qinglong:cache-debian-python3.10
|
||||
cache-to: type=registry,ref=whyour/qinglong:cache-debian-python3.10,mode=max
|
||||
|
||||
# One unmodified amd64 package for migration acceptance only. This does not
|
||||
# publish bundles or advance development/formal channel indexes.
|
||||
build-npm-migration-bundle:
|
||||
@@ -616,7 +471,7 @@ jobs:
|
||||
|
||||
publish:
|
||||
if: ${{ github.repository == 'whyour/qinglong' && github.ref == 'refs/heads/master' }}
|
||||
needs: [build-alpine, build-debian, build-alpine310, build-debian310, verify-npm-package]
|
||||
needs: [build-alpine, build-debian, verify-npm-package]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -652,7 +507,7 @@ jobs:
|
||||
jq -e --arg version "$version" '. == $version' "$RUNNER_TEMP/npm-version.json" > /dev/null
|
||||
echo "::notice::$name@$version is already published; skipping npm publication for this image rebuild."
|
||||
elif jq -e '.error.code == "E404"' "$RUNNER_TEMP/npm-version.json" > /dev/null; then
|
||||
npm publish "$archive" --ignore-scripts --access public --registry=https://registry.npmjs.org
|
||||
npm publish "./$archive" --ignore-scripts --access public --registry=https://registry.npmjs.org
|
||||
else
|
||||
echo '::error::Could not check the published npm version; refusing to publish.'
|
||||
exit 1
|
||||
|
||||
@@ -25,7 +25,7 @@ on:
|
||||
type: boolean
|
||||
default: false
|
||||
prerelease_version:
|
||||
description: Develop only, e.g. 0.1.2-beta.0 (alpha/beta/rc)
|
||||
description: Develop only, e.g. 0.1.5-beta.0 (alpha/beta/rc)
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
@@ -93,8 +93,26 @@ jobs:
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
platform-smoke:
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 15
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [windows-latest, ubuntu-24.04-arm]
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '24'
|
||||
cache: npm
|
||||
cache-dependency-path: cli/package-lock.json
|
||||
- run: npm ci --prefix cli --no-audit --no-fund
|
||||
- run: npm run build:cli
|
||||
- run: node --test cli/test/remote/platformSmoke.test.cjs
|
||||
|
||||
publish:
|
||||
needs: package
|
||||
needs: [package, platform-smoke]
|
||||
if: >-
|
||||
github.repository == 'whyour/qinglong' &&
|
||||
((github.ref == 'refs/heads/master' &&
|
||||
|
||||
@@ -70,14 +70,6 @@ jobs:
|
||||
file: docker/Dockerfile.debian
|
||||
cache: debian
|
||||
platforms: linux/amd64,linux/arm64,linux/ppc64le,linux/s390x
|
||||
- image: alpine-python3.10
|
||||
file: docker/Dockerfile.310
|
||||
cache: alpine-python3.10
|
||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x,linux/386
|
||||
- image: debian-python3.10
|
||||
file: docker/Dockerfile.debian310
|
||||
cache: debian-python3.10
|
||||
platforms: linux/amd64,linux/arm64,linux/ppc64le,linux/s390x
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
|
||||
+4
-4
@@ -76,7 +76,7 @@ ql auth status --scope crons --json
|
||||
# For automation, inject QL_CLIENT_ID and QL_CLIENT_SECRET before the same login command
|
||||
```
|
||||
|
||||
Credentials/token are stored in `~/.config/qinglong/cli.json`, a plaintext file owned by the current user with mode 0600. `QL_CLI_CONFIG` selects another file. Login validates application credentials, not every resource permission.
|
||||
Credentials/token are stored in `~/.config/qinglong/cli.json` as plaintext. The CLI requests mode `0600` when creating the file and rejects symlinks and non-regular files when reading it. Windows, WSL mounts, and some network or device filesystems do not reliably report POSIX permissions, so reads do not depend on mode bits or UID. Keep the config in a location accessible only to your user and protect it with the filesystem's actual permissions or ACLs; use the direct-token environment variables below if that cannot be guaranteed. `QL_CLI_CONFIG` selects another file. Login validates application credentials, not every resource permission.
|
||||
|
||||
### Direct access token
|
||||
|
||||
@@ -128,11 +128,11 @@ npm run test:cli
|
||||
node cli/scripts/verify-package.cjs
|
||||
```
|
||||
|
||||
Tests cover requests, token refresh, output, errors, no automatic retries, permissions and isolated installation. Package verification installs the archive offline, verifies the sole ql executable and rejects local commands.
|
||||
Tests cover requests, token refresh, output, errors, no automatic retries, config file type and isolated installation. Package verification installs the archive offline, verifies the sole ql executable and rejects local commands.
|
||||
|
||||
The CLI package workflow checks, builds and tests Node 22.12/24 on relevant PRs, develop/master pushes and manual runs. Node 24 uploads the archive verified by offline installation as `qinglong-cli-<commit>`. After both matrix jobs succeed, master pushes publish that exact archive to npm as latest, using GitHub Actions OIDC trusted publishing without an NPM_TOKEN secret. Manual runs publish only when run on master with publish enabled; PRs, develop and forks never publish. Configure the npm Trusted Publisher for repository `whyour/qinglong`, workflow `cli-package.yml`, with `npm publish` allowed. The panel package `@whyour/qinglong` separately trusts `build-docker-image.yml`. Publishing uses Node 24 with `id-token: write` granted only to the publish job. New packages need an initial publication before configuring their trusted publisher.
|
||||
The CLI package workflow checks, builds and tests Node 22.12/24 on relevant PRs, develop/master pushes and manual runs, and verifies saved login and command reads on real Windows and Linux ARM64 filesystems. Node 24 uploads the archive verified by offline installation as `qinglong-cli-<commit>`. After all validation jobs succeed, master pushes publish that exact archive to npm as `latest`; a manual run on master with `publish` enabled can also publish a stable release. Develop pushes only verify. A manual run on develop with `publish` enabled and `prerelease_version` set (for example `0.1.5-beta.0`) publishes under the matching `alpha`, `beta` or `rc` tag. PRs and forks never publish. CLI-only changes do not trigger the main image build, except for the few internal CLI upgrade files used by the image. Configure the npm Trusted Publisher for repository `whyour/qinglong`, workflow `cli-package.yml`, with `npm publish` allowed. The panel package `@whyour/qinglong` separately trusts `build-docker-image.yml`. The publish job uses Node 24 and GitHub Actions OIDC without an `NPM_TOKEN` secret, with `id-token: write` granted only to that job. New packages need an initial publication before configuring their trusted publisher.
|
||||
|
||||
The CLI has an independent stable version in cli/package.json and cli/package-lock.json. Before releasing changes, run `npm version patch --prefix cli --no-git-tag-version` (or minor/major) and commit both files. Existing versions are skipped with a notice; registry failures stop publication. Only stable X.Y.Z versions are published by this workflow. Publication does not rebuild the verified archive or run package lifecycle scripts.
|
||||
The CLI stable version is maintained independently in cli/package.json and cli/package-lock.json. Before releasing changes, run `npm version patch --prefix cli --no-git-tag-version` (or minor/major) and commit both files. A develop prerelease version is set only in the workflow run and is not written back to the repository. An existing stable version is skipped with a notice; a duplicate prerelease version fails, as does a registry lookup error. The publish job does not rebuild the verified archive or run package lifecycle scripts.
|
||||
|
||||
## Panel-internal tools
|
||||
|
||||
|
||||
+4
-4
@@ -76,7 +76,7 @@ ql auth status --scope crons --json
|
||||
# 自动化环境事先注入 QL_CLIENT_ID 和 QL_CLIENT_SECRET,再运行同一 login 命令
|
||||
```
|
||||
|
||||
凭据和 token 保存到 `~/.config/qinglong/cli.json`,为当前用户所有的 0600 明文文件。`QL_CLI_CONFIG` 可选择其他配置文件。登录只验证应用凭据,不意味着具备所有资源权限。
|
||||
凭据和 token 保存到 `~/.config/qinglong/cli.json` 明文文件。CLI 创建文件时会请求 `0600` 权限,并在读取时拒绝符号链接和非普通文件;由于 Windows、WSL 挂载盘及部分网络或设备文件系统不能可靠提供 POSIX 权限位,读取时不以权限位或 UID 为准。请将配置保存在仅当前用户可访问的位置,并按实际文件系统的权限或 ACL 保护它;无法保证时可使用下述环境令牌方式。`QL_CLI_CONFIG` 可选择其他配置文件。登录只验证应用凭据,不意味着具备所有资源权限。
|
||||
|
||||
### 直接使用访问令牌
|
||||
|
||||
@@ -128,11 +128,11 @@ npm run test:cli
|
||||
node cli/scripts/verify-package.cjs
|
||||
```
|
||||
|
||||
测试覆盖请求格式、认证刷新、输出、错误、禁止自动重试、权限和独立安装。打包验证会离线安装 tgz,并确认唯一入口是 `ql`,本机命令不可调用。
|
||||
测试覆盖请求格式、认证刷新、输出、错误、禁止自动重试、配置文件类型和独立安装。打包验证会离线安装 tgz,并确认唯一入口是 `ql`,本机命令不可调用。
|
||||
|
||||
CLI package 工作流在相关 PR、develop/master 推送和手动触发时执行 Node 22.12/24 类型检查、构建及测试。Node 24 上传通过离线安装验证的 `qinglong-cli-<commit>` artifact。两个矩阵任务成功后,master 推送会将这份已验证的 tgz 发布到 npm 的 latest 标签,通过 GitHub Actions OIDC 可信发布,无需 `NPM_TOKEN` Secret。手动运行需选择 master 并勾选 publish;PR、develop 和 fork 不发布。npm 的 Trusted Publisher 需绑定仓库 `whyour/qinglong` 和工作流 `cli-package.yml`,允许 `npm publish`;面板包 `@whyour/qinglong` 单独绑定 `build-docker-image.yml`。发布 job 使用 Node 24,并仅在该 job 授予 `id-token: write`。新包需先完成首次发布,再配置对应包的可信发布关系。
|
||||
CLI package 工作流在相关 PR、develop/master 推送和手动触发时执行 Node 22.12/24 类型检查、构建及测试,并在 Windows 与 Linux ARM64 上用真实文件系统验证登录和命令读取。Node 24 上传通过离线安装验证的 `qinglong-cli-<commit>` artifact。全部验证成功后,master 推送会将这份 tgz 发布到 npm 的 `latest` 标签;手动在 master 勾选 `publish` 也可发布稳定版。develop 推送只验证;手动在 develop 勾选 `publish` 并填写 `prerelease_version`(例如 `0.1.5-beta.0`)可发布到对应的 `alpha`、`beta` 或 `rc` 标签。PR 和 fork 不发布。CLI 专属改动不会触发主镜像构建,少数被镜像使用的 CLI 内部升级文件除外。npm 的 Trusted Publisher 需绑定仓库 `whyour/qinglong` 和工作流 `cli-package.yml`,允许 `npm publish`;面板包 `@whyour/qinglong` 单独绑定 `build-docker-image.yml`。发布 job 使用 Node 24 和 GitHub Actions OIDC,无需 `NPM_TOKEN` Secret,仅该 job 授予 `id-token: write`。新包需先完成首次发布,再配置对应包的可信发布关系。
|
||||
|
||||
CLI 版本独立维护在 cli/package.json 和 cli/package-lock.json。发布改动前执行 `npm version patch --prefix cli --no-git-tag-version`(也可用 minor/major),提交这两个文件。已发布的版本会提示并跳过;registry 查询失败则停止发布。该流程仅发布 X.Y.Z 稳定版本,不重新构建产物或执行包生命周期脚本。
|
||||
CLI 稳定版版本号独立维护在 cli/package.json 和 cli/package-lock.json。发布改动前执行 `npm version patch --prefix cli --no-git-tag-version`(也可用 minor/major),提交这两个文件。develop 预发布版版本号仅在工作流运行时设置,不写回仓库。已发布的稳定版版本号会提示并跳过;重复的预发布版版本号会报错,registry 查询失败也会停止发布。发布 job 不重新构建产物或执行包生命周期脚本。
|
||||
|
||||
## 面板内部工具
|
||||
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@whyour/qinglong-cli",
|
||||
"version": "0.1.3",
|
||||
"version": "0.1.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@whyour/qinglong-cli",
|
||||
"version": "0.1.3",
|
||||
"version": "0.1.4",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"ql": "dist/npm/ql.js"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@whyour/qinglong-cli",
|
||||
"version": "0.1.3",
|
||||
"version": "0.1.4",
|
||||
"description": "Authenticated remote management CLI for QingLong 2.x",
|
||||
"type": "commonjs",
|
||||
"bin": {
|
||||
|
||||
@@ -17,20 +17,28 @@ export function configPath(): string {
|
||||
export function readConfig(): StoredConfig {
|
||||
let fd: number | undefined;
|
||||
try {
|
||||
fd = fs.openSync(
|
||||
configPath(),
|
||||
fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW,
|
||||
const file = configPath();
|
||||
const pathStat = fs.lstatSync(file);
|
||||
if (!pathStat.isFile())
|
||||
fail(
|
||||
translate(
|
||||
process.env,
|
||||
'CLI 配置必须是普通文件,且不能是符号链接。',
|
||||
),
|
||||
);
|
||||
// Not every platform or filesystem implements POSIX mode bits or O_NOFOLLOW.
|
||||
const noFollow = fs.constants.O_NOFOLLOW ?? 0;
|
||||
fd = fs.openSync(file, fs.constants.O_RDONLY | noFollow);
|
||||
const stat = fs.fstatSync(fd);
|
||||
if (
|
||||
!stat.isFile() ||
|
||||
stat.mode & 0o077 ||
|
||||
(process.getuid && stat.uid !== process.getuid())
|
||||
pathStat.dev !== stat.dev ||
|
||||
pathStat.ino !== stat.ino
|
||||
) {
|
||||
fail(
|
||||
translate(
|
||||
process.env,
|
||||
'CLI 配置必须是当前用户拥有的私有普通文件(0600)。',
|
||||
'CLI 配置必须是普通文件,且不能是符号链接。',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -175,8 +175,8 @@ export const english: Record<string, string> = {
|
||||
'Request failed or returned invalid JSON. Check the instance URL, network and TLS certificate.',
|
||||
'API 请求被拒绝(code %s),请检查%s。':
|
||||
'API request rejected (code %s). Check %s.',
|
||||
'CLI 配置必须是当前用户拥有的私有普通文件(0600)。':
|
||||
'CLI config must be an owned private regular file (0600).',
|
||||
'CLI 配置必须是普通文件,且不能是符号链接。':
|
||||
'CLI config must be a regular file, not a symbolic link.',
|
||||
'CLI 配置无效,请运行 ql login。': 'Invalid CLI config. Run ql login.',
|
||||
'尚未登录,请运行 ql login。': 'Not logged in. Run ql login.',
|
||||
'无法读取 CLI 配置,请检查文件权限并运行 ql login。':
|
||||
|
||||
@@ -296,11 +296,13 @@ test('malformed successful responses are rejected, including expired authenticat
|
||||
failure(await f.invoke(['login', '--url', f.url, '--json']), 1);
|
||||
});
|
||||
|
||||
test('config permissions, malformed content and symlinks fail closed', async (t) => {
|
||||
test('config content and symlinks are checked independently of permission bits', async (t) => {
|
||||
const f = await fixture(t);
|
||||
await f.login();
|
||||
fs.chmodSync(f.file, 0o644);
|
||||
failure(await f.invoke(['task', 'list', '--json']), 1);
|
||||
const sharedMode = await f.invoke(['task', 'list', '--json']);
|
||||
assert.equal(sharedMode.code, 0, sharedMode.err);
|
||||
assert.equal(JSON.parse(sharedMode.out).data.total, 1);
|
||||
fs.chmodSync(f.file, 0o600);
|
||||
fs.writeFileSync(f.file, '{broken');
|
||||
failure(await f.invoke(['task', 'list', '--json']), 1);
|
||||
@@ -312,12 +314,73 @@ test('config permissions, malformed content and symlinks fail closed', async (t)
|
||||
const target = path.join(f.dir, 'target');
|
||||
fs.renameSync(f.file, target);
|
||||
fs.symlinkSync(target, f.file);
|
||||
failure(await f.invoke(['task', 'list', '--json']), 1);
|
||||
const linked = await f.invoke(['task', 'list', '--json'], { QL_LANG: 'en' });
|
||||
failure(linked, 1);
|
||||
assert.match(JSON.parse(linked.err).message, /regular file, not a symbolic link/);
|
||||
await f.login();
|
||||
assert.equal(fs.lstatSync(f.file).isSymbolicLink(), false);
|
||||
assert.equal(JSON.parse(fs.readFileSync(target)).clientId, 123);
|
||||
});
|
||||
|
||||
test('Windows accepts a saved config despite synthetic 0666 mode bits', async (t) => {
|
||||
const f = await fixture(t);
|
||||
await f.login();
|
||||
const preload = path.join(f.dir, 'windows-mode.cjs');
|
||||
fs.writeFileSync(
|
||||
preload,
|
||||
`Object.defineProperty(process, 'platform', { value: 'win32' });
|
||||
const fs = require('node:fs');
|
||||
const originalFstatSync = fs.fstatSync;
|
||||
const originalOpenSync = fs.openSync;
|
||||
fs.openSync = function (file, flags, ...args) {
|
||||
return originalOpenSync(file, typeof flags === 'number'
|
||||
? flags & ~fs.constants.O_NOFOLLOW : flags, ...args);
|
||||
};
|
||||
fs.fstatSync = function (...args) {
|
||||
const stat = originalFstatSync(...args);
|
||||
stat.mode = (stat.mode & ~0o777) | 0o666;
|
||||
return stat;
|
||||
};\n`,
|
||||
);
|
||||
const env = { NODE_OPTIONS: `--require=${preload}` };
|
||||
const status = await f.invoke(['auth', 'status', '--json'], env);
|
||||
assert.equal(status.code, 0, status.err);
|
||||
assert.equal(JSON.parse(status.out).data.scopeChecked, 'crons');
|
||||
const list = await f.invoke(['task', 'list', '--json'], env);
|
||||
assert.equal(list.code, 0, list.err);
|
||||
assert.equal(JSON.parse(list.out).data.total, 1);
|
||||
const target = path.join(f.dir, 'target');
|
||||
fs.renameSync(f.file, target);
|
||||
fs.symlinkSync(target, f.file);
|
||||
failure(await f.invoke(['task', 'list', '--json'], env), 1);
|
||||
});
|
||||
|
||||
test('Linux ARM accepts a saved config with synthetic mode and owner', async (t) => {
|
||||
const f = await fixture(t);
|
||||
await f.login();
|
||||
const preload = path.join(f.dir, 'arm-mode.cjs');
|
||||
fs.writeFileSync(
|
||||
preload,
|
||||
`Object.defineProperty(process, 'platform', { value: 'linux' });
|
||||
Object.defineProperty(process, 'arch', { value: 'arm' });
|
||||
const fs = require('node:fs');
|
||||
const originalFstatSync = fs.fstatSync;
|
||||
fs.fstatSync = function (...args) {
|
||||
const stat = originalFstatSync(...args);
|
||||
stat.mode = (stat.mode & ~0o777) | 0o666;
|
||||
stat.uid = 999999;
|
||||
return stat;
|
||||
};\n`,
|
||||
);
|
||||
const env = { NODE_OPTIONS: `--require=${preload}` };
|
||||
const status = await f.invoke(['auth', 'status', '--json'], env);
|
||||
assert.equal(status.code, 0, status.err);
|
||||
assert.equal(JSON.parse(status.out).data.scopeChecked, 'crons');
|
||||
const list = await f.invoke(['task', 'list', '--json'], env);
|
||||
assert.equal(list.code, 0, list.err);
|
||||
assert.equal(JSON.parse(list.out).data.total, 1);
|
||||
});
|
||||
|
||||
test('standalone compiled artifact and symlink run without repo runtime dependencies', async (t) => {
|
||||
const f = await fixture(t);
|
||||
const dist = path.join(f.dir, 'standalone');
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const http = require('node:http');
|
||||
const { spawn } = require('node:child_process');
|
||||
|
||||
const entry = path.resolve(__dirname, '../../dist/npm/ql.js');
|
||||
|
||||
test('saved login works on the host filesystem', async (t) => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ql-platform-smoke-'));
|
||||
const config = path.join(dir, 'cli.json');
|
||||
const server = http.createServer((req, res) => {
|
||||
const url = new URL(req.url, 'http://127.0.0.1');
|
||||
res.setHeader('content-type', 'application/json');
|
||||
if (url.pathname === '/panel/open/auth/token') {
|
||||
assert.equal(url.searchParams.get('client_id'), 'smoke-id');
|
||||
assert.equal(url.searchParams.get('client_secret'), 'smoke-secret');
|
||||
res.end(JSON.stringify({
|
||||
code: 200,
|
||||
data: {
|
||||
token: 'smoke-token',
|
||||
expiration: Math.floor(Date.now() / 1000) + 3600,
|
||||
},
|
||||
}));
|
||||
return;
|
||||
}
|
||||
assert.equal(req.headers.authorization, 'Bearer smoke-token');
|
||||
assert.equal(url.pathname, '/panel/open/crons');
|
||||
res.end(JSON.stringify({ code: 200, data: { data: [], total: 0 } }));
|
||||
});
|
||||
await new Promise((resolve, reject) => {
|
||||
server.once('error', reject);
|
||||
server.listen(0, '127.0.0.1', resolve);
|
||||
});
|
||||
t.after(async () => {
|
||||
server.closeAllConnections();
|
||||
await new Promise((resolve) => server.close(resolve));
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
QL_CLI_CONFIG: config,
|
||||
QL_CLIENT_ID: 'smoke-id',
|
||||
QL_CLIENT_SECRET: 'smoke-secret',
|
||||
QL_URL: '',
|
||||
QL_ACCESS_TOKEN: '',
|
||||
};
|
||||
const invoke = (args) => new Promise((resolve, reject) => {
|
||||
const child = spawn(process.execPath, [entry, ...args], {
|
||||
env,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
let out = '';
|
||||
let err = '';
|
||||
child.stdout.on('data', (data) => (out += data));
|
||||
child.stderr.on('data', (data) => (err += data));
|
||||
child.once('error', reject);
|
||||
child.once('close', (code) => resolve({ code, out, err }));
|
||||
});
|
||||
const url = `http://127.0.0.1:${server.address().port}/panel`;
|
||||
const login = await invoke(['login', '--url', url, '--json']);
|
||||
assert.equal(login.code, 0, login.err);
|
||||
assert.equal(JSON.parse(login.out).data.authenticated, true);
|
||||
assert.equal(fs.lstatSync(config).isFile(), true);
|
||||
const status = await invoke(['auth', 'status', '--json']);
|
||||
assert.equal(status.code, 0, status.err);
|
||||
assert.equal(JSON.parse(status.out).data.authenticated, true);
|
||||
const list = await invoke(['task', 'list', '--json']);
|
||||
assert.equal(list.code, 0, list.err);
|
||||
assert.equal(JSON.parse(list.out).data.total, 0);
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
# 青龙 2.24.0
|
||||
|
||||
## 新增功能
|
||||
|
||||
- Node.js 原生部署支持 Python venv 隔离环境。Python 依赖可使用包名、Git URL,以及位于脚本或订阅仓库中的 requirements 文件和 pyproject.toml;拉取订阅不会自动安装仓库声明的依赖。
|
||||
- 新增通知审计日志,便于查看通知发送结果和失败原因。
|
||||
- 提供经过平台校验的升级包,支持在线暂存、离线安装及失败回退。升级过程保留账号、配置、环境变量、任务和脚本等数据。
|
||||
|
||||
## 修复与优化
|
||||
|
||||
- 默认 Alpine 与 Debian 镜像统一使用 Node.js 24;SQLite 依赖升级至 1.1.4。ARM64 升级包使用原生构建环境。
|
||||
- 修复额外调度规则校验、单实例任务运行期间的新触发排队、订阅同步、Python 依赖安装与脚本发现等问题。
|
||||
- 修复系统日志清理时活跃日志文件被移除、页面加载时序和升级流程中的异常情况,并补齐相关中英文提示。
|
||||
- 独立远程 CLI 更新至 0.1.3,最低 Node.js 版本为 22.12.0。
|
||||
|
||||
## 镜像与升级提醒
|
||||
|
||||
- `2.24.0` 的 Alpine 镜像支持 amd64、arm/v6、arm/v7、arm64、ppc64le、s390x 和 386;`2.24.0-debian` 支持 amd64、arm64、ppc64le 和 s390x。Debian 暂不提供 arm/v7。
|
||||
- 2.23.0 是 Python 3.10 兼容镜像的最后一个版本;2.24.0 不再更新该变体。需要 Python 3.10 的用户请先评估迁移方案。
|
||||
- 升级前停止任务并备份完整持久化数据目录,记录原镜像摘要和启动配置。升级后检查脚本、依赖、订阅与通知;如需回退,请同时恢复与旧版本匹配的数据备份。
|
||||
- 主 npm 包 `@whyour/qinglong` 与独立远程 CLI 包 `@whyour/qinglong-cli` 均提供 `ql` 命令。在已有面板的主机上安装远程 CLI 时,使用独立 npm prefix 或 `npm exec --package=@whyour/qinglong-cli -- ql ...`,避免覆盖面板内置入口。
|
||||
|
||||
本版本以 2.23.0 为上一正式版本。开发候选的源码、npm 包与 11 个发布镜像架构已完成隔离验收;正式产物发布后仍以各目标渠道的实际构建结果为准。
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@whyour/qinglong",
|
||||
"packageManager": "pnpm@8.3.1",
|
||||
"version": "2.24.0-dev.0",
|
||||
"version": "2.24.0",
|
||||
"description": "Timed task management platform supporting Python3, JavaScript, Shell, Typescript",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -51,8 +51,6 @@ test('release images receive the same-run artifact and verify it before use', ()
|
||||
for (const name of [
|
||||
'build-alpine',
|
||||
'build-debian',
|
||||
'build-alpine310',
|
||||
'build-debian310',
|
||||
]) {
|
||||
const job = workflow.jobs[name];
|
||||
assert.equal(job.needs, 'build-static');
|
||||
|
||||
@@ -273,8 +273,6 @@ test('Gitee workflows verify both mirrors and do not gate image publication', ()
|
||||
for (const name of [
|
||||
'build-alpine',
|
||||
'build-debian',
|
||||
'build-alpine310',
|
||||
'build-debian310',
|
||||
]) {
|
||||
assert.equal(workflow.jobs[name].needs, 'build-static');
|
||||
}
|
||||
|
||||
@@ -220,7 +220,7 @@ test('npm publication consumes the exact archive whose isolated installation pas
|
||||
const download = workflow.jobs.publish.steps.find((step) => step.uses?.startsWith('actions/download-artifact@'));
|
||||
assert.equal(download.with.name, 'qinglong-npm-${{ github.sha }}');
|
||||
const publication = workflow.jobs.publish.steps.find((step) => step.name === 'Publish npm package with OIDC');
|
||||
assert.match(publication.run, /npm publish "\$archive" --ignore-scripts/);
|
||||
assert.match(publication.run, /npm publish "\.\/\$archive" --ignore-scripts/);
|
||||
assert.ok(!workflow.jobs.publish.steps.some((step) => /pnpm build/.test(step.run || '')));
|
||||
});
|
||||
|
||||
|
||||
@@ -12,12 +12,10 @@ const publication = workflow.jobs.publish.steps.find(
|
||||
(step) => step.name === 'Publish npm package with OIDC',
|
||||
).run;
|
||||
|
||||
test('npm publication waits for every release image, including Python 3.10', () => {
|
||||
test('npm publication waits for every supported release image', () => {
|
||||
for (const name of [
|
||||
'build-alpine',
|
||||
'build-debian',
|
||||
'build-alpine310',
|
||||
'build-debian310',
|
||||
])
|
||||
assert.ok(workflow.jobs.publish.needs.includes(name), name);
|
||||
});
|
||||
@@ -77,7 +75,7 @@ case 'network': console.log('upstream unavailable'); process.exit(1);
|
||||
assert.equal(fs.existsSync(path.join(root, 'published')), published);
|
||||
if (published)
|
||||
assert.deepEqual(JSON.parse(fs.readFileSync(path.join(root, 'published'), 'utf8')), [
|
||||
'npm-panel/verified.tgz', '--ignore-scripts', '--access', 'public', '--registry=https://registry.npmjs.org',
|
||||
'./npm-panel/verified.tgz', '--ignore-scripts', '--access', 'public', '--registry=https://registry.npmjs.org',
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -225,8 +225,6 @@ test('static backfill uses existing snapshots and skips all source, build and im
|
||||
'verify-gitee-mirrors',
|
||||
'build-alpine',
|
||||
'build-debian',
|
||||
'build-alpine310',
|
||||
'build-debian310',
|
||||
]) {
|
||||
assert.equal(workflow.jobs[name].needs, 'build-static');
|
||||
assert.doesNotMatch(workflow.jobs[name].if || '', /always\(/);
|
||||
|
||||
+11
-8
@@ -1,9 +1,12 @@
|
||||
version: 2.24.0-dev.0
|
||||
changeLogLink: https://github.com/whyour/qinglong/blob/develop/docs/releases/2.24.0-dev.0.md
|
||||
publishTime: "2026-10-25 19:22:54"
|
||||
version: 2.24.0
|
||||
changeLogLink: https://github.com/whyour/qinglong/blob/v2.24.0/docs/releases/2.24.0.md
|
||||
publishTime: "2026-10-29 02:14:58"
|
||||
changeLog: |
|
||||
1. 开发候选 2.24.0-dev.0,尚未正式发布;以 2.23.0 为上一正式版本
|
||||
2. Alpine 与 Debian 镜像统一使用 Node.js 24,升级 sqlite3 至 1.1.4
|
||||
3. Alpine 保留 arm/v6 和 arm/v7;Debian 暂不提供 arm/v7 镜像
|
||||
4. ARM64 升级包在原生 runner 构建,避免 x64 QEMU 下的归档异常
|
||||
5. 升级前备份完整数据,并在目标设备验证脚本、依赖和通知
|
||||
1. Alpine 与 Debian 默认镜像统一使用 Node.js 24,SQLite 依赖升级至 1.1.4
|
||||
2. 新增 Node.js 原生部署的 Python venv 隔离支持,以及 Python Git URL、requirements、pyproject 依赖源
|
||||
3. 新增通知审计日志,完善通知失败记录与配置提示
|
||||
4. 升级包按平台构建并校验;支持在线暂存、离线安装、失败回退及保留数据迁移
|
||||
5. 修复任务调度、额外规则、依赖安装、脚本发现和订阅同步等问题
|
||||
6. 独立远程 CLI 更新至 0.1.3,最低 Node.js 版本为 22.12.0
|
||||
7. Alpine 保留 arm/v6 和 arm/v7;Debian 暂不提供 arm/v7,Python 3.10 兼容镜像停止更新
|
||||
8. 升级前备份完整数据,并在目标设备验证脚本、依赖、订阅和通知
|
||||
|
||||
Reference in New Issue
Block a user