Shellcheck - PR #10083 ("sunxi-6.18: fix H3/H5 RCU stall / CPU hang during DVFS (current kernel)") #2269
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: "Maintenance: Lint scripts" | |
| run-name: 'Shellcheck - PR #${{ github.event.pull_request.number }} ("${{ github.event.pull_request.title }}")' | |
| # | |
| # Lint phase of a two-phase pipeline that produces both: | |
| # * a pass/fail Check on the PR (red cross on critical findings); and | |
| # * artifacts consumed by maintenance-lint-scripts-post.yml, which | |
| # posts findings and auto-fix suggestions inline on the PR via | |
| # reviewdog. | |
| # | |
| # The post workflow runs via `workflow_run` in the base repository | |
| # context with a write token, so PR comments work for forks too. | |
| # This workflow itself only needs a read token. | |
| # | |
| # Runs on every fork of armbian/build by default. If a fork doesn't | |
| # want it (private fork, repurposed tree, etc.), disable either: | |
| # * the whole Actions surface — Settings → Actions → General → | |
| # "Disable actions"; or | |
| # * just this workflow — Settings → Actions → General → | |
| # "Allow ... and select non-Armbian actions" + explicitly disable | |
| # "Maintenance: Lint scripts" under Actions → Workflows. | |
| on: | |
| pull_request: | |
| types: [opened, reopened, synchronize] | |
| permissions: | |
| contents: read | |
| # Required for the `gh api repos/.../pulls/N/files` call below. | |
| # `contents: read` alone leaves all other scopes at `none` under | |
| # fine-grained / app tokens, and the endpoint then returns | |
| # `Resource not accessible by integration`. | |
| pull-requests: read | |
| concurrency: | |
| group: pipeline-lint-${{ github.event.pull_request.number }} | |
| cancel-in-progress: true | |
| jobs: | |
| Shellcheck: | |
| name: Shell script analysis | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Surface workflow purpose + disable instructions in the Check summary | |
| run: | | |
| cat >>"$GITHUB_STEP_SUMMARY" <<'EOF' | |
| ## What this Check does | |
| `Maintenance: Lint scripts` runs ShellCheck on the PR's shell scripts and produces: | |
| * a **red cross / green check** on the PR (red on critical or `--severity=error` findings in changed scripts); and | |
| * artifacts consumed by `Maintenance: Lint scripts (post)`, which posts inline review comments and auto-fix suggestions back on the PR via [reviewdog](https://github.com/reviewdog/reviewdog). | |
| ### Don't want this in a fork? | |
| This workflow runs on every fork of `armbian/build` by default. To disable in your fork: | |
| * **All Actions** — Settings → Actions → General → "Disable actions". | |
| * **Just this workflow** — Settings → Actions → Workflows → "Maintenance: Lint scripts" → "Disable workflow". The companion `Maintenance: Lint scripts (post)` becomes a no-op once this one is disabled. | |
| EOF | |
| - name: Checkout repository | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| fetch-depth: 2 | |
| # PR-scope file list. Uses the GitHub PR API (merge-base diff, | |
| # the same set the PR's "Files changed" tab and reviewdog's | |
| # filter_mode=added operate on), not `git diff base..head` — | |
| # the latter expands to every commit since the head branch | |
| # forked, which on stale-rebased PRs balloons to hundreds of | |
| # legacy files and produces red Checks unrelated to the PR. | |
| # | |
| # The list is written to `pr-changed-files.lines` as one | |
| # JSON-encoded path per line. Both the `$GITHUB_OUTPUT` | |
| # multiline-heredoc and the env-block alternatives are too | |
| # fragile here: Git allows newlines and quotes in paths, and | |
| # filename data is attacker-controlled (any PR can add any | |
| # path), so any encoding that uses raw newlines as separator | |
| # — or any in-script `${{ … }}` interpolation — is a route | |
| # to either truncation, lost files, or command injection at | |
| # parse time. JSON-per-line cleanly addresses all three: the | |
| # newline separator cannot appear inside a JSON-encoded path, | |
| # and consumers `jq -r .` to recover the raw string into a | |
| # shell variable that already exists (no parsing of the path | |
| # *as* shell code). | |
| - name: Get changed files (PR-scope, merge-base) | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| PR_FILE_COUNT: ${{ github.event.pull_request.changed_files }} | |
| run: | | |
| # `pulls/N/files` caps at 3000 entries. Above that GitHub | |
| # silently returns only a prefix, the downstream gate would | |
| # lint a partial PR, and SC errors in trailing files could | |
| # ship as green. Refuse loudly instead — a local merge-base | |
| # fallback is possible but rarely worth carrying since PRs | |
| # of this size are exceptional. | |
| if (( PR_FILE_COUNT > 3000 )); then | |
| echo "::error::PR changes ${PR_FILE_COUNT} files; GitHub's pulls/N/files API caps at 3000. Split the PR, or extend this workflow with a local merge-base fallback." | |
| exit 1 | |
| fi | |
| gh api --paginate \ | |
| "repos/${GITHUB_REPOSITORY}/pulls/${{ github.event.pull_request.number }}/files" \ | |
| --jq '.[].filename | @json' > pr-changed-files.lines | |
| echo "PR-scope changed files (#${{ github.event.pull_request.number }}):" | |
| jq -r . < pr-changed-files.lines | sed 's/^/ /' | |
| - name: Install shfmt (used by `shfmt -f .` enumerator and the drift artifact) | |
| run: sudo apt-get update -qq && sudo apt-get install -y shfmt | |
| # ============================================================ | |
| # Produce artifacts for the post workflow BEFORE the red-cross | |
| # gate, so even a red-cross failure ships diagnostics to the PR. | |
| # SHELLCHECK_INSTALL_ONLY=1 just populates the framework cache; | |
| # subsequent steps reuse the binary directly. | |
| # ============================================================ | |
| - name: ShellCheck — populate platform-detected binary cache | |
| env: | |
| SHELLCHECK_INSTALL_ONLY: "1" | |
| run: bash lib/tools/shellcheck.sh | |
| - name: ShellCheck — produce findings in gcc format (PR-comment artifact) | |
| run: | | |
| # Mirror calculate_params_for_severity SEVERITY=critical from | |
| # lib/tools/shellcheck.sh, emit gcc-format so the post job can | |
| # parse with reviewdog -efm. compile.sh as the entry-point | |
| # follows the entire source closure (--check-sourced | |
| # --external-sources); SC2154 against cross-file definitions | |
| # resolves correctly. | |
| SC=$(find cache/tools/shellcheck -name 'shellcheck-v*' -type f -executable | head -1) | |
| if [[ -z "$SC" || ! -x "$SC" ]]; then | |
| echo "::error::ShellCheck binary not found in cache/tools/shellcheck (SC='$SC')" | |
| exit 1 | |
| fi | |
| "$SC" \ | |
| --check-sourced --external-sources --shell=bash \ | |
| --severity=warning --format=gcc \ | |
| --exclude=SC2034,SC2207,SC2046,SC2086,SC2206 \ | |
| compile.sh \ | |
| > shellcheck-findings.txt 2>&1 || true | |
| # Per-file ShellCheck on standalone bash-shebang scripts | |
| # outside compile.sh's source closure — those are linted | |
| # in the red-cross gate at end of job, but their output | |
| # also needs to land in the artifact so the post phase can | |
| # inline-comment on them. Same exclusion regex as the gate. | |
| # | |
| # Read each path as JSON (escapes embedded newlines/quotes) | |
| # then jq-decode into $file. Because $file is *assigned*, not | |
| # interpolated, even a path like `x"$(id)".sh` becomes a | |
| # plain string here — no command substitution at parse time. | |
| while IFS= read -r line; do | |
| [[ -z "$line" ]] && continue | |
| file=$(jq -r . <<< "$line") | |
| [[ -f "$file" ]] || continue | |
| if [[ ! "$file" =~ lib/|extensions/|\.py$|\.service$|\.rules$|\.network$|\.netdev$ ]]; then | |
| if grep -qE '^#!/.*bash' "$file" 2>/dev/null; then | |
| "$SC" --shell=bash --severity=error --format=gcc "$file" >> shellcheck-findings.txt 2>&1 || true | |
| fi | |
| fi | |
| done < pr-changed-files.lines | |
| - name: ShellCheck — produce auto-fix diff (PR-suggestion artifact) | |
| run: | | |
| # `--severity=style` (broadest) on purpose: ShellCheck's auto-fix | |
| # coverage is mostly style/info-level (SC2006, SC2196, SC2129…); | |
| # --severity=warning leaves only excluded codes. filter_mode=added | |
| # in the post step limits noise to lines this PR adds. | |
| SC=$(find cache/tools/shellcheck -name 'shellcheck-v*' -type f -executable | head -1) | |
| if [[ -z "$SC" || ! -x "$SC" ]]; then | |
| echo "::error::ShellCheck binary not found in cache/tools/shellcheck (SC='$SC')" | |
| exit 1 | |
| fi | |
| "$SC" -f diff \ | |
| --severity=style --shell=bash \ | |
| --exclude=SC2034,SC2207,SC2046,SC2086,SC2206 \ | |
| $(shfmt -f .) \ | |
| > shellcheck-fix.diff 2>/dev/null || true | |
| - name: shfmt — produce drift diff (PR-suggestion artifact) | |
| run: | | |
| # `|| true`: shfmt aborts with exit 1 on any unparseable file | |
| # (some Armbian shell scripts use bash-extension syntax that | |
| # confuses the parser). Files that *did* produce diff hunks | |
| # still ship to the post step. | |
| shfmt -d $(shfmt -f .) > shfmt.diff 2>/dev/null || true | |
| - name: Stash PR metadata for the post workflow | |
| run: | | |
| echo "${{ github.event.pull_request.number }}" > pr-number.txt | |
| echo "${{ github.event.pull_request.head.sha }}" > head-sha.txt | |
| # head-repo.txt is intentionally captured even though the | |
| # current post workflow doesn't consume it — it's the only | |
| # place we have authoritative access to the PR's fork-owner | |
| # name, and the post workflow may need it later (e.g., to | |
| # build a cross-fork compare URL in a Check Run summary). | |
| echo "${{ github.event.pull_request.head.repo.full_name }}" > head-repo.txt | |
| echo "${{ github.event.pull_request.base.sha }}" > base-sha.txt | |
| echo "${{ github.event.pull_request.base.ref }}" > base-ref.txt | |
| echo "${{ github.repository }}" > base-repo.txt | |
| - name: Upload artifact (always — even if the red-cross gate below fails) | |
| if: always() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: lint-scripts-results | |
| path: | | |
| shellcheck-findings.txt | |
| shellcheck-fix.diff | |
| shfmt.diff | |
| pr-number.txt | |
| head-sha.txt | |
| head-repo.txt | |
| base-sha.txt | |
| base-ref.txt | |
| base-repo.txt | |
| retention-days: 1 | |
| if-no-files-found: error | |
| # ============================================================ | |
| # Red-cross gate — preserves the previous maintenance-lint-scripts.yml | |
| # pass/fail semantics: framework run (via compile.sh entry-point, | |
| # follows the source chain so cross-file vars aren't reported as | |
| # "unused") + per-file --severity=error on bash-shebang scripts | |
| # outside lib/ and extensions/. Anything that previously turned | |
| # the PR Check red still does so here. | |
| # ============================================================ | |
| - name: Red-cross gate (framework run + critical per-file) | |
| run: | | |
| # Capture every gate diagnostic so a non-zero exit can publish | |
| # a readable report to the Step Summary. Without this the only | |
| # artifact of a fail is the bare job log buried under collapsed | |
| # step groups. | |
| gate_log="$(mktemp)" | |
| trap 'rm -f "$gate_log"' EXIT | |
| set +e | |
| bash lib/tools/shellcheck.sh 2>&1 | tee -a "$gate_log" | |
| framework_ret=${PIPESTATUS[0]} | |
| # Same cached, version-pinned ShellCheck binary the artifact | |
| # step uses — not the runner's system `shellcheck`, whose | |
| # version may differ and report findings the gate and the | |
| # artifact disagree on. | |
| SC=$(find cache/tools/shellcheck -name 'shellcheck-v*' -type f -executable | head -1) | |
| # Read each path as JSON-encoded (escapes newlines/quotes) | |
| # then jq-decode into $file. Path content is attacker- | |
| # controlled (any PR can add any filename), so it must | |
| # never reach the shell parser as code — see comments on | |
| # the `Get changed files` step above. | |
| per_file_ret=0 | |
| while IFS= read -r line; do | |
| [[ -z "$line" ]] && continue | |
| file=$(jq -r . <<< "$line") | |
| [[ -f "$file" ]] || continue | |
| if [[ ! "$file" =~ lib/|extensions/|\.py$|\.service$|\.rules$|\.network$|\.netdev$ ]]; then | |
| if grep -qE '^#!/.*bash' "$file" 2>/dev/null; then | |
| # gcc format keeps the captured log compact and | |
| # file:line-prefixed for the Step Summary. | |
| "$SC" --shell=bash --severity=error --format=gcc "$file" 2>&1 | tee -a "$gate_log" | |
| rc=${PIPESTATUS[0]} | |
| [[ $rc -ne 0 ]] && per_file_ret=$rc | |
| fi | |
| fi | |
| done < pr-changed-files.lines | |
| set -e | |
| ret=0 | |
| [[ $framework_ret -ne 0 ]] && ret=$framework_ret | |
| [[ $per_file_ret -ne 0 ]] && ret=$per_file_ret | |
| if [[ $ret -ne 0 ]]; then | |
| # Step Summary — the readable report a PR author sees on the | |
| # Check's run page. Inline per-line markers are intentionally | |
| # left to the post workflow's reviewdog pass (which also | |
| # surfaces them on the Conversation tab); emitting `::error` | |
| # annotations here too would double up on the same lines. | |
| # The Summary is the fallback that still works when reviewdog | |
| # no-ops (cross-fork token, filter mismatch, reviewdog down). | |
| { | |
| echo "## Red-cross gate failed" | |
| echo | |
| echo "ShellCheck reported findings in PR-scope files. Details:" | |
| echo | |
| echo '```' | |
| cat "$gate_log" | |
| echo '```' | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| exit $ret |