Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 139 additions & 9 deletions .github/workflows/qwen-autofix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -747,6 +747,9 @@ jobs:
exit 1
fi

# The npm-ci retry recipe and the 'Prepare Qwen Code CLI' shim below
# are duplicated in build-cli and review-address; the workflow
# contract tests pin every copy in lockstep — edit them together.
- name: 'Install dependencies and build'
env:
QWEN_SKIP_PREPARE: '1'
Expand Down Expand Up @@ -2586,12 +2589,107 @@ jobs:
echo "targets=${TARGETS}" >> "${GITHUB_OUTPUT}"
echo "has_targets=$([[ "${COUNT}" -gt 0 ]] && echo true || echo false)" >> "${GITHUB_OUTPUT}"

# ===========================================================================
# REVIEW PHASE (build) — compile the trusted-base CLI bundle ONCE per scan
# and fan it out to the address legs as an artifact. Each leg otherwise
# repeated the same base build: measured 3.5-5 minutes of npm ci + build +
# bundle per leg (6 legs ≈ 25 runner-minutes on one 2026-08 scan) before
# the agent could start. Gated on has_targets so an idle tick builds
# nothing — the issue phase runs only when there are no review targets, so
# gating on do_issue too would rebuild on every quiet scheduled tick.
# fetch-depth 1 suffices: nothing here needs history, and the legs (full
# fetch) always contain this commit as an ancestor of their base fetch.
# ===========================================================================
build-cli:
needs: ['route', 'review-scan']
if: |-
${{ needs.review-scan.outputs.has_targets == 'true' }}
runs-on: 'ubuntu-latest'
timeout-minutes: 30
permissions:
contents: 'read'
outputs:
base_sha: '${{ steps.meta.outputs.base_sha }}'
steps:
- name: 'Checkout trusted base'
uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
with:
ref: '${{ github.event.repository.default_branch }}'
fetch-depth: 1
persist-credentials: false

- name: 'Set up Node.js (hosted)'
if: |-
${{ runner.environment == 'github-hosted' }}
uses: 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e' # v6.4.0
with:
node-version: '22.x'
cache: 'npm'
cache-dependency-path: 'package-lock.json'

# The npm-ci retry recipe and the 'Prepare Qwen Code CLI' shim below
# are duplicated in issue-autofix and review-address; the workflow
# contract tests pin every copy in lockstep — edit them together.
- name: 'Install dependencies and build'
env:
QWEN_SKIP_PREPARE: '1'
Comment on lines +2633 to +2635

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] This diff adds a third copy of the npm-ci retry + build + bundle recipe and a third byte-identical qwen shim heredoc, leaving the build procedure triplicated across issue-autofix, build-cli, and review-address — all three sites are touched by this change. Cross-site agreement on node-version: '22.x', the GITHUB_PATH wiring, chmod, and the --version smoke is pinned by no test.

Failure scenario: a partial recipe edit (Node version bump applied to two of the three jobs, npm flag, backoff tweak) passes the whole suite and silently diverges build-cli's bundle from what the legs' shims point at — the exact class of mismatch this PR's SHA-pinning was written to prevent, reintroduced at the recipe level.

Suggested fix: extract the shared install/build/bundle + shim-staging block into a composite action (e.g. .github/actions/prepare-qwen-cli) invoked by all three jobs; at minimum add cross-reference comments at each copy and extend the contract tests to pin node-version and the GITHUB_PATH wiring across all three sites.

中文说明

本 diff 新增了第三份 npm-ci 重试 + build + bundle 配方和第三份逐字节相同的 qwen shim heredoc,使构建配方在 issue-autofixbuild-clireview-address 三处重复——三处都在本次改动中被触及。三处之间 node-version: '22.x'GITHUB_PATH 接线、chmod--version 冒烟的一致性没有任何测试固定。

失败场景:一次局部的配方修改(对三个 job 中的两个升级 Node 版本、npm 参数、退避调整)会通过整套测试,并悄悄让 build-cli 的 bundle 与各 leg 的 shim 指向产生偏差——正是本 PR 的 SHA 固定要防止的那类不匹配,在配方层面被重新引入。

建议修复:把共享的安装/构建/打包 + shim 部署代码块提取为复合 action(如 .github/actions/prepare-qwen-cli)供三个 job 调用;至少在每个副本处加交叉引用注释,并扩展契约测试以固定三处的 node-versionGITHUB_PATH 接线。

— qwen3.8-max via Qwen Code /review (v0.21.5)

run: |-
for attempt in 1 2 3; do
if npm ci --prefer-offline --no-audit --progress=false; then
break
fi
if [[ "${attempt}" == "3" ]]; then
exit 1
fi
sleep $((attempt * 15))
done
npm run build
npm run bundle

# Producer-side sanity: a bundle that cannot even answer --version
# fails THIS job once, instead of every leg failing its restore or
# first qwen invocation minutes later and N times over.
- name: 'Prepare Qwen Code CLI'
run: |-
qwen_version="$(node -p "require('./package.json').version")"
echo "Using checked-out Qwen Code bundle ${qwen_version}"
qwen_bin="${RUNNER_TEMP}/qwen-bin"
mkdir -p "${qwen_bin}"
cat > "${qwen_bin}/qwen" <<'EOF'
#!/usr/bin/env bash
exec node "${GITHUB_WORKSPACE}/dist/cli.js" "$@"
EOF
chmod +x "${qwen_bin}/qwen"
echo "${qwen_bin}" >> "${GITHUB_PATH}"
PATH="${qwen_bin}:${PATH}"
qwen --version

# Only the repo-root dist/ is shipped: copy_bundle_assets.js already
# gathers every runtime asset (chunks, vendor, web-shell, locales)
# under it. packages/*/dist would triple the artifact size and is
# never read by the legs — the verify gate's full `npm run build`
# wipes and rebuilds each package's dist from branch sources
# (build_package.js rms it first, so no staleness leaks through).
- name: 'Upload CLI bundle'
id: 'meta'
run: |-
echo "base_sha=$(git rev-parse HEAD)" >> "${GITHUB_OUTPUT}"
tar -czf "${RUNNER_TEMP}/qwen-cli-dist.tar.gz" dist

- name: 'Publish CLI bundle artifact'
uses: 'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' # v7.0.1
with:
name: 'qwen-autofix-cli-dist'
path: '${{ runner.temp }}/qwen-cli-dist.tar.gz'
retention-days: 1
if-no-files-found: 'error'

# ===========================================================================
# REVIEW PHASE (address) — one job per PR: triage, address (incl. conflict
# resolution), verify, push, and report.
# ===========================================================================
review-address:
needs: ['route', 'review-scan']
needs: ['route', 'review-scan', 'build-cli']
if: |-
${{ needs.review-scan.outputs.has_targets == 'true' }}
runs-on: 'ubuntu-latest'
Expand Down Expand Up @@ -2660,14 +2758,30 @@ jobs:
HEAD_REPO: '${{ matrix.target.head_repo }}'
steps:
# SECURITY: checkout trusted base code first. The PR branch is checked
# out later in "Prepare branch and feedback" after the trusted CLI bundle
# is built from this base. Without this pin, pull_request_review events
# would check out the PR merge ref by default, letting PR-controlled
# code influence the secret-bearing address run.
# out later in "Prepare branch and feedback" after the trusted CLI
# bundle (built once from this base in build-cli) is in place. Without
# this pin, pull_request_review events would check out the PR merge ref
# by default, letting PR-controlled code influence the secret-bearing
# address run. The ref is pinned to the SHA build-cli compiled — not
# the live default branch — so a mid-run base push can never leave a
# leg running a bundle built from DIFFERENT sources than its checkout.
# The SHA is validated fail-loud FIRST: actions/checkout resolves an
# empty ref to the event default — on pull_request_review triggers the
# PR merge ref — so a broken build-cli output must fail this leg
# instead of silently unpinning it.
- name: 'Validate bundle SHA'
env:
BASE_SHA: '${{ needs.build-cli.outputs.base_sha }}'
run: |-
if [[ ! "${BASE_SHA}" =~ ^[0-9a-f]{40}$ ]]; then
echo "::error::build-cli published no usable base_sha ('${BASE_SHA:-<empty>}') — refusing to fall back to the event-default ref."
exit 1
fi

- name: 'Checkout trusted base'
uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
with:
ref: '${{ github.event.repository.default_branch }}'
ref: '${{ needs.build-cli.outputs.base_sha }}'
Comment on lines 2783 to +2784

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] This ref pin degrades fail-open: if needs.build-cli.outputs.base_sha ever resolves empty, actions/checkout silently falls back to the event-default ref — on pull_request_review triggers that is refs/pull/N/merge, the contributor-controlled tree the adjacent SECURITY comment says this pin exists to prevent. Verified against the pinned action's source (input-helper.ts: empty ref falls through to github.context.ref), and no guard validates the SHA before checkout. The current wiring is sound (hence Suggestion, not Critical), but this diff converted an always-present context pin into a cross-job output reference that can resolve empty, and the test suite is proven not a backstop — renaming id: 'meta' survives 126/126.

Failure scenario: a future rename of the step id or a typo in the output name → ref: '' → every leg silently checks out the PR merge ref → PR-influenced .github/scripts/* and run-agent.mjs are staged as the "trusted" copies and PR-influenced npm lifecycle scripts run at npm ci time, in the job carrying CI_DEV_BOT_PAT and the model key.

Suggested fix — a fail-loud validation step before this checkout (a job-level if: is inadequate: it only skips, silently stalling the fleet; skip ≠ fail-closed):

      - name: 'Validate bundle SHA'
        env:
          BASE_SHA: '${{ needs.build-cli.outputs.base_sha }}'
        run: |-
          if [[ ! "${BASE_SHA}" =~ ^[0-9a-f]{40}$ ]]; then
            echo "::error::build-cli published no usable base_sha ('${BASE_SHA:-<empty>}') — refusing to fall back to the event-default ref."
            exit 1
          fi
中文说明

此 ref 固定以 fail-open 方式退化:一旦 needs.build-cli.outputs.base_sha 解析为空,actions/checkout 会静默回退到事件默认 ref——在 pull_request_review 触发器下即 refs/pull/N/merge,也就是相邻 SECURITY 注释声明本固定要防止的贡献者可控代码树。已对照固定版本的 action 源码核实(input-helper.ts:空 ref 会落到 github.context.ref),且 checkout 前没有任何校验该 SHA 的保护。当前接线是健全的(因此是 Suggestion 而非 Critical),但本 diff 把一个永远存在的上下文固定转换成了可能解析为空的跨 job 输出引用,而测试套件已被证明不是兜底——将 id: 'meta' 改名后 126/126 测试仍全绿。

失败场景:未来某次 step id 改名或输出名拼写错误 → ref: '' → 每条 leg 静默 checkout PR 合并 ref → PR 可控的 .github/scripts/*run-agent.mjs 被当作 "trusted" 副本部署,PR 可控的 npm 生命周期脚本在 npm ci 时运行,而该 job 携带 CI_DEV_BOT_PAT 与模型密钥。

建议修复——在此 checkout 之前增加一个 fail-loud 校验步骤(job 级 if: 不够:它只会 skip,使整个队列静默停摆;skip ≠ fail-closed),代码见上。

— qwen3.8-max via Qwen Code /review (v0.21.5)

fetch-depth: 0
persist-credentials: false

Expand Down Expand Up @@ -2736,7 +2850,14 @@ jobs:
exit 1
fi

- name: 'Install dependencies and build'
# npm ci only — the build itself ran ONCE in build-cli. The legs still
# need node_modules: the agent builds/tests the PR branch, and the
# verify gate rebuilds from branch sources (wiping the restored
# packages/*/dist via build_package.js, so no base staleness leaks
# into branch verification). The retry recipe (and the shim below) is
# duplicated in issue-autofix and build-cli; the workflow contract
# tests pin every copy in lockstep — edit them together.
- name: 'Install dependencies'
env:
QWEN_SKIP_PREPARE: '1'
run: |-
Expand All @@ -2750,8 +2871,17 @@ jobs:
sleep $((attempt * 15))
done
git config core.hooksPath .husky
npm run build
npm run bundle

- name: 'Download CLI bundle'
uses: 'actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c' # v8.0.1
with:
name: 'qwen-autofix-cli-dist'
path: '${{ runner.temp }}/cli-dist'

- name: 'Restore CLI bundle'
run: |-
tar -xzf "${RUNNER_TEMP}/cli-dist/qwen-cli-dist.tar.gz"
test -f dist/cli.js

- name: 'Prepare Qwen Code CLI'
run: |-
Expand Down
19 changes: 13 additions & 6 deletions scripts/tests/package-scripts.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -520,18 +520,25 @@ describe('package scripts', () => {
it('skips autofix install-time prepare without disabling dependency scripts', () => {
const workflow = readWorkflow('.github/workflows/qwen-autofix.yml');

for (const jobName of ['issue-autofix', 'review-address']) {
// review-address restores the shared build-cli bundle instead of
// compiling, so its install step is npm ci only; the other two jobs
// still build from sources. Husky hooks are re-armed after the
// prepare-skip only where git commits happen (build-cli never commits).
for (const [jobName, stepName, armsHooks] of [
['issue-autofix', 'Install dependencies and build', true],
['build-cli', 'Install dependencies and build', false],
['review-address', 'Install dependencies', true],
]) {
const job = getWorkflowJob(workflow, jobName);
const installStep = getWorkflowStep(
job,
'Install dependencies and build',
);
const installStep = getWorkflowStep(job, stepName);

expect(installStep).toContain("QWEN_SKIP_PREPARE: '1'");
expect(installStep).toContain(
'npm ci --prefer-offline --no-audit --progress=false',
);
expect(installStep).toContain('git config core.hooksPath .husky');
if (armsHooks) {
expect(installStep).toContain('git config core.hooksPath .husky');
}
expect(installStep).not.toContain('--ignore-scripts');
}
});
Expand Down
119 changes: 118 additions & 1 deletion scripts/tests/qwen-autofix-workflow.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,10 @@ const installAndBuildSteps =
workflow.match(
/- name: 'Install dependencies and build'[\s\S]*?(?=\n[ ]{6}- name: ')/g,
) ?? [];
const nodeSetupSteps =
workflow.match(
/- name: 'Set up Node.js \(hosted\)'[\s\S]*?(?=\n[ ]{6}- name: ')/g,
) ?? [];

function readAutofixSkill() {
return readFileSync('.qwen/skills/autofix/SKILL.md', 'utf8');
Expand Down Expand Up @@ -5205,6 +5209,7 @@ describe('qwen-autofix workflow', () => {

expect(workflow).toMatch(/issue-autofix:[\s\S]*?runs-on: 'ubuntu-latest'/);
expect(workflow).toMatch(/review-address:[\s\S]*?runs-on: 'ubuntu-latest'/);
expect(workflow).toMatch(/build-cli:[\s\S]*?runs-on: 'ubuntu-latest'/);
expect(workflow).not.toContain(
'["self-hosted", "linux", "x64", "autofix"]',
);
Expand All @@ -5214,7 +5219,9 @@ describe('qwen-autofix workflow', () => {
expect(workflow).toContain(
"RUNNER_ENVIRONMENT: '${{ runner.environment }}'",
);
expect(prepareQwenCliSteps).toHaveLength(2);
// issue-autofix, build-cli, and review-address each stage the qwen shim
// against the workspace bundle.
expect(prepareQwenCliSteps).toHaveLength(3);
for (const step of prepareQwenCliSteps) {
expect(step).toContain(
'qwen_version="$(node -p "require(\'./package.json\').version")"',
Expand All @@ -5223,6 +5230,9 @@ describe('qwen-autofix workflow', () => {
'exec node "${GITHUB_WORKSPACE}/dist/cli.js" "$@"',
);
expect(step).toContain('qwen-bin');
expect(step).toContain('chmod +x "${qwen_bin}/qwen"');
expect(step).toContain('echo "${qwen_bin}" >> "${GITHUB_PATH}"');
expect(step).toContain('qwen --version');
expect(step).not.toContain('current_version="$(qwen --version');
expect(step).not.toContain('Using pre-installed Qwen Code');
expect(step).not.toContain('npm install -g');
Expand Down Expand Up @@ -5279,6 +5289,8 @@ describe('qwen-autofix workflow', () => {
});

it('retries dependency installation before building', () => {
// issue-autofix and build-cli build from sources; review-address restores
// the shared bundle instead (see 'builds the review CLI bundle once...').
expect(installAndBuildSteps).toHaveLength(2);
for (const step of installAndBuildSteps) {
expect(step).toContain('for attempt in 1 2 3; do');
Expand All @@ -5291,6 +5303,111 @@ describe('qwen-autofix workflow', () => {
}
});

it('keeps the Node setup recipe identical across the autofix jobs', () => {
// The three setup steps are lockstep copies; a partial recipe edit (a
// Node bump applied to two of the three jobs) must not ship green.
expect(nodeSetupSteps).toHaveLength(3);
for (const step of nodeSetupSteps) {
expect(step).toContain(
'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e',
);
expect(step).toContain("node-version: '22.x'");
expect(step).toContain("cache: 'npm'");
expect(step).toContain("cache-dependency-path: 'package-lock.json'");
}
});

it('builds the review CLI bundle once and fans it out to the address legs', () => {
// Measured driver: 6 review-address legs each spent 3.5-5 minutes on
// npm ci + build + bundle of the SAME trusted base (~25 runner-minutes
// per scan) before the agent could start. The legs download the shared
// artifact instead; only their npm ci remains (the agent and the verify
// gate still need node_modules against the PR branch).
const buildCliJob =
workflow.match(
/\n {2}build-cli:[\s\S]*?(?=\n {2}review-address:)/,
)?.[0] ?? '';
const addressJob =
workflow.match(/\n {2}review-address:[\s\S]*$/)?.[0] ?? '';
const stepOf = (job, name) =>
job.match(
new RegExp(`- name: '${name}'[\\s\\S]*?(?=\\n[ ]{6}- name: |$)`),
)?.[0] ?? '';
expect(buildCliJob).toBeTruthy();
expect(addressJob).toBeTruthy();

expect(buildCliJob).toContain("needs: ['route', 'review-scan']");
expect(addressJob).toContain(
"needs: ['route', 'review-scan', 'build-cli']",
);
// An idle tick (no review targets) must not spend a build; the issue
// phase only runs when there are none, so gating on do_issue too would
// rebuild on every quiet scheduled tick.
expect(buildCliJob).toContain(
"needs.review-scan.outputs.has_targets == 'true'",
);
expect(buildCliJob).not.toContain('do_issue');

// The leg checks out the SHA the bundle was compiled from — a mid-run
// base push can never leave a leg running a bundle built from different
// sources than its checkout.
expect(buildCliJob).toContain(
"base_sha: '${{ steps.meta.outputs.base_sha }}'",
);
Comment on lines +5354 to +5356

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The new fan-out test pins each side of the SHA chain but not the link itself: nothing asserts the step writing base_sha actually carries id: 'meta' — which is what makes steps.meta.outputs.base_sha resolve. Probed empirically: renaming id: 'meta'id: 'meta2' survives the full suite (126/126). At runtime a nonexistent step id resolves to '', so needs.build-cli.outputs.base_sha becomes empty and every leg checks out ref: '' — falling back to the event-default ref, which on pull_request_review triggers is the PR merge ref, the exact checkout the SECURITY comment in the workflow exists to prevent.

Failure scenario: a future rename of the step id ships with a green suite → every leg silently checks out the PR merge ref in a secret-bearing run.

Suggested change
expect(buildCliJob).toContain(
"base_sha: '${{ steps.meta.outputs.base_sha }}'",
);
expect(buildCliJob).toContain(
"base_sha: '${{ steps.meta.outputs.base_sha }}'",
);
expect(stepOf(buildCliJob, 'Upload CLI bundle')).toContain("id: 'meta'");
中文说明

新增的 fan-out 测试固定了 SHA 链的两端,但没有固定链本身:没有任何断言保证写出 base_sha 的步骤携带 id: 'meta'——而正是它让 steps.meta.outputs.base_sha 能够解析。经实测:将 id: 'meta' 改名为 id: 'meta2' 后整套测试仍然通过(126/126)。运行时,不存在的 step id 会静默解析为 '',于是 needs.build-cli.outputs.base_sha 变为空,每条 leg 以 ref: '' checkout——回退到事件默认 ref,在 pull_request_review 触发器下即 PR 合并 ref,正是 workflow 中 SECURITY 注释要防止的 checkout。

失败场景:未来某次 step id 改名会在测试全绿的情况下上线 → 每条 leg 在携带密钥的运行中静默 checkout PR 合并 ref。

— qwen3.8-max via Qwen Code /review (v0.21.5)

expect(buildCliJob).toContain(
'echo "base_sha=$(git rev-parse HEAD)" >> "${GITHUB_OUTPUT}"',
);
// The step id is the LINK between those two pins: without id: 'meta',
// steps.meta.outputs.base_sha resolves to '' at runtime and every leg
// silently checks out the event-default ref.
expect(stepOf(buildCliJob, 'Upload CLI bundle')).toContain("id: 'meta'");
expect(stepOf(addressJob, 'Checkout trusted base')).toContain(
"ref: '${{ needs.build-cli.outputs.base_sha }}'",
);
// The guard must fail the leg LOUD before the checkout: an empty ref
// makes actions/checkout fall back to the event default — on
// pull_request_review triggers the PR merge ref.
const validateShaStep = stepOf(addressJob, 'Validate bundle SHA');
expect(validateShaStep).toContain(
"BASE_SHA: '${{ needs.build-cli.outputs.base_sha }}'",
);
expect(validateShaStep).toContain(
'if [[ ! "${BASE_SHA}" =~ ^[0-9a-f]{40}$ ]]; then',
);
expect(validateShaStep).toContain('exit 1');
expect(addressJob.indexOf("- name: 'Validate bundle SHA'")).toBeLessThan(
addressJob.indexOf("- name: 'Checkout trusted base'"),
);

// The artifact is the repo-root dist/ only — copy_bundle_assets.js
// already gathers every runtime asset under it; packages/*/dist would
// triple the size and is rebuilt from branch sources by the verify gate.
expect(buildCliJob).toContain(
'tar -czf "${RUNNER_TEMP}/qwen-cli-dist.tar.gz" dist',
);
expect(buildCliJob).toContain("name: 'qwen-autofix-cli-dist'");
expect(buildCliJob).toContain('retention-days: 1');
expect(buildCliJob).toContain("if-no-files-found: 'error'");

const restoreStep = stepOf(addressJob, 'Restore CLI bundle');
expect(restoreStep).toContain(
'tar -xzf "${RUNNER_TEMP}/cli-dist/qwen-cli-dist.tar.gz"',
);
expect(restoreStep).toContain('test -f dist/cli.js');
const downloadStep = stepOf(addressJob, 'Download CLI bundle');
expect(downloadStep).toContain("name: 'qwen-autofix-cli-dist'");
// Download directory and restore extract path are one contract — pin
// both sides so a rename of either fails this suite.
expect(downloadStep).toContain("path: '${{ runner.temp }}/cli-dist'");

// The leg itself never rebuilds the base bundle — that is the entire
// point of the fan-out.
const legInstall = stepOf(addressJob, 'Install dependencies');
expect(legInstall).toContain('npm ci --prefer-offline');
expect(legInstall).not.toContain('npm run build');
expect(legInstall).not.toContain('npm run bundle');
});

it('uses the standard checkout action for autonomous runner jobs', () => {
expect(workflow).toContain('actions/checkout@');
expect(workflow).not.toContain('Checkout with retry');
Expand Down
Loading