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
195 changes: 195 additions & 0 deletions .github/workflows/public-quality.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
name: Public Quality

on:
workflow_call:
inputs:
profile:
description: Public repository quality profile
required: true
type: string

permissions:
contents: read

jobs:
typescript:
name: TypeScript
if: inputs.profile == 'typescript'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Set up pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9

- name: Set up Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "24"
cache: pnpm

- name: Install public dependencies
run: >-
env
-u GITHUB_TOKEN
-u NODE_AUTH_TOKEN
-u NPM_AUTH_TOKEN
-u NPM_TOKEN
-u PACKAGE_TOKEN
-u PACKAGES_TOKEN
pnpm install --frozen-lockfile

- name: Check format
run: pnpm run format:check

- name: Check types
run: pnpm run typecheck

- name: Check lint
run: pnpm run lint

- name: Run tests with coverage
run: pnpm run coverage

- name: Check unused files and dependencies
run: pnpm run knip

python:
name: Python ${{ matrix.python-version }}
if: inputs.profile == 'python'
runs-on: ubuntu-latest
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ matrix.python-version }}

- name: Set up uv
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
enable-cache: true
cache-dependency-glob: uv.lock

- name: Install locked dependencies
run: uv sync --all-extras --frozen --no-default-groups

- name: Check format
run: uv run --frozen black --check --diff src tests

- name: Check lint
run: >-
uv run --frozen flake8 src tests
--count
--max-complexity=10
--max-line-length=88
--statistics

- name: Check types
run: uv run --frozen mypy

- name: Check Python security
run: uv run --frozen bandit -r src

- name: Run tests with coverage
run: >-
uv run --frozen pytest
--cov
--cov-branch
--cov-fail-under=80
--cov-report=term-missing
--junitxml=junit.xml
-o junit_family=legacy

metadata:
name: Metadata
if: inputs.profile == 'metadata'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Set up Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "24"

- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.13"

- name: Install workflow security tool
run: python -m pip install "zizmor==1.26.1"

- name: Check workflow syntax
run: npx --yes github-actionlint@1.7.12 -shellcheck ""

- name: Check workflow security
run: >-
zizmor
--min-severity=medium
--min-confidence=medium
.github/workflows

- name: Check format
run: npx --yes prettier@3.9.4 --check .

- name: Build repository context
run: npx --yes repomix@1.16.0

result:
name: Quality
if: always()
needs: [typescript, python, metadata]
runs-on: ubuntu-latest
timeout-minutes: 5
env:
METADATA_RESULT: ${{ needs.metadata.result }}
PROFILE: ${{ inputs.profile }}
PYTHON_RESULT: ${{ needs.python.result }}
TYPESCRIPT_RESULT: ${{ needs.typescript.result }}
steps:
- name: Validate selected profile
shell: bash
run: |
set -euo pipefail

case "${PROFILE}" in
typescript)
selected_result="${TYPESCRIPT_RESULT}"
;;
python)
selected_result="${PYTHON_RESULT}"
;;
metadata)
selected_result="${METADATA_RESULT}"
;;
*)
echo "::error::Unknown public quality profile: ${PROFILE}"
exit 1
;;
esac

if [[ "${selected_result}" != "success" ]]; then
echo "::error::The ${PROFILE} quality profile ended with ${selected_result}."
exit 1
fi

echo "The ${PROFILE} quality profile passed."
14 changes: 12 additions & 2 deletions .github/workflows/public-token-free-security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,11 @@ jobs:
r"^\s*-?\s*['\"]?uses['\"]?\s*:\s*['\"]?([^\s#'\"]+)",
re.MULTILINE,
)
public_quality_pattern = re.compile(
r"MNPPI/\.github/\.github/workflows/"
r"public-quality\.yml@[0-9a-f]{40}",
re.IGNORECASE,
)
secret_pattern = re.compile(
r"\$\{\{\s*secrets\.([A-Za-z0-9_]+)", re.IGNORECASE
)
Expand Down Expand Up @@ -185,12 +190,17 @@ jobs:

for match in uses_pattern.finditer(text):
target = match.group(1)
if target.casefold().startswith("mnppi/"):
if (
target.casefold().startswith("mnppi/")
and not public_quality_pattern.fullmatch(target)
):
add_error(
path,
text,
match.start(),
"MNPPI actions and reusable workflows are not allowed",
"MNPPI actions and reusable workflows are not allowed "
"unless they call the approved public quality workflow "
"at a full commit SHA",
)

for match in secret_pattern.finditer(text):
Expand Down
31 changes: 5 additions & 26 deletions .github/workflows/quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,36 +5,15 @@ on:
branches: [main]
push:
branches: [main]
merge_group:
workflow_dispatch:

permissions:
contents: read

jobs:
quality:
name: Quality
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false

- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 22

- name: Install zizmor
run: python -m pip install "zizmor==1.26.1"

- name: actionlint
run: npx --yes github-actionlint@1.7.12 -shellcheck ""

- name: zizmor
run: zizmor --min-severity=medium --min-confidence=medium .github/workflows

- name: Repomix context pack
run: npx --yes repomix@1.16.0

- name: Format check
run: npx --yes prettier@3.9.4 --check .
name: Public Quality
uses: ./.github/workflows/public-quality.yml
with:
profile: metadata
62 changes: 62 additions & 0 deletions docs/public-quality.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Public Quality

The `public-quality.yml` reusable workflow provides quality checks for public
MNPPI repositories. It does not use private packages or package credentials.

## Profiles

The workflow has three profiles:

- `typescript` checks format, types, lint, test coverage, and unused files.
- `python` checks Python 3.10 through 3.13 with a frozen `uv.lock` file.
- `metadata` checks workflow syntax, workflow security, format, and repository
context generation.

Each profile ends with one stable `Quality` job. A repository ruleset can
require this job after a live pull request proves its exact check name.

## Caller

Each repository stores a small caller workflow:

```yaml
jobs:
quality:
permissions:
contents: read
uses: MNPPI/.github/.github/workflows/public-quality.yml@COMMIT_SHA
with:
profile: typescript
```

Replace `COMMIT_SHA` with the full approved commit SHA. Do not use a branch or
tag.

The caller must not pass secrets. The reusable workflow uses a standard
GitHub-hosted runner and read-only repository access.

## Profile Requirements

The TypeScript profile requires these package scripts:

- `format:check`
- `typecheck`
- `lint`
- `coverage`
- `knip`

The repository must commit `pnpm-lock.yaml`.

The Python profile requires the `dev` project extra and these tools:

- Black
- Flake8
- mypy
- Bandit
- pytest
- pytest-cov

The repository must commit `uv.lock`.

The metadata profile requires `repomix.config.jsonc`. The repository must
ignore `.repomix/` and `repomix-output.*`.
Loading