Skip to content
Merged
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
164 changes: 164 additions & 0 deletions .github/workflows/pnpm-hardening-validate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
name: Validate pnpm hardening

on:
workflow_call: {}

permissions:
contents: read

jobs:
require-pnpm-files:
name: Require pnpm lockfile and workspace file
runs-on: ubuntu-latest
outputs:
pnpm_workspace_changed: ${{ steps.workspace_files.outputs.any_changed }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Detect pnpm-workspace.yaml changes
id: workspace_files
uses: tj-actions/[email protected]
with:
files: |
pnpm-workspace.yaml

- name: Require pnpm-lock.yaml and pnpm-workspace.yaml at repo root
shell: bash
run: |
set -euo pipefail
root="${GITHUB_WORKSPACE}"
missing=0
if [[ ! -f "${root}/pnpm-lock.yaml" ]]; then
echo "::error title=pnpm hardening::Missing pnpm-lock.yaml at the repository root (expected next to package.json)."
missing=1
fi
if [[ ! -f "${root}/pnpm-workspace.yaml" ]]; then
echo "::error title=pnpm hardening::Missing pnpm-workspace.yaml at the repository root."
missing=1
fi
if [[ "${missing}" -ne 0 ]]; then
exit 1
fi
echo "pnpm-lock.yaml and pnpm-workspace.yaml are present."

validate-pnpm-workspace:
name: Validate pnpm-workspace.yaml policy
runs-on: ubuntu-latest
needs: require-pnpm-files
if: needs.require-pnpm-files.outputs.pnpm_workspace_changed == 'true'
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Validate pnpm-workspace.yaml
shell: bash
run: |
set -euo pipefail
python3 -m pip install --disable-pip-version-check -q 'PyYAML==6.0.2'
python3 << 'PY'
import os
import sys

import yaml

REQUIRED_TOP_LEVEL_KEYS = (
"minimumReleaseAge",
"verifyDepsBeforeRun",
"trustPolicy",
"blockExoticSubdeps",
"strictDepBuilds",
"allowBuilds",
)
FORBIDDEN_TOP_LEVEL_KEY = "minimumReleaseAgeExclude"
MIN_MINIMUM_RELEASE_AGE = 1440
# Unquoted no-downgrade is valid YAML (same as pnpm.io/settings#trustpolicy); it is a
# string, not coerced like plain `off` (which PyYAML parses as boolean false).
EXPECTED_TRUST_POLICY = "no-downgrade"
EXPECTED_VERIFY_DEPS_BEFORE_RUN = "error"

root = os.environ.get("GITHUB_WORKSPACE", ".")
workspace_path = os.path.join(root, "pnpm-workspace.yaml")

errors: list[str] = []

parsed = None
workspace_load_ok = False
try:
with open(workspace_path, encoding="utf-8") as f:
parsed = yaml.safe_load(f)
workspace_load_ok = True
except OSError as e:
errors.append(f"pnpm-workspace.yaml could not be read: {e}")
except yaml.YAMLError as e:
errors.append(f"pnpm-workspace.yaml is not valid YAML: {e}")

if workspace_load_ok:
data = {} if parsed is None else parsed
if not isinstance(data, dict):
errors.append(
"pnpm-workspace.yaml must be a YAML mapping (object) at the top level."
)
else:
for key in REQUIRED_TOP_LEVEL_KEYS:
if key not in data:
errors.append(
f"pnpm-workspace.yaml: missing required top-level key '{key}'."
)
if FORBIDDEN_TOP_LEVEL_KEY in data:
errors.append(
"pnpm-workspace.yaml: forbidden top-level key "
f"'{FORBIDDEN_TOP_LEVEL_KEY}' must not be present."
)
if "minimumReleaseAge" in data:
mra = data["minimumReleaseAge"]
if mra is None or isinstance(mra, bool):
errors.append(
"pnpm-workspace.yaml: minimumReleaseAge must be a numeric "
f"value (minutes) and at least {MIN_MINIMUM_RELEASE_AGE}."
)
else:
try:
n = float(mra)
except (TypeError, ValueError):
errors.append(
"pnpm-workspace.yaml: minimumReleaseAge must be a numeric "
f"value (minutes) and at least {MIN_MINIMUM_RELEASE_AGE}."
)
else:
if n < MIN_MINIMUM_RELEASE_AGE:
errors.append(
"pnpm-workspace.yaml: minimumReleaseAge must be at least "
f"{MIN_MINIMUM_RELEASE_AGE} minutes (found {mra!r})."
)
if "verifyDepsBeforeRun" in data:
if data["verifyDepsBeforeRun"] != EXPECTED_VERIFY_DEPS_BEFORE_RUN:
errors.append(
"pnpm-workspace.yaml: verifyDepsBeforeRun must be "
f"{EXPECTED_VERIFY_DEPS_BEFORE_RUN!r} "
f"(found {data['verifyDepsBeforeRun']!r})."
)
if "trustPolicy" in data:
if data["trustPolicy"] != EXPECTED_TRUST_POLICY:
errors.append(
"pnpm-workspace.yaml: trustPolicy must be "
f"{EXPECTED_TRUST_POLICY!r} (found {data['trustPolicy']!r})."
)
for bool_key in ("blockExoticSubdeps", "strictDepBuilds"):
if bool_key in data and data[bool_key] is not True:
errors.append(
f"pnpm-workspace.yaml: {bool_key} must be YAML boolean true "
f"(found {data[bool_key]!r})."
)

for msg in errors:
print(f"::error title=pnpm hardening::{msg}")

if errors:
print(f"\nValidation failed with {len(errors)} error(s).", file=sys.stderr)
sys.exit(1)

print("pnpm-workspace.yaml policy validation passed.")
PY
Loading