Run ShellCheck on scripts in GitHub Actions workflows (#55486)

Summary:
Resolves https://github.com/pytorch/pytorch/issues/55314.

- [x] Extract shell scripts from `.github/workflows/*.yml` into `.shellcheck_generated` dir
- [x] Run ShellCheck on `.shellcheck_generated`
- [x] Fail if any of the extracted scripts contain [GitHub Actions expressions][]: `${{ <expression> }}`
- [x] Fix the newly-surfaced warnings

Pull Request resolved: https://github.com/pytorch/pytorch/pull/55486

Test Plan:
Locally run the "ShellCheck" step from "Lint / quick-checks".

[github actions expressions]: https://docs.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#about-contexts-and-expressions

Reviewed By: malfet

Differential Revision: D27627590

Pulled By: samestep

fbshipit-source-id: 8a22c6743e11b3059506043735f100efdd7c5a26
This commit is contained in:
Sam Estep
2021-04-08 13:13:22 -07:00
committed by Facebook GitHub Bot
parent 960b40156c
commit adc65974b2
12 changed files with 130 additions and 39 deletions

76
tools/extract_scripts.py Executable file
View File

@ -0,0 +1,76 @@
#!/usr/bin/env python3
# these two pages have some relevant information:
# https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions
# https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners
import argparse
import re
import sys
from pathlib import Path
from typing import Any, Dict
import yaml
Job = Dict[str, Any]
windows_labels = {'windows-latest', 'windows-2019'}
def get_default_shell(job: Job) -> str:
return 'pwsh' if job['runs-on'] in windows_labels else 'bash'
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument('--out', required=True)
args = parser.parse_args()
out = Path(args.out)
if out.exists():
sys.exit(f'{out} already exists; aborting to avoid overwriting')
gha_expressions_found = False
for p in Path('.github/workflows').iterdir():
with open(p) as f:
workflow = yaml.safe_load(f)
for job_name, job in workflow['jobs'].items():
job_dir = out / p / job_name
default_shell = get_default_shell(job)
steps = job['steps']
index_chars = len(str(len(steps) - 1))
for i, step in enumerate(steps, start=1):
script = step.get('run')
if script:
step_name = step['name']
if '${{' in script:
gha_expressions_found = True
print(
f'{p} job `{job_name}` step {i}: {step_name}',
file=sys.stderr
)
if step.get('shell', default_shell) == 'bash':
job_dir.mkdir(parents=True, exist_ok=True)
sanitized = re.sub(
'[^a-zA-Z_]+', '_',
f'_{step_name}',
).rstrip('_')
filename = f'{i:0{index_chars}}{sanitized}.sh'
(job_dir / filename).write_text(
f'#!/usr/bin/env bash\nset -eo pipefail\n{script}'
)
if gha_expressions_found:
sys.exit(
'Each of the above scripts contains a GitHub Actions '
'${{ <expression> }} which must be replaced with an `env` variable'
' for security reasons.'
)
if __name__ == '__main__':
main()