From 3fdb0cf22cc2135517afb609715a8a7de72c8ea4 Mon Sep 17 00:00:00 2001 From: Mikail Kocak Date: Fri, 27 Sep 2024 15:02:17 +0200 Subject: [PATCH 1/5] Add license checker reusable workflow This adds a reusable GitHub Workflow that checks licenses of the caller's repository. It works as follows: 1. It generates an SBOM using `./sbom-generator/action.yaml` (cdxgen + fetches licenses from NPM and PyPI) 2. It analyzes the SBOM using grant (`./grant-license-checker/action.yaml`) 3. It sends the summary as a pull request comment (edits the comment if it already commented previously) --- .github/workflows/run-license-check.yaml | 250 +++++++++++++++++++++ .github/workflows/self-check-licenses.yaml | 26 +++ README.md | 23 +- 3 files changed, 298 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/run-license-check.yaml create mode 100644 .github/workflows/self-check-licenses.yaml diff --git a/.github/workflows/run-license-check.yaml b/.github/workflows/run-license-check.yaml new file mode 100644 index 0000000..0eeab48 --- /dev/null +++ b/.github/workflows/run-license-check.yaml @@ -0,0 +1,250 @@ +# This reusable workflow checks and summarizes licenses of the caller's repository. +# +# Works as follows: +# +# 1. It generates an SBOM using ./sbom-generator/action.yaml +# (cdxgen + fetches licenses from NPM, PyPI, etc.) +# 2. It analyzes the SBOM using grant (./grant-license-checker/action.yaml) +# 3. It sends the summary as a pull request comment +# (edits the comment if it already commented previously) +name: Analyze Dependencies Licenses + +on: + workflow_call: + inputs: + rules: + type: string + default: | + - name: default-allow-all + reason: No configuration provided, defaulted to 'allow all.' + pattern: '*' + mode: allow + description: >- + A list of grant YAML rules (default: deny all GPL licenses). + More details at: https://github.com/anchore/grant/blob/v0.2.1/README.md#usage. + +permissions: + contents: read + +jobs: + # 1. Generate an SBOM using cdxgen (CycloneDX generator), + # and fetches licenses from PyPI, NPM, etc. + # 2. Outputs the SBOM JSON file as an artifact. + generate-sbom: + name: Generate SBOM and Fetch Licenses + runs-on: ubuntu-22.04 + + permissions: + contents: read + + steps: + # Clone the invoker's repository. + - name: Checkout Caller's Code + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + + # When running inside the same repository as the action, + # use the action from the same branch. + # This allows us to test our workflows in pull requests. + - if: ${{ endsWith(github.repository, '/saleor-internal-actions') }} + name: Generate SBOM + uses: ./sbom-generator + with: + sbom_path: "./bom.json" + ecosystems: >- + python + javascript + + # When not running inside the same repository as the action, + # use the action version as we cannot access our files inside + # reusable workflows. + - if: ${{ !endsWith(github.repository, '/saleor-internal-actions') }} + name: Generate SBOM + uses: saleor/saleor-internal-actions/sbom-generator@main + with: + sbom_path: "./bom.json" + ecosystems: >- + python + javascript + + # Send the results to the next job ('analyze-licenses'). + - name: Upload Results + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + with: + name: Generated SBOM with Licenses + path: ./bom.json + + # 1. Takes an SBOM as input (artifact), + # 2. Analyzes it using grant and sends a summary as HTML (error if any + # rules are violated), + # 3. Outputs the HTML and JSON results as an artifact. + analyze-licenses: + name: Analyze Licenses + runs-on: ubuntu-22.04 + + needs: + - generate-sbom + + permissions: + contents: read + + steps: + # We cannot detect the GitHub repository, and git ref of + # the invoked workflow (this file) in pull requests due to not having access + # to id_token permission. + # Feature request: https://github.com/orgs/community/discussions/31054 + # + # The code checkout is needed to be able to use the actions from the + # current branch of 'saleor-internal-actions'. We do not check the owner in order + # to allow forks to work as well. + # + # `github.repository` is `/` + - if: ${{ endsWith(github.repository, '/saleor-internal-actions') }} + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + + - name: Download SBOM + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + name: Generated SBOM with Licenses + path: ./bom.json + + # Prepends the dependency group to the dependency name + # (as: ".group" + "/" + ".name") + # + # This is needed due to a bug in Syft that leads 'grant' to not handle + # properly dependency groups (e.g., NPM's '@org/pkg-name'). + # + # Example: the NPM package '@types/node' would be reported by 'grant' as + # having for name 'node' whereas 'node' and '@types/node` are two + # (totally) unrelated packages. + # + # Ticket: https://github.com/anchore/syft/issues/1202 + - name: Prepend Dependency Group + run: | + set -eu + + # When .group is not blank, preprend .group into .name (using a slash (/)) + jq ' + ( + .components[] | select ( .group != "" ) + ) |= ( + . + { name: (.group + "/" + .name) } + ) + ' bom.json > fixed.bom.json + + mv fixed.bom.json bom.json + + - id: license-analyzer-self + if: ${{ endsWith(github.repository, '/saleor-internal-actions') }} + name: Analyze Licenses + uses: ./grant-license-checker + with: + rules: ${{ inputs.rules }} + sbom_path: ./bom.json + + - id: license-analyzer-workflow-call + if: ${{ !endsWith(github.repository, '/saleor-internal-actions') }} + name: Analyze Licenses + uses: saleor/saleor-internal-actions/grant-license-checker@main + with: + rules: ${{ inputs.rules }} + sbom_path: ./bom.json + + - name: Upload Analysis Results + if: ${{ success() || ( failure() && steps.license-checker.conclusion == 'failure' ) }} + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + with: + name: Grant Analysis Results + path: >- + ${{ + steps.license-analyzer-self.outputs.results_dir_path + || steps.license-analyzer-workflow-call.outputs.results_dir_path + }} + + # 1. Takes a grant HTML result as input, + # 2. Send a GitHub pull request comment with the HTML contents. + report: + name: Report Summary + runs-on: ubuntu-22.04 + + needs: + - analyze-licenses + + permissions: + contents: read + pull-requests: write + + env: + HTML_FILE_PATH: ./results/grant.html + PR_NUMBER: ${{ github.event.pull_request.number }} + # There could be multiple comments with the author 'github-action[bot]', + # thus we need to look for an identifier that demonstrates we are + # updating the right bot comment. + COMMENT_SUFFIX: "" + + steps: + - name: Download Analysis Results + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + name: Grant Analysis Results + path: ./results + + - name: Validate HTML Report + shell: bash + run: | + # Append into the file the comment suffix, + # but ensure it exists in order to prevent sending empty comments + # if there is a bug. + if [ -f "$HTML_FILE_PATH" ]; then + printf "%s\n" "$COMMENT_SUFFIX" >> "$HTML_FILE_PATH" + else + printf "Did not find find HTML result file: %s\n" "$HTML_FILE_PATH" + exit 1 + fi + + # Validate length doesn't not exceed GitHub limits. + body_length=$(wc -m "$HTML_FILE_PATH" | cut -d' ' -f1) + if [ "$body_length" -gt 65536 ]; then + printf "Comment body is too long, aborting..." >&2 + exit 1 + fi + + - name: Find Previous Comment + uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0 + id: find-previous-comment + with: + issue-number: ${{ env.PR_NUMBER }} + comment-author: github-actions[bot] + body-includes: ${{ env.COMMENT_SUFFIX }} + + - name: Post Summary + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + OLD_COMMENT_ID: ${{ steps.find-previous-comment.outputs.comment-id }} + run: | + endpoint="/repos/${GITHUB_REPOSITORY}/issues" + + cmd_args=() + + # Run 'gh api' comment silently if runner is not in debug mode. + test -n "${RUNNER_DEBUG+x}" || cmd_args+=( "--silent" ) + + if [ -z "$OLD_COMMENT_ID" ]; then + # Didn't find a comment, create one. + method=POST + endpoint="${endpoint}/${PR_NUMBER}/comments" + else + # Update the comment + method=PATCH + endpoint="${endpoint}/comments/${OLD_COMMENT_ID}" + fi + + printf "Creating or updating comment at %s (%s)\n" "$endpoint" "$method" >&2 + + gh api \ + --method "$method" \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "$endpoint" \ + -F "body=@$HTML_FILE_PATH" \ + "${cmd_args[@]}" diff --git a/.github/workflows/self-check-licenses.yaml b/.github/workflows/self-check-licenses.yaml new file mode 100644 index 0000000..096484b --- /dev/null +++ b/.github/workflows/self-check-licenses.yaml @@ -0,0 +1,26 @@ +# This workflow only checks the licenses of this repository +# (saleor/saleor-internal-actions). +name: Check Licenses +on: + pull_request: + types: + - opened + - synchronize + paths: + # Python Ecosystem + - "**/pyproject.toml" + - "**/setup.py" + - "**/requirements*.txt" + - "**/Pipfile.lock" + - "**/poetry.lock" + # JS/TS Ecosystem + - "**/package.json" + - "**/pnpm-lock.yaml" + - "**/package-lock.json" + +jobs: + default: + permissions: + contents: read + pull-requests: write + uses: ./.github/workflows/run-license-check.yaml diff --git a/README.md b/README.md index 6636ec2..339e5d6 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,28 @@ -# saleor-actions +# saleor-internal-actions This is a collection of GitHub Actions created and used by Saleor internally. > [!WARNING] > This project is solely intended for internal use within the Saleor organization. > It is provided as is, breaking changes may be published without notice, and may not be compatible for specific use cases. + +## Actions + +| Name | Description | +|------------------------------------------------|-------------------------------------------------------------------| +| [sbom-generator](sbom-generator) | Generates a CycloneDX SBOM with license fetching enabled. | +| [grant-license-checker](grant-license-checker) | Generates a license usage report of a given SBOM using [`grant`]. | + + +## Reusable Workflows + +| Name | Description | +|--------------------------------------------------------------------|------------------------------------------------------------------------------------| +| [run-license-check.yaml](.github/workflows/run-license-check.yaml) | Summarizes the list of licenses as a pull request comment (by generating an SBOM.) | + + +## Development + +Refer to [CONTRIBUTING.md](CONTRIBUTING.md). + +[`grant`]: https://github.com/anchore/grant From 03fac7913d379c61efd358dbd88bb8af117c349f Mon Sep 17 00:00:00 2001 From: Mikail Kocak Date: Fri, 27 Sep 2024 15:31:40 +0200 Subject: [PATCH 2/5] Expose ecosystem list as input value --- .github/workflows/run-license-check.yaml | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/.github/workflows/run-license-check.yaml b/.github/workflows/run-license-check.yaml index 0eeab48..301d08f 100644 --- a/.github/workflows/run-license-check.yaml +++ b/.github/workflows/run-license-check.yaml @@ -22,6 +22,20 @@ on: description: >- A list of grant YAML rules (default: deny all GPL licenses). More details at: https://github.com/anchore/grant/blob/v0.2.1/README.md#usage. + # The ecosystems to scan, scans all by default. + # + # See https://cyclonedx.github.io/cdxgen/#/PROJECT_TYPES for the supported + # ecosystems ("Project Types" column). + ecosystems: + type: string + default: >- + python + javascript + description: >- + The ecosystem list to scan (space or newline separated). + Default: python and javascript. + + See https://cyclonedx.github.io/cdxgen/#/PROJECT_TYPES for the supported values. permissions: contents: read @@ -50,9 +64,7 @@ jobs: uses: ./sbom-generator with: sbom_path: "./bom.json" - ecosystems: >- - python - javascript + ecosystems: ${{ inputs.ecosystems }} # When not running inside the same repository as the action, # use the action version as we cannot access our files inside @@ -62,9 +74,7 @@ jobs: uses: saleor/saleor-internal-actions/sbom-generator@main with: sbom_path: "./bom.json" - ecosystems: >- - python - javascript + ecosystems: ${{ inputs.ecosystems }} # Send the results to the next job ('analyze-licenses'). - name: Upload Results From c6186c84ca7a1386ae012667ef1e3e956d0859e5 Mon Sep 17 00:00:00 2001 From: Mikail Kocak Date: Fri, 27 Sep 2024 16:14:47 +0200 Subject: [PATCH 3/5] Add debug logging for every step --- .github/workflows/run-license-check.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/run-license-check.yaml b/.github/workflows/run-license-check.yaml index 301d08f..55b9c7f 100644 --- a/.github/workflows/run-license-check.yaml +++ b/.github/workflows/run-license-check.yaml @@ -130,6 +130,7 @@ jobs: # Ticket: https://github.com/anchore/syft/issues/1202 - name: Prepend Dependency Group run: | + test -n "${RUNNER_DEBUG+x}" || set -x set -eu # When .group is not blank, preprend .group into .name (using a slash (/)) @@ -201,6 +202,8 @@ jobs: - name: Validate HTML Report shell: bash run: | + test -n "${RUNNER_DEBUG+x}" || set -x + # Append into the file the comment suffix, # but ensure it exists in order to prevent sending empty comments # if there is a bug. @@ -232,6 +235,8 @@ jobs: PR_NUMBER: ${{ github.event.pull_request.number }} OLD_COMMENT_ID: ${{ steps.find-previous-comment.outputs.comment-id }} run: | + test -n "${RUNNER_DEBUG+x}" || set -x + endpoint="/repos/${GITHUB_REPOSITORY}/issues" cmd_args=() From 8d3b4dc0731666b4796e8412765d129a8e1a670a Mon Sep 17 00:00:00 2001 From: Mikail Kocak Date: Fri, 27 Sep 2024 18:12:21 +0200 Subject: [PATCH 4/5] Fix invalid download path --- .github/workflows/run-license-check.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-license-check.yaml b/.github/workflows/run-license-check.yaml index 55b9c7f..297cef8 100644 --- a/.github/workflows/run-license-check.yaml +++ b/.github/workflows/run-license-check.yaml @@ -115,7 +115,7 @@ jobs: uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: name: Generated SBOM with Licenses - path: ./bom.json + path: . # Prepends the dependency group to the dependency name # (as: ".group" + "/" + ".name") From ab257318fd6de76f946b68e19db9a20be7b2696d Mon Sep 17 00:00:00 2001 From: Mikail Kocak Date: Mon, 30 Sep 2024 12:52:51 +0200 Subject: [PATCH 5/5] Trigger workflow when YAML updated --- .github/workflows/self-check-licenses.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/self-check-licenses.yaml b/.github/workflows/self-check-licenses.yaml index 096484b..8b968e6 100644 --- a/.github/workflows/self-check-licenses.yaml +++ b/.github/workflows/self-check-licenses.yaml @@ -7,6 +7,8 @@ on: - opened - synchronize paths: + # Self + - ".github/workflows/self-check-licenses.yaml" # Python Ecosystem - "**/pyproject.toml" - "**/setup.py"