Skip to content

Commit 7f8d2f6

Browse files
authored
Integrate pinact with support for pulling in gh tokens to correctly manage gh actions versions (#1141)
Pinact allows for autofixes and upgrades of github actions alongside the ability to confirm shas in comments match versions
1 parent ba1a4ea commit 7f8d2f6

17 files changed

Lines changed: 493 additions & 11 deletions

.bandit

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[bandit]
2+
# subprocess is used intentionally in CLI wrappers and linter integrations.
3+
skips = B404,B603

.github/actionlint.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ self-hosted-runner:
22
# Labels of self-hosted runner in array of strings.
33
labels:
44
- ubuntu-x64
5-
- macOS
65
- ubuntu-latest
76
# Configuration variables in array of strings defined in your repository or
87
# organization. `null` means disabling configuration variables check.

.github/workflows/nightly.yaml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ jobs:
3535
matrix:
3636
linter-version: [Snapshots, Latest]
3737
# TODO(Tyler): Re-add Windows runners.
38-
os: [ubuntu-latest, macOS]
38+
os: [ubuntu-latest, macos-latest]
3939
steps:
4040
- name: Checkout
4141
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -81,12 +81,12 @@ jobs:
8181
fail-fast: false
8282
matrix:
8383
linter-version: [Snapshots, Latest]
84-
os: [ubuntu-latest, macOS]
84+
os: [ubuntu-latest, macos-latest]
8585
include:
8686
# Normalize the filenames as inputs for ease of parsing
8787
- os: ubuntu-latest
8888
results-file: ubuntu-latest
89-
- os: macOS
89+
- os: macos-latest
9090
results-file: macos-latest
9191
outputs:
9292
plugin-version: ${{ steps.get-release.outputs.tag }}
@@ -216,12 +216,12 @@ jobs:
216216
fail-fast: false
217217
matrix:
218218
# TODO(Tyler): Re-add Windows runners.
219-
os: [ubuntu-latest, macOS]
219+
os: [ubuntu-latest, macos-latest]
220220
include:
221221
# Normalize the filenames as inputs for ease of parsing
222222
- os: ubuntu-latest
223223
results-file: ubuntu-latest
224-
- os: macOS
224+
- os: macos-latest
225225
results-file: macos-latest
226226
# - os: windows-latest
227227
# results-file: windows-latest

.github/workflows/pr.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,14 +126,14 @@ jobs:
126126
strategy:
127127
fail-fast: false
128128
matrix:
129-
os: [ubuntu-latest, macOS]
129+
os: [ubuntu-latest, macos-latest]
130130
steps:
131131
- name: Checkout
132132
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
133133

134134
# TODO(Tyler): Remove this once the cache has stabilized
135135
- name: Delete cache (mac only)
136-
if: matrix.os == 'macOS'
136+
if: matrix.os == 'macos-latest'
137137
# For now, avoid deleting cache on pull request changes to nightly. This improves PR experience.
138138
run: |
139139
if [ -d "${TMPDIR:-/tmp}/plugins_testing_download_cache" ]
@@ -183,7 +183,7 @@ jobs:
183183
strategy:
184184
fail-fast: false
185185
matrix:
186-
os: [ubuntu-latest, macOS]
186+
os: [ubuntu-latest, macos-latest]
187187
steps:
188188
- name: Checkout
189189
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ trunk check enable {linter}
5555
| Dart | [dart] |
5656
| Docker | [hadolint], [checkov] |
5757
| Dotenv | [dotenv-linter] |
58-
| GitHub | [actionlint], [zizmor] |
58+
| GitHub | [actionlint], [pinact], [zizmor] |
5959
| Go | [gofmt], [gofumpt], [goimports], [gokart], [golangci-lint], [golines], [semgrep] |
6060
| GraphQL | [graphql-schema-linter], [prettier] |
6161
| HAML | [haml-lint] |
@@ -81,7 +81,7 @@ trunk check enable {linter}
8181
| Ruby | [brakeman], [rubocop], [rufo], [semgrep], [standardrb] |
8282
| Rust | [clippy], [rustfmt] |
8383
| Scala | [scalafmt] |
84-
| Security | [checkov], [dustilock], [grype], [nancy], [osv-scanner], [snyk], [tfsec], [trivy], [trufflehog], [terrascan], [zizmor] |
84+
| Security | [checkov], [dustilock], [grype], [nancy], [osv-scanner], [pinact], [snyk], [tfsec], [trivy], [trufflehog], [terrascan], [zizmor] |
8585
| SQL | [sqlfluff], [sqlfmt], [sql-formatter], [squawk] |
8686
| SVG | [svgo] |
8787
| Swift | [stringslint], [swiftlint], [swiftformat] |
@@ -151,6 +151,7 @@ trunk check enable {linter}
151151
[oxipng]: https://github.com/shssoichiro/oxipng#readme
152152
[perlcritic]: https://metacpan.org/pod/Perl::Critic
153153
[perltidy]: https://metacpan.org/dist/Perl-Tidy/view/bin/perltidy
154+
[pinact]: https://github.com/suzuki-shunsuke/pinact#readme
154155
[php-cs-fixer]: https://github.com/PHP-CS-Fixer/PHP-CS-Fixer#readme
155156
[phpstan]: https://phpstan.org/
156157
[pmd]: https://pmd.github.io/

linters/pinact/pinact.test.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import * as fs from "fs";
2+
import * as path from "path";
3+
import { customLinterCheckTest } from "tests";
4+
import { TrunkLintDriver } from "tests/driver";
5+
import { TEST_DATA } from "tests/utils";
6+
7+
const moveWorkflowFile =
8+
(filename: string, disableGhAuth = false) =>
9+
async (driver: TrunkLintDriver) => {
10+
if (disableGhAuth) {
11+
process.env.PINACT_DISABLE_GH_AUTH = "1";
12+
} else {
13+
delete process.env.PINACT_DISABLE_GH_AUTH;
14+
}
15+
driver.moveFile(path.join(TEST_DATA, filename), path.join(".github/workflows", filename));
16+
await driver.gitDriver?.add(".").commit("moved");
17+
};
18+
19+
const moveWorkflowFiles =
20+
(sourceDir = TEST_DATA) =>
21+
async (driver: TrunkLintDriver) => {
22+
delete process.env.PINACT_DISABLE_GH_AUTH;
23+
24+
fs.readdirSync(path.resolve(driver.getSandbox(), sourceDir), { withFileTypes: true })
25+
.filter((file) => file.isFile())
26+
.forEach((file) => {
27+
driver.moveFile(path.join(sourceDir, file.name), path.join(".github/workflows", file.name));
28+
});
29+
await driver.gitDriver?.add(".").commit("moved");
30+
};
31+
32+
const enablePinactCommand =
33+
(command: string, preCheck?: (driver: TrunkLintDriver) => Promise<void>) =>
34+
async (driver: TrunkLintDriver) => {
35+
delete process.env.PINACT_DISABLE_GH_AUTH;
36+
37+
const trunkYamlPath = ".trunk/trunk.yaml";
38+
const currentContents = driver.readFile(trunkYamlPath);
39+
const pinactRegex = /- pinact@(.+)\n/;
40+
41+
driver.writeFile(
42+
trunkYamlPath,
43+
currentContents.replace(pinactRegex, `- pinact@$1:\n commands: [${command}]\n`),
44+
);
45+
46+
if (preCheck) {
47+
await preCheck(driver);
48+
}
49+
};
50+
51+
const skipIfMissingGitHubToken = () => {
52+
if (!process.env.GH_TOKEN && !process.env.GITHUB_TOKEN && !process.env.PINACT_GITHUB_TOKEN) {
53+
console.log(
54+
"Skipping pinact online audit test because GH_TOKEN, GITHUB_TOKEN, and PINACT_GITHUB_TOKEN are not set.",
55+
);
56+
return true;
57+
}
58+
return false;
59+
};
60+
61+
customLinterCheckTest({
62+
linterName: "pinact",
63+
testName: "missing_version_comment",
64+
args: ".github",
65+
preCheck: moveWorkflowFile("missing_version_comment.in.yaml", true),
66+
});
67+
68+
customLinterCheckTest({
69+
linterName: "pinact",
70+
testName: "unpinned",
71+
args: ".github",
72+
preCheck: moveWorkflowFile("unpinned.in.yaml", true),
73+
});
74+
75+
customLinterCheckTest({
76+
linterName: "pinact",
77+
testName: "version_comment",
78+
args: ".github",
79+
preCheck: moveWorkflowFiles(path.join(TEST_DATA, "online")),
80+
skipTestIf: skipIfMissingGitHubToken,
81+
});
82+
83+
customLinterCheckTest({
84+
linterName: "pinact",
85+
testName: "upgrade",
86+
args: ".github",
87+
preCheck: enablePinactCommand("upgrade", moveWorkflowFile("unpinned.in.yaml")),
88+
skipTestIf: skipIfMissingGitHubToken,
89+
});

linters/pinact/pinact_run.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
#!/usr/bin/env python3
2+
import os
3+
import shutil
4+
import subprocess
5+
import sys
6+
from pathlib import Path
7+
8+
9+
def resolve_executable(name: str) -> str | None:
10+
return shutil.which(name)
11+
12+
13+
def require_executable(name: str) -> str:
14+
path = resolve_executable(name)
15+
if path is None:
16+
print(f"{name} not found in PATH", file=sys.stderr)
17+
raise SystemExit(127)
18+
return path
19+
20+
21+
def validate_targets(paths: list[str]) -> list[str]:
22+
validated: list[str] = []
23+
for path in paths:
24+
target = Path(path)
25+
if not target.exists():
26+
print(f"target not found: {path}", file=sys.stderr)
27+
raise SystemExit(2)
28+
validated.append(str(target.resolve()))
29+
return validated
30+
31+
32+
def gh_auth_token() -> str | None:
33+
gh = resolve_executable("gh")
34+
if gh is None:
35+
return None
36+
37+
try:
38+
result = subprocess.run(
39+
[gh, "auth", "token"],
40+
shell=False,
41+
check=False,
42+
capture_output=True,
43+
text=True,
44+
)
45+
except OSError:
46+
return None
47+
48+
if result.returncode != 0:
49+
return None
50+
51+
token = result.stdout.strip()
52+
return token or None
53+
54+
55+
def configure_token_env() -> None:
56+
github_token = os.environ.get("GITHUB_TOKEN")
57+
pinact_github_token = os.environ.get("PINACT_GITHUB_TOKEN")
58+
gh_token = os.environ.get("GH_TOKEN")
59+
60+
if not github_token and gh_token:
61+
os.environ["GITHUB_TOKEN"] = gh_token
62+
github_token = gh_token
63+
64+
if not pinact_github_token and github_token:
65+
os.environ["PINACT_GITHUB_TOKEN"] = github_token
66+
67+
if not os.environ.get("GITHUB_TOKEN") and not os.environ.get("PINACT_GITHUB_TOKEN"):
68+
if not os.environ.get("PINACT_DISABLE_GH_AUTH"):
69+
token = gh_auth_token()
70+
if token:
71+
os.environ["GITHUB_TOKEN"] = token
72+
os.environ["PINACT_GITHUB_TOKEN"] = token
73+
74+
75+
def has_github_token() -> bool:
76+
return bool(os.environ.get("GITHUB_TOKEN") or os.environ.get("PINACT_GITHUB_TOKEN"))
77+
78+
79+
def build_pinact_args(mode: str) -> list[str]:
80+
args = ["pinact", "run", "-format", "sarif"]
81+
if mode == "upgrade":
82+
# -update bumps to latest semver; SARIF suggestions only (Trunk applies fixes).
83+
args.extend(["-update"])
84+
return args
85+
86+
# SARIF output implies -fix=false; let Trunk apply fixes from SARIF suggestions.
87+
if os.environ.get("PINACT_DISABLE_GH_AUTH"):
88+
args.append("-no-api")
89+
elif has_github_token():
90+
args.append("-verify-comment")
91+
else:
92+
args.append("-no-api")
93+
return args
94+
95+
96+
def run_pinact(mode: str, targets: list[str]) -> int:
97+
pinact = require_executable("pinact")
98+
return subprocess.run(
99+
[pinact, *build_pinact_args(mode)[1:], *validate_targets(targets)],
100+
shell=False,
101+
check=False,
102+
).returncode
103+
104+
105+
def main() -> int:
106+
configure_token_env()
107+
108+
argv = sys.argv[1:]
109+
mode = "lint"
110+
if argv and argv[0] == "--upgrade":
111+
mode = "upgrade"
112+
argv = argv[1:]
113+
114+
return run_pinact(mode, argv)
115+
116+
117+
if __name__ == "__main__":
118+
raise SystemExit(main())

linters/pinact/plugin.yaml

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
version: 0.1
2+
downloads:
3+
- name: pinact
4+
downloads:
5+
- os:
6+
linux: linux
7+
macos: darwin
8+
cpu:
9+
x86_64: amd64
10+
arm_64: arm64
11+
url: https://github.com/suzuki-shunsuke/pinact/releases/download/v${version}/pinact_${os}_${cpu}.tar.gz
12+
- os: windows
13+
cpu:
14+
x86_64: amd64
15+
arm_64: arm64
16+
url: https://github.com/suzuki-shunsuke/pinact/releases/download/v${version}/pinact_windows_${cpu}.zip
17+
tools:
18+
definitions:
19+
- name: pinact
20+
download: pinact
21+
shims: [pinact]
22+
known_good_version: 4.0.0
23+
lint:
24+
definitions:
25+
- name: pinact
26+
files: [github-workflow, github-actions]
27+
tools: [pinact]
28+
runtime: python
29+
hold_the_line: false
30+
description: Pin and verify GitHub Actions and reusable workflows
31+
commands:
32+
- name: lint
33+
output: sarif
34+
run: python3 ${plugin}/linters/pinact/pinact_run.py ${target}
35+
success_codes: [0, 1, 2, 3]
36+
batch: true
37+
cache_results: true
38+
is_security: true
39+
read_output_from: stdout
40+
disable_upstream: true
41+
- name: upgrade
42+
output: sarif
43+
run: python3 ${plugin}/linters/pinact/pinact_run.py --upgrade ${target}
44+
success_codes: [0, 1, 2, 3]
45+
batch: true
46+
cache_results: true
47+
is_security: true
48+
read_output_from: stdout
49+
disable_upstream: true
50+
enabled: false
51+
direct_configs:
52+
- .pinact.yml
53+
- .pinact.yaml
54+
- .github/pinact.yml
55+
- .github/pinact.yaml
56+
suggest_if: files_present
57+
environment:
58+
- name: GITHUB_TOKEN
59+
value: ${env.GITHUB_TOKEN}
60+
optional: true
61+
- name: PINACT_GITHUB_TOKEN
62+
value: ${env.PINACT_GITHUB_TOKEN}
63+
optional: true
64+
- name: GH_TOKEN
65+
value: ${env.GH_TOKEN}
66+
optional: true
67+
- name: PINACT_DISABLE_GH_AUTH
68+
value: ${env.PINACT_DISABLE_GH_AUTH}
69+
optional: true
70+
- name: PATH
71+
list: ["${runtime}", "${linter}", "${env.PATH}"]
72+
issue_url_format: https://github.com/suzuki-shunsuke/pinact/blob/main/docs/codes/
73+
known_good_version: 4.0.0
74+
version_command:
75+
parse_regex: ${semver}
76+
run: pinact version

0 commit comments

Comments
 (0)