Repository navigation
ci: bump actions/checkout from 4 to 5 - #1
Closed
dependabot[bot] wants to merge 1 commit into
Closed
dependabot[bot] wants to merge 1 commit into
dependabot[bot] wants to merge 1 commit into
Conversation
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](actions/checkout@v4...v5) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com>
Contributor
Author
|
Superseded by #5. |
dependabot
Bot
deleted the
dependabot/github_actions/actions/checkout-5
branch
November 25, 2025 00:05
cabljac
added a commit
that referenced
this pull request
Apr 28, 2026
Address three more Copilot comments. All three are real: #1. Nested redaction blocks were leaking. The previous iterative-regex approach matched from the FIRST opening marker to the FIRST closing marker, which on properly-nested input (outer wraps inner) ate the inner pair and left an orphaned outer close marker plus the intended-private content between the inner close and the outer close. Replaced with `stripInternalBlocks` — a depth-tracking pass that walks all marker positions in document order and emits content only when depth=0. Defaults: unmatched close drops the marker and keeps surrounding content; unmatched open drops to end-of-input (fail-safe — better to drop too much than leak intended-private content upstream). Exported as `stripInternalBlocks` (with @internal JSDoc) so it can be unit-tested directly. 9 tests in tests/redaction.test.ts cover sibling, nested, whitespace tolerance, dangling close, unmatched open, multi-line, and lastIndex-reset cases. #2. The fallback compare URL (shown when --pr wasn't set or `gh pr create` failed) used `plan.upstreamDefaultBranch` instead of `baseBranch`. With `--base develop`, the URL pointed at `main`. Now uses the resolved `baseBranch`. #3. `syncPulledPr` was writing pulledPrs head/lastSyncedAt even when the push to origin failed, so the mirror state diverged from the recorded linkage. Same fix pattern as the recent pullRequestCommand bug: skip the config write when push fails. Tests cover the no-write-on-push-fail case. Plus: replaced `gh issue list --search` with plain `--state all --limit 20` in the Tier 5 e2e and added a 5-attempt retry around it. GitHub's search index AND list endpoint both have eventual-consistency lag for freshly-created issues; the retry makes the test deterministic. Verification - 239 unit tests pass / 0 fail (was 229; +10 new tests) - 4 e2e tiers pass / 0 fail across 3 consecutive runs (was 1 flaky run in 3 before this fix) - Lint clean, build OK
cabljac
added a commit
that referenced
this pull request
Apr 28, 2026
…/pull (#32) * refactor(commands): extract planStaging + executeStagingPush from stageCommand Pre-factor for shipCommand. No behaviour change — all 172 unit tests pass unchanged. The split: - planStaging(branch, cwd): pure read. Verifies the branch, resolves public and upstream remotes, detects upstream's default branch, checks whether scheduled sync is enabled. Returns a StagingPlan that callers display before any user confirmation. - executeStagingPush(plan, cwd, spinner): the actual push. Strips the internal workflow commit when scheduled sync is on; otherwise direct branch push. Returns the SHA pushed. stageCommand now reads as: auth → plan → confirm → execute → render PR URL. shipCommand will reuse plan + execute, slotting an internal-PR lookup and upstream PR creation between confirm and the post-execute step. * feat(config): add shippedBranches and pulledPrs to VenforkConfig Two new branch-keyed maps in `venfork-config`: - shippedBranches: linkage between an internal review PR and the upstream PR it was promoted to (recorded by `venfork stage --pr`). - pulledPrs: tracking record for upstream PRs pulled into the mirror for internal review (recorded by `venfork pull-request`, used by `venfork sync <branch>` to refresh). Patch semantics: per-entry merge with `null` to delete a single entry, or `null` for the whole field to clear the map. Same shape for both. Per-entry validation drops malformed records during normalize so a bad write never wedges the config branch. No callers yet — just the schema + the patch plumbing. * feat: add stage --pr, pull-request, and sync <pulled-branch> Three user-visible additions for 0.6.0, all sharing the same use case: closing the round-trip between internal review on the private mirror and upstream PR activity. stage --pr (opt-in) - `venfork stage <branch> --pr` runs the existing stage logic, then opens the upstream PR via `gh pr create --repo <upstream> --head <fork>:<branch>`. - The upstream PR body comes from the most recent internal-mirror PR for that branch (open first, then any state). Blocks delimited by `<!-- venfork:internal -->...<!-- /venfork:internal -->` are stripped before posting; a footer linking back to the internal review is appended. - The translated body is shown in a confirm prompt before any push happens so contractors can catch redaction mistakes before they go public. - The internal/upstream PR pair is recorded in `venfork-config.shippedBranches[<branch>]` for follow-up tooling. - `--draft`, `--title <text>`, `--base <branch>` flags supported. `--draft` implies `--pr`. Default `stage` behaviour without `--pr` is unchanged. pull-request <pr-number-or-url> - Brings a third-party upstream PR's commits onto a local branch (default `upstream-pr/<n>`) and pushes it to the mirror so the team can review internally before it lands upstream. - Reads PR metadata via `gh pr view --json`; renders title/author/state/ body preview as a `p.note` so the team has context. - Refuses to clobber an existing local branch unless --branch-name is passed (prevents stomping on a previous review). - `--no-push` skips the mirror push for purely-local inspection. - Records `pulledPrs[<branch>]` so sync can later refresh. sync <pulled-branch> - When the targetBranch matches a `pulledPrs` entry (or the `upstream-pr/<n>` naming convention), syncCommand refetches `pull/<n>/head` from upstream and force-with-lease pushes to origin instead of running the default-branch +1-commit sync flow. - Falls back to the convention if no config entry exists, so users can refresh branches that were created manually. Tests - 28 new tests across stage-args, pull-request-args, stageCommand --pr flow, pullRequestCommand happy/sad paths, and syncCommand pulled-PR routing. Total: 203 pass / 0 fail. Docs - README: rewrote `venfork stage` section with the new flags + the redaction marker convention; added `venfork pull-request` section and a sync-pulled-branch note. Quick Start example updated. - showHelp() output covers the new commands. * feat: add venfork issue stage/pull + e2e fixtures for ship/pull/sync issue stage|pull - `venfork issue stage <internal-#>` reads an internal mirror issue, applies the same `<!-- venfork:internal -->...<!-- /venfork:internal -->` redaction as stage --pr, and opens the upstream counterpart. - `venfork issue pull <upstream-#>` creates an internal triage issue on the mirror titled `[upstream #N] <title>` referencing the upstream source, so teams can review without leaving the private space. - Both record one-shot linkage in venfork-config (`shippedIssues` / `pulledIssues` maps). No comment sync — these are audit-log records. Config schema additions - `ShippedIssue` and `PulledIssue` types alongside ShippedBranch / PulledPr. - Same per-entry merge + null-clear semantics in updateVenforkConfig. - Per-entry validators drop malformed records during normalize. E2E fixtures (Tier 3, 4, 5) - Tier 3: stage --pr against real GitHub. Creates a feature branch + an internal review PR with a redaction block, runs `venfork stage feat --pr --draft`, asserts the upstream PR exists with the redacted block removed and isDraft=true. - Tier 4: pull-request + sync upstream-pr. Opens an upstream PR via the contents API, runs `venfork pull-request <n>`, asserts mirror branch matches local. Pushes another commit upstream, runs `venfork sync upstream-pr/<n>`, asserts the mirror branch advanced. - Tier 5: issue stage + pull round-trip. Creates an internal issue with a redaction block, stages it upstream, asserts redaction applied. Creates an upstream issue, pulls it into the mirror, asserts the internal copy has the [upstream #N] prefix and links back. Tests - 12 new unit tests across issue-args (6) + issueCommand (5) + config round-trip (1). Total: 215 pass / 0 fail. - E2E tiers 3-5 add 3 tests gated behind VENFORK_E2E=1; all use the same beforeAll setup as Tier 1, sharing the upstream/mirror/fork repos. Helpers - `openUpstreamPr` (creates branch + commit + PR via the contents API) - `pushToUpstreamPrBranch` (adds another commit to an existing PR branch — used by the sync test) - `createIssueOnRepo`, `getIssueMeta`, `getPrMeta` for assertions Docs - README: new `venfork issue` section under Commands; quick-start example unchanged (issues are an opt-in flow). - showHelp: includes the new sub-command. * fix(commands): VENFORK_NONINTERACTIVE bypass for stage --pr / issue prompts clack's confirm reads keypresses, so piping `y\n` over stdin in non-TTY mode doesn't reliably resolve the prompt — the e2e Tier 3 (stage --pr) and Tier 5 (issue stage/pull) tests both saw stdin EOF interpreted as cancel, exiting cleanly without performing the action. - New `confirmOrAutoYes` wrapper returns true immediately when `VENFORK_NONINTERACTIVE=1` is set; otherwise delegates to `p.confirm`. - Applied to stageCommand's "push to public fork" prompt and to both issueCommand prompts (stage + pull). - Setup's personal-account safety prompt is intentionally NOT bypassed — that one's a guardrail against accidental personal-account creation in scripts. - Tier 3 and Tier 5 e2e tests now set the env var instead of piping `y\n`. - README documents the env var under Environment Variables. * fix(stage): pass --state value as separate arg in gh pr list lookup Internal-PR lookup was passing '--state open' as a single execa template interpolation, which gh either rejects or filters wrong — producing zero results and triggering the synthetic-body fallback. Splitting into '--state ${state}' separates the flag from the value. * test(e2e): tier 3 filters upstream PR by headRefName instead of --head gh's --head filter on cross-repo PRs is unreliable — listing all PRs and filtering in JS by headRefName + headRepositoryOwner is robust. * fix: review-found issues for 0.6.0 Self-review surfaced 11 issues; fixing all of them on this branch before opening the PR keeps the release cohesive. Stage / PR creation - Synthetic body (when no internal review PR exists) is now generated from `git log --oneline upstream/<default>..<branch>` instead of a literal "please add a description" placeholder. The upstream maintainer sees real commit subjects. - Redaction marker stripping iterates until no marker pair remains — nested `<!-- venfork:internal -->` blocks now redact correctly. - `--internal-pr <n>` flag pins a specific internal PR (skips the most-recent-open lookup). Useful when a branch has had multiple internal PRs and you want to ship from a specific one. - When `gh pr create` reports "already exists", venfork now follows up with `gh pr edit --body-file -` to refresh the upstream PR body from the (possibly updated) internal review. `--no-update-existing` opts out. URL handling - `resolvePullRequestArg` and `resolveIssueArg` now throw instead of warning when the URL's owner/repo doesn't match the upstream remote. Pasting a stray URL no longer silently uses upstream as the source. Pull-request flow - `pullRequestCommand` only records the `pulledPrs` linkage when the push to origin actually succeeded. Previously the entry was written even on push failure, leading `venfork sync <branch>` to think the mirror had a branch it didn't. Config branch - `writeConfigBranch` switches from `git push --force` to an explicit `--force-with-lease=venfork-config:<sha>`, where the expected SHA comes from a fresh `git ls-remote`. Concurrent venfork commands now fail loud (the second push errors) instead of silently overwriting each other. Status - `venfork status` now reads `venfork-config` and renders four best-effort linkage blocks: shippedBranches, pulledPrs, shippedIssues, pulledIssues. No live `gh` round-trips per entry. Tests - Stage-args: --internal-pr (value + equals form, validation), --no-update-existing. - Stage command: regression for the `--state open` execa-arg bug (asserts the rendered command has `--state` separated from `open`); --internal-pr override path; auto-update via `gh pr edit`; `--no-update-existing` opt-out; VENFORK_NONINTERACTIVE bypass. - Total: 215 → 225 pass. Docs - README: `--branch-name` caveat under `pull-request`; new "Concurrency" subsection under environment variables explaining the --force-with-lease retry behaviour. * fix: apply all reviewer feedback from PR review thread Agent-Logs-Url: https://github.com/cabljac/venfork/sessions/1ed2e6ce-a0f0-4ce4-a530-8550020fc15d Co-authored-by: cabljac <32874567+cabljac@users.noreply.github.com> * style(commands): wrap translateInternalBody call to satisfy biome line-length * fix: address copilot review round 2 - Tighten issue/PR number validation in normalizers to require positive integers (was: any finite number). Hand-edited config with garbage numbers (negatives, floats, NaN) is now dropped during normalize. New `isPositiveInt` helper de-duplicates the four call sites. - Fix --no-push semantics in `pullRequestCommand`. Previously initialised `pushSucceeded = !push`, which evaluated to `true` when the user passed --no-push and let the flow fall through to record a pulledPrs entry. A later `venfork sync <branch>` would then push the branch to the mirror unexpectedly, defeating --no-push. Renamed to `pushedToMirror` (defaults to false) and skip the linkage write whenever the mirror doesn't have the branch — covers both the push-fail path and --no-push. - Strengthen the existing --no-push unit test to assert no pulledPrs config write happens, not just no `git push origin`. E2E smoke green: 4 pass / 2 skip / 0 fail / 85s. * feat(config): auto-retry venfork-config writes on lease failure Concurrent venfork commands previously crashed the losing run with a "stale info" error; the user had to re-run by hand. The losing run already has all the information needed to recover automatically (its patch, the read+write helpers, the lease semantics). Wrap the read-merge-write cycle in a bounded retry loop so the recovery is invisible. How it works - New `fetchConfigContentAndSha` reads the venfork-config branch content together with the SHA of the commit it came from, in a single fetch. Capturing the read-from SHA is what makes the lease correct under concurrency — a separate ls-remote right before the push would race with concurrent writers. - `writeConfigBranch` now accepts an optional `expectedSha`. When set, the push leases against that exact SHA (the one we read from). The ls-remote fallback stays for first-time writes (`createConfigBranch`). - `updateVenforkConfig` runs the full read → merge-patch → write cycle in a `for` loop bounded at MAX_RETRIES = 3. On a lease failure (git stderr containing "stale info" or the rejected-with-stale-info pattern) it re-reads, re-applies the same patch on top of the now- fresh content, and retries. Auth/network failures are NOT retried. - Patch application is factored into `applyPatchAndNormalize` so the same patch is applied cleanly on each retry without duplication. Tests (4 new, 11 total in tests/config.test.ts) - Push uses --force-with-lease against the read sha (not a fresh ls-remote). - Stale-info on first push → re-read → retry succeeds; final push leases against the new sha. - Three consecutive stale-info failures → throws after attempt 3. - Auth-failure error message → throws immediately, no retry. Docs - README "Concurrency" subsection rewritten: explains that concurrent runs are normally invisible (auto-retry merges both updates) and surfaces the manual-retry expectation only after sustained contention. * fix: round-3 review (nested redaction, --base URL, sync push fail) Address three more Copilot comments. All three are real: #1. Nested redaction blocks were leaking. The previous iterative-regex approach matched from the FIRST opening marker to the FIRST closing marker, which on properly-nested input (outer wraps inner) ate the inner pair and left an orphaned outer close marker plus the intended-private content between the inner close and the outer close. Replaced with `stripInternalBlocks` — a depth-tracking pass that walks all marker positions in document order and emits content only when depth=0. Defaults: unmatched close drops the marker and keeps surrounding content; unmatched open drops to end-of-input (fail-safe — better to drop too much than leak intended-private content upstream). Exported as `stripInternalBlocks` (with @internal JSDoc) so it can be unit-tested directly. 9 tests in tests/redaction.test.ts cover sibling, nested, whitespace tolerance, dangling close, unmatched open, multi-line, and lastIndex-reset cases. #2. The fallback compare URL (shown when --pr wasn't set or `gh pr create` failed) used `plan.upstreamDefaultBranch` instead of `baseBranch`. With `--base develop`, the URL pointed at `main`. Now uses the resolved `baseBranch`. #3. `syncPulledPr` was writing pulledPrs head/lastSyncedAt even when the push to origin failed, so the mirror state diverged from the recorded linkage. Same fix pattern as the recent pullRequestCommand bug: skip the config write when push fails. Tests cover the no-write-on-push-fail case. Plus: replaced `gh issue list --search` with plain `--state all --limit 20` in the Tier 5 e2e and added a 5-attempt retry around it. GitHub's search index AND list endpoint both have eventual-consistency lag for freshly-created issues; the retry makes the test deterministic. Verification - 239 unit tests pass / 0 fail (was 229; +10 new tests) - 4 e2e tiers pass / 0 fail across 3 consecutive runs (was 1 flaky run in 3 before this fix) - Lint clean, build OK --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cabljac
added a commit
that referenced
this pull request
Oct 2, 2026
Three gaps in findDeniedText: - File content: a bare mirror repo name now needs a host before it (github.com/, git@host:, ssh://host/, https://host/). A package or directory with the same name, such as "acme-ui/widget-private", no longer blocks a stage. Commit messages, branch names, PR text and file names keep the loose owner/name match. - The bare-name lookahead rejected any "/" or "." after the name, so "https://github.com/other/<name>/pull/3", "other/<name>." at a sentence end and "other/<name>.git." slipped through. A hosted match now allows any path after the name, and a trailing "." counts only when it starts a file extension. - Every term except "venfork" now ends at a repo-name boundary, so "acme/widget-private-public" no longer matches "acme/widget-private" while ".git", "/pull/1", "#7" and "?x" suffixes still do.
cabljac
added a commit
that referenced
this pull request
Oct 6, 2026
…e to upstream (#83) * chore(deps): bump execa to ^9.6.1 * fix(stage): detect managed commits from history, not schedule config `venfork stage` only sanitized the branch when `schedule.enabled` was set in config. With a `preserve` allowlist and no schedule, sync still writes the managed "+1 commit" (carrying mirror-only files) onto origin/<default>, so any branch cut from it pushed that commit raw to the public fork, or straight to upstream in --no-public mode. Stage now walks `upstream/<default>..<branch>` and takes the sanitize path whenever any commit matches `isManagedCommit`. This also covers branches cut before a schedule was disabled. Branches with no managed commit push as-is with unchanged SHAs. If the range cannot be inspected (upstream ref missing locally), stage refuses to push rather than falling back to a raw push. * refactor: split commands.ts into per-command modules Move every command into src/commands/<command>.ts and the helpers shared between commands into src/shared/ (net, managed-commit, mirror-commit, divergence, redaction, worktree, confirm, constants). src/commands.ts is now a barrel, so index.ts and the tests import unchanged. No behaviour change. Two mechanical extractions beyond plain moves: the divergence closure inside syncCommand becomes checkDivergence() with explicit parameters, and the mkdtemp/worktree add/remove pattern used three times becomes withDetachedWorktree(). Stage now runs the evil-merge check before creating its temp worktree instead of just after. * test: add real-git integration harness Unit tests mock execa with mock.module, which leaks across files in one bun process, so they move to tests/unit and the new real-git tests live in tests/integration. `bun run test` runs the two suites as separate processes; CI calls them as separate steps. tests/harness/mirror-fixture.ts builds upstream/origin/public bare repos in a temp dir, wires a working clone the way `venfork setup` does, writes the venfork-config branch, and isolates git from the developer's global config. Fixture commits use pinned dates; commands under test use the real clock unless a test sets one. Two changes make commands callable from tests without changing CLI behaviour: - syncCommand throws SyncDivergenceError (counts and files per remote) instead of calling process.exit(1). index.ts prints it and exits 1. - The gh auth check moves from the command bodies into the index.ts dispatcher (ensureGhAuth). `stage` only requires gh with --pr/--draft. Two sync tests are test.todo: they pin the idempotent, single-push sync that the next change implements. * fix(sync): copy preserved files with git checkout applyMirrorPlusOneCommit read each preserved file with `git show <tip>:<path>` through execa's `encoding: 'buffer'`. Under the Bun runtime that option crashes child_process with "Unknown encoding: buffer", so `venfork sync` with a preserve list failed when run via `bun run dev` or the `bun build --compile` binary. Node was unaffected. The managed-commit worktree now runs `git checkout <tip> -- <path>`, which writes the file and stages it with its recorded mode. That drops the separate ls-tree/chmod step for the executable bit. Caught by the new real-git integration tests, which run under Bun. * test(harness): point HOME at a fixture directory GIT_CONFIG_GLOBAL already keeps git away from the developer's config. Pointing HOME at an empty directory under the fixture root also stops any other tool from reading the real home during integration tests. The previous value is saved and restored with the other keys. * fix(config): reject glob and pathspec-magic preserve entries Preserve entries are passed to git as pathspecs. A glob such as 'src/file-*.txt' made sync silently revert upstream changes to every matching file, and ':!nothing' rolled the whole tree back to the previous mirror tip. normalizePreservePath now rejects '*', '?', '[', ']' and a leading ':', so such entries are refused by 'venfork preserve add' and dropped from config. Preserve entries are single literal files. * fix(sync): restore preserved files literally and only as single files Each preserve entry was restored with `git checkout <tip> -- <path>`, which treats the path as a pathspec and accepts directories. Sync now: - runs ls-tree and checkout with --literal-pathspecs; - requires exactly one blob entry (100644, 100755 or 120000) on the previous mirror tip, and rejects directories and submodules with an error that names the path; - refuses when a parent directory of the path is now a file upstream (for example preserve 'config/x.yml' after upstream adds a file 'config'); before, the checkout silently deleted upstream's file; - includes git's stderr when the checkout itself fails. The test prompt stub gains select/multiselect/group no-ops and a mocked log.error so tests can assert on error text. * test(harness): isolate the fixture from every GIT_* variable With GIT_DIR exported (as inside husky or lefthook hooks) the integration suite wrote commits into the parent repository, and GIT_CONFIG_COUNT/KEY_n/VALUE_n bypassed GIT_CONFIG_GLOBAL. isolateGitEnv now snapshots and hides every GIT_* variable plus HOME, XDG_CONFIG_HOME, GH_TOKEN and GITHUB_TOKEN, applies the fixture's own values, and restores the exact snapshot on cleanup. Nested fixtures restore in reverse order. createMirrorFixture restores the environment and removes its temp dir when the build throws, and the integration tests only clean up a fixture that was actually created. * test: make bare bun test run only the unit suite A bare 'bun test' loaded unit, integration and e2e files into one process, where the unit tests' execa mock broke the real-git tests. bunfig.toml sets the test root to tests/unit. 'bun run test' still runs both suites, and the e2e script now passes an explicit ./tests/e2e path because a bare filter no longer reaches outside the root. * refactor(cli): decide gh auth per command in one tested function The per-command AuthenticationError tests went away when the gh check moved to the dispatcher, leaving that decision untested. requiresGhAuth(command, args) now holds it: setup, clone, pull-request and issue always need gh; stage needs it only with --pr or --draft; every other command runs without it. index.ts calls it once before dispatch, and a table test covers each command and stage flag. * ci: pin Bun to 1.2.18 CI used the latest Bun while the suites are developed and verified on 1.2.18. Pin both jobs so a Bun release cannot change test or build behaviour unnoticed. * fix(sync): make scheduled sync idempotent and push once Every sync rewrote origin/<default>: the managed commit was a fresh `git commit --allow-empty` with the wall-clock date, so its SHA changed on every cron tick and teammates saw a diverged default branch. Sync also pushed bare upstream to origin first and re-added the managed commit in a second push, leaving the workflow file off the default branch in between; a runner dying there stopped cron for good. Sync now builds the target tip before pushing anything: - The managed commit is made with `git commit-tree`, bot author and committer, both dates set to the upstream tip's committer date, and a `Venfork-Managed: 1` trailer. Its SHA depends only on the upstream tip and the tree. - When the managed content adds nothing to the upstream tree, there is no managed commit and origin equals upstream (+0). - Origin gets one push with an explicit `--force-with-lease=refs/heads/<d>:<previous tip>`, or none when it already points at the target. Public is skipped when it already equals upstream. isManagedCommit checks the trailer first and keeps the subject, legacy subject and path fallbacks. checkDivergence treats only a missing remote branch as "first sync"; any other git failure now aborts instead of being read as zero divergence. The first sync after upgrading rewrites the managed commit once; later syncs are stable. * fix(sync): harden the deterministic tip and re-stamp on schedule changes Review findings on the idempotent sync: - `i18n.commitEncoding` added an encoding header to the commit-tree object and changed the managed commit's SHA. commit-tree now runs with `-c i18n.commitEncoding=UTF-8`. - `schedule set` / `schedule disable` still made a wall-clock managed commit on top of origin, leaving upstream + managed + managed until the next sync. They now fetch, refuse on origin divergence, update config and re-stamp origin through the same updateOriginTip path as sync, so a following sync pushes nothing. updateWorkflowOnOriginDefault is gone. - upstream/<d> is resolved before the divergence checks, so a missing upstream branch gets the friendly error instead of a rev-list failure. - A stale lease on the origin push now says origin moved since the fetch and to re-run sync. - The sync fetches and pushBranchWithLease go through netExec (no stdin, VENFORK_GIT_TIMEOUT); netExec gains a buffered, non-rejecting mode. Tests: encoding and stale-lease message (red first), schedule set/disable producing one managed commit that sync keeps (red first), missing upstream error (red first), plus migration stability for legacy and trailer-less managed commits and trailer-first detection. * feat(schedule): pin the venfork version in the generated workflow The sync workflow ran `npm install -g venfork`, so every run picked up whatever was latest on npm. A new release could change the generated YAML or sync behaviour under a mirror without anyone running it locally. generateSyncWorkflow now takes a version (default: the running CLI, read from package.json and inlined by `bun build`) and emits `npm install -g venfork@<version>`. On a runner the running version equals the pin, so the regenerated YAML matches the default branch and the managed commit stays stable. Running a newer CLI locally bumps the pin; that is the upgrade path. Adds `venfork --version`. scripts/release.ts checks RELEASE_TAG against package.json and runs the host binary's --version after the build. * feat(schedule): allow overriding the venfork install spec in the sync workflow The install step now reads the VENFORK_INSTALL_SPEC repository variable and falls back to the pinned venfork@<version>. A tarball URL, git spec or other version can replace the published package, for example to run unreleased sync code or to bridge the window before npm publish lands. * feat(doctor): add venfork doctor health checks Scheduled sync fails quietly: a missing VENFORK_PUSH_TOKEN, a stale or missing workflow file, a preserved file that vanished, or a cron that GitHub paused all surface only as red runs nobody looks at. `venfork doctor` (also `venfork status --check`) runs ten checks: repo, remotes, mode, invariant, divergence, preserve, workflow, token, last-run and cron-age. Each returns { id, ok, detail, fix? } with ok as true, false or 'skipped'. It prints a table, or JSON with --json, and exits 1 when any check fails. It reads only, apart from fetching the remotes and the config branch. The gh-backed checks are skipped when gh is not authenticated or origin is not a GitHub repo. cron-age flags a last run older than twice the longest gap between cron fire times, with a one-hour floor because GitHub routinely delays scheduled runs. * fix(doctor): run network calls through netExec The fetch, gh secret list and gh run list calls in doctor used raw execa, so a credential or host-key prompt could hang and there was no timeout. They now use netExec with a new bufferOutput mode (piped, buffered, never rejects), so they get NET_ENV, no stdin and VENFORK_GIT_TIMEOUT. A failed or timed-out gh call still reports its check as skipped, with the reason from netFailureReason. * feat(sync): report blocked and failed scheduled syncs as a mirror issue A scheduled sync that aborts on divergence, or fails for any other reason, was only a red run in the Actions tab. Nobody gets told. Layer A, in the CLI: `venfork sync --report-issues` (the generated workflow passes it). On SyncDivergenceError it opens an issue labelled venfork-sync-blocked on the mirror, or refreshes the body of the open one, with the divergence report: remotes, changed files, and the exact `venfork preserve add` / rebase steps. A successful sync comments "Resolved by <run>" and closes it. gh failures are logged and never mask the sync result. The divergence report text moves to formatDivergenceReport so the console note and the issue share it. Layer B, in the YAML: the job gains `issues: write`, the sync step gets GH_TOKEN from github.token, and a final `if: failure()` step opens the same labelled issue, or comments the run URL on it unless the CLI already wrote that run into the body. This covers npm install failures, a missing token and timeouts. * fix(utils): accept only github.com owner/repo in parseRepoPath parseRepoPath matched "github.com" anywhere in the string and kept whatever followed. A trailing slash gave "acme/w-private/", ".git/" gave "acme/w.git/", an ssh URL with a port gave "22/acme/w", and "https://evilgithub.com/acme/w" was accepted as "acme/w", so gh would target github.com/acme/w. With `sync --report-issues` taking the repo from origin, that is a way to post mirror details to the wrong repo. The host must now be exactly github.com (or www.github.com) after an optional scheme and user info, an optional port is skipped, trailing slashes and ".git" are stripped, and the result must be exactly owner/repo. Anything else returns the existing empty-string sentinel. * fix(schedule): serialize sync runs and leave an existing label alone The generated workflow could start overlapping runs (a manual dispatch during a scheduled one) and had no time cap beyond GitHub's six hours. It now sets `concurrency: { group: venfork-sync, cancel-in-progress: false }` and `timeout-minutes: 30` on the job. The failure step creates the venfork-sync-blocked label only when `gh label list` does not find it, instead of overwriting it with --force. * fix(sync): report blocked syncs only to the private mirror, safely Review findings on --report-issues: - Target repo: GITHUB_REPOSITORY inside Actions; otherwise origin's GitHub repo, and only after `gh repo view --json isPrivate` returns true. Anything else logs a warning and skips reporting, so mirror details never reach a public repo. - gh calls go through netExec (no prompts, VENFORK_GIT_TIMEOUT); netExec can feed fixed stdin for --body-file -. - Title and lead line name the remotes that actually diverged (origin, public, or both) instead of always origin. - The file lists are capped at 100 paths with "...and N more" and the body stays under 60000 characters. - `gh issue edit` also sets the title, so an issue opened by the YAML failure step gets the divergence title. - The label is created only when `gh label list` lacks it (no --force). - A successful sync closes every open labelled issue, not just one. The commands tests now give each remote its own URL and assert that gh targets the origin repo. * fix(stage): lease public pushes explicitly and name preserved paths Standard-mode stage pushed the sanitized branch with an implicit `--force-with-lease=refs/heads/<b>`, whose expected value comes from refs/remotes/public/<b>. When that tracking ref was missing or stale (staged from another clone, or never fetched) the push was rejected as "stale info". Both modes now read the remote tip with ls-remote and pass it as an explicit lease; a branch that does not exist yet is leased as "must not exist". When a commit cannot be replayed onto upstream because it changes a preserved mirror-only path, the error now names the path and says to move that change to the mirror default branch, instead of a generic "rebase and retry". * test(e2e): drop the unimplemented real-cron tier and fix stale subjects The tier-2 real-cron test only threw "not yet implemented". It is removed with its env flag and the test:e2e:cron script; real cron timing is best-effort and would make the test flaky anyway. The e2e assertions still expected the legacy managed-commit subject. They now expect "chore: venfork-managed mirror commit", and listCommitMessages returns subject lines only so the Venfork-Managed trailer does not shift the indices. * docs: explain how scheduled sync behaves Adds a README section on cron being best-effort, GitHub's 60-day idle rule (which applies to public repositories), why the workflow file must stay on the default branch, the VENFORK_PUSH_TOKEN requirement, the failure issue, and the upgrade path. The doctor cron-age hint now says the idle rule is for public repositories. * test(e2e): run the dispatched sync with the code under test Tier 2 installed the published venfork on the runner, which lags the checkout and rejected new flags such as sync --report-issues. The test now packs the fresh build, uploads it as a release asset on the public upstream repo and points the mirror's VENFORK_INSTALL_SPEC variable at it. Tier 2 also asserts that no venfork-sync-blocked issue is open. The README documents VENFORK_INSTALL_SPEC and the window after a release tag before npm publish finishes. * test: stub process.exit for every test file in a preload A command that calls process.exit(0) ended the bun process with a passing status, so a broken command could turn both suites green. The preload throws instead, and the per-file stubs are gone. * ci: fail the job when a suite runs fewer tests than its floor Each suite writes a junit report and scripts/test-count.ts compares the executed count with tests/expected-counts.json, so a suite that silently stops loading files cannot pass. * test(sync): pin that a failing divergence range aborts sync A mutation that swallowed rev-list failures and reported no divergence survived the suite. An integration test deletes the upstream ref and a unit test makes rev-list fail with execa's reject semantics; both now fail against that mutation. The unit mock passes execa options to function responses so tests can model reject: false. * test(harness): isolate GH_* and VENFORK_*, enforce LIFO cleanup isolateGitEnv now hides every GH_* and VENFORK_* variable and restores override keys too. Fixtures take a tmpRoot so the failed-build test no longer lists the shared temp dir, cleanup throws when nested fixtures are released out of order, and the preload clears the quietPrompts log.error mock before each test. The harness tests snapshot and restore the env keys they touch, and the sentinel repo is created without GIT_* set. * test(sync): prove the managed SHA ignores the wall clock The idempotency test pinned GIT_COMMITTER_DATE itself, which the managed commit overrides anyway, and both syncs could land in the same second. It now waits for the next second between syncs, so it fails if the managed commit stops taking its dates from upstream. * ci: pin Bun from one .bun-version file and test before release ci.yml and release.yml both read .bun-version, so the release no longer builds with whatever Bun is latest. Both release jobs run the unit suite before they build. * chore(release): keep breaking changes on the minor line before 1.0 With bump-minor-pre-major, a breaking change released from 0.x bumps the minor version instead of jumping to 1.0.0. * fix: require a Node.js version that execa 9 supports execa 9 needs ^18.19.0 || >=20.5.0, so engines.node now says so instead of >=18.0.0. * test(cli): spawn the built entry point for --version and gh auth index.ts had no test. The integration suite now builds the CLI, runs node dist/index.js --version, and runs setup with a gh stub that fails auth, expecting exit 1 and the AuthenticationError message. The test count floors are raised to the new totals. * fix(config)!: fail closed on an unreadable or invalid venfork-config A config problem used to read as no config, so sync reset origin to bare upstream and deleted preserved files. readVenforkConfigFromRepo and fetchVenforkConfig now return null only when git ls-remote --exit-code says the branch is absent; a fetch, JSON or validation failure throws ConfigError, which sync propagates. Invalid preserve entries (globs, pathspec magic, directories) are kept in invalidPreserve instead of being dropped: sync and schedule refuse to run and name each entry with its venfork preserve remove fix, remove accepts them, and saves write them back. schedule.cron is validated with the shared isValidCronExpression; cron.ts rejects step 0 and macros and searches 800 days. The managed path heuristic now requires the bot author, and sync lists commits it replaces on a signal weaker than the trailer. Doctor tells a fetch failure, a missing branch and invalid content apart. BREAKING CHANGE: sync and schedule now abort while the preserve list holds an entry that is not a single literal file path. Remove it with venfork preserve remove <entry>. * fix(sync): route remaining network calls through netExec getDefaultBranch's git remote set-head -a and the pulled-PR fetch now run with no stdin and the network timeout. The pulled-PR push uses an explicit lease on the mirror copy of the branch instead of a bare --force-with-lease, so a teammate's push to that branch is never overwritten. Config reads and writes already go through netExec. * test: raise the test count floors * fix(sync): build the managed tip with plumbing on a temporary index The managed commit was assembled in a detached worktree, so the user's sparse-checkout, post-checkout hooks, clean filters, core.quotePath and case-insensitive filesystem could change its tree or its SHA. buildMirrorTip now reads the upstream tree into a temporary index (GIT_INDEX_FILE, no inherited GIT_DIR or GIT_WORK_TREE, empty hooks directory), adds the workflow blob with hash-object --no-filters, removes blocked workflows by exact path, and copies preserved files as blobs after checking upstream's tree rather than the filesystem. Path listings in the divergence check and the managed-commit heuristic use -z, so non-ASCII names compare by exact bytes. The README documents that a preserved path upstream later deletes keeps upstream's last version. * fix(stage): check out the stage worktree with an empty hooks directory A post-checkout hook in the clone ran inside stage's temporary worktree and could stage files into the cherry-picked branch. The worktree add and each cherry-pick now run with core.hooksPath pointing at an empty directory. * style: format the stage test expectations * fix(redaction): match marker variants and fail closed on leftovers Markers now match case-insensitively with optional whitespace around the slash and colon and a note after the keyword. An unmatched close marker, or any HTML comment mentioning venfork that survives stripping (such as a misspelled marker), throws RedactionError instead of publishing. PR and issue titles go through the same redaction as bodies. StageLeakError is added for the stage boundary check. * docs(claude): drop env overrides no code reads, document redaction rules * fix(stage): refuse to publish mirror-only paths or non-upstream branches Stage could leak the managed workflow or preserved files: a branch cut from origin before sync, a squash onto upstream, or a cherry-pick carried them in commits that are not managed, so the raw push sent them out. On both the raw and the sanitized path, stage now diffs the head against upstream/<default> before pushing and throws StageLeakError naming any added or changed sync workflow or preserved path absent upstream. Stage refuses venfork-config and branches with no merge-base with upstream, lists every commit it drops as managed with its classification, builds the synthetic PR body from published commit subjects only (no SHAs, no managed commit), shows the whole body in the confirm preview, and requires GitHub remotes for --pr. parseRepoPath accepts ssh.github.com. * fix(schedule): install venfork before checkout, without lifecycle scripts The generated workflow installed venfork after checking out the mirror with the push token, so an install-time script ran with that token in the git config. Install now runs first with --ignore-scripts (the bin needs no lifecycle script; checked with a packed tarball). The concurrency group includes the workflow name. * fix(sync): trust GITHUB_REPOSITORY only when it names origin resolveReportRepo filed the blocked-sync issue on whatever repository GITHUB_REPOSITORY named. It now uses that value only when it equals origin's GitHub repo, and otherwise falls back to checking that origin is private. * fix(sync): refuse to downgrade the pinned venfork version An older CLI rewrote a workflow that pinned a newer venfork, so local and runner syncs could keep rewriting each other's managed commit. Before rebuilding the tip with a schedule, sync and schedule read the pin from origin's venfork-sync.yml and abort when it is newer than the running CLI. Doctor's workflow check names both versions. * test(e2e): pin that the job token cannot push upstream workflow changes Tier 6 sets up a no-public mirror with no VENFORK_PUSH_TOKEN, pushes an upstream commit that edits .github/workflows/ci.yml, and dispatches the sync. GitHub rejects the push with "refusing to allow a GitHub App to create or update workflow ... without workflows permission"; the tier asserts that failure, that the mirror did not move, and that the failure step opened the venfork-sync-blocked issue. The code-under-test tarball is published once per run and shared with tier 2. * fix(schedule): require VENFORK_PUSH_TOKEN with workflow scope in both modes The job's GITHUB_TOKEN cannot push upstream commits that change .github/workflows (shown by e2e tier 6), so no-public mirrors need the token too. Doctor's token check now runs in both modes and names the workflow scope, and the schedule set outro says what the token is for without mentioning a public fork in no-public mode. The README explains the token requirement and the pinned-version rules: keep VENFORK_INSTALL_SPEC unset or equal to the local CLI version, and use 0.11.0 or later. * fix(doctor): make checks match sync behaviour and say what to run The invariant check now separates stacked venfork-managed or preserve-only commits (clean divergence; run venfork sync to fold them) from real divergence. A failed fetch is one remotes row with a one-line reason and the git-state checks are skipped; a missing upstream remote gets the exact add and push-disable commands; a missing config branch, an unreachable origin and invalid content each get their own fix. cron-age reads only scheduled runs, fails with `gh workflow enable` when the workflow is disabled, and no longer recommends a manual dispatch. An in-progress run reads "in progress", and the outro counts failed, passed and skipped checks instead of claiming all passed. * feat(cli): add per-command --help and send CLI errors to stderr `venfork <command> -h` or `--help` prints that command's usage and exits 0 before any gh auth check; `venfork help` still prints everything, built from the same per-command sections. Argument errors and unknown commands go to stderr. The sync help no longer ties the managed commit to an enabled schedule, preserve help says single files only, and the environment section lists exactly the variables the CLI reads. An end-to-end test runs the built CLI's `doctor --json` against a real fixture in four states (not a repo, healthy, failed fetch, invalid cron) and parses stdout as JSON each time. * docs: document preserve, doctor and the e2e tiers accurately README gains a `venfork preserve` section: single files only, no globs or directories, upstream wins on collision, what happens when upstream later deletes a path it had taken over, and how `preserve remove` clears an invalid entry. The sync heading lists --report-issues, status lists --check and --json, troubleshooting starts at doctor, and the doctor table notes the 1 hour cron-age floor, the disabled-workflow check and the workflow-scope token requirement in both modes. The env section covers every variable the CLI reads. tests/e2e/README.md now explains the bunfig test root, names both default owners, labels the PAT example as a fine-grained token and describes tiers 3 to 6. * fix(doctor): say where to re-run setup for a missing config branch Also drops an em dash from the issue help text. * fix(redaction): scan every HTML comment and refuse leftover venfork mentions The marker regexes stopped at the first '>' inside a comment, so a note such as 'owner->bob' or a '--!>' close let internal content through, and plain-text 'venfork' was never checked. Redaction now scans each comment from '<!--' to the first '-->' or '--!>', classifies it as an open or close marker, refuses any other comment that mentions venfork, and refuses any venfork mention left after stripping. Text is NFKC-folded and stripped of format characters first, so zero-width spaces and full-width colons cannot hide a marker. Adds a mirror deny-list (origin URL, origin owner/name, 'venfork') and MirrorReferenceError for the stage gate that follows. * fix(git): never follow tags on any push With push.followTags=true in the user's git config, every push venfork makes could carry local annotated tags along. Every push in src (sync's leased push, the config branch, pull-request, the seed push) now passes --no-follow-tags. * fix(stage)!: always rebuild the branch and gate every commit before pushing Stage had two push paths. A branch without a managed commit was pushed raw, so merge commits, mirror URLs in messages, internal markers, bot authors, and preserved files added then deleted all reached the public side, and `stage main` or `stage <tag>` pushed whatever the ref held. Every stage now rebuilds the branch as linear history on upstream/<default> (merges never ship) and checks each rebuilt commit before any push: no change to the sync workflow, .venfork/ or a preserved path (unless it is upstream's exact blob), no blob identical to mirror-held preserved content at any path, no venfork bot identity, and no origin URL, origin owner/name or 'venfork' in author, committer or message. Upstream's default branch, tags and remote refs are refused up front. The push uses an explicit refspec, a lease and --no-follow-tags. PR titles and bodies get the same deny-list scan, the synthetic body drops trailing (#N) references, and the confirm prompt lists every commit subject. BREAKING CHANGE: staged commits always get new SHAs, merge commits are dropped, and branches whose commits mention the mirror or venfork are refused. * fix(sync): close integrity gaps in the managed commit, config writes and tip builder - preserve refuses the sync workflow and anything under .venfork/ - a Venfork-Managed trailer commit is only managed when it changes the sync workflow, preserve entries and workflow deletions; anything else is divergence - sync refuses a missing venfork-config branch while origin carries a managed commit; createConfigBranch refuses to overwrite an existing one - schedule set/disable and preserve remove/clear build the tip and run the pin-downgrade guard before writing config, then push, and roll the config back when the push fails - the config blob read goes through netExec; VENFORK_GIT_TIMEOUT is read per call - tip builder: D/F and file-ancestor conflicts name both paths; workflow filtering only touches top-level workflow files - remotes are read with git remote get-url (paths with spaces) - replace tests that could not fail with real property tests * fix(sync): require managed-only content for every managed kind A commit with the managed subject, a legacy subject, or the bot path-heuristic was skipped as venfork-managed whatever it changed, so sync replaced it and dropped any user work it carried. Every kind now has to change only the sync workflow, preserve entries and workflow deletions; anything else is divergence. A bot commit that bundles a second workflow file is now divergence too. A trailer commit made and committed by the venfork bot whose only extra changes add files no longer in preserve (a removal whose re-stamp never reached origin, or an older venfork) is classified as stale-trailer: sync warns and re-stamps it without those files, preserve remove works again, and stage drops it instead of publishing the files. A trailer commit amended by a user stays divergence. The prompts harness now records log.warn as a mock. * fix(preserve): warn when a removed file stays on a diverged origin preserve remove only looked at origin's tip. When user commits sat on top of the managed commit it wrote the config silently, and origin kept the file with no hint. It now walks upstream..origin for a trailer commit carrying a removed file: at the tip it re-stamps as before; buried under user commits it writes the config and warns that sync drops the file once the divergent commits are moved off origin. * test: assert the refused sync-workflow preserve names the path * fix(config): lease every config-change write and never roll back blindly applyConfigChange wrote the config through updateVenforkConfig, which re-applies the patch on a lease failure, and rolled back a failed origin push with a second retrying write. A change from another clone between the read and the write was merged under a tip built from stale config, and a rollback after another clone's write clobbered that write. The sequence is now: read the config at commit S, build the tip from the patched config, write with a lease on S and no retry (a moved branch aborts with "venfork-config changed on origin ... Re-run the command"), push origin with its lease. When the push fails, venfork-config is pointed back at S itself with a lease on the commit just written. When that lease fails, nothing is written; when the rollback fails for any reason, the error names the push error, the rollback failure, the written config SHA and the origin SHA, and says to run `venfork sync` once origin is reachable. New config.ts exports: readVenforkConfigFromRepoWithSha, writeVenforkConfigAt, restoreVenforkConfig. ConfigError gains the 'conflict' reason. applyConfigChange loses its rollback callback because the rollback restores the exact commit that was read. * fix(setup): refuse an existing venfork-config that disagrees Re-running setup against a mirror that already had a venfork-config kept the recorded config whatever it said, after the local remotes had already been rewired to the new arguments. Setup now writes or checks the config branch before touching remotes, and when one exists it reads it and refuses if mode, upstreamUrl or publicForkUrl differ (URLs compare by owner/repo), naming each mismatch. * feat(cron): accept month and weekday names in schedule cron * fix(status): warn when venfork-config cannot be read * test: pin invalid-preserve guard before divergence and harden doctor cleanup * test: gate on exact counts, fail on stale reports, run the gate in release and Node CI * test: update expected counts after restacking onto slice 15 * fix(test): fail the suite runner on signal exits and truncated junit reports run-suite treated a signalled child (exitCode null) as success. It now maps null to 1, deletes the stale report before the run, and drops the mtime comparison and --since flag. test-count requires the closing </testsuites> and a <testcase> count equal to the root total. * test: assert the node smoke run exits 0 with every expected doctor check ok The smoke script only checked that the output parsed and started with the repo row. It now also checks the exit code, every ok flag and the ordered id list. release-npm builds, verifies the shebang and runs the smoke test before publishing. * fix(schedule): substitute cron names only in list items and range ends Names in the step part (*/MON) or as the lone base of a step (MON/2) were rewritten to numbers and accepted. They are now rejected. Names stay case-insensitive; the README says they are written to the workflow as typed. * fix(status): warn on any error reading venfork-config Only ConfigError produced a warning, so other failures (for example a failed spawn) were swallowed and status reported as if no config existed. * test: update expected counts after restacking onto the fixed slice 15 * fix(stage): widen the deny-list and tighten the preview Add the mirror repo name as a deny term, fold combining marks and ignorable code points, scan the branch name, strip every #N from the synthetic PR body, warn about #N in commit messages, list published files in the preview, and report the real cherry-pick failure. VENFORK_ALLOW_SELF_REFERENCE=1 relaxes only the bare word venfork. * fix(stage)!: close the directory-preserve hole and scan published files preserve add now verifies each entry is a file on origin/<default>, so a directory or a missing path is refused. The gate matches preserve entries and mirror-only paths case-insensitively, treats a directory entry from an old config as covering every file under it, and reads every historical version of preserved files and of venfork-config:.venfork/config.json. It also scans added or modified text files and file names for the mirror URL, owner/name and repo name, and for the shape of a venfork config. Binary files are skipped; the bare word venfork stays allowed in content. BREAKING CHANGE: venfork preserve add exits non-zero for a path that is not yet committed on origin/<default>. Commit the file first. * test: seed preserve entries directly in integrity tests after restack Slice 17 made `preserve add` require the file to exist on origin's default branch first. The integrity tests predate that and added entries before committing the files, so they now use the harness seeding helper like the other integration files. Also updates the exact test counts for the restacked top of the stack. * perf(stage): batch the mirror blob scan and skip upstream history collectMirrorBlobs ran one git process per preserve entry per commit, so 10 preserved files and a 300-entry reflog took about 47 s. It now lists the candidate commits once (ref tips, their reflog entries and the history of both that upstream/<default> cannot reach), deduplicates their trees and resolves every <tree>:<entry> pair in a single cat-file --batch-check. The same fixture now takes under 200 ms; 10 entries across 1000 distinct trees take about 1.1 s. Only commits upstream cannot reach are read, and a blob that is any version of the same path at upstream/<default> or in its history is dropped, so an old upstream version of a preserved file copied elsewhere no longer counts as mirror content. The scan returns warnings, which stage prints, when origin's reflog is empty or the history cap is hit: mirror-only history older than that cannot be checked. * fix(stage): scan UTF-16 and binary files, refuse unreadable blobs The content scan treated any blob with a NUL in its first 8000 bytes as binary and skipped it, so a UTF-16 file or text after a leading NUL could carry the mirror URL to the public fork. Text with a UTF-16LE or UTF-16BE byte order mark is now decoded, and a binary blob is still byte-searched for the URL-derived terms (origin's URL and owner/name) as UTF-8 and UTF-16LE. The bare repo name is not searched in binary data. A blob over execa's 100 MB buffer used to fail with execa's raw maxBuffer message; stage now refuses it with a message naming the commit and file. * fix(stage): match the config signature only on a parsed config object Any text naming both "upstreamUrl" and "publicForkUrl" was refused, which blocked ordinary source code (venfork's own config types, or an upstream that happens to use those names). The signature now requires a JSON object with a version key, a URL-valued upstreamUrl and, when present, a URL-valued publicForkUrl, and it is skipped when VENFORK_ALLOW_SELF_REFERENCE=1. A verbatim copy of the mirror's config is still caught by the mirror-blob check. * fix(stage): preview every file the history publishes and warn on PR #N The preview listed the net diff from upstream to the rebuilt head, so a file added in one commit and deleted in a later one was not shown, yet the public history still held it. The gate now returns the union of paths each commit adds, modifies or retypes, and the preview lists them all, marking the ones absent at the head as "removed later in the branch, still in history". With --pr, the preview also warns when the upstream PR title or body contains a #N reference, with the same wording as the commit-message warning. * fix(stage): keep marker checks under self-reference, match repo names in context VENFORK_ALLOW_SELF_REFERENCE=1 dropped the leftover check in redaction entirely, so a malformed marker such as <!- venfork:internal -> or [venfork:internal] published the text it was meant to hide. With the variable set, leftover text shaped like a marker (venfork : internal in any wrapper) is still refused; only the plain word is relaxed. The bare mirror repo name was matched as a substring anywhere, so a mirror named acme/backend refused every commit mentioning src/backend/. It now matches only as the repo of an owner/name or URL (after "/" or ":" and an owner, optionally with .git, and not followed by more path). The full URL and owner/name terms are unchanged. findDeniedText now passes terms through canonicalText as it does the text, so a term with accents (a local mirror path) still matches. * fix(stage): strip only standalone #N from the synthetic PR body syntheticBody removed every #<digits> and every empty "()" pair, which mangled "init()" into "init", URL fragments, "issue#12" and left "fix : crash" behind. It now removes only "(#N)" groups (with the space before them) and #N preceded by the start or whitespace and followed by the end, whitespace or punctuation. #N inside a word or URL and cross-repo owner/repo#N references are kept; a numeric colour such as #333 reads the same as a reference and is still removed. * fix(stage): refuse while the preserve list holds invalid entries Stage folded invalid preserve entries (a trailing-slash directory from an old config, for example) into the list it gated on, and an entry such as docs/ matched nothing, so an edited file under it shipped. Stage now refuses up front with the same message as sync, naming the `venfork preserve remove <entry>` command for each invalid entry. * test: pin that preserve add refuses a gitlink * test: seed preserve entries in the stale-managed tests after restack The round-5 integrity tests on slice 15 added preserve entries with `preserve add` before committing the files, which slice 17 now refuses. They use the harness seeding helper like the rest of the integration suite. Also sets the exact test counts for the restacked top of the stack. * fix(stage): byte-search every published blob in UTF-8 and UTF-16 The content scan only ran the raw byte search on blobs it judged binary. A UTF-16 byte order mark in front of UTF-8 text, a UTF-16LE URL after 8000 ASCII bytes, or UTF-16BE text without a BOM all slipped past it. Now every blob is byte-searched for each URL-derived term as UTF-8, UTF-16LE and UTF-16BE, ignoring ASCII case, in addition to the decoded text scan. The BOM tests now hide the URL behind zero-width spaces so they pin the decoding path on its own. * test(stage): pin upstream history and reflog coverage in the mirror blob scan Adds a stage test where an old managed commit preserved an upstream version of a file that upstream later changed: copying that old upstream content elsewhere must still ship. It fails if upstreamVersions stops reading upstream history. The 10 x 300 timing test reused one tree for every reflog entry, so it passed even when the reflog was not read at all. Each entry now carries new versions of the preserved files, and the test asserts every version is collected. * fix(stage): tighten repo-name matching in the deny list Three gaps in findDeniedText: - File content: a bare mirror repo name now needs a host before it (github.com/, git@host:, ssh://host/, https://host/). A package or directory with the same name, such as "acme-ui/widget-private", no longer blocks a stage. Commit messages, branch names, PR text and file names keep the loose owner/name match. - The bare-name lookahead rejected any "/" or "." after the name, so "https://github.com/other/<name>/pull/3", "other/<name>." at a sentence end and "other/<name>.git." slipped through. A hosted match now allows any path after the name, and a trailing "." counts only when it starts a file extension. - Every term except "venfork" now ends at a repo-name boundary, so "acme/widget-private-public" no longer matches "acme/widget-private" while ".git", "/pull/1", "#7" and "?x" suffixes still do. * fix(stage): match the venfork config by its venfork-only keys The config signature needed a "version" key and a URL-valued upstreamUrl, so an ordinary package-like JSON with both was refused, while a config without "version" shipped. It now needs a URL-valued upstreamUrl plus a URL-valued publicForkUrl, "mode": "no-public", or any of preserve, schedule, shippedBranches, pulledPrs, shippedIssues and pulledIssues. VENFORK_ALLOW_SELF_REFERENCE=1 used to skip the check entirely, so this mirror's own config could be re-serialised and published. Under self-reference a config-shaped file is still refused when its upstreamUrl or publicForkUrl names this mirror's upstream or public fork (by owner/repo, from the config and the remotes) or when a link map is not empty. The error message and README describe the new signature. * fix(stage): refuse instead of warning when mirror history exceeds the scan cap The mirror blob scan read at most 2000 reflog entries and commits and only warned past that, so an old preserved version beyond the cap could ship. The cap is now 20000 (the scan is cheap since the batch rewrite). It reads cap + 1 and, when a reflog or the mirror-only history is strictly longer than the cap, refuses with a message naming the cap and the way out: stage from a clone with a shorter reflog, or expire old reflog entries after confirming no mirror-only history is needed. There is no override. Exactly cap entries are read in full without complaint. The "no reflog" warning is gone: every fresh clone has an empty reflog for its remote-tracking refs, so it fired on almost every run. * fix(preserve): single-quote entries in every preserve remove hint The hints printed "venfork preserve remove *.md", which a shell expands before venfork sees it, so copying the hint removed the wrong entries or failed. preserveRemoveHint single-quotes each entry (escaping embedded quotes). Sync, stage, preserve status, doctor and the managed-commit errors all use it. * fix(stage): treat a directory preserve entry as invalid at stage and sync normalizePreservePath cannot tell a file from a directory, so an old config could still hold a directory entry. Stage then expanded it with one "git ls-tree -r" per distinct subtree in the mirror history, which took about 25 s for 2000 versions, and sync failed with a divergence error that did not name the fix. Stage (origin/<default> and the local default branch), sync (origin/<default>, before the divergence check) and every origin tip rebuild (the previous mirror tip) now refuse an entry that is a directory, with the invalid-entry error and its quoted "venfork preserve remove '<entry>'" hint. collectMirrorBlobs no longer expands subtrees: a directory found for a preserve entry anywhere in the history it reads is refused the same way. The 2000-version case is refused in well under 2 s. * fix(redaction): let a plain venfork comment through under self-reference With VENFORK_ALLOW_SELF_REFERENCE=1, any HTML comment that mentioned venfork was still refused as a possible misspelled marker, so a body with "<!-- uses venfork -->" could not be staged. Under self-reference only a comment shaped like a marker ("venfork:" or "venfork" followed by "internal") is refused now; without it every venfork comment is still refused. * docs(stage): describe the byte search, host-only repo names and its limits The stage section now says every file is byte-searched for origin's URL and owner/name as UTF-8, UTF-16LE and UTF-16BE whatever its BOM or NUL bytes, that compressed archives and UTF-32 text are not decoded, that a bare repo name in file content needs a host, and which owner/name forms pass in commit messages. * feat(doctor)!: fold status into doctor and remove the status command doctor now prints the shipped branch, pulled PR, shipped issue and pulled issue links from venfork-config after the checks. doctor --json prints { checks, links } instead of a bare array; links is null when the config cannot be read. BREAKING CHANGE: venfork status and status --check are removed. Use venfork doctor. The doctor --json output is now an object with checks and links. * feat(cli)!: rename pull-request and issue commands to pull pr, pull issue and stage issue Commands now follow direction: `pull` brings upstream content in, `stage` pushes mirror content out. BREAKING CHANGE: `venfork pull-request` is now `venfork pull pr`, `venfork issue pull` is `venfork pull issue`, and `venfork issue stage` is `venfork stage issue`. The old names are removed with no aliases. `venfork stage branch <name>` is the explicit form of `stage <name>`; a branch named `issue` or `branch` must use it. Config link-map keys are unchanged. * fix(stage)!: reject branch-only flags on stage issue `stage issue` silently ignored `--pr`, `--draft`, `--base`, `--internal-pr` and `--no-update-existing`. It now refuses them and names the flag, so a typo cannot look like it worked. Also sets the exact test counts for the restacked top of the stack. * fix(stage): run the deny list on staged issue titles and bodies stage issue only stripped internal markers before opening the upstream issue, so a body that named the private mirror by owner/name or URL was published verbatim. stage --pr already refused the same text. Both paths now run assertNoMirrorReference over the translated title and body before the confirm prompt. * fix(sync): check divergence, build and lease from one resolved origin tip checkDivergence took ref names and re-read origin/<default> itself, and sync resolved the same name again later for the lease and the preserved blobs. A fetch between those reads (an IDE autofetch is enough) moved the ref, the lease matched the new tip, and the push dropped a commit the check never saw. Sync now resolves refs/remotes/<remote>/<default> to a commit id once and passes that id to checkDivergence, buildOriginTip and the push; config-change, doctor and preserve do the same. Sync also read venfork-config before fetching origin. Config writers write the config before they push the tip, so the old order could pair a new tip with a stale preserve list and classify a fresh bot commit as stale-trailer, dropping the only copy of a newly preserved file. The config is now read after origin is fetched. A preserved path that upstream also has is upstream's content. A mirror commit that changed it was still skipped as preserved-only, then the rebuilt tip took upstream's version and the change was lost with no error. Such a commit now counts as divergence when the tip's blob differs from upstream's. The invalid-preserve guard test no longer claims the guard runs before any fetch; it pins that the guard wins over divergence and that neither remote is pushed. * fix(stage): anchor the gate on the resolved upstream tip, not the short ref name planStaging, the rebuild, the evil-merge check and the publish gate all named the trust anchor as upstream/<default>. Git resolves that short name as a tag or local branch before the remote-tracking ref, so a ref of that name pointing at the mirror tip moved the anchor onto the managed commit: the rebuild sat on top of it, base..head left it out, and the push shipped the preserved files and the bot commit. The plan now resolves refs/remotes/upstream/<default> to a commit id once and every step uses that id. * fix(preserve): write add and remove as deltas on the list as read at write time preserve add and remove computed the full new list from an early read and wrote it under a lease taken on a later read, so a concurrent change between the two reads was overwritten with no conflict. For remove the re-stamp then built the tip from that stale list while the divergence check used the fresh one, and a file a teammate had just preserved was dropped from origin with no error. Patches now carry preserveAdd and preserveRemove, applied to the config inside the leased write, and applyConfigChange allows preserved-only commits by the patched list. preserve remove also refuses an entry that is neither listed nor carried by origin's managed commit, instead of reporting success and writing a config commit. * test(unit): mock remote tips as stable ids now that sync resolves full refs The string-mocked execa dispatcher answered rev-parse on refs/remotes/* with the generic '.git' stub, so every sync and stage unit test looked up rev-list ranges that no longer occur. Remote tips now resolve to <remote>0tip and the test keys use those ids. * fix(doctor): judge the preserve list with the tip builder sync runs Doctor only checked that each preserved path existed on origin's tip, so it passed a directory entry, an upstream directory at a preserved path and an upstream file at an ancestor path while sync refused all three, and it failed a path missing on origin that upstream now has while sync passed. The preserve check now calls buildOriginTip, which pushes nothing, and reports whatever it throws. A pin-downgrade refusal is a typed PinDowngradeError, so doctor can skip the check and leave the workflow check to report it. * docs(schedule): recommend a scoped push token and warn while upstream workflows can read it schedule set, doctor's fix text and the README all told users to store $(gh auth token) as VENFORK_PUSH_TOKEN. That is the whole account, and a repository secret is readable by every workflow on the mirror's default branch, which by default includes every workflow upstream ships. All three now describe a fine-grained token limited to the mirror and the public fork with Contents and Workflows write, and schedule set and doctor warn while both workflow lists are empty. * test: raise the count gate for the review-blocker tests * fix(stage): refuse internal markers in published file content and commit metadata Marker-shaped text (venfork:internal, canonical-folded) shipped in file content in every mode and in commit messages under VENFORK_ALLOW_SELF_REFERENCE=1. Refuse it in file content and in the author, committer and message fields whether or not self-reference is set. * fix(stage): refuse near copies of preserved text Blob identity let a preserved file out after a one-letter edit, a prepended line, CRLF, a BOM or UTF-16. collectMirrorBlobs now also returns a normalized fingerprint and the long-line set of each mirror text blob (lines upstream's own version of the path has are left out). The gate refuses a published text with the same fingerprint or sharing more than half of those lines (at least three). gzip, base64 and UTF-32 copies stay undetected and are documented in the README. * fix(stage): refuse the bare mirror name, encoded paths and Pages URLs in published text A bare repo name (widget-private, widget-private#12) passed in messages, titles, bodies and branch names because the name needed an owner/ prefix. It now matches as a whole word there. File names keep the owner/ rule (nameMatch 'owner') and file content keeps the host rule (nameMatch 'host'), replacing hostOnlyNames. The unit tests that pinned a plain word and a src/<name>/ directory passing in messages now pin them for file names. Text and terms are percent-decoded (up to three layers, malformed sequences tolerated) before matching, and <owner>.github.io/<name> is a term. owner/name is also read from non-github.com origins such as GitHub Enterprise with a local helper; parseRepoPath is unchanged. Decoding lives in the matcher, not canonicalText, so JSON config detection still parses strings that contain %22. * fix(sync): read commit metadata without log.showSignature output * fix(sync): apply workflow allow/block lists whenever the managed commit is built * fix(sync): treat a workflow deletion the policy does not make as a user change * fix(sync): word the stale-trailer warning per kind and list the paths it discards * fix(workflow): fail fast when VENFORK_PUSH_TOKEN is unset and cover more SSH forms Drop the github.token fallback for checkout, add a first step that fails with an error annotation when the secret is empty, and name the cause in the blocked-sync issue. Also rewrite ssh://git@github.com:22/, ssh://git@ssh.github.com:443/ and git@GitHub.com: to HTTPS. Snapshot updated after reviewing the diff: only these YAML changes. * fix(workflow): avoid SIGPIPE when checking the blocked-sync issue body Capture the issue body before grep -q so bodies over 64 KiB no longer fail the pipeline under pipefail and trigger a duplicate comment. Snapshot updated after reviewing the diff: only the issue-view block changed. * fix(workflow): validate VENFORK_INSTALL_SPEC and drop the git spec claim Accept only venfork@<semver> or an https tarball URL ending in .tgz. A git install has no dist/ and no venfork binary, so it passed the install step and failed at sync. Also update the README for the token check. Snapshot updated after reviewing the diff: only the install step changed. * docs(schedule): add the 0.10 upgrade path and an unpinned-workflow helper Document installing the new CLI, setting VENFORK_PUSH_TOKEN and running venfork sync once before the release reaches npm. Add isUnpinnedWorkflow so sync can detect an unmigrated mirror when running inside Actions. * fix(cli): name the replacement when a renamed command is run * fix(cli): reject unknown options and extra arguments in every command parser Shares consumeValue and a scanArgs walker in shared/args. pull accepts flags in any position. Numeric refs and --internal-pr take plain positive integers; PR and issue URLs are anchored and compared case-insensitively. * fix(cli): report missing-argument usage errors on stderr from the parsers Adds schedule-args and moves the missing branch and missing ref checks for stage, pull pr and pull issue into their parsers. * fix(cli): route help <cmd> to per-command help and parse pull before the gh check * fix(sync): refuse to rewrite an unpinned workflow from a scheduled run A runner that installs the latest venfork on a mirror whose workflow was written before 0.11 would rewrite the managed commit from a job that may lack the push token, failing silently because the old YAML has no report step. Under GITHUB_ACTIONS sync now refuses with a typed error that names the local migration step; a local sync still migrates the mirror. * test: raise the count gate for the follow-up slices * fix(release): drop bun-windows-arm64 and report every failed compile target Pinned Bun 1.2.18 cannot compile bun-windows-arm64 (Unsupported compile target), which failed release-cli before any upload. Bun 1.4.2 supports it, but bumping the toolchain pin this late is riskier than dropping a target, so option (b): remove it. Targets move to scripts/release-targets.ts and build with Promise.allSettled so each failing target is named. release.yml uploads dist/targets/venfork-* by glob, so no asset name changes. * fix(net): keep the user's ssh command when forcing BatchMode GIT_SSH_COMMAND set by NET_ENV beat core.sshCommand and overwrote a user-set GIT_SSH_COMMAND, breaking custom keys, ProxyCommand and agent paths. netExec now builds the value per call from GIT_SSH_COMMAND, else core.sshCommand of the cwd, else plain ssh, and appends the fail-fast options. * fix(net): retry seed pushes on slow, reset, DNS and 429 failures isTransientPushError missed the abort its own http.lowSpeedTime produces (Operation too slow), macOS wording (Connection was reset), Could not resolve host and HTTP 429. It also threw a TypeError for undefined input. * fix(cli): exit 130 when a prompt is cancelled Cancelling a prompt (Ctrl-C or EOF) in setup, stage and issue exited 0, so venfork setup </dev/null read as success in CI. Cancel now exits 130; an explicit No still exits 0. In stage and issue the exit moves out of the try so the generic catch cannot turn it into 1. * test: raise the count gate for the pre-merge fixes * test(e2e): assert tier 1 against the validated install step and token preflight * test(e2e): describe tier 2 against the token preflight with no checkout fallback * test(e2e): pin tier 6 to the VENFORK_PUSH_TOKEN preflight failure * fix(stage): match the mirror name with a - or _ suffix in text bound for upstream In word mode (branch names, PR titles and bodies, commit metadata) the mirror name followed by '-' or '_' counted as a longer repo name, so fix/<mirror>-sync and <mirror>-staging passed the deny list and reached the public fork and the upstream PR. Word mode now only treats a following letter or digit as part of a longer word. Owner and host modes keep the old boundary so file paths like src/<name>-ui/ still publish. * fix(sync): list sync-blocked issues through REST and report a failed run once gh issue list reads GitHub's search index, which lags a just-opened issue, so the workflow failure step missed the issue sync --report-issues had opened seconds earlier and opened a duplicate. Both the CLI and the generated workflow now list open labelled issues through the REST issues endpoint, skipping pull requests it also returns. The sync step gets id: sync and venfork sync --report-issues writes reported=true to GITHUB_OUTPUT after it opens or updates the issue; the failure step is skipped when that output is set. The e2e helper lists issues the same way. Workflow snapshot updated with bun test -u after reading the diff: the sync step id, the failure step guard and the REST listing line. * fix(doctor): read the sync workflow state through the REST API gh workflow view has no --json flag, so on a real mirror the cron-age check always skipped with 'unknown flag: --json'; the unit mocks answered the non-existent command and hid it. Doctor now asks gh api repos/<mirror>/actions/workflows/venfork-sync.yml --jq .state, and a disabled_manually or disabled_inactivity state fails the check with a fix that names the Actions tab and gh workflow enable. * fix(workflows): add to the allow and block lists instead of replacing them workflows block other.yml replaced the whole block list, so it silently unblocked ci.yml and exposed VENFORK_PUSH_TOKEN to it again. allow and block now add basenames to their list through new delta patches (enabledWorkflowsAdd/Remove, disabledWorkflowsAdd/Remove) applied to the config as read at write time, like preserveAdd/preserveRemove. New unallow and unblock actions remove files from one list; clear still empties both. Separate verbs were chosen over a single 'remove' so the command cannot be read as removing a workflow from the mirror. * fix(cli): exit 130 when input ends at a prompt With @clack/prompts 0.11 a prompt whose stdin hits EOF never resolves. The event loop drained with the command still pending and Node exited 0, so 'venfork setup owner/repo --org x </dev/null' looked like success with nothing created. index.ts now registers a beforeExit handler before dispatch that, while the command promise is unsettled, prints 'Cancelled: input ended at a prompt' to stderr and exits 130; the handler is removed once the command settles. * fix(git): stop guessing main when a remote's default branch is unknown getDefaultBranch ignored the exit code of git remote set-head -a and returned 'main' whenever refs/remotes/<remote>/HEAD was missing, so venfork schedule status on a fresh clone of a master mirror printed 'Branch: main'. It now throws a GitError naming the remote and the set-head reason when neither set-head -a nor an existing remote HEAD yields a branch. schedule fetches upstream before asking (status fetches itself; set and disable ask after applyConfigChange has fetched), and doctor reports the error on its git-state checks instead of crashing. * fix(clone): add public and upstream over the transport gh uses clone copied upstreamUrl and publicForkUrl from venfork-config verbatim, so with gh git_protocol=https origin was https while public and upstream stayed SSH, and an HTTPS-only user could not sync. clone now rewrites github.com URLs to https://github.com/<owner>/<repo>.git or git@github.com:<owner>/<repo>.git to match gh config get git_protocol; other hosts and local paths are untouched, and nothing is rewritten when gh reports no protocol. setup records and adds its remotes the same way, and re-points an existing remote whose URL differs. * docs: correct the 0.10 upgrade path and the messages that point at it The README said the first scheduled run after 0.11 rewrites the managed commit and told users to install venfork@latest before the release; the code refuses with UnmigratedMirrorError instead, and @lates…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bumps actions/checkout from 4 to 5.
Release notes
Sourced from actions/checkout's releases.
... (truncated)
Changelog
Sourced from actions/checkout's changelog.
... (truncated)
Commits
08c6903Prepare v5.0.0 release (#2238)9f26565Update actions checkout to use node 24 (#2226)Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting
@dependabot rebase.Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
@dependabot rebasewill rebase this PR@dependabot recreatewill recreate this PR, overwriting any edits that have been made to it@dependabot mergewill merge this PR after your CI passes on it@dependabot squash and mergewill squash and merge this PR after your CI passes on it@dependabot cancel mergewill cancel a previously requested merge and block automerging@dependabot reopenwill reopen this PR if it is closed@dependabot closewill close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually@dependabot show <dependency name> ignore conditionswill show all of the ignore conditions of the specified dependency@dependabot ignore this major versionwill close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)@dependabot ignore this minor versionwill close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)@dependabot ignore this dependencywill close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)