Skip to content

Add GitLab export diagnostics command - #1607

Draft
pmartindev wants to merge 3 commits into
github:mainfrom
pmartindev:gitlab-export-diagnostics
Draft

Add GitLab export diagnostics command#1607
pmartindev wants to merge 3 commits into
github:mainfrom
pmartindev:gitlab-export-diagnostics

Conversation

@pmartindev

@pmartindev pmartindev commented Sep 2, 2026

Copy link
Copy Markdown
  • Did you write/update appropriate tests
  • Release notes updated (if appropriate)
  • Appropriate logging output
  • Issue linked
  • Docs updated (or issue created)
  • New package licenses are added to ThirdPartyNotices.txt (if applicable)

Summary

Adds a gl2gh diagnose-gitlab-export command that collects API-visible GitLab project export diagnostics and writes a Markdown report. Optional administrator SSH access collects retained export jobs, child-job errors and project-matched server logs from Linux-package GitLab installations, directly or inside Docker.

This is intended for failures where GitLab reports export_status: failed before a GitHub migration ID is created, so download-logs cannot retrieve GitHub-side migration logs yet.

  • API-only mode remains available; SSH host, user and private-key path are opt-in, with an optional port and Docker container name.
  • Uses native OpenSSH with strict host-key verification, non-interactive authentication, no agent forwarding and bounded collection.
  • Retrieves actual export/child job IDs and errors rather than import state. Explains user-scoped API export status and removes the misleading project-ID-as-export-ID label.
  • Writes owner-only reports on Linux/macOS. SSH failures exit nonzero and preserve the API report; missing logs and collection limits are explicit warnings.
  • Read-only collection does not start or retry exports or change GitLab settings. Reports and verbose logs must be reviewed/redacted before sharing.

Validation

  • dotnet format src/OctoshiftCLI.sln
  • dotnet test src/OctoshiftCLI.Tests/OctoshiftCLI.Tests.csproj --filter 'FullyQualifiedName~DiagnoseGitlabExport|FullyQualifiedName~GitlabApiTests|FullyQualifiedName~GitlabSshDiagnosticsCollector' — 35 tests passed.
  • CLI help and embedded Ruby syntax checked.
  • Isolated GitLab CE 18.3.1: genuine export permission failure, successful sibling export, real administrator-key SSH collection, failed child-job details and correlated errors, owner-only report creation/overwrite, unknown-host and bad-key rejection, API-only preservation on failure.
  • Docker command routing was exercised through an explicit SSH test wrapper; the exact collector invocation was separately verified against the real Docker engine. A single SSH-to-real-Docker-daemon end-to-end chain and non-root sudo were not exercised.
  • Temporary test containers, volumes, network and private keys were removed.

Notes

  • Release notes: updated in RELEASENOTES.md.
  • Issue linked: no public tracking issue was available for this implementation.
  • Docs: README includes API-only/SSH usage, optional Docker execution, administrator permissions, host trust, sensitive-data handling and collection limits.
  • Third-party notices: not applicable, no new dependencies.
  • The test build reports an existing SSH.NET NU1903 advisory in bbs2gh; this change does not add or update that dependency.

Example output: administrator SSH diagnostics

Implementation status: The SSH enhancement and documentation are included in commit d2a2b474 on this draft PR. The example below was captured from the implemented feature in an isolated local test.

This is an excerpt from an actual isolated GitLab CE 18.3.1 run using synthetic projects, not customer data. Making one project's temporary export directory unwritable produced a real export failure. The API exposed only export_status: failed; administrator SSH collection retrieved the failed export/child jobs, concrete error and backtrace.

Invocation

The GitLab PAT is supplied through GITLAB_PAT. The SSH key authenticates an OS administrator, not GitLab's Git-over-SSH endpoint.

gh gl2gh diagnose-gitlab-export \
  --gitlab-server-url http://gei-ssh-a7a17-0910-gitlab \
  --gitlab-group ssh-fixture-lab \
  --gitlab-project permission-denied-export \
  --ssh-host gei-ssh-a7a17-0910-gitlab \
  --ssh-user root --ssh-key /fixture/secrets/id_ed25519 \
  --output /fixture/work/feature-native-ssh.md

For GitLab inside Docker on the SSH host, add --gitlab-container NAME. Omit it when SSH already reaches the GitLab installation. OpenSSH host-key verification remains enabled.

Terminal output

Selected actual lines; startup configuration and verbose HTTP responses are omitted:

[2026-09-10 18:40:30] [INFO] Collecting GitLab export diagnostics...
[2026-09-10 18:40:30] [WARNING] Collecting administrator diagnostics over SSH. The report may contain sensitive project data, paths and error messages; review and redact it before sharing.
[2026-09-10 18:40:51] [WARNING] Server-side diagnostics have collection warnings. See the warnings in the report for missing logs and collection limits.
[2026-09-10 18:40:51] [WARNING] GitLab reported the project export as failed before GitHub received an archive. Ask a GitLab administrator to inspect the project export job on the GitLab instance using the commands in the report.
[2026-09-10 18:40:51] [INFO] Wrote GitLab export diagnostics to /fixture/work/feature-native-ssh.md.

Generated Markdown report

Summary excerpt:

# GitLab export diagnostics

## Summary

- GitLab version: 18.3.1 (Community Edition)
- Project path: ssh-fixture-lab/permission-denied-export
- Project ID: 1
- Export status: failed
- API export status is scoped to the authenticated GitLab user. Use the same user that initiated the export; another user may see `none`.

## Server-side diagnostics (SSH)

- SSH host: gei-ssh-a7a17-0910-gitlab:22
- SSH user: root
- GitLab container: (direct installation)

The server-side section contains JSON like the following. This is a selected subset of the actual output: one failed relation/log entry is retained, the backtrace is shortened, and long temporary paths are replaced with <export-directory> for readability. Status 3 is the failed state on the tested GitLab version.

{
  "collected_at": "2026-09-10T18:40:51Z",
  "gitlab_version": "18.3.1",
  "project_id": 1,
  "project_path": "ssh-fixture-lab/permission-denied-export",
  "export_jobs": [
    {
      "id": 1,
      "jid": "f75e42d9bb968c5af2b60261",
      "status": 3,
      "user_id": 1,
      "created_at": "2026-09-10 18:30:38 UTC",
      "updated_at": "2026-09-10 18:31:51 UTC",
      "relations": [
        {
          "relation": "wiki_repository",
          "jid": "ea4c6460583e02bb57a7e836",
          "status": 3,
          "export_error": "Permission denied @ dir_s_mkdir - <export-directory>"
        }
      ]
    }
  ],
  "logs": [
    {
      "path": "/var/log/gitlab/gitlab-rails/exporter.log",
      "matches": [
        {
          "time": "2026-09-10T18:31:31.597Z",
          "severity": "ERROR",
          "message": "Project relation export failed",
          "relation": "wiki_repository",
          "project_export_job_id": "1",
          "export_error": "Permission denied @ dir_s_mkdir - <export-directory>",
          "exception.class": "Errno::EACCES",
          "exception.backtrace": [
            "/opt/gitlab/embedded/lib/ruby/3.2.0/fileutils.rb:403:in `mkdir'",
            "/opt/gitlab/embedded/lib/ruby/3.2.0/fileutils.rb:403:in `fu_mkdir'",
            "/opt/gitlab/embedded/lib/ruby/3.2.0/fileutils.rb:384:in `block (2 levels) in mkdir_p'"
          ]
        }
      ]
    }
  ]
}

The full report also retains project statistics, raw export API data, administrator follow-up commands, collection scope and explicit warnings. This run reported a missing exceptions_json.log and bounded/truncated Sidekiq results rather than silently implying complete collection.

Interpretation: unlike the API-only failed status, these records identify a filesystem permission error and the affected export relation. This establishes the cause of the synthetic failure, not the cause of any customer incident. Reports can contain sensitive data and must be reviewed/redacted before sharing.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings September 2, 2026 19:15
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown

Unit Test Results

    1 files      1 suites   25s ⏱️
1 207 tests 1 207 ✅ 0 💤 0 ❌
1 208 runs  1 208 ✅ 0 💤 0 ❌

Results for commit 2e0fca3.

♻️ This comment has been updated with latest results.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

API coverage and export-job diagnostics must be corrected before approval; release notes are also missing.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review tier: Balanced
Findings: 3 Medium severity · 1 Low severity

New issues introduced by this change (4)
Severity Finding
Medium severity src/​Octoshift/​Services/​GitlabApi.cs — The handler tests mock both new API methods, so they do not verify either endpoint URL,…
Medium severity src/​gl2gh/​Commands/​DiagnoseGitlabExport/​DiagnoseGitlabExportCommandHandler.cs — This value is the project ID, not an export-job ID: GitLab's export-status entity inherits id
Medium severity src/​gl2gh/​Commands/​DiagnoseGitlabExport/​DiagnoseGitlabExportCommandHandler.csimport_state describes an import into GitLab, so this command can return an unrelated import JID…
Low severity src/​gl2gh/​Commands/​DiagnoseGitlabExport/​DiagnoseGitlabExportCommand.cs — This introduces a customer-facing CLI command, but RELEASENOTES.md is unchanged. The repository…
What changed in this PR

Adds gl2gh diagnose-gitlab-export to generate Markdown diagnostics for failed GitLab exports.

Changes:

  • Adds command options, validation, reporting, and overwrite handling.
  • Adds GitLab project/export API retrieval.
  • Adds command and handler unit tests.
File Summary and review
src/​OctoshiftCLI.Tests/​gl2gh/​Commands/​DiagnoseGitlabExport/​DiagnoseGitlabExportCommandTests.cs Tests command options and API construction.
src/​OctoshiftCLI.Tests/​gl2gh/​Commands/​DiagnoseGitlabExport/​DiagnoseGitlabExportCommandHandlerTests.cs Tests report creation and overwrite protection.
src/​OctoshiftCLI.Tests/​gl2gh/​Commands/​DiagnoseGitlabExport/​DiagnoseGitlabExportCommandArgsTests.cs Tests required argument validation.
src/​Octoshift/​Services/​GitlabApi.cs Adds project/export retrieval. Moderate: Add focused API tests for URLs, path encoding, and response mapping.
src/​gl2gh/​Commands/​DiagnoseGitlabExport/​DiagnoseGitlabExportCommandHandler.cs Builds the report. Moderate: “Export ID” is actually the project ID; the administrator query also uses unrelated import_state instead of the export job.
src/​gl2gh/​Commands/​DiagnoseGitlabExport/​DiagnoseGitlabExportCommandArgs.cs Defines and validates command arguments.
src/​gl2gh/​Commands/​DiagnoseGitlabExport/​DiagnoseGitlabExportCommand.cs Defines the CLI command. Nit: Add the required RELEASENOTES.md entry.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

);
}

public virtual async Task<GitlabExportDetails> GetExportDetails(string groupPath, string projectPath)
Comment on lines +77 to +78
builder.AppendLine($"- Export ID: {ValueOrUnknown(exportDetails.Id)}");
builder.AppendLine();
builder.AppendLine("Run these commands on the GitLab instance to retrieve the server-side export job error. GitLab does not expose these logs through the project export API.");
builder.AppendLine();
builder.AppendLine("```bash");
builder.AppendLine($"sudo gitlab-rails runner \"p = Project.find_by_full_path({quotedProjectPath}); puts p.import_state.slice(:jid, :status, :last_error)\"");
Comment on lines +13 to +14
name: "diagnose-gitlab-export",
description: "Collects GitLab project export diagnostics and writes a report with GitLab admin log commands.")
@pmartindev
pmartindev marked this pull request as draft September 2, 2026 19:22

@brianaj brianaj left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

took a peak since this popped up on my radar cutting a new release today, leaving early feedback before you get too far

TL;DR

I'd lean away from a new diagnose-gitlab-export command. I think the real value is a clearer error at the existing failure point that links to GitLab's own import/export docs — most of the benefit, without baking GitLab's server internals (and their upkeep) into our CLI. Separately, it's worth following up with GitLab to improve export-failure visibility at the source.

Context worth keeping in mind: this repo is maintained by a small team without dedicated CLI-support resources, so I'd weight the ongoing maintenance cost of anything we add fairly heavily.

Concerns with the command as proposed

  • Maintenance burden. It hardcodes GitLab server internals (/var/log/gitlab/..., gitlab-ctl, gitlab-rails runner, a version-specific Ruby snippet) — things we'd have to keep correct as GitLab changes.
  • Wrong for many installs. Those paths assume an omnibus install; they don't fit Kubernetes/Helm, Docker, source, or GitLab.com SaaS (no shell access), yet the command emits them regardless of install type.
  • Some guidance is already off (flagged by Copilot review): import_state is the import model, not export (can return an unrelated JID or nil; exports use Project#export_jobs), and "Export ID" is really the project ID.
  • Would fail CI as-is: an "unnecessary using directive" warning breaks the build under TreatWarningsAsErrors=true.
  • Test gap: no GitlabApiTests for the new GetExportDetails / GetProjectDetails.
  • Missing RELEASENOTES.md entry. Nice touches worth keeping: [Secret] on the PAT, ShellQuote escaping, --output/--overwrite conventions.

Do we need a new command?

Probably not. A user shouldn't have to find and run a second command to learn why the first one failed — this reads more like an error-messaging gap on the existing migrate path than a missing tool.

For GitHub migrations: GetMigration returns a failureReason surfaced in the gei/ado2gh/bbs2gh and WaitForMigration handlers — but only after a GitHub migration ID exists. For the GitLab export failure, there's nothing to log: GitlabApi.GetExport returns only (ExportStatus, DownloadUrl), and GitLab's export API (/projects/:id/export) doesn't expose a reason — it just reports export_status: failed. That gap is exactly why this PR reached for server-side logs, and it's the strongest argument for raising this with GitLab so the reason is available via the API rather than us inferring it.

Proposed change (no new command)

Location: src/gl2gh/Commands/MigrateRepo/MigrateRepoCommandHandler.cs (in GenerateArchive, the ExportState.IsError starement).

Before

if (ExportState.IsError(exportState))
{
    throw new OctoshiftCliException($"GitLab archive export failed!");
}

After

if (ExportState.IsError(exportState))
{
    throw new OctoshiftCliException(
        $"GitLab reported the project export for {args.GitlabGroup}/{args.GitlabProject} as '{exportState}'. " +
        "GitLab's export API does not provide a failure reason, and no GitHub migration was created, so there are no migration logs to download. " +
        "Ask a GitLab administrator to inspect the project export job on your GitLab instance. " +
        "See GitLab's project import/export documentation for details: https://docs.gitlab.com/ee/user/project/settings/import_export");
}

Ideally we should link to GitLab's official docs, not our own (please confirm the exact URL/anchor)

Suggested follow-up with GitLab

The root issue is that GitLab's project-export API doesn't surface a failure reason. Worth having customers raise with GitLab (issue/support) so export failures expose a machine-readable reason we can relay directly — which would make this whole class of problem self-explanatory without either a bespoke command or a docs hunt.

Happy to be discussed over slack as well. Maybe we can discuss with product for unofficially supported commands which fall into the grey area.

pmartindev and others added 2 commits September 10, 2026 13:52
Collect bounded export-job and server-log evidence over verified OpenSSH, with optional Docker execution and API-only fallback reports. Update documentation, release notes, and focused tests.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Keep previously released notes in their archived file and retain the new GitLab diagnostics note in RELEASENOTES.md.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
public void Rejects_Missing_Key_File()
{
var args = Args();
args.SshKey = Path.Combine(_key, "missing");
@github-actions

Copy link
Copy Markdown

Code Coverage

Package Line Rate Branch Rate Complexity Health
gl2gh 77% 68% 556
gei 81% 74% 688
Octoshift 80% 70% 2070
bbs2gh 83% 78% 668
ado2gh 71% 70% 749
Summary 79% (9272 / 11763) 72% (2393 / 3342) 4731

@pmartindev

Copy link
Copy Markdown
Author

Thanks for the follow up @brianaj ! I've been getting a few requests lately from users who are seeing failures pre-migration id generation, from the GitLab export. My goal is to try to reduce the barrier for users to discover these errors. I'm fine with with keeping the diagnostics and log fetching outside of GEI, and creating a separate gh cli extension instead. I do think there would be value in having it built into GEI to reduce the friction in discovering these errors.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants