<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>GitHub · XB 现场笔记</title><link>https://www.xbcatm.com/zh/tags/github/</link><description>基于 HTTP、DNS、TLS、Caddy、SSH、发布与恢复的可复现运维现场笔记。</description><generator>Hugo</generator><language>zh-CN</language><atom:link href="https://www.xbcatm.com/zh/tags/github/index.xml" rel="self" type="application/rss+xml"/><item><title>GitHub Actions 工作流给构建把门槛</title><link>https://www.xbcatm.com/zh/posts/github-actions-workflows-gate-the-build/</link><pubDate>Thu, 02 Sep 2027 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/zh/posts/github-actions-workflows-gate-the-build/</guid><description>可靠的构建门禁将工作流事件、准确版本和真实测试成功，与另行配置的必需检查规则连接起来。</description><content:encoded>&lt;p&gt;工作流能够为合并决定提供证据，但只有 YAML 并不会让这些证据成为强制要求。可靠的构建门禁需要连接正确的事件、实际测试的准确版本、有意义的测试结果，以及要求对应检查通过的仓库规则。审阅时应把这些环节一起核对，不能看到早先某次运行有绿色标志，就认为这次拟合并的内容已经满足当前要求。&lt;/p&gt;
&lt;h2 id="区分执行与强制要求"&gt;区分执行与强制要求&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;.github/workflows/&lt;/code&gt; 下的文件描述触发器和作业。&lt;code&gt;on&lt;/code&gt; 选择事件，分支保护或规则集则另行决定合并之前必须通过哪些检查。应记录稳定的检查名称，并在改名时同时审阅相应规则。不同工作流也应避免产生含糊的同名作业；如果规则指定检查来源，还需确认结果确实由预期的可信应用提供。&lt;/p&gt;
&lt;p&gt;还要查看合并请求检查框显示的版本。&lt;code&gt;pull_request&lt;/code&gt; 运行通常检查的是合成的合并提交，而不只是贡献者分支末端。合并队列为候选组合生成检查时，需要独立的 &lt;code&gt;merge_group&lt;/code&gt; 事件。本地构建成功，或者手动触发过某份工作流，都不能自动替代规则要求的事件与版本；检查对象不一致时，即使输出相同，也不构成同一次验证。&lt;/p&gt;
&lt;h2 id="明确依赖结果"&gt;明确依赖结果&lt;/h2&gt;
&lt;p&gt;没有依赖的作业可以并行运行，单个作业里的步骤通常顺序执行。&lt;code&gt;needs&lt;/code&gt; 建立前置关系，条件控制失败或跳过之后哪些工作仍会执行。只有一个必需测试作业的工作流，并不需要人为增加依赖作业，也不必额外添加事件条件。因此，把缺少这些字段一律判成错误的检查器，无法准确判断门禁是否有效。&lt;/p&gt;
&lt;p&gt;如果使用汇总门禁，应等待每个必需作业，并显式检查其结果。通过 &lt;code&gt;if: ${{ always() }}&lt;/code&gt; 避免前置失败后只把门禁跳过，再要求结果确实为成功。如果某项可选工作允许跳过，应明确编码这个例外，而不是把所有缺失结果都视为通过。必需测试也不能随意开启忽略错误，否则一条普通的成功提示就可能掩盖没有完成的验证。&lt;/p&gt;
&lt;h2 id="权限声明约束令牌不约束所有凭据"&gt;权限声明约束令牌，不约束所有凭据&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;permissions&lt;/code&gt; 控制的是 &lt;code&gt;GITHUB_TOKEN&lt;/code&gt;，不会限制另行传给命令的其他秘密凭据。严格的工作流级声明可以由作业继承，不需要每个作业机械重复同样内容。只有真正需要的地方才增加权限。下面的离线实验没有检出私有源码，也不调用 GitHub 接口，因此不使用令牌；真实项目接入源码时，应重新检查实际最低权限需求。&lt;/p&gt;
&lt;p&gt;普通的复刻仓库合并请求通常收到只读令牌，而不会收到仓库秘密，具体仍受仓库设置影响。“秘密只读”不是准确说法，令牌权限与秘密可用性是两个问题。不能为了取得秘密，就改用具有更高权限的 &lt;code&gt;pull_request_target&lt;/code&gt; 并执行不可信的请求代码。日志遮盖只是辅助保护，不是打印凭据的许可，也不保证经过转换后的值绝不会泄漏。&lt;/p&gt;
&lt;h2 id="坏掉的样子"&gt;坏掉的样子&lt;/h2&gt;
&lt;p&gt;整个工作流因路径或分支过滤没有运行时，对应必需检查可能一直等待；作业因条件而跳过，却可能在没有执行测试的情况下满足检查规则。这是两种不同结果。修改检查名称时应小心，并在托管服务上验证失败、跳过和取消路径，不能只用一次成功运行证明规则配置正确。检查能否出现、是否真正运行和能否阻止合并，需要分别确认。&lt;/p&gt;
&lt;p&gt;缓存需要兼容的键，也需要按依赖声明执行可靠安装。普通合并请求产生的缓存属于该请求的合并引用，仅仅键相同，并不意味着基础分支可以恢复它。不能笼统声称任何复刻请求缓存都能污染主分支；应结合事件、引用范围和信任关系判断风险。另外，测试取消与部署恢复也不同：取消正在运行的作业，不会自动撤销它已经产生的外部变化，重试必须了解实际完成到了哪里。&lt;/p&gt;
&lt;h2 id="快速验证"&gt;快速验证&lt;/h2&gt;
&lt;p&gt;下面实验单独检查一项汇总门禁决定：只有前置作业的结果严格等于 &lt;code&gt;success&lt;/code&gt; 才接受。在真实工作流中，这个值应来自 &lt;code&gt;needs.test.result&lt;/code&gt;，并通过 &lt;code&gt;needs&lt;/code&gt; 声明前置依赖。这里的输入则刻意采用合成状态，是本地判断练习，不是工作流校验器，也不能替代项目真实测试。它不会安装工作流或启动托管运行，只让这一项判定逻辑能够独立复现。&lt;/p&gt;
&lt;p&gt;把下面仅用标准库的探针保存为 &lt;code&gt;gate_check.py&lt;/code&gt;，在有 Python 3 和 Bash 的环境运行 &lt;code&gt;python3 gate_check.py&lt;/code&gt;。它向同一条 Shell 判断传入合成结果，并包括一个故意失败的测试断言。程序不写文件，不访问任何服务，也不使用真实仓库凭据；需要的解释器和 Shell 必须已经可用。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;os&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;subprocess&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;sys&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;executable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;-c&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;assert 2 + 2 == 4&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;negative&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;executable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;-c&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;assert 2 + 2 == 5&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;negative&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;returncode&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;AssertionError&amp;#34;&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;negative&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;test-negative-control: rejected&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;command&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;test &amp;#34;$TEST_RESULT&amp;#34; = success&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;success&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;failure&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;cancelled&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;skipped&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;unexpected&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;bash&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;--noprofile&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;--norc&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;-c&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;PATH&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;PATH&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;TEST_RESULT&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;accepted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;returncode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;accepted&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;success&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;lt;missing&amp;gt;&amp;#39;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;accept&amp;#39;&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;accepted&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;reject&amp;#39;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;PASS: local predicate only; GitHub enforcement not exercised&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;预期故意失败的测试被拒绝，只有 &lt;code&gt;success&lt;/code&gt; 被接受，最后输出范围提醒。缺失、异常、失败、取消及跳过结果都不能被当成真实成功。这证明了本地命令和拒绝分支，但没有解析任意 YAML、求值 GitHub 表达式、调度作业、读取仓库规则，也没有证明线上合并会被阻止。这些仍需针对真实仓库配置进行托管集成检查，不能用离线探针的成功代替。&lt;/p&gt;
&lt;h2 id="延伸阅读"&gt;延伸阅读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.xbcatm.com/zh/posts/ci-artifacts-are-transient-handoffs/"&gt;CI 产物需要明确的交接契约&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.xbcatm.com/zh/posts/secrets-need-expiry-not-just-rotation/"&gt;凭据到期必须由验证方执行&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions"&gt;GitHub：工作流语法&lt;/a&gt;，依赖、条件和令牌权限继承，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/actions/learn-github-actions/understanding-github-actions"&gt;GitHub：Actions 入门&lt;/a&gt;，工作流、事件、作业和步骤，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/pull-requests/how-tos/merge-and-close-pull-requests/troubleshooting-required-status-checks"&gt;GitHub：排查必需状态检查&lt;/a&gt;，版本身份、跳过检查和合并队列，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows"&gt;GitHub：工作流事件&lt;/a&gt;，复刻请求和高权限触发器的边界，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/actions/reference/workflows-and-actions/dependency-caching"&gt;GitHub：依赖缓存&lt;/a&gt;，分支与合并请求缓存范围，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-secrets"&gt;GitHub：使用秘密&lt;/a&gt;，秘密可用性与日志处理，已核验 2026-10-15。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>