Skip to content

feat: Add dependency license scanning with licensed #26

feat: Add dependency license scanning with licensed

feat: Add dependency license scanning with licensed #26

name: Check dependency licenses
on:
pull_request:
permissions:
contents: read
jobs:
check-dependency-licenses:
runs-on: ubuntu-24.04-arm
env:
PYTHON_VERSION: "3.13"
TASKFILE_VERSION: "v3.44.0"
TASKFILE_PATH: "/home/runner/go/bin"
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3'
- name: Install system dependencies
run: sudo apt-get install -y -qq portaudio19-dev libzbar0
- name: Install Taskfile
run: which task || curl -sSfL https://taskfile.dev/install.sh | sh -s -- -b ${{ env.TASKFILE_PATH }} ${{ env.TASKFILE_VERSION }}
- name: Check dependency licenses (licensed status)
id: licensed
run: |
export PATH="${{ env.TASKFILE_PATH }}:$PATH"
task license:deps 2>&1 | tee licensed_status.log || true
- name: Annotate and summarize errors
if: always()
run: |
actual_errors_file=$(mktemp)
install_warnings_file=$(mktemp)
updated_records_raw_file=$(mktemp)
updated_records_file=$(mktemp)
awk '
function flush() {
if (block != "") {
print block >> output_file
print "" >> output_file
block = ""
}
}
/^Errors:$/ { in_errors = 1; next }
in_errors && /^\* / { flush(); block = $0; next }
in_errors && /^[[:space:]]+/ {
if (block != "") {
block = block "\n" $0
}
next
}
in_errors && /^$/ { flush(); next }
in_errors { flush(); in_errors = 0 }
END { flush() }
' output_file="$actual_errors_file" licensed_status.log
grep '^::warning::Failed to install ' licensed_status.log | \
sed -E 's/^::warning::Failed to install ([^ ]+) in (.+)$/- `\1` in `\2`/' | \
sort -u > "$install_warnings_file" || true
git diff --name-only -- .licenses | \
grep -E '\.dep\.ya?ml$' | \
sed -E 's#^\.licenses/##; s#\.dep\.ya?ml$##; s#/#.#g' | \
sort -u > "$updated_records_raw_file" || true
sed -E 's#^#- `#; s#$#`#' "$updated_records_raw_file" > "$updated_records_file" || true
echo "### Licensed Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ -s "$actual_errors_file" ]; then
echo "::error::Dependency license cache is out of date. Run 'task license:deps' locally, then review the changes, commit, and push the updated files."
echo "❌ The following dependency license issues require review:" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
awk '
BEGIN { RS=""; ORS="\n\n" }
NF { print "```text\n" $0 "\n```" }
' "$actual_errors_file" >> $GITHUB_STEP_SUMMARY
else
echo "✅ No blocking dependency license issues found." >> $GITHUB_STEP_SUMMARY
fi
if [ -s "$install_warnings_file" ]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Errors installing dependencies" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "⚠️ These warnings are non-blocking, but they can make the license scan less complete for the affected environment." >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
cat "$install_warnings_file" >> $GITHUB_STEP_SUMMARY
fi
if [ -s "$updated_records_file" ]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Outdated dependency records" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "⚠️ These records changed during \`licensed cache\`. This can reflect dependency version updates, license text changes, or other cached metadata changes. They are non-blocking here, but usually mean the branch did not yet contain the latest cached dependency metadata. Please consider to run \`task license:deps\` locally, then review the changes, commit, and push the updated files." >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
cat "$updated_records_file" >> $GITHUB_STEP_SUMMARY
while IFS= read -r record; do
[ -n "$record" ] || continue
echo "::warning::Dependency record out-of-date: $record"
done < "$updated_records_raw_file"
fi
if [ -s "$actual_errors_file" ]; then
# GitHub workflow commands need escaped newlines, otherwise only the
# first line is attached to the annotation and the rest is plain log output.
awk '
function escape(text, escaped) {
escaped = text
gsub(/%/, "%25", escaped)
gsub(/\r/, "%0D", escaped)
gsub(/\n/, "%0A", escaped)
return escaped
}
function flush() {
if (block != "") {
print "::error::" escape(block)
block = ""
}
}
/^\* / { flush(); block = $0; next }
/^[[:space:]]+/ {
if (block != "") {
block = block "\n" $0
}
next
}
/^$/ { flush(); next }
END { flush() }
' "$actual_errors_file"
exit 1
fi