Skip to content

Commit 44b6d08

Browse files
committed
fix(ci): enforce invisible-character findings
1 parent 5b8ae65 commit 44b6d08

1 file changed

Lines changed: 56 additions & 35 deletions

File tree

‎.github/workflows/dogfood-gate.yml‎

Lines changed: 56 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -121,12 +121,20 @@ jobs:
121121
122122
root = Path(os.environ["GITHUB_WORKSPACE"])
123123
skipped_dirs = {
124-
".git", ".deno", ".lake", "_build", "deps",
125-
"external_corpora", "node_modules", "target",
124+
".cache", ".deno", ".elixir_ls", ".git", ".lake", ".zig-cache",
125+
"_build", "build", "coverage", "deps", "dist", "external_corpora",
126+
"node_modules", "out", "target", "vendor", "zig-cache", "zig-out",
127+
}
128+
intentional_fixture_dirs = {
129+
("tests", "fixtures", "bom-detection"),
130+
("tests", "fixtures", "empty-linter"),
126131
}
127132
source_suffixes = {
128-
".adoc", ".ex", ".exs", ".gleam", ".hs", ".idr", ".jl",
129-
".js", ".json", ".md", ".ml", ".res", ".rs", ".sh",
133+
".adoc", ".adb", ".ads", ".agda", ".c", ".cc", ".clj", ".cljs",
134+
".cpp", ".erl", ".ex", ".exs", ".fs", ".fsi", ".fsx", ".gleam",
135+
".h", ".hh", ".hpp", ".hrl", ".hs", ".idr", ".java", ".jl",
136+
".js", ".json", ".kt", ".kts", ".lean", ".lua", ".md", ".ml",
137+
".php", ".r", ".rb", ".res", ".rs", ".scala", ".sh", ".swift",
130138
".toml", ".ts", ".v", ".yaml", ".yml", ".zig",
131139
}
132140
invisible_codepoints = {
@@ -136,50 +144,62 @@ jobs:
136144
*range(0x2066, 0x206A),
137145
}
138146
139-
def annotation_escape(value):
147+
def command_escape(value):
140148
return str(value).replace("%", "%25").replace("\r", "%0D").replace("\n", "%0A")
141149
150+
def property_escape(value):
151+
return command_escape(value).replace(":", "%3A").replace(",", "%2C")
152+
153+
# Runtime regression for GitHub workflow-command property delimiters.
154+
assert property_escape("docs/a,b::c.md") == "docs/a%2Cb%3A%3Ac.md"
155+
156+
def intentionally_invalid_fixture(relative):
157+
return any(relative.parts[:len(prefix)] == prefix for prefix in intentional_fixture_dirs)
158+
142159
findings = []
143160
errors = []
144-
for path in root.rglob("*"):
145-
relative = path.relative_to(root)
146-
if (
147-
path.is_symlink()
148-
or not path.is_file()
149-
or path.suffix.lower() not in source_suffixes
150-
or any(part in skipped_dirs for part in relative.parts[:-1])
151-
):
152-
continue
153-
try:
154-
data = path.read_bytes()
155-
except OSError as error:
156-
errors.append((relative, f"could not read file: {error}"))
157-
continue
161+
for directory, dirnames, filenames in os.walk(root, topdown=True):
162+
dirnames[:] = [name for name in dirnames if name not in skipped_dirs]
163+
directory_path = Path(directory)
164+
for filename in filenames:
165+
path = directory_path / filename
166+
relative = path.relative_to(root)
167+
if (
168+
path.is_symlink()
169+
or path.suffix.lower() not in source_suffixes
170+
or intentionally_invalid_fixture(relative)
171+
):
172+
continue
173+
try:
174+
data = path.read_bytes()
175+
except OSError as error:
176+
errors.append((relative, f"could not read file: {error}"))
177+
continue
158178
159-
reasons = set()
160-
if data.startswith(b"\xef\xbb\xbf"):
161-
reasons.add("leading UTF-8 BOM")
162-
if any(byte <= 0x08 or byte in (0x0B, 0x0C) or 0x0E <= byte <= 0x1F for byte in data):
163-
reasons.add("C0 control character")
164-
try:
165-
text_content = data.decode("utf-8", errors="strict")
166-
except UnicodeDecodeError as error:
167-
errors.append((relative, f"invalid UTF-8 at byte {error.start}"))
168-
continue
169-
if any(ord(character) in invisible_codepoints for character in text_content):
170-
reasons.add("invisible Unicode code point")
171-
if reasons:
172-
findings.append((relative, ", ".join(sorted(reasons))))
179+
reasons = set()
180+
if data.startswith(b"\xef\xbb\xbf"):
181+
reasons.add("leading UTF-8 BOM")
182+
if any(byte <= 0x08 or byte in (0x0B, 0x0C) or 0x0E <= byte <= 0x1F for byte in data):
183+
reasons.add("C0 control character")
184+
try:
185+
text_content = data.decode("utf-8", errors="strict")
186+
except UnicodeDecodeError as error:
187+
errors.append((relative, f"invalid UTF-8 at byte {error.start}"))
188+
continue
189+
if any(ord(character) in invisible_codepoints for character in text_content):
190+
reasons.add("invisible Unicode code point")
191+
if reasons:
192+
findings.append((relative, ", ".join(sorted(reasons))))
173193
174194
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output:
175195
output.write(f"findings={len(findings)}\n")
176196
output.write(f"exit_code={2 if errors else 0}\n")
177197
output.write("ready=true\n")
178198
179199
for relative, reasons in findings:
180-
print(f"::warning file={annotation_escape(relative)}::Invisible characters detected: {reasons}")
200+
print(f"::warning file={property_escape(relative)}::Invisible characters detected: {command_escape(reasons)}")
181201
for relative, reason in errors:
182-
print(f"::error file={annotation_escape(relative)}::Invisible-character scan failed: {annotation_escape(reason)}")
202+
print(f"::error file={property_escape(relative)}::Invisible-character scan failed: {command_escape(reason)}")
183203
PY
184204
- name: Write summary
185205
run: |
@@ -196,6 +216,7 @@ jobs:
196216
echo "## Empty-Linter Results" >> "$GITHUB_STEP_SUMMARY"
197217
echo "" >> "$GITHUB_STEP_SUMMARY"
198218
echo "Found **${FINDINGS}** invisible character issue(s). See annotations above." >> "$GITHUB_STEP_SUMMARY"
219+
exit 1
199220
else
200221
echo "## Empty-Linter Results" >> "$GITHUB_STEP_SUMMARY"
201222
echo "" >> "$GITHUB_STEP_SUMMARY"

0 commit comments

Comments
 (0)