Laminar 1.4 配置参数详解

Laminar 1.4 配置体系逐项实测:服务端 7 个环境变量、job 级 TIMEOUT/DESCRIPTION/CONTEXTS、三层环境变量文件优先级、contexts 并发模型与隐式 default 消失陷阱、全局钩子与分组,附参数速查表与变更检查清单。

LaminarLaminar持续集成CIC++自动化运维2026/10/27

Laminar 1.4 配置参数详解

目标:把 Laminar 1.4 的配置体系拆成「服务端变量、job 级配置、contexts 并发模型、全局钩子」四层,每一层都给出真实服务器上的修改命令与生效验证。文中 11 组参数/变量均在 Ubuntu 24.04.5 + Laminar 1.4 上实测,回显照抄原始输出。

一、环境信息

项目实测值
操作系统Ubuntu 24.04.5 LTS
内核7.0.0-38-generic
主机名ci.example.com
Laminar 版本1.4-1~upstream-ubuntu2404
服务端配置文件/etc/laminar.conf
数据根目录/var/lib/laminar
运行账户laminar(uid=995)
Web*:8080
RPC实测期间改为 unix:/var/lib/laminar/laminar.sock

所有配置修改都通过 systemctl restart laminar 或热加载生效,验证手段统一为三种:curl 抓 HTTP/SSE 回显、laminarc 看队列、journalctl 看调度日志。

二、配置体系总览

Laminar 的配置分散在四个层次,改之前必须先判断改哪一层,否则会出现「改了没效果」或「重启把队列弄丢」:

层级位置生效方式影响范围
服务端变量/etc/laminar.confsystemctl restart laminar全局,含监听地址、标题、归档地址
job 配置/var/lib/laminar/cfg/jobs/$JOB.conf热加载,下次调度读取单个 job(超时、描述、上下文)
job 环境cfg/jobs/$JOB.env、cfg/env、cfg/contexts/$C.env下次 run 读取单 job / 全局 / 单上下文
全局钩子cfg/before、cfg/after、cfg/scripts/、cfg/groups.conf热加载所有 job

热加载是真实存在的,新增 job 文件后日志里会立刻出现:

laminar.cpp:133: info: Reloading configuration

但 /etc/laminar.conf 不在热加载范围内——它是 systemd 的 EnvironmentFile,只读一次,必须重启进程。

三、服务端变量实测

3.1 LAMINAR_HOME

数据根目录,默认 /var/lib/laminar,配置模板里是注释状态:

#LAMINAR_HOME=/var/lib/laminar

它是所有路径的基准:cfg/(配置)、run/(运行目录与 workspace)、archive/(归档)、laminar.sqlite(历史库)都在它下面。修改它意味着同时迁移全部数据并保证 laminar 用户对新目录有读写权限,否则服务启动后无法创建 run 目录,job 会直接失败。没有特殊需求时保持默认。

3.2 LAMINAR_BIND_HTTP

控制 Web 监听地址,默认 *:8080,支持三种形式:

形式示例说明
通配地址*:8080监听所有网卡
指定网卡127.0.0.1:8136只允许本机或反向代理访问
unix socketunix:/path/web.sock交给前置 Web 服务器

实测把它改成只监听本机 8136:

$ sudo sed -i 's|^LAMINAR_BIND_HTTP=.*|LAMINAR_BIND_HTTP=127.0.0.1:8136|' /etc/laminar.conf
$ sudo systemctl restart laminar

$ curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8136/
200

$ curl -sS -o /dev/null -w "%{http_code}\n" --max-time 3 http://127.0.0.1:8080/
curl: (7) Failed to connect to 127.0.0.1 port 8080 after 0 ms: Couldn't connect to server
000

$ LAMINAR_HOST=unix:/var/lib/laminar/laminar.sock laminarc show-jobs
env-demo
fail

关键结论:HTTP 与 RPC 是两个独立端点。换掉 HTTP 端口不会影响 laminarc,因为客户端走的是 RPC;反过来改 RPC 也不影响浏览器。做端口收敛时两处要分别考虑。

3.3 LAMINAR_BIND_RPC

laminarc 连接守护进程的通道,默认 unix-abstract:laminar(抽象命名空间 socket,不落文件)。改成文件 socket 更便于用文件权限做访问控制:

$ sudo sed -i 's|^LAMINAR_BIND_RPC=.*|LAMINAR_BIND_RPC=unix:/var/lib/laminar/laminar.sock|' /etc/laminar.conf
$ sudo systemctl restart laminar

$ ls -l /var/lib/laminar/laminar.sock
srw-rw---- 1 laminar laminar 0 Oct 27 15:12 /var/lib/laminar/laminar.sock

srw-rw---- 表示只有 laminar 用户和 laminar 组能连接——这就是 Laminar 的「鉴权」:把运维账号加入 laminar 组即可,无需令牌。客户端通过环境变量指向 socket:

$ LAMINAR_HOST=unix:/var/lib/laminar/laminar.sock laminarc show-jobs
env-demo
fail

如果客户端仍按默认地址连接,会直接崩溃(这是 1.4 的真实行为,不是脚本问题):

$ laminarc show-jobs
exit=134
terminate called after throwing an instance of 'kj::ExceptionImpl'
  what():  kj/async-io-unix.c++:1517: disconnected: connect(): Connection refused; address = unix-abstract:laminar

反过来,socket 路径选错会让守护进程起不来。实测把路径指向 /var/run/laminar.sock:

$ sudo sed -i 's|^LAMINAR_BIND_RPC=.*|LAMINAR_BIND_RPC=unix:/var/run/laminar.sock|' /etc/laminar.conf
$ sudo systemctl restart laminar
$ systemctl is-active laminar
failed

$ journalctl -u laminar --no-pager | tail -4
Oct 27 15:12:59 ci.example.com laminard[3880]: fatal: ::bind(sockfd, &addr.generic, addrlen): Permission denied; toString() = unix:/var/run/laminar.sock
Oct 27 15:12:59 ci.example.com laminard[3880]: laminard version 1.4-1~upstream-ubuntu2404 started
Oct 27 15:12:59 ci.example.com systemd[1]: laminar.service: Main process exited, code=exited, status=1/FAILURE
Oct 27 15:12:59 ci.example.com systemd[1]: laminar.service: Failed with result 'exit-code'.

原因很朴素:/run 归 root 所有,laminar 用户无权在其中创建 socket。要么把 socket 放在 /var/lib/laminar 下(推荐),要么预建 /run/laminar 并 chown laminar:laminar。

3.4 LAMINAR_TITLE

Web 页面标题,默认空。实测值:

LAMINAR_TITLE=Laminar CI - Build Farm

验证方式是抓 SSE 首帧(前端标题由 SSE 下发):

$ curl -s -N --max-time 2 -H "Accept: text/event-stream" http://127.0.0.1:8080/ | grep -o '"title":"[^"]*"'
"title":"Laminar CI - Build Farm"

这个标题同时出现在首页、Jobs 页和 Wallboard,是给大屏看板做标识最省事的一个变量。

3.5 LAMINAR_KEEP_RUNDIRS

默认 0,表示 run 目录(run/$JOB/$RUN,即脚本的工作目录)在构建结束后立即删除;设为 N 则保留最近 N 个。实测设为 2 后连续跑三次 inspect:

$ laminarc run inspect
inspect:8
$ laminarc run inspect
inspect:9
$ laminarc run inspect
inspect:10

$ ls -1 /var/lib/laminar/run/inspect
10
9
workspace

注意三点:

  • 保留的是最新的 N 个,编号 8 的目录已被清理;
  • workspace 不属于 rundir,永远保留,不受该变量影响;
  • 该变量与 archive/ 无关——归档从不清除,所以「保留现场」的正确做法是保留 rundir,而不是依赖 archive 里的产物。

对照默认值 0 的表现:跑完一次 first-job 后 run/first-job 下只剩 workspace,构建号目录已消失。排障时建议至少设成 1,否则失败现场的中间文件会一起被删掉。

3.6 LAMINAR_BASE_URL

当 Laminar 被反向代理挂在子路径(如 https://ci.example.com/ci/)时使用,影响前端 <base href>:

$ sudo sed -i '/^LAMINAR_BASE_URL=/d' /etc/laminar.conf
$ echo 'LAMINAR_BASE_URL=/ci/' | sudo tee -a /etc/laminar.conf
$ sudo systemctl restart laminar

$ curl -s --compressed http://127.0.0.1:8080/ | grep -o 'base href="[^"]*"'
base href="/ci/"

$ curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8080/ci/js/app.js
404
$ curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8080/js/app.js
200

这是一个必须理解清楚的半成品能力:它只改写 <base href>,不会真的把静态资源挂到 /ci/ 下。浏览器会先请求 /ci/(由反向代理转发),再按 base 标签请求 /ci/js/app.js——如果代理只做了前缀转发而不改写路径,就会 404。正确做法是在反向代理上把 /ci/ 前缀剥掉后再转给 Laminar 的根路径。

3.7 LAMINAR_ARCHIVE_URL

默认不设置,由 laminard 自己提供 /archive/...(官方手册明确指出这种方式「很朴素、效率低」,建议交给真正的 Web 服务器)。设置后,前端生成的产物链接会指向该地址:

LAMINAR_ARCHIVE_URL=http://ci.example.com/archive/

实测抓某次构建的 SSE 详情,产物链接已替换:

$ curl -s -N --max-time 2 -H "Accept: text/event-stream" http://127.0.0.1:8080/jobs/hello/8 | grep -o '"artifacts":\[[^]]*\]'
"artifacts":[{"url":"http://ci.example.com/archive/hello/8/build-time.txt","filename":"build-time.txt","size":21}]

该变量只影响链接前缀,不做任何上传或同步。你需要自己把 /var/lib/laminar/archive 通过 Nginx/Apache 暴露出来,并保证浏览器能访问到配置里的地址。

四、job 级配置

4.1 $JOB.conf:超时、描述、上下文

每个 job 可在 cfg/jobs/$JOB.conf 里声明三个键。实测文件:

TIMEOUT=5
DESCRIPTION=Demo job that exceeds its configured timeout

TIMEOUT 的单位是秒,超时后 Laminar 会终止整个进程组并把结果记为 aborted:

$ laminarc run timeout-demo
timeout-demo:2
$ echo $?
3

$ curl -s http://127.0.0.1:8080/log/timeout-demo/latest | tail -5
+ echo start
start
+ sleep 30
[laminar] Executing cfg/after
[global after] job=timeout-demo run=2 result=aborted last=aborted context=default

三个细节值得记住:

  • 客户端退出码是 3,与普通失败(也是 3)相同,但结果字段是 aborted 而不是 failed;
  • 超时终止的是整个进程组,脚本里 fork 出去的子进程会一起被回收;
  • cfg/after 仍会执行,并能通过 $RESULT=aborted 判断这是超时终止。

DESCRIPTION 会出现在 Job 详情页,实测 SSE:

$ curl -s -N --max-time 2 -H "Accept: text/event-stream" http://127.0.0.1:8080/jobs/timeout-demo | grep -o '"description":"[^"]*"'
"description":"Demo job that exceeds its configured timeout"

CONTEXTS 用于把 job 绑定到指定并发上下文(见第五节):

CONTEXTS=single
DESCRIPTION=Reads the per-context environment file

.conf 是热加载的,改完不需要重启,下一次调度即生效。

4.2 环境变量文件:三层来源与优先级

环境变量有三层文件来源,加载顺序固定:

  1. cfg/env —— 全局,所有 job 可见;
  2. cfg/contexts/$CONTEXT.env —— 该上下文内的所有 job 可见;
  3. cfg/jobs/$JOB.env —— 仅该 job 可见。

后加载的会覆盖先加载的(内部用 overwrite=true 写入)。实测文件内容:

# cfg/env
GLOBAL_FLAG=on

# cfg/contexts/single.env
DUT_IP=192.168.0.123

# cfg/jobs/env-demo.env
QUOTED="a b"
PLAIN=value

用一个 job 打印它们,同时通过命令行传入同名参数:

$ laminarc run env-demo QUOTED=cli PLAIN=cli JOB_FLAG=off
env-demo:1

$ curl -s http://127.0.0.1:8080/log/env-demo/latest
+ echo 'QUOTED=["a b"]'
QUOTED=["a b"]
+ echo 'PLAIN=[value]'
PLAIN=[value]
+ echo 'GLOBAL_FLAG=[on]'
GLOBAL_FLAG=[on]
+ echo 'JOB_FLAG=[off]'
JOB_FLAG=[off]

从这四行能读出两条重要规则:

  • 引号不会被剥离:QUOTED="a b" 的值是带引号的 "a b",不是 a b。这些文件不是 shell,不做展开,写什么就是什么;
  • 命令行参数不覆盖环境文件:PLAIN=cli 被 PLAIN=value 顶掉了。参数只用于填充「文件里没有定义」的变量,所以想让参数生效,就别在同名环境文件里再定义一次。

上下文环境文件的效果在另一个 job 上直观可见:

$ curl -s http://127.0.0.1:8080/log/single-env/latest
[laminar] Executing cfg/before
[global before] time=15:14:11 job=single-env
[laminar] Executing cfg/jobs/single-env.run
CONTEXT=single DUT_IP=192.168.0.123
[laminar] Executing cfg/after
[global after] job=single-env run=2 result=success last=success context=single

4.3 .init / .before / .after:执行时机

$JOB.init 只在 workspace 首次被创建时执行一次,适合放 git clone 或依赖初始化。实测建一个 init-demo:

# cfg/jobs/init-demo.init
#!/bin/bash -ex
echo "workspace created at $(date -u +%T)"
echo ready > "$WORKSPACE/state"

# cfg/jobs/init-demo.run
#!/bin/bash -ex
cat "$WORKSPACE/state"

第一次运行的日志:

$ curl -s http://127.0.0.1:8080/log/init-demo/1 | head -4
[laminar] Executing cfg/jobs/init-demo.init
++ date -u +%T
+ echo 'workspace created at 15:14:24'
workspace created at 15:14:24

第二次运行的日志里不再有 init,直接从 before 开始:

[laminar] Executing cfg/before
[global before] time=15:14:24 job=init-demo
[laminar] Executing cfg/jobs/init-demo.run
+ cat /var/lib/laminar/run/init-demo/workspace/state
ready
[laminar] Executing cfg/after
[global after] job=init-demo run=2 result=success last=success context=default

$JOB.before / $JOB.after 每次 run 都执行,顺序是:

init(仅首次) → cfg/before → $JOB.before → $JOB.run → $JOB.after → cfg/after

after 类脚本在失败或超时时也会执行,且能读到 $RESULT,适合做通知与清理。

五、Contexts 并发模型

5.1 语法

cfg/contexts/ 下每个文件就是一个上下文,文件名即上下文名:

# cfg/contexts/default.conf
EXECUTORS=4
JOBS=hello,inspect,fail,setdemo

# cfg/contexts/single.conf
EXECUTORS=1
JOBS=slow
  • EXECUTORS:该上下文允许同时运行几个 job;
  • JOBS:用逗号分隔的 job 名列表(支持 glob),决定哪些 job 归属此上下文;
  • job 侧也可以用 $JOB.conf 的 CONTEXTS= 反向声明归属。

5.2 单槽位排队实测

single 上下文只有 1 个执行槽,slow 每条要跑 60 秒。连续入队三次:

$ laminarc queue slow
slow:6
$ laminarc queue slow
slow:7
$ laminarc queue slow
slow:8

$ laminarc show-running
slow:6
$ laminarc show-queued
slow:7
slow:8

一个在跑、两个在等,符合 EXECUTORS=1。中止正在跑的那条:

$ laminarc abort slow 6
$ echo $?
0

$ laminarc show-running
slow:7
$ laminarc show-queued
slow:8

中止后队列立即推进:7 接替 6 开跑,8 继续排队。这说明 Laminar 的调度是「槽位释放即触发」而不是定时轮询。

5.3 隐式 default 上下文的消失(最容易踩的坑)

没有定义任何 context 文件时,Laminar 会隐式创建一个 6 槽位的 default,首次启动日志可见:

laminard[414]: laminar.cpp:560: info: Creating a default context with 6 executors

但只要 cfg/contexts/ 下出现任意 context 文件,这个隐式 default 就不再创建。实测把 default.conf 移走并重启后入队 hello:

$ mv /var/lib/laminar/cfg/contexts/default.conf /tmp/default.conf.off
$ sudo systemctl restart laminar
$ laminarc queue hello
hello:9
$ sleep 2
$ laminarc show-running
$ laminarc show-queued
hello:9

现象是:入队成功、永远不跑、日志没有任何报错。journal 里只有一行无害的入队记录:

laminard[3996]: rpc.cpp:55: info: RPC queue; jobName = hello

原因是未声明 CONTEXTS 的 job 期望归属 default,而 default 已经不存在,匹配不到任何上下文自然没有槽位。恢复 default.conf 并重启即可。

⚠️ 注意:这个坑的代价不止「排队不动」。Laminar 的队列在内存里,重启即丢。上面恢复配置后重启,排队中的 hello:9 直接消失,show-running/show-queued 均为空,log/hello/latest 返回 Not Found。所以排查此类问题时,先确认没有重要任务在排队再重启。

六、全局钩子、脚本目录与分组

6.1 cfg/before 与 cfg/after

两个文件对所有 job 生效,实测内容:

# cfg/before
#!/bin/bash
echo "[global before] time=$(date -u +%T) job=$JOB"

# cfg/after
#!/bin/bash
echo "[global after] job=$JOB run=$RUN result=$RESULT last=$LAST_RESULT context=$CONTEXT"

任意 job 的日志里都能看到它们:

[laminar] Executing cfg/before
[global before] time=15:15:02 job=env-demo
...
[laminar] Executing cfg/after
[global after] job=env-demo run=1 result=success last=unknown context=default

$LAST_RESULT 是该 job 上一次的结果,首次运行时为 unknown——这是判断「连续失败」的现成信号。超时中止时 after 仍会执行,$RESULT 为 aborted。

6.2 cfg/scripts:自动加入 PATH

cfg/scripts/ 目录会被前置到 PATH,里面的可执行脚本可以直接当命令调用。实测放一个:

# cfg/scripts/report-tool
#!/bin/bash
echo "report-tool invoked with: $*"

在 job 里直接调用:

+ report-tool alpha beta
report-tool invoked with: alpha beta

这一层适合放团队内部的构建辅助工具,避免在每个 job 里写长路径。

6.3 groups.conf:Web 端分组

cfg/groups.conf 用「组名=正则」的方式给 Jobs 页签分组:

Builds=^(hello|inspect|fail)$
All=.*

实测 SSE 返回的分组结果:

$ curl -s -N --max-time 2 -H "Accept: text/event-stream" http://127.0.0.1:8080/jobs | grep -o '"groups":{[^}]*}'
"groups":{"All":".*","Builds":"^(hello|inspect|fail)$"}

正则是对 job 名做匹配,.* 表示兜底分组。job 数量上到几十个之后,这个文件能显著改善看板可读性。

七、参数速查表

参数 / 文件默认值实测取值生效方式用途
LAMINAR_HOME/var/lib/laminar默认重启数据根目录
LAMINAR_BIND_HTTP*:8080127.0.0.1:8136重启Web 监听
LAMINAR_BIND_RPCunix-abstract:laminarunix:/var/lib/laminar/laminar.sock重启CLI 通道
LAMINAR_TITLE空Laminar CI - Build Farm重启页面/大屏标题
LAMINAR_KEEP_RUNDIRS02重启保留最近 N 个 run 目录
LAMINAR_BASE_URL未设置/ci/重启子路径部署的 base 标签
LAMINAR_ARCHIVE_URL未设置http://ci.example.com/archive/重启产物链接前缀
$JOB.conf: TIMEOUT无限制5热加载单 job 超时(结果 aborted)
$JOB.conf: DESCRIPTION空见上文热加载Job 详情页描述
$JOB.conf: CONTEXTS空single热加载绑定上下文
contexts/$C.conf: EXECUTORS6(仅隐式 default)4 / 1热加载并发槽位
contexts/$C.conf: JOBS空hello,inspect,...热加载上下文归属
cfg/env、$JOB.env、contexts/$C.env无见上文下次 run环境变量三层来源
cfg/before、cfg/after无见上文热加载全局前后置
cfg/scripts/无report-tool热加载加入 PATH 的工具
cfg/groups.conf无Builds=...热加载Web 分组

八、配置变更检查清单

改完配置后按这个顺序自查,能挡住绝大多数「改了不生效」:

  1. 改的是哪一层? /etc/laminar.conf 必须 systemctl restart laminar;cfg/ 下的文件热加载。
  2. 重启前有没有排队任务? 队列在内存中,重启即丢,先 laminarc show-queued 确认。
  3. RPC socket 路径的父目录是否可写? 不在 /var/lib/laminar 下时先 chown laminar:laminar,否则 is-active 直接 failed。
  4. contexts 是否留下了没有归属的 job? 只要有 context 文件,就要显式提供 default.conf 或给 job 写 CONTEXTS=。
  5. 环境变量是不是被文件覆盖了? 命令行参数优先级最低,同名变量在 env 文件里存在时参数无效。
  6. 超时与失败是否分清? 超时结果字段是 aborted,退出码同样是 3。
  7. 改了 LAMINAR_BASE_URL 之后反向代理是否正确剥前缀? 只改 base 标签,不移动静态资源。
  8. LAMINAR_KEEP_RUNDIRS 是否真的保留了现场? 只作用于 run/$JOB/$RUN,不影响 archive。

九、参考链接

系列导航: