ci: add validation workflows and project tooling

This commit is contained in:
DiheChen committed 2026-10-30 01:34:50 +08:00
1 parent c41b1d350a
commit 8794686c3a
4 files changed
+367

No files matched your search

+101
View File
@@ -0,0 +1,101 @@
name: CI
on:
push:
branches: [master]
tags: ['v*']
pull_request:
workflow_dispatch:
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
permissions:
contents: read
jobs:
go:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
- run: go run github.com/sqlc-dev/sqlc/cmd/sqlc@v1.31.1 diff
- run: go vet ./...
- run: go run ./cmd/protocolgen -check
- run: sudo apt-get update && sudo apt-get install --yes --no-install-recommends ffmpeg
timeout-minutes: 30
- run: go test -race ./...
- uses: golangci/golangci-lint-action@v9
with:
version: v2.15.0
web:
runs-on: ubuntu-latest
defaults:
run:
working-directory: web
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
cache-dependency-path: web/package-lock.json
- run: npm ci
- run: npm run type-check
- run: npm run lint
- run: npm test
- run: npm run build
images:
needs: [go, web]
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
strategy:
matrix:
include:
- image: server
file: Dockerfile
target: alisten
- image: worker
file: worker.Dockerfile
target: alisten-worker
- image: web
file: web/Dockerfile
target: ""
steps:
- uses: actions/checkout@v7
- uses: docker/setup-qemu-action@v4
- uses: docker/setup-buildx-action@v4
- uses: docker/login-action@v4
if: github.event_name != 'pull_request'
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: meta
uses: docker/metadata-action@v6
with:
images: ghcr.io/${{ github.repository }}/${{ matrix.image }}
flavor: latest=false
tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=semver,pattern={{version}}
type=sha,prefix=
- uses: docker/build-push-action@v7
with:
context: .
file: ${{ matrix.file }}
target: ${{ matrix.target }}
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
build-args: VERSION=${{ github.sha }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha,scope=${{ matrix.image }}
cache-to: type=gha,mode=max,scope=${{ matrix.image }}
+4
View File
@@ -0,0 +1,4 @@
/alisten
/web/node_modules/
/web/dist/
.env
+203
View File
@@ -0,0 +1,203 @@
version: "2"
linters:
default: standard
enable:
- depguard
- forbidigo
exclusions:
rules:
# 这两个文件中未检查的错误不影响结果:decode.go 跳过 fmt 块的对齐填充失败时,读取下一个块头会报错;loudness_bench_test.go 的 binary.Write 写入 bytes.Buffer,不会失败。
- path: internal/lib/wav/decode\.go|internal/lib/loudness/loudness_bench_test\.go
linters:
- errcheck
# forbidigo 只检查 domain,其他目录可以读系统时钟。
- path-except: ^internal/domain/
linters:
- forbidigo
settings:
depguard:
# 不以 $ 结尾的条目按前缀匹配。depguard 只拿排序后紧挨在导入路径之前的一项比较,已有前缀的子包不要再单列:
# deny 中同时有 os 和 os/exec 时,os/signal 就不再被禁止。
rules:
domain:
list-mode: strict
files:
- "**/internal/domain/**"
- "!$test"
allow:
- $gostd
- github.com/dihemoe/alisten/internal/domain
- github.com/dihemoe/alisten/internal/lib
deny:
- pkg: os
desc: domain 不做 I/O
- pkg: net$
desc: domain 不做 I/O
- pkg: net/http
desc: domain 不做 I/O
- pkg: database/sql
desc: domain 不做 I/O
- pkg: io/fs
desc: domain 不做 I/O
- pkg: path/filepath
desc: domain 不做 I/O
- pkg: syscall
desc: domain 不做 I/O
- pkg: net/textproto
desc: domain 不做 I/O
- pkg: net/smtp
desc: domain 不做 I/O
- pkg: net/rpc
desc: domain 不做 I/O
- pkg: crypto/tls
desc: domain 不做 I/O
- pkg: log/syslog
desc: domain 不做 I/O
- pkg: plugin
desc: domain 不做 I/O
- pkg: sync
desc: domain 不加锁
domain-test:
list-mode: strict
files:
- "**/internal/domain/**/*_test.go"
allow:
- $gostd
- github.com/dihemoe/alisten/internal/domain
- github.com/dihemoe/alisten/internal/lib
- golang.org/x/tools/txtar
app:
list-mode: strict
files:
- "**/internal/app/**"
- "!$test"
allow:
- $gostd
- github.com/dihemoe/alisten/internal/domain
- github.com/dihemoe/alisten/internal/lib
- github.com/dihemoe/alisten/internal/app
# 用于房间密码的 argon2id 哈希;纯计算,不做 I/O。
- golang.org/x/crypto/argon2
# 用于合并同一首歌的并发播放地址解析;只协调 goroutine,不做 I/O。
- golang.org/x/sync/singleflight
deny:
- pkg: os
desc: app 的 I/O 经由出站端口
- pkg: net$
desc: app 的 I/O 经由出站端口
- pkg: net/http
desc: app 的 I/O 经由出站端口
- pkg: database/sql
desc: app 的 I/O 经由出站端口
- pkg: io/fs
desc: app 的 I/O 经由出站端口
- pkg: path/filepath
desc: app 的 I/O 经由出站端口
- pkg: syscall
desc: app 的 I/O 经由出站端口
- pkg: net/textproto
desc: app 的 I/O 经由出站端口
- pkg: net/smtp
desc: app 的 I/O 经由出站端口
- pkg: net/rpc
desc: app 的 I/O 经由出站端口
- pkg: crypto/tls
desc: app 的 I/O 经由出站端口
- pkg: log/syslog
desc: app 的 I/O 经由出站端口
- pkg: plugin
desc: app 的 I/O 经由出站端口
app-test:
list-mode: strict
files:
- "**/internal/app/**/*_test.go"
allow:
- $gostd
- github.com/dihemoe/alisten/internal/domain
- github.com/dihemoe/alisten/internal/lib
- github.com/dihemoe/alisten/internal/app
- golang.org/x/tools/txtar
porttest:
files:
- "!$test"
deny:
- pkg: github.com/dihemoe/alisten/internal/app/port/porttest
desc: 端口的内存实现只供测试使用
lib:
files:
- "**/internal/lib/**"
deny:
- pkg: github.com/dihemoe/alisten/internal/domain
desc: lib 与业务无关,不能依赖 domain
- pkg: github.com/dihemoe/alisten/internal/app
desc: lib 与业务无关,不能依赖 app
- pkg: github.com/dihemoe/alisten/internal/adapter
desc: lib 与业务无关,不能依赖 adapter
- pkg: github.com/dihemoe/alisten/internal/bootstrap
desc: lib 与业务无关,不能依赖 bootstrap
- pkg: github.com/dihemoe/alisten/protocol
desc: protocol 只能被 internal/adapter/in 使用
# app 通过 logctx 记录日志的关联字段;logctx 只依赖标准库,app 才不会经由它依赖 OpenTelemetry。
logctx:
list-mode: strict
files:
- "**/internal/lib/logctx/**"
allow:
- $gostd
adapter-in:
files:
- "**/internal/adapter/in/**"
deny:
- pkg: github.com/dihemoe/alisten/internal/adapter
desc: 只有 bootstrap 可以 import adapter
- pkg: github.com/dihemoe/alisten/internal/bootstrap
desc: adapter 不能依赖 bootstrap
adapter-out:
list-mode: lax
files:
- "**/internal/adapter/out/**"
# sqlc 把查询代码生成在 postgres 适配器的 internal 目录下;lax 模式让这个更长的 allow 前缀优先于 deny。
allow:
- github.com/dihemoe/alisten/internal/adapter/out/postgres/internal/query
deny:
- pkg: github.com/dihemoe/alisten/internal/adapter
desc: 只有 bootstrap 可以 import adapter
- pkg: github.com/dihemoe/alisten/internal/bootstrap
desc: adapter 不能依赖 bootstrap
- pkg: github.com/dihemoe/alisten/protocol
desc: protocol 只能被 internal/adapter/in 使用
bootstrap:
files:
- "**/internal/bootstrap/**"
deny:
- pkg: github.com/dihemoe/alisten/protocol
desc: protocol 只能被 internal/adapter/in 使用
cmd:
files:
- "**/cmd/**"
deny:
- pkg: github.com/dihemoe/alisten/internal/adapter
desc: 只有 bootstrap 可以 import adapter
- pkg: github.com/dihemoe/alisten/protocol
desc: protocol 只能被 internal/adapter/in 使用
protocol:
list-mode: strict
files:
- "**/protocol/**"
allow:
- $gostd
forbidigo:
analyze-types: true
forbid:
- pattern: ^time\.(Now|Since|Until)$
msg: domain 不读系统时钟,时间作为输入传入
formatters:
enable:
- gofmt
- goimports
settings:
goimports:
local-prefixes:
- github.com/dihemoe/alisten
+59
View File
@@ -0,0 +1,59 @@
import antfu from '@antfu/eslint-config'
// 一个文件匹配到多个设置 no-restricted-imports 的配置对象时,只有最后一个生效:features 因此要带上这一条;core 和 shared 禁止引用 features 的任何路径,已经包含它。
const featureInternals = {
pattern: /^(?:@\/|\.\.?\/(?:.*\/)?)features\/[^/]+\/[^/]/i,
message: '功能之间只能通过对方的 index.ts 引用。',
}
const appImports = {
pattern: /^(?:@\/|\.\.\/(?:.*\/)?)app(?:\/|$)/i,
message: 'features、core 和 shared 不能依赖 app。',
}
// no-restricted-imports 不检查 import(),动态导入由 no-restricted-syntax 用同一个正则检查。
// no-restricted-syntax 同样只有最后一个配置对象生效,所以要带上 @antfu/eslint-config 设置的两项,升级 antfu 时要与它保持一致。
function restrictImports(...restrictions) {
return {
'no-restricted-imports': ['error', { patterns: restrictions.map(({ pattern, message }) => ({ regex: pattern.source, message })) }],
'no-restricted-syntax': ['error', 'TSEnumDeclaration[const=true]', 'TSExportAssignment', ...restrictions.map(({ pattern, message }) => ({ selector: `ImportExpression[source.value=${pattern}]`, message }))],
}
}
export default antfu(
{
react: true,
// lint:fix 会改写生成物,之后 protocolgen -check 就会失败。
ignores: ['src/**/*.gen.ts'],
},
{
files: ['src/**/*.{ts,tsx}'],
rules: restrictImports(featureInternals),
},
{
files: ['src/features/**/*.{ts,tsx}'],
rules: restrictImports(featureInternals, {
pattern: /^\.\.\//,
message: '功能内部使用相对路径,功能之外的模块用 @/ 引用。',
}, appImports),
},
{
files: ['src/core/**/*.{ts,tsx}'],
rules: restrictImports({
pattern: /^(?:@\/|\.\.\/(?:.*\/)?)features(?:\/|$)/i,
message: 'core 不能依赖 features。',
}, appImports),
},
{
files: ['src/shared/**/*.{ts,tsx}'],
rules: restrictImports({
pattern: /^(?:@\/|\.\.\/(?:.*\/)?)(?:core|features)(?:\/|$)/i,
message: 'shared 不能依赖 core 和 features。',
}, appImports),
},
{
// @antfu/eslint-config 对声明文件关闭了 no-restricted-syntax,上面的配置对象会把它重新打开。
files: ['**/*.d.?([cm])ts'],
rules: { 'no-restricted-syntax': 'off' },
},
)