Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions .github/workflows/min-api-lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
name: Min-API Lint

# Guards the declared minimum Unity version (package.json `unity` = 2021.3). The dev project and
# the Unity test job both run on Unity 6000.3, so they cannot catch APIs that fail to compile on an
# early 2021.3.x consumer. This license-free static check flags such APIs when they are used WITHOUT
# a version guard. It is a heuristic (not a real 2021.3 compile), focused on the Find*ByType family
# that previously broke the floor; guarded usage (#if UNITY_..._OR_NEWER ... #else FindObjectOfType)
# is allowed.

on:
push:
branches:
- main
paths:
- "Packages/**/*.cs"
- ".github/workflows/min-api-lint.yml"
pull_request:
paths:
- "Packages/**/*.cs"
- ".github/workflows/min-api-lint.yml"

jobs:
min-api-lint:
name: Min-API lint (Unity 2021.3 floor)
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Scan for unguarded post-2021.3 APIs
run: |
set -uo pipefail
PKG="Packages/com.orkunmanap.runtime-transform-handles"

# APIs introduced after 2021.3.0 (Find*ByType: 2021.3.18f1 / 2022.2). Add more here as
# the floor concern grows. FindObjectOfType (the pre-2021.3 API) is intentionally allowed.
DENY='FindAnyObjectByType|FindFirstObjectByType|FindObjectsByType'

# awk tracks #if/#elif/#else/#endif nesting and marks a level "version-guarded" when its
# condition mentions UNITY_<year> or *_OR_NEWER. A denied API is reported only when it sits
# outside every version-guarded region (comment-only lines are ignored).
HITS=$(while IFS= read -r -d '' f; do
awk -v deny="$DENY" -v file="$f" '
function isver(s){ return (s ~ /UNITY_[0-9]/ || s ~ /OR_NEWER/) }
/^[ \t]*#[ \t]*if/ { depth++; g[depth]=isver($0)?1:0; next }
/^[ \t]*#[ \t]*elif/ { if(depth>0 && isver($0)) g[depth]=1; next }
/^[ \t]*#[ \t]*else/ { next }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reset guard state on #else branches

In a version guard, the #else branch is the code compiled by older Unity versions, but this rule leaves g[depth] set when it reaches #else. As a result, #if UNITY_2023_1_OR_NEWER ... #else FindFirstObjectByType<T>() #endif is treated as guarded and produces no HITS, even though the denied API is exactly in the 2021.3 fallback path this workflow is meant to protect.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#else branch inherits version-guard, missing real violations

Medium Severity

The #else handler simply does next without resetting g[depth], so the #else branch of a version-guarded #if is still considered "guarded." The #else branch is the code path for old Unity versions (where the denied APIs don't exist), so a denied API placed there is exactly the bug this lint exists to catch — yet it would be silently allowed. For example, if someone copies FindFirstObjectByType into the #else fallback by mistake, the linter would not flag it.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1e8f4b5. Configure here.

/^[ \t]*#[ \t]*endif/ { if(depth>0) depth--; next }
{
ing=0; for(i=1;i<=depth;i++) if(g[i]) ing=1
if(ing==0 && $0 ~ ("(" deny ")") && $0 !~ /^[ \t]*\/\//)
printf "%s:%d:%s\n", file, NR, $0
}
' "$f"
done < <(find "$PKG" -name '*.cs' -print0))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lint silently passes when package directory is missing

Low Severity

If the $PKG directory doesn't exist (e.g., the package is renamed), find fails inside the process substitution, but its exit status is silently discarded. HITS ends up empty and the script reports "Min-API lint passed" with exit 0 — even though nothing was actually scanned. The set -uo pipefail flags don't help here because process substitution exit codes aren't captured by pipefail, and set -e is not enabled. A pre-flight [ -d "$PKG" ] check would prevent the lint from becoming a silent no-op.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1e8f4b5. Configure here.


if [ -n "$HITS" ]; then
echo "::error::Unguarded post-2021.3 API usage (wrap in '#if UNITY_2023_1_OR_NEWER ... #else <FindObjectOfType> #endif'):"
echo "$HITS"
exit 1
fi

echo "Min-API lint passed: no unguarded post-2021.3 Find*ByType usage."
Loading