diff --git a/.github/workflows/astyle.yml b/.github/workflows/astyle.yml deleted file mode 100644 index 9d144e21066ae..0000000000000 --- a/.github/workflows/astyle.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: astyle - -on: pull_request - -jobs: - skip-duplicates: - continue-on-error: true - runs-on: ubuntu-latest - # Map a step output to a job output - outputs: - should_skip: ${{ steps.skip_check.outputs.should_skip }} - steps: - - id: skip_check - uses: fkirc/skip-duplicate-actions@04a1aebece824b56e6ad6a401d015479cd1c50b3 # master - with: - cancel_others: 'true' - paths: '[".github/workflows/astyle.yml", "Makefile", ".astylerc", "**.cpp", "**.h", "**.c"]' - - run: echo ${{ github.event.number }} > pull_request_id - - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 - with: - name: pull_request_id - path: pull_request_id - astyle-code: - name: astyle check - needs: skip-duplicates - if: ${{ needs.skip-duplicates.outputs.should_skip != 'true' }} - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - with: - persist-credentials: false - - - name: install dependencies - run: sudo apt-get install astyle - - - name: astyle check - run: make astyle-check - - - name: Format - if: failure() - run: make astyle-fast - - - name: Display Corrections - if: failure() - run: git diff --color diff --git a/.github/workflows/check-branch-name.yml b/.github/workflows/check-branch-name.yml index 116427454fecf..7ebf35fc6cbb9 100644 --- a/.github/workflows/check-branch-name.yml +++ b/.github/workflows/check-branch-name.yml @@ -1,6 +1,9 @@ name: Check pull request head branch name on: + # Normally risky but the workflow doesn't interpolate any user controlled content. + # i.e. no ${{ }} expressions anywhere, and none that reference anything that can be + # written by the pr author. Does not check out and execute user controlled code. pull_request_target: types: - opened @@ -11,9 +14,8 @@ jobs: if: github.head_ref == 'master' && github.repository == 'CleverRaven/Cataclysm-DDA' steps: - name: Post warning - uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 with: - github-token: ${{ secrets.GITHUB_TOKEN }} script: | await github.rest.issues.createComment({ owner: context.repo.owner, diff --git a/.github/workflows/detect-translation-file-changes.yml b/.github/workflows/detect-translation-file-changes.yml index 19fbdb46727e9..9b25ebbe445cb 100644 --- a/.github/workflows/detect-translation-file-changes.yml +++ b/.github/workflows/detect-translation-file-changes.yml @@ -2,6 +2,8 @@ name: Detect translation file changes on: + # Does not interpolate i.e. ${{ }} user controlled text into script. + # Does not checkout and run user controlled code. pull_request_target: paths: - lang/po/*.po diff --git a/.github/workflows/iwyu-linter.yml b/.github/workflows/iwyu-linter.yml deleted file mode 100644 index dcca411fd7737..0000000000000 --- a/.github/workflows/iwyu-linter.yml +++ /dev/null @@ -1,90 +0,0 @@ -name: IWYU Suggester - -on: - pull_request_target: - types: ['opened', 'reopened', 'synchronize', 'ready_for_review'] - paths: - - '**.cpp' - - '**.h' - - '**.c' - -concurrency: - group: iwyu-linter-${{ github.event.pull_request.number }} - cancel-in-progress: true - -jobs: - iwyu-suggest: - runs-on: ubuntu-24.04 - env: - COMPILER: clang++-19 - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - with: - ref: '${{ github.event.pull_request.head.sha }}' - persist-credentials: false - - - name: install dependencies - run: | - sudo apt install llvm-19-dev clang-19 libclang-19-dev cmake - pip install PyGithub - - - name: checkout IWYU repository - id: iwyu-checkout - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - with: - repository: include-what-you-use/include-what-you-use - path: include-what-you-use-src - ref: clang_19 - persist-credentials: false - - - name: cache IWYU build - id: iwyu-cache - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 - with: - path: iwyu-build - key: iwyu-clang19-ubuntu2404-${{ steps.iwyu-checkout.outputs.commit }} - - - name: build IWYU - if: steps.iwyu-cache.outputs.cache-hit != 'true' - run: | - cmake -B iwyu-build -DCMAKE_PREFIX_PATH=/usr/lib/llvm-19 \ - -DCMAKE_BUILD_TYPE=Release include-what-you-use-src - cmake --build iwyu-build --parallel 4 - - - name: determine changed files - uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 - with: - script: | - var fs = require('fs'); - const response = await github.paginate(github.rest.pulls.listFiles, - { - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: context.issue.number, - } - ); - const files = response.map(x => x.filename); - for (const path of files) { console.log(path); } - fs.writeFileSync("files_changed", files.join('\n') + '\n'); - - - name: create compilation database - run: | - cmake -B build \ - -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \ - -DCMAKE_CXX_COMPILER=$COMPILER \ - -DCMAKE_BUILD_TYPE=Release \ - -DTILES=${TILES:-0} \ - -DSOUND=${SOUND:-0} \ - -DLOCALIZE=${LOCALIZE:-0} \ - . - make includes -j4 --silent TILES=${TILES:-0} SOUND=${SOUND:-0} LOCALIZE=${LOCALIZE:-0} - - - name: run IWYU and post suggestions - if: ${{ always() }} - env: - PR_NUMBER: ${{ github.event.pull_request.number }} - COMMIT_SHA: ${{ github.event.pull_request.head.sha }} - GH_TOKEN: ${{ github.token }} - run: | - export PATH="${PWD}/iwyu-build/bin:${PWD}/include-what-you-use-src:${PATH}" - python build-scripts/ci-iwyu-suggest.py diff --git a/.github/workflows/iwyu.yml b/.github/workflows/iwyu.yml index 4c91d7c84762d..23ce421bc9935 100644 --- a/.github/workflows/iwyu.yml +++ b/.github/workflows/iwyu.yml @@ -3,9 +3,11 @@ on: push: branches: - master + - main pull_request: branches: - master + - main types: [opened, reopened, synchronize, ready_for_review] # We only care about the latest revision of a PR, so cancel all previous instances. @@ -21,7 +23,7 @@ jobs: - name: check for relevant file changes id: changed-files if: ${{ github.event_name == 'pull_request' }} - uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 with: script: | const response = await github.paginate(github.rest.pulls.listFiles, @@ -43,6 +45,8 @@ jobs: iwyu: needs: check-changes + outputs: + has_errors: ${{ steps.run_iwyu.outputs.has_errors }} if: ${{ needs.check-changes.outputs.should_run == 'true' }} runs-on: ubuntu-24.04 env: @@ -83,7 +87,7 @@ jobs: echo "IWYU_BIN_DIR=${PWD}/iwyu-build/bin">> "$GITHUB_OUTPUT" - name: determine changed files if: ${{ github.event_name == 'pull_request' }} - uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 with: script: | var fs = require('fs'); @@ -118,25 +122,62 @@ jobs: make includes -j4 --silent TILES=${TILES:-0} SOUND=${SOUND:-0} LOCALIZE=${LOCALIZE:-0} - uses: ammaraskar/gcc-problem-matcher@0f9c86f9e693db67dacf53986e1674de5f2e5f28 # master - name: run IWYU + id: run_iwyu working-directory: Cataclysm-DDA env: IWYU_SRC_DIR: ${{ steps.build-iwyu.outputs.IWYU_SRC_DIR }} IWYU_BIN_DIR: ${{ steps.build-iwyu.outputs.IWYU_BIN_DIR }} run: | + set +e PATH="${PATH}:${IWYU_BIN_DIR}:${IWYU_SRC_DIR}" python build-scripts/ci-iwyu-run.py + exit_code=$? + echo "has_errors=$exit_code" >> $GITHUB_OUTPUT + exit $exit_code + - name: Create pr comment artifacts + if: ${{ failure() && github.event_name == 'pull_request' && steps.run_iwyu.outputs.has_errors != 0 }} + working-directory: Cataclysm-DDA + run: | + set +e + cat files_changed | while read f; do + git add $f + done + git checkout . + git reset + git diff --exit-code + if [ $? -ne 0 ]; then + mkdir -p ../suggestions + git diff > ../suggestions/diff.txt + echo ${{ github.event.pull_request.number }} > ../suggestions/pr_number + echo ${{ github.event.pull_request.head.sha }} > ../suggestions/commit_sha + echo '[Include What You Use](https://github.com/akrieger/Cataclysm-DDA/blob/main/doc/c%2B%2B/DEVELOPER_TOOLING.md#include-what-you-use)' > ../suggestions/comment.txt + fi + exit 0 # Always 'succeed' this step to reduce job noise. + - name: Upload pr comment artifacts + if: ${{ failure() && github.event_name == 'pull_request' && steps.run_iwyu.outputs.has_errors != 0 }} + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: suggestions + path: suggestions/ + iwyu-result: - if: ${{ always() }} - needs: [iwyu] + if: ${{ !cancelled() }} + needs: iwyu runs-on: ubuntu-latest steps: - name: require successful IWYU + env: + JOB_RESULT: ${{ needs.iwyu.result }} + JOB_ERRORS: ${{ needs.iwyu.outputs.has_errors }} run: | - result="${{ needs.iwyu.result }}" - if [ "$result" = "success" ] || [ "$result" = "skipped" ]; then - echo "IWYU result: $result" + if [[ "$JOB_RESULT" == "skipped" ]]; then + echo "IWYU result: skipped" + exit 0 + fi + if [[ "$JOB_RESULT" == "success" && "$JOB_ERRORS" == "0" ]]; then + echo "IWYU result: success" exit 0 fi - echo "IWYU failed with result: $result" + echo "IWYU failed with result: $JOB_RESULT" exit 1 diff --git a/.github/workflows/json.yml b/.github/workflows/json.yml deleted file mode 100644 index a5089416c64ee..0000000000000 --- a/.github/workflows/json.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: JSON Validation - -on: pull_request - -jobs: - skip-duplicates: - continue-on-error: true - runs-on: ubuntu-latest - # Map a step output to a job output - outputs: - should_skip: ${{ steps.skip_check.outputs.should_skip }} - steps: - - id: skip_check - uses: fkirc/skip-duplicate-actions@04a1aebece824b56e6ad6a401d015479cd1c50b3 # master - with: - cancel_others: 'true' - paths: '["**.json", ".github/workflows/json.yml"]' - - run: echo ${{ github.event.number }} > pull_request_id - - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 - with: - name: pull_request_id - path: pull_request_id - style-json: - name: JSON style check - - runs-on: ubuntu-latest - needs: skip-duplicates - if: ${{ needs.skip-duplicates.outputs.should_skip != 'true' }} - - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - with: - persist-credentials: false - - name: JSON style check - run: make style-all-json-parallel RELEASE=1 - - name: Display Corrections - if: failure() - run: git diff --color diff --git a/.github/workflows/label-first-time-contributor.yml b/.github/workflows/label-first-time-contributor.yml index a709cddc0c1ca..a1851593fdd86 100644 --- a/.github/workflows/label-first-time-contributor.yml +++ b/.github/workflows/label-first-time-contributor.yml @@ -1,34 +1,45 @@ name: Sort new contributors for test approval on: + # Does not interpolate i.e. ${{ }} user controlled fields into the script. + # Does not check out and run user controlled code. pull_request_target: types: - opened -env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PR_NUMBER: ${{ github.event.pull_request.number }} - jobs: triage: runs-on: ubuntu-latest steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - name: Label contributors with no merged PRs + if: steps.pr-check.outputs.pr_count == 0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 with: - persist-credentials: false + script: | + script: | + // Get a list of all issues created by the PR opener + // See: https://octokit.github.io/rest.js/#pagination + const creator = context.payload.sender.login + const opts = github.rest.pulls.list.endpoint.merge({ + context.repo.owner, + context.repo.repo, + state: 'closed' + }) - - name: Check if user has any merged PRs in this reposiory - id: pr-check - run: | - author="${GITHUB_EVENT_PULL_REQUEST_USER_LOGIN}" - pr_count=$(gh pr list --state merged --author $author --json number | jq 'length') + const this_pr = context.pull_request.number; + let is_new_contributor = true; + for await (const pr of github.paginate.iterator(opts)) { + if (pr.number !== this_pr && pr.merged_at !== null) { + is_new_contributor = false; + break; + } + } - echo "Debug: $author with $pr_count merged PRs." - echo "pr_count=$pr_count" >> $GITHUB_OUTPUT - env: - GITHUB_EVENT_PULL_REQUEST_USER_LOGIN: ${{ github.event.pull_request.user.login }} - - - name: Label contributors with no merged PRs - if: steps.pr-check.outputs.pr_count == 0 - run: | - gh pr edit "$PR_NUMBER" --add-label "new contributor" + if (is_new_contributor) { + await github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ['new contributor'] + }) + } diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 2bbb59d1c6850..9f9c11efd4108 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -1,28 +1,25 @@ name: Code Style Reviewer on: - pull_request_target: - types: ['opened', 'reopened', 'synchronize', 'ready_for_review'] - paths: - - '**.json' - - '**.cpp' - - '**.h' - - '**.c' pull_request: types: ['opened', 'reopened', 'synchronize', 'ready_for_review'] paths: + - 'Makefile' + - '.astylerc' + - '.github/workflows/linter.yml' - '**.json' - '**.cpp' - '**.h' - '**.c' + - '!src/third-party/**' concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number }}-${{ github.event_name }} + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} cancel-in-progress: true jobs: style-code: - if: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.draft == false }} + if: ${{ github.event.pull_request.draft == false }} runs-on: ubuntu-latest @@ -32,13 +29,50 @@ jobs: ref: '${{ github.event.pull_request.head.sha }}' persist-credentials: false + - name: determine changed files + if: ${{ github.event_name == 'pull_request' }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + with: + script: | + var fs = require('fs'); + const response = await github.paginate(github.rest.pulls.listFiles, + { + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + } + ); + const files = response.map(x => x.filename); + for (const path of files) { + console.log(path); + } + fs.writeFileSync("files_changed", files.join('\n') + '\n'); + - run: sudo apt-get install astyle - run: make astyle-fast - run: make style-all-json-parallel - - name: 'suggester / JSON & C++' - uses: reviewdog/action-suggester@aa38384ceb608d00f84b4690cacc83a5aba307ff # v1 - if: ${{ always() }} + - name: Create pr comment artifacts + if: ${{ failure() }} + run: | + set +e + cat files_changed | while read f; do + git add $f + done + git checkout . + git reset + git diff --exit-code + if [ $? -ne 0 ]; then + mkdir -p suggestions + git diff > suggestions/diff.txt + echo ${{ github.event.pull_request.number }} > suggestions/pr_number + echo ${{ github.event.pull_request.head.sha }} > suggestions/commit_sha + echo '[JSON & C++ formatters](https://github.com/CleverRaven/Cataclysm-DDA/blob/master/doc/c++/DEVELOPER_TOOLING.md)' > suggestions/comment.txt + fi + - name: Upload pr comment artifacts + if: ${{ failure() }} + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: - tool_name: '[JSON & C++ formatters](https://github.com/CleverRaven/Cataclysm-DDA/blob/master/doc/c++/DEVELOPER_TOOLING.md)' + name: suggestions + path: suggestions/ diff --git a/.github/workflows/post-suggestions.yml b/.github/workflows/post-suggestions.yml new file mode 100644 index 0000000000000..d814511995f77 --- /dev/null +++ b/.github/workflows/post-suggestions.yml @@ -0,0 +1,40 @@ +name: Post suggestions + +on: + workflow_run: + workflows: ["IWYU (include-what-you-use)", "Code Style Reviewer"] + types: [completed] + +jobs: + iwyu-suggest: + if: ${{ github.event.workflow_run.head_branch != 'master' && github.event.workflow_run.conclusion == 'failure' }} + runs-on: ubuntu-24.04 + steps: + - name: Checkout repository + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + persist-credentials: false + - name: Download artifact + id: download-artifact + uses: dawidd6/action-download-artifact@8305c0f1062bb0d184d09ef4493ecb9288447732 # v20 + with: + workflow: ${{ github.event.workflow_run.name }} + run_id: ${{ github.event.workflow_run.id }} + name: suggestions + skip_unpack: true + if_no_artifact_found: ignore + - name: Parse suggestions + id: suggestions + if: ${{ steps.download-artifact.outputs.found_artifact == 'true' }} + run: | + # Don't trust the zip, unzip only the specific files we need in the specific locations they should go + echo extracting pr_number + unzip -p suggestions.zip pr_number > pr_number.txt + echo extracting commit_sha + unzip -p suggestions.zip commit_sha > commit_sha.txt + echo extracting comment + unzip -p suggestions.zip comment.txt > comment.txt + echo extracting diff + unzip -p suggestions.zip diff.txt > diff.txt + pip install PyGithub + python build-scripts/post-diff-as-comments.py IWYU diff --git a/build-scripts/ci-iwyu-run.py b/build-scripts/ci-iwyu-run.py index fd20c35ec787c..b72f438a6ede7 100644 --- a/build-scripts/ci-iwyu-run.py +++ b/build-scripts/ci-iwyu-run.py @@ -171,7 +171,7 @@ def filter_analyzable_files(in_files: list[Path]) -> list[Path]: def run_iwyu_on(iwyu_tool_path: str, files: list[Path]) -> int: argslist = [iwyu_tool_path] argslist.extend(str(f) for f in files) - argslist.extend(["-p", "build", "--output-format", "clang", "--jobs", "4"]) + argslist.extend(["-p", "build", "--jobs", "4"]) argslist.extend(["--"]) cdda_root = Path(__file__).parent.parent mapping_path = cdda_root / "tools/iwyu/cata.imp" @@ -182,24 +182,33 @@ def run_iwyu_on(iwyu_tool_path: str, files: list[Path]) -> int: "-Xiwyu", "--max_line_length=1000", "-Xiwyu", "--error=1"]) + fix_args = ["fix_includes.py", "--nosafe_headers", "--reorder"] + print("::group::IWYU full output") print("Running: ") print_long_list(argslist) + print("Piping output to: %s " % " ".join(fix_args)) flush_both() # start the process, consume its stdout, leave stderr be - proc = subprocess.Popen(argslist, stdout=subprocess.PIPE, encoding="utf-8") + iwyu_proc = subprocess.Popen(argslist, stdout=subprocess.PIPE, encoding="utf-8") + fix_proc = subprocess.Popen(fix_args, stdin=subprocess.PIPE, encoding="utf-8") problem_lines = [] + fix_lines = [] while True: - line = proc.stdout.readline() + line = iwyu_proc.stdout.readline() if line == '': break # IWYU finished and closed the pipe + fix_lines.append(line) line = line.strip() print(line) if "#includes/fwd-decls are correct" not in line: problem_lines.append(line) - proc.wait() + iwyu_proc.wait() + print("Applying fixes to files.") + fix_proc.communicate("\n".join(fix_lines)) + fix_proc.wait() flush_both() - print("Return code ", proc.returncode) + print("Return code ", iwyu_proc.returncode) print("::endgroup::") # remove the matcher to prevent double-posting the annotations @@ -209,12 +218,12 @@ def run_iwyu_on(iwyu_tool_path: str, files: list[Path]) -> int: print("Problems found:") for line in problem_lines: print(line) - elif proc.returncode == 0: + elif iwyu_proc.returncode == 0: print("No issues found!") else: print("No suggestions provided, but the process still failed somehow?") - return proc.returncode + return iwyu_proc.returncode # GHA truncates each line to 1024 characters. diff --git a/build-scripts/ci-iwyu-suggest.py b/build-scripts/ci-iwyu-suggest.py deleted file mode 100644 index 77199c1c718be..0000000000000 --- a/build-scripts/ci-iwyu-suggest.py +++ /dev/null @@ -1,308 +0,0 @@ -# Companion to ci-iwyu-run.py for the IWYU suggester workflow. -# Runs IWYU and pipes the output through fix_includes.py to apply -# fixes in-place, then posts review comments with suggestion blocks -# for each diff hunk. -# -# Requires environment variables (set by the workflow): -# GITHUB_REPOSITORY - e.g. "CleverRaven/Cataclysm-DDA" -# PR_NUMBER - pull request number -# COMMIT_SHA - head commit SHA of the PR -# GH_TOKEN - GitHub token for API access - -import os -import re -import subprocess -import sys - -from pathlib import Path - -CHANGED_FILES_INDEX = "files_changed" -GET_AFFECTED_FILES_SCRIPT = "build-scripts/get_affected_files.py" -BLACKLIST_PATH = "tools/iwyu/bad_files.txt" -COMMENT_MARKER = "" - - -def main(): - print("::group::Determining files to analyze") - - changed_files = get_changed_files() - print("changed files:") - print_list(changed_files) - - affected_files = get_affected_files(changed_files) - print("affected files:") - print_list(affected_files) - - files_to_analyze = filter_analyzable_files(affected_files) - print("files to analyze:") - print_list(files_to_analyze) - print("::endgroup::") - - if not files_to_analyze: - print("Nothing to analyze!") - return - - try: - run_iwyu_and_fix(files_to_analyze) - except Exception as e: - # Don't let IWYU/fix_includes failures prevent posting - # whatever partial changes were applied. - print("IWYU run failed: %s" % e, file=sys.stderr) - - diff_text = get_diff() - if not diff_text: - print("No changes after IWYU -- nothing to suggest.") - return - - file_hunks = parse_hunks(diff_text) - print("Files with IWYU changes: %s" % ", ".join(file_hunks.keys())) - - post_suggestions(file_hunks) - - -def get_changed_files() -> list[Path]: - files_index = Path(CHANGED_FILES_INDEX) - if not files_index.exists(): - print("no changed files index -- analyzing entire codebase") - return generate_global_file_list() - paths = [] - with open(files_index) as f: - for line in f.readlines(): - line = line.strip() - if line: - paths.append(Path(line)) - return paths - - -def get_affected_files(changed_files: list[Path]) -> list[Path]: - global_files = set(Path(x) for x in [ - ".github/workflows/iwyu-linter.yml", - "build-scripts/ci-iwyu-suggest.py", - "build-scripts/get_affected_files.py", - "tools/iwyu", - "CMakeLists.txt", - "src/CMakeLists.txt", - "tests/CMakeLists.txt", - ]) - for changed in changed_files: - if changed in global_files or any( - parent in global_files for parent in changed.parents): - print("File %s affects global config, analyzing all files" - % changed) - return generate_global_file_list() - - if not Path(CHANGED_FILES_INDEX).exists(): - return generate_global_file_list() - - out = subprocess.run( - [GET_AFFECTED_FILES_SCRIPT, - "--changed-files-list", CHANGED_FILES_INDEX], - capture_output=True, encoding="utf-8") - if out.returncode != 0: - print("get_affected_files.py failed (code %d)" % out.returncode, - file=sys.stderr) - print("stdout: %s" % out.stdout, file=sys.stderr) - print("stderr: %s" % out.stderr, file=sys.stderr) - sys.exit(1) - return [Path(line.strip()) for line in out.stdout.splitlines() - if line.strip()] - - -def generate_global_file_list() -> list[Path]: - paths = [] - for d in [Path("src"), Path("tests")]: - for item in os.listdir(d): - p = d / item - if p.is_file() and p.suffix == ".cpp": - paths.append(p) - paths.sort() - return paths - - -def filter_analyzable_files(in_files: list[Path]) -> list[Path]: - blacklist = [] - with open(BLACKLIST_PATH, "r") as f: - for line in f.readlines(): - line = line.strip() - if line and not line.startswith("#"): - blacklist.append(line) - - result = [] - for p in in_files: - if p.suffix != ".cpp": - continue - p_str = str(p.as_posix()) - if not any(pattern in p_str for pattern in blacklist): - result.append(p) - return result - - -def run_iwyu_and_fix(files: list[Path]): - cdda_root = Path(__file__).parent.parent - mapping_path = cdda_root / "tools/iwyu/cata.imp" - - iwyu_args = ["iwyu_tool.py"] - iwyu_args.extend(str(f) for f in files) - iwyu_args.extend(["-p", "build", "--jobs", "4"]) - iwyu_args.extend(["--"]) - iwyu_args.extend([ - "-Xiwyu", "--mapping_file=%s" % mapping_path, - "-Xiwyu", "--cxx17ns", - "-Xiwyu", "--comment_style=long", - "-Xiwyu", "--max_line_length=1000"]) - - fix_args = ["fix_includes.py", "--nosafe_headers", "--reorder"] - - print("::group::IWYU + fix_includes output") - print("Running IWYU: %s" % " ".join(iwyu_args[:5])) - print("Piping through: %s" % " ".join(fix_args)) - sys.stdout.flush() - - # Only pipe stdout to fix_includes -- stderr must stay separate - # so it doesn't corrupt the IWYU output format. - iwyu_proc = subprocess.Popen( - iwyu_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - fix_proc = subprocess.Popen( - fix_args, stdin=iwyu_proc.stdout, stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, encoding="utf-8") - iwyu_proc.stdout.close() - - fix_output, _ = fix_proc.communicate() - _, iwyu_stderr = iwyu_proc.communicate() - iwyu_proc.wait() - - if iwyu_stderr: - print("IWYU stderr:") - print(iwyu_stderr.decode("utf-8", errors="replace")) - if fix_output: - print("fix_includes output:") - print(fix_output) - print("::endgroup::") - print("IWYU exit code: %d, fix_includes exit code: %d" - % (iwyu_proc.returncode, fix_proc.returncode)) - - -def get_diff() -> str: - result = subprocess.run( - ["git", "diff", "--no-color"], - capture_output=True, encoding="utf-8") - return result.stdout.strip() - - -def parse_hunks(diff_text: str) -> dict[str, list[dict]]: - """Parse a unified diff into per-file lists of hunks. - - Each hunk has old_start, old_count (the original line range) - and new_lines (the replacement content for a suggestion block). - """ - files = {} - current_file = None - in_hunk = False - - for line in diff_text.split("\n"): - m = re.match(r"^diff --git a/.+ b/(.+)$", line) - if m: - current_file = m.group(1) - files.setdefault(current_file, []) - in_hunk = False - continue - - m = re.match(r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@", line) - if m and current_file is not None: - old_start = int(m.group(1)) - old_count = int(m.group(2)) if m.group(2) is not None else 1 - files[current_file].append({ - "old_start": old_start, - "old_count": old_count, - "new_lines": [], - }) - in_hunk = True - continue - - if in_hunk and current_file is not None and files[current_file]: - hunk = files[current_file][-1] - if line.startswith("+"): - hunk["new_lines"].append(line[1:]) - elif line.startswith(" "): - hunk["new_lines"].append(line[1:]) - # '-' lines are already counted in old_count; - # '\' (no newline) lines are ignored. - - return files - - -def post_suggestions(file_hunks: dict[str, list[dict]]): - """Post review comments with suggestion blocks for each hunk.""" - from github import Github, GithubException - - token = os.environ.get("GH_TOKEN", "") - repo_name = os.environ.get("GITHUB_REPOSITORY", "") - pr_number = os.environ.get("PR_NUMBER", "") - commit_sha = os.environ.get("COMMIT_SHA", "") - - if not all([token, repo_name, pr_number, commit_sha]): - print("Missing environment variables -- skipping comment posting.", - file=sys.stderr) - return - - print("::group::Posting IWYU suggestions") - - gh = Github(token) - repo = gh.get_repo(repo_name) - pr = repo.get_pull(int(pr_number)) - commit = repo.get_commit(commit_sha) - - # Delete stale IWYU comments from previous runs. - for comment in pr.get_review_comments(): - if COMMENT_MARKER in comment.body: - print("Deleting stale IWYU comment %d on %s" - % (comment.id, comment.path)) - comment.delete() - - # Post one suggestion per hunk. - for path, hunks in file_hunks.items(): - for hunk in hunks: - old_start = hunk["old_start"] - old_count = hunk["old_count"] - new_content = "\n".join(hunk["new_lines"]) - - if old_count < 1: - continue - - body = "%s\n```suggestion\n%s\n```" % ( - COMMENT_MARKER, new_content) - end_line = old_start + old_count - 1 - - try: - print("Posting suggestion on %s lines %d-%d" - % (path, old_start, end_line)) - if old_count > 1: - pr.create_review_comment( - body=body, commit=commit, path=path, - line=end_line, start_line=old_start, - side="RIGHT") - else: - pr.create_review_comment( - body=body, commit=commit, path=path, - line=old_start, side="RIGHT") - except GithubException as e: - print("Failed to post on %s:%d - %s" - % (path, old_start, e), file=sys.stderr) - - print("::endgroup::") - - -def print_list(things: list): - line = "" - for thing in things: - thing = str(thing) - if len(line) + len(thing) > 1000: - print(" %s" % line) - line = "" - line = "%s %s" % (line, thing) - if line: - print(" %s" % line) - - -if __name__ == '__main__': - main() diff --git a/build-scripts/post-diff-as-comments.py b/build-scripts/post-diff-as-comments.py new file mode 100644 index 0000000000000..2e8e3f696f695 --- /dev/null +++ b/build-scripts/post-diff-as-comments.py @@ -0,0 +1,151 @@ +# Posts review comments with suggestion blocks for each diff hunk. +# +# Requires environment variables (set by the workflow): +# GITHUB_REPOSITORY - e.g. "CleverRaven/Cataclysm-DDA" +# GH_TOKEN - GitHub token for API access + +import base64 +import os +import re +import subprocess +import sys + +from github import Auth, Commit, Github, GithubException, PullRequest + +def read_file(path: str): + with open(path, "rb") as f: + return f.read() + +def read_one_line(path: str): + with open(path, "rb") as f: + return f.readline().strip().decode("utf-8") + +def main(): + token = os.environ.get("GH_TOKEN", "") + repo_name = os.environ.get("GITHUB_REPOSITORY", "") + + pr_number = read_one_line("pr_number.txt") + commit_sha = read_one_line("commit_sha.txt") + diff_text = read_file("diff.txt").decode("utf-8") + comment = read_file("comment.txt") # Keep as bytes for base64 encode later + + if not all([token, repo_name, pr_number, commit_sha, comment]): + print("Missing required inputs -- skipping comment posting.", + file=sys.stderr) + return + + gh = Github(Auth.Token(token)) + repo = gh.get_repo(repo_name) + pr = repo.get_pull(int(pr_number)) + commit = repo.get_commit(commit_sha) + + comment_marker_tag = base64.b64encode(comment).decode("utf-8") + comment_marker = "".format(comment_marker_tag) + + file_hunks = parse_hunks(diff_text) + print("Files with changes: %s" % ", ".join(file_hunks.keys())) + + post_suggestions( + file_hunks, + comment_marker, + comment.decode("utf-8"), + pr, + commit, + ) + return + + +def parse_hunks(diff_text: str) -> dict[str, list[dict]]: + """Parse a unified diff into per-file lists of hunks. + + Each hunk has old_start, old_count (the original line range) + and new_lines (the replacement content for a suggestion block). + """ + files = {} + current_file = None + in_hunk = False + + for line in diff_text.split("\n"): + m = re.match(r"^diff --git a/.+ b/(.+)$", line) + if m: + current_file = m.group(1) + files.setdefault(current_file, []) + in_hunk = False + continue + + m = re.match(r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@", line) + if m and current_file is not None: + old_start = int(m.group(1)) + old_count = int(m.group(2)) if m.group(2) is not None else 1 + files[current_file].append({ + "old_start": old_start, + "old_count": old_count, + "new_lines": [], + }) + in_hunk = True + continue + + if in_hunk and current_file is not None and files[current_file]: + hunk = files[current_file][-1] + if line.startswith("+"): + hunk["new_lines"].append(line[1:]) + elif line.startswith(" "): + hunk["new_lines"].append(line[1:]) + # '-' lines are already counted in old_count; + # '\' (no newline) lines are ignored. + + return files + + +def post_suggestions( + file_hunks: dict[str, list[dict]], + comment_marker: str, + comment_text: str, + pr: PullRequest, + commit: Commit, +): + """Post review comments with suggestion blocks for each hunk.""" + print("::group::Posting suggestions") + + # Delete stale comments from previous runs. + for comment in pr.get_review_comments(): + if comment_marker in comment.body: + print("Deleting stale comment %d on %s" + % (comment.id, comment.path)) + comment.delete() + + # Post one suggestion per hunk. + for path, hunks in file_hunks.items(): + for hunk in hunks: + old_start = hunk["old_start"] + old_count = hunk["old_count"] + new_content = "\n".join(hunk["new_lines"]) + + if old_count < 1: + continue + + body = "%s\n%s\n```suggestion\n%s\n```" % ( + comment_marker, comment_text, new_content) + end_line = old_start + old_count - 1 + + try: + print("Posting suggestion on %s lines %d-%d" + % (path, old_start, end_line)) + if old_count > 1: + pr.create_review_comment( + body=body, commit=commit, path=path, + line=end_line, start_line=old_start, + side="RIGHT") + else: + pr.create_review_comment( + body=body, commit=commit, path=path, + line=old_start, side="RIGHT") + except GithubException as e: + print("Failed to post on %s:%d - %s" + % (path, old_start, e), file=sys.stderr) + + print("::endgroup::") + + +if __name__ == "__main__": + main() diff --git a/data/json/statistics.json b/data/json/statistics.json index 70eb73b6eeab5..e0d0312df4024 100644 --- a/data/json/statistics.json +++ b/data/json/statistics.json @@ -4,7 +4,7 @@ "type": "event_statistic", "stat_type": "last_value", "event_type": "game_avatar_new", - "field": "avatar_id" + "field": "avatar_id" }, { "id": "last_words", diff --git a/src/iuse_software.h b/src/iuse_software.h index 349afa74f017b..7c0b3e070e301 100644 --- a/src/iuse_software.h +++ b/src/iuse_software.h @@ -4,6 +4,11 @@ #include #include +#include +#include +#include +#include +#include bool play_videogame( const std::string &function_name, std::map &game_data, diff --git a/src/path_info.cpp b/src/path_info.cpp index 84bfc00c9950c..68e003e154fb8 100644 --- a/src/path_info.cpp +++ b/src/path_info.cpp @@ -115,7 +115,7 @@ void PATH_INFO::set_standard_filenames() // Data is always relative to itself. Also, the base path might not be writeable. datadir_path_value = cata_path{ cata_path::root_path::data, std::filesystem::path{} }; - if( !base_path_value.empty() ) { + if(!base_path_value.empty()) { #if defined(DATA_DIR_PREFIX) datadir_value = base_path_value + "share/cataclysm-dda/"; prefix = datadir_value;