Skip to content

Instantly share code, notes, and snippets.

@gwpl
Created March 2, 2026 19:34
Show Gist options
  • Select an option

  • Save gwpl/63763c12270d35e98c293eb9850f1943 to your computer and use it in GitHub Desktop.

Select an option

Save gwpl/63763c12270d35e98c293eb9850f1943 to your computer and use it in GitHub Desktop.
GitHub License Detection NOASSERTION — Diagnosis Guide & Script
#!/usr/bin/env bash
# check-github-license-detection.sh
#
# Diagnose GitHub license detection issues for a repository.
# Checks the API response, inspects license files, and suggests fixes.
#
# Usage:
# ./check-github-license-detection.sh OWNER/REPO
# ./check-github-license-detection.sh # (auto-detect from current git repo)
#
# Requirements: gh (GitHub CLI), jq
#
# License: MIT OR Apache-2.0
set -euo pipefail
# --- Colors (disabled if not a terminal) ---
if [ -t 1 ]; then
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m'
else
RED=''; GREEN=''; YELLOW=''; CYAN=''; BOLD=''; RESET=''
fi
info() { printf "${CYAN}[INFO]${RESET} %s\n" "$*"; }
ok() { printf "${GREEN}[OK]${RESET} %s\n" "$*"; }
warn() { printf "${YELLOW}[WARN]${RESET} %s\n" "$*"; }
fail() { printf "${RED}[FAIL]${RESET} %s\n" "$*"; }
# --- Dependency check ---
for cmd in gh jq; do
if ! command -v "$cmd" &>/dev/null; then
fail "Required command '$cmd' not found. Please install it."
exit 1
fi
done
# --- Determine repository ---
if [ $# -ge 1 ]; then
REPO="$1"
else
REPO=$(gh repo view --json nameWithOwner --jq '.nameWithOwner' 2>/dev/null || true)
if [ -z "$REPO" ]; then
echo "Usage: $0 OWNER/REPO"
echo " or: run from inside a git repo with a GitHub remote"
exit 1
fi
fi
printf "\n${BOLD}=== GitHub License Detection Check: ${REPO} ===${RESET}\n\n"
# --- Step 1: Check API license detection ---
info "Querying GitHub API for license info..."
LICENSE_JSON=$(gh api "repos/${REPO}" --jq '{
spdx_id: .license.spdx_id,
name: .license.name,
key: .license.key
}' 2>/dev/null || echo '{}')
SPDX_ID=$(echo "$LICENSE_JSON" | jq -r '.spdx_id // "null"')
LICENSE_NAME=$(echo "$LICENSE_JSON" | jq -r '.name // "null"')
echo " API response:"
echo " spdx_id: ${SPDX_ID}"
echo " name: ${LICENSE_NAME}"
echo ""
if [ "$SPDX_ID" = "NOASSERTION" ]; then
fail "License detection returned NOASSERTION — GitHub cannot identify the license."
echo ""
elif [ "$SPDX_ID" = "null" ]; then
fail "No license detected at all — no recognized license file found."
echo ""
else
ok "License detected successfully: ${SPDX_ID} (${LICENSE_NAME})"
echo ""
fi
# --- Step 2: List license-related files in the repo root ---
info "Listing license-related files in repo root..."
LICENSE_FILES=$(gh api "repos/${REPO}/contents/" --jq '
[.[] | select(.name | test("^(LICENSE|LICENCE|COPYING|UNLICENSE)"; "i")) | .name]
' 2>/dev/null || echo '[]')
NUM_FILES=$(echo "$LICENSE_FILES" | jq 'length')
if [ "$NUM_FILES" -eq 0 ]; then
fail "No license files found in repo root!"
else
echo " Found ${NUM_FILES} license file(s):"
echo "$LICENSE_FILES" | jq -r '.[] | " - " + .'
fi
echo ""
# --- Step 3: Analyze each license file ---
if [ "$NUM_FILES" -gt 0 ]; then
info "Analyzing license file contents..."
echo "$LICENSE_FILES" | jq -r '.[]' | while read -r fname; do
CONTENT=$(gh api "repos/${REPO}/contents/${fname}" --jq '@base64d | .content' 2>/dev/null \
|| gh api "repos/${REPO}/contents/${fname}" --jq '.content' 2>/dev/null | base64 -d 2>/dev/null \
|| echo "")
SIZE=${#CONTENT}
printf " ${BOLD}%s${RESET} (%d bytes)\n" "$fname" "$SIZE"
if [ "$SIZE" -lt 500 ]; then
warn " Very short (${SIZE} bytes) — likely a wrapper/index, not actual license text."
warn " The shortest SPDX license template (MIT) is ~1050 bytes."
warn " licensee probably cannot match this to any known license."
elif echo "$CONTENT" | grep -qiE "apache.*license.*2\.0|apache-2\.0"; then
ok " Appears to contain Apache 2.0 license text."
elif echo "$CONTENT" | grep -qiE "mit license|permission is hereby granted"; then
ok " Appears to contain MIT license text."
elif echo "$CONTENT" | grep -qiE "gnu general public|GPL"; then
ok " Appears to contain GPL license text."
elif echo "$CONTENT" | grep -qiE "bsd.*license|redistribution and use"; then
ok " Appears to contain BSD license text."
else
warn " Content does not match common license patterns — may not be detected."
fi
done
echo ""
fi
# --- Step 4: Check package.json license field (if exists) ---
PKG_LICENSE=$(gh api "repos/${REPO}/contents/package.json" --jq '@base64d | .content' 2>/dev/null \
| jq -r '.license // empty' 2>/dev/null || true)
if [ -n "$PKG_LICENSE" ]; then
info "package.json license field: ${PKG_LICENSE}"
if [ "$SPDX_ID" = "NOASSERTION" ]; then
warn "package.json has a license but GitHub detection still fails."
warn "GitHub UI uses licensee (file-based), not package.json."
fi
echo ""
fi
# --- Step 5: Recommendations ---
if [ "$SPDX_ID" = "NOASSERTION" ] || [ "$SPDX_ID" = "null" ]; then
printf "${BOLD}=== Recommendations ===${RESET}\n\n"
# Check if there's a short wrapper file
HAS_WRAPPER=false
echo "$LICENSE_FILES" | jq -r '.[]' | while read -r fname; do
if echo "$fname" | grep -qiE "^license(\.md|\.txt)?$"; then
CONTENT=$(gh api "repos/${REPO}/contents/${fname}" --jq '.content' 2>/dev/null | base64 -d 2>/dev/null || echo "")
if [ ${#CONTENT} -lt 500 ]; then
echo " 1. Your root license file '${fname}' is too short for licensee to match."
echo " → Rename full-text license files to LICENSE-APACHE-2.0, LICENSE-MIT, etc."
echo " → Or replace '${fname}' with the full text of your primary license."
echo ""
fi
fi
done
echo " General fix options:"
echo " a) Rename: git mv LICENSE.Apache-2.0.txt LICENSE-APACHE-2.0"
echo " b) Replace: cp <full-license-text> LICENSE"
echo " c) Test locally: gem install licensee && licensee detect ."
echo ""
echo " Reference: https://github.com/licensee/licensee/blob/main/docs/what-we-look-at.md"
fi
printf "${BOLD}=== Done ===${RESET}\n"

GitHub License Detection Returns NOASSERTION — Diagnosis and Fix

The Problem

Your GitHub repository shows "Other" instead of the correct license badge. The GitHub API returns "spdx_id": "NOASSERTION" — meaning license detection failed.

This is a common issue for repositories with dual-license or non-standard license file layouts.

Quick Check

# Replace OWNER/REPO with your repository
gh api repos/OWNER/REPO --jq '.license.spdx_id'
  • "MIT", "Apache-2.0", etc. — detection works
  • "NOASSERTION" — detection failed (this guide is for you)
  • null — no license file found at all

How GitHub License Detection Works

GitHub uses licensee (a Ruby gem) to detect licenses.

The algorithm:

  1. Find files matching common license filenames: LICENSE, LICENSE.md, LICENSE.txt, LICENCE, COPYING, LICENSE-*, etc.
  2. Read the content of the best-matching filename
  3. Fuzzy-match the content against a corpus of known SPDX license texts
  4. If similarity ≥ ~90%, identify the license; otherwise return NOASSERTION

Key behaviors:

  • licensee prefers files named exactly LICENSE (no extension) over others
  • Files named LICENSE-* (e.g., LICENSE-MIT, LICENSE-APACHE-2.0) are recognized as additional license files
  • The file must contain actual license text, not just a description or links
  • The shortest known template (MIT) is ~1000 bytes — a short wrapper file won't match anything

References

Root Cause: Common Patterns That Fail

Pattern 1: Wrapper/index LICENSE file (most common for dual-license)

LICENSE.md               ← short markdown with links (NOT actual license text)
LICENSE.Apache-2.0.txt   ← full Apache 2.0 text
LICENSE.MIT.txt           ← full MIT text

licensee reads LICENSE.md first, finds ~200 bytes of markdown links, can't match it to any known license → NOASSERTION.

Pattern 2: Non-standard file naming

License_Apache2.txt      ← not recognized as a license filename
mit-license.txt          ← not recognized

licensee has a specific list of filename patterns it recognizes. Creative naming causes it to skip the file entirely.

Pattern 3: License text with heavy modifications

LICENSE                  ← standard MIT text but with large custom preamble/appendix

If modifications push similarity below ~90%, detection fails.

Solutions

Solution A: Rename to LICENSE-* pattern (recommended for dual-license)

This is the pattern used by projects like vis-network and many Apache Software Foundation projects:

git mv LICENSE.md LICENSE               # or delete it
git mv LICENSE.Apache-2.0.txt LICENSE-APACHE-2.0
git mv LICENSE.MIT.txt LICENSE-MIT

Why it works: licensee recognizes LICENSE-APACHE-2.0 as a license file, reads the full Apache 2.0 text, and matches it successfully.

Solution B: Put full license text in the root LICENSE file

Replace the wrapper with one of the actual license texts:

cp LICENSE-MIT LICENSE          # root file = full MIT text
# Keep LICENSE-APACHE-2.0 as the secondary license

Solution C: Concatenate both licenses into a single LICENSE file

Some projects place both license texts in a single file with a separator:

=== MIT License ===

[full MIT text]

=== Apache License 2.0 ===

[full Apache 2.0 text]

This works but licensee will typically only detect the first/primary license.

Solution D: Use a COPYING file with the primary license

cp LICENSE.Apache-2.0.txt COPYING

licensee also recognizes COPYING as a license filename (common in GPL projects).

After Fixing — Verification

License detection updates are not instant. GitHub re-runs licensee periodically or when files change on the default branch.

# Check after merge (may take a few minutes)
gh api repos/OWNER/REPO --jq '.license.spdx_id'

# Expected: "Apache-2.0", "MIT", or whichever license is in the primary file

You can also test locally before pushing:

# Install licensee (requires Ruby)
gem install licensee

# Run against your local repo
licensee detect .

Ensuring package.json / Cargo.toml / etc. Stay Consistent

File renaming only affects GitHub's UI detection. Package manager metadata is independent:

  • npm: "license": "(Apache-2.0 OR MIT)" in package.json (docs)
  • Cargo: license = "Apache-2.0 OR MIT" in Cargo.toml (docs)
  • PyPI: license = "Apache-2.0 OR MIT" in pyproject.toml

These use SPDX expressions directly and are unaffected by file naming.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment