@goodtek/vibeops 3.1.8 → 3.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,12 @@ All notable changes to VibeOps are documented here.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 3.1.9 - 2026-10-17
8
+
9
+ ### Fixed
10
+
11
+ - **The merge gate was blocked by GitHub's dynamic workflow runs.** A failed Dependabot run on the head commit (`event: dynamic`, `dynamic/dependabot/dependabot-updates`) refused a PR whose CI was green — and such runs cannot be re-run. Dynamic runs and `(dynamic)` check runs are now ignored; a `(dynamic)` check named in `requiredChecks` is still required (D-011, TASK-043).
12
+
7
13
  ## 3.1.8 - 2026-10-17
8
14
 
9
15
  ### Changed
package/README.md CHANGED
@@ -218,6 +218,7 @@ Before calling `gh pr merge` / `glab mr merge`:
218
218
  - **Expected workflows:** every `.github/workflows/*.yml|yaml` at the head commit whose `on:` triggers `pull_request` for this PR, using GitHub's filter rules — `branches` / `branches-ignore` against the base branch, `paths` / `paths-ignore` against the PR's changed files (first 300, like GitHub), and `types` (a custom list must contain both `opened` and `synchronize`). Disabled workflows are skipped; `pull_request_target`, `workflow_call` and other events are not awaited. Each expected workflow must have a `pull_request` run for the head SHA — until it appears the gate **waits** (`workflow … (pull_request) has not started for <sha> yet`), then refuses at `waitTimeoutSeconds`.
219
219
  - **All runs for the head SHA** (any event, latest run per workflow + event) must be `completed` with `success`, `neutral` or `skipped`. A queued / in-progress / waiting run waits; any other conclusion refuses immediately — including `startup_failure` (no jobs, so nothing in the rollup) and `action_required` (fork PR awaiting approval).
220
220
  - The Actions API is required: if it cannot be read, the gate keeps waiting (back-off) and refuses at the timeout — it never merges blind.
221
+ - **Dynamic workflows are not CI:** runs GitHub generates on the fly — `event: dynamic`, e.g. `dynamic/dependabot/dependabot-updates`, the jobs that open Dependabot PRs — and check runs suffixed `(dynamic)` are ignored. GitHub documents them as "generated for a specific run and … not stored in your repository's `.github/workflows` directory" ([Dependabot on GitHub Actions runners](https://docs.github.com/en/code-security/dependabot/working-with-dependabot/about-dependabot-on-github-actions-runners)); a failed one says nothing about the commit and cannot be re-run. A `(dynamic)` check named in `requiredChecks` is still required.
221
222
  - **GitLab:** the MR head pipeline must be `success` or `skipped`. `failed`, `canceled`, `manual` and unknown statuses refuse; `created` / `pending` / `running` / `scheduled` / … wait.
222
223
  - **Host output is verified:** if `gh pr view` lacks or has empty `state`, `mergeable`, `headRefOid`, or `statusCheckRollup` is not a list (GitLab: `state`, `sha`, `merge_status`, `detailed_merge_status`, `head_pipeline`), the merge is refused.
223
224
  - **Failed check → refuse immediately** (exit code 1, checks named). **Pending → wait** up to `waitTimeoutSeconds`, then refuse. There is no override flag: fix CI (re-run `task ship`) and rerun `task merge`.
@@ -94,6 +94,15 @@ export function checkNameMatches(name, pattern) {
94
94
  * - any pending, or a required check not (yet) present → pending
95
95
  * - otherwise green (including "no checks at all" when nothing is required)
96
96
  */
97
+ /**
98
+ * Check runs GitHub suffixes with " (dynamic)" belong to dynamic workflows (see
99
+ * `isDynamicRun` in github-actions-gate.ts), not to the commit's CI: dropped unless a required
100
+ * check pattern names them.
101
+ */
102
+ export function withoutDynamicChecks(checks, requiredChecks = []) {
103
+ return checks.filter((c) => !(c.kind === "CheckRun" && c.name.endsWith(" (dynamic)")) ||
104
+ requiredChecks.some((p) => checkNameMatches(c.name, p)));
105
+ }
97
106
  export function evaluateCheckGate(checks, requiredChecks = []) {
98
107
  const failed = checks.filter((c) => c.verdict === "failed");
99
108
  const pending = checks.filter((c) => c.verdict === "pending");
@@ -171,6 +171,17 @@ export async function workflowExpectedForPullRequest(yamlText, ctx) {
171
171
  return { expected: true };
172
172
  }
173
173
  const RUN_PASS = new Set(["success", "neutral", "skipped"]);
174
+ /**
175
+ * GitHub-generated runs that are not the commit's CI: "dynamic workflows are generated for a
176
+ * specific run and are not stored in your repository's `.github/workflows` directory" — e.g.
177
+ * the jobs that generate Dependabot pull requests, `dynamic/dependabot/dependabot-updates`
178
+ * (docs.github.com/code-security/dependabot/working-with-dependabot/about-dependabot-on-github-actions-runners).
179
+ * Their result says nothing about the head commit, and a failed one cannot be re-run, so the
180
+ * gate ignores them (required checks named in `.vibeops.json` are still judged by name).
181
+ */
182
+ export function isDynamicRun(run) {
183
+ return run.event === "dynamic" || run.path.startsWith("dynamic/");
184
+ }
174
185
  function runPath(run) {
175
186
  return run.path.split("@")[0];
176
187
  }
@@ -180,11 +191,13 @@ function describeRun(run, detail) {
180
191
  /**
181
192
  * Latest run per (workflow, event) must be completed and passing; every
182
193
  * expected workflow must have a `pull_request` run. Unknown statuses /
183
- * conclusions fail closed.
194
+ * conclusions fail closed. Dynamic runs ({@link isDynamicRun}) are ignored.
184
195
  */
185
196
  export function evaluateActionsRuns(runs, expectedPaths) {
186
197
  const latest = new Map();
187
198
  for (const run of runs) {
199
+ if (isDynamicRun(run))
200
+ continue;
188
201
  const key = `${runPath(run)}\u0000${run.event}`;
189
202
  const prev = latest.get(key);
190
203
  if (prev === undefined || run.id > prev.id)
@@ -1,9 +1,9 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
3
  import { mergeRequestLabel } from "./git-host.js";
4
- import { classifyRollup, evaluateCheckGate, formatCheckGateProblems, } from "./check-rollup.js";
4
+ import { classifyRollup, evaluateCheckGate, withoutDynamicChecks, formatCheckGateProblems, } from "./check-rollup.js";
5
5
  import { isMergeRequestReadyToMerge, isPipelineActive, pipelineGateState, pipelineStatusFromHost, } from "./merge-request-readiness.js";
6
- import { evaluateActionsRuns, listExpectedPullRequestWorkflows, listWorkflowRunsForSha, parseGitHubPullUrl, } from "./github-actions-gate.js";
6
+ import { evaluateActionsRuns, isDynamicRun, listExpectedPullRequestWorkflows, listWorkflowRunsForSha, parseGitHubPullUrl, } from "./github-actions-gate.js";
7
7
  import { dim, log } from "./logger.js";
8
8
  import { checkBaseInHead } from "./merge-base-freshness.js";
9
9
  import { MERGE_GATE_DEFAULTS, mergeGateTiming } from "./merge-config.js";
@@ -292,7 +292,10 @@ export async function readMergeRequestDetails(cwd, host, url, options) {
292
292
  throw new MergeRequestShapeError("gh", problems);
293
293
  }
294
294
  const checks = classifyRollup(parsed.statusCheckRollup);
295
- const gate = evaluateCheckGate(checks);
295
+ // Dynamic workflows' check runs are not the commit's CI (see `withoutDynamicChecks`);
296
+ // required ones are still judged by name in the merge gate.
297
+ const ciChecks = withoutDynamicChecks(checks);
298
+ const gate = evaluateCheckGate(ciChecks);
296
299
  let pipelineStatus = "none";
297
300
  if (gate.state === "red") {
298
301
  pipelineStatus = "failed";
@@ -300,7 +303,7 @@ export async function readMergeRequestDetails(cwd, host, url, options) {
300
303
  else if (gate.state === "pending") {
301
304
  pipelineStatus = "running";
302
305
  }
303
- else if (checks.length > 0) {
306
+ else if (ciChecks.length > 0) {
304
307
  pipelineStatus = "success";
305
308
  }
306
309
  const mergeable = parsed.mergeable?.trim().toUpperCase() ?? "";
@@ -340,11 +343,12 @@ export class MergeGateError extends Error {
340
343
  }
341
344
  function evaluateDetailsGate(details, requiredChecks) {
342
345
  if (details.checks !== null) {
343
- const gate = evaluateCheckGate(details.checks, requiredChecks);
346
+ const checks = withoutDynamicChecks(details.checks, requiredChecks);
347
+ const gate = evaluateCheckGate(checks, requiredChecks);
344
348
  return {
345
349
  state: gate.state,
346
350
  problems: formatCheckGateProblems(gate),
347
- noChecks: details.checks.length === 0,
351
+ noChecks: checks.length === 0,
348
352
  };
349
353
  }
350
354
  const state = pipelineGateState(details.pipelineStatus);
@@ -383,7 +387,7 @@ async function evaluateGitHubActions(cwd, url, details, expectedCache) {
383
387
  expected = await listExpectedPullRequestWorkflows(cwd, pr, sha, details.baseBranch);
384
388
  expectedCache.set(sha, expected);
385
389
  }
386
- const runs = await listWorkflowRunsForSha(cwd, pr, sha);
390
+ const runs = (await listWorkflowRunsForSha(cwd, pr, sha)).filter((r) => !isDynamicRun(r));
387
391
  return {
388
392
  result: evaluateActionsRuns(runs, expected.map((w) => w.path)),
389
393
  runCount: runs.length,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodtek/vibeops",
3
- "version": "3.1.8",
3
+ "version": "3.1.9",
4
4
  "description": "Minimal CLI rails for Cursor-based TASK-driven vibe coding.",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@9.15.9",
@@ -62,7 +62,7 @@
62
62
  "devDependencies": {
63
63
  "@eslint/js": "10.0.1",
64
64
  "@types/node": "^20.14.0",
65
- "eslint": "10.11.0",
65
+ "eslint": "10.12.0",
66
66
  "tsx": "^4.23.15",
67
67
  "typescript": "^5.5.4",
68
68
  "typescript-eslint": "8.71.0"