轻量级开源 CI/CD:Woodpecker 从零部署到跑通完整流水线

Sep 13,2026 linux woodpecker gitea

red-leaves

软件写好了,如何部署?部署之后,又如何持续迭代、频繁发布?这是软件工程绕不开的问题。常见的答案是引入 CI/CD 工具,把测试、构建、部署交给机器自动完成。但流行的 CI/CD 工具各有侧重,能力、性能、适用场景不尽相同,该如何选择?本文以轻量、开源的 Woodpecker 为例,从部署和流水线讲起,看它如何集成测试和部署过程变得简单和优雅。

最知名的开源 CI/CD 工具非 Jenkins 莫属,功能强大、插件丰富,但对小团队或者个人 Homelab 来说偏重;GitLab CI 也很成熟,但前提是整套流程都构建在 GitLab 上;GitHub Actions 体验一流,但它不开源,构建数据都在第三方手里。

如果你的需求与我的类似——自托管、开源、轻量,并且已经在使用 Gitea 这类自建代码托管平台——Woodpecker CI 是一个值得考虑的选择。它的理念与用法和 GitHub Actions 有不少相通之处:

  • 容器原生:流水线的每一步都跑在一个独立容器里,环境干净,用完即弃;
  • 语法简单:一个 YAML 文件就能描述构建、测试、发布,没有太多概念负担;
  • 架构简单:一个 Server 负责界面与调度,一个或多个 Agent 负责执行任务,用 Docker Compose 就能快速拉起一套;

同时,Woodpecker 完全开源、支持多种代码托管平台、不与特定厂商绑定

本文介绍如何在 Linux 服务器上部署 Gitea 与 Woodpecker,编写第一条 Hello World 流水线,并以一个 Python 项目为例,完整演示“提交 → 测试 → PR → 构建镜像 → 自动部署”的流程。

Woodpecker 官网:https://woodpecker-ci.org/,有丰富的文档可供参考。

起源

Woodpecker 起源于开源项目 Drone。我最初使用的也是 Drone,后来才迁移到 Woodpecker。早期 Woodpecker 曾兼容 .drone.yml,如今已完全独立演进,使用自己的 .woodpecker.yaml 配置体系。

2019 年,Drone 在 0.8 版本之后将许可证从 Apache 2.0 改为私有协议,随后被 Harness 收购,核心版本转向付费用户,开源版本更新放缓。社区开发者 laszlocph 于 2019 年 4 月 3 日基于最后一个 Apache 2.0 授权的 Drone 0.8 代码库创建分叉,三天后发布第一个版本;同年 8 月 27 日,项目更名为 Woodpecker(啄木鸟)

此后项目由社区维护至今,始终保持 Apache 2.0 协议,主仓库位于 GitHub(https://github.com/woodpecker-ci/woodpecker)。2025 年初发布 3.0 版本,目前演进到 3.18,迭代活跃。

架构一览

Woodpecker 使用 Golang 编写,轻量高效,架构也很简单,主要有两个角色:Server 和 Agent:

  • Server:对外提供 Web 界面和 API(默认 8000 端口),管理用户、仓库与流水线记录,默认使用内置 SQLite 存储数据;
  • Agent:真正干活的执行器,通过 gRPC(默认 9000 端口)连接 Server 领任务,为流水线的每一步在本机启动临时 Docker 容器执行。

一个流水线内部的层级是:Pipeline(一次流水线执行)→ Workflow(一个 YAML 文件)→ Step(文件里的一个步骤)。每个步骤指定一个镜像(image)和一组命令(commands)或设置(settings),步骤之间共享同一个工作区(源码在工作区里,上一步的构建产物下一步也能用),按定义顺序串行执行。

Woodpecker 的流水线使用 YAML 格式配置,整体较为清晰易读,语法参考:https://woodpecker-ci.org/docs/usage/workflow-syntax

另外,Woodpecker 通常搭配私有的 SCM(Source Code Management,即代码托管平台)使用,支持 GitHub、Gitea、GitLab 等常用平台。Woodpecker 没有自己的用户认证系统,直接复用 SCM 提供的用户认证。

一个典型的任务流程是:

  • 事先,将 Woodpecker 接入 SCM,并将仓库激活到 Woodpecker 中,仓库中包含 .woodpecker 目录和相应的流水线文件;
  • 用户推送代码到 SCM 时,触发 SCM 的 Webhook,调用 Woodpecker Server 开始运行;
  • Woodpecker Server 调度任务给 Woodpecker Agent,Agent 拉取代码,依次执行 workflow 和 step 里定义的步骤。

部署

本文基于一下版本测试

  • Woodpecker 3.18.0
  • Gitea 为 1.27.x

我们的目标拓扑是:一台服务器上,用一份 docker-compose.yml 同时运行 Gitea、Woodpecker Server、Woodpecker Agent,三者处于同一个 Docker 网络。

准备工作

确认 Docker 环境:

$ docker version          # 29.8.0
$ docker compose version  # v5.5.1

编写 compose 文件

由于 Gitea、Woodpecker Server、Agent 都部署在同一台机器上,为了让服务间通信和浏览器访问都简单可靠,本文统一用机器的真实 IP 地址加端口的方式访问,示例中使用 192.168.1.10 这个本机 IP。

先创建 .env 文件,把所有敏感配置集中放在这里(.env 不要提交到任何仓库):

# .env
# Woodpecker 对外访问地址(注意:不要以 / 结尾)
WOODPECKER_HOST=http://192.168.1.10:8000

# Gitea OAuth 应用的凭证(创建 OAuth 应用后回填)
WOODPECKER_GITEA_CLIENT=待回填
WOODPECKER_GITEA_SECRET=待回填

# Server 与 Agent 之间的通信密钥,随机生成即可
#   生成命令:openssl rand -hex 32
WOODPECKER_AGENT_SECRET=换成你的随机字符串

然后编写 docker-compose.yml

services:
  gitea:
    image: gitea/gitea:1.27
    restart: always
    ports:
      - "3000:3000"
    volumes:
      - ./gitea-data:/data
    environment:
      # 对外访问地址,影响页面跳转、clone 地址和 webhook 回调
      GITEA__server__ROOT_URL: http://192.168.1.10:3000/
      GITEA__webhook__ALLOWED_HOST_LIST: external,loopback,private

  woodpecker-server:
    image: woodpeckerci/woodpecker-server:v3
    restart: always
    depends_on:
      - gitea
    ports:
      - "8000:8000"
    volumes:
      - ./woodpecker-server-data:/var/lib/woodpecker
    environment:
      - WOODPECKER_OPEN=true            # 允许 Gitea 用户注册登录
      - WOODPECKER_ADMIN=gitadmin       # 该 Gitea 用户将成为 Woodpecker 管理员
      - WOODPECKER_HOST=${WOODPECKER_HOST}
      - WOODPECKER_GITEA=true
      - WOODPECKER_GITEA_URL=http://192.168.1.10:3000
      - WOODPECKER_GITEA_CLIENT=${WOODPECKER_GITEA_CLIENT}
      - WOODPECKER_GITEA_SECRET=${WOODPECKER_GITEA_SECRET}
      # ---- Agent 通信密钥 ----
      - WOODPECKER_AGENT_SECRET=${WOODPECKER_AGENT_SECRET}

  woodpecker-agent:
    image: woodpeckerci/woodpecker-agent:v3
    restart: always
    depends_on:
      - woodpecker-server
    command: agent
    volumes:
      - ./woodpecker-agent-config:/etc/woodpecker
      - /var/run/docker.sock:/var/run/docker.sock   # 用宿主机 Docker 跑步骤容器
    environment:
      - WOODPECKER_SERVER=woodpecker-server:9000
      - WOODPECKER_AGENT_SECRET=${WOODPECKER_AGENT_SECRET}
      - WOODPECKER_MAX_WORKFLOWS=5    # 本 Agent 能同时运行的最大 workflow 数量

几个关键点解释一下:

  • WOODPECKER_HOST:Server 的对外地址,浏览器访问、OAuth 回跳、Agent 连接(跨机时)都依赖它,必须与浏览器实际访问的地址完全一致
  • WOODPECKER_GITEA_URL:Gitea 的地址,Server 用它调用 Gitea API、发起 OAuth 跳转;
  • WOODPECKER_AGENT_SECRET:Server 和 Agent 之间的“暗号”(共享密钥),两边必须一致;
  • WOODPECKER_GITEA_CLIENT / WOODPECKER_GITEA_SECRET:在 Gitea 中创建的 OAuth 凭据,Woodpecker 用它完成认证;
  • WOODPECKER_MAX_WORKFLOWS:设置 Agent 能同时运行的最大 workflow 数量。

Gitea 和 Woodpecker 都支持 MySQL 和 PostgreSQL,这里简化起见不指定数据库类型,默认使用 SQLite。生产环境建议换用具备高可用能力的数据库服务,并做好备份。

启动 Gitea 并创建 OAuth 应用

Gitea 的 OAuth 凭证需要先创建,所以我们分两步启动:

$ docker compose up -d gitea

第一步:初始化 Gitea。 浏览器打开 http://192.168.1.10:3000,进入 Gitea 的安装页面:

  • 数据库保持默认的 SQLite3(演示环境够用,生产环境可更换为 MySQL);
  • 其余保持默认,滚动到页面下方“管理员账号设置”,创建管理员:用户名 gitadmin,设置一个强密码;
  • 点击“立即安装”。

第二步:创建 OAuth 应用。gitadmin 登录 Gitea,右上角头像 → 设置(Settings)→ 应用(Applications)→ 管理 OAuth2 应用 → 创建新的 OAuth2 应用程序

  • 应用名称:Woodpecker
  • 重定向 URI(Redirect URI):http://192.168.1.10:8000/authorize

注意:回调地址必须与 WOODPECKER_HOST + /authorize 完全一致(协议、端口、IP 任一项不同都会导致失败),这是登录失败的常见原因。

点击“创建应用”后,页面会显示 Client IDClient Secret(Secret 仅显示一次,请注意保存),回填到 .env

WOODPECKER_GITEA_CLIENT=刚拿到的 client_id
WOODPECKER_GITEA_SECRET=刚拿到的 client_secret

如果你同时是 Gitea 和 Woodpecker 的管理员,也可以在 Gitea 的右上角头像进入 “站点管理 → 设置 → 应用”,创建系统级 OAuth 应用,对所有用户可见,更适合多人共用的场景。

第三步:启动全部服务。

$ mkdir woodpecker-server-data
$ sudo chown -R 1000:1000 woodpecker-server-data   # Server 容器以 UID 1000 运行,不改属主 SQLite 可能写不进去
$ docker compose up -d
$ docker compose ps    # 确保 3 个服务都正常

登录验证

浏览器打开 http://192.168.1.10:8000 进入 Woodpecker 页面,点击 Login 跳转到 Gitea 的授权页面,授权后回到 Woodpecker 首页。

此时 Gitea 和 Woodpecker 的界面都是全新的,因为还没有项目和代码仓库。

编写流水线

激活仓库

  • 在 Gitea 上创建一个仓库 gitadmin/demo-api(建议勾选“初始化仓库”);
  • 到 Woodpecker 界面右侧点 + (Add Repository) → Enable 激活。

激活时 Woodpecker 会向 Gitea 的仓库写入一个 webhook(因此需要对仓库具有管理员权限),此后 Gitea 上的 push、PR、tag 等事件都会通知 Woodpecker。

Hello World

Woodpecker 的流水线配置放在仓库里。单文件时用根目录的 .woodpecker.yaml;也可以放在 .woodpecker/ 目录下,一个 YAML 文件就是一个独立 workflow(文件名即 workflow 名)。

我们从最简单的单文件开始,在仓库根目录创建 .woodpecker.yaml

when:
  - event: push
    branch: ${CI_REPO_DEFAULT_BRANCH}

steps:
  - name: greeting
    image: alpine
    commands:
      - echo "hello, woodpecker" > greeting.txt

  - name: shout
    image: python:3.14-slim
    commands:
      - python -c "print(open('greeting.txt').read().strip().upper())"

这个例子虽然小,却用上了 Woodpecker 最重要的两个特性:

  • 每个步骤是一个独立的容器——greeting 用的是 alpine 镜像,shout 换成了 python 镜像,互不干扰;
  • 步骤之间共享同一个工作区——greeting 写下的 greeting.txtshout 能直接读到,最终输出 HELLO, WOODPECKER

提交并推送到默认分支:

$ git add .woodpecker.yaml
$ git commit -m "ci: first pipeline"
$ git push

回到 Woodpecker 界面,几秒内就会出现一条新的流水线记录,点进去能看到 clone、greeting、shout 三个步骤:clone 是 Woodpecker 自动加上的(把仓库拉进工作区),后两个是你在 YAML 里定义的。每个步骤都能展开看实时日志,全部变绿即成功。

这就是 Woodpecker 的基本心智模型:每个步骤是一个容器,共享一个工作区,顺序执行,非零退出码即失败

常用语法速览

触发条件 when。流水线级(文件顶部)和步骤级(步骤内)都可以写,支持事件、分支、路径等过滤:

when:
  - event: push
    branch: main            # 只在 main 分支的 push 触发
  - event: pull_request     # PR 也触发
  - event: tag              # 打 tag 也触发

常用事件一览:push(推送)、pull_request(PR 打开或更新)、tag(打标签)、manual(界面手动触发)、cron(定时任务)、deployment(部署事件)、release(发布 Release)。

路径过滤也常用——文档变了不跑构建:

when:
  - event: push
    branch: ${CI_REPO_DEFAULT_BRANCH}
    path: "src/**/*"       # 只有 src 下的文件变化才触发

内置环境变量。Woodpecker 会往每个步骤注入一批 CI_ 开头的变量,常用的有:

  • CI_REPO:仓库全名,如 gitadmin/demo-api
  • CI_REPO_DEFAULT_BRANCH:默认分支名(通常是 main);
  • CI_COMMIT_SHA / CI_COMMIT_BRANCH:提交哈希 / 分支名;
  • CI_COMMIT_TAG:标签名(tag 事件时才有值);
  • CI_COMMIT_MESSAGE:提交信息;
  • CI_PIPELINE_NUMBER:流水线序号,同一仓库内从 1 递增,常用来给镜像打版本;
  • CI_PIPELINE_PARENT:父流水线序号(作为子流水线触发时才有值);
  • CI_PIPELINE_URL:本次流水线在 Woodpecker 界面的链接;
  • CI_PIPELINE_EVENT:本次触发事件类型。

它们既是步骤容器里的环境变量,也能在配置文件中用 ${...} 直接替换,甚至支持 Bash 风格的截取,比如 ${CI_COMMIT_SHA:0:8} 取短哈希。

不过要分清“两种取值时机”:

steps:
  - name: info
    image: alpine
    commands:
      - echo "repo: ${CI_REPO}"        # 配置阶段替换:加载 YAML 时就替换成字面量
      - echo "repo: $CI_REPO"          # 运行时读取:步骤容器里再展开(推荐)
      - echo "path: $${PATH}"          # $$ 转义:${...} 原样传给运行时 shell
  • ${VAR}:解析配置时被 Woodpecker 预先替换成具体值,等于写死进脚本;
  • $VAR:原样保留,由步骤容器里的 shell 在运行时展开(推荐);
  • $${VAR}$$ 转义后 ${...} 原样传给运行时——引用 PATHenvironment 注入的自定义变量(它们不参与配置阶段替换)时需要。

自定义环境变量environment

steps:
  - name: test
    image: python:3.14-slim
    environment:
      PIP_INDEX_URL: https://mirrors.aliyun.com/pypi/simple
    commands:
      - pip install pytest

允许失败的步骤。比如 lint 想提示但不卡流程:

steps:
  - name: lint
    image: python:3.14-slim
    commands:
      - pip install ruff && ruff check .
    failure: ignore    # 失败标记为红,但不影响整体结果

Secrets:管理敏感信息

镜像仓库密码、部署私钥等敏感信息不应写入 YAML。在 Woodpecker 界面进入仓库 → Settings → Secrets 添加:

  • registry_user:镜像仓库用户名
  • registry_password:镜像仓库密码/Token

在流水线里通过 from_secret 引用(环境和插件参数两种用法都支持):

steps:
  - name: deploy
    image: some/deploy-tool
    environment:
      TOKEN_ENV:
        from_secret: registry_password   # 注入为环境变量 TOKEN_ENV
    settings:
      api_token:
        from_secret: registry_password   # 传给插件的 settings 参数

两个值得注意的默认行为:

  • Secrets 默认不会注入 pull_request 事件,防止密钥通过外部 PR 泄露。确有需要时,可以在 Secret 设置里手动勾选允许的事件;
  • 可以给 Secret 限定“只允许哪些插件镜像使用”,进一步收窄暴露面。

Secret 也分系统全局和项目级别两种,如果多个仓库都要用同一组凭据,可以由管理员在系统级统一添加。

实战:Python 项目从测试到自动部署

现在来看一个更完整的项目:push 到默认分支自动跑测试、构建镜像并推送、SSH 部署到测试服务器

示例是一个用 uv 管理的 Python Web 小项目,最终以 docker compose 的方式跑在测试机上。

准备项目

使用 uv 创建并管理新项目:

$ mkdir web-app
$ cd web-app
$ uv init --name=web-app .
$ uv add flask
$ uv add --group test pytest ruff mypy   # pytest 等测试/检查工具放进 test 依赖组
$ uv add --group wsgi gunicorn           # 生产服务器放进 wsgi 依赖组

修改 main.py

from flask import Flask, jsonify

app = Flask(__name__)

@app.get("/")
def hello():
    return jsonify({"message": "Hello, World!"})


@app.get("/health")
def health():
    return jsonify({"status": "ok"})

if __name__ == "__main__":
    app.run(debug=True)

再添加单元测试 tests/test_app.py

import pytest

from main import app


@pytest.fixture()
def client():
    app.config["TESTING"] = True
    with app.test_client() as client:
        yield client


def test_hello_returns_greeting(client):
    resp = client.get("/")

    assert resp.status_code == 200
    assert resp.get_json() == {"message": "Hello, World!"}


def test_health_returns_ok(client):
    resp = client.get("/health")

    assert resp.status_code == 200
    assert resp.get_json() == {"status": "ok"}


def test_unknown_path_returns_404(client):
    resp = client.get("/no-such-page")

    assert resp.status_code == 404

Dockerfile 按常规写法把项目构建成可运行镜像即可;compose.yaml 是部署模板,注意里面要有一行 image:,部署脚本靠替换这一行来指定版本:

FROM python:3.14-slim

RUN pip install uv -i https://mirrors.aliyun.com/pypi/simple

WORKDIR /app
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev --group wsgi
COPY main.py ./

ENV PATH="/app/.venv/bin:$PATH"

EXPOSE 8080
CMD ["gunicorn", "--bind", "0.0.0.0:8080", "main:app"]

compose.yaml(部署模板):

services:
  web:
    image: 192.168.1.10:5000/mybuild/web-app   # 占位用,部署时会被替换成具体版本
    ports:
      - "8080:8080"

这里复用了上一篇文章 Docker buildx 实现跨架构构建与多架构镜像里的镜像仓库。

代码和逻辑都很简单,有兴趣的可以照着做一遍。最终的项目结构如下:

.
├── .woodpecker
│   └── ci.yaml
├── compose.yaml
├── Dockerfile
├── main.py
├── pyproject.toml
├── README.md
├── tests
│   └── test_app.py
└── uv.lock

流水线配置

在仓库根目录创建 .woodpecker/ci.yaml(目录形式,一个文件就是一个 workflow):

when:
  - event: push
    branch: ${CI_REPO_DEFAULT_BRANCH}   # 默认分支,通常是 main
  - event: pull_request                 # PR 也触发,但只跑测试(见下方步骤级 when)

steps:
  - name: test
    image: astral/uv:python3.14-alpine
    environment:
      UV_CACHE_DIR: /cache/uv
      UV_LINK_MODE: copy
      PIP_CACHE_DIR: /cache/pip
      MYPY_CACHE_DIR: /cache/mypy
      RUFF_CACHE_DIR: /cache/ruff
      PYTHONPATH: .         # 让 pytest 能导入根目录的 main.py
    commands:
      - uv sync --group test            # 安装项目依赖(flask)+ test 组
      - uv run pytest .
      - uv run mypy .
      - uv run ruff check .
    volumes:
      - /data/program/woodpecker/agent-cache:/cache   # 宿主机缓存目录,按需修改

  - name: build-image
    when:
      - event: push   # PR 时跳过,只测不发布
    image: woodpeckerci/plugin-kaniko
    settings:
      cache: true                # 开启构建缓存,缓存仓库从 destination 自动推断,可用 cache_repo 显式指定
      repo: mybuild/web-app
      registry: 192.168.1.10:5000
      insecure: true            # registry 为 HTTP 时需要;若已配置 TLS 则删除此行
      tags: ${CI_PIPELINE_NUMBER}-${CI_COMMIT_SHA:0:8}
      username: admin
      password:
        from_secret: registry_password
      dockerfile: ./Dockerfile

  - name: copy-compose
    when:
      - event: push
    image: appleboy/drone-scp
    settings:
      host: 127.0.0.1              # 测试机与 CI 同机时用回环地址即可
      user: root
      key:
        from_secret: deploy_key
      source:
        - compose.yaml
      target: /tmp/
      command_timeout: 1m

  - name: deploy
    when:
      - event: push
    image: appleboy/drone-ssh
    settings:
      host: 127.0.0.1
      username: root
      key:
        from_secret: deploy_key
      command_timeout: 5m
      script:
        - mkdir -p /opt/web-app
        - export IMAGE=192.168.1.10:5000/mybuild/web-app:${CI_PIPELINE_NUMBER}-${CI_COMMIT_SHA:0:8}
        - bash -c "cd /opt/web-app && [[ -f compose.yaml ]] && cp compose.yaml compose.yaml.last_version"
        - 'sed "s#image: .*#image: $IMAGE#" /tmp/compose.yaml > /opt/web-app/compose.yaml'
        - cd /opt/web-app && docker compose up -d

必要的设置:

  • 流水线中用到了 registry_password(镜像仓库密码)和 deploy_key(测试机 SSH 私钥)两个 Secret,需要先把它们加到仓库设置中。仓库用户名、主机地址这类不敏感信息直接写在流水线里即可,口令和私钥才需要走 Secret;
  • 在流水线里挂载宿主机目录属于提权能力,需要在 Woodpecker 的仓库设置中开启 Trusted(Project settings → Trusted → Volumes),否则挂载会失败。

常见的需要启用的配置项还有 Project → Allow DeploymentsTrusted → Security 等,可根据报错信息和日志按需启用。

几点说明:

  • 两级触发条件的配合:文件顶部的 when 让默认分支 push 和 PR 都触发流水线;后面三个步骤各自带 when: event: push,PR 时自动跳过——这是步骤级过滤的典型用法:测试对所有变更生效,发布只对合入主干生效;
  • 测试步骤挂了宿主机缓存卷,让 uv/pip/mypy/ruff 的缓存在流水线之间复用,能明显提速;
  • 镜像标签${CI_PIPELINE_NUMBER}-${CI_COMMIT_SHA:0:8},每次提交都唯一,而且能追溯到流水线序号与提交哈希。想改成打 tag 才发布正式版,把后三个步骤的 when 换成 event: tag,并给构建插件开启 auto_tag,按语义化版本自动生成镜像标签;
  • 流水线里用到了 plugin-kaniko、drone-scp、drone-ssh 三个插件,更多插件可以到官网查看:https://woodpecker-ci.org/plugins

流水线的逻辑:

  • 先执行单元测试、lint 和类型检查;
  • 然后构建 Docker 镜像并推送到自建的 Docker Registry(HTTP 协议:kaniko 侧用 insecure: true 推送,拉取侧需在测试机的 daemon.json 配置 insecure-registries,参见上一篇文章);
  • drone-scp 把 compose 模板分发到测试机;
  • drone-ssh 远程执行——备份旧 compose、用 sed 把 image: 行替换成本次构建的版本,最后 docker compose up -d 滚动更新。

缓存。 CI 的每一步都跑在一次性容器里,不做缓存的话,每次流水线都要从零下载全部依赖、重建全部镜像层,跑测试的时间大半耗在等待下载上。缓存能成倍提升构建速度、显著减少等待时间——本文的流水线里其实埋了两层:test 步骤的宿主机缓存卷(依赖缓存)和 build-image 步骤的 cache: true(镜像层缓存)。当流水线中某些步骤耗时较长时,值得优先考虑能否通过缓存优化。

验证

  • 在 Gitea 上新建空仓库(如 gitadmin/web-app),并在 Woodpecker 里激活(步骤同“激活仓库”一节);
  • 根据流水线的需求,配置 registry_passworddeploy_key 两个 Secret,并开启 Trusted → Volumes 设置;
  • 推送代码:
$ git add . && git commit -m "feat: demo app with ci" && git push

预期:test 步骤依次输出 pytest、mypy、ruff 的结果;build-image 日志里能看到 kaniko 的构建与推送信息;copy-composedeploy 相继变绿。到测试机上执行 cd /opt/web-app && docker compose ps,能看到 web 服务已经用新镜像跑起来了;浏览器访问 http://<测试机IP>:8080/health,返回 {"status": "ok"} 即部署成功。

再开一个 PR 试试:流水线只会执行 test 步骤,后面三个步骤自动跳过,合入默认分支后才会构建镜像并部署——这正是步骤级 when 的用途。

woodpecker-cli:命令行操作与本地执行

前面的操作都在 Web 界面完成,但改流水线时最麻烦的是“推一次、等一次”。官方的 woodpecker-cli 能把很多操作搬回终端,其中 exec 甚至可以在本地直接跑流水线。

安装:到 GitHub Releases(https://github.com/woodpecker-ci/woodpecker/releases)下载对应平台的压缩包,解压出的 woodpecker-cli 放进 PATH 即可;部分 Linux 发行版的软件源里也有打包。

连接服务器

$ woodpecker-cli setup --server http://192.168.1.10:8000 --token <你的令牌>

woodpecker token 获取:从 Woodpecker 右上角的头像进入用户设置,在 Cli & App 中可以获得 Personal Access Token

令牌在 Woodpecker 界面的用户设置(User settings)里生成。连接信息保存在本地,还支持用 context 管理多套环境。

常用命令速览

  • woodpecker-cli info:查看当前连接的用户与服务器信息;
  • woodpecker-cli repo ls:列出已激活的仓库;
  • woodpecker-cli pipeline ls:列出最近的流水线;
  • woodpecker-cli pipeline log show:在终端查看某次流水线的日志;
  • woodpecker-cli lint .woodpecker.yaml:校验配置文件语法,加 --strict 更严格。

本地试跑流水线——这是 CLI 最实用的能力。在仓库目录里执行:

$ woodpecker-cli exec

它会读取本地 .woodpecker.yaml(或 .woodpecker/ 目录),用本机的 Docker 后端真实执行各步骤,不推代码就能验证语法和命令输出。默认执行全部 workflow,用 --workflow-name 可以只跑其中一个;涉及 Secret 的步骤,可以用 --secrets 临时传入测试值。

总结

回顾整套体系:一台服务器、一份 compose 文件,Gitea 负责代码托管,Woodpecker 负责构建,SQLite 存储记录,几十行 YAML 即可描述从测试到发布的全部流程。对个人项目和小团队来说,这就是一套“麻雀虽小,五脏俱全”的私有 CI/CD。

在本文之外,还有几个日常会遇到的场景:

  • deployment 部署事件:除 push/PR 外,Woodpecker 还支持 deployment 事件,可通过界面或 API 发起,配合 when: event: deployment 单独编写发布流水线,实现“测试通过后再手动部署到指定环境”的流程;
  • 多 Agent 协作:一个 Server 可以接入多个 Agent,用 WOODPECKER_MAX_WORKFLOWS 控制单个 Agent 的并发数;还可以给 Agent 配置标签(labels),把特定任务(如 arm64 构建)调度到对应的机器执行;
  • matrix 构建矩阵matrix 关键字一次定义多组变量,生成批量任务——比如同时在多个 Python 版本、amd64/arm64 多平台上跑测试。

CI/CD 是 DevOps 的重要一环,而 Woodpecker 是补齐这块拼图的极佳工具,面临相似场景的时候,非常推荐 Woodpecker。

Last updates at Sep 13,2026

Views (8)

Leave a Comment

total 0 comments