Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .bandit
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[bandit]
# subprocess is used intentionally in CLI wrappers and linter integrations.
skips = B404,B603
1 change: 0 additions & 1 deletion .github/actionlint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ self-hosted-runner:
# Labels of self-hosted runner in array of strings.
labels:
- ubuntu-x64
- macOS
- ubuntu-latest
# Configuration variables in array of strings defined in your repository or
# organization. `null` means disabling configuration variables check.
Expand Down
10 changes: 5 additions & 5 deletions .github/workflows/nightly.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
matrix:
linter-version: [Snapshots, Latest]
# TODO(Tyler): Re-add Windows runners.
os: [ubuntu-latest, macOS]
os: [ubuntu-latest, macos-latest]
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
Expand Down Expand Up @@ -81,12 +81,12 @@ jobs:
fail-fast: false
matrix:
linter-version: [Snapshots, Latest]
os: [ubuntu-latest, macOS]
os: [ubuntu-latest, macos-latest]
include:
# Normalize the filenames as inputs for ease of parsing
- os: ubuntu-latest
results-file: ubuntu-latest
- os: macOS
- os: macos-latest
results-file: macos-latest
outputs:
plugin-version: ${{ steps.get-release.outputs.tag }}
Expand Down Expand Up @@ -216,12 +216,12 @@ jobs:
fail-fast: false
matrix:
# TODO(Tyler): Re-add Windows runners.
os: [ubuntu-latest, macOS]
os: [ubuntu-latest, macos-latest]
include:
# Normalize the filenames as inputs for ease of parsing
- os: ubuntu-latest
results-file: ubuntu-latest
- os: macOS
- os: macos-latest
results-file: macos-latest
# - os: windows-latest
# results-file: windows-latest
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -126,14 +126,14 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macOS]
os: [ubuntu-latest, macos-latest]
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

# TODO(Tyler): Remove this once the cache has stabilized
- name: Delete cache (mac only)
if: matrix.os == 'macOS'
if: matrix.os == 'macos-latest'
# For now, avoid deleting cache on pull request changes to nightly. This improves PR experience.
run: |
if [ -d "${TMPDIR:-/tmp}/plugins_testing_download_cache" ]
Expand Down Expand Up @@ -183,7 +183,7 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macOS]
os: [ubuntu-latest, macos-latest]
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ trunk check enable {linter}
| Dart | [dart] |
| Docker | [hadolint], [checkov] |
| Dotenv | [dotenv-linter] |
| GitHub | [actionlint], [zizmor] |
| GitHub | [actionlint], [pinact], [zizmor] |
| Go | [gofmt], [gofumpt], [goimports], [gokart], [golangci-lint], [golines], [semgrep] |
| GraphQL | [graphql-schema-linter], [prettier] |
| HAML | [haml-lint] |
Expand All @@ -81,7 +81,7 @@ trunk check enable {linter}
| Ruby | [brakeman], [rubocop], [rufo], [semgrep], [standardrb] |
| Rust | [clippy], [rustfmt] |
| Scala | [scalafmt] |
| Security | [checkov], [dustilock], [grype], [nancy], [osv-scanner], [snyk], [tfsec], [trivy], [trufflehog], [terrascan], [zizmor] |
| Security | [checkov], [dustilock], [grype], [nancy], [osv-scanner], [pinact], [snyk], [tfsec], [trivy], [trufflehog], [terrascan], [zizmor] |
| SQL | [sqlfluff], [sqlfmt], [sql-formatter], [squawk] |
| SVG | [svgo] |
| Swift | [stringslint], [swiftlint], [swiftformat] |
Expand Down Expand Up @@ -151,6 +151,7 @@ trunk check enable {linter}
[oxipng]: https://github.com/shssoichiro/oxipng#readme
[perlcritic]: https://metacpan.org/pod/Perl::Critic
[perltidy]: https://metacpan.org/dist/Perl-Tidy/view/bin/perltidy
[pinact]: https://github.com/suzuki-shunsuke/pinact#readme
[php-cs-fixer]: https://github.com/PHP-CS-Fixer/PHP-CS-Fixer#readme
[phpstan]: https://phpstan.org/
[pmd]: https://pmd.github.io/
Expand Down
89 changes: 89 additions & 0 deletions linters/pinact/pinact.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import * as fs from "fs";
import * as path from "path";
import { customLinterCheckTest } from "tests";
import { TrunkLintDriver } from "tests/driver";
import { TEST_DATA } from "tests/utils";

const moveWorkflowFile =
(filename: string, disableGhAuth = false) =>
async (driver: TrunkLintDriver) => {
if (disableGhAuth) {
process.env.PINACT_DISABLE_GH_AUTH = "1";
} else {
delete process.env.PINACT_DISABLE_GH_AUTH;
}
driver.moveFile(path.join(TEST_DATA, filename), path.join(".github/workflows", filename));
await driver.gitDriver?.add(".").commit("moved");
};

const moveWorkflowFiles =
(sourceDir = TEST_DATA) =>
async (driver: TrunkLintDriver) => {
delete process.env.PINACT_DISABLE_GH_AUTH;

fs.readdirSync(path.resolve(driver.getSandbox(), sourceDir), { withFileTypes: true })
.filter((file) => file.isFile())
.forEach((file) => {
driver.moveFile(path.join(sourceDir, file.name), path.join(".github/workflows", file.name));
});
await driver.gitDriver?.add(".").commit("moved");
};

const enablePinactCommand =
(command: string, preCheck?: (driver: TrunkLintDriver) => Promise<void>) =>
async (driver: TrunkLintDriver) => {
delete process.env.PINACT_DISABLE_GH_AUTH;

const trunkYamlPath = ".trunk/trunk.yaml";
const currentContents = driver.readFile(trunkYamlPath);
const pinactRegex = /- pinact@(.+)\n/;

driver.writeFile(
trunkYamlPath,
currentContents.replace(pinactRegex, `- pinact@$1:\n commands: [${command}]\n`),
);

if (preCheck) {
await preCheck(driver);
}
};

const skipIfMissingGitHubToken = () => {
if (!process.env.GH_TOKEN && !process.env.GITHUB_TOKEN && !process.env.PINACT_GITHUB_TOKEN) {
console.log(
"Skipping pinact online audit test because GH_TOKEN, GITHUB_TOKEN, and PINACT_GITHUB_TOKEN are not set.",
);
return true;
}
return false;
};

customLinterCheckTest({
linterName: "pinact",
testName: "missing_version_comment",
args: ".github",
preCheck: moveWorkflowFile("missing_version_comment.in.yaml", true),
});

customLinterCheckTest({
linterName: "pinact",
testName: "unpinned",
args: ".github",
preCheck: moveWorkflowFile("unpinned.in.yaml", true),
});

customLinterCheckTest({
linterName: "pinact",
testName: "version_comment",
args: ".github",
preCheck: moveWorkflowFiles(path.join(TEST_DATA, "online")),
skipTestIf: skipIfMissingGitHubToken,
});

customLinterCheckTest({
linterName: "pinact",
testName: "upgrade",
args: ".github",
preCheck: enablePinactCommand("upgrade", moveWorkflowFile("unpinned.in.yaml")),
skipTestIf: skipIfMissingGitHubToken,
});
118 changes: 118 additions & 0 deletions linters/pinact/pinact_run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
#!/usr/bin/env python3
import os
import shutil
import subprocess
import sys
from pathlib import Path


def resolve_executable(name: str) -> str | None:
return shutil.which(name)


def require_executable(name: str) -> str:
path = resolve_executable(name)
if path is None:
print(f"{name} not found in PATH", file=sys.stderr)
raise SystemExit(127)
return path


def validate_targets(paths: list[str]) -> list[str]:
validated: list[str] = []
for path in paths:
target = Path(path)
if not target.exists():
print(f"target not found: {path}", file=sys.stderr)
raise SystemExit(2)
validated.append(str(target.resolve()))
return validated


def gh_auth_token() -> str | None:
gh = resolve_executable("gh")
if gh is None:
return None

try:
result = subprocess.run(
[gh, "auth", "token"],
shell=False,
check=False,
capture_output=True,
text=True,
)
except OSError:
return None

if result.returncode != 0:
return None

token = result.stdout.strip()
return token or None


def configure_token_env() -> None:
github_token = os.environ.get("GITHUB_TOKEN")
pinact_github_token = os.environ.get("PINACT_GITHUB_TOKEN")
gh_token = os.environ.get("GH_TOKEN")

if not github_token and gh_token:
os.environ["GITHUB_TOKEN"] = gh_token
github_token = gh_token

if not pinact_github_token and github_token:
os.environ["PINACT_GITHUB_TOKEN"] = github_token

if not os.environ.get("GITHUB_TOKEN") and not os.environ.get("PINACT_GITHUB_TOKEN"):
if not os.environ.get("PINACT_DISABLE_GH_AUTH"):
token = gh_auth_token()
if token:
os.environ["GITHUB_TOKEN"] = token
os.environ["PINACT_GITHUB_TOKEN"] = token


def has_github_token() -> bool:
return bool(os.environ.get("GITHUB_TOKEN") or os.environ.get("PINACT_GITHUB_TOKEN"))


def build_pinact_args(mode: str) -> list[str]:
args = ["pinact", "run", "-format", "sarif"]
if mode == "upgrade":
# -update bumps to latest semver; SARIF suggestions only (Trunk applies fixes).
args.extend(["-update"])
return args

# SARIF output implies -fix=false; let Trunk apply fixes from SARIF suggestions.
if os.environ.get("PINACT_DISABLE_GH_AUTH"):
args.append("-no-api")
elif has_github_token():
args.append("-verify-comment")
else:
args.append("-no-api")
return args


def run_pinact(mode: str, targets: list[str]) -> int:
pinact = require_executable("pinact")
return subprocess.run(
[pinact, *build_pinact_args(mode)[1:], *validate_targets(targets)],
shell=False,
check=False,
).returncode


def main() -> int:
configure_token_env()

argv = sys.argv[1:]
mode = "lint"
if argv and argv[0] == "--upgrade":
mode = "upgrade"
argv = argv[1:]

return run_pinact(mode, argv)


if __name__ == "__main__":
raise SystemExit(main())
76 changes: 76 additions & 0 deletions linters/pinact/plugin.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
version: 0.1
downloads:
- name: pinact
downloads:
- os:
linux: linux
macos: darwin
cpu:
x86_64: amd64
arm_64: arm64
url: https://github.com/suzuki-shunsuke/pinact/releases/download/v${version}/pinact_${os}_${cpu}.tar.gz
- os: windows
cpu:
x86_64: amd64
arm_64: arm64
url: https://github.com/suzuki-shunsuke/pinact/releases/download/v${version}/pinact_windows_${cpu}.zip
tools:
definitions:
- name: pinact
download: pinact
shims: [pinact]
known_good_version: 4.0.0
lint:
definitions:
- name: pinact
files: [github-workflow, github-actions]
tools: [pinact]
runtime: python
hold_the_line: false
description: Pin and verify GitHub Actions and reusable workflows
commands:
- name: lint
output: sarif
run: python3 ${plugin}/linters/pinact/pinact_run.py ${target}
success_codes: [0, 1, 2, 3]
batch: true
cache_results: true
is_security: true
read_output_from: stdout
disable_upstream: true
- name: upgrade
output: sarif
run: python3 ${plugin}/linters/pinact/pinact_run.py --upgrade ${target}
success_codes: [0, 1, 2, 3]
batch: true
cache_results: true
is_security: true
read_output_from: stdout
disable_upstream: true
enabled: false
direct_configs:
- .pinact.yml
- .pinact.yaml
- .github/pinact.yml
- .github/pinact.yaml
suggest_if: files_present
environment:
- name: GITHUB_TOKEN
value: ${env.GITHUB_TOKEN}
optional: true
- name: PINACT_GITHUB_TOKEN
value: ${env.PINACT_GITHUB_TOKEN}
optional: true
- name: GH_TOKEN
value: ${env.GH_TOKEN}
optional: true
- name: PINACT_DISABLE_GH_AUTH
value: ${env.PINACT_DISABLE_GH_AUTH}
optional: true
- name: PATH
list: ["${runtime}", "${linter}", "${env.PATH}"]
issue_url_format: https://github.com/suzuki-shunsuke/pinact/blob/main/docs/codes/
known_good_version: 4.0.0
version_command:
parse_regex: ${semver}
run: pinact version
Loading
Loading