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