From f39f279b12a9f572e8af98d23d153524239928dc Mon Sep 17 00:00:00 2001 From: Eli Schleifer <1265982+EliSchleifer@users.noreply.github.com> Date: Fri, 5 Jun 2026 16:21:53 -0700 Subject: [PATCH] Enforce pinact on actions in plugins (#1142) - Use pinact in the plugins repo - updates actions and workflows --- .github/actions/action_tests/action.yaml | 4 +- .github/actions/linter_tests/action.yaml | 2 +- .github/actions/tool_tests/action.yaml | 2 +- .github/workflows/codeql.yml | 6 +- .github/workflows/pr.yaml | 2 +- .github/workflows/scorecard.yml | 4 +- .github/workflows/upgrade_trunk.yaml | 4 +- .../workflows/upload_results.reusable.yaml | 4 +- .pinact.yaml | 13 +++ .trunk/trunk.yaml | 1 + linters/pinact/pinact.test.ts | 17 ++++ linters/pinact/pinact_run.py | 96 ++++++++++++++++++- linters/pinact/test_data/bad.pinact.yaml | 11 +++ .../pinact_v4.0.0_bad_config.check.shot | 36 +++++++ 14 files changed, 186 insertions(+), 16 deletions(-) create mode 100644 .pinact.yaml create mode 100644 linters/pinact/test_data/bad.pinact.yaml create mode 100644 linters/pinact/test_data/pinact_v4.0.0_bad_config.check.shot diff --git a/.github/actions/action_tests/action.yaml b/.github/actions/action_tests/action.yaml index b0ce2cd05..53650df72 100644 --- a/.github/actions/action_tests/action.yaml +++ b/.github/actions/action_tests/action.yaml @@ -32,7 +32,7 @@ runs: # The uv action tests' preCheck shells out to `uv lock` directly (not # through trunk), so we need uv available on PATH. - name: Setup uv - uses: astral-sh/setup-uv@v5 + uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 uses: astral-sh/setup-uv@v5 with: version: 0.7.8 @@ -83,7 +83,7 @@ runs: - name: Upload prod results # TODO(Tyler): Add upload on Windows once the action supports it. if: "!cancelled() && runner.os != 'Windows' && inputs.trunk-prod-token != ''" - uses: trunk-io/analytics-uploader@main + uses: trunk-io/analytics-uploader@413ecdb8036c3698131a278a921c8481ffeec502 # v2.1.1 uses: trunk-io/analytics-uploader@v2.1.1 with: junit-paths: junit.xml org-slug: trunk diff --git a/.github/actions/linter_tests/action.yaml b/.github/actions/linter_tests/action.yaml index 5218227e2..65be2cf10 100644 --- a/.github/actions/linter_tests/action.yaml +++ b/.github/actions/linter_tests/action.yaml @@ -117,7 +117,7 @@ runs: - name: Upload prod results # TODO(Tyler): Add upload on Windows once the action supports it. if: "!cancelled() && runner.os != 'Windows' && inputs.trunk-prod-token != ''" - uses: trunk-io/analytics-uploader@main + uses: trunk-io/analytics-uploader@413ecdb8036c3698131a278a921c8481ffeec502 # v2.1.1 uses: trunk-io/analytics-uploader@v2.1.1 with: junit-paths: junit.xml org-slug: trunk diff --git a/.github/actions/tool_tests/action.yaml b/.github/actions/tool_tests/action.yaml index 79e2b84cb..7d541374d 100644 --- a/.github/actions/tool_tests/action.yaml +++ b/.github/actions/tool_tests/action.yaml @@ -79,7 +79,7 @@ runs: - name: Upload prod results # TODO(Tyler): Add upload on Windows once the action supports it. if: "!cancelled() && runner.os != 'Windows' && inputs.trunk-prod-token != ''" - uses: trunk-io/analytics-uploader@main + uses: trunk-io/analytics-uploader@413ecdb8036c3698131a278a921c8481ffeec502 # v2.1.1 uses: trunk-io/analytics-uploader@v2.1.1 with: junit-paths: junit.xml org-slug: trunk diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index df19b5788..0ffb40c92 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -34,7 +34,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@b2f9ef845756500b97acbdaf5c1dd4e9c1d15734 # v3.35.2 + uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 uses: github/codeql-action/init@v4.36.2 # Override language selection by uncommenting this and choosing your languages with: languages: javascript @@ -42,7 +42,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). # If this step fails, then you should remove it and run the build manually (see below). - name: Autobuild - uses: github/codeql-action/autobuild@b2f9ef845756500b97acbdaf5c1dd4e9c1d15734 # v3.35.2 + uses: github/codeql-action/autobuild@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 uses: github/codeql-action/autobuild@v4.36.2 # â„šī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -56,4 +56,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@b2f9ef845756500b97acbdaf5c1dd4e9c1d15734 # v3.35.2 + uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 uses: github/codeql-action/analyze@v4.36.2 diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index e281f3467..99eee7c3f 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -232,7 +232,7 @@ jobs: lfs: true - name: Trunk Check - uses: trunk-io/trunk-action@75699af9e26881e564e9d832ef7dc3af25ec031b + uses: trunk-io/trunk-action@75699af9e26881e564e9d832ef7dc3af25ec031b # v1.2.4 uses: trunk-io/trunk-action@75699af9e26881e564e9d832ef7dc3af25ec031b env: TRUNK_GITHUB_CHECK_RUN_TITLE: Trunk Check diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 5925e72c0..819a4e002 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -35,7 +35,7 @@ jobs: persist-credentials: false - name: Run analysis - uses: ossf/scorecard-action@99c09fe975337306107572b4fdf4db224cf8e2f2 # v2.4.3 + uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 uses: ossf/scorecard-action@v2.4.3 with: results_file: results.sarif results_format: sarif @@ -65,6 +65,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: Upload to code-scanning - uses: github/codeql-action/upload-sarif@b2f9ef845756500b97acbdaf5c1dd4e9c1d15734 # v3.35.2 + uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 uses: github/codeql-action/upload-sarif@v4.36.2 with: sarif_file: results.sarif diff --git a/.github/workflows/upgrade_trunk.yaml b/.github/workflows/upgrade_trunk.yaml index a96240ac3..a8e68ea6e 100644 --- a/.github/workflows/upgrade_trunk.yaml +++ b/.github/workflows/upgrade_trunk.yaml @@ -19,14 +19,14 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Create App Token for TrunkBuild App (Internal) - uses: tibdex/github-app-token@v2 + uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a # v2.1.0 uses: tibdex/github-app-token@v2 id: generate-token with: app_id: ${{ secrets.TRUNK_OPEN_PR_APP_ID }} private_key: ${{ secrets.TRUNK_OPEN_PR_APP_PRIVATE_KEY }} - name: Trunk Upgrade - uses: trunk-io/trunk-action/upgrade@75699af9e26881e564e9d832ef7dc3af25ec031b + uses: trunk-io/trunk-action/upgrade@75699af9e26881e564e9d832ef7dc3af25ec031b # v1.2.4 uses: trunk-io/trunk-action/upgrade@75699af9e26881e564e9d832ef7dc3af25ec031b with: arguments: -n --bleeding-edge github-token: ${{ steps.generate-token.outputs.token }} diff --git a/.github/workflows/upload_results.reusable.yaml b/.github/workflows/upload_results.reusable.yaml index 287787bda..df3c33d83 100644 --- a/.github/workflows/upload_results.reusable.yaml +++ b/.github/workflows/upload_results.reusable.yaml @@ -225,14 +225,14 @@ jobs: git ls-files --others --exclude-standard | grep ".shot" | xargs sed -i '2i // trunk-upgrade-validation:RELEASE' - name: Create App Token for TrunkBuild App (Internal) - uses: tibdex/github-app-token@v2 + uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a # v2.1.0 uses: tibdex/github-app-token@v2 id: generate-token with: app_id: ${{ secrets.TRUNK_OPEN_PR_APP_ID }} private_key: ${{ secrets.TRUNK_OPEN_PR_APP_PRIVATE_KEY }} - name: Create Pull Request - uses: peter-evans/create-pull-request@v7 + uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7.0.11 uses: peter-evans/create-pull-request@v7 with: title: Auto-add missing snapshots body: diff --git a/.pinact.yaml b/.pinact.yaml new file mode 100644 index 000000000..65e155fe0 --- /dev/null +++ b/.pinact.yaml @@ -0,0 +1,13 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/suzuki-shunsuke/pinact/refs/heads/main/json-schema/pinact.json +# pinact - https://github.com/suzuki-shunsuke/pinact +version: 3 +min_age: + value: 30 + always: true + +rules: + - min_age: 0 + conditions: + - expr: | + ActionRepoFullName == "trunk-io/analytics-uploader" || + ActionRepoFullName == "github/codeql-action" diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index a2bab2477..4a46a0e85 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -43,6 +43,7 @@ lint: success_codes: [0] enabled: # enabled linters inherited from github.com/trunk-io/configs plugin + - pinact@4.0.0 - ls-lint@2.3.1 - pmd@7.18.0 - definition-checker diff --git a/linters/pinact/pinact.test.ts b/linters/pinact/pinact.test.ts index 8318ebf84..fd6ed7648 100644 --- a/linters/pinact/pinact.test.ts +++ b/linters/pinact/pinact.test.ts @@ -58,6 +58,23 @@ const skipIfMissingGitHubToken = () => { return false; }; +const preCheckBadConfig = async (driver: TrunkLintDriver) => { + process.env.PINACT_DISABLE_GH_AUTH = "1"; + driver.moveFile(path.join(TEST_DATA, "bad.pinact.yaml"), path.join(".pinact.yaml")); + driver.moveFile( + path.join(TEST_DATA, "missing_version_comment.in.yaml"), + path.join(".github/workflows", "missing_version_comment.in.yaml"), + ); + await driver.gitDriver?.add(".").commit("moved"); +}; + +customLinterCheckTest({ + linterName: "pinact", + testName: "bad_config", + args: ".github", + preCheck: preCheckBadConfig, +}); + customLinterCheckTest({ linterName: "pinact", testName: "missing_version_comment", diff --git a/linters/pinact/pinact_run.py b/linters/pinact/pinact_run.py index 75c77a545..79175e06b 100644 --- a/linters/pinact/pinact_run.py +++ b/linters/pinact/pinact_run.py @@ -1,10 +1,22 @@ #!/usr/bin/env python3 +import json import os +import re import shutil import subprocess import sys from pathlib import Path +SARIF_SCHEMA = ( + "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/" + "Schemata/sarif-schema-2.1.0.json" +) +ANSI_ESCAPE = re.compile(r"\x1b\[[0-9;]*m") + +# pinact uses 1/2 when SARIF findings exist; empty stdout means it failed before emitting SARIF. +PINACT_SARIF_EXIT_CODES = (1, 2) +CONFIG_ERROR_RULE_ID = "config-error" + def resolve_executable(name: str) -> str | None: return shutil.which(name) @@ -93,13 +105,93 @@ def build_pinact_args(mode: str) -> list[str]: return args +def strip_ansi(text: str) -> str: + return ANSI_ESCAPE.sub("", text) + + +def parse_pinact_stderr(stderr: str) -> str: + clean = strip_ansi(stderr).strip() + if not clean: + return "pinact failed" + + match = re.search(r'error="((?:\\.|[^"\\])*)"', clean) + if match: + return match.group(1).encode("utf-8").decode("unicode_escape").strip() + + if "error=" in clean: + return clean.split("error=", 1)[1].strip().strip('"') + + return clean + + +def build_failure_sarif(target: str, message: str) -> str: + sarif = { + "$schema": SARIF_SCHEMA, + "version": "2.1.0", + "runs": [ + { + "results": [ + { + "ruleId": CONFIG_ERROR_RULE_ID, + "level": "error", + "message": {"text": message}, + "locations": [ + { + "physicalLocation": { + "artifactLocation": {"uri": target}, + "region": { + "startLine": 1, + "startColumn": 1, + }, + } + } + ], + } + ] + } + ], + } + return json.dumps(sarif, indent=2) + + +def sarif_for_pinact_failure(stderr: str, targets: list[str]) -> str: + return build_failure_sarif( + targets[0] if targets else ".", + parse_pinact_stderr(stderr), + ) + + def run_pinact(mode: str, targets: list[str]) -> int: pinact = require_executable("pinact") - return subprocess.run( + result = subprocess.run( [pinact, *build_pinact_args(mode)[1:], *validate_targets(targets)], shell=False, check=False, - ).returncode + capture_output=True, + text=True, + ) + + stdout = result.stdout + stderr = result.stderr + returncode = result.returncode + + if returncode in PINACT_SARIF_EXIT_CODES and not stdout.strip(): + sarif = sarif_for_pinact_failure(stderr, targets) + sys.stdout.write(sarif) + if not sarif.endswith("\n"): + sys.stdout.write("\n") + return 2 + + if stdout: + sys.stdout.write(stdout) + if not stdout.endswith("\n"): + sys.stdout.write("\n") + if stderr: + sys.stderr.write(stderr) + if not stderr.endswith("\n"): + sys.stderr.write("\n") + + return returncode def main() -> int: diff --git a/linters/pinact/test_data/bad.pinact.yaml b/linters/pinact/test_data/bad.pinact.yaml new file mode 100644 index 000000000..0495d98bb --- /dev/null +++ b/linters/pinact/test_data/bad.pinact.yaml @@ -0,0 +1,11 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/suzuki-shunsuke/pinact/refs/heads/main/json-schema/pinact.json +version: 3 +min_age: + value: 30 + always: true + +rules: + - min_age: 0 + conditions: + - expr: | + ActionRepoFullName == "trunk-io/analytics-uploader" || "github/codeql-action/analyze" diff --git a/linters/pinact/test_data/pinact_v4.0.0_bad_config.check.shot b/linters/pinact/test_data/pinact_v4.0.0_bad_config.check.shot new file mode 100644 index 000000000..82e9e6e2e --- /dev/null +++ b/linters/pinact/test_data/pinact_v4.0.0_bad_config.check.shot @@ -0,0 +1,36 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Testing linter pinact test bad_config 1`] = ` +{ + "issues": [ + { + "code": "config-error", + "column": "1", + "file": ".github/workflows/missing_version_comment.in.yaml", + "isSecurity": true, + "issueClass": "ISSUE_CLASS_NEW", + "issueUrl": "https://github.com/suzuki-shunsuke/pinact/blob/main/docs/codes/", + "level": "LEVEL_HIGH", + "line": "1", + "linter": "pinact", + "message": "read configuration file: initialize rules[0]: initialize conditions[0]: compile expr: invalid operation: || (mismatched types bool and string) (1:53) + | ActionRepoFullName == "trunk-io/analytics-uploader" || "github/codeql-action/analyze" + | ....................................................^", + "targetType": "github-workflow", + }, + ], + "lintActions": [ + { + "command": "lint", + "fileGroupName": "github-workflow", + "linter": "pinact", + "paths": [ + ".github/workflows/missing_version_comment.in.yaml", + ], + "verb": "TRUNK_VERB_CHECK", + }, + ], + "taskFailures": [], + "unformattedFiles": [], +} +`;