diff --git a/.github/actions/setup-node-env/action.yml b/.github/actions/setup-node-env/action.yml index a722982004..334cd3c24f 100644 --- a/.github/actions/setup-node-env/action.yml +++ b/.github/actions/setup-node-env/action.yml @@ -37,7 +37,7 @@ runs: exit 1 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: ${{ inputs.node-version }} check-latest: true @@ -70,14 +70,29 @@ runs: shell: bash env: CI: "true" + FROZEN_LOCKFILE: ${{ inputs.frozen-lockfile }} run: | + set -euo pipefail export PATH="$NODE_BIN:$PATH" which node node -v pnpm -v - LOCKFILE_FLAG="" - if [ "${{ inputs.frozen-lockfile }}" = "true" ]; then - LOCKFILE_FLAG="--frozen-lockfile" + case "$FROZEN_LOCKFILE" in + true) LOCKFILE_FLAG="--frozen-lockfile" ;; + false) LOCKFILE_FLAG="" ;; + *) + echo "::error::Invalid frozen-lockfile input: '$FROZEN_LOCKFILE' (expected true or false)" + exit 2 + ;; + esac + + install_args=( + install + --ignore-scripts=false + --config.engine-strict=false + --config.enable-pre-post-scripts=true + ) + if [ -n "$LOCKFILE_FLAG" ]; then + install_args+=("$LOCKFILE_FLAG") fi - pnpm install $LOCKFILE_FLAG --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true || \ - pnpm install $LOCKFILE_FLAG --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true + pnpm "${install_args[@]}" || pnpm "${install_args[@]}" diff --git a/.github/actions/setup-pnpm-store-cache/action.yml b/.github/actions/setup-pnpm-store-cache/action.yml index c866393ee4..8e25492ac9 100644 --- a/.github/actions/setup-pnpm-store-cache/action.yml +++ b/.github/actions/setup-pnpm-store-cache/action.yml @@ -14,11 +14,17 @@ runs: steps: - name: Setup pnpm (corepack retry) shell: bash + env: + PNPM_VERSION: ${{ inputs.pnpm-version }} run: | set -euo pipefail + if [[ ! "$PNPM_VERSION" =~ ^[0-9]+(\.[0-9]+){1,2}([.-][0-9A-Za-z.-]+)?$ ]]; then + echo "::error::Invalid pnpm-version input: '$PNPM_VERSION'" + exit 2 + fi corepack enable for attempt in 1 2 3; do - if corepack prepare "pnpm@${{ inputs.pnpm-version }}" --activate; then + if corepack prepare "pnpm@$PNPM_VERSION" --activate; then pnpm -v exit 0 fi diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index efaf8039a3..cb67c44231 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -368,7 +368,7 @@ jobs: test -s dist/plugin-sdk/index.js - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: 22.x check-latest: true diff --git a/.github/workflows/install-smoke.yml b/.github/workflows/install-smoke.yml index 45154a5fab..6a59074b25 100644 --- a/.github/workflows/install-smoke.yml +++ b/.github/workflows/install-smoke.yml @@ -34,7 +34,7 @@ jobs: uses: actions/checkout@v4 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: 22.x check-latest: true diff --git a/.github/workflows/workflow-sanity.yml b/.github/workflows/workflow-sanity.yml index 438a71162d..4629db63f8 100644 --- a/.github/workflows/workflow-sanity.yml +++ b/.github/workflows/workflow-sanity.yml @@ -40,3 +40,24 @@ jobs: print(f"- {path}") sys.exit(1) PY + + actionlint: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install actionlint + shell: bash + run: | + set -euo pipefail + ACTIONLINT_VERSION="1.7.11" + archive="actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz" + curl -sSfL "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/${archive}" | tar -xz actionlint + sudo mv actionlint /usr/local/bin/actionlint + + - name: Lint workflows + run: actionlint + + - name: Disallow direct inputs interpolation in composite run blocks + run: python3 scripts/check-composite-action-input-interpolation.py diff --git a/scripts/check-composite-action-input-interpolation.py b/scripts/check-composite-action-input-interpolation.py new file mode 100644 index 0000000000..18c52501b8 --- /dev/null +++ b/scripts/check-composite-action-input-interpolation.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import pathlib +import re +import sys + + +INPUT_INTERPOLATION_RE = re.compile(r"\$\{\{\s*inputs\.") +RUN_LINE_RE = re.compile(r"^(\s*)run:\s*(.*)$") +USING_COMPOSITE_RE = re.compile(r"^\s*using:\s*composite\s*$", re.MULTILINE) + + +def indentation(line: str) -> int: + return len(line) - len(line.lstrip(" ")) + + +def scan_file(path: pathlib.Path) -> list[tuple[int, str]]: + text = path.read_text(encoding="utf-8") + if not USING_COMPOSITE_RE.search(text): + return [] + + lines = text.splitlines() + violations: list[tuple[int, str]] = [] + line_count = len(lines) + index = 0 + + while index < line_count: + line = lines[index] + match = RUN_LINE_RE.match(line) + if not match: + index += 1 + continue + + run_indent = len(match.group(1)) + run_value = match.group(2).strip() + line_no = index + 1 + + if run_value and run_value[0] not in ("|", ">"): + if INPUT_INTERPOLATION_RE.search(run_value): + violations.append((line_no, line.strip())) + index += 1 + continue + + index += 1 + while index < line_count: + script_line = lines[index] + if script_line.strip() == "": + index += 1 + continue + if indentation(script_line) <= run_indent: + break + if INPUT_INTERPOLATION_RE.search(script_line): + violations.append((index + 1, script_line.strip())) + index += 1 + + return violations + + +def main() -> int: + root = pathlib.Path(".github/actions") + files = sorted(root.rglob("action.y*ml")) + all_violations: list[tuple[pathlib.Path, int, str]] = [] + + for file_path in files: + for line_no, line in scan_file(file_path): + all_violations.append((file_path, line_no, line)) + + if all_violations: + print("Disallowed direct inputs interpolation in composite run blocks:") + for file_path, line_no, line in all_violations: + print(f"- {file_path}:{line_no}: {line}") + print("Use env: and reference shell variables instead.") + return 1 + + print("No direct inputs interpolation found in composite run blocks.") + return 0 + + +if __name__ == "__main__": + sys.exit(main())