diff --git a/slang/action.yml b/slang/action.yml index c16812a..7db49d8 100644 --- a/slang/action.yml +++ b/slang/action.yml @@ -24,6 +24,12 @@ inputs: reporters (e.g., spawned by a matrix) may overwrite each others results. Defaults to slang.' required: false default: 'slang' + fail-on-error: + description: 'Fail the job when slang reports an error. Defaults to false, which keeps the + action a pure annotator: run_slang.py exits 0 regardless of compile errors so reviewdog + can annotate them. Set to true to use the action as a gate.' + required: false + default: 'false' pyslang-version: description: 'pyslang version to install, as an exact version number (e.g. "11.0.0"). pyslang bundles the slang engine and is released in lockstep with it, so this single @@ -64,3 +70,11 @@ runs: -reporter=${{ inputs.reviewdog-reporter }} \ -filter-mode=nofilter \ -fail-level=error + + # After reviewdog, so the diagnostics are still annotated on a failing run. + # Read from the diagnostics rather than reviewdog's exit status: reviewdog + # needs a check-write token, which a pull request from a fork does not get. + - name: Fail on slang errors + if: inputs.fail-on-error == 'true' + shell: bash + run: uv run ${{ github.action_path }}/check_diags.py slang_diags.json diff --git a/slang/check_diags.py b/slang/check_diags.py new file mode 100644 index 0000000..07f67d0 --- /dev/null +++ b/slang/check_diags.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +# +# Copyright 2026 ETH Zurich and University of Bologna. +# Licensed under the Apache License, Version 2.0, see LICENSE for details. +# SPDX-License-Identifier: Apache-2.0 +# +# Daniel Keller + +"""Exit non-zero when a slang diagnostic file contains errors. + +Read straight from the diagnostics rather than from reviewdog's exit status: +reviewdog needs a token with check-write permission, which a pull request from +a fork does not get, so a gate built on it would pass silently exactly where a +public gate is worth the most. +""" + +import json +import sys + + +def main(): + if len(sys.argv) != 2: + print(f"Usage: {sys.argv[0]} ", file=sys.stderr) + sys.exit(1) + + path = sys.argv[1] + try: + with open(path, encoding="utf-8") as handle: + diagnostics = json.load(handle) + except FileNotFoundError: + # slang always writes the file, so its absence means the run never + # happened; failing here keeps a missing run from reading as success. + print(f"error: no diagnostics at {path}; slang did not run", file=sys.stderr) + sys.exit(1) + except json.JSONDecodeError as exc: + print(f"error: {path} is not valid JSON: {exc}", file=sys.stderr) + sys.exit(1) + + errors = [d for d in diagnostics if d.get("severity", "").lower() == "error"] + if errors: + print(f"slang reported {len(errors)} error(s):", file=sys.stderr) + for diag in errors: + location = diag.get("location", "") + print(f" {location}: {diag.get('message', '')}", file=sys.stderr) + sys.exit(1) + + print(f"slang: no errors in {len(diagnostics)} diagnostic(s)") + + +if __name__ == "__main__": + main()