Skip to content

Commit 5116d47

Browse files
feat: major enhancements to actions usage scripts (#112)
* feat: enhance Action usage in organization script - fixing formatting (@3.%2A.%2A to @V3) - warning message for repos that don't have Dependency graph enabled - add --resolve-shas capability for count-by-version - add --dedupe-by-repo for count-by-action - added informational notes to help interpret results * feat: enhance Action usage in repository script - fixing formatting (@3.%2A.%2A to @V3) - warning message for repos that don't have Dependency graph enabled - add --resolve-shas capability for count-by-version * docs: update usage instructions and examples for action usage scripts * fix: only show warning for non-csv * fix: suppress errors when fetching tags for actions * docs: update README with examples/notes * feat: add caching for SHA to tag resolution in actions usage scripts
1 parent af595df commit 5116d47

File tree

3 files changed

+367
-33
lines changed

3 files changed

+367
-33
lines changed

gh-cli/README.md

Lines changed: 78 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -601,41 +601,102 @@ Gets the status of Actions on a repository (ie, if Actions are disabled)
601601

602602
Returns a list of all actions used in an organization using the SBOM API
603603

604-
Example output:
604+
Usage:
605+
606+
- `./get-actions-usage-in-organization.sh <org> [count-by-version|count-by-action] [txt|csv|md] [--resolve-shas] [--dedupe-by-repo]`
607+
608+
Examples:
609+
610+
- `./get-actions-usage-in-organization.sh joshjohanning-org count-by-version txt > output.txt`
611+
- `./get-actions-usage-in-organization.sh joshjohanning-org count-by-action md > output.md`
612+
- `./get-actions-usage-in-organization.sh joshjohanning-org count-by-version txt --resolve-shas > output.txt`
613+
- `./get-actions-usage-in-organization.sh joshjohanning-org count-by-action txt --dedupe-by-repo > output.txt`
614+
615+
Output formats:
616+
617+
- `txt` (default) - Plain text format
618+
- `csv` - Comma-separated values
619+
- `md` - Markdown table format
620+
621+
Count methods:
622+
623+
- `count-by-version` (default) - Count actions by version (actions/checkout@v2 separate from actions/checkout@v3)
624+
- `count-by-action` - Count actions by name only (versions stripped)
625+
626+
Optional flags:
627+
628+
- `--resolve-shas` - Resolve commit SHAs to their corresponding tags (works with count-by-version only)
629+
- `--dedupe-by-repo` - Count unique repositories per action (works with count-by-action only)
630+
631+
Example output (count-by-version) (with `--resolve-shas`):
605632

606633
```csv
607-
71 actions/checkout@3
608-
42 actions/checkout@2
609-
13 actions/upload-artifact@2
610-
13 actions/setup-node@3
634+
Count,Action
635+
4 actions/upload-artifact@v4
636+
3 actions/setup-node@v3
637+
2,actions/checkout@v4.3.0
638+
2,actions/checkout@main
639+
2,actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # sha not associated to tag
640+
2,actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
641+
2,actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
642+
1,actions/dependency-review-action@v4
643+
1,actions/checkout@v4
611644
```
612645

613-
Or (`count-by-action` option to count by action as opposed to action@version):
646+
Example output (count-by-action) (with `--dedupe-by-repo`):
614647

615648
```csv
616-
130 actions/checkout
617-
35 actions/upload-artifact
618-
27 actions/github-script
619-
21 actions/setup-node
649+
Count,Action
650+
3,actions/checkout
651+
2,actions/upload-artifact
652+
2,actions/setup-node
653+
1,actions/dependency-review-action
620654
```
621655

656+
> [!TIP]
657+
> If outputting to `txt` or `md`, you'll see a warning message for each repository that returned an error (because Dependency Graph is disabled). You will also see an informational message providing context around what the count is returning. `csv` returns clean data.
658+
622659
> [!NOTE]
623-
> The count returned is the # of repositories that use the action - if single a repository uses the action 2x times, it will only be counted 1x
660+
> The count returned is the # of repositories that use the `action@version` combination - if a single repository uses the `action@version` combination 2x times, it will only be counted 1x (unless using `count-by-action` in combination with `--dedupe-by-repo`, which counts unique repositories per action). Conversely, if different `action@version` combinations are being used, they will be counted separately (for example, if the same action appears twice in a repository but one uses `@v2` and one uses `@v3`, by default they will be counted separately unless using `count-by-action` in combination with `--dedupe-by-repo`).
661+
662+
> [!NOTE]
663+
> Using `--resolve-shas` will add additional API calls, but we attempt to cache tag lookups to improve performance. The cache is stored in temporary files and automatically cleaned up when the script exits.
624664
625665
### get-actions-usage-in-repository.sh
626666

627667
Returns a list of all actions used in a repository using the SBOM API
628668

629-
Example output:
669+
Usage:
670+
671+
- `./get-actions-usage-in-repository.sh <org> <repo> [--resolve-shas]`
672+
673+
Examples:
674+
675+
- `./get-actions-usage-in-repository.sh joshjohanning-org ghas-demo`
676+
- `./get-actions-usage-in-repository.sh joshjohanning-org ghas-demo --resolve-shas`
677+
678+
Optional flags:
679+
680+
- `--resolve-shas` - Resolve commit SHAs to their corresponding tags
681+
682+
Example output (with `--resolve-shas`):
630683

631684
```csv
632-
actions/checkout@3
633-
github/codeql-action/analyze@2
634-
github/codeql-action/autobuild@2
635-
github/codeql-action/init@2
636-
actions/dependency-review-action@3
685+
actions/checkout@v4
686+
actions/dependency-review-action@v4
687+
ossf/scorecard-action@e38b1902ae4f44df626f11ba0734b14fb91f8f86 # sha not associated to tag
688+
actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # v3.1.0
689+
actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 # v3.1.0
690+
github/codeql-action/upload-sarif@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # sha not associated to tag
691+
actions/checkout@v3
692+
github/codeql-action/analyze@v2
693+
github/codeql-action/autobuild@v2
694+
github/codeql-action/init@v2
637695
```
638696

697+
> [!NOTE]
698+
> Using `--resolve-shas` will add significant time to resolve commit SHAs to their corresponding tags
699+
639700
### get-all-users-in-repository.sh
640701

641702
Gets all users who have created an issue, pull request, issue comment, or pull request comment in a repository.

gh-cli/get-actions-usage-in-organization.sh

Lines changed: 206 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,38 @@
33
# Returns a list of all actions used in an organization using the SBOM API
44

55
# Example usage:
6-
# - ./get-actions-usage-in-repository.sh joshjohanning-org count-by-version txt > output.txt
7-
# - ./get-actions-usage-in-repository.sh joshjohanning-org count-by-action md > output.md
6+
# - ./get-actions-usage-in-organization.sh joshjohanning-org count-by-version txt > output.txt
7+
# - ./get-actions-usage-in-organization.sh joshjohanning-org count-by-action md > output.md
8+
# - ./get-actions-usage-in-organization.sh joshjohanning-org count-by-version txt --resolve-shas > output.txt
9+
# - ./get-actions-usage-in-organization.sh joshjohanning-org count-by-action txt --dedupe-by-repo > output.txt
810

911
# count-by-version (default): returns a count of actions by version; actions/checkout@v2 would be counted separately from actions/checkout@v3
1012
# count-by-action: returns a count of actions by action name; only care about actions/checkout usage, not the version
1113

1214
# Notes:
1315
# - The count returned is the # of repositories that use the action - if a single repository uses the action 2x times, it will only be counted 1x
1416
# - The script will take about 1 minute per 100 repositories
17+
# - Using --resolve-shas will add significant time to resolve commit SHAs to their corresponding tags
1518

16-
if [ $# -lt 1 ] || [ $# -gt 3 ] ; then
17-
echo "Usage: $0 <org> <count-by-version (default) | count-by-action> | <report format: txt (default) | csv | md>"
19+
if [ $# -lt 1 ] || [ $# -gt 5 ] ; then
20+
echo "Usage: $0 <org> <count-by-version (default) | count-by-action> <report format: txt (default) | csv | md> [--resolve-shas] [--dedupe-by-repo]"
1821
exit 1
1922
fi
2023

2124
org=$1
2225
count_method=$2
2326
report_format=$3
27+
resolve_shas=""
28+
dedupe_by_repo=""
29+
30+
# Parse parameters and flags
31+
for arg in "$@"; do
32+
if [ "$arg" == "--resolve-shas" ]; then
33+
resolve_shas="true"
34+
elif [ "$arg" == "--dedupe-by-repo" ]; then
35+
dedupe_by_repo="true"
36+
fi
37+
done
2438

2539
if [ -z "$count_method" ]; then
2640
count_method="count-by-version"
@@ -30,6 +44,88 @@ if [ -z "$report_format" ]; then
3044
report_format="txt"
3145
fi
3246

47+
# Validate that --resolve-shas only works with count-by-version
48+
if [ "$resolve_shas" == "true" ] && [ "$count_method" == "count-by-action" ]; then
49+
echo "Error: --resolve-shas can only be used with count-by-version (not count-by-action)" >&2
50+
exit 1
51+
fi
52+
53+
# Validate that --dedupe-by-repo only works with count-by-action
54+
if [ "$dedupe_by_repo" == "true" ] && [ "$count_method" != "count-by-action" ]; then
55+
echo "Error: --dedupe-by-repo can only be used with count-by-action" >&2
56+
exit 1
57+
fi
58+
59+
# Create temporary files for caching (compatible with bash 3.2)
60+
sha_cache_file=$(mktemp)
61+
action_tags_cache_dir=$(mktemp -d)
62+
63+
# Cleanup function to remove temp files
64+
cleanup_cache() {
65+
rm -f "$sha_cache_file" 2>/dev/null
66+
rm -rf "$action_tags_cache_dir" 2>/dev/null
67+
}
68+
trap cleanup_cache EXIT
69+
70+
# Function to resolve SHA to tag for a given action (with caching)
71+
resolve_sha_to_tag() {
72+
local action_with_sha="$1"
73+
local action_name
74+
local sha
75+
76+
action_name=$(echo "$action_with_sha" | cut -d'@' -f1)
77+
sha=$(echo "$action_with_sha" | cut -d'@' -f2)
78+
79+
# Create safe filename for cache (replace / with _)
80+
local safe_action_name=$(echo "$action_name" | tr '/' '_')
81+
local cache_key="${safe_action_name}@${sha}"
82+
83+
# Check SHA cache first
84+
if grep -q "^${cache_key}|" "$sha_cache_file" 2>/dev/null; then
85+
grep "^${cache_key}|" "$sha_cache_file" | cut -d'|' -f2- | head -1
86+
return
87+
fi
88+
89+
# Only process if it looks like a SHA (40 character hex string)
90+
if [[ ${#sha} -eq 40 && "$sha" =~ ^[a-f0-9]+$ ]]; then
91+
local action_cache_file="${action_tags_cache_dir}/${safe_action_name}"
92+
93+
# Check if we have tags cached for this action
94+
if [ ! -f "$action_cache_file" ]; then
95+
# Fetch and cache all tags for this action
96+
gh api repos/"$action_name"/git/refs/tags --paginate 2>/dev/null | \
97+
jq -r '.[] | "\(.object.sha)|\(.ref | sub("refs/tags/"; ""))"' 2>/dev/null > "$action_cache_file" || \
98+
touch "$action_cache_file"
99+
fi
100+
101+
# Look up the SHA in the cached tags
102+
local tag_name=""
103+
if [ -s "$action_cache_file" ]; then
104+
# First try to find a semantic version tag (prefer v1.2.3 over v1)
105+
tag_name=$(grep "^${sha}|" "$action_cache_file" | cut -d'|' -f2 | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+' | head -1)
106+
107+
# If no semantic version found, fall back to any tag
108+
if [ -z "$tag_name" ]; then
109+
tag_name=$(grep "^${sha}|" "$action_cache_file" | cut -d'|' -f2 | head -1)
110+
fi
111+
fi
112+
113+
if [ -n "$tag_name" ] && [ "$tag_name" != "null" ]; then
114+
local result="$action_with_sha # $tag_name"
115+
echo "${cache_key}|${result}" >> "$sha_cache_file"
116+
echo "$result"
117+
else
118+
local result="$action_with_sha # sha not associated to tag"
119+
echo "${cache_key}|${result}" >> "$sha_cache_file"
120+
echo "$result"
121+
fi
122+
else
123+
# Not a SHA, cache and return as-is
124+
echo "${cache_key}|${action_with_sha}" >> "$sha_cache_file"
125+
echo "$action_with_sha"
126+
fi
127+
}
128+
33129
repos=$(gh api graphql --paginate -F org="$org" -f query='query($org: String!$endCursor: String){
34130
organization(login:$org) {
35131
repositories(first:100,after: $endCursor) {
@@ -57,28 +153,125 @@ elif [ "$report_format" == "csv" ]; then
57153
echo "Count,Action"
58154
fi
59155

60-
actions=()
156+
actions=""
157+
repos_without_dependency_graph=()
61158

62159
for repo in $repos; do
63-
actions+=$(gh api repos/$repo/dependency-graph/sbom --jq '.sbom.packages[].externalRefs.[0].referenceLocator' 2>&1 | grep "pkg:githubactions" | sed 's/pkg:githubactions\///') || true
64-
actions+="\n"
160+
# Try to get SBOM data - if it fails, dependency graph is likely disabled
161+
sbom_data=$(gh api repos/$repo/dependency-graph/sbom --jq '.sbom.packages[].externalRefs.[0].referenceLocator' 2>&1)
162+
163+
# Also check if the API call returned an HTTP error code
164+
if echo "$sbom_data" | grep -q "HTTP "; then
165+
repos_without_dependency_graph+=("$repo")
166+
continue
167+
fi
168+
169+
repo_actions=$(echo "$sbom_data" | grep "pkg:githubactions" | sed 's/pkg:githubactions\///' | sed 's/%2A/*/g' 2>/dev/null || true)
170+
if [ "$dedupe_by_repo" == "true" ]; then
171+
# For dedupe mode, prefix each action with the repo name so we can track repo usage
172+
# Use awk to avoid sed delimiter issues with special characters
173+
repo_actions=$(echo "$repo_actions" | awk -v repo="$repo" '{print repo "|" $0}')
174+
fi
175+
actions+="$repo_actions"$'\n'
65176
done
66177

67178
# clean up extra spaces
68-
results=$(echo -e "${actions[@]}" | tr -s '\n' '\n' | sed 's/\n\n/\n/g')
179+
results=$(echo -e "$actions" | tr -s '\n' '\n' | sed 's/\n\n/\n/g')
180+
181+
# convert version patterns like 4.*.* to v4 format
182+
results=$(echo -e "$results" | sed 's/@\([0-9]\)\.\*\.\*/@v\1/g')
183+
184+
# convert semantic version numbers like @4.3.0 to @v4.3.0 (but not if they already have v, are branches, or are SHAs)
185+
results=$(echo -e "$results" | sed 's/@\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\)/@v\1/g')
186+
187+
# resolve SHAs to tags if requested
188+
if [ "$resolve_shas" == "true" ]; then
189+
# Create temporary file to store resolved results
190+
temp_results=""
191+
192+
# Process each line and resolve SHAs
193+
while IFS= read -r line; do
194+
if [ -n "$line" ] && [ "$line" != " " ]; then
195+
resolved_line=$(resolve_sha_to_tag "$line")
196+
if [ -n "$resolved_line" ] && [ "$resolved_line" != " " ]; then
197+
temp_results+="$resolved_line"$'\n'
198+
fi
199+
fi
200+
done <<< "$results"
201+
202+
# Clean up any trailing newlines
203+
results=$(echo -e "$temp_results" | sed '/^$/d')
204+
fi
69205

70206
# if count_method=count-by-action, then remove the version from the action name
71207
if [ "$count_method" == "count-by-action" ]; then
72-
results=$(echo -e "${results[@]}" | sed 's/@.*//g')
208+
results=$(echo -e "$results" | sed 's/@.*//g')
209+
210+
# If dedupe-by-repo is enabled, count unique repositories per action
211+
if [ "$dedupe_by_repo" == "true" ]; then
212+
# Each line now looks like: "repo|action"
213+
# We want to count unique repos per action
214+
temp_results=""
215+
for action in $(echo -e "$results" | cut -d'|' -f2 | sort | uniq); do
216+
repo_count=$(echo -e "$results" | grep "|$action$" | cut -d'|' -f1 | sort | uniq | wc -l)
217+
temp_results+="$repo_count $action"$'\n'
218+
done
219+
results="$temp_results"
220+
else
221+
# Strip repo prefixes if they exist (but shouldn't in non-dedupe mode)
222+
results=$(echo -e "$results" | sed 's/^[^|]*|//')
223+
fi
73224
fi
74225

75-
results=$(echo -e "$results" | sort | uniq -c | sort -nr | awk '{print $1 " " $2}')
226+
if [ "$count_method" == "count-by-action" ] && [ "$dedupe_by_repo" == "true" ]; then
227+
# Results are already formatted as "count action" from the dedupe logic
228+
results=$(echo -e "$results" | sed '/^$/d' | sort -nr | awk '{$1=$1; print $1 " " substr($0, index($0, $2))}')
229+
else
230+
# Standard processing: count occurrences
231+
results=$(echo -e "$results" | sed '/^$/d' | sort | uniq -c | sort -nr | awk '{$1=$1; print $1 " " substr($0, index($0, $2))}')
232+
fi
76233

77234
# if report_format = md
78235
if [ "$report_format" == "md" ]; then
79-
echo -e "${results[@]}" | awk '{print "| " $1 " | " $2 " |"}'
236+
echo -e "$results" | awk '{print "| " $1 " | " substr($0, index($0, $2)) " |"}'
80237
elif [ "$report_format" == "csv" ]; then
81-
echo -e "${results[@]}" | awk '{print $1 "," $2}'
238+
echo -e "$results" | awk '{print $1 "," substr($0, index($0, $2))}'
82239
else
83-
echo -e "${results[@]}"
240+
echo -e "$results"
241+
fi
242+
243+
# Add explanatory note for count-by-action mode (but not for CSV)
244+
if [ "$count_method" == "count-by-action" ] && [ "$report_format" != "csv" ]; then
245+
if [ "$dedupe_by_repo" == "true" ]; then
246+
note_text="Count represents the number of repositories using each action (deduplicated per repository)."
247+
else
248+
note_text="Count represents unique action@version combinations (versions stripped). Each repository using different versions of the same action contributes multiple counts."
249+
fi
250+
echo ""
251+
if [ "$report_format" == "md" ]; then
252+
echo "📝 **Note**: $note_text"
253+
elif [ "$report_format" == "txt" ]; then
254+
echo "📝 Note: $note_text"
255+
fi
256+
fi
257+
258+
# Add explanatory note for count-by-version mode (but not for CSV)
259+
if [ "$count_method" == "count-by-version" ] && [ "$report_format" != "csv" ]; then
260+
note_text="Count represents unique action@version combinations (with each unique action@version combination only showing up once per repository)."
261+
echo ""
262+
if [ "$report_format" == "md" ]; then
263+
echo "📝 **Note**: $note_text"
264+
elif [ "$report_format" == "txt" ]; then
265+
echo "📝 Note: $note_text"
266+
fi
267+
fi
268+
269+
# Show warning about repos that couldn't be analyzed (but not for CSV)
270+
if [ ${#repos_without_dependency_graph[@]} -gt 0 ] && [ "$report_format" != "csv" ]; then
271+
echo "" >&2
272+
echo "⚠️ Warning: The following repositories could not be analyzed (likely due to disabled Dependency Graph or permissions):" >&2
273+
for repo in "${repos_without_dependency_graph[@]}"; do
274+
echo " - $repo" >&2
275+
done
276+
echo "" >&2
84277
fi

0 commit comments

Comments
 (0)