From f9551ac5ca08bb851b2a17d2f5259de13c105876 Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Wed, 27 May 2026 20:24:18 +0100 Subject: [PATCH] style: Run shfmt on workflows, actions and markdown bash code (#7333) --- .github/actions/build-deps/action.yml | 18 +- .github/actions/generate-version/action.yml | 10 +- .github/scripts/format-inline-bash.py | 403 ++++++++++++++++++ .github/workflows/build-nix-image.yml | 4 +- .github/workflows/check-pr-description.yml | 10 +- .github/workflows/check-pr-title.yml | 2 +- .github/workflows/on-pr.yml | 8 +- .github/workflows/pre-commit.yml | 2 +- .../workflows/reusable-build-docker-image.yml | 2 +- .../workflows/reusable-build-test-config.yml | 76 ++-- .../workflows/reusable-check-levelization.yml | 10 +- .github/workflows/reusable-check-rename.yml | 10 +- .github/workflows/reusable-clang-tidy.yml | 48 +-- .github/workflows/reusable-package.yml | 2 +- .../workflows/reusable-strategy-matrix.yml | 2 +- .pre-commit-config.yaml | 13 + BUILD.md | 6 +- cspell.config.yaml | 1 + include/xrpl/protocol_autogen/README.md | 4 +- package/README.md | 18 +- 20 files changed, 533 insertions(+), 116 deletions(-) create mode 100755 .github/scripts/format-inline-bash.py diff --git a/.github/actions/build-deps/action.yml b/.github/actions/build-deps/action.yml index 9d52be1998..0891d56dfa 100644 --- a/.github/actions/build-deps/action.yml +++ b/.github/actions/build-deps/action.yml @@ -37,12 +37,12 @@ runs: run: | echo 'Installing dependencies.' conan install \ - --profile ci \ - --build="${BUILD_OPTION}" \ - --options:host='&:tests=True' \ - --options:host='&:xrpld=True' \ - --settings:all build_type="${BUILD_TYPE}" \ - --conf:all tools.build:jobs=${BUILD_NPROC} \ - --conf:all tools.build:verbosity="${LOG_VERBOSITY}" \ - --conf:all tools.compilation:verbosity="${LOG_VERBOSITY}" \ - . + --profile ci \ + --build="${BUILD_OPTION}" \ + --options:host='&:tests=True' \ + --options:host='&:xrpld=True' \ + --settings:all build_type="${BUILD_TYPE}" \ + --conf:all tools.build:jobs=${BUILD_NPROC} \ + --conf:all tools.build:verbosity="${LOG_VERBOSITY}" \ + --conf:all tools.compilation:verbosity="${LOG_VERBOSITY}" \ + . diff --git a/.github/actions/generate-version/action.yml b/.github/actions/generate-version/action.yml index 8edb7920c6..50b3166596 100644 --- a/.github/actions/generate-version/action.yml +++ b/.github/actions/generate-version/action.yml @@ -15,7 +15,7 @@ runs: shell: bash env: VERSION: ${{ github.ref_name }} - run: echo "VERSION=${VERSION}" >> "${GITHUB_ENV}" + run: echo "VERSION=${VERSION}" >>"${GITHUB_ENV}" # When a tag is not pushed, then the version (e.g. 1.2.3-b0) is extracted # from the BuildInfo.cpp file and the shortened commit hash appended to it. @@ -28,17 +28,17 @@ runs: echo 'Extracting version from BuildInfo.cpp.' VERSION="$(cat src/libxrpl/protocol/BuildInfo.cpp | grep "versionString =" | awk -F '"' '{print $2}')" if [[ -z "${VERSION}" ]]; then - echo 'Unable to extract version from BuildInfo.cpp.' - exit 1 + echo 'Unable to extract version from BuildInfo.cpp.' + exit 1 fi echo 'Appending shortened commit hash to version.' SHA='${{ github.sha }}' VERSION="${VERSION}+${SHA:0:7}" - echo "VERSION=${VERSION}" >> "${GITHUB_ENV}" + echo "VERSION=${VERSION}" >>"${GITHUB_ENV}" - name: Output version id: version shell: bash - run: echo "version=${VERSION}" >> "${GITHUB_OUTPUT}" + run: echo "version=${VERSION}" >>"${GITHUB_OUTPUT}" diff --git a/.github/scripts/format-inline-bash.py b/.github/scripts/format-inline-bash.py new file mode 100755 index 0000000000..423c78109c --- /dev/null +++ b/.github/scripts/format-inline-bash.py @@ -0,0 +1,403 @@ +#!/usr/bin/env python3 + +""" +Format embedded shell snippets using the shfmt hook configured in +.pre-commit-config.yaml. + +Two shapes are recognised: + +* YAML workflow/action files: literal block-scalar runs (`run: |`) and + single-line runs (`run: some command`). A single-line run is upgraded to + a `run: |` block scalar if shfmt's output spans multiple lines. + +* Markdown files: ``` ```bash ``` fenced code blocks. + +Any block that shfmt cannot parse is skipped with a warning on stderr, so +the file is left untouched and surrounding blocks still get formatted. + +For each occurrence the body is dedented, written to a temp .sh file, +formatted via `pre-commit run shfmt --files ` (falling back to +`prek`), then re-indented and written back in place. + +When invoked without arguments, every .yml/.yaml under .github/ plus every +.md file in the repo is scanned. When invoked with file arguments (the +pre-commit case), only those files are processed. +""" + +from __future__ import annotations + +import re +import shutil +import subprocess +import sys +import tempfile +from dataclasses import dataclass +from pathlib import Path +from typing import Union + +REPO = Path(__file__).resolve().parents[2] + +_HOOK_RUNNER = next((cmd for cmd in ("pre-commit", "prek") if shutil.which(cmd)), None) +if _HOOK_RUNNER is None: + sys.exit("error: neither `pre-commit` nor `prek` found on PATH") + +RUN_BLOCK_RE = re.compile(r"^(?P[ \t]*(?:- )?)run:[ \t]*\|[+-]?[ \t]*$") +RUN_INLINE_RE = re.compile( + r"^(?P[ \t]*(?:- )?)run:[ \t]+" r"(?P(?!\|[+-]?[ \t]*$)\S.*?)[ \t]*$" +) +MD_BASH_OPEN_RE = re.compile(r"^(?P[ ]{0,3})`{3}bash[ \t]*$") +MD_FENCE_CLOSE_RE = re.compile(r"^[ ]{0,3}`{3,}[ \t]*$") + + +@dataclass(frozen=True) +class BlockRun: + """A `run: |` block scalar; `body_start:body_end` slices into `lines`.""" + + body_start: int + body_end: int + body_indent: int + + +@dataclass(frozen=True) +class InlineRun: + """A single-line `run: value` at `line_idx`.""" + + line_idx: int + prefix: str + value: str + + +@dataclass(frozen=True) +class MdBashBlock: + """A markdown ``` ```bash ``` fenced code block. + + `body_start:body_end` slices into the file's lines; `open_line_idx` + points at the opening fence line. + """ + + open_line_idx: int + body_start: int + body_end: int + body_indent: int + + +RunItem = Union[BlockRun, InlineRun] + + +def _scan_block_body( + lines: list[str], body_start: int, run_col: int +) -> tuple[int | None, int]: + """Locate the body of a `run: |` block scalar starting at `body_start`. + + Returns `(body_indent, scan_end)`. `scan_end` is the line index where the + outer scanner should resume. `body_indent` is `None` when no body is + present (the scalar is empty, or the next non-blank line has indent + `<= run_col`). + """ + body_indent: int | None = None + scan_end = len(lines) + for idx in range(body_start, len(lines)): + line = lines[idx] + if line.strip() == "": + continue + indent = len(line) - len(line.lstrip(" ")) + if body_indent is None: + if indent > run_col: + body_indent = indent + else: + scan_end = idx + break + elif indent < body_indent: + scan_end = idx + break + if body_indent is not None: + while scan_end > body_start and lines[scan_end - 1].strip() == "": + scan_end -= 1 + if scan_end <= body_start: + body_indent = None + return body_indent, scan_end + + +def find_run_blocks(lines: list[str]) -> list[RunItem]: + """Return run items in document order.""" + items: list[RunItem] = [] + line_idx = 0 + while line_idx < len(lines): + line = lines[line_idx] + if block_match := RUN_BLOCK_RE.match(line): + run_col = len(block_match.group("prefix")) + body_start = line_idx + 1 + body_indent, scan_end = _scan_block_body(lines, body_start, run_col) + if body_indent is not None: + items.append( + BlockRun( + body_start=body_start, + body_end=scan_end, + body_indent=body_indent, + ) + ) + line_idx = scan_end + continue + if inline_match := RUN_INLINE_RE.match(line): + items.append( + InlineRun( + line_idx=line_idx, + prefix=inline_match.group("prefix"), + value=inline_match.group("value"), + ) + ) + line_idx += 1 + return items + + +def find_md_bash_blocks(lines: list[str]) -> list[MdBashBlock]: + """Return ``` ```bash ``` fenced code blocks in document order.""" + blocks: list[MdBashBlock] = [] + line_idx = 0 + while line_idx < len(lines): + open_match = MD_BASH_OPEN_RE.match(lines[line_idx]) + if not open_match: + line_idx += 1 + continue + body_start = line_idx + 1 + close_idx = next( + ( + j + for j in range(body_start, len(lines)) + if MD_FENCE_CLOSE_RE.match(lines[j]) + ), + None, + ) + if close_idx is None: + line_idx = body_start + continue + body = lines[body_start:close_idx] + non_blank = [b for b in body if b.strip()] + body_indent = ( + min(len(b) - len(b.lstrip(" ")) for b in non_blank) + if non_blank + else len(open_match.group("indent")) + ) + blocks.append( + MdBashBlock( + open_line_idx=line_idx, + body_start=body_start, + body_end=close_idx, + body_indent=body_indent, + ) + ) + line_idx = close_idx + 1 + return blocks + + +def dedent(lines: list[str], n: int) -> list[str]: + pad = " " * n + return [ + ( + "" + if line.strip() == "" + else (line[n:] if line.startswith(pad) else line.lstrip(" ")) + ) + for line in lines + ] + + +def reindent(lines: list[str], n: int) -> list[str]: + pad = " " * n + return [pad + line if line else "" for line in lines] + + +_SHFMT_ERR_RE = re.compile(r"\.sh:\d+:\d+:\s") +_GHA_EXPR_RE = re.compile(r"\$\{\{.*?\}\}", re.DOTALL) +_GHA_PLACEHOLDER_RE = re.compile(r"__GHA_EXPR_(\d+)__") + + +def _encode_gha_exprs(text: str) -> tuple[str, list[str]]: + """Replace `${{ ... }}` expressions with bash-safe placeholder identifiers.""" + exprs: list[str] = [] + + def repl(match: re.Match[str]) -> str: + exprs.append(match.group(0)) + return f"__GHA_EXPR_{len(exprs) - 1}__" + + return _GHA_EXPR_RE.sub(repl, text), exprs + + +def _decode_gha_exprs(text: str, exprs: list[str]) -> str: + """Restore `${{ ... }}` expressions from placeholder identifiers.""" + return _GHA_PLACEHOLDER_RE.sub(lambda m: exprs[int(m.group(1))], text) + + +def shfmt_via_hook(tmp_path: Path) -> tuple[bool, str]: + # `${{ ... }}` is not valid shell, so swap it for a placeholder identifier + # that shfmt can parse, then restore it after formatting. + encoded, exprs = _encode_gha_exprs(tmp_path.read_text()) + if exprs: + tmp_path.write_text(encoded) + res = subprocess.run( + [_HOOK_RUNNER, "run", "shfmt", "--files", str(tmp_path)], + cwd=REPO, + capture_output=True, + text=True, + ) + output = res.stdout + res.stderr + # shfmt emits parse errors as "::: ". + parse_err = bool(_SHFMT_ERR_RE.search(output)) + # A non-zero exit that is neither a parse error nor pre-commit's "I had + # to modify files" signal means the hook itself failed to run (missing + # binary, install failure, bad config, ...). Surface that loudly rather + # than silently treating it as a no-op. + if ( + res.returncode != 0 + and not parse_err + and "files were modified by this hook" not in output + ): + sys.exit( + f"error: `{_HOOK_RUNNER} run shfmt` failed with exit {res.returncode}:\n{output}" + ) + if exprs and not parse_err: + tmp_path.write_text(_decode_gha_exprs(tmp_path.read_text(), exprs)) + return not parse_err, output + + +def _skip(path: Path, where: int, kind: str, output: str) -> None: + print( + f" shfmt could not parse {kind} at {path}:{where + 1} — skipped", + file=sys.stderr, + ) + print(f" {output.strip()}", file=sys.stderr) + + +def process_yaml_file(path: Path, tmp_path: Path) -> int: + text = path.read_text() + had_nl = text.endswith("\n") + lines = text.split("\n") + if had_nl: + lines = lines[:-1] + items = find_run_blocks(lines) + if not items: + return 0 + changed = 0 + # Process in reverse so earlier indices remain valid as we splice. + for item in reversed(items): + if isinstance(item, BlockRun): + body = lines[item.body_start : item.body_end] + tmp_path.write_text("\n".join(dedent(body, item.body_indent)) + "\n") + ok, output = shfmt_via_hook(tmp_path) + if not ok: + _skip(path, item.body_start, "block", output) + continue + formatted = tmp_path.read_text().rstrip("\n") + new_body = reindent(formatted.split("\n"), item.body_indent) + if new_body != body: + lines[item.body_start : item.body_end] = new_body + changed += 1 + else: + tmp_path.write_text(item.value + "\n") + ok, output = shfmt_via_hook(tmp_path) + if not ok: + _skip(path, item.line_idx, "inline run", output) + continue + formatted = tmp_path.read_text().rstrip("\n") + if formatted == item.value: + continue + formatted_lines = formatted.split("\n") + if len(formatted_lines) == 1: + lines[item.line_idx] = f"{item.prefix}run: {formatted}" + else: + body_indent = len(item.prefix) + 2 + lines[item.line_idx : item.line_idx + 1] = [ + f"{item.prefix}run: |", + *reindent(formatted_lines, body_indent), + ] + changed += 1 + new_text = "\n".join(lines) + ("\n" if had_nl else "") + if new_text != text: + path.write_text(new_text) + return changed + + +def process_md_file(path: Path, tmp_path: Path) -> int: + text = path.read_text() + had_nl = text.endswith("\n") + lines = text.split("\n") + if had_nl: + lines = lines[:-1] + blocks = find_md_bash_blocks(lines) + if not blocks: + return 0 + changed = 0 + for block in reversed(blocks): + body = lines[block.body_start : block.body_end] + tmp_path.write_text("\n".join(dedent(body, block.body_indent)) + "\n") + ok, output = shfmt_via_hook(tmp_path) + if not ok: + _skip(path, block.open_line_idx, "```bash block", output) + continue + formatted = tmp_path.read_text().rstrip("\n") + formatted_lines = formatted.split("\n") if formatted else [] + new_body = reindent(formatted_lines, block.body_indent) + if new_body != body: + lines[block.body_start : block.body_end] = new_body + changed += 1 + new_text = "\n".join(lines) + ("\n" if had_nl else "") + if new_text != text: + path.write_text(new_text) + return changed + + +def process_file(path: Path, tmp_path: Path) -> int: + if path.suffix in (".yml", ".yaml"): + return process_yaml_file(path, tmp_path) + if path.suffix == ".md": + return process_md_file(path, tmp_path) + return 0 + + +def gather_files(argv: list[str]) -> list[Path]: + """Return YAML workflow/action files and markdown files that we should + process — either the paths in `argv` or, when `argv` is empty, every + such file in the repo (skipping `external/`).""" + if argv: + candidates: list[Path] = [ + (REPO / a).resolve() if not Path(a).is_absolute() else Path(a) for a in argv + ] + else: + gh = REPO / ".github" + candidates = [ + *gh.rglob("*.yml"), + *gh.rglob("*.yaml"), + *( + p + for p in REPO.rglob("*.md") + if "external" not in p.relative_to(REPO).parts + ), + ] + return sorted( + p + for p in candidates + if p.exists() + and ( + (p.suffix in (".yml", ".yaml") and ".github" in p.parts) + or p.suffix == ".md" + ) + ) + + +def main(argv: list[str]) -> int: + files = gather_files(argv) + if not files: + return 0 + with tempfile.TemporaryDirectory(prefix="format-inline-bash-") as tmpdir: + tmp_path = Path(tmpdir) / "shfmt.sh" + total = 0 + for f in files: + n = process_file(f, tmp_path) + if n: + print(f"{f.relative_to(REPO)}: reformatted {n} block(s)") + total += n + return 1 if total else 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/.github/workflows/build-nix-image.yml b/.github/workflows/build-nix-image.yml index edd35132fa..bae4cfd437 100644 --- a/.github/workflows/build-nix-image.yml +++ b/.github/workflows/build-nix-image.yml @@ -100,8 +100,8 @@ jobs: - name: Create multi-arch manifests run: | - for tag in $(jq -cr '.tags[]' <<< "$DOCKER_METADATA_OUTPUT_JSON"); do - docker buildx imagetools create -t "$tag" "${tag}-amd64" "${tag}-arm64" + for tag in $(jq -cr '.tags[]' <<<"$DOCKER_METADATA_OUTPUT_JSON"); do + docker buildx imagetools create -t "$tag" "${tag}-amd64" "${tag}-arm64" done - name: Inspect image diff --git a/.github/workflows/check-pr-description.yml b/.github/workflows/check-pr-description.yml index f6eee50291..3f345a9dcb 100644 --- a/.github/workflows/check-pr-description.yml +++ b/.github/workflows/check-pr-description.yml @@ -20,11 +20,11 @@ jobs: env: PR_BODY: ${{ github.event.pull_request.body }} if: ${{ github.event_name == 'pull_request' }} - run: printenv PR_BODY > pr_body.md + run: printenv PR_BODY >pr_body.md - name: Check PR description differs from template if: ${{ github.event_name == 'pull_request' }} - run: > - python .github/scripts/check-pr-description.py - --template-file .github/pull_request_template.md - --pr-body-file pr_body.md + run: | + python .github/scripts/check-pr-description.py \ + --template-file .github/pull_request_template.md \ + --pr-body-file pr_body.md diff --git a/.github/workflows/check-pr-title.yml b/.github/workflows/check-pr-title.yml index 5631950df6..79962e0620 100644 --- a/.github/workflows/check-pr-title.yml +++ b/.github/workflows/check-pr-title.yml @@ -11,4 +11,4 @@ on: jobs: check_title: if: ${{ github.event.pull_request.draft != true }} - uses: XRPLF/actions/.github/workflows/check-pr-title.yml@291206777251b4d493641b5afbdf7c23009d2988 + uses: XRPLF/actions/.github/workflows/check-pr-title.yml@cba1f0891650baf1a9c88624dc2d72573be2eb81 diff --git a/.github/workflows/on-pr.yml b/.github/workflows/on-pr.yml index ca715e0376..db3c8667e5 100644 --- a/.github/workflows/on-pr.yml +++ b/.github/workflows/on-pr.yml @@ -98,7 +98,7 @@ jobs: READY: ${{ contains(github.event.pull_request.labels.*.name, 'Ready to merge') }} MERGE: ${{ github.event_name == 'merge_group' }} run: | - echo "go=${{ (env.DRAFT != 'true' && env.READY == 'true') || env.FILES == 'true' || env.MERGE == 'true' }}" >> "${GITHUB_OUTPUT}" + echo "go=${{ (env.DRAFT != 'true' && env.READY == 'true') || env.FILES == 'true' || env.MERGE == 'true' }}" >>"${GITHUB_OUTPUT}" cat "${GITHUB_OUTPUT}" outputs: go: ${{ steps.go.outputs.go == 'true' }} @@ -168,9 +168,9 @@ jobs: PR_URL: ${{ github.event.pull_request.html_url }} run: | gh api --method POST -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" \ - /repos/xrplf/clio/dispatches -f "event_type=check_libxrpl" \ - -F "client_payload[ref]=${{ needs.upload-recipe.outputs.recipe_ref }}" \ - -F "client_payload[pr_url]=${PR_URL}" + /repos/xrplf/clio/dispatches -f "event_type=check_libxrpl" \ + -F "client_payload[ref]=${{ needs.upload-recipe.outputs.recipe_ref }}" \ + -F "client_payload[pr_url]=${PR_URL}" passed: if: failure() || cancelled() diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 2f15b82266..de6a4f40b4 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -14,7 +14,7 @@ on: jobs: # Call the workflow in the XRPLF/actions repo that runs the pre-commit hooks. run-hooks: - uses: XRPLF/actions/.github/workflows/pre-commit.yml@5e942d61bf32f7557a7c159cfac4712a687b3e3a + uses: XRPLF/actions/.github/workflows/pre-commit.yml@cba1f0891650baf1a9c88624dc2d72573be2eb81 with: runs_on: ubuntu-latest container: '{ "image": "ghcr.io/xrplf/ci/tools-rippled-pre-commit:sha-41ec7c1" }' diff --git a/.github/workflows/reusable-build-docker-image.yml b/.github/workflows/reusable-build-docker-image.yml index 5b555b713f..c3795e56fa 100644 --- a/.github/workflows/reusable-build-docker-image.yml +++ b/.github/workflows/reusable-build-docker-image.yml @@ -53,7 +53,7 @@ jobs: env: PLATFORM: ${{ inputs.platform }} run: | - echo "arch=${PLATFORM##*/}" >> $GITHUB_OUTPUT + echo "arch=${PLATFORM##*/}" >>$GITHUB_OUTPUT - name: Set up Docker Buildx uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 diff --git a/.github/workflows/reusable-build-test-config.yml b/.github/workflows/reusable-build-test-config.yml index 4f9b926e98..31457bb892 100644 --- a/.github/workflows/reusable-build-test-config.yml +++ b/.github/workflows/reusable-build-test-config.yml @@ -113,7 +113,7 @@ jobs: - name: Set ccache log file if: ${{ inputs.ccache_enabled && runner.debug == '1' }} - run: echo "CCACHE_LOGFILE=${{ runner.temp }}/ccache.log" >> "${GITHUB_ENV}" + run: echo "CCACHE_LOGFILE=${{ runner.temp }}/ccache.log" >>"${GITHUB_ENV}" - name: Print build environment uses: XRPLF/actions/print-build-env@59dec886e4afb05a1724443af08baccbc045b574 @@ -146,11 +146,11 @@ jobs: CMAKE_ARGS: ${{ inputs.cmake_args }} run: | cmake \ - -G '${{ runner.os == 'Windows' && 'Visual Studio 17 2022' || 'Ninja' }}' \ - -DCMAKE_TOOLCHAIN_FILE:FILEPATH=build/generators/conan_toolchain.cmake \ - -DCMAKE_BUILD_TYPE="${BUILD_TYPE}" \ - ${CMAKE_ARGS} \ - .. + -G '${{ runner.os == 'Windows' && 'Visual Studio 17 2022' || 'Ninja' }}' \ + -DCMAKE_TOOLCHAIN_FILE:FILEPATH=build/generators/conan_toolchain.cmake \ + -DCMAKE_BUILD_TYPE="${BUILD_TYPE}" \ + ${CMAKE_ARGS} \ + .. - name: Check protocol autogen files are up-to-date working-directory: ${{ env.BUILD_DIR }} @@ -172,10 +172,10 @@ jobs: cmake --build . --target code_gen DIFF=$(git -C .. status --porcelain -- include/xrpl/protocol_autogen src/tests/libxrpl/protocol_autogen) if [ -n "${DIFF}" ]; then - echo "::error::Generated protocol files are out of date" - git -C .. diff -- include/xrpl/protocol_autogen src/tests/libxrpl/protocol_autogen - echo "${MESSAGE}" - exit 1 + echo "::error::Generated protocol files are out of date" + git -C .. diff -- include/xrpl/protocol_autogen src/tests/libxrpl/protocol_autogen + echo "${MESSAGE}" + exit 1 fi - name: Build the binary @@ -186,18 +186,18 @@ jobs: CMAKE_TARGET: ${{ inputs.cmake_target }} run: | cmake \ - --build . \ - --config "${BUILD_TYPE}" \ - --parallel "${BUILD_NPROC}" \ - --target "${CMAKE_TARGET}" + --build . \ + --config "${BUILD_TYPE}" \ + --parallel "${BUILD_NPROC}" \ + --target "${CMAKE_TARGET}" - name: Show ccache statistics if: ${{ inputs.ccache_enabled }} run: | ccache --show-stats -vv if [ '${{ runner.debug }}' = '1' ]; then - cat "${CCACHE_LOGFILE}" - curl ${CCACHE_REMOTE_STORAGE%|*}/status || true + cat "${CCACHE_LOGFILE}" + curl ${CCACHE_REMOTE_STORAGE%|*}/status || true fi - name: Upload the binary (Linux) @@ -214,7 +214,7 @@ jobs: working-directory: ${{ env.BUILD_DIR }} run: | set -o pipefail - ./xrpld --definitions | python3 -m json.tool > server_definitions.json + ./xrpld --definitions | python3 -m json.tool >server_definitions.json - name: Upload server definitions if: ${{ github.event.repository.visibility == 'public' && inputs.config_name == 'debian-bookworm-gcc-13-amd64-release' }} @@ -231,10 +231,10 @@ jobs: run: | ldd ./xrpld if [ "$(ldd ./xrpld | grep -E '(libstdc\+\+|libgcc)' | wc -l)" -eq 0 ]; then - echo 'The binary is statically linked.' + echo 'The binary is statically linked.' else - echo 'The binary is dynamically linked.' - exit 1 + echo 'The binary is dynamically linked.' + exit 1 fi - name: Verify presence of instrumentation (Linux) @@ -250,12 +250,12 @@ jobs: run: | ASAN_OPTS="include=${GITHUB_WORKSPACE}/sanitizers/suppressions/runtime-asan-options.txt:suppressions=${GITHUB_WORKSPACE}/sanitizers/suppressions/asan.supp" if [[ "${CONFIG_NAME}" == *gcc* ]]; then - ASAN_OPTS="${ASAN_OPTS}:alloc_dealloc_mismatch=0" + ASAN_OPTS="${ASAN_OPTS}:alloc_dealloc_mismatch=0" fi - echo "ASAN_OPTIONS=${ASAN_OPTS}" >> ${GITHUB_ENV} - echo "TSAN_OPTIONS=include=${GITHUB_WORKSPACE}/sanitizers/suppressions/runtime-tsan-options.txt:suppressions=${GITHUB_WORKSPACE}/sanitizers/suppressions/tsan.supp" >> ${GITHUB_ENV} - echo "UBSAN_OPTIONS=include=${GITHUB_WORKSPACE}/sanitizers/suppressions/runtime-ubsan-options.txt:suppressions=${GITHUB_WORKSPACE}/sanitizers/suppressions/ubsan.supp" >> ${GITHUB_ENV} - echo "LSAN_OPTIONS=include=${GITHUB_WORKSPACE}/sanitizers/suppressions/runtime-lsan-options.txt:suppressions=${GITHUB_WORKSPACE}/sanitizers/suppressions/lsan.supp" >> ${GITHUB_ENV} + echo "ASAN_OPTIONS=${ASAN_OPTS}" >>${GITHUB_ENV} + echo "TSAN_OPTIONS=include=${GITHUB_WORKSPACE}/sanitizers/suppressions/runtime-tsan-options.txt:suppressions=${GITHUB_WORKSPACE}/sanitizers/suppressions/tsan.supp" >>${GITHUB_ENV} + echo "UBSAN_OPTIONS=include=${GITHUB_WORKSPACE}/sanitizers/suppressions/runtime-ubsan-options.txt:suppressions=${GITHUB_WORKSPACE}/sanitizers/suppressions/ubsan.supp" >>${GITHUB_ENV} + echo "LSAN_OPTIONS=include=${GITHUB_WORKSPACE}/sanitizers/suppressions/runtime-lsan-options.txt:suppressions=${GITHUB_WORKSPACE}/sanitizers/suppressions/lsan.supp" >>${GITHUB_ENV} - name: Run the separate tests if: ${{ !inputs.build_only }} @@ -266,9 +266,9 @@ jobs: PARALLELISM: ${{ runner.os == 'Windows' && '1' || steps.nproc.outputs.nproc }} run: | ctest \ - --output-on-failure \ - -C "${BUILD_TYPE}" \ - -j "${PARALLELISM}" + --output-on-failure \ + -C "${BUILD_TYPE}" \ + -j "${PARALLELISM}" - name: Run the embedded tests if: ${{ !inputs.build_only }} @@ -278,7 +278,7 @@ jobs: run: | set -o pipefail # Coverage builds are slower due to instrumentation; use fewer parallel jobs to avoid flakiness - [ "$COVERAGE_ENABLED" = "true" ] && BUILD_NPROC=$(( BUILD_NPROC - 2 )) + [ "$COVERAGE_ENABLED" = "true" ] && BUILD_NPROC=$((BUILD_NPROC - 2)) ./xrpld --unittest --unittest-jobs "${BUILD_NPROC}" 2>&1 | tee unittest.log - name: Show test failure summary @@ -287,19 +287,19 @@ jobs: WORKING_DIR: ${{ runner.os == 'Windows' && format('{0}\{1}', env.BUILD_DIR, inputs.build_type) || env.BUILD_DIR }} run: | if [ ! -d "${WORKING_DIR}" ]; then - echo "Working directory '${WORKING_DIR}' does not exist." - exit 0 + echo "Working directory '${WORKING_DIR}' does not exist." + exit 0 fi cd "${WORKING_DIR}" if [ ! -f unittest.log ]; then - echo "unittest.log not found; embedded tests may not have run." - exit 0 + echo "unittest.log not found; embedded tests may not have run." + exit 0 fi if ! grep -E "failed" unittest.log; then - echo "Log present but no failure lines found in unittest.log." + echo "Log present but no failure lines found in unittest.log." fi - name: Debug failure (Linux) if: ${{ failure() && runner.os == 'Linux' && !inputs.build_only }} @@ -317,10 +317,10 @@ jobs: BUILD_TYPE: ${{ inputs.build_type }} run: | cmake \ - --build . \ - --config "${BUILD_TYPE}" \ - --parallel "${BUILD_NPROC}" \ - --target coverage + --build . \ + --config "${BUILD_TYPE}" \ + --parallel "${BUILD_NPROC}" \ + --target coverage - name: Upload coverage report if: ${{ github.repository == 'XRPLF/rippled' && !inputs.build_only && env.COVERAGE_ENABLED == 'true' }} diff --git a/.github/workflows/reusable-check-levelization.yml b/.github/workflows/reusable-check-levelization.yml index 4efe3e1138..b5d57a177a 100644 --- a/.github/workflows/reusable-check-levelization.yml +++ b/.github/workflows/reusable-check-levelization.yml @@ -38,9 +38,9 @@ jobs: run: | DIFF=$(git status --porcelain) if [ -n "${DIFF}" ]; then - # Print the differences to give the contributor a hint about what to - # expect when running levelization on their own machine. - git diff - echo "${MESSAGE}" - exit 1 + # Print the differences to give the contributor a hint about what to + # expect when running levelization on their own machine. + git diff + echo "${MESSAGE}" + exit 1 fi diff --git a/.github/workflows/reusable-check-rename.yml b/.github/workflows/reusable-check-rename.yml index 56a1a3e637..7aa5b80594 100644 --- a/.github/workflows/reusable-check-rename.yml +++ b/.github/workflows/reusable-check-rename.yml @@ -48,9 +48,9 @@ jobs: run: | DIFF=$(git status --porcelain) if [ -n "${DIFF}" ]; then - # Print the differences to give the contributor a hint about what to - # expect when running the renaming scripts on their own machine. - git diff - echo "${MESSAGE}" - exit 1 + # Print the differences to give the contributor a hint about what to + # expect when running the renaming scripts on their own machine. + git diff + echo "${MESSAGE}" + exit 1 fi diff --git a/.github/workflows/reusable-clang-tidy.yml b/.github/workflows/reusable-clang-tidy.yml index e01a50cf6d..8be1db5fb2 100644 --- a/.github/workflows/reusable-clang-tidy.yml +++ b/.github/workflows/reusable-clang-tidy.yml @@ -70,13 +70,13 @@ jobs: working-directory: ${{ env.BUILD_DIR }} run: | cmake \ - -G 'Ninja' \ - -DCMAKE_TOOLCHAIN_FILE:FILEPATH=build/generators/conan_toolchain.cmake \ - -DCMAKE_BUILD_TYPE="${BUILD_TYPE}" \ - -Dtests=ON \ - -Dwerr=ON \ - -Dxrpld=ON \ - .. + -G 'Ninja' \ + -DCMAKE_TOOLCHAIN_FILE:FILEPATH=build/generators/conan_toolchain.cmake \ + -DCMAKE_BUILD_TYPE="${BUILD_TYPE}" \ + -Dtests=ON \ + -Dwerr=ON \ + -Dxrpld=ON \ + .. # clang-tidy needs headers generated from proto files - name: Build libxrpl.libpb @@ -133,7 +133,7 @@ jobs: - name: Write issue header if: ${{ steps.run_clang_tidy.outcome != 'success' }} run: | - cat > "${ISSUE_FILE}" <"${ISSUE_FILE}" < filtered-output.txt || true + # Extract lines containing 'error:', 'warning:', or 'note:' + grep -E '(error:|warning:|note:)' "${OUTPUT_FILE}" >filtered-output.txt || true - # If filtered output is empty, use original (might be a different error format) - if [ ! -s filtered-output.txt ]; then - cp "${OUTPUT_FILE}" filtered-output.txt - fi + # If filtered output is empty, use original (might be a different error format) + if [ ! -s filtered-output.txt ]; then + cp "${OUTPUT_FILE}" filtered-output.txt + fi - # Truncate if too large - head -c 60000 filtered-output.txt >> "${ISSUE_FILE}" - if [ "$(wc -c < filtered-output.txt)" -gt 60000 ]; then - echo "" >> "${ISSUE_FILE}" - echo "... (output truncated, see artifacts for full output)" >> "${ISSUE_FILE}" - fi + # Truncate if too large + head -c 60000 filtered-output.txt >>"${ISSUE_FILE}" + if [ "$(wc -c >"${ISSUE_FILE}" + echo "... (output truncated, see artifacts for full output)" >>"${ISSUE_FILE}" + fi - rm filtered-output.txt + rm filtered-output.txt else - echo "No output file found" >> "${ISSUE_FILE}" + echo "No output file found" >>"${ISSUE_FILE}" fi - name: Append issue footer if: ${{ steps.run_clang_tidy.outcome != 'success' }} run: | - cat >> "${ISSUE_FILE}" <>"${ISSUE_FILE}" <> "${GITHUB_OUTPUT}" + ./generate.py --packaging --config=linux.json >>"${GITHUB_OUTPUT}" generate-version: runs-on: ubuntu-latest diff --git a/.github/workflows/reusable-strategy-matrix.yml b/.github/workflows/reusable-strategy-matrix.yml index b1232a138f..62d65ad3fa 100644 --- a/.github/workflows/reusable-strategy-matrix.yml +++ b/.github/workflows/reusable-strategy-matrix.yml @@ -42,4 +42,4 @@ jobs: env: GENERATE_CONFIG: ${{ inputs.os != '' && format('--config={0}.json', inputs.os) || '' }} GENERATE_OPTION: ${{ inputs.strategy_matrix == 'all' && '--all' || '' }} - run: ./generate.py ${GENERATE_OPTION} ${GENERATE_CONFIG} >> "${GITHUB_OUTPUT}" + run: ./generate.py ${GENERATE_OPTION} ${GENERATE_CONFIG} >>"${GITHUB_OUTPUT}" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 23441c9dde..c9dec89435 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -66,6 +66,19 @@ repos: - id: shfmt args: [--write, --indent=4, --case-indent=true] + - repo: local + hooks: + - id: format-inline-bash-workflows + name: "format `run:` blocks in workflows/actions" + entry: ./.github/scripts/format-inline-bash.py + language: python + files: ^\.github/(workflows|actions)/.*\.ya?ml$ + - id: format-inline-bash-markdown + name: "format ```bash blocks in markdown" + entry: ./.github/scripts/format-inline-bash.py + language: python + files: \.md$ + - repo: https://github.com/streetsidesoftware/cspell-cli rev: 4643f154907327ee0a2c7038f0296e0dd77d9776 # frozen: v10.0.0 hooks: diff --git a/BUILD.md b/BUILD.md index a1163dbfcc..1d3fc8f774 100644 --- a/BUILD.md +++ b/BUILD.md @@ -151,8 +151,8 @@ git init git remote add origin git@github.com:XRPLF/conan-center-index.git git sparse-checkout init for recipe in "${recipes[@]}"; do - echo "Checking out recipe '${recipe}'..." - git sparse-checkout add recipes/${recipe} + echo "Checking out recipe '${recipe}'..." + git sparse-checkout add recipes/${recipe} done git fetch origin master git checkout master @@ -180,7 +180,7 @@ the new recipe will be automatically pulled from the official Conan Center. If you see an error similar to the following after running `conan profile show`: -```bash +```text ERROR: Invalid setting '17' is not a valid 'settings.compiler.version' value. Possible values are ['5.0', '5.1', '6.0', '6.1', '7.0', '7.3', '8.0', '8.1', '9.0', '9.1', '10.0', '11.0', '12.0', '13', '13.0', '13.1', '14', '14.0', '15', diff --git a/cspell.config.yaml b/cspell.config.yaml index f26268c804..ed936941e4 100644 --- a/cspell.config.yaml +++ b/cspell.config.yaml @@ -93,6 +93,7 @@ words: - daria - dcmake - dearmor + - dedented - deleteme - demultiplexer - deserializaton diff --git a/include/xrpl/protocol_autogen/README.md b/include/xrpl/protocol_autogen/README.md index 860ed1a242..608ffed085 100644 --- a/include/xrpl/protocol_autogen/README.md +++ b/include/xrpl/protocol_autogen/README.md @@ -15,8 +15,8 @@ Generation requires a one-time setup step to create a virtual environment and install Python dependencies, followed by running the generation target: ```bash -cmake --build . --target setup_code_gen # create venv and install dependencies (once) -cmake --build . --target code_gen # generate code +cmake --build . --target setup_code_gen # create venv and install dependencies (once) +cmake --build . --target code_gen # generate code ``` By default, `CODEGEN_VENV_DIR` points to `.venv` in the project root. The diff --git a/package/README.md b/package/README.md index 2089e32e64..867ca273b4 100644 --- a/package/README.md +++ b/package/README.md @@ -74,10 +74,10 @@ VERSION=2.4.0-local PKG_RELEASE=1 docker run --rm \ - -v "$(pwd):/src" \ - -w /src \ - "$IMAGE" \ - ./package/build_pkg.sh --pkg-version "$VERSION" --pkg-release "$PKG_RELEASE" + -v "$(pwd):/src" \ + -w /src \ + "$IMAGE" \ + ./package/build_pkg.sh --pkg-version "$VERSION" --pkg-release "$PKG_RELEASE" # Output: # build/debbuild/*.deb (DEB + dbgsym .ddeb) @@ -92,12 +92,12 @@ needed, but the host toolchain replaces the pinned CI image: ```bash cmake \ - -Dxrpld=ON \ - -Dxrpld_version=2.4.0-local \ - -Dtests=OFF \ - .. + -Dxrpld=ON \ + -Dxrpld_version=2.4.0-local \ + -Dtests=OFF \ + .. -cmake --build . --target package # deb on Debian/Ubuntu, rpm on RHEL +cmake --build . --target package # deb on Debian/Ubuntu, rpm on RHEL ``` The `cmake/XrplPackaging.cmake` module defines the target only if at least one