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
7 changes: 6 additions & 1 deletion .githooks/commit-msg
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
#!/usr/bin/env bash
npx -y conventional-commit-msg "$1"
# `@latest` + explicit `--package` dodge two npm footguns: an inherited
# npm_config_package (agent wrappers like axexec/axrun set it, hijacking
# `npx -y` into exit 127), and local-root shadowing — without the version,
# `npm exec` resolves the conventional-commit-msg repo's own package and the
# bin is "not found", breaking the hook in that repo after prepare wires it up.
npm exec --yes --package=conventional-commit-msg@latest -- conventional-commit-msg "$1"
91 changes: 59 additions & 32 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,21 @@
# Native Git hook - enable with: git config core.hooksPath .githooks
set -euo pipefail

git diff --cached --check
ignored_staged_paths=(
":(exclude).agents/**"
":(exclude).claude/**"
":(exclude).clawpatch/**"
":(exclude).codex/**"
)

git diff --cached --check -- . "${ignored_staged_paths[@]}"

has_staged_changes() {
if [[ "$#" -eq 0 ]]; then
git diff --cached --quiet -- . "${ignored_staged_paths[@]}" || return 0
return 1
fi

git diff --cached --quiet -- "$@" || return 0
return 1
}
Expand All @@ -13,6 +25,10 @@ if [[ ! -f package.json ]]; then
exit 0
fi

if ! has_staged_changes; then
exit 0
fi

# `npm pkg fix` normalizes package.json (e.g., sorts keys, trims invalid
# fields). Only run it when package.json is already staged, so the hook never
# pulls unrelated working-tree edits into a commit.
Expand All @@ -21,52 +37,63 @@ if has_staged_changes package.json && command -v npm >/dev/null 2>&1; then
git add package.json 2>/dev/null || true
fi

run_package_script() {
local script_name="$1"
# --if-present runs the script when package.json defines it and exits 0 when it
# doesn't, so the hook can list a superset of scripts no repo fully implements.
if command -v pnpm >/dev/null 2>&1; then
pnpm run --if-present "$script_name"
elif command -v npm >/dev/null 2>&1; then
npm run --if-present "$script_name"
else
# return, not exit: callers redirect this function's output into a log
# they only dump on failure — exit here would abort before the dump,
# leaving the commit to die with no visible error.
echo "No package manager available to run script: $script_name" >&2
return 127
fi
}

script_log=$(mktemp)
trap 'rm -f "$script_log"' EXIT

# Logs a "==> <name>" header, then runs the script with output captured and shown only on failure before exiting non-zero.
run_script() {
local script_name="$1"
# Check if the script exists in package.json (node - reads from stdin, arg becomes argv[2])
if node - "$script_name" <<'EOF'
const fs = require("fs");
const pkg = JSON.parse(fs.readFileSync("package.json", "utf8"));
const scripts = pkg.scripts || {};
process.exit(scripts[process.argv[2]] ? 0 : 1);
EOF
then
if command -v pnpm >/dev/null 2>&1; then
pnpm -s run "$script_name"
elif command -v npm >/dev/null 2>&1; then
npm run --silent "$script_name"
else
echo "No package manager available to run script: $script_name" >&2
exit 1
fi
echo "==> $script_name" >&2
if run_package_script "$script_name" > "$script_log" 2>&1; then
return 0
fi
cat "$script_log" >&2
exit 1
}

if [[ -f pnpm-lock.yaml ]] && command -v pnpm >/dev/null 2>&1; then
if has_staged_changes package.json pnpm-lock.yaml pnpm-workspace.yaml; then
# Order matters: dedupe is the cheaper read-only check, so fail fast on
# lockfile bloat before the heavier lockfile-vs-package.json sync check.
pnpm dedupe --check
pnpm dedupe --check --config.confirm-modules-purge=false
pnpm install --frozen-lockfile --ignore-scripts --config.confirm-modules-purge=false
fi
fi

if has_staged_changes; then
run_script "format:check"
fi

run_script "format:check"
run_script "knip"
run_script "typecheck"
run_script "lint"
run_script "fta"

# Run tests quietly - only show output on failure
test_log=$(mktemp)
trap 'rm -f "$test_log"' EXIT

if run_script "test" > "$test_log" 2>&1; then
: # Success, stay silent
else
cat "$test_log" >&2
# Scrub git's hook env vars so a test that shells out to git hits a scratch repo, not this repo's real gitdir.
run_test_script() {
local script_name="$1"
echo "==> $script_name" >&2
if (
unset GIT_DIR GIT_INDEX_FILE GIT_WORK_TREE GIT_COMMON_DIR GIT_PREFIX GIT_OBJECT_DIRECTORY
run_package_script "$script_name"
) > "$script_log" 2>&1; then
return 0
fi
cat "$script_log" >&2
exit 1
fi
}

run_test_script "test"
31 changes: 27 additions & 4 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ jobs:
- name: Checkout code
uses: actions/checkout@v6

- name: Strip untrusted project install config
# pull_request checks out attacker-controlled code: drop committed .npmrc (redirects installs / leaks the registry token) and both pnpmfile variants .pnpmfile.cjs/.pnpmfile.mjs (pnpm 11 loads .mjs first, and their install hooks run even under --ignore-scripts).
run: rm -f .npmrc .pnpmfile.cjs .pnpmfile.mjs

- name: Setup pnpm
uses: pnpm/action-setup@v5

Expand All @@ -24,8 +28,27 @@ jobs:
node-version-file: "package.json"
cache: "pnpm"

- name: Verify install secret
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
set -eu
if [ -z "${NODE_AUTH_TOKEN:-}" ]; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid requiring NPM_TOKEN on forked PR checks

For PRs opened from forks, secrets.NPM_TOKEN is empty because GitHub does not pass repository secrets to fork-triggered workflows except GITHUB_TOKEN (docs). Since this workflow runs on every pull_request, the new unconditional guard fails before install for any outside contributor PR; dedupe-check.yml has the same pattern. Consider gating the private-registry install to trusted PRs or using a public/non-secret fallback for forked PR checks.

Useful? React with 👍 / 👎.

echo "::error::Missing required install secret NPM_TOKEN; set it in the repo or org Actions secrets."
exit 1
fi

- name: Configure registry auth
run: echo "//npm.j4k.dev/:_authToken=${NODE_AUTH_TOKEN}" > ~/.npmrc
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Install dependencies
run: pnpm install --frozen-lockfile
run: pnpm install --frozen-lockfile --ignore-scripts --ignore-pnpmfile

- name: Strip registry token
# pull_request gets secrets on same-repo PRs; remove the token before any PR-controlled script reads ~/.npmrc.
run: rm -f ~/.npmrc

- name: Run knip
run: pnpm knip
Expand All @@ -42,8 +65,8 @@ jobs:
- name: Run fta
run: pnpm fta

- name: Run tests
run: pnpm run test

- name: Run build
run: pnpm build

- name: Run tests
run: pnpm run test
32 changes: 20 additions & 12 deletions .github/workflows/commit-msg.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
---
name: commit-msg

# Validate on pull_request only; a post-merge --last check would choke on the
# immutable, auto-composed squash body whose lines exceed the body-length limit.

on:
push:
branches: [main]
pull_request:

permissions:
Expand Down Expand Up @@ -31,17 +32,24 @@ jobs:
path: ~/.npm
key: npm-conventional-commit-msg-${{ env.COMMITLINT_PACKAGE_VERSION }}

- name: Validate commits with commitlint
- name: Validate commit messages with commitlint
# A squash merge (the default) lands the PR title as main's subject; a
# rebase merge lands the PR's commits. Validate both so semantic-release
# never sees an unparseable subject on main.
shell: bash
env:
# Pass the attacker-controllable PR title via env, never inline in run: (script injection).
PR_TITLE: ${{ github.event.pull_request.title }}
run: |
set -euo pipefail

if [[ "${{ github.event_name }}" == "pull_request" ]]; then
npm exec --yes --package=conventional-commit-msg@${COMMITLINT_PACKAGE_VERSION} -- \
conventional-commit-msg \
--from "${{ github.event.pull_request.base.sha }}" \
--to "${{ github.event.pull_request.head.sha }}"
else
npm exec --yes --package=conventional-commit-msg@${COMMITLINT_PACKAGE_VERSION} -- \
conventional-commit-msg --last
fi
# Squash: the PR title becomes main's subject.
printf '%s\n' "$PR_TITLE" \
| npm exec --yes --package=conventional-commit-msg@${COMMITLINT_PACKAGE_VERSION} -- \
conventional-commit-msg

# Rebase: the PR's individual commits land on main.
npm exec --yes --package=conventional-commit-msg@${COMMITLINT_PACKAGE_VERSION} -- \
conventional-commit-msg \
--from "${{ github.event.pull_request.base.sha }}" \
--to "${{ github.event.pull_request.head.sha }}"
24 changes: 22 additions & 2 deletions .github/workflows/dedupe-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ jobs:
- name: Checkout code
uses: actions/checkout@v6

- name: Strip untrusted project install config
# pull_request checks out attacker-controlled code: drop committed .npmrc (redirects installs / leaks the registry token) and both pnpmfile variants .pnpmfile.cjs/.pnpmfile.mjs (pnpm 11 loads .mjs first, and their hooks run under --ignore-scripts during both install and dedupe, which keep the token on disk).
run: rm -f .npmrc .pnpmfile.cjs .pnpmfile.mjs

- name: Setup pnpm
uses: pnpm/action-setup@v5

Expand All @@ -28,8 +32,24 @@ jobs:
node-version-file: "package.json"
cache: "pnpm"

- name: Verify install secret
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
set -eu
if [ -z "${NODE_AUTH_TOKEN:-}" ]; then
echo "::error::Missing required install secret NPM_TOKEN; set it in the repo or org Actions secrets."
exit 1
fi

- name: Configure registry auth
run: echo "//npm.j4k.dev/:_authToken=${NODE_AUTH_TOKEN}" > ~/.npmrc
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Install dependencies
run: pnpm install --frozen-lockfile
run: pnpm install --frozen-lockfile --ignore-scripts --ignore-pnpmfile

- name: Check for dedupe opportunities
run: pnpm dedupe --check
# No token strip here (unlike checks.yml, which strips before untrusted build/test): install ran --ignore-scripts --ignore-pnpmfile and `pnpm dedupe --check` is a builtin, so no untrusted code runs with ~/.npmrc present — stripping it would only risk breaking dedupe on private-registry repos.
run: pnpm dedupe --check --ignore-scripts
27 changes: 0 additions & 27 deletions .github/workflows/pr-review-on-open.yml

This file was deleted.

27 changes: 0 additions & 27 deletions .github/workflows/pr-review-on-push.yml

This file was deleted.

42 changes: 42 additions & 0 deletions .github/workflows/pr-review.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
---
name: PR Review

on:
pull_request_target:
types: [opened, synchronize, reopened]
workflow_dispatch:
inputs:
pr_number:
description: "PR number to review"
required: true
type: number

jobs:
approach:
if: ${{ github.event_name == 'workflow_dispatch' || github.event.action == 'opened' }}
permissions:
contents: read
pull-requests: write
actions: read
uses: Jercik/axgithub/.github/workflows/pr-review.yml@v1
with:
label: approach
recipes: '[{"recipe":"pr-review-approach-smart","name":"smart draw 1"},{"recipe":"pr-review-approach-smart","name":"smart draw 2"},"pr-review-approach-2","pr-review-approach-3"]'
pr_number: ${{ github.event.pull_request.number || inputs.pr_number }}
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
AXRECIPE_API_KEY: ${{ secrets.AXRECIPE_API_KEY }}

code:
permissions:
contents: read
pull-requests: write
actions: read
uses: Jercik/axgithub/.github/workflows/pr-review.yml@v1
with:
label: code
recipes: '[{"recipe":"pr-review-code-smart","name":"smart draw 1"},{"recipe":"pr-review-code-smart","name":"smart draw 2"}]'
pr_number: ${{ github.event.pull_request.number || inputs.pr_number }}
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
AXRECIPE_API_KEY: ${{ secrets.AXRECIPE_API_KEY }}
Loading
Loading