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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 140 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,149 @@
branches: [main, master, '**']
pull_request:
types: [opened, synchronize, reopened]
workflow_dispatch:
inputs:
provider:
description: 'Provider(s) to run (github, gitlab, gitea, or all)'
default: 'all'
keep_branch:
description: 'Keep the E2E branch/container after the run for debugging'
type: boolean
default: false
schedule:
# Weekly API-drift check: same real-provider suites, no code change required to trigger them.
- cron: '0 6 * * 1'

jobs:
# `on.push.paths`/`on.pull_request.paths` would gate this *whole* workflow
# file by path -- including the release-critical `CI` job below, which must
# keep running for every push/PR regardless of path. This job instead
# computes a per-job boolean so only `provider-e2e` skips on irrelevant
# changes, while `CI`/`build-artifact` are unaffected.
changes:
name: Detect sync/provider-relevant changes
runs-on: ubuntu-latest
outputs:
e2e-relevant: ${{ steps.filter.outputs.e2e-relevant }}
steps:
- uses: actions/checkout@v6
- uses: dorny/paths-filter@v3

Check failure on line 41 in .github/workflows/ci.yml

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use full commit SHA hash for this dependency.

See more on https://sonarcloud.io/project/issues?id=firstsun-dev_git-files-sync&issues=AZ_4xb65Ul2r1QOc8Xo9&open=AZ_4xb65Ul2r1QOc8Xo9&pullRequest=122
id: filter
with:
filters: |
e2e-relevant:
- 'src/services/**'
- 'src/logic/sync-manager.ts'
- 'src/utils/git-blob-sha.ts'
- 'src/utils/path.ts'
- 'src/utils/symlink.ts'
- 'e2e/**'
- 'package.json'
- 'package-lock.json'
- '.github/workflows/ci.yml'

# Real-provider E2E: one matrix job covering GitHub, GitLab, and Gitea (see
# docs/testing/real-provider-e2e.md).
provider-e2e:
name: E2E / ${{ matrix.provider }}
needs: changes
runs-on: [self-hosted, linux, x64, 32gb-ram]
# Runs when sync/provider-relevant paths changed, or unconditionally on
# workflow_dispatch/schedule/a push to main (main always gets the full
# tier regardless of path, per the issue's CI wiring). The per-provider
# part of the gating (internal PRs/main/dispatch/schedule get every
# provider; a fork PR only gets Gitea) can't live here: job-level `if:`
# has no access to the `matrix` context (GitHub Actions error
# "Unrecognized named-value: 'matrix'" if you try) -- only step-level
# `if:` can see it. That part is done by the "Determine whether this
# provider leg should run" step below instead, gating every later step.
if: >-
needs.changes.outputs.e2e-relevant == 'true' ||
github.event_name == 'workflow_dispatch' ||
github.event_name == 'schedule' ||
github.ref == 'refs/heads/main'
strategy:
fail-fast: false
max-parallel: 3
matrix:
provider: [github, gitlab, gitea]
env:
E2E_GITHUB_OWNER: ${{ vars.E2E_GITHUB_OWNER }}
E2E_GITHUB_REPO: ${{ vars.E2E_GITHUB_REPO }}
E2E_GITHUB_TOKEN: ${{ secrets.E2E_GITHUB_TOKEN }}
# E2E_GITLAB_PROJECT_ID is configured as a repo *secret*, not a
# variable, on firstsun-dev/git-files-sync (confirmed via `gh secret
# list` while wiring this workflow) -- unlike E2E_GITHUB_OWNER/REPO,
# which are plain (non-sensitive) vars.
E2E_GITLAB_PROJECT_ID: ${{ secrets.E2E_GITLAB_PROJECT_ID }}
E2E_GITLAB_TOKEN: ${{ secrets.E2E_GITLAB_TOKEN }}
E2E_KEEP_BRANCH: ${{ github.event.inputs.keep_branch }}
steps:
# Per-provider gate (needs `matrix`, so it runs as a step, not the job-level
# `if:` above -- see the comment on that `if:` for why). A fork PR (head repo
# != base repo) only gets Gitea, which needs no repository secrets and can
# safely run against an untrusted fork's code; GitHub/GitLab need real sandbox
# credentials that must never be exposed to a fork PR's workflow run. All
# other events/providers run.
- name: Determine whether this provider leg should run
id: gate
run: |
run=true
if [ "${{ github.event_name }}" = "pull_request" ] \
&& [ "${{ matrix.provider }}" != "gitea" ] \
&& [ "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.repository }}" ]; then
run=false
fi
if [ "${{ github.event_name }}" = "workflow_dispatch" ] \
&& [ "${{ github.event.inputs.provider }}" != "all" ] \
&& [ "${{ github.event.inputs.provider }}" != "${{ matrix.provider }}" ]; then
run=false
fi
echo "run=$run" >> "$GITHUB_OUTPUT"

- uses: actions/checkout@v6
if: steps.gate.outputs.run == 'true'

- uses: actions/setup-node@v6
if: steps.gate.outputs.run == 'true'
with:
node-version: '22'
cache: npm

- run: npm ci

Check warning on line 124 in .github/workflows/ci.yml

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Omitting "--ignore-scripts" allows lifecycle scripts to run during package installation.

See more on https://sonarcloud.io/project/issues?id=firstsun-dev_git-files-sync&issues=AZ_4xb65Ul2r1QOc8Xo-&open=AZ_4xb65Ul2r1QOc8Xo-&pullRequest=122
if: steps.gate.outputs.run == 'true'

- name: Run provider E2E
if: steps.gate.outputs.run == 'true'
run: node scripts/run-e2e-ci.mjs --provider=${{ matrix.provider }}

# Aggregates the matrix into a single required status so branch protection
# only has to reference one check name (see docs/testing/real-provider-e2e.md
# for the "Gitea required, GitHub/GitLab not required at branch-protection
# level" split -- required-vs-optional per *provider* still comes from the
# "Determine whether this provider leg should run" step above; this gate
# only asks "did whatever ran, pass?"). A gated-off leg's steps are all
# skipped without failing the job, so it still reports "success" here.
# `if: always()` so a real provider-e2e failure/cancellation is caught
# here and blocks CI/release, instead of GitHub Actions silently treating
# an upstream failure as "this job never needed to run".
e2e-gate:
name: E2E gate
needs: provider-e2e
if: always()
runs-on: ubuntu-latest
steps:
- name: Check provider-e2e result
run: |
result="${{ needs.provider-e2e.result }}"
echo "provider-e2e result: $result"
if [ "$result" != "success" ] && [ "$result" != "skipped" ]; then
echo "::error::provider-e2e failed or was cancelled ($result) -- blocking CI/release."
exit 1
fi

CI:
needs: e2e-gate
uses: firstsun-dev/.github/.github/workflows/obsidian-plugin-ci.yml@v1
with:
plugin-id: "git-file-sync"
Expand Down
123 changes: 123 additions & 0 deletions docs/testing/real-provider-e2e.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Real-provider E2E

Issue #57. Real `SyncManager`/`GitHubService`/`GitLabService`/`GiteaService` code run
against real GitHub, GitLab, and Gitea servers, with every remote assertion made through an
independent verifier (raw REST calls, never the service under test reading back its own
write). See `e2e/` for the harness itself:

- `e2e/providers/` — one adapter per provider (`provision()` -> real, already-configured
`GitServiceInterface`; `teardown()`).
- `e2e/provision/` — GitHub/GitLab: validates credentials against a dedicated sandbox
repo/project and creates a run-specific branch. Gitea: provisions a pinned Docker container
from scratch.
- `e2e/verifier/` — one `RemoteVerifier` per provider, raw API calls only.
- `e2e/suites/{github,gitlab,gitea}.e2e.test.ts` — provider contract suites (create/read/
update/delete/batch/rename, plus provider-specific regressions).
- `e2e/suites/sync-manager.e2e.test.ts` — one suite, parametrized by `E2E_PROVIDER`, covering
`SyncManager` itself (push/pull/conflict/rename/delete/batch) against a real provider with an
in-memory fake Vault (`e2e/shim/fake-vault.ts`) standing in for the Obsidian filesystem
boundary — see that file's header comment for why the vault is the only thing faked.

## Running locally

```sh
npm run test:e2e -- --provider gitea # no credentials needed, runs a real Gitea in Docker
npm run test:e2e -- --provider github # needs E2E_GITHUB_* below
npm run test:e2e -- --provider gitlab # needs E2E_GITLAB_* below
```

Each command runs that provider's contract suite *and* the SyncManager suite in one process
(`scripts/run-e2e.mjs`). Export credentials in your shell before running (there is no
`.env`-style file loader in this harness — plain `process.env`, matching `e2e/config/env.ts`):

| Var | Required for | Notes |
|---|---|---|
| `E2E_GITHUB_OWNER` | github | e.g. `firstsun-dev` |
| `E2E_GITHUB_REPO` | github | dedicated sandbox repo — **never** a real user's repo |
| `E2E_GITHUB_TOKEN` | github | fine-grained PAT, scoped to that one repo, Contents: Read and write |
| `E2E_GITHUB_BASE_BRANCH` | github (optional) | defaults to `main` |
| `E2E_GITLAB_PROJECT_ID` | gitlab | dedicated sandbox project |
| `E2E_GITLAB_TOKEN` | gitlab | token with `api` scope on that project — `write_repository` alone is not enough, the verifier and branch setup use REST endpoints outside its coverage |
| `E2E_GITLAB_BASE_URL` | gitlab (optional) | defaults to `https://gitlab.com` |
| `E2E_KEEP_BRANCH` | any (optional) | `1`/`true` skips teardown (branch for GitHub/GitLab, container for Gitea) so you can inspect a failing run |

Gitea needs Docker locally and nothing else — see `e2e/provision/gitea-provision.ts`.

## CI

`.github/workflows/ci.yml` runs a `provider-e2e` matrix job (`github`, `gitlab`, `gitea`) via
`scripts/run-e2e-ci.mjs`, gated on relevant paths (`src/services/**`,
`src/logic/sync-manager.ts`, `e2e/**`, etc. — computed by the `changes` job, since GitHub
Actions' own `on.*.paths` would gate the *entire* workflow file, including the always-must-run
`CI`/release job). It always runs in full on `workflow_dispatch`, `schedule` (weekly, Monday
06:00 UTC, for API-drift detection), and pushes to `main`.

**Secrets/variables** (repo-level, `firstsun-dev/git-files-sync`; confirmed already configured
via `gh secret list` / `gh variable list` while wiring this workflow):

| Name | Kind |
|---|---|
| `E2E_GITHUB_TOKEN` | secret |
| `E2E_GITHUB_OWNER` | variable |
| `E2E_GITHUB_REPO` | variable |
| `E2E_GITLAB_PROJECT_ID` | secret (not a variable — it's treated as sensitive here) |
| `E2E_GITLAB_TOKEN` | secret |

**Fork PRs** only run the Gitea cell (checked in the `Determine whether this provider leg should
run` step — GitHub Actions job-level `if:` can't reference the `matrix` context, so this can't
live on the job itself; it gates every later step instead) — GitHub/GitLab need
real credentials that must never be exposed to an untrusted fork's workflow run. Gitea needs no
repo secrets at all, so it's safe to run unconditionally.

**Missing credentials are always a hard failure**, never a silent skip, for any cell that
actually runs (`scripts/run-e2e-ci.mjs` checks required env vars up front) — the job-level `if:`
above is what decides whether a cell *should* run for a given event; once it runs, it's expected
to have what it needs.

## Release gating

```
changes -> provider-e2e [github | gitlab | gitea, parallel] -> e2e-gate -> CI (shared workflow, includes semantic-release)
```

`e2e-gate` runs with `if: always()` and treats `provider-e2e`'s aggregate result as pass-through
on `success` or `skipped` (the latter covers path-filtered-out runs), and a hard failure on
anything else — so a real provider regression blocks the release instead of shipping and being
caught after the fact.

**Branch protection** (not something this repo checkout can change — a GitHub repo-settings
change, left for whoever has admin access): add `E2E / gitea` as a required status check.
GitHub/GitLab (`E2E / github`, `E2E / gitlab`) are deliberately **not** required at the
branch-protection level, so a fork PR (which only runs Gitea) is never wedged by checks it
structurally cannot produce — internal-PR/main-branch release gating still depends on them
through the `e2e-gate`/`CI` job dependency chain above, just not through branch protection.

## Cleanup / troubleshooting

- **Stale `gfs-e2e-<provider>-*` branch** (GitHub/GitLab only — Gitea's whole container is
destroyed in `afterAll`): `scripts/run-e2e-ci.mjs` runs `scripts/e2e-sweep-branches.mjs`
before every CI run, which best-effort deletes any branch of that pattern older than 24h. Run
it manually (`node scripts/e2e-sweep-branches.mjs --provider github`) if you need it sooner.
- **Inspecting a failing run**: set `E2E_KEEP_BRANCH=1` before running so teardown is skipped,
then look at the branch/container directly. Remember to clean it up yourself afterward, or let
the sweeper (GitHub/GitLab) catch it after 24h.
- **Gitea container port/name clashes**: every Docker resource is namespaced per run
(`e2e/namespace.ts`, `gfs-e2e-gitea-<run-id>-<attempt>` in CI, `gfs-e2e-gitea-local-<random>`
locally), so concurrent runs on the same Docker host don't collide — a leftover container from
an interrupted local run can just be removed manually (`docker rm -f <name>`).
- **`E2E_PROVIDER is not set` error**: the E2E vitest config (`vitest.e2e.config.ts`) refuses to
run directly under `npx vitest` — always go through `npm run test:e2e -- --provider <name>` (or
`scripts/run-e2e-ci.mjs` in CI), which sets it.

## Known gaps

- SyncManager E2E against GitHub/GitLab is written to the same harness as Gitea (no
provider-specific code) but has only been run end-to-end locally against Gitea (Docker,
no external credentials available in that environment) — not yet actually executed against
live GitHub/GitLab sandboxes. Lint/build/typecheck pass for all three.
- The `provider-e2e` matrix job targets `runs-on: [self-hosted, linux, x64, 32gb-ram]` per the
issue's runner-fleet revision; its actual execution on that fleet, and the `e2e-gate` ->
`CI` dependency chain end-to-end in a real workflow run, are unverified from this checkout
(no self-hosted runner access here).
- Branch-protection required-check configuration (`E2E / gitea`) is a manual follow-up for
whoever has admin access to the repo.
14 changes: 14 additions & 0 deletions e2e/provision/docker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,20 @@ export async function removeContainer(name: string): Promise<void> {
await dockerAllowFailure(['rm', '-f', name]);
}

/** Best-effort: the container's own stdout/stderr, for diagnosing a readiness
* timeout (e.g. a slow/failed startup) directly from CI output instead of
* needing shell access to the runner. Never throws. `docker logs` writes the
* container's stdout/stderr to its own stdout/stderr respectively, so both
* are captured and combined, not just stdout. */
export async function containerLogsAllowFailure(name: string, tailLines = 200): Promise<string> {
try {
const { stdout, stderr } = await execFileAsync('docker', ['logs', '--tail', String(tailLines), name]);
return [stdout, stderr].filter(Boolean).join('\n').trim();
} catch (e) {
return `(failed to fetch container logs: ${e instanceof Error ? e.message : String(e)})`;
}
}

/** Reads back the dynamic host port Docker assigned for a `-p 0:<containerPort>` mapping. */
export async function hostPortFor(containerName: string, containerPort: number): Promise<number> {
const output = await docker(['port', containerName, String(containerPort)]);
Expand Down
16 changes: 13 additions & 3 deletions e2e/provision/gitea-provision.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import { runNamespace } from '../namespace';
import { globalSecrets, logInfo } from '../redact';
import { giteaImage, timeouts } from '../config/env';
import { createNetwork, removeNetwork, removeContainer, hostPortFor, waitUntilReady, docker } from './docker';
import { createNetwork, removeNetwork, removeContainer, hostPortFor, waitUntilReady, docker, containerLogsAllowFailure } from './docker';

const execFileAsync = promisify(execFile);

Expand Down Expand Up @@ -86,9 +86,15 @@

return { baseUrl, owner: ADMIN_USERNAME, repo: SANDBOX_REPO, token, containerName, networkName };
} catch (e) {
// Provisioning failed partway through — clean up what we started before rethrowing.
// Provisioning failed partway through. A readiness timeout in particular
// gives no clue *why* Gitea never came up (self-hosted runner Docker/
// network hiccup vs. a real startup failure) without runner shell access
// -- attach the container's own logs to the error before it's torn down,
// so a future CI failure is diagnosable straight from the job output.
const logs = await containerLogsAllowFailure(containerName);
await teardownGitea({ containerName, networkName } as GiteaEnvironment);
throw e;
const message = e instanceof Error ? e.message : String(e);
throw new Error(`${message}\n\n-- gitea container logs (tail) --\n${logs}`);

Check failure on line 97 in e2e/provision/gitea-provision.ts

View workflow job for this annotation

GitHub Actions / E2E / gitea

e2e/suites/sync-manager.e2e.test.ts > SyncManager E2E

Error: Timed out after 60000ms waiting for readiness: fetch failed -- gitea container logs (tail) -- Generating /data/ssh/ssh_host_ed25519_key... Generating /data/ssh/ssh_host_rsa_key... Generating /data/ssh/ssh_host_ecdsa_key... Server listening on :: port 22. Server listening on 0.0.0.0 port 22. 2026/08/07 11:58:59 cmd/web.go:242:runWeb() [I] Starting Gitea on PID: 18 2026/08/07 11:58:59 cmd/web.go:111:showWebStartupMessage() [I] Gitea version: 1.22.6 built with GNU Make 4.4.1, go1.22.10 : bindata, timetzdata, sqlite, sqlite_unlock_notify 2026/08/07 11:58:59 cmd/web.go:112:showWebStartupMessage() [I] * RunMode: prod 2026/08/07 11:58:59 cmd/web.go:113:showWebStartupMessage() [I] * AppPath: /usr/local/bin/gitea 2026/08/07 11:58:59 cmd/web.go:114:showWebStartupMessage() [I] * WorkPath: /data/gitea 2026/08/07 11:58:59 cmd/web.go:115:showWebStartupMessage() [I] * CustomPath: /data/gitea 2026/08/07 11:58:59 cmd/web.go:116:showWebStartupMessage() [I] * ConfigFile: /data/gitea/conf/app.ini 2026/08/07 11:58:59 cmd/web.go:117:showWebStartupMessage() [I] Prepare to run web server 2026/08/07 11:58:59 routers/init.go:116:InitWebInstalled() [I] Git version: 2.45.2 (home: /data/gitea/home) 2026/08/07 11:58:59 ...s/setting/session.go:77:loadSessionFrom() [I] Session Service Enabled 2026/08/07 11:58:59 ...s/storage/storage.go:176:initAttachments() [I] Initialising Attachment storage with type: local 2026/08/07 11:58:59 ...les/storage/local.go:33:NewLocalStorage() [I] Creating new Local Storage at /data/gitea/attachments 2026/08/07 11:58:59 ...s/storage/storage.go:166:initAvatars() [I] Initialising Avatar storage with type: local 2026/08/07 11:58:59 ...les/storage/local.go:33:NewLocalStorage() [I] Creating new Local Storage at /data/gitea/avatars 2026/08/07 11:58:59 ...s/storage/storage.go:192:initRepoAvatars() [I] Initialising Repository Avatar storage with type: local 2026/08/07 11:58:59 ...les/storage/local.go:33:NewLocalStorage() [I] Creating new Local Storage at /data/gitea/repo-avatars 2026/08/07 11:58:59 ...s/storage/storage.go:198:initRepoArchives() [I] Initialising Repository Archive storage with type: local 2026/08/07 11:58:59 ...les/storage/local.go:33:NewLocalStorage() [I] Creating new Local Storage at /data/gitea/repo-archive 2026/08/07 11:58:59 ...s/storage/storage.go:208:initPackages() [I] Initialising Packages storage with type: local 2026/08/07 11:58:59 ...les/storage/local.go:33:NewLocalStorage() [I] Creating new Local Storage at /data/gitea/packages 2026/08/07 11:58:59 ...s/storage/storage.go:219:initActions() [I] Initialising Actions storage with type: local 2026/08/07 11:58:59 ...les/storage/local.go:33:NewLocalStorage() [I] Creating new Local Storage at /data/gitea/actions_log 2026/08/07 11:58:59 ...s/storage/storage.go:223:initActions() [I] Initialising ActionsArtifacts storage with type: local 2026/08/07 11:58:59 ...les/storage/local.go:33:NewLocalStorage() [I] Creating new Local Storage at /data/gitea/actions_artifacts 2026/08/07 11:59:00 routers/init.go:138:InitWebInstalled() [I] SQLite3 support is enabled 2026/08/07 11:59:00 routers/common/db.go:23:InitDBEngine() [I] Beginning ORM engine initialization. 2026/08/07 11:59:00 routers/common/db.go:30:InitDBEngine() [I] ORM engine initialization attempt #1/10... 2026/08/07 11:59:00 cmd/web.go:194:serveInstalled() [I] PING DATABASE sqlite3 2026/08/07 11:59:09 routers/init.go:144:InitWebInstalled() [I] ORM engine initialization successful! 2026/08/07 11:59:11 ...er/issues/indexer.go:76:func1() [I] PID 18: Initializing Issue Indexer: bleve 2026/08/07 11:59:11 ...xer/stats/indexer.go:41:populateRepoIndexer() [I] Populating the repo stats indexer with existing repositories 2026/08/07 11:59:11 routers/init.go:85:syncAppConfForGit() [I] AppPath changed from '' to '/usr/local/bin/gitea' 2026/08/07 11:59:11 routers/init.go:90:syncAppConfForGit() [I] CustomConf changed from '' to '/data/gitea/conf/app.ini' 2026/08/07 11:59:11 routers/init.go:96:syncAppConfForGit() [I] re-sync repository hooks ... 2026/08/07 11:59:11 routers/init.go:99:syncAppConfForGit() [I] re-write ssh

Check failure on line 97 in e2e/provision/gitea-provision.ts

View workflow job for this annotation

GitHub Actions / E2E / gitea

e2e/suites/gitea.e2e.test.ts > GiteaService E2E

Error: Timed out after 60000ms waiting for readiness: fetch failed -- gitea container logs (tail) -- Generating /data/ssh/ssh_host_ed25519_key... Generating /data/ssh/ssh_host_rsa_key... Generating /data/ssh/ssh_host_ecdsa_key... Server listening on :: port 22. Server listening on 0.0.0.0 port 22. 2026/08/07 12:00:01 cmd/web.go:242:runWeb() [I] Starting Gitea on PID: 18 2026/08/07 12:00:01 cmd/web.go:111:showWebStartupMessage() [I] Gitea version: 1.22.6 built with GNU Make 4.4.1, go1.22.10 : bindata, timetzdata, sqlite, sqlite_unlock_notify 2026/08/07 12:00:01 cmd/web.go:112:showWebStartupMessage() [I] * RunMode: prod 2026/08/07 12:00:01 cmd/web.go:113:showWebStartupMessage() [I] * AppPath: /usr/local/bin/gitea 2026/08/07 12:00:01 cmd/web.go:114:showWebStartupMessage() [I] * WorkPath: /data/gitea 2026/08/07 12:00:01 cmd/web.go:115:showWebStartupMessage() [I] * CustomPath: /data/gitea 2026/08/07 12:00:01 cmd/web.go:116:showWebStartupMessage() [I] * ConfigFile: /data/gitea/conf/app.ini 2026/08/07 12:00:01 cmd/web.go:117:showWebStartupMessage() [I] Prepare to run web server 2026/08/07 12:00:01 routers/init.go:116:InitWebInstalled() [I] Git version: 2.45.2 (home: /data/gitea/home) 2026/08/07 12:00:01 ...s/setting/session.go:77:loadSessionFrom() [I] Session Service Enabled 2026/08/07 12:00:01 ...s/storage/storage.go:176:initAttachments() [I] Initialising Attachment storage with type: local 2026/08/07 12:00:01 ...les/storage/local.go:33:NewLocalStorage() [I] Creating new Local Storage at /data/gitea/attachments 2026/08/07 12:00:01 ...s/storage/storage.go:166:initAvatars() [I] Initialising Avatar storage with type: local 2026/08/07 12:00:01 ...les/storage/local.go:33:NewLocalStorage() [I] Creating new Local Storage at /data/gitea/avatars 2026/08/07 12:00:01 ...s/storage/storage.go:192:initRepoAvatars() [I] Initialising Repository Avatar storage with type: local 2026/08/07 12:00:01 ...les/storage/local.go:33:NewLocalStorage() [I] Creating new Local Storage at /data/gitea/repo-avatars 2026/08/07 12:00:01 ...s/storage/storage.go:198:initRepoArchives() [I] Initialising Repository Archive storage with type: local 2026/08/07 12:00:01 ...les/storage/local.go:33:NewLocalStorage() [I] Creating new Local Storage at /data/gitea/repo-archive 2026/08/07 12:00:01 ...s/storage/storage.go:208:initPackages() [I] Initialising Packages storage with type: local 2026/08/07 12:00:01 ...les/storage/local.go:33:NewLocalStorage() [I] Creating new Local Storage at /data/gitea/packages 2026/08/07 12:00:01 ...s/storage/storage.go:219:initActions() [I] Initialising Actions storage with type: local 2026/08/07 12:00:01 ...les/storage/local.go:33:NewLocalStorage() [I] Creating new Local Storage at /data/gitea/actions_log 2026/08/07 12:00:01 ...s/storage/storage.go:223:initActions() [I] Initialising ActionsArtifacts storage with type: local 2026/08/07 12:00:01 ...les/storage/local.go:33:NewLocalStorage() [I] Creating new Local Storage at /data/gitea/actions_artifacts 2026/08/07 12:00:01 routers/init.go:138:InitWebInstalled() [I] SQLite3 support is enabled 2026/08/07 12:00:01 routers/common/db.go:23:InitDBEngine() [I] Beginning ORM engine initialization. 2026/08/07 12:00:01 routers/common/db.go:30:InitDBEngine() [I] ORM engine initialization attempt #1/10... 2026/08/07 12:00:01 cmd/web.go:194:serveInstalled() [I] PING DATABASE sqlite3 2026/08/07 12:00:02 routers/init.go:144:InitWebInstalled() [I] ORM engine initialization successful! 2026/08/07 12:00:03 ...er/issues/indexer.go:76:func1() [I] PID 18: Initializing Issue Indexer: bleve 2026/08/07 12:00:03 ...xer/stats/indexer.go:41:populateRepoIndexer() [I] Populating the repo stats indexer with existing repositories 2026/08/07 12:00:03 routers/init.go:85:syncAppConfForGit() [I] AppPath changed from '' to '/usr/local/bin/gitea' 2026/08/07 12:00:03 routers/init.go:90:syncAppConfForGit() [I] CustomConf changed from '' to '/data/gitea/conf/app.ini' 2026/08/07 12:00:03 routers/init.go:96:syncAppConfForGit() [I] re-sync repository hooks ... 2026/08/07 12:00:03 routers/init.go:99:syncAppConfForGit() [I] re-write ssh

Check failure on line 97 in e2e/provision/gitea-provision.ts

View workflow job for this annotation

GitHub Actions / E2E / gitea

e2e/suites/sync-manager.e2e.test.ts > SyncManager E2E

Error: Timed out after 60000ms waiting for readiness: fetch failed -- gitea container logs (tail) -- Generating /data/ssh/ssh_host_ed25519_key... Generating /data/ssh/ssh_host_rsa_key... Generating /data/ssh/ssh_host_ecdsa_key... Server listening on :: port 22. Server listening on 0.0.0.0 port 22. 2026/08/13 01:39:14 cmd/web.go:242:runWeb() [I] Starting Gitea on PID: 17 2026/08/13 01:39:14 cmd/web.go:111:showWebStartupMessage() [I] Gitea version: 1.22.6 built with GNU Make 4.4.1, go1.22.10 : bindata, timetzdata, sqlite, sqlite_unlock_notify 2026/08/13 01:39:14 cmd/web.go:112:showWebStartupMessage() [I] * RunMode: prod 2026/08/13 01:39:14 cmd/web.go:113:showWebStartupMessage() [I] * AppPath: /usr/local/bin/gitea 2026/08/13 01:39:14 cmd/web.go:114:showWebStartupMessage() [I] * WorkPath: /data/gitea 2026/08/13 01:39:14 cmd/web.go:115:showWebStartupMessage() [I] * CustomPath: /data/gitea 2026/08/13 01:39:14 cmd/web.go:116:showWebStartupMessage() [I] * ConfigFile: /data/gitea/conf/app.ini 2026/08/13 01:39:14 cmd/web.go:117:showWebStartupMessage() [I] Prepare to run web server 2026/08/13 01:39:14 routers/init.go:116:InitWebInstalled() [I] Git version: 2.45.2 (home: /data/gitea/home) 2026/08/13 01:39:14 ...s/setting/session.go:77:loadSessionFrom() [I] Session Service Enabled 2026/08/13 01:39:14 ...s/storage/storage.go:176:initAttachments() [I] Initialising Attachment storage with type: local 2026/08/13 01:39:14 ...les/storage/local.go:33:NewLocalStorage() [I] Creating new Local Storage at /data/gitea/attachments 2026/08/13 01:39:14 ...s/storage/storage.go:166:initAvatars() [I] Initialising Avatar storage with type: local 2026/08/13 01:39:14 ...les/storage/local.go:33:NewLocalStorage() [I] Creating new Local Storage at /data/gitea/avatars 2026/08/13 01:39:14 ...s/storage/storage.go:192:initRepoAvatars() [I] Initialising Repository Avatar storage with type: local 2026/08/13 01:39:14 ...les/storage/local.go:33:NewLocalStorage() [I] Creating new Local Storage at /data/gitea/repo-avatars 2026/08/13 01:39:14 ...s/storage/storage.go:198:initRepoArchives() [I] Initialising Repository Archive storage with type: local 2026/08/13 01:39:14 ...les/storage/local.go:33:NewLocalStorage() [I] Creating new Local Storage at /data/gitea/repo-archive 2026/08/13 01:39:14 ...s/storage/storage.go:208:initPackages() [I] Initialising Packages storage with type: local 2026/08/13 01:39:14 ...les/storage/local.go:33:NewLocalStorage() [I] Creating new Local Storage at /data/gitea/packages 2026/08/13 01:39:14 ...s/storage/storage.go:219:initActions() [I] Initialising Actions storage with type: local 2026/08/13 01:39:14 ...les/storage/local.go:33:NewLocalStorage() [I] Creating new Local Storage at /data/gitea/actions_log 2026/08/13 01:39:14 ...s/storage/storage.go:223:initActions() [I] Initialising ActionsArtifacts storage with type: local 2026/08/13 01:39:14 ...les/storage/local.go:33:NewLocalStorage() [I] Creating new Local Storage at /data/gitea/actions_artifacts 2026/08/13 01:39:14 routers/init.go:138:InitWebInstalled() [I] SQLite3 support is enabled 2026/08/13 01:39:14 routers/common/db.go:23:InitDBEngine() [I] Beginning ORM engine initialization. 2026/08/13 01:39:14 routers/common/db.go:30:InitDBEngine() [I] ORM engine initialization attempt #1/10... 2026/08/13 01:39:14 cmd/web.go:194:serveInstalled() [I] PING DATABASE sqlite3 2026/08/13 01:39:15 routers/init.go:144:InitWebInstalled() [I] ORM engine initialization successful! 2026/08/13 01:39:16 ...er/issues/indexer.go:76:func1() [I] PID 17: Initializing Issue Indexer: bleve 2026/08/13 01:39:16 ...xer/stats/indexer.go:41:populateRepoIndexer() [I] Populating the repo stats indexer with existing repositories 2026/08/13 01:39:16 routers/init.go:85:syncAppConfForGit() [I] AppPath changed from '' to '/usr/local/bin/gitea' 2026/08/13 01:39:16 routers/init.go:90:syncAppConfForGit() [I] CustomConf changed from '' to '/data/gitea/conf/app.ini' 2026/08/13 01:39:16 routers/init.go:96:syncAppConfForGit() [I] re-sync repository hooks ... 2026/08/13 01:39:16 routers/init.go:99:syncAppConfForGit() [I] re-write ssh

Check failure on line 97 in e2e/provision/gitea-provision.ts

View workflow job for this annotation

GitHub Actions / E2E / gitea

e2e/suites/gitea.e2e.test.ts > GiteaService E2E

Error: Timed out after 60000ms waiting for readiness: fetch failed -- gitea container logs (tail) -- Generating /data/ssh/ssh_host_ed25519_key... Generating /data/ssh/ssh_host_rsa_key... 2026/08/13 01:40:14 cmd/web.go:242:runWeb() [I] Starting Gitea on PID: 18 2026/08/13 01:40:14 cmd/web.go:111:showWebStartupMessage() [I] Gitea version: 1.22.6 built with GNU Make 4.4.1, go1.22.10 : bindata, timetzdata, sqlite, sqlite_unlock_notify 2026/08/13 01:40:14 cmd/web.go:112:showWebStartupMessage() [I] * RunMode: prod 2026/08/13 01:40:14 cmd/web.go:113:showWebStartupMessage() [I] * AppPath: /usr/local/bin/gitea 2026/08/13 01:40:14 cmd/web.go:114:showWebStartupMessage() [I] * WorkPath: /data/gitea 2026/08/13 01:40:14 cmd/web.go:115:showWebStartupMessage() [I] * CustomPath: /data/gitea 2026/08/13 01:40:14 cmd/web.go:116:showWebStartupMessage() [I] * ConfigFile: /data/gitea/conf/app.ini 2026/08/13 01:40:14 cmd/web.go:117:showWebStartupMessage() [I] Prepare to run web server 2026/08/13 01:40:14 routers/init.go:116:InitWebInstalled() [I] Git version: 2.45.2 (home: /data/gitea/home) Generating /data/ssh/ssh_host_ecdsa_key... Server listening on :: port 22. Server listening on 0.0.0.0 port 22. 2026/08/13 01:40:14 ...s/setting/session.go:77:loadSessionFrom() [I] Session Service Enabled 2026/08/13 01:40:14 ...s/storage/storage.go:176:initAttachments() [I] Initialising Attachment storage with type: local 2026/08/13 01:40:14 ...les/storage/local.go:33:NewLocalStorage() [I] Creating new Local Storage at /data/gitea/attachments 2026/08/13 01:40:14 ...s/storage/storage.go:166:initAvatars() [I] Initialising Avatar storage with type: local 2026/08/13 01:40:14 ...les/storage/local.go:33:NewLocalStorage() [I] Creating new Local Storage at /data/gitea/avatars 2026/08/13 01:40:14 ...s/storage/storage.go:192:initRepoAvatars() [I] Initialising Repository Avatar storage with type: local 2026/08/13 01:40:14 ...les/storage/local.go:33:NewLocalStorage() [I] Creating new Local Storage at /data/gitea/repo-avatars 2026/08/13 01:40:14 ...s/storage/storage.go:198:initRepoArchives() [I] Initialising Repository Archive storage with type: local 2026/08/13 01:40:14 ...les/storage/local.go:33:NewLocalStorage() [I] Creating new Local Storage at /data/gitea/repo-archive 2026/08/13 01:40:14 ...s/storage/storage.go:208:initPackages() [I] Initialising Packages storage with type: local 2026/08/13 01:40:14 ...les/storage/local.go:33:NewLocalStorage() [I] Creating new Local Storage at /data/gitea/packages 2026/08/13 01:40:14 ...s/storage/storage.go:219:initActions() [I] Initialising Actions storage with type: local 2026/08/13 01:40:14 ...les/storage/local.go:33:NewLocalStorage() [I] Creating new Local Storage at /data/gitea/actions_log 2026/08/13 01:40:14 ...s/storage/storage.go:223:initActions() [I] Initialising ActionsArtifacts storage with type: local 2026/08/13 01:40:14 ...les/storage/local.go:33:NewLocalStorage() [I] Creating new Local Storage at /data/gitea/actions_artifacts 2026/08/13 01:40:14 routers/init.go:138:InitWebInstalled() [I] SQLite3 support is enabled 2026/08/13 01:40:14 routers/common/db.go:23:InitDBEngine() [I] Beginning ORM engine initialization. 2026/08/13 01:40:14 routers/common/db.go:30:InitDBEngine() [I] ORM engine initialization attempt #1/10... 2026/08/13 01:40:14 cmd/web.go:194:serveInstalled() [I] PING DATABASE sqlite3 2026/08/13 01:40:16 routers/init.go:144:InitWebInstalled() [I] ORM engine initialization successful! 2026/08/13 01:40:17 ...er/issues/indexer.go:76:func1() [I] PID 18: Initializing Issue Indexer: bleve 2026/08/13 01:40:17 ...xer/stats/indexer.go:41:populateRepoIndexer() [I] Populating the repo stats indexer with existing repositories 2026/08/13 01:40:17 routers/init.go:85:syncAppConfForGit() [I] AppPath changed from '' to '/usr/local/bin/gitea' 2026/08/13 01:40:17 routers/init.go:90:syncAppConfForGit() [I] CustomConf changed from '' to '/data/gitea/conf/app.ini' 2026/08/13 01:40:17 routers/init.go:96:syncAppConfForGit() [I] re-sync repository hooks ... 2026/08/13 01:40:17 routers/init.go:99:syncAppConfForGit() [I] re-write ssh
}
}

Expand Down Expand Up @@ -128,6 +134,10 @@

/** Best-effort cleanup — safe to call even if provisioning only partially completed. */
export async function teardownGitea(env: Pick<GiteaEnvironment, 'containerName' | 'networkName'>): Promise<void> {
if (process.env.E2E_KEEP_BRANCH === '1' || process.env.E2E_KEEP_BRANCH === 'true') {
logInfo(`E2E_KEEP_BRANCH set — leaving container ${env.containerName} running for debugging`);
return;
}
logInfo(`Removing container ${env.containerName}`);
await removeContainer(env.containerName);
logInfo(`Removing network ${env.networkName}`);
Expand Down
4 changes: 4 additions & 0 deletions e2e/provision/github-provision.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ export async function provisionGitHub(): Promise<GitHubEnvironment> {

/** Best-effort cleanup — safe to call even if provisioning only partially completed. */
export async function teardownGitHub(env: GitHubEnvironment): Promise<void> {
if (process.env.E2E_KEEP_BRANCH === '1' || process.env.E2E_KEEP_BRANCH === 'true') {
logInfo(`E2E_KEEP_BRANCH set — leaving run branch ${env.branch} in place for debugging`);
return;
}
logInfo(`Removing run branch ${env.branch}`);
try {
await fetch(`${API_BASE}/repos/${env.owner}/${env.repo}/git/refs/heads/${encodeURIComponent(env.branch)}`, {
Expand Down
4 changes: 4 additions & 0 deletions e2e/provision/gitlab-provision.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ export async function provisionGitLab(): Promise<GitLabEnvironment> {

/** Best-effort cleanup — deletes the run-specific branch. Must not throw. */
export async function teardownGitLab(env: GitLabEnvironment): Promise<void> {
if (process.env.E2E_KEEP_BRANCH === '1' || process.env.E2E_KEEP_BRANCH === 'true') {
logInfo(`E2E_KEEP_BRANCH set — leaving branch ${env.branch} in place for debugging`);
return;
}
try {
logInfo(`Removing branch ${env.branch}`);
const encodedProjectId = encodeURIComponent(env.projectId);
Expand Down
Loading
Loading