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
14 changes: 14 additions & 0 deletions DEVELOPER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,13 @@ On startup, `cdidx` walks up from the current directory looking for `.cdidx-vers

`cdidx --check-updates` and `cdidx status --check-updates` query the GitHub latest-release endpoint through `UpdateChecker`, using the same 24-hour cache and `CDIDX_DISABLE_UPDATE_CHECK=1` opt-out as the `--version` hint. `cdidx upgrade --check-only` reuses that check. `cdidx upgrade` is intentionally a thin wrapper around the signed release installer: it downloads `install.sh`, verifies the current binary directory is writable, sets `CDIDX_INSTALL_DIR` to that directory, and runs the installer for the latest release.

`cdidx upgrade --json` has a stdout contract suitable for automation. Check-only
and no-update results use the update-check fields
(`current_version`, `latest_version`, `update_available`, `from_cache`,
`error`). When an update is installed, installer stdout/stderr is captured so
stdout remains one JSON document, with `install_attempted`, `install_exit_code`,
and `install_succeeded` added to the update-check fields.

### Degradation reason codes

Readiness degradation reason codes are centralized in `DegradationReasonCodes`. Add new codes there with human text, a recommended action, and an alternative action before emitting them from readers, CLI, or MCP payloads.
Expand Down Expand Up @@ -2393,6 +2400,13 @@ endpoint を確認します。`cdidx upgrade --check-only` はこの check を
現在の binary directory が writable か確認し、`CDIDX_INSTALL_DIR` をその directory に向けて
latest release の installer を実行します。

`cdidx upgrade --json` は automation 向けの stdout contract を持ちます。check-only と
no-update の結果は update-check fields (`current_version`, `latest_version`,
`update_available`, `from_cache`, `error`) を使います。update を install する場合、
installer stdout/stderr は capture されるため stdout は 1 個の JSON document のままになり、
update-check fields に `install_attempted`、`install_exit_code`、`install_succeeded` が
追加されます。

### 劣化理由コード

readiness degradation reason code は `DegradationReasonCodes` に集約します。reader、CLI、
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,13 @@ performed. Set `CDIDX_RELEASE_GPG_FINGERPRINT=<fingerprint>` to pin the
expected release signing key; strict mode requires this fingerprint once GPG
verification succeeds.

`cdidx upgrade --json` keeps stdout machine-readable even when an update is
available. `--check-only --json` and no-update checks keep the update-check
shape (`current_version`, `latest_version`, `update_available`, `from_cache`,
`error`); install attempts suppress installer stdout/stderr and add
`install_attempted`, `install_exit_code`, and `install_succeeded` to the single
JSON document written to stdout.

See [DISTRIBUTION.md](DISTRIBUTION.md) for the full channel matrix and
[isolated network install notes](USER_GUIDE.md#isolated-networks-and-proxies).
For database compatibility across `cdidx` upgrades and downgrades, see
Expand Down Expand Up @@ -440,6 +447,13 @@ installer は `sha256sums.txt.asc` も取得して GnuPG がある場合は
`CDIDX_RELEASE_GPG_FINGERPRINT=<fingerprint>` を設定します。strict mode では、
GPG 検証が成功した後にこの fingerprint の設定も必須です。

`cdidx upgrade --json` は update が見つかった場合も stdout を機械処理向けに保ちます。
`--check-only --json` と no-update の check は update-check shape
(`current_version`, `latest_version`, `update_available`, `from_cache`, `error`)
を維持します。install を試行する場合は installer stdout/stderr を抑制し、stdout に書く
1 個の JSON document に `install_attempted`、`install_exit_code`、
`install_succeeded` を追加します。

### Validate

`cdidx validate [--db <path>] [--json[=array]] [--verbose] [--limit <n>|--top <n>] [--kind <kind>] [--severity <info|warning|error>] [--path <glob>]`
Expand Down
16 changes: 16 additions & 0 deletions changelog.d/unreleased/3011.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
category: fixed
issues:
- 3011
affected:
- src/CodeIndex/Cli/ProgramRunner.cs
- tests/CodeIndex.Tests/ProgramRunnerTests.cs
---

## English

- **Upgrade release downloads now honor caller cancellation (#3011)** — `cdidx upgrade` passes the active cancellation token into release checksum and installer script downloads instead of waiting for download timeouts after cancellation.

## 日本語

- **upgrade のリリース download が caller cancellation を尊重するようになりました (#3011)** — `cdidx upgrade` は release checksum と installer script の download に active cancellation token を渡し、キャンセル後に download timeout まで待ち続けないようになりました。
16 changes: 16 additions & 0 deletions changelog.d/unreleased/3058.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
category: fixed
issues:
- 3058
affected:
- src/CodeIndex/Cli/ProgramRunner.cs
- tests/CodeIndex.Tests/ProgramRunnerTests.cs
---

## English

- **Upgrade installer runs now honor caller cancellation (#3058)** — `cdidx upgrade` observes cancellation while waiting for `install.sh` and terminates the installer process tree instead of blocking until the installer timeout.

## 日本語

- **upgrade の installer 実行が caller cancellation を尊重するようになりました (#3058)** — `cdidx upgrade` は `install.sh` の終了待機中にもキャンセルを監視し、installer timeout までブロックせず installer process tree を終了します。
16 changes: 16 additions & 0 deletions changelog.d/unreleased/3074.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
category: fixed
issues:
- 3074
affected:
- src/CodeIndex/Cli/UpdateChecker.cs
- tests/CodeIndex.Tests/ProgramRunnerTests.cs
---

## English

- **Update-check cache parsing now enforces a JSON depth cap (#3074)** — malformed over-depth local cache files are ignored as cache misses instead of forcing unbounded parser depth work.

## 日本語

- **update-check cache の JSON parse が depth cap を適用するようになりました (#3074)** — 深すぎる malformed local cache file は unbounded な parser depth work を発生させず、cache miss として無視されます。
19 changes: 19 additions & 0 deletions changelog.d/unreleased/3191.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
category: fixed
issues:
- 3191
affected:
- DEVELOPER_GUIDE.md
- README.md
- src/CodeIndex/Cli/JsonOutputContracts.cs
- src/CodeIndex/Cli/ProgramRunner.cs
- tests/CodeIndex.Tests/ProgramRunnerTests.cs
---

## English

- **`cdidx upgrade --json` now keeps stdout as JSON when installing updates (#3191)** — update-available upgrade runs suppress installer console noise and emit a structured install result with the update-check fields.

## 日本語

- **`cdidx upgrade --json` が update install 時も stdout を JSON のまま保つようになりました (#3191)** — update available の upgrade 実行は installer の console 出力を混ぜず、update-check fields を含む構造化された install result を出力します。
11 changes: 11 additions & 0 deletions src/CodeIndex/Cli/JsonOutputContracts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@ internal sealed record CommandErrorJsonResult(
[property: JsonPropertyName("path")] string? Path = null,
[property: JsonPropertyName("category")] string? Category = null);

internal sealed record UpgradeJsonResult(
[property: JsonPropertyName("current_version")] string CurrentVersion,
[property: JsonPropertyName("latest_version")] string? LatestVersion,
[property: JsonPropertyName("update_available")] bool UpdateAvailable,
[property: JsonPropertyName("from_cache")] bool FromCache,
[property: JsonPropertyName("error")] string? Error,
[property: JsonPropertyName("install_attempted")] bool InstallAttempted,
[property: JsonPropertyName("install_exit_code")] int? InstallExitCode,
[property: JsonPropertyName("install_succeeded")] bool? InstallSucceeded);

internal sealed record DbIntegrityCheckJsonResult(
[property: JsonPropertyName("db_path")] string DbPath,
[property: JsonPropertyName("ok")] bool Ok,
Expand Down Expand Up @@ -485,6 +495,7 @@ internal sealed record VersionInfoJsonResult(
[JsonSerializable(typeof(SymbolResult))]
[JsonSerializable(typeof(UnusedSymbolResult))]
[JsonSerializable(typeof(CodeIndex.Models.UpdateCheckResult))]
[JsonSerializable(typeof(UpgradeJsonResult))]
[JsonSerializable(typeof(VacuumResult))]
[JsonSerializable(typeof(VersionInfoJsonResult))]
[JsonSerializable(typeof(WorkspaceListJsonResult))]
Expand Down
149 changes: 133 additions & 16 deletions src/CodeIndex/Cli/ProgramRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using CodeIndex.Indexer.Hooks;
using CodeIndex.Lsp;
using CodeIndex.Mcp;
using CodeIndex.Models;
using Microsoft.Data.Sqlite;

namespace CodeIndex.Cli;
Expand All @@ -38,6 +39,7 @@ internal static class ProgramRunner
private static readonly HashSet<string> TopLevelValueOptionNames =
CliFlagSchema.GetTopLevelValueOptionNames();
internal static TimeProvider TimeProvider { get; set; } = TimeProvider.System;
internal static Func<HttpClient> UpgradeHttpClientFactory { get; set; } = CreateUpgradeHttpClient;

private sealed record CommandRunContext(
JsonSerializerOptions JsonOptions,
Expand Down Expand Up @@ -3043,29 +3045,47 @@ internal static int RunUpgrade(

if (!OperatingSystem.IsLinux() && !OperatingSystem.IsMacOS())
{
Console.Error.WriteLine("Error: cdidx upgrade currently requires a POSIX shell installer on Linux or macOS.");
Console.Error.WriteLine("Hint: download the latest release asset manually, or rerun install.sh from a shell environment.");
if (wantsJson)
{
Console.WriteLine(JsonSerializer.Serialize(
CreateUpgradeJsonResult(result, installAttempted: false, installExitCode: null, "unsupported_platform"),
jsonOptions));
}
else
{
Console.Error.WriteLine("Error: cdidx upgrade currently requires a POSIX shell installer on Linux or macOS.");
Console.Error.WriteLine("Hint: download the latest release asset manually, or rerun install.sh from a shell environment.");
}
return CommandExitCodes.FeatureUnavailable;
}

var installDir = AppContext.BaseDirectory.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
if (!CanWriteDirectory(installDir))
{
Console.Error.WriteLine($"Error: install directory is not writable: {installDir}");
Console.Error.WriteLine("Hint: rerun with permissions that can write this directory, or reinstall cdidx into a per-user directory.");
if (wantsJson)
{
Console.WriteLine(JsonSerializer.Serialize(
CreateUpgradeJsonResult(result, installAttempted: false, installExitCode: null, "install_directory_not_writable"),
jsonOptions));
}
else
{
Console.Error.WriteLine($"Error: install directory is not writable: {installDir}");
Console.Error.WriteLine("Hint: rerun with permissions that can write this directory, or reinstall cdidx into a per-user directory.");
}
return CommandExitCodes.UsageError;
}

var scriptPath = Path.Combine(Path.GetTempPath(), $"cdidx-install-{Guid.NewGuid():N}.sh");
try
{
using (var client = new HttpClient { Timeout = TimeSpan.FromSeconds(20) })
using (var client = UpgradeHttpClientFactory())
{
var checksumManifest = DownloadReleaseChecksumManifestAsync(
client,
result.LatestVersion,
TimeSpan.FromSeconds(20),
CancellationToken.None)
cancellationToken)
.GetAwaiter()
.GetResult();
var expectedInstallerSha256 = GetReleaseAssetChecksum(checksumManifest, InstallerScriptAssetName);
Expand All @@ -3075,19 +3095,46 @@ internal static int RunUpgrade(
result.LatestVersion,
scriptPath,
TimeSpan.FromSeconds(20),
CancellationToken.None)
cancellationToken)
.GetAwaiter()
.GetResult();
VerifyFileSha256(scriptPath, expectedInstallerSha256, InstallerScriptAssetName);
}

var startInfo = CreateInstallerProcessStartInfo(scriptPath, result.LatestVersion, installDir);
return RunInstallerProcess(startInfo, InstallerRunTimeout);
var installExitCode = RunInstallerProcess(
startInfo,
InstallerRunTimeout,
cancellationToken,
suppressOutput: wantsJson);
if (wantsJson)
{
var error = installExitCode == CommandExitCodes.Success
? null
: $"installer_exit_code_{installExitCode.ToString(CultureInfo.InvariantCulture)}";
Console.WriteLine(JsonSerializer.Serialize(
CreateUpgradeJsonResult(result, installAttempted: true, installExitCode, error),
jsonOptions));
}
return installExitCode;
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch (Exception ex)
{
Console.Error.WriteLine($"Error: upgrade failed before install.sh completed ({ex.GetType().Name}: {ex.Message}).");
Console.Error.WriteLine("Hint: rerun `install.sh` manually for the desired release.");
if (wantsJson)
{
Console.WriteLine(JsonSerializer.Serialize(
CreateUpgradeJsonResult(result, installAttempted: false, installExitCode: null, ex.GetType().Name),
jsonOptions));
}
else
{
Console.Error.WriteLine($"Error: upgrade failed before install.sh completed ({ex.GetType().Name}: {ex.Message}).");
Console.Error.WriteLine("Hint: rerun `install.sh` manually for the desired release.");
}
return CommandExitCodes.DatabaseError;
}
finally
Expand All @@ -3109,27 +3156,94 @@ internal static ProcessStartInfo CreateInstallerProcessStartInfo(string scriptPa
return startInfo;
}

internal static int RunInstallerProcess(ProcessStartInfo startInfo, TimeSpan timeout)
internal static int RunInstallerProcess(
ProcessStartInfo startInfo,
TimeSpan timeout,
CancellationToken cancellationToken = default,
bool suppressOutput = false)
{
if (suppressOutput)
{
startInfo.RedirectStandardOutput = true;
startInfo.RedirectStandardError = true;
}

using var process = Process.Start(startInfo);
if (process == null)
{
Console.Error.WriteLine("Error: failed to start install.sh for upgrade.");
if (!suppressOutput)
Console.Error.WriteLine("Error: failed to start install.sh for upgrade.");
return CommandExitCodes.DatabaseError;
}

if (process.WaitForExit(ToWaitMilliseconds(timeout)))
var outputDrainTask = suppressOutput
? Task.WhenAll(process.StandardOutput.ReadToEndAsync(), process.StandardError.ReadToEndAsync())
: Task.CompletedTask;

try
{
var waitTask = process.WaitForExitAsync(cancellationToken);
var timeoutTask = Task.Delay(ToWaitMilliseconds(timeout));
if (Task.WhenAny(waitTask, timeoutTask).GetAwaiter().GetResult() == waitTask)
{
waitTask.GetAwaiter().GetResult();
outputDrainTask.GetAwaiter().GetResult();
return process.ExitCode;
}
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
TryKillProcessTree(process);
if (!process.WaitForExit(ToWaitMilliseconds(InstallerKillWaitTimeout)))
{
if (!suppressOutput)
Console.Error.WriteLine("Error: install.sh was cancelled and did not exit after cancellation.");
}
else
{
outputDrainTask.GetAwaiter().GetResult();
}
throw;
}

if (process.HasExited)
{
outputDrainTask.GetAwaiter().GetResult();
return process.ExitCode;
}

TryKillProcessTree(process);
if (!process.WaitForExit(ToWaitMilliseconds(InstallerKillWaitTimeout)))
Console.Error.WriteLine("Error: install.sh timed out and did not exit after cancellation.");
{
if (!suppressOutput)
Console.Error.WriteLine("Error: install.sh timed out and did not exit after cancellation.");
}
else
Console.Error.WriteLine($"Error: install.sh timed out after {FormatDuration(timeout)}.");
Console.Error.WriteLine("Hint: rerun `install.sh` manually for the desired release.");
{
outputDrainTask.GetAwaiter().GetResult();
if (!suppressOutput)
Console.Error.WriteLine($"Error: install.sh timed out after {FormatDuration(timeout)}.");
}
if (!suppressOutput)
Console.Error.WriteLine("Hint: rerun `install.sh` manually for the desired release.");
return CommandExitCodes.DatabaseError;
}

private static UpgradeJsonResult CreateUpgradeJsonResult(
UpdateCheckResult result,
bool installAttempted,
int? installExitCode,
string? error)
=> new(
result.CurrentVersion,
result.LatestVersion,
result.UpdateAvailable,
result.FromCache,
error ?? result.Error,
installAttempted,
installExitCode,
installExitCode is null ? null : installExitCode == CommandExitCodes.Success);

internal static string BuildInstallerScriptUrl(string releaseTag)
=> BuildReleaseAssetUrl(releaseTag, InstallerScriptAssetName);

Expand All @@ -3140,6 +3254,9 @@ internal static string BuildReleaseAssetUrl(string releaseTag, string assetName)
Uri.EscapeDataString(releaseTag.Trim()),
Uri.EscapeDataString(assetName));

private static HttpClient CreateUpgradeHttpClient()
=> new() { Timeout = TimeSpan.FromSeconds(20) };

internal static async Task<string> DownloadReleaseChecksumManifestAsync(
HttpClient client,
string releaseTag,
Expand Down
Loading
Loading