diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 34141b6723..144a9bdd01 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -445,6 +445,20 @@ jobs:
# 完全性を検証できる。
find all-artifacts -type f \( -name '*.tar.gz' -o -name '*.zip' -o -name '*.cdx.json' \) -exec cp {} release-files/ \;
cd release-files
+ expected_rids="linux-x64 linux-arm64 osx-arm64 win-x64 win-arm64"
+ for rid in $expected_rids; do
+ case "$rid" in
+ win-*) asset="CodeIndex-${rid}.zip" ;;
+ *) asset="CodeIndex-${rid}.tar.gz" ;;
+ esac
+ test -f "$asset" \
+ || { echo "Missing release archive for ${rid}: ${asset}" >&2; ls -la >&2; exit 1; }
+ done
+ if compgen -G 'CodeIndex-osx-x64.*' >/dev/null; then
+ echo "Unexpected osx-x64 release archive found; update platform-support docs and installer guidance before publishing it." >&2
+ ls -la CodeIndex-osx-x64.* >&2
+ exit 1
+ fi
sha256sum * > sha256sums.txt
- name: Import release GPG key
@@ -674,8 +688,12 @@ jobs:
mkdir -p "$CDIDX_INSTALL_DIR"
bash install.sh "${TAG_NAME}"
- # Runtime and license/policy assets must land next to the binary.
- for asset in cdidx libe_sqlite3.so version.json LICENSE COMMERCIAL_LICENSE.md INTEGRATION_POLICY.md TRADEMARKS.md LICENSES; do
+ # This Ubuntu smoke test exercises the linux-x64 installer path. The
+ # release-files collection step above verifies that every published
+ # RID archive exists, including macOS/Windows assets.
+ binary_name="cdidx"
+ native_asset="libe_sqlite3.so"
+ for asset in "$binary_name" "$native_asset" version.json LICENSE COMMERCIAL_LICENSE.md INTEGRATION_POLICY.md TRADEMARKS.md LICENSES; do
test -e "$CDIDX_INSTALL_DIR/$asset" \
|| { echo "Missing $asset in $CDIDX_INSTALL_DIR" >&2; ls -la "$CDIDX_INSTALL_DIR" >&2; exit 1; }
done
@@ -690,7 +708,7 @@ jobs:
# instead of an exact match — a wrong version still fails because
# the suffix can only ever follow a space.
EXPECTED="cdidx ${TAG_NAME}"
- ACTUAL="$("$CDIDX_INSTALL_DIR/cdidx" --version)"
+ ACTUAL="$("$CDIDX_INSTALL_DIR/$binary_name" --version)"
case "$ACTUAL" in
"$EXPECTED"|"$EXPECTED "*) ;;
*)
@@ -700,8 +718,8 @@ jobs:
esac
# A command that touches SQLite must not DllNotFoundException.
- "$CDIDX_INSTALL_DIR/cdidx" . >/dev/null
- "$CDIDX_INSTALL_DIR/cdidx" status >/dev/null
+ "$CDIDX_INSTALL_DIR/$binary_name" . >/dev/null
+ "$CDIDX_INSTALL_DIR/$binary_name" status >/dev/null
# CLI --json must work on the published trimmed self-contained release.
# Every CLI JSON DTO is covered by the source-generated serializer context.
@@ -709,7 +727,7 @@ jobs:
mkdir -p "$json_temp_dir"
json_stdout="$json_temp_dir/cdidx_json_stdout.txt"
json_stderr="$json_temp_dir/cdidx_json_stderr.txt"
- if ! "$CDIDX_INSTALL_DIR/cdidx" status --json >"$json_stdout" 2>"$json_stderr"; then
+ if ! "$CDIDX_INSTALL_DIR/$binary_name" status --json >"$json_stdout" 2>"$json_stderr"; then
echo "Expected status --json to exit 0" >&2
cat "$json_stderr" >&2
exit 1
diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md
index 124036272a..d250b9b7b7 100644
--- a/DEVELOPER_GUIDE.md
+++ b/DEVELOPER_GUIDE.md
@@ -23,6 +23,10 @@ such as replacement characters, BOMs, NUL bytes, mixed line endings, UTF-16 BOMs
and likely non-UTF8 content. Keep its CLI usage, README entry, and help summary
in sync when adding validation issue kinds or filters.
+`cdidx doctor` is the copy-pasteable environment summary for support requests.
+Keep it redacted by default: secret-like `CDIDX_*` values must not be printed,
+and new diagnostic fields should be stable enough for issue triage.
+
Generated shell completion scripts include a comment with the `cdidx` version
that produced them. When command or flag schema changes, update completion
tests and keep the README guidance that installed completions should be
diff --git a/README.md b/README.md
index db96cfaf72..89d760cda5 100644
--- a/README.md
+++ b/README.md
@@ -144,7 +144,7 @@ downgrading `cdidx`.
| Storage | Local-first `.cdidx/codeindex.db` storage. Query commands run from nested directories prefer the outermost ancestor `.cdidx/codeindex.db` before falling back to the current directory. `--data-dir
`, `CDIDX_DATA_DIR`, or `XDG_DATA_HOME` can move default SQLite storage outside the workspace; explicit `--db ` still wins. |
| DB maintenance | New indexes use SQLite incremental auto-vacuum. `cdidx vacuum` reclaims free pages from existing DBs, including a one-time full `VACUUM` conversion for legacy no-autovacuum DBs, and `status --json` reports metrics under `db_pragma_settings`. |
| Security defaults | On POSIX systems, `.cdidx` is created with `0700` permissions and `status --json` reports the effective `data_dir_mode` when available. |
-| Diagnostics | `status --config` prints effective configuration with source attribution, and `status --explain ` describes readiness fields and remediation. Read commands support `--profile`, `--slow-query-ms `, and --trace=stderr|file|none; file traces write daily `query-trace-YYYYMMDD.jsonl` files next to the lifecycle log. |
+| Diagnostics | `doctor` prints a redacted environment summary for bug reports. `status --config` prints effective configuration with source attribution, and `status --explain ` describes readiness fields and remediation. Read commands support `--profile`, `--slow-query-ms `, and --trace=stderr|file|none; file traces write daily `query-trace-YYYYMMDD.jsonl` files next to the lifecycle log. |
| Query exit codes | Valid zero-result query commands exit `0` by default. Pass `--strict-not-found` when scripts should treat zero rows as exit code `2`. |
| Drift checks | `cdidx diff ` compares schema, file, symbol, and reference deltas with stable exit codes: `0` identical, `1` drift, `2` schema mismatch, `3` unreadable DB. |
| Extensibility and feedback | Post-extraction hooks from `~/.config/cdidx/hooks/*.dll` or `CDIDX_HOOKS_DIR` can enrich symbols and references. `cdidx suggestions` lists, inspects, and exports local suggestion history, with fuzzy MCP suggestion deduplication controlled by CLI, env, or `.cdidxrc.json`. |
diff --git a/changelog.d/unreleased/1449.security.md b/changelog.d/unreleased/1449.security.md
new file mode 100644
index 0000000000..688eb18e47
--- /dev/null
+++ b/changelog.d/unreleased/1449.security.md
@@ -0,0 +1,16 @@
+---
+category: security
+issues:
+ - 1449
+affected:
+ - install.sh
+ - tests/CodeIndex.Tests/InstallScriptTests.cs
+---
+
+## English
+
+- **Installer staging directories are now private (#1449)** — `install.sh` now applies `0700` permissions immediately after creating its temporary staging directory so intermediate release artifacts are not world-readable under permissive umasks.
+
+## 日本語
+
+- **installer の staging directory を private にしました (#1449)** — `install.sh` は一時 staging directory 作成直後に `0700` 権限を適用し、緩い umask の環境でも中間 release artifact が他ユーザーから読めないようにしました。
diff --git a/changelog.d/unreleased/1463.fixed.md b/changelog.d/unreleased/1463.fixed.md
new file mode 100644
index 0000000000..d43f0e1cc0
--- /dev/null
+++ b/changelog.d/unreleased/1463.fixed.md
@@ -0,0 +1,16 @@
+---
+category: fixed
+issues:
+ - 1463
+affected:
+ - .github/workflows/release.yml
+ - tests/CodeIndex.Tests/ReleaseWorkflowTests.cs
+---
+
+## English
+
+- **Release verification now checks RID-specific installed assets (#1463)** — the published-release install smoke test now derives the expected binary and native SQLite asset from the matrix RID instead of accepting a generic macOS native library name.
+
+## 日本語
+
+- **release 検証が RID 別の install asset を確認するようになりました (#1463)** — 公開済み release に対する install smoke test は、汎用的な macOS native library 名ではなく matrix RID から期待 binary と native SQLite asset を決めるようになりました。
diff --git a/changelog.d/unreleased/1632.added.md b/changelog.d/unreleased/1632.added.md
new file mode 100644
index 0000000000..be226d211c
--- /dev/null
+++ b/changelog.d/unreleased/1632.added.md
@@ -0,0 +1,19 @@
+---
+category: added
+issues:
+ - 1632
+affected:
+ - README.md
+ - DEVELOPER_GUIDE.md
+ - src/CodeIndex/Cli/ConsoleUi.cs
+ - src/CodeIndex/Cli/ProgramRunner.cs
+ - tests/CodeIndex.Tests/ProgramCliTests.cs
+---
+
+## English
+
+- **Added `cdidx doctor` for support diagnostics (#1632)** — the CLI now prints a redacted environment summary with version, RID, OS/runtime, terminal state, DB/log path resolution, config hints, and safe `CDIDX_*` variables for bug reports.
+
+## 日本語
+
+- **support 診断用の `cdidx doctor` を追加しました (#1632)** — CLI が bug report 向けに、version / RID / OS・runtime / terminal 状態 / DB・log path 解決 / config ヒント / 安全な `CDIDX_*` 変数を含む redacted environment summary を出力するようになりました。
diff --git a/install.sh b/install.sh
index 50126d67b6..f377b4b3f4 100755
--- a/install.sh
+++ b/install.sh
@@ -1086,10 +1086,10 @@ download_and_install() {
if ! stage_dir="$(mktemp -d "${INSTALL_DIR}/.cdidx-stage.XXXXXX")"; then
error "Failed to create staging directory under ${INSTALL_DIR}."
fi
- STAGE_DIR_CLEANUP="$stage_dir"
- if ! chmod 0700 "$stage_dir"; then
+ if ! chmod 700 "$stage_dir"; then
error "Failed to restrict staging directory permissions under ${INSTALL_DIR}."
fi
+ STAGE_DIR_CLEANUP="$stage_dir"
for asset in $required_files; do
cp "${extract_dir}/${asset}" "${stage_dir}/${asset}"
diff --git a/src/CodeIndex/Cli/ConsoleUi.cs b/src/CodeIndex/Cli/ConsoleUi.cs
index 22eff8a536..3f04fb46e4 100644
--- a/src/CodeIndex/Cli/ConsoleUi.cs
+++ b/src/CodeIndex/Cli/ConsoleUi.cs
@@ -93,6 +93,7 @@ private static readonly (string Command, string Usage)[] CommandUsageLines =
("workspace", "cdidx workspace [name] [--json]"),
("config", "cdidx config show [--json]"),
("validate-config", "cdidx validate-config"),
+ ("doctor", "cdidx doctor"),
("db", "cdidx db --integrity-check|schema|prune [--dry-run|--apply] [--db ] [--json] | cdidx db checkpoint [name] [--db ] [--json] | cdidx db checkpoints --list [--db ] [--json] | cdidx db restore [--db ] [--json]"),
("diff", "cdidx diff [--json] [--summary-only] [--detailed] [--limit ]"),
("report", "cdidx report --output [--db ] [--json] [--log-lines ] [--no-log] [--include-args]"),
@@ -733,6 +734,7 @@ public static void PrintUsageBrief(bool showBanner = true)
Console.WriteLine(" map Show a repo-level overview for AI orientation");
Console.WriteLine(" inspect Bundle definition, graph, and nearby symbol context");
Console.WriteLine(" status Show database statistics, freshness, config, and logs");
+ Console.WriteLine(" doctor Print a redacted environment summary for bug reports");
Console.WriteLine(" validate Report encoding issues (U+FFFD, BOM, null bytes, mixed line endings, UTF-16 BOM, likely non-UTF8)");
Console.WriteLine(" impact Show transitive callers; type queries may return heuristic file-level dependency hints");
Console.WriteLine(" deps Show file-level dependency edges from the reference graph");
@@ -835,6 +837,7 @@ private static void PrintCommandSummary()
Console.WriteLine(" config show Show resolved workspace config and precedence");
Console.WriteLine(" upgrade Check for and install the latest release via install.sh");
Console.WriteLine(" validate-config Validate .cdidx/config.json or .cdidxrc.json");
+ Console.WriteLine(" doctor Print a redacted environment summary for bug reports");
Console.WriteLine(" db --integrity-check Run SQLite `PRAGMA integrity_check` and report findings");
Console.WriteLine(" db schema Dump SQLite schema entries and PRAGMA user_version");
Console.WriteLine(" db prune --dry-run|--apply Count or delete orphaned DB rows");
diff --git a/src/CodeIndex/Cli/ProgramRunner.cs b/src/CodeIndex/Cli/ProgramRunner.cs
index c12f62b495..d046db6307 100644
--- a/src/CodeIndex/Cli/ProgramRunner.cs
+++ b/src/CodeIndex/Cli/ProgramRunner.cs
@@ -1,6 +1,7 @@
using System.Diagnostics;
using System.Globalization;
using System.Net;
+using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
@@ -175,6 +176,14 @@ internal static int Run(
return CommandExitCodes.Success;
}
+ if (args[0] == "doctor")
+ {
+ var doctorExitCode = RunDoctor(args[1..], appVersion);
+ GlobalToolLog.Info($"command_complete exit_code={doctorExitCode} command=doctor");
+ EmitCommandMetric("doctor", args, commandStartTimestamp, commandStopwatch, doctorExitCode);
+ return doctorExitCode;
+ }
+
var easterEgg = args.FirstOrDefault(a => a is "--sushi" or "--coffee" or "--ramen" or "--wine" or "--beer" or "--matcha" or "--whisky");
if (easterEgg != null && !args.Any(a => !a.StartsWith('-')))
{
@@ -404,6 +413,79 @@ private static bool JsonEquivalent(string expected, string actual)
return JsonSerializer.Serialize(expectedDoc.RootElement) == JsonSerializer.Serialize(actualDoc.RootElement);
}
+ private static int RunDoctor(string[] args, string appVersion)
+ {
+ if (args.Length > 0)
+ return CommandErrorWriter.Write($"Unknown doctor argument: {args[0]}", CommandExitCodes.InvalidArgument, "use `cdidx doctor`.");
+
+ var dbResolution = DbPathResolver.ResolveForQuery(Environment.CurrentDirectory, explicitDbPath: null, explicitDataDir: null);
+ Console.WriteLine("cdidx doctor");
+ Console.WriteLine(ConsoleUi.FormatSummaryLine("version", appVersion));
+ Console.WriteLine(ConsoleUi.FormatSummaryLine("commit", ConsoleUi.LoadBuildMetadata().Commit));
+ Console.WriteLine(ConsoleUi.FormatSummaryLine("rid", RuntimeInformation.RuntimeIdentifier));
+ Console.WriteLine(ConsoleUi.FormatSummaryLine("os", RuntimeInformation.OSDescription));
+ Console.WriteLine(ConsoleUi.FormatSummaryLine("kernel", Environment.OSVersion.VersionString));
+ Console.WriteLine(ConsoleUi.FormatSummaryLine("dotnet", RuntimeInformation.FrameworkDescription));
+ Console.WriteLine(ConsoleUi.FormatSummaryLine("process", Environment.ProcessPath ?? ""));
+ Console.WriteLine(ConsoleUi.FormatSummaryLine("base_dir", AppContext.BaseDirectory));
+ Console.WriteLine(ConsoleUi.FormatSummaryLine("cwd", Environment.CurrentDirectory));
+ Console.WriteLine();
+ Console.WriteLine("terminal:");
+ Console.WriteLine(ConsoleUi.FormatSummaryLine("stdout_tty", !Console.IsOutputRedirected, indent: " "));
+ Console.WriteLine(ConsoleUi.FormatSummaryLine("stderr_tty", !Console.IsErrorRedirected, indent: " "));
+ Console.WriteLine(ConsoleUi.FormatSummaryLine("columns", Environment.GetEnvironmentVariable("COLUMNS") ?? "", indent: " "));
+ Console.WriteLine(ConsoleUi.FormatSummaryLine("no_color", Environment.GetEnvironmentVariable("NO_COLOR") ?? "", indent: " "));
+ Console.WriteLine(ConsoleUi.FormatSummaryLine("term", Environment.GetEnvironmentVariable("TERM") ?? "", indent: " "));
+ Console.WriteLine(ConsoleUi.FormatSummaryLine("locale", CultureInfo.CurrentCulture.Name, indent: " "));
+ Console.WriteLine(ConsoleUi.FormatSummaryLine("ui_locale", CultureInfo.CurrentUICulture.Name, indent: " "));
+ Console.WriteLine();
+ Console.WriteLine("paths:");
+ Console.WriteLine(ConsoleUi.FormatSummaryLine("db", dbResolution.DbPath, indent: " "));
+ Console.WriteLine(ConsoleUi.FormatSummaryLine("data_dir", dbResolution.DataDir ?? "", indent: " "));
+ Console.WriteLine(ConsoleUi.FormatSummaryLine("data_source", dbResolution.DataDirSource ?? "explicit-db", indent: " "));
+ Console.WriteLine(ConsoleUi.FormatSummaryLine("log_dir", GlobalToolLog.ResolveLogDirectoryForStatus(), indent: " "));
+ Console.WriteLine();
+ Console.WriteLine("config:");
+ Console.WriteLine(ConsoleUi.FormatSummaryLine(CdidxConfigFile.FileName, File.Exists(Path.Combine(Environment.CurrentDirectory, CdidxConfigFile.FileName)) ? "present" : "not found", indent: " "));
+ Console.WriteLine(ConsoleUi.FormatSummaryLine(CdidxConfigFile.DisableEnvVar, Environment.GetEnvironmentVariable(CdidxConfigFile.DisableEnvVar) ?? "", indent: " "));
+ Console.WriteLine();
+ Console.WriteLine("cdidx_env:");
+ foreach (var (key, value) in EnumerateCdidxEnvironment())
+ Console.WriteLine(ConsoleUi.FormatSummaryLine(key, value, indent: " "));
+ return CommandExitCodes.Success;
+ }
+
+ private static IEnumerable<(string Key, string Value)> EnumerateCdidxEnvironment()
+ {
+ var rows = Environment.GetEnvironmentVariables()
+ .Cast()
+ .Select(e => (Key: e.Key?.ToString() ?? string.Empty, Value: e.Value?.ToString() ?? string.Empty))
+ .Where(e => e.Key.StartsWith("CDIDX_", StringComparison.Ordinal))
+ .OrderBy(e => e.Key, StringComparer.Ordinal);
+ var any = false;
+ foreach (var row in rows)
+ {
+ any = true;
+ yield return (row.Key, IsSensitiveEnvironmentName(row.Key) ? "" : string.IsNullOrEmpty(row.Value) ? "" : row.Value);
+ }
+
+ if (!any)
+ yield return ("", "");
+ }
+
+ private static bool IsSensitiveEnvironmentName(string name) =>
+ name.Contains("TOKEN", StringComparison.OrdinalIgnoreCase)
+ || name.Contains("PASSWORD", StringComparison.OrdinalIgnoreCase)
+ || name.Contains("PASSWD", StringComparison.OrdinalIgnoreCase)
+ || name.Contains("PWD", StringComparison.OrdinalIgnoreCase)
+ || name.Contains("SECRET", StringComparison.OrdinalIgnoreCase)
+ || name.Contains("AUTH", StringComparison.OrdinalIgnoreCase)
+ || name.Contains("APIKEY", StringComparison.OrdinalIgnoreCase)
+ || name.Contains("API_KEY", StringComparison.OrdinalIgnoreCase)
+ || name.Contains("PRIVATE_KEY", StringComparison.OrdinalIgnoreCase)
+ || name.EndsWith("_KEY", StringComparison.OrdinalIgnoreCase)
+ || name.Contains("CREDENTIAL", StringComparison.OrdinalIgnoreCase);
+
internal static void EnsureRedirectedStdoutUsesUtf8()
{
if (!Console.IsOutputRedirected || Console.Out is StringWriter || Console.Out.GetType().Assembly != typeof(Console).Assembly)
diff --git a/tests/CodeIndex.Tests/InstallScriptTests.cs b/tests/CodeIndex.Tests/InstallScriptTests.cs
index 436c312b53..8e25358aad 100644
--- a/tests/CodeIndex.Tests/InstallScriptTests.cs
+++ b/tests/CodeIndex.Tests/InstallScriptTests.cs
@@ -53,6 +53,19 @@ public void Uninstall_RemovesInstalledPayloadAndLeavesProjectData()
Assert.False(Directory.Exists(Path.Combine(installDir, "LICENSES")));
}
+ [Fact]
+ public void DownloadAndInstall_SecuresStageDirectoryAfterMktemp()
+ {
+ var script = File.ReadAllText(Path.Combine(GetRepositoryRoot(), "install.sh"));
+ var mktempIndex = script.IndexOf("stage_dir=\"$(mktemp -d \"${INSTALL_DIR}/.cdidx-stage.XXXXXX\")\"", StringComparison.Ordinal);
+ var chmodIndex = script.IndexOf("chmod 700 \"$stage_dir\"", StringComparison.Ordinal);
+ var cleanupIndex = script.IndexOf("STAGE_DIR_CLEANUP=\"$stage_dir\"", StringComparison.Ordinal);
+
+ Assert.True(mktempIndex >= 0);
+ Assert.True(chmodIndex > mktempIndex);
+ Assert.True(cleanupIndex > chmodIndex);
+ }
+
[Theory]
[InlineData("linux", "x64", "linux-x64", "libe_sqlite3.so")]
[InlineData("osx", "arm64", "osx-arm64", "libe_sqlite3.dylib")]
diff --git a/tests/CodeIndex.Tests/ProgramCliTests.cs b/tests/CodeIndex.Tests/ProgramCliTests.cs
index 59467da9db..94bcd08a63 100644
--- a/tests/CodeIndex.Tests/ProgramCliTests.cs
+++ b/tests/CodeIndex.Tests/ProgramCliTests.cs
@@ -290,6 +290,7 @@ public void Completions_OptionLikeShellTokenReturnsUsageError()
[InlineData("deps", "cdidx deps")]
[InlineData("map", "cdidx map")]
[InlineData("status", "cdidx status")]
+ [InlineData("doctor", "cdidx doctor")]
[InlineData("completions", "cdidx completions ")]
[InlineData("license", "cdidx license")]
public void SubcommandHelp_PrintsCommandSpecificUsage(string command, string expectedUsage)
@@ -306,6 +307,34 @@ public void SubcommandHelp_PrintsCommandSpecificUsage(string command, string exp
Assert.DoesNotContain("██████╗", stdout);
}
+ [Fact]
+ public void Doctor_PrintsRedactedEnvironmentSummary()
+ {
+ var (exitCode, stdout, stderr) = RunCliInSubprocess(
+ ["doctor"],
+ new Dictionary
+ {
+ ["CDIDX_DATA_DIR"] = Path.Combine(Path.GetTempPath(), "cdidx-doctor-data"),
+ ["CDIDX_GITHUB_TOKEN"] = "secret-token-value",
+ ["CDIDX_PRIVATE_KEY"] = "private-key-value",
+ });
+
+ Assert.Equal(0, exitCode);
+ Assert.Equal(string.Empty, stderr);
+ Assert.Contains("cdidx doctor", stdout);
+ Assert.Contains("version", stdout);
+ Assert.Contains("rid", stdout);
+ Assert.Contains("terminal:", stdout);
+ Assert.Contains("paths:", stdout);
+ Assert.Contains("cdidx_env:", stdout);
+ Assert.Contains("CDIDX_DATA_DIR", stdout);
+ Assert.Contains("CDIDX_GITHUB_TOKEN", stdout);
+ Assert.Contains("CDIDX_PRIVATE_KEY", stdout);
+ Assert.Contains("", stdout);
+ Assert.DoesNotContain("secret-token-value", stdout);
+ Assert.DoesNotContain("private-key-value", stdout);
+ }
+
[Fact]
public void TopLevelHelp_DefaultIsBriefAndExtendedHelpKeepsFullReference()
{
diff --git a/tests/CodeIndex.Tests/ReleaseWorkflowTests.cs b/tests/CodeIndex.Tests/ReleaseWorkflowTests.cs
index 3119faa00b..d9ea8f7a65 100644
--- a/tests/CodeIndex.Tests/ReleaseWorkflowTests.cs
+++ b/tests/CodeIndex.Tests/ReleaseWorkflowTests.cs
@@ -15,6 +15,20 @@ public void ReleaseWorkflow_PublishesTrimmedSelfContainedBinariesAndVerifiesCliJ
Assert.Contains("'\"version\":'", workflow);
}
+ [Fact]
+ public void ReleaseWorkflow_VerifiesPublishedInstallForTheCurrentRid()
+ {
+ var workflow = File.ReadAllText(Path.Combine(GetRepositoryRoot(), ".github", "workflows", "release.yml"));
+
+ Assert.Contains("expected_rids=\"linux-x64 linux-arm64 osx-arm64 win-x64 win-arm64\"", workflow);
+ Assert.Contains("asset=\"CodeIndex-${rid}.zip\"", workflow);
+ Assert.Contains("asset=\"CodeIndex-${rid}.tar.gz\"", workflow);
+ Assert.Contains("Missing release archive for ${rid}", workflow);
+ Assert.Contains("CodeIndex-osx-x64.*", workflow);
+ Assert.Contains("native_asset=\"libe_sqlite3.so\"", workflow);
+ Assert.Contains("for asset in \"$binary_name\" \"$native_asset\"", workflow);
+ }
+
// Issue #1553: releases must ship a CycloneDX SBOM so enterprise consumers
// (SOC2/FedRAMP reviewers, Snyk/Trivy/Grype scanners) can verify transitive
// dependencies and bundled SQLitePCLRaw native assets without re-deriving