Skip to content

ci(e2e): real-provider E2E with multi-run isolation (Phases 0-2) - #124

Merged
ClaudiaFang merged 26 commits into
mainfrom
test/real-provider-e2e
Aug 14, 2026
Merged

ci(e2e): real-provider E2E with multi-run isolation (Phases 0-2)#124
ClaudiaFang merged 26 commits into
mainfrom
test/real-provider-e2e

Conversation

@ClaudiaFang

Copy link
Copy Markdown
Member

Summary

Real SyncManager/GitHubService/GitLabService/GiteaService E2E 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.

  • Phase 0 — reconciled test/real-provider-e2e against main (post scanner-driven removal)
    and claude/unify-push-pull-pipeline's new SyncManager.pushFiles API.
  • Phase 1 — replaced the Node-based E2E harness (e2e/provision, e2e/verifier,
    e2e/providers, e2e/shim, scripts/run-e2e*.mjs — all of which used fetch/
    node:child_process/node:crypto directly in committed .ts, flagged by the Obsidian
    community-plugin scanner) with scripts/e2e-harness.sh (Shell + Git CLI for
    provision/seed/verify/cleanup) plus Node-only runtime glue generated fresh per run, never
    committed.
  • Phase 2 — multi-run isolation: scripts/e2e-namespace.sh is the single canonical branch
    identity generator (e2e/pr/<n>/<provider>/run-<id>-<attempt> /
    e2e/branch/<sanitized-id>/<provider>/run-<id>-<attempt>), sourced by every layer. Three-layer
    best-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 errors
  • npm run build (incl. Obsidian 1.11.0 compat typecheck) — 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 E2E tests passed, including a real Docker
    provision/seed/cleanup cycle
  • Real CI run (prior push to this branch) against live GitHub/GitLab sandboxes — both green
  • YAML-validated all new/changed workflow files; bash -n on all new/changed shell scripts;
    functional dry-runs of the namespace/janitor/cleanup logic against throwaway local git repos
  • Not yet exercised: the Phase 2 isolation model (concurrent PRs, cancellation, janitor TTL)
    against the real self-hosted runner fleet and live sandbox repos — no runner/credential
    access from this checkout; see "Known gaps" in the docs
  • Gitea leg is currently disabled in ci.yml pending a runner-Docker-topology follow-up
    (tracked in progress.md's Outstanding Items) — harness code is unchanged and passes
    locally every time

🤖 Generated with Claude Code

ClaudiaFang and others added 25 commits August 7, 2026 18:04
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.
…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
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
ClaudiaFang force-pushed the test/real-provider-e2e branch from ec665dc to c42fa35 Compare August 13, 2026 13:53
@ClaudiaFang ClaudiaFang changed the title feat(e2e): real-provider E2E with multi-run isolation (Phases 0-2) ci(e2e): real-provider E2E with multi-run isolation (Phases 0-2) Aug 13, 2026
@sonarqubecloud

Copy link
Copy Markdown

@ClaudiaFang
ClaudiaFang merged commit 7e59727 into main Aug 14, 2026
27 of 38 checks passed
@ClaudiaFang
ClaudiaFang deleted the test/real-provider-e2e branch August 14, 2026 04:32
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))
@ClaudiaFang

Copy link
Copy Markdown
Member Author

🎉 This PR is included in version 1.5.9 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant