From 48697dd63dedbb706b8c72cbe75d0b6acbebf451 Mon Sep 17 00:00:00 2001 From: Charles Coggins Date: Fri, 28 Jun 2024 14:46:44 -0500 Subject: [PATCH] feat: allow groups to be specified without a project This change allows users to specify a group without also specifying a corresponding project. Neither the project nor the group needs to exist beforehand. The group will now be created if it does not already exist. Additionally, the values for project and group are now allowed to be "mixed" between what is specified on the command line and anything found in a `.phylum_project` file. This "mixing" was specifically denied previously but allowed now with a warning to let users know that may be an unexpected combination. The group name is still optional and can be specified on the command line. It can also be specified in the `.phylum_project` file. The value specified with the option takes precedence when both are provided. Other changes made include: * Update documentation * Add an example of using the `--group` option to each integration * Format the Jenkins groovy samples to use 2-space tabs instead of 4 * Update `.phylum_project` file to use `depfiles` instead of `lockfiles` * Rename CLI exit code enum entry for group or project already existing * From `PROJECT_ALREADY_EXISTS` to `ALREADY_EXISTS` * Simplify some output messages * Use fewer words where possible * Be clear about when paid accounts are needed and provide info link --- .phylum_project | 4 +- docs/integrations/azure_pipelines.md | 4 + docs/integrations/bitbucket_pipelines.md | 4 + docs/integrations/git_precommit.md | 4 + docs/integrations/gitlab_ci.md | 4 + docs/integrations/jenkins.md | 330 ++++++++++++----------- src/phylum/ci/ci_base.py | 139 +++++++--- src/phylum/ci/cli.py | 4 +- src/phylum/ci/common.py | 4 +- 9 files changed, 299 insertions(+), 198 deletions(-) diff --git a/.phylum_project b/.phylum_project index d6c540b3..feb63ef4 100644 --- a/.phylum_project +++ b/.phylum_project @@ -1,7 +1,7 @@ id: 56f7f1b0-7f63-47a4-9f5e-8194772b2e13 name: phylum-ci -created_at: 2023-12-07T16:29:55.527161-06:00 +created_at: 2024-06-28T10:11:12.990575-05:00 group_name: phylum_bot -lockfiles: +depfiles: - path: ./poetry.lock type: poetry diff --git a/docs/integrations/azure_pipelines.md b/docs/integrations/azure_pipelines.md index ca0d0a66..595e1493 100644 --- a/docs/integrations/azure_pipelines.md +++ b/docs/integrations/azure_pipelines.md @@ -315,6 +315,10 @@ view the [script options output][script_options] for the latest release. # for *workspace* manifest files where there is no companion lockfile (e.g., libraries). - script: phylum-ci --force-analysis --all-deps --depfile Cargo.toml + # Perform analysis as part of a group-owned project. + # A paid account is needed to use groups: https://phylum.io/pricing + - script: phylum-ci --group my_group + # Analyze all dependencies in audit mode, to gain insight without failing builds. - script: phylum-ci --all-deps --audit diff --git a/docs/integrations/bitbucket_pipelines.md b/docs/integrations/bitbucket_pipelines.md index 7ed0bc1d..6f54a9a2 100644 --- a/docs/integrations/bitbucket_pipelines.md +++ b/docs/integrations/bitbucket_pipelines.md @@ -313,6 +313,10 @@ view the [script options output][script_options] for the latest release. # for *workspace* manifest files where there is no companion lockfile (e.g., libraries). - phylum-ci --force-analysis --all-deps --depfile Cargo.toml + # Perform analysis as part of a group-owned project. + # A paid account is needed to use groups: https://phylum.io/pricing + - phylum-ci --group my_group + # Analyze all dependencies in audit mode, to gain insight without failing builds. - phylum-ci --all-deps --audit diff --git a/docs/integrations/git_precommit.md b/docs/integrations/git_precommit.md index 02f7afc0..597113f7 100644 --- a/docs/integrations/git_precommit.md +++ b/docs/integrations/git_precommit.md @@ -153,6 +153,10 @@ with `--help` output as specified in the [Usage section of the top-level README. # for *workspace* manifest files where there is no companion lockfile (e.g., libraries). args: [--force-analysis, --all-deps, --depfile=Cargo.toml] + # Perform analysis as part of a group-owned project. + # A paid account is needed to use groups: https://phylum.io/pricing + args: [--group, my_group] + # Ensure the latest Phylum CLI is installed. args: [--force-install] diff --git a/docs/integrations/gitlab_ci.md b/docs/integrations/gitlab_ci.md index 7ab031b7..3503b30d 100644 --- a/docs/integrations/gitlab_ci.md +++ b/docs/integrations/gitlab_ci.md @@ -301,6 +301,10 @@ view the [script options output][script_options] for the latest release. # for *workspace* manifest files where there is no companion lockfile (e.g., libraries). - phylum-ci --force-analysis --all-deps --depfile Cargo.toml + # Perform analysis as part of a group-owned project. + # A paid account is needed to use groups: https://phylum.io/pricing + - phylum-ci --group my_group + # Analyze all dependencies in audit mode, to gain insight without failing builds. - phylum-ci --all-deps --audit diff --git a/docs/integrations/jenkins.md b/docs/integrations/jenkins.md index 6f22ce5b..734da8ca 100644 --- a/docs/integrations/jenkins.md +++ b/docs/integrations/jenkins.md @@ -8,25 +8,25 @@ The credential ID should be specified since it will be used in the `Jenkinsfile` 1. Add the following "Phylum" stage to an existing `Jenkinsfile` declarative pipeline configuration: ```groovy - stage('Phylum') { - agent { - docker { - image 'phylumio/phylum-ci:latest' - alwaysPull true - } - } - environment { - // The environment variable must be named like - // this but the credential ID can be different. - PHYLUM_API_KEY = credentials('phylum-token') - } - steps { - // The `--force-analysis` and `--all-deps` flags - // are needed because this configuration has no - // history available for computing changes. - sh 'phylum-ci -vv --force-analysis --all-deps' - } + stage('Phylum') { + agent { + docker { + image 'phylumio/phylum-ci:latest' + alwaysPull true } + } + environment { + // The environment variable must be named like + // this but the credential ID can be different. + PHYLUM_API_KEY = credentials('phylum-token') + } + steps { + // The `--force-analysis` and `--all-deps` flags + // are needed because this configuration has no + // history available for computing changes. + sh 'phylum-ci -vv --force-analysis --all-deps' + } + } ``` > ⚠️ **INFO** ⚠️ @@ -78,46 +78,46 @@ pipeline configuration: ```groovy pipeline { - agent none - stages { - stage('Phylum') { - agent { - docker { - image 'phylumio/phylum-ci:latest' - alwaysPull true - } - } - environment { - PHYLUM_API_KEY = credentials('phylum-token') - } - options { - // This is optional but may save time since - // a full checkout is needed later. - skipDefaultCheckout() - } - steps { - checkout scmGit( - branches: [[name: '**']], - extensions: [cleanBeforeCheckout()], - // Change to match your repository URL and creds. - userRemoteConfigs: [[ - credentialsId: 'CHANGEME', - url: 'https://github.com/CHANGEME/CHANGEME.git' - ]] - ) - withCredentials([gitUsernamePassword(credentialsId: 'CHANGEME', gitToolName: 'Default')]) { - sh 'phylum-ci -vv' - } - } - post { - always { - // Cleaning the workspace ensures the git - // checkout is valid for future runs. - cleanWs() - } - } + agent none + stages { + stage('Phylum') { + agent { + docker { + image 'phylumio/phylum-ci:latest' + alwaysPull true + } + } + environment { + PHYLUM_API_KEY = credentials('phylum-token') + } + options { + // This is optional but may save time since + // a full checkout is needed later. + skipDefaultCheckout() + } + steps { + checkout scmGit( + branches: [[name: '**']], + extensions: [cleanBeforeCheckout()], + // Change to match your repository URL and creds. + userRemoteConfigs: [[ + credentialsId: 'CHANGEME', + url: 'https://github.com/CHANGEME/CHANGEME.git' + ]] + ) + withCredentials([gitUsernamePassword(credentialsId: 'CHANGEME', gitToolName: 'Default')]) { + sh 'phylum-ci -vv' + } + } + post { + always { + // Cleaning the workspace ensures the git + // checkout is valid for future runs. + cleanWs() } + } } + } } ``` @@ -146,23 +146,23 @@ anymore...use the SHA256 digest of the tag. The digest can be found by looking a For instance, at the time of this writing, all of these tag references pointed to the same image: ```groovy - agent { - docker { - // NOTE: These are examples. Only one image line for `phylum-ci` is expected. - // - // Not specifying a tag means a default of `latest` - image 'phylumio/phylum-ci' - // Be more explicit about wanting the `latest` tag - image 'phylumio/phylum-ci:latest' - // Use a specific release version of the `phylum-ci` package - image 'phylumio/phylum-ci:0.42.4-CLIv6.1.2' - // Use a specific image with it's SHA256 digest - image 'phylumio/phylum-ci@sha256:77b761ccef10edc28b0f009a40fbeab240bf004522edaaea05572dc3728b6ca6' - - // This option is useful when image is NOT specified by hash - alwaysPull true - } - } + agent { + docker { + // NOTE: These are examples. Only one image line for `phylum-ci` is expected. + // + // Not specifying a tag means a default of `latest` + image 'phylumio/phylum-ci' + // Be more explicit about wanting the `latest` tag + image 'phylumio/phylum-ci:latest' + // Use a specific release version of the `phylum-ci` package + image 'phylumio/phylum-ci:0.42.4-CLIv6.1.2' + // Use a specific image with it's SHA256 digest + image 'phylumio/phylum-ci@sha256:77b761ccef10edc28b0f009a40fbeab240bf004522edaaea05572dc3728b6ca6' + + // This option is useful when image is NOT specified by hash + alwaysPull true + } + } ``` Only the last tag reference, by SHA256 digest, is guaranteed to not have the underlying image it points to change. @@ -184,19 +184,19 @@ manifest files are present and/or **only** lockfiles are used. Here are examples of using the slim image tags: ```groovy - agent { - docker { - // NOTE: These are examples. Only one image line for `phylum-ci` is expected. - // - // Use the most current release of *both* `phylum-ci` and the Phylum CLI - image 'phylumio/phylum-ci:slim' - // Use the `slim` image with a specific release version of `phylum-ci` and Phylum CLI - image 'phylumio/phylum-ci:0.42.4-CLIv6.1.2-slim' - - // This option is useful when image is NOT specified by hash - alwaysPull true - } - } + agent { + docker { + // NOTE: These are examples. Only one image line for `phylum-ci` is expected. + // + // Use the most current release of *both* `phylum-ci` and the Phylum CLI + image 'phylumio/phylum-ci:slim' + // Use the `slim` image with a specific release version of `phylum-ci` and Phylum CLI + image 'phylumio/phylum-ci:0.42.4-CLIv6.1.2-slim' + + // This option is useful when image is NOT specified by hash + alwaysPull true + } + } ``` See the documentation for [using Docker with Pipeline][docker_pipeline] more information. @@ -216,11 +216,11 @@ higher. The value for this variable is sensitive and should be set as a secret t **Care should be taken to protect it appropriately**. ```groovy - environment { - // Variable must be named `PHYLUM_API_KEY` - // but the credential ID can be different. - PHYLUM_API_KEY = credentials('phylum-token') - } + environment { + // Variable must be named `PHYLUM_API_KEY` + // but the credential ID can be different. + PHYLUM_API_KEY = credentials('phylum-token') + } ``` [user_vars]: https://www.jenkins.io/doc/book/pipeline/jenkinsfile/#setting-environment-variables @@ -233,19 +233,19 @@ repository is required to ensure that the local working copy is always pristine requested information. ```groovy - steps { - // A full checkout is needed to provide history and proper diffs. - // A `checkout scm` step is not enough here. - checkout scmGit( - branches: [[name: '**']], - extensions: [cleanBeforeCheckout()], - // Change to match your repository URL and creds. - userRemoteConfigs: [[ - credentialsId: 'CHANGEME', - url: 'https://github.com/CHANGEME/CHANGEME.git' - ]] - ) - } + steps { + // A full checkout is needed to provide history and proper diffs. + // A `checkout scm` step is not enough here. + checkout scmGit( + branches: [[name: '**']], + extensions: [cleanBeforeCheckout()], + // Change to match your repository URL and creds. + userRemoteConfigs: [[ + credentialsId: 'CHANGEME', + url: 'https://github.com/CHANGEME/CHANGEME.git' + ]] + ) + } ``` See the [checkout step][checkout_step] and [checkout plugin][scm_plugin] documentation for more information. @@ -265,61 +265,65 @@ release. [script_options]: https://github.com/phylum-dev/phylum-ci/blob/main/docs/script_options.md ```groovy - steps { - // This block is needed for change detection and using - // authenticated git commands. Change the `credentialsId`. - withCredentials([gitUsernamePassword(credentialsId: 'CHANGEME', gitToolName: 'Default')]) { - // NOTE: These are examples. Only one `phylum-ci` entry is expected. - // - // Use the defaults for all the arguments. - // The default behavior is to only analyze newly added dependencies - // against the active policy set at the Phylum project level. - sh 'phylum-ci' - - // Provide debug level output. Highly recommended. - sh 'phylum-ci -vv' - - // Consider all dependencies in analysis results instead of just the newly added ones. - // The default is to only analyze newly added dependencies, which can be useful for - // existing code bases that may not meet established policy rules yet, - // but don't want to make things worse. Specifying `--all-deps` can be useful for - // casting the widest net for strict adherence to Quality Assurance (QA) standards. - sh 'phylum-ci --all-deps' - - // Some lockfile types (e.g., Python/pip `requirements.txt`) are ambiguous in that - // they can be named differently and may or may not contain strict dependencies. - // In these cases it is best to specify an explicit path, either with the `--depfile` - // option or in a `.phylum_project` file. The easiest way to do that is with the - // Phylum CLI, using `phylum init` command (docs.phylum.io/cli/commands/phylum_init) - // and committing the generated `.phylum_project` file. - sh 'phylum-ci --depfile requirements-prod.txt' - - // Specify multiple explicit dependency file paths. - sh 'phylum-ci --depfile requirements-prod.txt Cargo.toml path/to/dependency.file' - - // Force analysis for all dependencies in a manifest file. This is especially useful - // for *workspace* manifest files where there is no companion lockfile (e.g., libraries). - sh 'phylum-ci --force-analysis --all-deps --depfile Cargo.toml' - - // Analyze all dependencies in audit mode, to gain insight without failing builds. - sh 'phylum-ci --all-deps --audit' - - // Ensure the latest Phylum CLI is installed. - sh 'phylum-ci --force-install' - - // Install a specific version of the Phylum CLI. - sh 'phylum-ci --phylum-release 6.4.0 --force-install' - - // Mix and match for your specific use case. - sh 'phylum-ci \ - -vv \ - --depfile requirements-dev.txt \ - --depfile requirements-prod.txt path/to/dependency.file \ - --depfile Cargo.toml \ - --force-analysis \ - --all-deps' - } - } + steps { + // This block is needed for change detection and using + // authenticated git commands. Change the `credentialsId`. + withCredentials([gitUsernamePassword(credentialsId: 'CHANGEME', gitToolName: 'Default')]) { + // NOTE: These are examples. Only one `phylum-ci` entry is expected. + // + // Use the defaults for all the arguments. + // The default behavior is to only analyze newly added dependencies + // against the active policy set at the Phylum project level. + sh 'phylum-ci' + + // Provide debug level output. Highly recommended. + sh 'phylum-ci -vv' + + // Consider all dependencies in analysis results instead of just the newly added ones. + // The default is to only analyze newly added dependencies, which can be useful for + // existing code bases that may not meet established policy rules yet, + // but don't want to make things worse. Specifying `--all-deps` can be useful for + // casting the widest net for strict adherence to Quality Assurance (QA) standards. + sh 'phylum-ci --all-deps' + + // Some lockfile types (e.g., Python/pip `requirements.txt`) are ambiguous in that + // they can be named differently and may or may not contain strict dependencies. + // In these cases it is best to specify an explicit path, either with the `--depfile` + // option or in a `.phylum_project` file. The easiest way to do that is with the + // Phylum CLI, using `phylum init` command (docs.phylum.io/cli/commands/phylum_init) + // and committing the generated `.phylum_project` file. + sh 'phylum-ci --depfile requirements-prod.txt' + + // Specify multiple explicit dependency file paths. + sh 'phylum-ci --depfile requirements-prod.txt Cargo.toml path/to/dependency.file' + + // Force analysis for all dependencies in a manifest file. This is especially useful + // for *workspace* manifest files where there is no companion lockfile (e.g., libraries). + sh 'phylum-ci --force-analysis --all-deps --depfile Cargo.toml' + + // Perform analysis as part of a group-owned project. + // A paid account is needed to use groups: https://phylum.io/pricing + sh 'phylum-ci --group my_group' + + // Analyze all dependencies in audit mode, to gain insight without failing builds. + sh 'phylum-ci --all-deps --audit' + + // Ensure the latest Phylum CLI is installed. + sh 'phylum-ci --force-install' + + // Install a specific version of the Phylum CLI. + sh 'phylum-ci --phylum-release 6.4.0 --force-install' + + // Mix and match for your specific use case. + sh 'phylum-ci \ + -vv \ + --depfile requirements-dev.txt \ + --depfile requirements-prod.txt path/to/dependency.file \ + --depfile Cargo.toml \ + --force-analysis \ + --all-deps' + } + } ``` ## Alternatives @@ -330,10 +334,10 @@ or "standalone" pipeline configuration, the solution is to force analysis of all is needed. This is done with the following flags: ```groovy - steps { - // Force analysis for all current dependencies. - sh 'phylum-ci --force-analysis --all-deps' - } + steps { + // Force analysis for all current dependencies. + sh 'phylum-ci -vv --force-analysis --all-deps' + } ``` It is also possible to make direct use of the [`phylum` Python package][pypi] within CI. diff --git a/src/phylum/ci/ci_base.py b/src/phylum/ci/ci_base.py index ba9e1ec5..f8aafd15 100644 --- a/src/phylum/ci/ci_base.py +++ b/src/phylum/ci/ci_base.py @@ -316,36 +316,54 @@ def phylum_project(self) -> str: LOG.debug("Project name provided as argument: %s", project_name) return project_name - LOG.info("Project name not provided as argument. Checking the `.phylum_project` file ...") + LOG.info("Project name not provided as argument. Checking `.phylum_project` file ...") project_name = self._project_settings.get("project") if project_name: LOG.debug("Project name provided in `.phylum_project` file: %s", project_name) return project_name - LOG.info("Project name not found in the `.phylum_project` file or file does not exist. Detecting instead ...") + LOG.info("Project name not found in `.phylum_project` file or file does not exist. Detecting instead ...") project_name = git_repo_name() LOG.debug("Project name detected from git repository name: %s", project_name) return project_name - @property + @cached_property def phylum_group(self) -> Optional[str]: """Get the effective Phylum group in use. The Phylum group name can be specified as an option or contained in the `.phylum_project` file. A group name provided as an input option will be preferred over an entry in the `.phylum_project` file. + Generate a warning when the possibility of unintended project/group pairings exist. This happens when + one of a project or group (but not both) is explicitly specified by argument and the other is specified + in the `.phylum_project` file. + Return `None` when the group name is not available. """ - # Group supplied on command-line - # A group can not be specified without a project - if self.args.group and self.args.project: - return self.args.group - - # Group supplied in `.phylum_project` - # Don't "mix" a project supplied as an option with a group that wasn't - if not self.args.project: - return self._project_settings.get("group") + group_name = self.args.group + if group_name: + LOG.debug("Group name provided as argument: %s", group_name) + if not self.args.project and self._project_file_already_existed: + msg = """ + Group name was explicitly specified but without a matching + project argument. This can result in creation of an unexpected + project/group pairing. Please check if this was intended.""" + LOG.warning(cleandoc(msg)) + return group_name + + LOG.debug("Group name not provided as argument. Checking `.phylum_project` file...") + group_name = self._project_settings.get("group") + if group_name: + LOG.debug("Group name provided in `.phylum_project` file: %s", group_name) + if self.args.project: + msg = """ + Project name was explicitly specified but without a matching + group argument. This can result in creation of an unexpected + project/group pairing. Please check if this was intended.""" + LOG.warning(cleandoc(msg)) + return group_name + LOG.debug("Group name not found in `.phylum_project` file or file does not exist. Assuming no group ...") return None @cached_property @@ -489,10 +507,10 @@ def update_depfiles_change_status(self, commit: str, err_msg: Optional[str] = No cmd = ["git", "diff", "--exit-code", "--quiet", commit, "--", str(depfile.path)] ret = subprocess.run(cmd, check=False) # noqa: S603 if ret.returncode == 0: - LOG.debug("The dependency file [code]%r[/] has [b]NOT[/] changed", depfile, extra=MARKUP) + LOG.debug("Dependency file [code]%r[/] has [b]NOT[/] changed", depfile, extra=MARKUP) depfile.is_depfile_changed = False elif ret.returncode == 1: - LOG.debug("The dependency file [code]%r[/] has changed", depfile, extra=MARKUP) + LOG.debug("Dependency file [code]%r[/] has changed", depfile, extra=MARKUP) depfile.is_depfile_changed = True else: if err_msg: @@ -526,10 +544,10 @@ def _ensure_project_exists(self) -> None: A project may or may not already exist. Attempt to create the project, possibly overwriting a `.phylum_project` file that already exists. Continue on without error when the specified project already exists. """ - LOG.info("Attempting to create a Phylum project with the name: %s ...", self.phylum_project) + LOG.info("Attempting to create a Phylum project with name: %s ...", self.phylum_project) cmd = [str(self.cli_path), "project", "create"] if self.phylum_group: - LOG.debug("Using Phylum group: %s", self.phylum_group) + LOG.info("Using Phylum group: %s", self.phylum_group) cmd.extend(["--group", self.phylum_group]) if self.repo_url: LOG.debug("Using repository URL: %s", self.repo_url) @@ -537,25 +555,86 @@ def _ensure_project_exists(self) -> None: cmd.append(self.phylum_project) if not Path.cwd().joinpath(".git").is_dir(): LOG.warning("Attempting to create a Phylum project outside the top level of a `git` repository") + try: subprocess.run(cmd, check=True, capture_output=True, text=True) # noqa: S603 - except subprocess.CalledProcessError as err: - # The Phylum CLI will return a unique error code when a project that already - # exists is attempted to be created. This situation is recognized and allowed to happen - # since it means the project exists as expected. Any other exit code is an error. - if err.returncode == CLIExitCode.PROJECT_ALREADY_EXISTS.value: - LOG.info("Project %s already exists. Continuing with it ...", self.phylum_project) + except subprocess.CalledProcessError as outer_err: + # The Phylum CLI will return a unique error code when a project/group pairing + # that already exists is attempted to be created. This situation is recognized + # and allowed to happen since it means the project exists as expected. + project_exists_msg = f""" + Project/group pairing already exists. Continuing with it. + Project: {self.phylum_project} + Group: {self.phylum_group or '(no group)'}""" + if outer_err.returncode == CLIExitCode.ALREADY_EXISTS.value: + LOG.info(cleandoc(project_exists_msg)) self._set_repo_url() return - msg = """ - There was a problem creating the project. - A PRO account is needed to create a project with a group. + + err_msg = """ + There was a problem creating the project. A paid account is needed to + use groups or create more than five projects: https://phylum.io/pricing If the command was expected to succeed, please report this as a bug.""" - raise PhylumCalledProcessError(err, cleandoc(msg)) from err - LOG.info("Project %s created successfully", self.phylum_project) + + # The problem may be that a group was specified but does not exist yet. Check for that case and create it, + # if needed. This is done here instead of pre-emptively in an effort to avoid extra group creation calls. + if not self._created_group(): + # A missing group was not the problem, which means the project creation attempt is an error. + raise PhylumCalledProcessError(outer_err, cleandoc(err_msg)) from outer_err + + # A missing group was created, which means we need to try to create the project again. + try: + subprocess.run(cmd, check=True, capture_output=True, text=True) # noqa: S603 + except subprocess.CalledProcessError as inner_err: + # Check for this error code again because it is possible the same + # project/group pairing was created elsewhere since the last check. + if inner_err.returncode == CLIExitCode.ALREADY_EXISTS.value: + LOG.info(cleandoc(project_exists_msg)) + self._set_repo_url() + return + # Any other exit code is an error. + raise PhylumCalledProcessError(inner_err, cleandoc(err_msg)) from inner_err + + project_created_msg = f""" + Project/group pairing created successfully. + Project: {self.phylum_project} + Group: {self.phylum_group or '(no group)'}""" + LOG.info(cleandoc(project_created_msg)) if self._project_file_already_existed: LOG.warning("Overwrote previous `.phylum_project` file found at: %s", self._phylum_project_file) + def _created_group(self) -> bool: + """Ensure a Phylum group is created and in place, when specified. + + A group may or may not already exist. Attempt to create the group when one is specified. + Continue on without error when the specified group already exists. + + Return True if a group was created and False otherwise. + """ + if not self.phylum_group: + LOG.debug("No Phylum group specified. Nothing to do.") + return False + + LOG.info("Attempting to create a Phylum group with name: %s ...", self.phylum_group) + cmd = [str(self.cli_path), "group", "create", self.phylum_group] + try: + subprocess.run(cmd, check=True, capture_output=True, text=True) # noqa: S603 + except subprocess.CalledProcessError as err: + # The Phylum CLI will return a unique error code when a group that already + # exists is attempted to be created. This situation is recognized and allowed to happen + # since it means the group exists as expected. Any other exit code is an error. + if err.returncode == CLIExitCode.ALREADY_EXISTS.value: + LOG.info("Group %s already exists. Continuing with it ...", self.phylum_group) + return False + msg = """ + There was a problem creating the group. + A paid account is needed to use groups: https://phylum.io/pricing + If the command was expected to succeed, please report this as a bug.""" + raise PhylumCalledProcessError(err, cleandoc(msg)) from err + + LOG.info("Successfully created group: %s", self.phylum_group) + return True + def _set_repo_url(self) -> None: """Set the repository URL for the project. @@ -698,7 +777,7 @@ def analyze(self) -> None: else: msg = """ There was a problem analyzing the project. - A PRO account is needed to use groups. + A paid account is needed to use groups: https://phylum.io/pricing If the command was expected to succeed, please report this as a bug.""" raise PhylumCalledProcessError(err, cleandoc(msg)) from err @@ -757,10 +836,10 @@ def _parse_analysis_result(self, analysis_result: str) -> None: # https://github.com/phylum-dev/phylum-ci/issues/357 if analysis.incomplete_count == 0: if analysis.is_failure: - LOG.error("The analysis is complete and there were failures") + LOG.error("Analysis is complete and there were failures") self.returncode = ReturnCode.POLICY_FAILURE else: - LOG.info("The analysis is complete and there were NO failures") + LOG.info("Analysis is complete and there were NO failures") self.returncode = ReturnCode.SUCCESS elif analysis.is_failure: LOG.error("There were failures in one or more completed packages") diff --git a/src/phylum/ci/cli.py b/src/phylum/ci/cli.py index f99c2cc6..34bd55ec 100644 --- a/src/phylum/ci/cli.py +++ b/src/phylum/ci/cli.py @@ -151,7 +151,9 @@ def get_args(args: Optional[Sequence[str]] = None) -> tuple[argparse.Namespace, analysis_group.add_argument( "-g", "--group", - help="Optional group name, which will be the owner of the project. Only used when a project is also specified.", + help="""Optional group name, which will be the owner of the project. Can also specify this option's value in the + `.phylum_project` file. The value specified with this option takes precedence when both are provided. Group + will be created if it does not already exist. Groups require a paid account: https://phylum.io/pricing""", ) analysis_group.add_argument( "-s", diff --git a/src/phylum/ci/common.py b/src/phylum/ci/common.py index 3595988e..587f21f0 100644 --- a/src/phylum/ci/common.py +++ b/src/phylum/ci/common.py @@ -116,8 +116,8 @@ class ReturnCode(IntEnum): class CLIExitCode(IntEnum): """Integer enumeration to track the Phylum CLI exit codes.""" - # A project that already exists is attempted to be created - PROJECT_ALREADY_EXISTS = 14 + # A project or group that already exists is attempted to be created + ALREADY_EXISTS = 14 # A manifest is attempted to be parsed but lockfile generation has been disabled MANIFEST_WITHOUT_GENERATION = 20