From 8b398c2bd3cb39f0cfb6e58d392cb867d0bc1ea1 Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Sun, 23 Aug 2026 16:35:12 -0500 Subject: [PATCH 1/4] ci: post the LOC diff as a sticky pull request comment count_diff now takes two explicit tree paths instead of the MFC_PR env var, reports only files whose count moved, adds a per-directory rollup, and with --markdown writes a GitHub-flavored table (empty when nothing changed). The workflow moves to pull_request_target so fork PRs can be commented on, and posts via sticky-pull-request-comment. It counts against the merge base rather than the tip of the base branch, so a PR is only charged for the lines it actually adds. The counter also recurses into src/*/include, which the old glob silently skipped. --- .github/workflows/line-count.yml | 71 ++++++++++++------------ toolchain/mfc/args.py | 2 +- toolchain/mfc/cli/commands.py | 18 ++++++- toolchain/mfc/count.py | 92 ++++++++++++++++---------------- 4 files changed, 98 insertions(+), 85 deletions(-) diff --git a/.github/workflows/line-count.yml b/.github/workflows/line-count.yml index f7d2ec96ab..9beb4df648 100644 --- a/.github/workflows/line-count.yml +++ b/.github/workflows/line-count.yml @@ -1,58 +1,55 @@ name: Lines of Code +# pull_request_target so that the comment can be posted on pull requests from +# forks, whose GITHUB_TOKEN is read-only under the pull_request event. The PR's +# code is checked out as data and read by the base branch's counter; it is never +# executed, and no credentials are persisted alongside it. on: - push: - branches: [master] - pull_request: - workflow_dispatch: + pull_request_target: + paths: + - 'src/**' -jobs: - file-changes: - name: Detect File Changes - runs-on: 'ubuntu-latest' - outputs: - checkall: ${{ steps.changes.outputs.checkall }} - steps: - - name: Clone - uses: actions/checkout@v5 - - - name: Detect Changes - uses: dorny/paths-filter@v4 - id: changes - with: - filters: ".github/file-filter.yml" +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true +jobs: sz: name: Core MFC Line Difference - if: needs.file-changes.outputs.checkall == 'true' - needs: file-changes permissions: contents: read pull-requests: write runs-on: ubuntu-latest steps: - - name: Checkout code from PR branch + - name: Checkout the base branch uses: actions/checkout@v5 with: - path: pr + path: base + fetch-depth: 0 - - name: Checkout code from MFC master + - name: Checkout the PR branch uses: actions/checkout@v5 with: - repository: ${{ github.event.pull_request.repository }} - ref: ${{ github.event.pull_request.base.ref }} - path: base - # repository: MFlowCode/MFC - # ref: master - # path: base + repository: ${{ github.event.pull_request.head.repo.full_name }} + ref: ${{ github.event.pull_request.head.sha }} + path: pr + persist-credentials: false + # Counting against the merge base rather than the tip of the base branch + # means a PR is only ever charged for the lines it actually adds, however + # far behind the branch has fallen. - name: Get Line Diff + working-directory: base run: | - BASE="$GITHUB_WORKSPACE/base" - PR="$GITHUB_WORKSPACE/pr" - cd $BASE - export MFC_PR=$PR - pwd - ./mfc.sh init &> tmp.txt - ./mfc.sh count_diff + git fetch --no-tags origin "+refs/pull/${{ github.event.number }}/head:refs/mfc/pr" + git worktree add ../mb "$(git merge-base HEAD refs/mfc/pr)" + ./mfc.sh count_diff ../mb ../pr --markdown "$GITHUB_WORKSPACE/loc.md" + - name: Comment Line Diff + uses: marocchino/sticky-pull-request-comment@v3 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ignore_empty: true + skip_unchanged: true + recreate: true + path: loc.md diff --git a/toolchain/mfc/args.py b/toolchain/mfc/args.py index 5c68f35ef8..370cc6c2bf 100644 --- a/toolchain/mfc/args.py +++ b/toolchain/mfc/args.py @@ -112,7 +112,7 @@ def custom_error(message): # Add default arguments of other subparsers # This ensures all argument keys exist even for commands that don't define them # Only process subparsers that have common arguments we need - relevant_subparsers = ["run", "test", "build", "clean", "count", "count_diff", "validate", "viz"] + relevant_subparsers = ["run", "test", "build", "clean", "count", "validate", "viz"] for name in relevant_subparsers: if args["command"] == name: continue diff --git a/toolchain/mfc/cli/commands.py b/toolchain/mfc/cli/commands.py index ca60b53ee3..ca1d02ae19 100644 --- a/toolchain/mfc/cli/commands.py +++ b/toolchain/mfc/cli/commands.py @@ -900,8 +900,24 @@ COUNT_DIFF_COMMAND = Command( name="count_diff", - help="Compare LOC between branches.", + help="Compare LOC between two MFC source trees.", include_common=["targets", "mfc_config", "jobs", "verbose", "debug_log"], + positionals=[ + Positional(name="base", help="Path to the baseline MFC source tree.", completion=Completion(type=CompletionType.DIRECTORIES)), + Positional(name="pr", help="Path to the MFC source tree to compare against the baseline.", completion=Completion(type=CompletionType.DIRECTORIES)), + ], + arguments=[ + Argument( + name="markdown", + help="Also write the diff as GitHub-flavored Markdown to PATH (empty if nothing changed).", + type=str, + metavar="PATH", + completion=Completion(type=CompletionType.FILES), + ), + ], + examples=[ + Example("./mfc.sh count_diff ../master .", "Compare this tree's LOC against another checkout"), + ], ) FP_STABILITY_COMMAND = Command( diff --git a/toolchain/mfc/count.py b/toolchain/mfc/count.py index 3e809c1eb3..f67fbee599 100644 --- a/toolchain/mfc/count.py +++ b/toolchain/mfc/count.py @@ -4,7 +4,7 @@ import rich.table -from .common import MFC_ROOT_DIR, MFCException, format_list_to_string +from .common import MFC_ROOT_DIR, file_write, format_list_to_string from .printer import cons from .state import ARG @@ -13,15 +13,12 @@ def handle_dir(mfc_dir: str, srcdirname: str) -> typing.Tuple[typing.Dict[str, i files = {} total = 0 - for filepath in glob.glob(os.path.join(mfc_dir, "src", srcdirname, "*.*f*")): + for filepath in glob.glob(os.path.join(mfc_dir, "src", srcdirname, "**", "*.*f*"), recursive=True): with open(filepath) as f: counter = 0 for line in f.read().split("\n"): - # Skip whitespace - if line.isspace() or len(line) == 0: - continue - # Skip comments but not !$acc ones! - if line.lstrip().startswith("!") and not line.lstrip().startswith("!$acc"): + # Skip whitespace and comments + if line.isspace() or len(line) == 0 or line.lstrip().startswith("!"): continue counter += 1 @@ -44,8 +41,8 @@ def count(): table.add_column(f"File (in [magenta]{codedir}[/magenta])", justify="left") table.add_column(f"Lines ([cyan]{dircount}[/cyan])", justify="right") - for filepath, n in dirfiles.items(): - table.add_row(os.path.basename(filepath), f"[bold cyan]{n}[/bold cyan]") + for filepath, n in sorted(dirfiles.items()): + table.add_row(filepath, f"[bold cyan]{n}[/bold cyan]") total += dircount @@ -56,51 +53,54 @@ def count(): cons.unindent() +def _write_markdown(filepath: str, files: list, dirs: list, total: int, total_diff: int): + # An empty file tells the CI commenter there is nothing worth saying. + if not files: + file_write(filepath, "") + return + + lines = ["### Lines of Code", "", "| File | Lines | Diff |", "| :--- | ---: | ---: |"] + lines += [f"| `{path}` | {n} | {diff:+d} |" for path, n, diff in files] + lines += ["", "| Directory | Lines | Diff |", "| :--- | ---: | ---: |"] + lines += [f"| {codedir} | {n} | {diff:+d} |" for codedir, n, diff in dirs if diff != 0] + lines += [f"| **total** | **{total}** | **{total_diff:+d}** |", ""] + + file_write(filepath, "\n".join(lines)) + + def count_diff(): + base_dir, pr_dir = ARG("base"), ARG("pr") target_str_list = format_list_to_string(ARG("targets"), "magenta") + cons.print(f"[bold]Counting lines of code in {target_str_list}[/bold] (excluding whitespace lines)") cons.indent() - total = 0 - MFC_COMPARE_DIR = os.getenv("MFC_PR") - if MFC_COMPARE_DIR is None: - raise MFCException("MFC_PR is not in your environment.") + files, dirs, total, total_diff = [], [], 0, 0 + for codedir in ["common"] + ARG("targets"): + base_files, base_count = handle_dir(base_dir, codedir) + pr_files, pr_count = handle_dir(pr_dir, codedir) - print("compare dir", MFC_COMPARE_DIR) + for filepath in set(base_files) | set(pr_files): + diff = pr_files.get(filepath, 0) - base_files.get(filepath, 0) + if diff != 0: + files.append((filepath, pr_files.get(filepath, 0), diff)) - # MFC_COMPARE_DIR="/Users/spencer/Downloads/MFC-shbfork" - for codedir in ["common"] + ARG("targets"): - dirfiles_root, dircount_root = handle_dir(MFC_ROOT_DIR, codedir) - dirfiles_pr, dircount_pr = handle_dir(MFC_COMPARE_DIR, codedir) - table = rich.table.Table(show_header=True, box=rich.table.box.SIMPLE) - table.add_column(f"File (in [magenta]{codedir}[/magenta])", justify="left") - table.add_column(f"Lines [HEAD] ([cyan]{dircount_root}[/cyan])", justify="right") - table.add_column(f"Lines [PR] ([cyan]{dircount_pr}[/cyan])", justify="right") - table.add_column("", justify="right") - table.add_column("Diff", justify="right") - - for filepath in set(dirfiles_root.keys()) | set(dirfiles_pr.keys()): - dirfiles_root[filepath] = dirfiles_root.get(filepath, 0) - dirfiles_pr[filepath] = dirfiles_pr.get(filepath, 0) - - PLUS = "++ " - MINUS = "-- " - - diff_count = dirfiles_pr[filepath] - dirfiles_root[filepath] - mycolor = "red" if diff_count > 0 else "green" - mysymbol = PLUS if diff_count > 0 else MINUS - table.add_row( - os.path.basename(filepath), - f"[bold cyan]{dirfiles_root[filepath]}[/bold cyan]", - f"[bold cyan]{dirfiles_pr[filepath]}[/bold cyan]", - mysymbol, - f"[bold {mycolor}]{diff_count}[/bold {mycolor}]", - ) - - total += dircount_root + dirs.append((codedir, pr_count, pr_count - base_count)) + total, total_diff = total + pr_count, total_diff + pr_count - base_count - cons.raw.print(table) + files.sort(key=lambda entry: (-abs(entry[2]), entry[0])) - cons.print(f"[bold]Total {target_str_list} lines: [bold cyan]{total}[/bold cyan].[/bold]") + table = rich.table.Table(show_header=True, box=rich.table.box.SIMPLE) + table.add_column("Changed file", justify="left") + table.add_column("Lines", justify="right") + table.add_column("Diff", justify="right") + for path, n, diff in files: + table.add_row(path, f"[bold cyan]{n}[/bold cyan]", f"[bold {'red' if diff > 0 else 'green'}]{diff:+d}[/bold {'red' if diff > 0 else 'green'}]") + cons.raw.print(table) + + cons.print(f"[bold]Total {target_str_list} lines: [bold cyan]{total}[/bold cyan] ([bold cyan]{total_diff:+d}[/bold cyan]).[/bold]") cons.print() cons.unindent() + + if ARG("markdown") is not None: + _write_markdown(ARG("markdown"), files, dirs, total, total_diff) From 17ae833557eca0b9a1ffb1db50299e1d9ce67cb0 Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Sun, 23 Aug 2026 16:45:59 -0500 Subject: [PATCH 2/4] ci: address review of the LOC diff comment bot Key the concurrency group on the PR number rather than github.head_ref, which is a branch name and collides across forks (two PRs from branches both named master would cancel each other, silently dropping one comment). Skip non-regular files and read with errors=replace, since under pull_request_target the counted tree is attacker-controlled: a symlinked src/*.fpp pointing at /dev/zero would otherwise read until the runner died, and one non-UTF-8 byte in a comment would fail the job. Take the comment back down when the diff returns to zero instead of leaving a stale number, which also means the workflow can no longer be filtered on src/**: a PR that stops touching src/ has to still run in order to be cleaned up. --- .github/workflows/line-count.yml | 20 ++++++++++++++++---- toolchain/mfc/count.py | 7 ++++++- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/.github/workflows/line-count.yml b/.github/workflows/line-count.yml index 9beb4df648..55c77466a7 100644 --- a/.github/workflows/line-count.yml +++ b/.github/workflows/line-count.yml @@ -4,13 +4,14 @@ name: Lines of Code # forks, whose GITHUB_TOKEN is read-only under the pull_request event. The PR's # code is checked out as data and read by the base branch's counter; it is never # executed, and no credentials are persisted alongside it. +# +# Deliberately unfiltered by path: a pull request that stops touching src/ must +# still run, so that a comment left by an earlier push can be taken back down. on: pull_request_target: - paths: - - 'src/**' concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + group: ${{ github.workflow }}-${{ github.event.number }} cancel-in-progress: true jobs: @@ -39,17 +40,28 @@ jobs: # means a PR is only ever charged for the lines it actually adds, however # far behind the branch has fallen. - name: Get Line Diff + id: diff working-directory: base run: | git fetch --no-tags origin "+refs/pull/${{ github.event.number }}/head:refs/mfc/pr" git worktree add ../mb "$(git merge-base HEAD refs/mfc/pr)" ./mfc.sh count_diff ../mb ../pr --markdown "$GITHUB_WORKSPACE/loc.md" + test -s "$GITHUB_WORKSPACE/loc.md" && echo "changed=true" >> "$GITHUB_OUTPUT" || echo "changed=false" >> "$GITHUB_OUTPUT" - name: Comment Line Diff + if: steps.diff.outputs.changed == 'true' uses: marocchino/sticky-pull-request-comment@v3 with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - ignore_empty: true skip_unchanged: true recreate: true path: loc.md + + # No line difference: take down the comment an earlier push may have left, + # rather than leaving a number that is no longer true. + - name: Remove Line Diff Comment + if: steps.diff.outputs.changed == 'false' + uses: marocchino/sticky-pull-request-comment@v3 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + delete: true diff --git a/toolchain/mfc/count.py b/toolchain/mfc/count.py index f67fbee599..b2c51ce8e4 100644 --- a/toolchain/mfc/count.py +++ b/toolchain/mfc/count.py @@ -14,7 +14,12 @@ def handle_dir(mfc_dir: str, srcdirname: str) -> typing.Tuple[typing.Dict[str, i total = 0 for filepath in glob.glob(os.path.join(mfc_dir, "src", srcdirname, "**", "*.*f*"), recursive=True): - with open(filepath) as f: + # These trees are untrusted in CI: skip anything that is not a regular + # file, and never let one stray byte fail the run. + if not os.path.isfile(filepath): + continue + + with open(filepath, errors="replace") as f: counter = 0 for line in f.read().split("\n"): # Skip whitespace and comments From a67c1d12ef8619e7ab14511a044f6a2ec365026c Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Sun, 23 Aug 2026 16:59:45 -0500 Subject: [PATCH 3/4] ci: reject symlinks in the counted tree and drop the untrusted checkout os.path.isfile follows symlinks, so the previous guard rejected /dev/zero but happily counted src/common/x.f90 -> /etc/passwd, leaking a file's existence and size into a public PR comment. Worse, glob's ** recurses into symlinked directories, so a single src/common/link -> /etc pulled in every file beneath it, none of which are themselves symlinks and none of which an islink check would catch. Require instead that each path resolve to a regular file inside the tree being counted. Read lazily rather than via f.read(), so a large file in the tree costs one line of memory instead of all of it. Materialize the PR tree with git worktree from refs/pull/N/head in the base clone rather than with actions/checkout, which removes the checkout of untrusted code that CodeQL flags in this privileged workflow. Also say comment lines are excluded in the status line, which they always were. --- .github/workflows/line-count.yml | 22 +++++++++------------- toolchain/mfc/count.py | 26 +++++++++++++------------- 2 files changed, 22 insertions(+), 26 deletions(-) diff --git a/.github/workflows/line-count.yml b/.github/workflows/line-count.yml index 55c77466a7..4592e38a93 100644 --- a/.github/workflows/line-count.yml +++ b/.github/workflows/line-count.yml @@ -2,8 +2,9 @@ name: Lines of Code # pull_request_target so that the comment can be posted on pull requests from # forks, whose GITHUB_TOKEN is read-only under the pull_request event. The PR's -# code is checked out as data and read by the base branch's counter; it is never -# executed, and no credentials are persisted alongside it. +# code is only ever opened and line-counted by the base branch's counter, which +# runs from base/. Nothing in pr/ is executed, and no step may be added that +# does so. # # Deliberately unfiltered by path: a pull request that stops touching src/ must # still run, so that a comment left by an earlier push can be taken back down. @@ -28,22 +29,17 @@ jobs: path: base fetch-depth: 0 - - name: Checkout the PR branch - uses: actions/checkout@v5 - with: - repository: ${{ github.event.pull_request.head.repo.full_name }} - ref: ${{ github.event.pull_request.head.sha }} - path: pr - persist-credentials: false - - # Counting against the merge base rather than the tip of the base branch - # means a PR is only ever charged for the lines it actually adds, however - # far behind the branch has fallen. + # The PR's tree is materialized with git from the base clone rather than + # with actions/checkout, so this privileged workflow never runs a checkout + # of untrusted code. Counting against the merge base rather than the tip + # of the base branch means a PR is only ever charged for the lines it + # actually adds, however far behind the branch has fallen. - name: Get Line Diff id: diff working-directory: base run: | git fetch --no-tags origin "+refs/pull/${{ github.event.number }}/head:refs/mfc/pr" + git worktree add ../pr refs/mfc/pr git worktree add ../mb "$(git merge-base HEAD refs/mfc/pr)" ./mfc.sh count_diff ../mb ../pr --markdown "$GITHUB_WORKSPACE/loc.md" test -s "$GITHUB_WORKSPACE/loc.md" && echo "changed=true" >> "$GITHUB_OUTPUT" || echo "changed=false" >> "$GITHUB_OUTPUT" diff --git a/toolchain/mfc/count.py b/toolchain/mfc/count.py index b2c51ce8e4..a463c9b61f 100644 --- a/toolchain/mfc/count.py +++ b/toolchain/mfc/count.py @@ -13,22 +13,22 @@ def handle_dir(mfc_dir: str, srcdirname: str) -> typing.Tuple[typing.Dict[str, i files = {} total = 0 + # These trees are untrusted in CI, so count only regular files that really + # live inside them: a symlink, or a path reached through a symlinked + # directory, otherwise names a file anywhere on the runner. Read lazily and + # tolerate stray bytes so that neither a huge file nor a non-UTF-8 one can + # take the job down. + root = os.path.realpath(os.path.join(mfc_dir, "src")) + os.sep + for filepath in glob.glob(os.path.join(mfc_dir, "src", srcdirname, "**", "*.*f*"), recursive=True): - # These trees are untrusted in CI: skip anything that is not a regular - # file, and never let one stray byte fail the run. - if not os.path.isfile(filepath): + if not os.path.isfile(filepath) or not os.path.realpath(filepath).startswith(root): continue with open(filepath, errors="replace") as f: - counter = 0 - for line in f.read().split("\n"): - # Skip whitespace and comments - if line.isspace() or len(line) == 0 or line.lstrip().startswith("!"): - continue - counter += 1 + counter = sum(1 for line in f if line.strip() and not line.lstrip().startswith("!")) - files[os.path.relpath(filepath, mfc_dir)] = counter - total += counter + files[os.path.relpath(filepath, mfc_dir)] = counter + total += counter return (files, total) @@ -36,7 +36,7 @@ def handle_dir(mfc_dir: str, srcdirname: str) -> typing.Tuple[typing.Dict[str, i def count(): target_str_list = format_list_to_string(ARG("targets"), "magenta") - cons.print(f"[bold]Counting lines of code in {target_str_list}[/bold] (excluding whitespace lines)") + cons.print(f"[bold]Counting lines of code in {target_str_list}[/bold] (excluding whitespace and comment lines)") cons.indent() total = 0 @@ -77,7 +77,7 @@ def count_diff(): base_dir, pr_dir = ARG("base"), ARG("pr") target_str_list = format_list_to_string(ARG("targets"), "magenta") - cons.print(f"[bold]Counting lines of code in {target_str_list}[/bold] (excluding whitespace lines)") + cons.print(f"[bold]Counting lines of code in {target_str_list}[/bold] (excluding whitespace and comment lines)") cons.indent() files, dirs, total, total_diff = [], [], 0, 0 From cfdfabc727e91993bfcdf4a034898a5e0cabe7cc Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Sun, 23 Aug 2026 17:02:32 -0500 Subject: [PATCH 4/4] ci: trim comments in the LOC diff bot --- .github/workflows/line-count.yml | 22 +++++++--------------- toolchain/mfc/count.py | 8 ++------ 2 files changed, 9 insertions(+), 21 deletions(-) diff --git a/.github/workflows/line-count.yml b/.github/workflows/line-count.yml index 4592e38a93..23c64e3fa0 100644 --- a/.github/workflows/line-count.yml +++ b/.github/workflows/line-count.yml @@ -1,13 +1,8 @@ name: Lines of Code -# pull_request_target so that the comment can be posted on pull requests from -# forks, whose GITHUB_TOKEN is read-only under the pull_request event. The PR's -# code is only ever opened and line-counted by the base branch's counter, which -# runs from base/. Nothing in pr/ is executed, and no step may be added that -# does so. -# -# Deliberately unfiltered by path: a pull request that stops touching src/ must -# still run, so that a comment left by an earlier push can be taken back down. +# pull_request_target: fork PRs are read-only under pull_request and cannot be +# commented on. Nothing in pr/ may ever be executed. +# Unfiltered by path: a PR that stops touching src/ must still run to clean up. on: pull_request_target: @@ -29,11 +24,9 @@ jobs: path: base fetch-depth: 0 - # The PR's tree is materialized with git from the base clone rather than - # with actions/checkout, so this privileged workflow never runs a checkout - # of untrusted code. Counting against the merge base rather than the tip - # of the base branch means a PR is only ever charged for the lines it - # actually adds, however far behind the branch has fallen. + # git worktree, not actions/checkout: no untrusted checkout in a + # privileged workflow. Merge base, not branch tip: a stale branch is not + # charged for other people's lines. - name: Get Line Diff id: diff working-directory: base @@ -53,8 +46,7 @@ jobs: recreate: true path: loc.md - # No line difference: take down the comment an earlier push may have left, - # rather than leaving a number that is no longer true. + # No diff: take down any comment an earlier push left. - name: Remove Line Diff Comment if: steps.diff.outputs.changed == 'false' uses: marocchino/sticky-pull-request-comment@v3 diff --git a/toolchain/mfc/count.py b/toolchain/mfc/count.py index a463c9b61f..5a1b1b7a8f 100644 --- a/toolchain/mfc/count.py +++ b/toolchain/mfc/count.py @@ -13,11 +13,7 @@ def handle_dir(mfc_dir: str, srcdirname: str) -> typing.Tuple[typing.Dict[str, i files = {} total = 0 - # These trees are untrusted in CI, so count only regular files that really - # live inside them: a symlink, or a path reached through a symlinked - # directory, otherwise names a file anywhere on the runner. Read lazily and - # tolerate stray bytes so that neither a huge file nor a non-UTF-8 one can - # take the job down. + # Untrusted tree in CI: a symlink must not reach off it. root = os.path.realpath(os.path.join(mfc_dir, "src")) + os.sep for filepath in glob.glob(os.path.join(mfc_dir, "src", srcdirname, "**", "*.*f*"), recursive=True): @@ -59,7 +55,7 @@ def count(): def _write_markdown(filepath: str, files: list, dirs: list, total: int, total_diff: int): - # An empty file tells the CI commenter there is nothing worth saying. + # Empty file: the CI commenter posts nothing. if not files: file_write(filepath, "") return