From 46291fc8500cfc2453e07e05d74e0c05832b1f89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jalil=20David=20Salam=C3=A9=20Messina?= Date: Tue, 17 Dec 2024 23:15:39 +0100 Subject: [PATCH] feat!(report): generate JSON instead of markdown This allows us to analyze the report more easily. There is experimental support for comparing against a base report, but we don't expose that functionality to the action. --- action.yml | 46 ++-------------- comment_on_pr.sh | 135 +++++++++++++++++++++++++++++++++++++++++++++++ compare.jq | 20 +++++++ create-report.sh | 46 +++++----------- 4 files changed, 171 insertions(+), 76 deletions(-) create mode 100755 comment_on_pr.sh create mode 100644 compare.jq diff --git a/action.yml b/action.yml index 858f3f7..8aec574 100644 --- a/action.yml +++ b/action.yml @@ -28,59 +28,21 @@ inputs: default: 'false' artifact-name: description: The name of the generated artifact. - default: 'size-report.md' + default: report.json outputs: runs: using: 'composite' steps: - name: Create report run: | - "$GITHUB_ACTION_PATH/create-report.sh" + "$GITHUB_ACTION_PATH/create-report.sh" > report.json - name: Upload Artifact uses: https://code.forgejo.org/forgejo/upload-artifact@v4 if: inputs.generate-artifact == 'true' with: - path: size-report.md + path: report.json name: ${{ inputs.artifact-name }} - name: Comment Report if: inputs.comment-on-pr == 'true' run: | - set -eu - - echo 'Determine head_ref' - # For push & tag events it'll bet GITHUB_REF_NAME, for pull_request events it'll be GITHUB_HEAD_REF - head_ref=${GITHUB_REF_NAME-$GITHUB_HEAD_REF} - - echo "Get PR number for $head_ref" - prs=$(curl -X 'GET' \ - "$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/pulls?state=open&sort=recentupdate" \ - -H "Authorization: token $GITHUB_TOKEN" \ - -H 'accept: application/json') - - pr_number=$(echo "$prs" | - jq --arg head_ref "$head_ref" '.[] | select(.head.ref == $head_ref) | .number') - - # Protect against running before a PR is made or if it is triggered on the main branch - if [ -z "$pr_number" ]; then - echo "No PR created for this commit" - exit 0 - fi - - echo "Retrieved index: $pr_number" >&2 - echo "Expected PR URL: $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/pulls/$pr_number" >&2 - - echo 'Generating comment body' >&2 - comment=$(cat size-report.md) - - echo 'Posting comment:' >&2 - echo "$comment" >&2 - - echo 'Request data:' >&2 - data=$(echo '{}' | jq --arg comment "$comment" '.body=$comment') - echo "$data" >&2 - curl -o - -X 'POST' \ - "$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/issues/$pr_number/comments" \ - -H 'accept: application/json' \ - -H "Authorization: token $GITHUB_TOKEN" \ - -H 'Content-Type: application/json' \ - -d "$data" + "$GITHUB_ACTION_PATH/comment_on_pr.sh" report.json diff --git a/comment_on_pr.sh b/comment_on_pr.sh new file mode 100755 index 0000000..8f99989 --- /dev/null +++ b/comment_on_pr.sh @@ -0,0 +1,135 @@ +#!/bin/sh + +set -eu + +# USAGE: json_to_md_rows [JSON_FILE] +# +# JSON_FILE can be piped from stdin +json_to_md_rows() { + jq --raw-output \ + ".$1[]"' | "| `\(.name)` | \(.size) | \(.narSize) |"' "$2" | + numfmt --to=iec-i --field=4,6 +} + +# USAGE: json_to_md_rows [JSON_FILE] +# +# JSON_FILE can be piped from stdin +json_to_md_rows_and_change() { + jq --raw-output \ + ".$1[]"' | "| `\(.name)` | \(.size) | \(.sizeChange) | \(.narSize) | \(.narSizeChange) |"' | + numfmt --to=iec-i --field=4,6,8,10 +} + +# USAGE: has_elements +has_elements() { + if [ "${2+set}" = 'set' ]; then + [ "$(jq ".$1 != []" "$2")" = 'true' ] + else + [ "$(jq ".$1 != []")" = 'true' ] + fi +} + +# USAGE: markdown_from_report [BASE_REPORT] +# +# If BASE_REPORT is provided, a comparison will be made +markdown_from_report() { + cat <<-"EOF" + # Flake output sizes + + - `Name`: the name of the package/configuration + - `Size`: the closure size (size on disk of the package/configuration) + - `NAR Size`: the size of the generated build instructions (Nix ARchive) + EOF + + if [ "${2+set}" = "set" ]; then + cat <<-"EOF" + - `[NAR] Size Change`: the amount changed compared to the main branch + EOF + compare=$(jq --slurp --from-file "${GITHUB_ACTION_PATH-.}/compare.jq" "$1" "$2") + if echo "$compare" | has_elements 'nixosConfigurations'; then + cat <<-"EOF" + # NixOS Configurations + + | Name | Size | Size Change | NAR Size | NAR Size Change | + |------|-----:|------------:|---------:|----------------:| + EOF + echo "$compare" | json_to_md_rows_and_change "nixosConfigurations" + echo + fi + if echo "$compare" | has_elements 'packages'; then + cat <<-"EOF" + # Packages + + | Name | Size | Size Change | NAR Size | NAR Size Change | + |------|-----:|------------:|---------:|----------------:| + EOF + echo "$compare" | json_to_md_rows_and_change "packages" + echo + fi + else + if has_elements 'nixosConfigurations' "$1"; then + cat <<-"EOF" + # NixOS Configurations + + | Name | Size | NAR Size | + |------|-----:|---------:| + EOF + json_to_md_rows "nixosConfigurations" "$1" + echo + fi + if has_elements 'packages' "$1"; then + cat <<-"EOF" + # Packages + + | Name | Size | NAR Size | + |------|-----:|---------:| + EOF + json_to_md_rows "packages" "$1" + echo + fi + fi +} + +# Test outside CI +if [ "${CI-false}" != 'true' ]; then + markdown_from_report "$@" + exit 0 +fi + +echo 'Determine head_ref' +# For push & tag events it'll bet GITHUB_REF_NAME, for pull_request events it'll be GITHUB_HEAD_REF +head_ref=${GITHUB_REF_NAME-$GITHUB_HEAD_REF} + +echo "Get PR number for $head_ref" +prs=$(curl -X 'GET' \ + "$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/pulls?state=open&sort=recentupdate" \ + -H "Authorization: token $GITHUB_TOKEN" \ + -H 'accept: application/json') + +pr_number=$(echo "$prs" | + jq --arg head_ref "$head_ref" '.[] | select(.head.ref == $head_ref) | .number') + +# Protect against running before a PR is made or if it is triggered on the main branch +if [ -z "$pr_number" ]; then + echo "No PR created for this commit" + exit 0 +fi + +echo "Retrieved index: $pr_number" >&2 +echo "Expected PR URL: $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/pulls/$pr_number" >&2 + +echo 'Generating comment body' >&2 +comment=$(markdown_from_report "$@") + +echo 'Posting comment:' >&2 +echo "$comment" >&2 + +echo 'Request data:' >&2 +data=$(echo '{}' | jq --arg comment "$comment" '.body=$comment') +echo "$data" >&2 +curl -o - -X 'POST' \ + "$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/issues/$pr_number/comments" \ + -H 'accept: application/json' \ + -H "Authorization: token $GITHUB_TOKEN" \ + -H 'Content-Type: application/json' \ + -d "$data" diff --git a/compare.jq b/compare.jq new file mode 100644 index 0000000..f663eb6 --- /dev/null +++ b/compare.jq @@ -0,0 +1,20 @@ +# Calculate the change in percentage of a nummeric key +# +# Where .[0] is the new value, and .[1] is the old value +def calc_change($key): + . as $input | $input[1][$key] - $input[0][$key]; + +# Calculate the change in percentage for multiple keys +# +# Returns an array of {"key": $key, "value": change%} +def keys_change_perc($keys): + . as $input | $keys | map(. as $key | $input | {"key": $key, "value": . | calc_change($key)}); + +# For a set of keys, calculate the change percentage and return it as +# "\($key)Change" = $value in the original object. +def set_change_perc($keys): + . as $input | reduce keys_change_perc($keys)[] as $change ($input[0]; .["\($change.key)Change"] = $change.value); + +# Bring everything together +reduce .[] as $x ({}; . as $acc | reduce ($x | keys[]) as $key ($acc; .[$key] |= . + $x[$key])) | + map_values(group_by(.name) | map(set_change_perc(["size", "narSize"]))) diff --git a/create-report.sh b/create-report.sh index 05f5ae5..d45e73c 100755 --- a/create-report.sh +++ b/create-report.sh @@ -19,52 +19,30 @@ configurations=$( echo "NixOS Configurations:" >&2 echo "$configurations" >&2 -package_size_table() { - table='| Installable | NAR Size | Closure Size | -|-------------|---------:|-------------:| -' +pkgs_json() { for package in $packages; do echo "Building $package" >&2 path=$(nix build --print-out-paths ".#$package" 2>/dev/null) echo "Calculating size of $package" >&2 - row=$(nix path-info --size --closure-size --human-readable "$path" 2>/dev/null | - sed "s/^\(\S\+\)\(\s\+\)\(\S\+\)\(\s\+\)\(\S\+\)$/| \`$package\` | \3 | \5 |/") - table="$table$row -" + nix path-info --closure-size --json "$path" 2>/dev/null | + jq --compact-output --arg pkg "$package" '.[] | {"name": $pkg, "size": .closureSize, "narSize": .narSize}' done - - printf '%s' "$table" } -configuration_size_table() { - table='| NixOS Configuration | NAR Size | Closure Size | -|-------------|---------:|-------------:| -' +configs_json() { for config in $configurations; do echo "Building $config" >&2 path=$(nix build --print-out-paths ".#nixosConfigurations.$config.config.system.build.toplevel" 2>/dev/null) echo "Calculating size of $config" >&2 - row=$(nix path-info --size --closure-size --human-readable "$path" 2>/dev/null | - sed "s/^\(\S\+\)\(\s\+\)\(\S\+\)\(\s\+\)\(\S\+\)$/| \`$config\` | \3 | \5 |/") - table="$table$row -" + nix path-info --closure-size --json "$path" 2>/dev/null | + jq --compact-output --arg pkg "$config" '.[] | {"name": $pkg, "size": .closureSize, "narSize": .narSize}' done - - printf '%s' "$table" } -markdown() { - cat <<-EOF - ## Outputs' size +pkgs=$(pkgs_json | jq --slurp '.') +configs=$(configs_json | jq --slurp '.') - ### NixOS Configurations sizes - - $(configuration_size_table) - - ### Package sizes - - $(package_size_table) - EOF -} - -markdown >size-report.md +echo "{}" | jq \ + --argjson pkgs "$pkgs" \ + --argjson configs "$configs" \ + '{"packages": $pkgs, "nixosConfigurations": $configs}' -- 2.47.0