diff --git a/docs/references/files/fossa-yml.md b/docs/references/files/fossa-yml.md index a38363c428..2161d64af5 100644 --- a/docs/references/files/fossa-yml.md +++ b/docs/references/files/fossa-yml.md @@ -15,8 +15,10 @@ project: locator: custom+1/github.com/fossas/fossa-cli id: github.com/fossas/fossa-cli name: fossa-cli + team: cli-team teams: - - cli-team + - cli-team-1 + - cli-team-2 policy: custom-cli-policy link: fossa.com url: github.com/fossas/fossa-cli @@ -148,11 +150,14 @@ Default: #### `project.name:` The name field sets the projects visible name in the FOSSA dashboard. By default, this will be set to the project's ID. +#### `project.team:` +The name of the team in your FOSSA organization to associate this project with. + #### `project.teams:` The name of the teams in your FOSSA organization to associate this project with. >NOTE: - Currently, commands such as `fossa analyze` and `fossa container analyze` will only use the first team in the list. Use [fossa project edit](../subcommands/project/edit.md) to associate a project to all teams in the list. + Currently, ONLY `fossa project edit` utilizes this field. Use [fossa project edit](../subcommands/project/edit.md) to add a project to all teams in the list. #### `project.policy:` The name of the policy in your FOSSA organization to associate this project with. diff --git a/docs/references/files/fossa-yml.v3.schema.json b/docs/references/files/fossa-yml.v3.schema.json index b1724cf73e..47c209563b 100644 --- a/docs/references/files/fossa-yml.v3.schema.json +++ b/docs/references/files/fossa-yml.v3.schema.json @@ -22,6 +22,11 @@ "minLength": 1, "description": "The name field sets the projects visible name in the FOSSA dashboard. By default, this will be set to the project's ID." }, + "team": { + "type": "string", + "minLength": 1, + "description": "The name of the team in your FOSSA organization to associate this project with." + }, "teams": { "type": "array", "description": "A list of team names in your FOSSA organization to associate this project with.", diff --git a/docs/references/subcommands/project/edit.md b/docs/references/subcommands/project/edit.md index 491d3aa026..8ea3d727b9 100644 --- a/docs/references/subcommands/project/edit.md +++ b/docs/references/subcommands/project/edit.md @@ -6,6 +6,7 @@ Argument | Required | Description -----------------------------|----------|-------------------------------------------------------------------------------------------------------------------------------------- +`--config` / `-c` | No | The to your path to your `.fossa.yml`. `--project-locator` | Yes | The project Locator defines a unique ID that the FOSSA API will use to reference this project within FOSSA. The project locator can be found in the UI on the project `Settings` page listed as the "Project Locator" underneath the "Project Title" setting. `--project-id` | Yes | The project ID defines an ID that is used to reference a project within your FOSSA organization. The project ID is a specific portion of the project locator and can be found in the UI on the project `Settings` page listed as the "Project Locator" underneath the "Project Title" setting. For example, if the "Project Locator" value of `custom+1/foo` is provided in the FOSSA UI, use `foo`. Project ID defaults to the .git/config file or project's remote "origin" URL (Git), "Repository Root" obtained using 'svn info' (SVN), or the name of the project's directory (No VCS), if project ID wasn't explicityly set during project creation. `--title` / `-t` | No | The title of the FOSSA project. @@ -19,13 +20,29 @@ Argument | Required | Description > NOTE: Either project ID OR project locator needs to be set. Project ID takes precedence over project locator. For more details on the differences between project ID and project locator refer to [documentation](../../files/fossa-yml.md#what-is-the-difference-between-project-id-and-project-locator). ->NOTE: When updating project labels through `fossa project edit`, the transaction is all or nothing. This means that the project labels specified through this command will overwrite the existing labels that are associated with the project. Be sure to include all the labels that you want to be associated with the project, even if some labels are already currently set. - ## .fossa.yml Configuration All of the previously mentioned CLI options can be provided through a `.fossa.yml`. Refer to [fossa configuration](../../files/fossa-yml.md) to set up your `.fossa.yml`. -> NOTE: CLI options take precedence over the configurations in `.fossa.yml`. +> NOTE: CLI options take precedence over the configurations in `.fossa.yml`. + +## `fossa project edit` usage and guidance + +### Updating a project's labels + +When updating project labels through `fossa project edit`, the transaction is all or nothing. This means that the project labels specified through this command will overwrite the existing labels that are associated with the project. Be sure to include all the labels that you want to be associated with the project, even if some labels are already currently set. + +### Updating the teams associated with a project + +Currently, `fossa project edit` only supports adding a project to the teams that are specified through the command. There will be support to remove a project from the provided teams in the future. + +Providing teams for `fossa project edit` takes the following precdence: + +1. CLI options - Adds the project to teams (1 or many) +2. `project.teams` in `.fossa.yml` - Adds the project to teams (1 or many) +3. `project.team` in `.fossa.yml` - Adds the project to a team + +There is support for `project.team` due to backwards compatability as `project.teams` is a newly added field in `.fossa.yml`. ## Example diff --git a/src/App/Fossa/ApiUtils.hs b/src/App/Fossa/ApiUtils.hs index b64ce95de0..58bc4b38d3 100644 --- a/src/App/Fossa/ApiUtils.hs +++ b/src/App/Fossa/ApiUtils.hs @@ -6,7 +6,7 @@ module App.Fossa.ApiUtils ( ) where import Control.Algebra (Has) -import Control.Effect.Diagnostics (Diagnostics, errHelp, fatalText, warn) +import Control.Effect.Diagnostics (Diagnostics, errHelp, fatalText) import Data.Map qualified as Map import Data.Maybe (mapMaybe) import Data.Text (Text, intercalate) @@ -42,22 +42,25 @@ retrieveTeamIds teamNames teams = do let missingTeamNames = filter (`Map.notMember` teamMap) teamNames fatalText $ "Teams " <> intercalate "," missingTeamNames <> "not found" -retrieveLabelIds :: Has Diagnostics sig m => [Text] -> Labels -> m [Int] +retrieveLabelIds :: Has Diagnostics sig m => [Text] -> Labels -> m ([Int], Maybe [Text]) retrieveLabelIds projectLabels (Labels orgLabels) = do let orgLabelMap = Map.fromList $ map (\label -> (labelName label, labelId label)) orgLabels go orgLabelMap projectLabels [] where - go :: Has Diagnostics sig m => Map.Map Text Int -> [Text] -> [Int] -> m [Int] - go _ [] acc = pure acc + go :: Has Diagnostics sig m => Map.Map Text Int -> [Text] -> [Int] -> m ([Int], Maybe [Text]) + go _ [] acc = pure (acc, Nothing) go labelMap (x : xs) acc = do case Map.lookup x labelMap of Just labelId' -> go labelMap xs (labelId' : acc) Nothing -> do - warn $ - renderIt $ - vsep - [ "Label `" <> pretty x <> "` does not exist" - , "Navigate to `Organization Settings` in the FOSSA web UI to create new labels: https://app.fossa.com/account/settings/organization" - ] - - go labelMap xs acc + (labelIds, maybeWarnings) <- go labelMap xs acc + let warning = + renderIt $ + vsep + [ "Label `" <> pretty x <> "` does not exist" + , "Navigate to `Organization Settings` in the FOSSA web UI to create new labels: https://app.fossa.com/account/settings/organization" + ] + let updatedWarnings = case maybeWarnings of + Just warnings -> Just (warning : warnings) + Nothing -> Just [warning] + pure (labelIds, updatedWarnings) diff --git a/src/App/Fossa/Config/ConfigFile.hs b/src/App/Fossa/Config/ConfigFile.hs index d6377764bf..ea2f70be8b 100644 --- a/src/App/Fossa/Config/ConfigFile.hs +++ b/src/App/Fossa/Config/ConfigFile.hs @@ -52,7 +52,6 @@ import Data.Foldable (asum) import Data.Functor (($>)) import Data.Map (Map) import Data.Map qualified as Map -import Data.Maybe (listToMaybe) import Data.Set (Set) import Data.Set qualified as Set import Data.String.Conversion (ToString (toString), ToText (toText)) @@ -183,19 +182,12 @@ mergeFileCmdMetadata meta cfgFile = , projectUrl = projectUrl meta <|> (configProject cfgFile >>= configUrl) , projectJiraKey = projectJiraKey meta <|> (configProject cfgFile >>= configJiraKey) , projectLink = projectLink meta <|> (configProject cfgFile >>= configLink) - , -- The config file now allows allows the `project` field to accept a list of teams. - -- `fossa project edit` uses the list of teams to add a project to all teams. - -- All other command flows that use the ProjectMetadata type (e.g. `fossa analyze`) does not account for the change so just take the first team in the list if it exists. - projectTeam = projectTeam meta <|> extractFirstTeamFromConfig (configProject cfgFile >>= configTeams) + , projectTeam = projectTeam meta <|> (configProject cfgFile >>= configTeam) , projectPolicy = policy , projectLabel = projectLabel meta <|> (maybe [] configLabel (configProject cfgFile)) , projectReleaseGroup = projectReleaseGroup meta <|> (configProject cfgFile >>= configProjectReleaseGroup) } - extractFirstTeamFromConfig :: Maybe [Text] -> Maybe Text - extractFirstTeamFromConfig Nothing = Nothing - extractFirstTeamFromConfig (Just teams) = listToMaybe teams - data ConfigFile = ConfigFile { configVersion :: Int , configServer :: Maybe Text @@ -222,6 +214,7 @@ data ConfigProject = ConfigProject , configProjID :: Maybe Text , configName :: Maybe Text , configLink :: Maybe Text + , configTeam :: Maybe Text , configTeams :: Maybe [Text] , configJiraKey :: Maybe Text , configUrl :: Maybe Text @@ -326,6 +319,7 @@ instance FromJSON ConfigProject where <*> obj .:? "id" <*> obj .:? "name" <*> obj .:? "link" + <*> obj .:? "team" <*> obj .:? "teams" <*> obj .:? "jiraProjectKey" <*> obj .:? "url" diff --git a/src/App/Fossa/Config/Project/Edit.hs b/src/App/Fossa/Config/Project/Edit.hs index e0caaabc01..447cedeb93 100644 --- a/src/App/Fossa/Config/Project/Edit.hs +++ b/src/App/Fossa/Config/Project/Edit.hs @@ -162,8 +162,10 @@ mergeOpts maybeConfig envVars cliOpts@EditOpts{..} = do , projectPolicy = maybePolicy , projectLink = projectLinkOpts <|> extractProjectConfigVal maybeConfig configLink , projectLabels = projectLabelsOpts <|> labelConfigToMaybe configProjectLabels - , -- Teams to add project to - teams = teamsOpts <|> extractProjectConfigVal maybeConfig configTeams + , -- Teams to add project to. + -- Precdence for teams is CLI input, `project.teams` field from config, then `project.team` field from config. + -- Account for `project.team` for backwards compatability. + teams = teamsOpts <|> extractProjectConfigVal maybeConfig configTeams <|> ((: []) <$> extractProjectConfigVal maybeConfig configTeam) } labelConfigToMaybe :: [Text] -> Maybe [Text] diff --git a/src/App/Fossa/Init/.fossa.yml b/src/App/Fossa/Init/.fossa.yml index fcfd77c729..ab4cc0e8b1 100644 --- a/src/App/Fossa/Init/.fossa.yml +++ b/src/App/Fossa/Init/.fossa.yml @@ -47,11 +47,15 @@ version: 3 # # Otherwise, they will be silently ignored. Use fossa project edit to modify this field for existing projects. # name: fossa-cli # -# # Name of the teams in your FOSSA organization to associate with this project. +# # Name of the team in your FOSSA organization to associate with this project. # # NOTE: # # Can only be set when creating a project (running fossa analyze for the first time), # # Otherwise, they will be silently ignored. Use fossa project edit to modify this field for existing projects. -# team: +# team: example-team +# +# # Name of the teams in your FOSSA organization to associate with this project. +# # Currently, this field is only supported through fossa project edit. +# teams: # - example-team-1 # - example-team-2 # diff --git a/src/App/Fossa/Project/Edit.hs b/src/App/Fossa/Project/Edit.hs index 031fb1d4ca..070955c483 100644 --- a/src/App/Fossa/Project/Edit.hs +++ b/src/App/Fossa/Project/Edit.hs @@ -10,11 +10,11 @@ import App.Types (Policy (..)) import Control.Algebra (Has) import Control.Carrier.Diagnostics (context, errHelp) import Control.Carrier.StickyLogger (logSticky) -import Control.Effect.Diagnostics (Diagnostics, fatalText) +import Control.Effect.Diagnostics (Diagnostics, fatalText, warn) import Control.Effect.FossaApiClient (FossaApiClient, addTeamProjects, getOrgLabels, getOrganization, getPolicies, getProjectV2, getTeams, updateProject, updateRevision) import Control.Effect.Lift (Lift) import Control.Effect.StickyLogger (StickyLogger) -import Control.Monad (void, when) +import Control.Monad (void) import Data.Foldable (traverse_) import Data.Maybe (isJust) import Data.String.Conversion (toText) @@ -44,11 +44,15 @@ editMain EditConfig{..} = do project <- getProjectV2 projectLocator - attemptToUpdateProjectMetadata projectLocator (projectDefaultBranch project) $ projectIssueTrackerIds project + maybeWarnings <- attemptToUpdateProjectMetadata projectLocator (projectDefaultBranch project) $ projectIssueTrackerIds project attemptToAddProjectToTeams projectLocator teams attemptToUpdateRevisionMetadata projectLocator (projectLatestRevision project) - logStdout $ "Project " <> "`" <> projectLocator <> "` has been updated." <> "\n" + case maybeWarnings of + Nothing -> logStdout $ "Project " <> "`" <> projectLocator <> "` has been updated successfully." <> "\n" + Just projectUpdateWarnings -> do + logStdout $ "Project " <> "`" <> projectLocator <> "` has been updated with warnings. Run the command with `--debug` to view the warnings." <> "\n" + traverse_ warn projectUpdateWarnings where constructProjectLocatorFromProjectId :: Organization -> Text -> Text constructProjectLocatorFromProjectId org projId = "custom+" <> toText (show $ organizationId org) <> "/" <> projId @@ -62,42 +66,47 @@ editMain EditConfig{..} = do Text -> Maybe Text -> Maybe [Text] -> - m () + m (Maybe [Text]) attemptToUpdateProjectMetadata projectLocator maybeDefaultBranch currentIssueTrackerIds = do -- Ensure that at least one project metadata field is being updated, otherwise the endpoint will throw an error - when (or [isJust projectTitle, isJust projectUrl, isJust projectJiraKey, isJust projectLabels, isJust projectPolicy]) $ do - logSticky "Updating project metadata" - maybePolicyId <- case projectPolicy of - Nothing -> pure Nothing - Just policy -> case policy of - PolicyId policyId -> pure $ Just policyId - PolicyName policyName -> do - policies <- getPolicies - context "Retrieving license policy ID" $ retrievePolicyId (Just policyName) LICENSING policies + if (or [isJust projectTitle, isJust projectUrl, isJust projectJiraKey, isJust projectLabels, isJust projectPolicy]) + then do + logSticky "Updating project metadata" + maybePolicyId <- case projectPolicy of + Nothing -> pure Nothing + Just policy -> case policy of + PolicyId policyId -> pure $ Just policyId + PolicyName policyName -> do + policies <- getPolicies + context "Retrieving license policy ID" $ retrievePolicyId (Just policyName) LICENSING policies - orgLabels <- getOrgLabels - maybeLabelIds <- case projectLabels of - Nothing -> pure Nothing - Just labels -> context "Retrieving label Ids" $ Just <$> retrieveLabelIds (labels) orgLabels + orgLabels <- getOrgLabels + (maybeLabelIds, maybeLabelWarnings) <- case projectLabels of + Nothing -> pure (Nothing, Nothing) + Just labels -> do + (labelIds, maybeLabelWarnings) <- context "Retrieving label Ids" $ retrieveLabelIds (labels) orgLabels + pure (Just labelIds, maybeLabelWarnings) - -- Updating the project's issue tracker ids is an all or nothing transaction. - -- Add the specified jira key to the current list of issue tracker ids if it isn't already present. - -- If it is present in the list, use the project's existing issue tracker ids to ensure nothing is overwritten. - let udpatedIssueTrackerIds = case projectJiraKey of - Nothing -> Nothing - Just jiraKey -> Just . updateIssueTrackerIds jiraKey =<< currentIssueTrackerIds - let req = - UpdateProjectRequest - { updateProjectTitle = projectTitle - , updateProjectUrl = projectUrl - , updateProjectIssueTrackerIds = udpatedIssueTrackerIds - , updateProjectLabelIds = maybeLabelIds - , updateProjectPolicyId = maybePolicyId - , -- This field needs to be set, otherwise the default branch will be removed - updateProjectDefaultBranch = maybeDefaultBranch - } - res <- updateProject projectLocator req - logDebug $ "Updated project: " <> pretty (pShow res) + -- Updating the project's issue tracker ids is an all or nothing transaction. + -- Add the specified jira key to the current list of issue tracker ids if it isn't already present. + -- If it is present in the list, use the project's existing issue tracker ids to ensure nothing is overwritten. + let udpatedIssueTrackerIds = case projectJiraKey of + Nothing -> Nothing + Just jiraKey -> Just . updateIssueTrackerIds jiraKey =<< currentIssueTrackerIds + let req = + UpdateProjectRequest + { updateProjectTitle = projectTitle + , updateProjectUrl = projectUrl + , updateProjectIssueTrackerIds = udpatedIssueTrackerIds + , updateProjectLabelIds = maybeLabelIds + , updateProjectPolicyId = maybePolicyId + , -- This field needs to be set, otherwise the default branch will be removed + updateProjectDefaultBranch = maybeDefaultBranch + } + res <- updateProject projectLocator req + logDebug $ "Updated project: " <> pretty (pShow res) + pure maybeLabelWarnings + else pure Nothing updateIssueTrackerIds :: Text -> [Text] -> [Text] updateIssueTrackerIds jiraKey currentIssueTrackerIds = diff --git a/test/App/Fossa/ApiUtilsSpec.hs b/test/App/Fossa/ApiUtilsSpec.hs index aca8722103..814ab325c2 100644 --- a/test/App/Fossa/ApiUtilsSpec.hs +++ b/test/App/Fossa/ApiUtilsSpec.hs @@ -72,10 +72,13 @@ retrieveLabelIdsSpec = do let label2 = Label 2 "example-label-2" let labels = Labels [label1, label2] - it' "should return empty list when no label names exists Labels" $ do + it' "should return empty list and label warnings when no label names exists Labels" $ do res <- retrieveLabelIds ["non-existent-label", "non-existent-label-2"] labels - res `shouldBe'` [] + let warning1 = "Label `non-existent-label` does not exist\nNavigate to `Organization Settings` in the FOSSA web UI to create new labels: https://app.fossa.com/account/settings/organization" + warning2 = "Label `non-existent-label-2` does not exist\nNavigate to `Organization Settings` in the FOSSA web UI to create new labels: https://app.fossa.com/account/settings/organization" + expectedWarnings = Just [warning1, warning2] + res `shouldBe'` ([], expectedWarnings) it' "should return all labels" $ do res <- retrieveLabelIds ["example-label", "example-label-2"] labels - res `shouldBe'` [2, 1] + res `shouldBe'` ([2, 1], Nothing) diff --git a/test/App/Fossa/Configuration/ConfigurationSpec.hs b/test/App/Fossa/Configuration/ConfigurationSpec.hs index 842a47b559..ecc46c6ee7 100644 --- a/test/App/Fossa/Configuration/ConfigurationSpec.hs +++ b/test/App/Fossa/Configuration/ConfigurationSpec.hs @@ -85,6 +85,7 @@ expectedConfigProject = , configProjID = Just "github.com/fossa-cli" , configName = Just "fossa-cli" , configLink = Just "fossa.com" + , configTeam = Just "fossa-team" , configTeams = Just ["fossa-team"] , configJiraKey = Just "key" , configUrl = Just "fossa.com" diff --git a/test/App/Fossa/Configuration/testdata/valid-default-yaml/.fossa.yaml b/test/App/Fossa/Configuration/testdata/valid-default-yaml/.fossa.yaml index c0f3970548..3f03554518 100644 --- a/test/App/Fossa/Configuration/testdata/valid-default-yaml/.fossa.yaml +++ b/test/App/Fossa/Configuration/testdata/valid-default-yaml/.fossa.yaml @@ -7,6 +7,7 @@ project: locator: custom+1/github.com/fossa-cli id: github.com/fossa-cli name: fossa-cli + team: fossa-team teams: - fossa-team policy: license-policy diff --git a/test/App/Fossa/Configuration/testdata/valid-default/.fossa.yml b/test/App/Fossa/Configuration/testdata/valid-default/.fossa.yml index 0a51e70b8b..e28018be14 100644 --- a/test/App/Fossa/Configuration/testdata/valid-default/.fossa.yml +++ b/test/App/Fossa/Configuration/testdata/valid-default/.fossa.yml @@ -7,6 +7,7 @@ project: locator: custom+1/github.com/fossa-cli id: github.com/fossa-cli name: fossa-cli + team: fossa-team teams: - fossa-team policy: license-policy