feat: add EfcptOfflineMode for air-gapped/secure CI (#185)#197
Merged
Conversation
…l checks Extracts RunEfcpt's SDK/dnx capability checks behind an ISdkProbe interface (plus a new IsGlobalToolInstalled probe for later offline pre-flight use). DefaultSdkProbe delegates to the existing #187 memoized probes on RunEfcpt (backed by SdkProbeCache) so caching and behavior are unchanged. Threads the probe through the tool resolution/restore Strategy contexts so predicates call ctx.Probe.* instead of the static methods directly - a pure testability seam with no behavior change; existing RunEfcptTests/DotNetToolUtilitiesTests stay green. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…rk-dependent tool branches
Adds EfcptOfflineMode (default false) to both mirrored property groups in
BuildTransitivePropsFactory.cs, and the OfflineMode task param on RunEfcpt in
BuildTransitiveTargetsFactory.cs. RunEfcpt gains an OfflineMode property
(OR-ed with the EFCPT_OFFLINE env var) and threads a resolved `Offline` flag
through the tool resolution/restore Strategy contexts, prepending
`!ctx.Offline &&` to the three network-dependent branches (dnx execution,
tool-manifest restore, global tool update) so none of them spawn a process
when offline. Emits a one-time high-importance status message when offline.
KNOWN GAP: buildTransitive/JD.Efcpt.Build.{props,targets} are NOT regenerated
in this commit. Regeneration is currently broken on main independent of this
change: dependabot PR #177 bumped JD.MSBuild.Fluent 1.3.20 -> 1.3.22 as a
"semver-patch" update, but TargetsBuilder.RegisterTasks's parameter names
changed (`assemblyPath` no longer valid), which cascades into ~44 compile
errors in JD.Efcpt.Build.Definitions (a project that is not part of the .sln
and is never built by CI - see GENERATING.md's note that CI always disables
generation). Reproduced identically on a clean checkout of origin/main via
`dotnet msbuild -t:JDMSBuildFluentGenerate` per GENERATING.md. Per explicit
instruction not to hand-edit the generated XML, this is left as a follow-up:
fix the JD.MSBuild.Fluent version mismatch (or pin back to 1.3.20), then
regenerate and commit the buildTransitive files including this property.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…arget, add docs RunEfcpt now validates, when offline, that the efcpt tool is guaranteed runnable without a network call: an explicit existing EfcptToolPath, a discovered (already-restored) tool manifest, or a global tool resolvable on PATH via the new ISdkProbe.IsGlobalToolInstalled. If none apply, the task logs actionable error JD0026 (mirroring ProviderDriverNotFoundException's shape - problem, exact dotnet tool install commands, detected state, docs URL) via log.Error and returns false rather than throwing, per TaskExecutionDecorator's contract. Adds EfcptToolNotAvailableOfflineException as the JD0026 message builder. Guards the _EfcptCheckForUpdates target with `and '$(EfcptOfflineMode)' != 'true'` so the SDK update-check network call is also skipped offline (same buildTransitive regeneration gap noted in the prior commit - source-level change only, pending the JD.MSBuild.Fluent version fix). Adds docs/user-guide/offline.md (full offline/air-gapped workflow) and a JD0026 entry in docs/user-guide/error-codes.md. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…pre-flight Adds TinyBDD Given/When/Then scenarios, mirroring RunEfcptTests/ DotNetToolUtilitiesTests conventions ([Collection(nameof(AssemblySetup))]): - offline-skips-network: a ThrowingSdkProbe that fails the test if any probe method is invoked; OfflineMode=true, TargetFramework=net10.0, a pre-provisioned explicit ToolPath (a real, always-succeeding .cmd script, not EFCPT_FAKE_EFCPT - which short-circuits before tool resolution runs at all) -> task succeeds, no probe/restore/acquire path is touched. - offline-fails-actionably: OfflineMode=true, no manifest, no ToolPath, TargetFramework=net8.0, an AllUnavailableSdkProbe -> task returns false and Engine.Errors contains code JD0026 with a "dotnet tool install" command. - offline-disabled-by-default: OfflineMode left unset -> defaults to "false" and fake-mode execution is unaffected. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… and regenerate with EfcptOfflineMode (#185) Dependabot PR #177 bumped JD.MSBuild.Fluent 1.3.20 -> 1.3.22, which changed several fluent-DSL APIs (ResolveMultiTargetedTaskAssembly, RegisterTasks, BeforeTargets/AfterTargets, OutputProperty<T1,T2>). This broke JD.Efcpt.Build.Definitions with ~44 compile errors, invisible to CI because that project is not in JD.Efcpt.Build.sln and CI runs with generation disabled. As a result, EfcptOfflineMode (added on this branch) never made it into the generated buildTransitive/JD.Efcpt.Build.{props,targets}. Adapted Definitions to the 1.3.22 API rather than pinning back to 1.3.20, since the bump also included other patch/security updates in the same Dependabot group: - SharedPropertyGroups/UsingTasksRegistry: updated ResolveMultiTargetedTaskAssembly to the new (tasksFolderProperty, taskAssemblyProperty, assemblyFileName, packageId, frameworkMappings) signature, and RegisterTasks' renamed assemblyPathProperty parameter. - BuildTransitiveTargetsFactory: multi-target BeforeTargets("A", "B") calls switched to the single semicolon-joined string form; OutputProperty<T1,T2>() calls (which required task-parameter marker types that were never defined, plus some property markers only visible from a different nested class) switched to the plain-string OutputProperty(taskParameter, propertyName) overload using the exact strings already present in the last known-good generated targets file. Separately, JD.Efcpt.Build.csproj never actually wired in the Definitions sources: JD.MSBuild.Fluent's generator reflects over the project's own compiled JD.Efcpt.Build.dll for JDMSBuildFluentDefinitionType, and a ProjectReference to JD.Efcpt.Build.Definitions.csproj is not enough - the type must be physically compiled into that assembly. Added a Compile Include so JD.Efcpt.Build.Definitions's sources compile directly into JD.Efcpt.Build.dll (IncludeBuildOutput=false, so this does not affect the packed NuGet output). Regenerated buildTransitive/JD.Efcpt.Build.{props,targets} (and build/): EfcptOfflineMode now appears in the props defaults and is wired into the RunEfcpt task's OfflineMode parameter. Diffed the regenerated files against the previous versions: aside from EfcptOfflineMode, all other properties/ targets are preserved; the only other differences are the newer generator's XML formatting and two target names (EfcptEnsureDacpac, EfcptQuerySchemaMetadata) that were already renamed in source but never regenerated. Follow-up recommended (not filed): add JD.Efcpt.Build.Definitions to the solution, or add a CI step that builds it / diffs regenerated output against committed buildTransitive files, so this class of breakage can't silently reoccur. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
EfcptOfflineMode's default now honors a truthy EFCPT_OFFLINE environment
variable (MSBuild exposes env vars as properties automatically), so the
documented "either is sufficient" contract holds for consumers of the
property directly - notably the _EfcptCheckForUpdates target, which reads
$(EfcptOfflineMode) rather than going through RunEfcpt's task-level OR.
Truthy tokens intentionally mirror StringExtensions.IsTrue()
(true/yes/on/1/enable/enabled/y); MSBuild condition equality is
case-insensitive so no extra case variants are needed.
EFCPT_OFFLINE=false/0/unset does not enable offline mode. Regenerated
buildTransitive/JD.Efcpt.Build.{props,targets} from the factory via
JDMSBuildFluentGenerate. Updated docs/user-guide/offline.md to describe
the two independent OR points (MSBuild property default vs. task-level
env-var read) and the exact accepted values.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
) The offline JD0026 pre-flight's manifest leg previously treated ANY discovered .config/dotnet-tools.json as proof the efcpt tool was runnable. A stale, foreign, or never-restored manifest (or a ToolMode that wouldn't even use it) would pass pre-flight and only fail later with a cryptic `dotnet tool run` error instead of the actionable JD0026. The manifest leg now only counts as runnable when (a) the current ToolMode would actually use the manifest - extracted the existing ToolIsAutoOrManifest condition into a reusable ToolModeUsesManifest(mode, manifestDir, forceManifestOnNonWindows) helper, callable before a ToolResolutionContext exists - and (b) the manifest file itself lists an entry for the target tool (matched by ToolPackageId or by a "commands" entry matching ToolCommand), via a new ManifestListsTool() helper that does a tolerant local JSON read (malformed/unreadable manifests are treated as "not runnable", never thrown). Both checks are local file reads only, so they remain offline-safe. The explicit-ToolPath and global-tool-on-PATH legs are unchanged. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The existing 3 OfflineModeTests scenarios all bypass the !ctx.Offline STRATEGY gates in RunEfcpt (explicit ToolPath short-circuits before them; EFCPT_FAKE_EFCPT returns before tool resolution runs at all). Adds 4 scenarios that exercise those gates directly, using capturing fake dotnet/global-tool .cmd scripts (append invocation to a log file, exit 0) plus counting/throwing ISdkProbe substitutes to assert what did and didn't run: - Offline + a manifest that lists the efcpt tool resolves via 'dotnet tool run', with dnx/global-tool probes and tool restore/update never invoked (TFM net10.0, no explicit ToolPath). - Offline + no manifest + a global tool on PATH succeeds by invoking the tool directly, without ever touching dotnet (proving tool update --global never runs). - EFCPT_OFFLINE set with EfcptOfflineMode left at its default reproduces the JD0026 pre-flight failure via the task-level env-var OR-in alone; the MSBuild-property-level bridge is verified separately via the generated buildTransitive/*.props XML (a full MSBuild target harness is out of scope for this task-level project). - Offline=false gate-regression: with a manifest present, TFM net8.0, and a counting probe, tool restore genuinely runs - proving the !ctx.Offline prefix is a real gate, not inverted or always-on. 7 OfflineModeTests scenarios total, all passing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ptQuerySchemaMetadataForDb) to avoid breaking samples/consumers (#185) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… CI) (#185) The offline-mode tests created fake dotnet/global-tool shims as Windows .cmd batch scripts. On the Linux CI runner a .cmd file is not executable, so the shim never ran, its invocation log was never written, and the assertions (task succeeds / no error / invocation recorded) failed with FileNotFoundException. This is a test-portability bug, not a defect in the offline-mode logic. Introduce TestScripts.CreateCaptureScript / CreateAlwaysSucceedsScript which branch on OperatingSystem.IsWindows(): Windows still gets a .cmd batch file (wrapped via cmd.exe /c by CommandNormalizationStrategy); Linux/macOS get a #!/bin/sh script with the executable bit set (File.SetUnixFileMode, via the existing TestFileSystem.MakeExecutable helper), launched directly by Process.Start. Test-only change; no product code touched. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… Linux auto-mode manifest fallback (#185) RunEfcpt.ExecuteCore forces `auto` ToolMode into tool-manifest resolution on non-Windows whenever ToolPath is empty (forceManifestOnNonWindows), regardless of whether a manifest actually exists - a deliberate guard against fragile bare-PATH assumptions on CI. The offline global-tool-on-PATH test used ToolMode="auto" with no manifest and no ToolPath, so on Linux it silently resolved via `dotnet tool run` (through the fake dotnet shim) instead of invoking the fake global-tool executable directly, leaving global-tool-invocations.log never written. Using ToolMode="global" (any non-auto/non-tool-manifest value, per the ToolMode doc comment) bypasses that platform-dependent branch entirely and exercises the intended Default/global-tool resolution branch identically on Windows and Linux. No product code changes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Contributor
Code Coverage |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #197 +/- ##
==========================================
+ Coverage 82.28% 90.41% +8.13%
==========================================
Files 72 74 +2
Lines 3985 4048 +63
Branches 584 600 +16
==========================================
+ Hits 3279 3660 +381
+ Misses 456 388 -68
+ Partials 250 0 -250
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
EfcptOfflineModesoRunEfcptcan be used in air-gapped / secure CI environments that must never make an outbound network call.ISdkProbeseam (src/JD.Efcpt.Build.Tasks/Utilities/ISdkProbe.cs): extracts the SDK/dnx/global-tool capability checks used byRunEfcpt's tool resolution/restore strategies behind an interface.DefaultSdkProbedelegates to the existingSdkProbeCache-backed memoized probes from Cache dnx/SDK capability probe to reduce build overhead #187 - no behavior change, no new caching. AddsIsGlobalToolInstalledfor offline pre-flight use.EfcptOfflineModeMSBuild property (defaultfalse): threads anOfflineflag through the tool resolution/restoreStrategycontexts and prepends!ctx.Offline &&to the three network-dependent branches - dnx execution, tool-manifest restore, and global toolupdate. Emits a one-time high-importance status message when offline. Also honors theEFCPT_OFFLINEenvironment variable.JD0026pre-flight validation: when offline,RunEfcptrequires the tool to already be runnable via an explicit existingEfcptToolPath, a discovered (already-restored) tool manifest, or a global tool onPATH. If none apply, it fails actionably with errorJD0026(modeled onProviderDriverNotFoundException's shape - problem, exactdotnet tool installcommands, detected state, docs URL) vialog.Error+return false, never throwing._EfcptCheckForUpdatestarget withand '$(EfcptOfflineMode)' != 'true'so the SDK update-check network call is also skipped offline.docs/user-guide/offline.mdand aJD0026entry indocs/user-guide/error-codes.md.tests/JD.Efcpt.Build.Tests/OfflineModeTests.cs(TinyBDD, mirrorsRunEfcptTests/DotNetToolUtilitiesTests): offline + pre-provisionedToolPathsucceeds while a throwing fake probe proves no SDK/dnx/global-tool check runs; offline + no manifest/noToolPath/all-probes-unavailable fails withJD0026containing adotnet tool installcommand; offline defaults to disabled.Known gap - buildTransitive regeneration blocked (pre-existing, unrelated)
buildTransitive/JD.Efcpt.Build.{props,targets}are not regenerated in this PR. Regeneration is currently broken onmain, independent of this change:JD.MSBuild.Fluent1.3.20->1.3.22as a "semver-patch" update.TargetsBuilder.RegisterTasks's parameter names (assemblyPathno longer valid), which cascades into ~44 compile errors acrosssrc/JD.Efcpt.Build.Definitions(confirmed viadotnet msbuild -t:JDMSBuildFluentGenerate -p:TargetFramework=net8.0 -p:JDMSBuildFluentGenerateEnabled=trueperGENERATING.md, reproduced identically on a cleanorigin/maincheckout).JD.Efcpt.Build.Definitions.csprojis not part ofJD.Efcpt.Build.sln, andGENERATING.mdnotes CI always runs with generation disabled - so this breakage has been dormant/undetected.The
EfcptOfflineModeproperty,OfflineModetask param, and_EfcptCheckForUpdatesguard are all added at the source (factory) level and are ready to regenerate once theJD.MSBuild.Fluentversion issue is resolved (pin back to1.3.20, or update theRegisterTasks/registry call sites for the new API). Until then, the packaged NuGet'sbuildTransitiveXML does not yet exposeEfcptOfflineModeto consumers, even though the task itself fully supports it when driven directly (e.g. via the task param, as covered by the new tests).Test plan
dotnet build JD.Efcpt.Build.sln -c Debug- 0 errors, warning-clean (only pre-existing NuGet TFM-support warnings, not compiler warnings)dotnet test tests/JD.Efcpt.Build.Tests -c Debug --filter "FullyQualifiedName~Offline |FullyQualifiedName~RunEfcpt|FullyQualifiedName~DotNetToolUtilities|FullyQualifiedName~SdkProbeCache"- 146 passed, 0 failed, 0 skippedJD.MSBuild.Fluentversion mismatch, regeneratebuildTransitive/, verifyEfcptOfflineMode/OfflineModeparam appear in the generated XMLFixes #185.
Co-Authored-By: Claude Opus 4.6 noreply@anthropic.com