Skip to content

Commit e96a280

Browse files
authored
[No Ticket] Tee output (#1546)
* --tee-output for fossa analyze * tee-output for fossa container analyze. * fmt-ci * Update changelog
1 parent 2bdfdde commit e96a280

File tree

7 files changed

+119
-54
lines changed

7 files changed

+119
-54
lines changed

Changelog.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# FOSSA CLI Changelog
22

3+
## 3.10.9
4+
5+
- CLI Args: Add a `--tee-output` argument to allow uploading results and also printing them to stdout.([#1546](https://github.com/fossas/fossa-cli/pull/1546))
6+
37
## 3.10.8
48

59
- Custom license scans: Apply `licenseScanPathFilters` to custom license scans ([#1535](https://github.com/fossas/fossa-cli/pull/1535)).

docs/references/subcommands/analyze.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,20 @@ The paths and targets filtering options allow you to specify the exact targets w
4242
| `--without-default-filters` | Ignore default path filters. See [default path filters](./analyze.md#what-are-the-default-path-filters) |
4343

4444

45-
### Printing results without uploading to FOSSA
45+
### Printing FOSSA results
4646

4747
The `--output` flag can be used to print projects and dependency graph information to stdout, rather than uploading to FOSSA
4848

4949
```sh
5050
fossa analyze --output
5151
```
5252

53+
To print projects and dependency graph information to stdout *in addition* to uploading to FOSSA as normal, use the `--tee-output` flag.
54+
55+
```sh
56+
fossa analyze --tee-output
57+
```
58+
5359
### Printing project metadata
5460

5561
The `--json` flag can be used to print project metadata after running `fossa analyze` successfully. This metadata can be used to reference your project when integrating with the FOSSA API.

src/App/Fossa/Analyze.hs

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import App.Fossa.Config.Analyze (
4949
WithoutDefaultFilters (..),
5050
)
5151
import App.Fossa.Config.Analyze qualified as Config
52+
import App.Fossa.Config.Common (DestinationMeta (..), destinationApiOpts, destinationMetadata)
5253
import App.Fossa.FirstPartyScan (runFirstPartyScan)
5354
import App.Fossa.Lernie.Analyze (analyzeWithLernie)
5455
import App.Fossa.Lernie.Types (LernieResults (..))
@@ -105,6 +106,7 @@ import Data.List.NonEmpty qualified as NE
105106
import Data.Maybe (fromMaybe, mapMaybe)
106107
import Data.String.Conversion (decodeUtf8, toText)
107108
import Data.Text.Extra (showT)
109+
import Data.Traversable (for)
108110
import Diag.Diagnostic as DI
109111
import Diag.Result (resultToMaybe)
110112
import Discovery.Archive qualified as Archive
@@ -278,9 +280,8 @@ analyze ::
278280
analyze cfg = Diag.context "fossa-analyze" $ do
279281
capabilities <- sendIO getNumCapabilities
280282

281-
let maybeApiOpts = case destination of
282-
OutputStdout -> Nothing
283-
UploadScan opts _ -> Just opts
283+
let maybeDestMeta = destinationMetadata destination
284+
maybeApiOpts = destinationApiOpts <$> maybeDestMeta
284285
BaseDir basedir = Config.baseDir cfg
285286
destination = Config.scanDestination cfg
286287
filters = Config.filterSet cfg
@@ -305,10 +306,12 @@ analyze cfg = Diag.context "fossa-analyze" $ do
305306
pure Nothing
306307
else Diag.context "fossa-deps" . runStickyLogger SevInfo $ analyzeFossaDepsFile basedir customFossaDepsFile maybeApiOpts vendoredDepsOptions
307308

308-
orgInfo <- case destination of
309-
OutputStdout -> pure Nothing
310-
UploadScan apiOpts metadata ->
311-
fmap Just . runFossaApiClient apiOpts . preflightChecks $ AnalyzeChecks revision metadata
309+
orgInfo <-
310+
for
311+
maybeDestMeta
312+
( \(DestinationMeta (apiOpts, metadata)) ->
313+
runFossaApiClient apiOpts . preflightChecks $ AnalyzeChecks revision metadata
314+
)
312315

313316
-- additional source units are built outside the standard strategy flow, because they either
314317
-- require additional information (eg API credentials), or they return additional information (eg user deps).
@@ -387,21 +390,20 @@ analyze cfg = Diag.context "fossa-analyze" $ do
387390
let filteredProjects = mapMaybe toProjectResult projectScans
388391
logDebug $ "Filtered project scans: " <> pretty (show filteredProjects)
389392

390-
maybeEndpointAppVersion <- case destination of
391-
UploadScan apiOpts _ -> runFossaApiClient apiOpts $ do
393+
maybeEndpointAppVersion <- fmap join . for maybeApiOpts $
394+
\apiOpts -> runFossaApiClient apiOpts $ do
392395
-- Using 'recovery' as API corresponding to 'getEndpointVersion',
393396
-- seems to be not stable and we sometimes see TimeoutError in telemetry
394397
version <- recover getEndpointVersion
395398
debugMetadata "FossaEndpointCoreVersion" version
396399
pure version
397-
_ -> pure Nothing
398400

399401
-- In our graph, we may have unresolved path dependencies
400402
-- If we are in output mode, do nothing. If we are in upload mode
401403
-- license scan all path dependencies, and upload findings to Endpoint,
402404
-- and queue a build for all path+ dependencies
403-
filteredProjects' <- case (shouldAnalyzePathDependencies, destination) of
404-
(True, UploadScan apiOpts _) ->
405+
filteredProjects' <- case (shouldAnalyzePathDependencies, destinationMetadata destination) of
406+
(True, Just (DestinationMeta (apiOpts, _))) ->
405407
Diag.context "path-dependencies"
406408
. runFossaApiClient apiOpts
407409
$ runStickyLogger SevInfo
@@ -440,19 +442,23 @@ analyze cfg = Diag.context "fossa-analyze" $ do
440442
(False, FilteredAll) -> Diag.warn ErrFilteredAllProjects $> emptyScanUnits
441443
(True, FilteredAll) -> Diag.warn ErrOnlyKeywordSearchResultsFound $> emptyScanUnits
442444
(_, CountedScanUnits scanUnits) -> pure scanUnits
443-
doUpload outputResult iatAssertion destination basedir jsonOutput revision scanUnits reachabilityUnits
445+
sendToDestination outputResult iatAssertion destination basedir jsonOutput revision scanUnits reachabilityUnits
444446

445447
pure outputResult
446448
where
447-
doUpload result iatAssertion destination basedir jsonOutput revision scanUnits reachabilityUnits =
448-
case destination of
449-
OutputStdout -> logStdout . decodeUtf8 $ Aeson.encode result
450-
UploadScan apiOpts metadata ->
451-
Diag.context "upload-results"
452-
. runFossaApiClient apiOpts
453-
$ do
454-
locator <- uploadSuccessfulAnalysis (BaseDir basedir) metadata jsonOutput revision scanUnits reachabilityUnits
455-
doAssertRevisionBinaries iatAssertion locator
449+
sendToDestination result iatAssertion destination basedir jsonOutput revision scanUnits reachabilityUnits =
450+
let doUpload (DestinationMeta (apiOpts, metadata)) =
451+
Diag.context "upload-results"
452+
. runFossaApiClient apiOpts
453+
$ do
454+
locator <- uploadSuccessfulAnalysis (BaseDir basedir) metadata jsonOutput revision scanUnits reachabilityUnits
455+
doAssertRevisionBinaries iatAssertion locator
456+
in case destination of
457+
OutputStdout -> logStdout . decodeUtf8 $ Aeson.encode result
458+
UploadScan meta -> doUpload meta
459+
OutputAndUpload meta -> do
460+
logStdout . decodeUtf8 $ Aeson.encode result
461+
doUpload meta
456462

457463
emptyScanUnits :: ScanUnits
458464
emptyScanUnits = SourceUnitOnly []

src/App/Fossa/Config/Analyze.hs

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ import App.Docs (fossaAnalyzeDefaultFilterDocUrl)
3636
import App.Fossa.Config.Common (
3737
CacheAction (WriteOnly),
3838
CommonOpts (..),
39+
DestinationMeta (..),
40+
OutputStyle (..),
3941
ScanDestination (..),
4042
applyReleaseGroupDeprecationWarning,
4143
baseDirArg,
@@ -46,6 +48,7 @@ import App.Fossa.Config.Common (
4648
collectRevisionData',
4749
commonOpts,
4850
metadataOpts,
51+
outputStyleArgs,
4952
pathOpt,
5053
targetOpt,
5154
validateDir,
@@ -201,7 +204,7 @@ instance ToJSON VendoredDependencyOptions where
201204

202205
data AnalyzeCliOpts = AnalyzeCliOpts
203206
{ commons :: CommonOpts
204-
, analyzeOutput :: Bool
207+
, analyzeOutput :: OutputStyle
205208
, analyzeUnpackArchives :: Flag UnpackArchives
206209
, analyzeJsonOutput :: Flag JsonOutput
207210
, analyzeIncludeAllDeps :: Flag IncludeAll
@@ -235,9 +238,9 @@ data AnalyzeCliOpts = AnalyzeCliOpts
235238

236239
instance GetCommonOpts AnalyzeCliOpts where
237240
getCommonOpts AnalyzeCliOpts{analyzeOutput, commons} =
238-
if analyzeOutput
239-
then Just commons{optTelemetry = Just NoTelemetry} -- When `--output` is used don't emit no telemetry.
240-
else Just commons
241+
case analyzeOutput of
242+
Output -> Just commons{optTelemetry = Just NoTelemetry} -- When `--output` is used don't emit no telemetry.
243+
_ -> Just commons
241244

242245
instance GetSeverity AnalyzeCliOpts where
243246
getSeverity AnalyzeCliOpts{commons = CommonOpts{optDebug}} = if optDebug then SevDebug else SevInfo
@@ -309,7 +312,7 @@ cliParser :: Parser AnalyzeCliOpts
309312
cliParser =
310313
AnalyzeCliOpts
311314
<$> commonOpts
312-
<*> switch (applyFossaStyle <> long "output" <> short 'o' <> stringToHelpDoc "Output results to stdout instead of uploading to FOSSA")
315+
<*> outputStyleArgs
313316
<*> flagOpt UnpackArchives (applyFossaStyle <> long "unpack-archives" <> stringToHelpDoc "Recursively unpack and analyze discovered archives")
314317
<*> flagOpt JsonOutput (applyFossaStyle <> long "json" <> stringToHelpDoc "Output project metadata as JSON to the console. This is useful for communicating with the FOSSA API.")
315318
<*> flagOpt IncludeAll (applyFossaStyle <> long "include-unused-deps" <> stringToHelpDoc "Include all deps found, instead of filtering non-production deps. Ignored by VSI.")
@@ -643,14 +646,17 @@ collectScanDestination ::
643646
AnalyzeCliOpts ->
644647
m ScanDestination
645648
collectScanDestination maybeCfgFile envvars AnalyzeCliOpts{..} =
646-
if analyzeOutput
647-
then pure OutputStdout
648-
else do
649+
case analyzeOutput of
650+
Output -> pure OutputStdout
651+
TeeOutput -> getScanUploadDest OutputAndUpload
652+
Default -> getScanUploadDest UploadScan
653+
where
654+
getScanUploadDest constructor = do
649655
apiOpts <- collectApiOpts maybeCfgFile envvars commons
650656
metaMerged <- maybe (pure analyzeMetadata) (mergeFileCmdMetadata analyzeMetadata) (maybeCfgFile)
651657
void $ applyReleaseGroupDeprecationWarning metaMerged
652658
when (length (projectLabel metaMerged) > 5) $ fatalText "Projects are only allowed to have 5 associated project labels"
653-
pure $ UploadScan apiOpts metaMerged
659+
pure $ constructor (DestinationMeta (apiOpts, metaMerged))
654660

655661
collectVsiModeOptions ::
656662
( Has Diagnostics sig m

src/App/Fossa/Config/Common.hs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ module App.Fossa.Config.Common (
3535

3636
-- * Configuration Types
3737
ScanDestination (..),
38+
OutputStyle (..),
39+
DestinationMeta (..),
40+
destinationMetadata,
41+
destinationApiOpts,
42+
outputStyleArgs,
3843

3944
-- * Global Defaults
4045
defaultTimeoutDuration,
@@ -127,6 +132,8 @@ import Options.Applicative (
127132
argument,
128133
auto,
129134
eitherReader,
135+
flag,
136+
flag',
130137
long,
131138
metavar,
132139
option,
@@ -151,12 +158,42 @@ import Text.Megaparsec (errorBundlePretty, runParser)
151158
import Text.URI (URI, mkURI)
152159
import Types (TargetFilter)
153160

161+
newtype DestinationMeta = DestinationMeta (ApiOpts, ProjectMetadata)
162+
deriving (Eq, Ord, Show, Generic)
163+
164+
destinationApiOpts :: DestinationMeta -> ApiOpts
165+
destinationApiOpts (DestinationMeta m) = fst m
166+
167+
instance ToJSON DestinationMeta where
168+
toEncoding = genericToEncoding defaultOptions
169+
170+
-- | CLI options describing what to do with analysis results.
171+
data OutputStyle
172+
= -- | Upload results
173+
Default
174+
| -- | Output results to stdout, but do not upload
175+
Output
176+
| -- | Upload results as with `Upload`, but also output them to stdout as with `Output`
177+
TeeOutput
178+
deriving (Ord, Eq, Show)
179+
180+
outputStyleArgs :: Parser OutputStyle
181+
outputStyleArgs =
182+
flag' Output (applyFossaStyle <> long "output" <> short 'o' <> stringToHelpDoc "Output results to stdout instead of uploading to FOSSA")
183+
<|> flag Default TeeOutput (applyFossaStyle <> long "tee-output" <> stringToHelpDoc "Like --output, but upload in addition to outputting to stdout.")
184+
154185
data ScanDestination
155186
= -- | upload to fossa with provided api key and base url
156-
UploadScan ApiOpts ProjectMetadata
187+
UploadScan DestinationMeta
188+
| OutputAndUpload DestinationMeta
157189
| OutputStdout
158190
deriving (Eq, Ord, Show, Generic)
159191

192+
destinationMetadata :: ScanDestination -> Maybe DestinationMeta
193+
destinationMetadata (UploadScan meta) = Just meta
194+
destinationMetadata (OutputAndUpload meta) = Just meta
195+
destinationMetadata _ = Nothing
196+
160197
instance ToJSON ScanDestination where
161198
toEncoding = genericToEncoding defaultOptions
162199

src/App/Fossa/Config/Container/Analyze.hs

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,16 @@ import App.Docs (fossaContainerAnalyzeDefaultFilterDocUrl)
1414
import App.Fossa.Config.Analyze (WithoutDefaultFilters, branchHelp, withoutDefaultFilterParser)
1515
import App.Fossa.Config.Common (
1616
CommonOpts (CommonOpts, optDebug, optProjectName, optProjectRevision),
17+
DestinationMeta (..),
18+
OutputStyle (..),
1719
ScanDestination (..),
1820
collectAPIMetadata,
1921
collectApiOpts,
2022
collectConfigFileFilters,
2123
collectRevisionOverride,
2224
commonOpts,
2325
metadataOpts,
26+
outputStyleArgs,
2427
)
2528
import App.Fossa.Config.ConfigFile
2629
import App.Fossa.Config.Container.Common (
@@ -38,7 +41,7 @@ import App.Types (
3841
import Control.Effect.Diagnostics (Diagnostics, Has)
3942
import Data.Aeson (ToJSON, defaultOptions, genericToEncoding)
4043
import Data.Aeson.Types (ToJSON (toEncoding))
41-
import Data.Flag (Flag, flagOpt, fromFlag)
44+
import Data.Flag (Flag, flagOpt)
4245
import Data.Monoid.Extra (isMempty)
4346
import Data.Text (Text)
4447
import Discovery.Filters
@@ -84,7 +87,7 @@ instance ToJSON ContainerAnalyzeConfig where
8487

8588
data ContainerAnalyzeOptions = ContainerAnalyzeOptions
8689
{ analyzeCommons :: CommonOpts
87-
, containerNoUpload :: Flag NoUpload
90+
, containerOutputStyle :: OutputStyle
8891
, containerJsonOutput :: Flag JsonOutput
8992
, containerBranch :: Maybe Text
9093
, containerMetadata :: ProjectMetadata
@@ -109,13 +112,7 @@ cliParser :: Parser ContainerAnalyzeOptions
109112
cliParser =
110113
ContainerAnalyzeOptions
111114
<$> commonOpts
112-
<*> flagOpt
113-
NoUpload
114-
( applyFossaStyle
115-
<> long "output"
116-
<> short 'o'
117-
<> stringToHelpDoc "Output results to stdout instead of uploading to FOSSA"
118-
)
115+
<*> outputStyleArgs
119116
<*> flagOpt JsonOutput (applyFossaStyle <> long "json" <> stringToHelpDoc "Output project metadata as JSON to the console. This is useful for communicating with the FOSSA API.")
120117
<*> optional
121118
( strOption
@@ -173,12 +170,15 @@ collectScanDestination ::
173170
ContainerAnalyzeOptions ->
174171
m ScanDestination
175172
collectScanDestination maybeCfgFile envvars ContainerAnalyzeOptions{..} =
176-
if fromFlag NoUpload containerNoUpload
177-
then pure OutputStdout
178-
else do
173+
case containerOutputStyle of
174+
Output -> pure OutputStdout
175+
TeeOutput -> getScanUploadDest OutputAndUpload
176+
Default -> getScanUploadDest UploadScan
177+
where
178+
getScanUploadDest constructor = do
179179
apiOpts <- collectApiOpts maybeCfgFile envvars analyzeCommons
180180
metaMerged <- collectAPIMetadata maybeCfgFile containerMetadata
181-
pure $ UploadScan apiOpts metaMerged
181+
pure $ constructor (DestinationMeta (apiOpts, metaMerged))
182182

183183
collectFilters :: Maybe ConfigFile -> AllFilters
184184
collectFilters maybeConfig = do

src/App/Fossa/Container/AnalyzeNative.hs

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import App.Fossa.API.BuildLink (getFossaBuildUrl)
1010
import App.Fossa.Analyze.Debug (collectDebugBundle)
1111
import App.Fossa.Analyze.Upload (emitBuildWarnings)
1212
import App.Fossa.Config.Common (
13-
ScanDestination (OutputStdout, UploadScan),
13+
DestinationMeta (..),
14+
ScanDestination (..),
1415
applyReleaseGroupDeprecationWarning,
1516
)
1617
import App.Fossa.Config.Container.Analyze (
@@ -114,14 +115,19 @@ analyze cfg = do
114115
let branchText = fromMaybe "No branch (detached HEAD)" $ projectBranch revision
115116
logInfo ("Using branch: `" <> pretty branchText <> "`")
116117

118+
let doUpload apiOpts projectMetadata = runFossaApiClient apiOpts $ do
119+
orgInfo <- preflightChecks $ AnalyzeChecks revision projectMetadata
120+
logProjectInfo
121+
void $ uploadScan orgInfo revision projectMetadata (jsonOutput cfg) scannedImage
122+
123+
let scannedImageToStdout = do
124+
logProjectInfo
125+
logStdout . decodeUtf8 $ Aeson.encode scannedImage
126+
117127
case scanDestination cfg of
118-
OutputStdout -> do
119-
logProjectInfo
120-
logStdout . decodeUtf8 $ Aeson.encode scannedImage
121-
UploadScan apiOpts projectMetadata -> runFossaApiClient apiOpts $ do
122-
orgInfo <- preflightChecks $ AnalyzeChecks revision projectMetadata
123-
logProjectInfo
124-
void $ uploadScan orgInfo revision projectMetadata (jsonOutput cfg) scannedImage
128+
OutputStdout -> scannedImageToStdout
129+
UploadScan (DestinationMeta (apiOpts, projectMetadata)) -> doUpload apiOpts projectMetadata
130+
OutputAndUpload (DestinationMeta (apiOpts, projectMetadata)) -> scannedImageToStdout >> doUpload apiOpts projectMetadata
125131

126132
pure scannedImage
127133

0 commit comments

Comments
 (0)