ci(e2e): real-provider E2E with multi-run isolation (Phases 0-2) - #124
Merged
Conversation
Adds e2e/suites/sync-manager.e2e.test.ts, parametrized by E2E_PROVIDER so it reuses the existing github/gitlab/gitea adapters and verifiers rather than duplicating provider-service contract coverage. Real SyncManager + real production provider service; only the Obsidian filesystem boundary is faked (e2e/shim/fake-vault.ts, an in-memory Map, not vi.fn() mocks). Covers: local new file -> push, unchanged file -> no remote mutation, remote update -> pull, conflict protection (push must not silently overwrite, and must not falsely mark synced), rename/move in exactly one commit, delete via the real service (SyncStatusView's actual call path, not a SyncManager method), and batch push-all in exactly one commit. Extends e2e/shim/obsidian-request-url.ts with the minimal set of real `obsidian` values SyncManager's dependency graph needs at runtime (TFile, Notice, Platform, FileSystemAdapter, Modal, plus PluginSettingTab/ TextComponent/AbstractInputSuggest/TFolder/Setting, pulled in transitively via `../settings`'s pure functions sharing a module with the settings-tab UI class) -- see that file's header comment for the full trace. Wires the new suite into scripts/run-e2e.mjs so `npm run test:e2e -- provider <name>` covers both the contract suite and SyncManager scenarios in one command/container lifecycle. Verified for real: `npm run test:e2e -- --provider gitea` against local Docker, 14/14 passing across multiple consecutive runs. GitHub/GitLab share the same harness but have only been lint/build/typecheck-verified here (no sandbox credentials in this environment) -- see docs/testing/real-provider-e2e.md.
GitHubVerifier already had a GitHub-specific listCommitShas (used by the symlink/GraphQL regression suite); promotes it to the shared RemoteVerifier contract and implements it for Gitea and GitLab too, so the new cross-provider SyncManager suite can assert "rename/batch landed as exactly one commit" identically for all three providers instead of casting to a provider-specific verifier type. Also adds E2E_KEEP_BRANCH support to all three provisioners' teardown (skip branch deletion / container removal for debugging a failing run) -- called out in the issue's sandbox-lifecycle acceptance criteria but not yet implemented.
Adds a provider-e2e matrix job (github/gitlab/gitea) to .github/workflows/ci.yml, per the issue's runner-fleet revision -- runs-on: [self-hosted, linux, x64, 32gb-ram], one matrix instead of three hand-written jobs. - `changes` job (dorny/paths-filter) decides whether provider-e2e should run for a given push/PR without gating the whole workflow by path -- on.push.paths/on.pull_request.paths would have also blocked the release-critical CI job for unrelated changes, which this avoids. - Fork PRs only get the Gitea cell (no repo secrets needed/exposed); internal PRs, pushes to main, workflow_dispatch, and the weekly schedule (Monday 06:00 UTC, API-drift detection) get all three. - scripts/run-e2e-ci.mjs wraps scripts/run-e2e.mjs for CI: sweeps stale gfs-e2e-<provider>-* branches first (scripts/e2e-sweep-branches.mjs), and turns a missing required credential into a hard failure rather than a silent skip, for any cell the job-level `if:` already decided should run. - e2e-gate aggregates the matrix (if: always(), success/skipped pass through, anything else fails) and CI now needs it, so a real provider regression blocks the shared CI/semantic-release workflow instead of shipping. E2E_GITLAB_PROJECT_ID is read from secrets, not vars, in this workflow -- confirmed via `gh secret list` that it's configured as a secret (unlike E2E_GITHUB_OWNER/REPO, which are plain vars) on this repo. Verified: scripts/run-e2e-ci.mjs and scripts/e2e-sweep-branches.mjs run correctly against Gitea locally (14/14 passing) and fail explicitly (exit 1, no silent skip) when GitHub credentials are absent; workflow YAML validated with js-yaml. Not verified: actual execution on the self-hosted runner fleet or the full e2e-gate -> CI dependency chain in a real workflow run (no self-hosted runner access from this checkout) -- see docs/testing/real-provider-e2e.md's "Known gaps".
Consolidates local setup, required secrets/vars (cross-checked against what's actually configured on firstsun-dev/git-files-sync via `gh secret list`/`gh variable list`, not just what the issue originally proposed), CI wiring, fork/secrets behavior, release gating, and cleanup/ troubleshooting into one operational doc -- docs/test/github-e2e-plan.md (agent 02) stays as the GitHub-specific implementation notes; this is the cross-provider operational reference. Explicitly lists what's unverified from this environment (GitHub/GitLab SyncManager E2E execution, self-hosted runner behavior, branch-protection required-check setup) rather than leaving it implicit.
GitHub Actions rejects the workflow file with 'Unrecognized named-value: matrix' -- job-level `if:` has no access to the `matrix` context, only step-level `if:` does. The provider-e2e job's `if:` referenced matrix.provider to skip GitHub/GitLab legs on fork PRs and to filter by workflow_dispatch input, which is invalid. Fix: keep only the non-matrix-dependent condition (path-relevance/dispatch/ schedule/main) on the job's own if:, and move the matrix-dependent part into a new 'Determine whether this provider leg should run' step that gates every subsequent step via its output. A gated-off leg's steps are all skipped without failing, so the job (and therefore the matrix as a whole, for the e2e-gate aggregation) still reports success -- same external behavior as originally intended, just relocated to where GitHub Actions actually allows matrix to be read. Also corrected docs/testing/real-provider-e2e.md and the comment in scripts/run-e2e-ci.mjs that described the now-nonexistent job-level if mechanism. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…failures
Investigated the three provider-e2e CI failures from the run right after the
matrix if: fix:
- github: sync-manager.e2e.test.ts's rename/batch tests asserted
listCommitShas(branch).length grew by exactly 1, but the sandbox repo's
main already has 47 commits -- past the API's default page size (30) --
so both the 'before' and 'after' calls silently cap at 30 and the
assertion ("expected 30 to be 31") fails deterministically, not flakily.
Confirmed by querying the real sandbox repo directly. Fixed by comparing
HEAD-before against the two newest commits after (listCommitShas(ref, 2))
instead of full-list length -- exact regardless of total history depth,
matching the pattern github.e2e.test.ts already uses correctly. Verified
against the real sandbox: both previously-failing assertions now pass.
- gitea: Docker container never answered its healthcheck within 60s on the
self-hosted runner. Reproduced locally with the same code (Docker
available here) and it passed cleanly in ~17s -- not a code bug, looks
like a runner-side Docker/network blip. Can't fix infra flakiness from
here, so instead made the next occurrence self-diagnosing: capture the
container's own stdout/stderr on a readiness timeout and attach it to the
thrown error (docker.ts: containerLogsAllowFailure; wired into
gitea-provision.ts's catch), so a future CI failure shows *why* Gitea
never came up instead of just "fetch failed".
- Same teardown TypeError as already fixed in gitlab.e2e.test.ts
(afterAll running adapter.teardown(ctx) with ctx still undefined when
beforeAll fails) also existed in gitea.e2e.test.ts and
sync-manager.e2e.test.ts -- applied the same 'if (ctx)' guard to both.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
commitBatch hardcoded operation:'create' for every addition regardless of existedRemotely, unlike pushBatch. A batch-resolved 'keep local' conflict on an existing path landed in commitBatch's additions list and would 422 against Gitea's contents API. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The conflict modal was capped at ~1100px wide regardless of viewport, leaving large unused margins and cramped Local/Remote panes on desktop. Widen to min(1600px, 96vw)/92vh, raise the two-column breakpoint from 600px to 900px (below that width two panes are too narrow to read a real Markdown file), and switch content panes to horizontal-scroll instead of wrapping long lines. Also styles the new batch conflict resolution modal, which shares the same modal shell. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Batch push used to silently skip conflicting files and push everything else, forcing users to resolve and re-push conflicts one at a time -- defeating the point of batch push for large vaults. Batch push is now one transaction: classify every candidate first (nothing written), let the user resolve any conflicts via the new BatchConflictResolutionModal (bulk actions + per-file keep-local/ keep-remote/skip, with a 'View Diff' drill-in reusing the widened SyncConflictModal, including binary-file support), review the final resolved plan, then commit everything ready to push/move in one grouped call. A 'keep local' resolution rides along in that same commit as an ordinary update; a 'keep remote' resolution is only written to the vault after that commit succeeds -- a failed or cancelled batch leaves both sides untouched. A pre-commit snapshot re-check aborts safely rather than silently applying a reviewed decision against remote content that moved on since planning. - src/logic/sync-manager.ts: split classification (buildBatchPushPlan) from execution; added BatchPushConflict/BatchPushPlan/ ResolvedBatchPushPlan types; commitResolvedBatch handles the pre-commit staleness re-check and post-commit 'keep remote' reconciliation; PushResults gained resolvedConflicts/ skippedConflicts/cancelled/conflictedPaths. - src/ui/BatchConflictResolutionModal.ts: new batch resolution UI. - src/ui/SyncConflictModal.ts: widened to accept binary content (string | ArrayBuffer) so it can be reused for a batch row's diff. - src/ui/types.ts, src/ui/SyncPlanModal.ts: SyncPlan gained optional acceptedRemote/skippedConflicts sections for the final review step. - i18n: new batchConflictModal.* / syncPlanModal.section.* strings (en, zh-tw, zh-cn). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Rename SyncManager.pushAllFiles to pushFiles and make it the only push entry point (ribbon, command palette, context menu, sync-status row, selected, all-modified). Individual Push is now literally pushFiles([file]), the same pipeline as Selected x1 Push. Also fixes classifyAsMoveCandidate to resolve a TFile from a bare path string via vault.getFileByPath when the caller only has a path (e.g. a sync-status row with no cached TFile yet), instead of skipping rename detection outright. Together these close the reported bug: renaming a file in the editor then using Individual Push produced an Addition instead of a Move whenever the caller resolved the file as a path string rather than a live TFile. Regression tests added to tests/logic/sync-manager.test.ts cover both the string-path case and direct single-vs-batch equivalence.
Delete pushFile, tryPushAsSymlinkOrRename, openPushConflictModal, handleRename, and commitMove now that every push entry point goes through pushFiles. Push conflicts (single or batch) now resolve through BatchConflictResolutionModal instead of the single-file SyncConflictModal flow; SyncConflictModal itself is kept (still used by pull, and by BatchConflictResolutionModal's per-row 'View Diff'). Migrated all tests that called the deleted SyncManager.pushFile to pushFiles([...]) instead, and updated assertions that depended on the old single-file mechanics: classification now reads a pre-fetched remote tree (listFilesDetailed) rather than a live getFile() per push, so several tests needed a matching tree-entry fixture; conflict tests now drive the BatchConflictResolutionModal mock instead of SyncConflictModal's local/remote callback. Full gate green: npx eslint . -- 0 errors; npm run build -- clean; npx vitest run -- 527/527 passed.
Test/real provider e2e agent04
…e-work # Conflicts: # e2e/provision/docker.ts # e2e/provision/gitea-provision.ts # e2e/provision/github-provision.ts # e2e/provision/gitlab-provision.ts # e2e/shim/obsidian-request-url.ts # e2e/suites/gitea.e2e.test.ts # e2e/verifier/gitea-verifier.ts # e2e/verifier/github-verifier.ts # e2e/verifier/gitlab-verifier.ts # e2e/verifier/verifier-contract.ts # scripts/run-e2e.mjs
… into test/real-provider-e2e-work
Real-provider E2E returns after the scanner-driven removal in main (002000e), rebuilt so no committed .ts uses the flagged APIs (fetch/globalThis/node:crypto/node:child_process/node:util/bare timers), regardless of directory: - scripts/e2e-harness.sh (provision/seed/verify/cleanup/sweep): Shell + Git CLI owns branch/container lifecycle. GitHub/GitLab isolation via `git push <sha>:refs/heads/<branch>`, no REST branch-creation calls. Gitea's disposable container+repo via plain docker/curl, never node:child_process. GIT_ASKPASS generated per-run under $RUNNER_TEMP/$E2E_WORKDIR, never persisted (no token in remote URLs, .git/config, credential.helper, args, or logs). - Node-only glue the suites still need at runtime (requestUrl shim, window timer alias, a git-CLI-backed verifier) is generated by `provision` into $E2E_RUNTIME_DIR, never committed -- suites import only a type-only contract (e2e/verifier-runtime-types.ts) statically and load the concrete implementation via a runtime-computed dynamic import(), so npm run build's typecheck never needs the harness to have run first. - Ported all four suites (github/gitlab/gitea/sync-manager) to the unified SyncManager.pushFiles API from claude/unify-push-pull-pipeline. - scripts/run-e2e.sh: local orchestration wrapper (provision -> seed -> vitest -> cleanup). CI drives the same steps directly per job step. - Removed e2e/provision, e2e/verifier/{github,gitlab,gitea}-verifier.ts, e2e/providers, e2e/shim/{obsidian-request-url,window-timers}.ts, e2e/namespace.ts, e2e/redact.ts, scripts/run-e2e*.mjs, scripts/e2e-sweep-branches.mjs -- superseded by the above. - e2e/**/*.ts back in tsconfig.json's include and eslint's scope. Verified with a real end-to-end run against a live local Gitea sandbox (npm run test:e2e -- --provider gitea): 14/14 E2E tests passed, including a real Docker provision/seed/cleanup cycle. GitHub/GitLab legs are written and typecheck/lint clean but unverified live (no sandbox credentials in this environment) -- see docs/testing/real-provider-e2e.md. npx eslint . -- 0 errors npm run build -- clean (incl. Obsidian 1.11.0 compat typecheck) npx vitest run -- 527 passed Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The pushed Phase 1 harness failed its first real CI run against firstsun-dev/git-files-sync (run 31665711682). Root causes, all found by reading the actual job logs: - github/gitlab legs: the vitest step's generated GitVerifier shells out to git, but GIT_ASKPASS/GIT_TERMINAL_PROMPT only ever existed inside the provision/seed/cleanup steps' own processes -- the vitest step is a separate process that only sources e2e.env, which never carried them. `git fetch` prompted for a username and failed. Now persisted (as a path, not a secret -- the token itself stays only in the mode-700 askpass file on disk) in e2e.env's write_env_file/load_env_file. - gitea leg: provisioning timed out waiting on `127.0.0.1:<host-port>` -- this runner fleet is itself a sibling container of the Docker daemon, so a published host port is only reachable from the Docker host's own network namespace, not from a sibling container's. Switched to the gitea container's own bridge IP (reachable from any container on the same default Docker network, including a sibling runner), dropping the -p mapping entirely. - gitea leg's cleanup step then also failed: cmd_cleanup called setup_askpass unconditionally before branching on provider, but gitea's cleanup is pure `docker rm` and needs no git credentials -- and since provision had already failed before provisioning a token, there was nothing for setup_askpass to require. Gitea's branch now runs first and skips setup_askpass entirely. Verified with another real end-to-end run against a live local Gitea sandbox (npm run test:e2e -- --provider gitea): 14/14 passed, using the container's bridge IP this time. Full gate still green: eslint 0 errors, build clean, vitest 527/527. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The github/gitlab-fix push (1393956) triggered a second real CI run: both github and gitlab legs passed this time, confirming the GIT_ASKPASS/ GIT_TERMINAL_PROMPT propagation fix. The gitea leg hung for 10+ minutes on "Provision isolated branch/container" -- well past the 60s ready_ms budget -- and had to be cancelled manually. Root cause: none of the curl calls in provision_gitea_container had a --max-time. A curl against an unreachable/blackholed address (e.g. an empty container_ip if `docker inspect` raced the container's network attachment) can hang far longer than the health-check loop's own timeout budget, instead of failing fast into the next retry -- the loop's `waited -ge ready_ms` check never gets a chance to fire if a single curl call itself never returns. Fixes: retry docker inspect up to 10x/1s if container_ip comes back empty before ever starting the health loop (fail fast with a clear error if it never does); --max-time on every curl call in this function (5s for the per-poll healthz check, 15s for the one-shot repo/token/user setup calls and the gitlab project-lookup call in normalize_env). Verified locally again (timed): full provision -> seed -> vitest -> cleanup in 13.6s, no hangs. Full gate still green. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The gitea leg still needs more investigation against this specific self-hosted runner fleet's Docker topology (bridge-IP reachability, health- check timing already needed two rounds of fixes) -- not something safe to keep iterating on inside the shared provider-e2e matrix while github/gitlab are otherwise green. Gate it off via the existing per-provider "Determine whether this leg should run" step rather than removing it from the matrix, so job structure/naming stays stable for whoever re-enables it. Suite and harness code (e2e/suites/gitea.e2e.test.ts, scripts/e2e- harness.sh's gitea path) is untouched -- verified locally again just now (`npm run test:e2e -- --provider gitea`, 14/14 passed, 13.6s) -- only CI execution is paused pending runner-environment follow-up. Re-enable by deleting the added `if` block once confirmed. Note: gitea is normally what covers fork PRs without needing real credentials -- while disabled, fork PRs get zero E2E coverage. Acceptable short-term given this branch has no open fork PRs yet. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Centralize branch-namespace identity in scripts/e2e-namespace.sh (e2e/pr/<n>/<provider>/run-<id>-<attempt> and e2e/branch/<sanitized-id>/<provider>/run-<id>-<attempt>, with a content-hash suffix so differently-slashed branch names can't collide), sourced by e2e-harness.sh, the new e2e-namespace-cleanup.sh, and e2e-janitor.sh -- one canonical implementation, not several drifting apart. Implement the three-layer cleanup hierarchy the isolation model needs: - Layer 1 (scripts/e2e-harness.sh cleanup): unchanged in spirit, now deletes only this run's own uniquely-named branch. - Layer 2 (scripts/e2e-namespace-cleanup.sh + new .github/workflows/e2e-pr-cleanup.yml / e2e-branch-cleanup.yml): authoritative on PR close or source-branch delete, removes the whole e2e/pr/<n>/** or e2e/branch/<id>/** namespace. PR cleanup uses pull_request_target with no ref: override on checkout, so it only ever runs this repo's own trusted code/secrets, never the closing PR's branch. - Layer 3 (scripts/e2e-janitor.sh + new .github/workflows/e2e-janitor.yml, scheduled every 6h): TTL sweep (24h default) of any leftover e2e/** branch via generic git for-each-ref/push --delete, tolerant of already-deleted refs. Removes e2e-harness.sh's old ad hoc sweep subcommand/gfs-e2e-<provider>-* naming, superseded by the above. ci.yml's provider-e2e job: E2E_WORKDIR now pinned per run-id/run-attempt/provider under (was a shared e2e-<provider> dir), E2E_PR_NUMBER/E2E_SOURCE_BRANCH passed through for provision, and a per-source/provider concurrency group (cancel-in-progress: true) so a repeated push/rerun cancels its own predecessor -- cancellation is not a cleanup mechanism, so this is only possible because every run still gets its own unique branch regardless. Rewrites docs/testing/real-provider-e2e.md's isolation model with a Mermaid diagram of the cleanup hierarchy. Verification: npx eslint . -- 0 errors; npm run build -- clean; npx vitest run -- 527 passed; yaml.safe_load on all touched/new workflow files; bash -n on all new/changed scripts; functional dry-runs of the namespace/janitor/cleanup logic against throwaway local git repos; real end-to-end run against a live local Gitea sandbox (14/14 E2E tests passed) with the new harness code. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ci.yml's provider-e2e job set E2E_WORKDIR in job-level env: using
${{ runner.temp }}, but the runner context isn't allowed there (only
github/inputs/matrix/needs/secrets/strategy/vars are) -- GitHub Actions
rejects the whole workflow file at parse time for this, which is why
the c8382cb push produced 0 jobs and no CI/CD check run at all.
Confirmed with actionlint and the GitHub API. Fixed by computing
E2E_WORKDIR in an unconditional first step instead, exporting it via
$GITHUB_ENV ($RUNNER_TEMP is available there).
Also fixes the SonarCloud Quality Gate failure (Security Rating D on
new code, required >= A):
- e2e-namespace.sh's e2e_branch_hash used sha1sum/shasum (CRITICAL
weak-hash, shell:S4790) for a plain collision-avoidance digest, not
a security use -- switched to sha256sum/shasum -a 256.
- Five http:// curl/log lines in e2e-harness.sh's gitea provisioning
(shell:S5332 clear-text-protocol) talk only to a per-run
Docker-bridge-only container with freshly random, run-scoped
credentials -- annotated # NOSONAR with inline justification.
- ci.yml's new npm ci (githubactions:S6505) gets --ignore-scripts
(husky's prepare hook isn't needed in CI); actions/checkout,
actions/setup-node, and dorny/paths-filter in the two new ci.yml
jobs and the three new standalone workflow files (githubactions:
S7637, unpinned action refs) are pinned to full commit SHAs. The
pre-existing build-artifact job is left untouched (not flagged).
Verification: actionlint v1.7.12 -- 0 errors on all 4 workflow files
(one expected false positive on the 32gb-ram custom self-hosted
label); bash -n on all 5 touched/changed scripts -- clean; npx eslint
. -- 0 errors; npm run build (incl. Obsidian 1.11.0 compat typecheck)
-- clean; npx vitest run -- 527 passed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The first fix commit's # NOSONAR comments landed on the *closing* line of each multi-line gitea-provisioning curl call, but SonarCloud attributes shell:S5332 (clear-text protocol) to the *opening* curl line -- and a line ending in a `\` continuation can't also carry a trailing comment. Confirmed via SonarCloud's issues API after the previous push: rating went from D to B but 2 of the 7 findings still showed as OPEN, both on the curl lines themselves. Collapsed the repo-creation and token-creation curl calls to single lines (payload JSON pulled into a local var first) so the NOSONAR marker lands on the same line as the flagged statement. Verification: bash -n -- clean; npx eslint . -- 0 errors; npm run build -- clean; npx vitest run -- 527 passed; real end-to-end run against a live local Gitea sandbox (npm run test:e2e -- --provider gitea) -- 14/14 passed, exercising both edited curl calls directly. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
provider-e2e's concurrency group keyed PR runs by PR number and branch-only runs by branch name -- two different groups for the same branch when that branch has an open PR, since push and pull_request both fire for the same commit in that case but only pull_request sets github.event.pull_request.number. The two groups let both runs execute fully concurrently against the same shared provider sandbox, which starved GitLab's real API: confirmed on PR #124 by rerunning the pull_request-triggered run twice while the push-triggered run for the identical commit passed cleanly -- the concurrent run failed two different ways (400 Deadline Exceeded, then a plain testConnection timeout), consistent with sandbox-side contention, not a logic bug. Fixed by keying the concurrency group by branch name alone (github.head_ref || github.ref_name, already used for E2E_SOURCE_BRANCH) regardless of trigger event, so a push and its matching pull_request run collide into the same group and the later cancels the earlier -- same handling as a repeated push or a workflow rerun. Updated e2e-pr-cleanup.yml's and e2e-branch-cleanup.yml's concurrency groups to match (they're documented as sharing provider-e2e's group so cleanup queues behind rather than races an active run). Updated docs/testing/real-provider-e2e.md's Concurrency section accordingly. Verification: actionlint -- 0 errors; npx eslint . -- 0 errors; npx vitest run -- 527 passed (no src/ changes, workflow/docs only). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ClaudiaFang
added a commit
that referenced
this pull request
Aug 13, 2026
provider-e2e's concurrency group keyed PR runs by PR number and branch-only runs by branch name -- two different groups for the same branch when that branch has an open PR, since push and pull_request both fire for the same commit in that case but only pull_request sets github.event.pull_request.number. The two groups let both runs execute fully concurrently against the same shared provider sandbox, which starved GitLab's real API: confirmed on PR #124 by rerunning the pull_request-triggered run twice while the push-triggered run for the identical commit passed cleanly -- the concurrent run failed two different ways (400 Deadline Exceeded, then a plain testConnection timeout), consistent with sandbox-side contention, not a logic bug. Fixed by keying the concurrency group by branch name alone (github.head_ref || github.ref_name, already used for E2E_SOURCE_BRANCH) regardless of trigger event, so a push and its matching pull_request run collide into the same group and the later cancels the earlier -- same handling as a repeated push or a workflow rerun. Updated e2e-pr-cleanup.yml's and e2e-branch-cleanup.yml's concurrency groups to match (they're documented as sharing provider-e2e's group so cleanup queues behind rather than races an active run). Updated docs/testing/real-provider-e2e.md's Concurrency section accordingly. Verification: actionlint -- 0 errors; npx eslint . -- 0 errors; npx vitest run -- 527 passed (no src/ changes, workflow/docs only). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… state progress.md's Current State/Outstanding Items/Latest Evidence and session-handoff.md now reflect the actual session outcome: PR #124 is fully green (real GitHub/GitLab/Gitea E2E, SonarCloud Security Rating A, lint/build/vitest, CI Node 22/24/build/release), after fixing the workflow-file parse rejection, the Sonar security gate (incl. a NOSONAR-placement follow-up), and a push/pull_request concurrency race against the shared GitLab sandbox. session-handoff.md was stale (dated 2026-08-07, predating this entire E2E effort) -- replaced with this session's actual stopping point. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ClaudiaFang
added a commit
that referenced
this pull request
Aug 13, 2026
… state progress.md's Current State/Outstanding Items/Latest Evidence and session-handoff.md now reflect the actual session outcome: PR #124 is fully green (real GitHub/GitLab/Gitea E2E, SonarCloud Security Rating A, lint/build/vitest, CI Node 22/24/build/release), after fixing the workflow-file parse rejection, the Sonar security gate (incl. a NOSONAR-placement follow-up), and a push/pull_request concurrency race against the shared GitLab sandbox. session-handoff.md was stale (dated 2026-08-07, predating this entire E2E effort) -- replaced with this session's actual stopping point. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ClaudiaFang
force-pushed
the
test/real-provider-e2e
branch
from
August 13, 2026 13:53
ec665dc to
c42fa35
Compare
|
ClaudiaFang
pushed a commit
that referenced
this pull request
Aug 20, 2026
## [1.5.9](1.5.8...1.5.9) (2026-08-20) ### Bug Fixes * **ci:** harden provider e2e failures ([948df28](948df28)) * **ci:** move matrix-dependent E2E gating out of job-level if ([155e55c](155e55c)) * **e2e:** dedupe push/pull_request E2E runs on the same branch ([04cd91a](04cd91a)), closes [#124](#124) * **e2e:** fix CI workflow-file rejection and Sonar security gate ([a44520a](a44520a)) * **e2e:** fix commit-count pagination bug, guard teardown, log gitea failures ([147736e](147736e)) * **e2e:** fix NOSONAR placement on line-continued curl calls ([b944023](b944023)) * **e2e:** fix real CI failures found by the first live run ([b3c6b81](b3c6b81)) * **e2e:** prevent indefinite curl hangs in gitea provisioning ([2e083c2](2e083c2)) * **gitea:** preserve update semantics in mixed batch commits ([6569895](6569895)) * **sync:** resolve batch conflicts before atomic push ([c8b4a84](c8b4a84)) * **sync:** route all push entry points through the batch push pipeline ([2d93d77](2d93d77)) * **sync:** unify push pull and move decisions ([dff95db](dff95db)) * **ui:** make conflict viewer use available desktop space ([c802166](c802166)) ### Documentation * **e2e:** document real-provider E2E setup, CI, and troubleshooting ([60e2d2c](60e2d2c)) * **progress:** record Phase 0 E2E reconcile evidence ([a4de430](a4de430)) * **progress:** record PR [#124](#124 three CI/Sonar fixes and final green state ([c42fa35](c42fa35)) * **progress:** record real CI results and gitea-disable follow-up ([c3acf83](c3acf83)) * record provider ci verification ([5f9d528](5f9d528)) ### Code Refactoring * **sync:** delete legacy single-file push orchestration ([3b3fd2e](3b3fd2e))
Member
Author
|
🎉 This PR is included in version 1.5.9 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
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.



Summary
Real
SyncManager/GitHubService/GitLabService/GiteaServiceE2E against real GitHub, GitLab,and Gitea servers, safe under concurrent PRs, branch-only development, repeated pushes, workflow
reruns, provider-matrix parallelism, cancellation, and runner termination.
test/real-provider-e2eagainstmain(post scanner-driven removal)and
claude/unify-push-pull-pipeline's newSyncManager.pushFilesAPI.e2e/provision,e2e/verifier,e2e/providers,e2e/shim,scripts/run-e2e*.mjs— all of which usedfetch/node:child_process/node:cryptodirectly in committed.ts, flagged by the Obsidiancommunity-plugin scanner) with
scripts/e2e-harness.sh(Shell + Git CLI forprovision/seed/verify/cleanup) plus Node-only runtime glue generated fresh per run, never
committed.
scripts/e2e-namespace.shis the single canonical branchidentity generator (
e2e/pr/<n>/<provider>/run-<id>-<attempt>/e2e/branch/<sanitized-id>/<provider>/run-<id>-<attempt>), sourced by every layer. Three-layerbest-effort cleanup hierarchy (current-run delete, PR-close/branch-delete namespace cleanup,
scheduled TTL janitor) — isolation itself comes from unique branch names per run, never from
cleanup succeeding. Per-source/provider CI concurrency groups with
cancel-in-progress: true.See
docs/testing/real-provider-e2e.md's "Isolation model" section for the full design(includes a Mermaid diagram of the cleanup hierarchy).
Test plan
npx eslint .— 0 errorsnpm run build(incl. Obsidian 1.11.0 compat typecheck) — cleannpx vitest run— 527 passed(
npm run test:e2e -- --provider gitea) — 14/14 E2E tests passed, including a real Dockerprovision/seed/cleanup cycle
bash -non all new/changed shell scripts;functional dry-runs of the namespace/janitor/cleanup logic against throwaway local git repos
against the real self-hosted runner fleet and live sandbox repos — no runner/credential
access from this checkout; see "Known gaps" in the docs
ci.ymlpending a runner-Docker-topology follow-up(tracked in
progress.md's Outstanding Items) — harness code is unchanged and passeslocally every time
🤖 Generated with Claude Code