GitHub Actions Runner 实现原理:它为什么能运行工作流
· 1594 words · ~ 8 min read
分析基线:本文基于 actions/runner 仓库 main 分支和官方设计文档在 2026-05-20 的公开内容整理。
先说结论:
- GitHub Actions Runner 本质上不是调度器,而是一个「拿到作业描述后在本机执行」的执行代理。
- 它之所以能运行 workflow,不是因为它自己理解了整套 GitHub Actions 平台规则,而是因为 GitHub 服务端先把 workflow 解析、筛选、编排成了一个可执行的 job message,再交给 runner。
- runner 真正做的事情可以概括成四段:注册和鉴权 -> 建立会话并长轮询拿消息 -> 派发 worker 子进程 -> 按 step 类型执行并回传结果。
背景与目标
很多人第一次看 GitHub Actions,会直觉认为 runner 做了两件事:
- 解析
.github/workflows/*.yml。 - 直接把整条流水线从头跑到尾。
但从 actions/runner 的实现看,这两个判断都不准确。
更接近事实的说法是:
- workflow 的事件触发、矩阵展开、依赖关系、权限收敛、runner 匹配主要发生在 GitHub 服务端。
- runner 收到的已经不是原始 YAML,而是服务端整理好的
AgentJobRequestMessage。 - runner 负责把这份 job message 落到当前机器:准备目录、下载 action、执行脚本或容器、上传日志、上报状态。
所以本文重点回答两个问题:
actions/runner的实现原理是什么?- 它为什么真的可以把 GitHub 上的一段 workflow 跑起来?
一、runner 在整套系统里的定位
先把边界划清楚。
从官方 README 看,runner 的定义很直接:它是运行 GitHub Actions job 的应用程序。这句话看起来普通,但它隐含了一个很重要的边界:它运行的是 job,不是整个平台。
从 src/Runner.Worker/JobRunner.cs 也能看出来,worker 启动后直接处理的是:
message.Resourcesmessage.Variablesmessage.Steps
也就是说,runner 接手时,服务端已经把一个 job 所需的资源、变量、步骤都准备成结构化消息了。runner 不是拿到 YAML 再自己去做完整编排,而是消费一个已经成型的执行计划。
这也是理解 GitHub Actions 的关键:
- GitHub 服务端负责控制面:触发、调度、权限、编排、消息投递。
- runner 负责数据面/执行面:在具体机器上把 job 跑出来。
二、注册与鉴权:runner 为什么能合法接入 GitHub
如果 runner 只是一个本地进程,它首先得回答一个问题:GitHub 为什么信任这台机器能接作业?
官方设计文档 docs/design/auth.md 给出了比较完整的答案。
1. 配置阶段:不是拿你的 GitHub 账号直接跑
自托管 runner 在 config.sh / config.cmd 阶段,使用的是短期有效的 registration token。文档明确写到:
- 你的个人凭据不会直接被 runner 保存用来访问 GitHub 资源。
- 配置过程中会在本机生成一对 RSA 公私钥。
- 公钥会注册到服务端,私钥保存在本地。
这一步的意义是:runner 以后和 GitHub 通信,不需要长期持有你的账号密码,它有自己独立的机器身份。
2. 运行阶段:listener 先换取会话级访问能力
src/Runner.Listener/MessageListener.cs 的 CreateSessionAsync() 会做几件事:
- 读取本地 runner 配置和凭据。
- 连接 Runner Server。
- 创建
TaskAgentSession。 - 把 runner 的版本、机器名、OS 信息带到会话里。
官方认证文档对这一步的解释更清楚:
- listener 进程启动后,会使用本地私钥向 Token Service 申请 OAuth token。
- 这个 token 只允许它访问消息队列和 Actions 服务的必要接口。
- token 不是长期有效,而是跟当前作业或会话生命周期绑定。
所以,runner 不是“装好以后永远有权限”,而是每次启动后重新建立一条受控的会话链路。
3. 为什么消息能安全地下发到这台 runner
官方文档写到:服务端下发给 runner 的工作流消息会使用 runner 公钥相关机制进行保护。源码里的 MessageListener.DecryptMessage() 则体现出另一层实现细节:
- runner 从服务端拿到消息后,会检查 session 是否带有加密信息。
- 如果消息体带有
IV,会用 AES 解密消息内容。
把两者合起来理解更稳妥:
- 配置阶段建立的是机器身份。
- 运行阶段建立的是会话级安全通道。
- 最终下发到本机的 job message 不是明文裸奔,而是 runner 能解、其他机器不能直接消费的消息。
三、主循环:runner 是怎么拿到作业的
runner 真正进入“等活干”状态,发生在 Runner.Listener 里。
1. 入口进程是 Runner.Listener
src/Runner.Listener/Program.cs 的主流程很简单:
- 校验操作系统和目录权限。
- 解析命令行参数。
- 把执行转交给
Runner.ExecuteCommand()。
也就是说,外部看起来你是在运行 ./run.sh,但内部真正常驻的是 Runner.Listener。
2. RunAsync 做的不是执行 step,而是维护一个消息循环
src/Runner.Listener/Runner.cs 里的 RunAsync() 才是 listener 的核心。
它在创建 session 成功后,会进入一个持续循环:
CreateSessionAsync()建立与服务端的会话。- 输出
Listening for Jobs。 - 创建
IJobDispatcher。 - 循环调用
_listener.GetNextMessageAsync()。 - 根据消息类型决定是更新、关闭还是派发 job。
这说明 listener 的职责更像一个控制平面代理,而不是具体执行器。
3. GetNextMessageAsync 的本质是长轮询
MessageListener.GetNextMessageAsync() 调用的是 _runnerServer.GetAgentMessageAsync(...)。调用参数里带着:
- pool id
- session id
- last message id
- 当前 runner 状态
- runner 版本、OS、架构
这基本就是一个典型的“长轮询消息拉取”模型。
如果拿不到消息,它会:
- 继续等待或重试。
- 在错误时做退避重试。
- 在 session 过期时尝试重建 session。
这也是 runner 可以长期在线的原因:它不是被动等服务端推送 TCP 连接,而是自己稳定地维护一条“我来取消息”的控制链路。
4. 新旧两条取 job 路径并存
从 Runner.cs 的消息分支可以看到,当前 runner 同时兼容两类 job 获取方式:
- 旧路径:直接拿到
PipelineAgentJobRequest,消息体里就是完整 job。 - 新路径:先收到一个
RunnerJobRequestRef,必要时先Acknowledge,再去 Run Service 拉完整 job message。
这说明 runner 不是一个静态实现,它在逐步从旧的队列模型迁移到 broker/run service 模型。
但不管哪条路径,核心都一样:先拿到一个 job 引用或 job 描述,再交给 job dispatcher。
四、为什么要再起一个 Runner.Worker 子进程
拿到 job 以后,listener 并不会自己执行,而是交给 JobDispatcher。
这是 runner 设计里很关键的一层隔离。
1. 一个 runner 同时只处理一个 job
src/Runner.Listener/JobDispatcher.cs 里写得很直白:
- 当前设计按队列一次处理一条消息。
- 每次只执行一个 job。
- 服务端在当前 job 没结束前,不应该再给这个 runner 发下一条 job。
所以“一个 runner 并发跑多个 job”不是默认模型。并发一般靠多 runner 实例或 runner scale set,而不是单进程内多 job 复用。
2. dispatcher 会先续租 job,再启动 worker
JobDispatcher.RunAsync() 在真正拉起 worker 前,会先:
- 启动
RenewJobRequestAsync(...)。 - 等待第一次续租成功。
- 只有确认当前 job 仍归这台 runner 所有,才继续派发。
这一步很像“租约确认”。
它解决的是一个分布式系统里的基本问题:服务端必须知道这份 job 当前确实被某个 runner 持有,而且 runner 还活着。
3. listener 和 worker 分进程,是为了隔离控制面和执行面
继续看 JobDispatcher.RunAsync():
- 它启动了
IProcessChannel作为 IPC 通道。 - 再起一个
Runner.Worker子进程,参数是spawnclient <pipeOut> <pipeIn>。 - 然后通过进程间通道把
AgentJobRequestMessage发给 worker。
这样设计有几个直接好处:
- listener 可以继续维护会话、接收取消和更新消息,不被具体 step 执行阻塞。
- worker 崩了,不一定要把整个 listener 一起带崩。
- 自更新、取消、超时处理会更清晰,因为控制进程和执行进程分开了。
一句话总结:listener 负责“接活和看场子”,worker 负责“真正干活”。
五、worker 拿到 job 后,到底怎么把 workflow 跑起来
这是最核心的一段。
1. worker 收到的不是 YAML,而是标准化的 job message
src/Runner.Worker/Worker.cs 启动后,会从 IPC 通道读取一条 NewJobRequest 消息,然后反序列化成 Pipelines.AgentJobRequestMessage。
随后它会做三件基础工作:
- 初始化 secret masker。
- 设置线程文化信息。
- 调用
IJobRunner.RunAsync(jobMessage, ...)。
这里已经很能说明问题了:runner 之所以能跑,是因为它消费的是一个标准 job message,而不是自己从磁盘读取 workflow YAML 重新解释。
2. JobRunner 负责把 job 变成执行上下文
JobRunner.RunAsync() 是 job 级执行的总控。
它主要做这些事:
- 连接 job server / run server,用于状态上报、日志回传、结果提交。
- 创建
IExecutionContext,把变量、资源、取消信号都挂进去。 - 检查并准备工作目录、工具目录、临时目录。
- 调用
IJobExtension.InitializeJob()把 job 初始化成可执行步骤列表。 - 把步骤交给
IStepsRunner.RunAsync()顺序执行。 - 最后做
FinalizeJob()和CompleteJobAsync()。
换句话说,JobRunner 干的是“把服务端下发的 job 描述,装配成当前机器上的执行现场”。
3. StepsRunner 才是 step 语义的真正落点
src/Runner.Worker/StepsRunner.cs 会循环处理 jobContext.JobSteps。
每个 step 执行前,它会先做几件 runner 语义层的工作:
- 注入
steps、env等表达式上下文。 - 合并 job 级和 step 级环境变量。
- 计算
if条件。 - 计算
timeout-minutes。 - 处理取消、失败、
continue-on-error和 post step。
这一步非常重要,因为它解释了一个常见疑问:GitHub Actions 的“条件、环境变量、post、超时”这些语义到底是谁实现的?
答案就是 runner。
服务端把 job 编排好,但真正把 step 级运行语义落到本机的,是 StepsRunner 和相关 handler。
4. 每个 step 执行前,runner 其实先做了一次 job 级准备
如果问题换成「每个 step 是不是直接开跑」,答案是否定的。
从 src/Runner.Worker/JobExtension.cs 可以看到,runner 在真正进入 step 循环前,会先执行一次 InitializeJob()。这个阶段会创建一个单独的 timeline 节点,名字就叫 Set up job。它不是某个业务 step,而是整个 job 的前置准备。
这一段准备大致是:
Prepare workflow directory:调用PipelineDirectoryManager.PrepareDirectory(...)创建或清理 pipeline/workspace 目录。- 设置
runner.workspace和github.workspace上下文。 - 计算 job 级
env、defaults.run、job container、service container。 - 调用
ActionManager.PrepareActionsAsync(...),把当前 job 用到的远程 action 先下载到本地缓存。 - 按顺序组装出 pre-job steps、job steps、post-job steps。
也就是说,step 不是裸跑的,它先站在一个已经准备好的 job 执行现场里运行。
5. 但是「准备代码」要分成两类看
这里最容易混淆的是:runner 会不会自动把仓库代码准备好?
答案要拆成两部分:
- action 代码会预先准备。如果 step 里有
uses: actions/checkout@v4、uses: actions/setup-go@v5这种远程 action,runner 会在PrepareActionsAsync()阶段先把这些 action 包下载到本地。 - 业务仓库代码不会自动 checkout。
PipelineDirectoryManager.PrepareDirectory()做的是创建工作目录、恢复 tracking config、按workspace.clean策略清理目录;它不是git clone。
换句话说:
github.workspace这个目录会先存在。- 但这个目录里未必已经有你的仓库代码。
- 只有 workflow 里显式执行了
actions/checkout,或者你自己写了git clone,后面的run:step 才能看到仓库内容。
这也是为什么很多 workflow 的第一步几乎总是:
|
|
因为 checkout 本身也是一个普通 action,不是 runner 自动附送的隐藏步骤。
6. 只看「step 开跑前」的机器内流程,可以这样理解
flowchart TD
A[Job message 到达 worker] --> B[JobExtension.InitializeJob]
B --> C[Prepare workflow directory]
C --> D[设置 runner.workspace / github.workspace]
D --> E[计算 job 级 env / defaults / container]
E --> F[PrepareActionsAsync 下载 uses action]
F --> G[生成 pre-job / job / post-job step 列表]
G --> H[StepsRunner 取出下一个 step]
H --> I[计算 step if / env / timeout]
I --> J{step 类型}
J -->|run| K[写临时脚本文件并调用 shell]
J -->|uses JS action| L[加载 action 定义并调用 node]
J -->|uses container action| M[准备镜像和挂载后 docker run]
K --> N[收集 stdout / stderr / exit code]
L --> N
M --> N
N --> O[更新 step 结果与上下文]
O --> P[进入下一个 step]
如果只关心你问的「是不是要先准备代码」,这张图可以读出两个层次:
- runner 会先准备执行环境:目录、上下文、环境变量、远程 action、本地 step 列表。
- runner 不会默认准备业务代码:仓库代码是否出现,取决于 workflow 里有没有 checkout 或其他拉代码动作。
7. 到了每一个具体 step,runner 又会再做一次 step 级准备
虽然 job 级准备只做一次,但每个 step 开始前,StepsRunner 还是会做一轮 step 级处理:
- 把 job 级环境变量合并进 step 的
env。 - 计算 step 的
if条件。 - 计算
timeout-minutes。 - 注入
steps、github、runner等表达式上下文。 - 处理取消信号和 post step 注册。
所以完整理解应该是:
- 先做一次 job 级准备,把场地搭好。
- 再做多次 step 级准备,每个 step 单独计算条件、环境和执行方式。
六、run: 和 uses: 为什么都能执行
很多人把 workflow step 统一理解成“执行一段命令”,但 runner 里其实分成了几类完全不同的执行模型。
1. run: 步骤:临时脚本文件 + shell 执行
src/Runner.Worker/Handlers/ScriptHandler.cs 负责 run: 类型步骤。
它的做法是:
- 根据 shell 选项决定用
bash、sh、pwsh、powershell等哪种解释器。 - 把 step 里的脚本内容写入一个临时文件。
- 拼出命令行参数。
- 调用
StepHost.ExecuteAsync()执行这个脚本。
所以 run: echo hello 的本质,不是 runner 自己实现了一个脚本语言,而是 runner 帮你生成了临时脚本文件,再交给系统 shell。
2. JavaScript Action:Node 进程执行
src/Runner.Worker/Handlers/NodeScriptActionHandler.cs 负责 Node 类型 action。
它会:
- 准备 action 输入和环境变量。
- 注入
ACTIONS_RUNTIME_URL、ACTIONS_RUNTIME_TOKEN、ACTIONS_RESULTS_URL等运行时信息。 - 选定 runner 自带的 Node 运行时。
- 执行 action 指向的 JS 文件。
这说明 JavaScript action 能跑,不是因为机器上恰好有一套对版 Node,而是因为 runner 自己带了执行环境和运行时协议。
3. Container Action:Docker 构建/拉取后执行
src/Runner.Worker/Handlers/ContainerActionHandler.cs 负责容器 action。
它会:
- 判断是
docker://镜像还是本地Dockerfile。 - 需要时先构建镜像。
- 准备容器网络、挂载目录和环境变量。
- 把 workspace、temp、file command 目录映射进容器。
- 再通过 Docker 运行这个 action。
所以“GitHub Actions 就是 Docker”这句话不准确。更准确的说法是:
run:默认不是 Docker。- JavaScript action 默认也不是 Docker。
- 只有容器 action 或 job container 场景才强依赖 Docker/容器运行时。
4. Composite Action:展开成更多 step
src/Runner.Worker/ActionRunner.cs 里,如果发现 action 是 composite 类型,会调用 PrepareActionsAsync() 继续展开嵌套步骤。
也就是说,composite action 并不是一种新的底层执行器,它更像一个“步骤模板”,最后仍会落回已有的 script / node / container 执行路径。
七、远程 action 为什么能在本机出现
这又是一个常见问题:workflow 写的是 uses: actions/checkout@v4,本机原本没有这个目录,runner 为什么就能执行?
答案在 src/Runner.Worker/ActionManager.cs。
1. runner 会先把远程 action 下载到本地
PrepareActionsAsync() 会解析当前 job 里引用到的 action,然后调用 DownloadRepositoryActionAsync()。
后者会:
- 以
owner/repo@ref作为标识构造目标目录。 - 如果本地已有 watermark,直接复用。
- 否则下载 action archive。
- 解压到 staging 目录。
- 最终落到 runner 的 action 缓存目录。
2. 缓存粒度不是工作流级,而是 action 版本级
从实现能看到几个关键点:
- 缓存路径按
NameWithOwner + Ref组织。 - 归档下载还可以按
ResolvedSha复用缓存。 - 某些场景还支持符号链接复用已解压目录。
这就是为什么同一个 runner 重复执行相同版本 action 时,后续会明显更快。
八、日志、状态、取消、结果是怎么回到 GitHub 的
如果 runner 只是本地执行,那 GitHub 页面为什么能实时看到:
- step 日志
- job 状态
- cancel 后的终止
- 最终成功或失败
原因是 runner 在执行过程中一直和服务端保持双向状态同步。
1. 日志和状态通过 job server 队列持续上报
JobRunner.RunAsync() 会启动 IJobServerQueue。这说明 runner 不是等全部结束后再一次性上传结果,而是在执行过程中持续把 timeline、日志、状态上报出去。
2. 取消不是本地 Ctrl+C 专属,而是协议里的一类消息
Worker.cs 在执行 job 时,会同时监听:
- job 执行任务本身
- 来自 IPC 通道的取消/关闭消息
如果 listener 收到取消请求,就可以通过进程间通道把 CancelRequest 发给 worker,worker 再取消当前 job token。
所以 GitHub 网页上点 Cancel 能生效,是因为:
- 服务端发取消信号;
- listener 收到后通知 worker;
- worker 终止当前 step 或 job。
3. 最终结果不是看进程退出码这么简单
runner 会综合:
- step 自身结果
continue-on-error- timeout
- cancellation
- post-job 执行情况
最后再汇总成 job result 提交给服务端。
这也是为什么 GitHub Actions 页面展示的是完整的 step/job 语义,而不是一串单纯的 Unix 退出码。
九、它为什么“可以运行 workflow”:本质上靠哪几层能力拼起来
把前面的实现拆开后,可以把答案压缩成六层。
1. 服务端先把 workflow 变成了 runner 能消费的 job
这是最根本的一层。
runner 并不是从零开始解释一份 workflow,而是接收 GitHub 服务端已经编排好的 job message。没有这层,runner 只能算一个脚本执行器,不足以成为 GitHub Actions 的执行节点。
2. 注册与会话机制给了 runner 一个可信机器身份
registration token、RSA 密钥、本地凭据、session 创建,这些机制共同保证:
- 不是任意机器都能冒充某个 runner。
- 服务端知道 job 应该发给谁。
- runner 具备受控、短期、可撤销的访问能力。
3. 长轮询消息队列把“云上的调度结果”送到了“本地机器”
没有消息循环,workflow 只是 GitHub 后台的一条记录;有了 session + long poll,它才真正有机会落到某台机器上执行。
4. worker 子进程把 job 执行和会话维护隔离开了
这层保证了:
- 控制链路不会被 step 执行拖死。
- 取消、超时、自更新有独立处理空间。
- listener 可以稳态在线,worker 可以按 job 生命周期创建和退出。
5. handler 体系把 workflow step 语义翻译成操作系统动作
这层是 runner 最像“执行引擎”的地方:
run:-> shell 脚本- Node action -> Node 进程
- Container action -> Docker 执行
- Composite action -> 展开成更多 step
也就是说,workflow 并不是直接“跑 YAML”,而是被 runner 翻译成了宿主机能执行的进程、容器和文件操作。
6. 结果回传闭环让 GitHub 页面和本地执行保持一致
没有日志和状态回传,runner 最多只是本地工具;正因为它持续把 timeline、日志、结果、取消状态回传给服务端,GitHub UI 才能实时展示一场 workflow run。
十、把完整链路串起来
下面用一张图把整条链路串起来:
flowchart LR
A[GitHub Event<br/>push / pull_request / cron] --> B[GitHub Actions 服务端]
B --> C[解析 workflow<br/>展开 matrix / needs / permissions]
C --> D[匹配 runner labels]
D --> E[向目标 runner 队列投递 job]
E --> F[Runner.Listener<br/>CreateSession + Long Poll]
F --> G[拿到 JobRequestMessage]
G --> H[JobDispatcher<br/>续租 job + 启动 worker]
H --> I[Runner.Worker]
I --> J[JobRunner / StepsRunner]
J --> K[下载 action / 准备 workspace]
J --> L[执行 run / node / container / composite]
L --> M[日志、timeline、结果回传]
M --> N[GitHub Actions UI]
如果再压缩成一句话,就是:
GitHub 负责把「应该跑什么」算清楚,runner 负责把「怎么在这台机器上跑出来」做完整。
如果只看「runner 这台机器内部」的执行流程
前一张图偏平台视角。如果只关心「job 到了这台机器之后,runner 内部怎么跑」,更适合看下面这张时序图:
sequenceDiagram
participant GH as GitHub Actions Service
participant RL as Runner.Listener
participant JD as JobDispatcher
participant RW as Runner.Worker
participant JR as JobRunner/StepsRunner
participant SH as Script/Node/Container Handler
participant OS as OS / Docker / Node
GH->>RL: 长轮询返回 JobRequestMessage
RL->>JD: Run(jobMessage)
JD->>GH: RenewJobRequestAsync(续租 job)
JD->>RW: 启动 Runner.Worker 子进程
JD->>RW: IPC 发送 AgentJobRequestMessage
RW->>JR: 反序列化消息,创建 ExecutionContext
JR->>JR: 初始化 workspace / temp / variables / steps
JR->>SH: 按 step 类型选择 handler
alt run: 脚本步骤
SH->>OS: 写临时脚本并调用 shell
else uses: JS Action
SH->>OS: 调用 runner 自带 node 执行 action
else uses: Docker Action
SH->>OS: docker build/pull + docker run
end
OS-->>SH: 返回 stdout/stderr/exit code
SH-->>JR: 汇总 step 结果
JR-->>GH: 持续上报日志、timeline、job 状态
GH-->>RL: 可选发送 cancel / shutdown / refresh
RL->>RW: IPC 转发取消或关闭消息
RW-->>JD: worker 退出并返回 job 结果
JD-->>RL: 当前 job 完成
RL-->>GH: 删除消息 / 保持监听下一条 job
可以把这张图按 8 个动作来读:
- listener 取消息:
Runner.Listener长轮询 GitHub,拿到一条 job request。 - dispatcher 接管:listener 不自己执行,而是把 job 交给
JobDispatcher。 - 先续租再开工:dispatcher 先调用
RenewJobRequestAsync(),确认这份 job 还归当前 runner 持有。 - 拉起 worker 子进程:dispatcher 启动
Runner.Worker,并通过 IPC 管道传 job message。 - worker 建上下文:worker 把 job message 反序列化成
AgentJobRequestMessage,然后创建ExecutionContext。 - 按 step 类型挑执行器:
run:走ScriptHandler,JS action 走NodeScriptActionHandler,容器 action 走ContainerActionHandler。 - 边跑边回传:日志、timeline、状态不是最后一次性上传,而是执行过程中持续回传给 GitHub。
- 收尾并回到监听态:job 完成后,worker 退出;listener 清理这条消息,然后继续等待下一条 job。
如果你只记一件事,可以记这句:
在机器内部,runner 不是一个单进程“从头跑到尾”,而是
Listener 负责控场 + Worker 负责执行 + Handler 负责把 step 翻译成宿主机动作的分层模型。
十一、几个容易混淆的点
1. GITHUB_TOKEN 不是 runner listener 自己拿消息的 token
官方认证文档明确区分了两类 token:
- runner 用来跟 Actions Service 通信的 token;
- workflow 内给 action/脚本使用的
GITHUB_TOKEN。
两者作用域、生命周期、使用对象都不同。
2. actions/checkout 不是 runner 内建能力
runner 只是把 actions/checkout@v4 下载下来再执行。checkout 是一个普通 action,不是 runner 的保留字。
3. runner 不是默认容器平台
只有容器 action 或 job container 场景,runner 才会走 Docker 相关逻辑。普通 run: 完全可以直接在宿主机 shell 上执行。
结论
如果只保留一句话,我会这样总结 GitHub Actions Runner:
它不是一个“读 YAML 然后运行命令”的小工具,而是 GitHub Actions 控制面在目标机器上的执行代理。
它之所以能运行 workflow,依赖的是一整套分层协作:
- GitHub 服务端把 workflow 编排成 job。
- runner 通过注册、会话、长轮询拿到 job。
- listener 把 job 派给独立 worker。
- worker 用 script/node/container/composite 等 handler 把 step 真正落到宿主机执行。
- 执行状态再持续回传给 GitHub。
从这个角度看,actions/runner 最值得关注的不是“它会不会执行 shell”,而是它把鉴权、消息协议、作业租约、执行隔离、step 语义、日志回传这些能力拼成了一个完整闭环。
参考链接
actions/runner仓库:https://github.com/actions/runner- Runner 认证设计:https://github.com/actions/runner/blob/main/docs/design/auth.md
- Self-hosted runners 文档:https://docs.github.com/actions/hosting-your-own-runners
- Workflow syntax 文档:https://docs.github.com/actions/using-workflows/workflow-syntax-for-github-actions
GITHUB_TOKEN文档:https://docs.github.com/actions/security-for-github-actions/security-guides/automatic-token-authentication