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
97 changes: 97 additions & 0 deletions .github/workflows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,103 @@ This directory hosts reusable workflows under `j7an/shared-workflows`. Consumers

> **Note:** `@v3` and `@v2` continue to work at their last-released revisions, but receive no further updates. See the root README's "v3 → v4 migration" section.

## `pre-commit-autoupdate.yml`

Runs `pre-commit autoupdate` for consumer repos, detects changes to the
configured pre-commit config file, and opens a dependency-update pull request.
The workflow is `workflow_call`-only: each caller keeps its own schedule,
manual dispatch trigger, branch filters, and optional concurrency.

The runner path is language-neutral but uv-runner scoped. The consumer repo
needs a pre-commit config file (defaults to `.pre-commit-config.yaml` via
`config_path`); it does not need to be a Python or uv project because the
reusable workflow installs uv and runs `uvx`.

### Recommended App-token caller

Use this shape for repos with required checks or branch protection. The caller
repository must define `vars.RELEASE_BOT_APP_ID` and the
`RELEASE_BOT_PRIVATE_KEY` secret.

```yaml
name: Pre-commit Autoupdate

on:
schedule:
- cron: "0 8 * * 1"
workflow_dispatch:

permissions: {}

concurrency:
group: pre-commit-autoupdate
cancel-in-progress: true

jobs:
autoupdate:
permissions:
contents: read
uses: j7an/shared-workflows/.github/workflows/pre-commit-autoupdate.yml@v4
secrets:
RELEASE_BOT_PRIVATE_KEY: ${{ secrets.RELEASE_BOT_PRIVATE_KEY }}
```

Pass the private key explicitly rather than using `secrets: inherit`. The App
token is minted inside the reusable workflow with contents and pull-request
write scopes, while the caller's automatic `GITHUB_TOKEN` remains read-only.

### Minimal GITHUB_TOKEN fallback caller

Use this shape only for repos that accept the trigger caveat or have no
required checks on the generated PRs.

```yaml
name: Pre-commit Autoupdate

on:
schedule:
- cron: "0 8 * * 1"
workflow_dispatch:

permissions: {}

concurrency:
group: pre-commit-autoupdate
cancel-in-progress: true

jobs:
autoupdate:
permissions:
contents: write
pull-requests: write
uses: j7an/shared-workflows/.github/workflows/pre-commit-autoupdate.yml@v4
```

No App var and no private-key secret are passed. If PR creation fails with a
403 in this mode, grant `contents: write` and `pull-requests: write` on the
caller job.

GITHUB_TOKEN-authored PRs may not trigger required CI automatically due to
GitHub's recursion guard. If checks do not start, close and reopen the PR or
push an empty commit. Prefer the App-token caller for repos with required
checks.

### Inputs

| Input | Type | Required | Default | Description |
|---|---|---|---|---|
| `config_path` | string | no | `.pre-commit-config.yaml` | File to check for changes and, when `restrict_paths` is true, the only path committed. |
| `branch` | string | no | `deps/pre-commit-autoupdate` | Pull request branch. |
| `title` | string | no | `deps: update pre-commit hooks` | Pull request title. |
| `commit_message` | string | no | `deps: update pre-commit hooks` | Commit message for hook updates. |
| `labels` | string | no | `dependencies` | Labels passed to `create-pull-request`. |
| `sign_commits` | boolean | no | `true` | Whether `create-pull-request` signs commits. |
| `restrict_paths` | boolean | no | `true` | When true, passes `add-paths: config_path` so only the pre-commit config is committed. |
| `pre_commit_version` | string | no | `""` | Optional pre-commit runner version. Empty uses latest; set it as a regression circuit-breaker. |

`delete-branch: true` is standardized by the reusable workflow, so recurring
automation branches are cleaned up after merge.

## `security-scan.yml`

Runs the shared security scanning baseline for sibling repos: CodeQL,
Expand Down
250 changes: 250 additions & 0 deletions .github/workflows/pre-commit-autoupdate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
name: Pre-commit Autoupdate

on:
workflow_call:
inputs:
config_path:
type: string
default: ".pre-commit-config.yaml"
description: "Pre-commit config path to update, diff, and optionally restrict commits to."
branch:
type: string
default: "deps/pre-commit-autoupdate"
description: "Pull request branch."
title:
type: string
default: "deps: update pre-commit hooks"
description: "Pull request title."
commit_message:
type: string
default: "deps: update pre-commit hooks"
description: "Commit message for hook updates."
labels:
type: string
default: "dependencies"
description: "Labels passed to create-pull-request."
sign_commits:
type: boolean
default: true
description: "Whether create-pull-request signs commits."
restrict_paths:
type: boolean
default: true
description: "Restrict the PR commit to config_path."
pre_commit_version:
type: string
default: ""
description: "Optional pre-commit runner version. Empty uses latest."
secrets:
RELEASE_BOT_PRIVATE_KEY:
required: false

permissions: {}

jobs:
autoupdate:
name: Autoupdate
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Harden runner
uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
with:
egress-policy: audit

- name: Preflight inputs and auth
id: preflight
env:
APP_ID: ${{ vars.RELEASE_BOT_APP_ID }}
APP_PRIVATE_KEY: ${{ secrets.RELEASE_BOT_PRIVATE_KEY }}
CONFIG_PATH: ${{ inputs.config_path }}
PRE_COMMIT_VERSION: ${{ inputs.pre_commit_version }}
run: |
if [ -n "$APP_PRIVATE_KEY" ]; then
HAS_APP_PRIVATE_KEY=true
else
HAS_APP_PRIVATE_KEY=false
fi
unset APP_PRIVATE_KEY
export APP_ID HAS_APP_PRIVATE_KEY CONFIG_PATH PRE_COMMIT_VERSION

# --- BEGIN inline:scripts/pre-commit-autoupdate-preflight.sh ---
pre_commit_autoupdate_preflight() (
# pre-commit-autoupdate-preflight.sh - validate workflow inputs and emit
# secret-free primitives for the reusable pre-commit autoupdate workflow.
#
# Inputs (env vars):
# CONFIG_PATH pre-commit config path to diff and optionally commit
# PRE_COMMIT_VERSION optional pre-commit runner version
# APP_ID caller repo vars.RELEASE_BOT_APP_ID, if configured
# HAS_APP_PRIVATE_KEY true when the private key secret is present, else false
#
# Outputs (stdout, GitHub output format):
# auth_mode
# config_path
# use_fallback_caveat
# use_pinned_pre_commit
# pre_commit_version

set -euo pipefail

fail() {
echo "::error::$*" >&2
exit 1
}

require_bool() {
local name="$1" value="$2"
case "$value" in
true|false) ;;
*) fail "${name} must be true or false" ;;
esac
}

contains_traversal_segment() {
local path="$1"
case "$path" in
..|../*|*/..|*/../*) return 0 ;;
*) return 1 ;;
esac
}

CONFIG_PATH="${CONFIG_PATH:-}"
PRE_COMMIT_VERSION="${PRE_COMMIT_VERSION:-}"
APP_ID="${APP_ID:-}"
HAS_APP_PRIVATE_KEY="${HAS_APP_PRIVATE_KEY:-false}"

require_bool "HAS_APP_PRIVATE_KEY" "$HAS_APP_PRIVATE_KEY"

if [ -z "$CONFIG_PATH" ]; then
fail "config_path must not be empty"
fi

case "$CONFIG_PATH" in
/*) fail "config_path must be relative: $CONFIG_PATH" ;;
esac

case "$CONFIG_PATH" in
*$'\n'*|*$'\r'*) fail "config_path must not contain newlines" ;;
esac

if contains_traversal_segment "$CONFIG_PATH"; then
fail "config_path must not contain '..' path traversal segments: $CONFIG_PATH"
fi

AUTH_MODE="github_token"
USE_FALLBACK_CAVEAT="true"
if [ -n "$APP_ID" ]; then
if [ "$HAS_APP_PRIVATE_KEY" != "true" ]; then
fail "App auth half-configured - set both vars.RELEASE_BOT_APP_ID and RELEASE_BOT_PRIVATE_KEY, or neither."
fi
AUTH_MODE="app"
USE_FALLBACK_CAVEAT="false"
fi

USE_PINNED_PRE_COMMIT="false"
if [ -n "$PRE_COMMIT_VERSION" ]; then
case "$PRE_COMMIT_VERSION" in
*[!A-Za-z0-9.+_!-]*)
fail "pre_commit_version contains unsupported characters (allowed: A-Z a-z 0-9 . + _ ! -)"
;;
esac
USE_PINNED_PRE_COMMIT="true"
fi

printf 'auth_mode=%s\n' "$AUTH_MODE"
printf 'config_path=%s\n' "$CONFIG_PATH"
printf 'use_fallback_caveat=%s\n' "$USE_FALLBACK_CAVEAT"
printf 'use_pinned_pre_commit=%s\n' "$USE_PINNED_PRE_COMMIT"
printf 'pre_commit_version=%s\n' "$PRE_COMMIT_VERSION"
)
# --- END inline:scripts/pre-commit-autoupdate-preflight.sh ---

pre_commit_autoupdate_preflight >> "$GITHUB_OUTPUT"

- name: Mint GitHub App token
id: app-token
if: steps.preflight.outputs.auth_mode == 'app'
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ vars.RELEASE_BOT_APP_ID }}
private-key: ${{ secrets.RELEASE_BOT_PRIVATE_KEY }}
permission-contents: write
permission-pull-requests: write

- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false

- name: Install uv
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0

- name: Run pre-commit autoupdate
env:
USE_PINNED_PRE_COMMIT: ${{ steps.preflight.outputs.use_pinned_pre_commit }}
PRE_COMMIT_VERSION: ${{ steps.preflight.outputs.pre_commit_version }}
CONFIG_PATH: ${{ steps.preflight.outputs.config_path }}
run: |
if [ "$USE_PINNED_PRE_COMMIT" = "true" ]; then
uvx --from "pre-commit==$PRE_COMMIT_VERSION" pre-commit autoupdate -c "$CONFIG_PATH"
else
uvx pre-commit autoupdate -c "$CONFIG_PATH"
fi

- name: Check for changes
id: diff
env:
CONFIG_PATH: ${{ steps.preflight.outputs.config_path }}
run: |
if git diff --quiet -- "$CONFIG_PATH"; then
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
fi

- name: Compose pull request body
id: pr-body
env:
USE_FALLBACK_CAVEAT: ${{ steps.preflight.outputs.use_fallback_caveat }}
run: |
{
echo "body<<EOF"
echo "Automated update of pre-commit hook versions via \`pre-commit autoupdate\`."
echo ""
echo "Review the diff to verify hook version changes are expected."
if [ "$USE_FALLBACK_CAVEAT" = "true" ]; then
echo ""
echo "Note: GITHUB_TOKEN-authored PRs may not trigger required CI automatically due to GitHub's recursion guard. If checks do not start, close and reopen this PR or push an empty commit."
fi
echo "EOF"
} >> "$GITHUB_OUTPUT"

- name: Open restricted PR with updates
if: ${{ steps.diff.outputs.changed == 'true' && inputs.restrict_paths }}
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
token: ${{ steps.app-token.outputs.token || github.token }}
branch: ${{ inputs.branch }}
title: ${{ inputs.title }}
commit-message: ${{ inputs.commit_message }}
body: ${{ steps.pr-body.outputs.body }}
labels: ${{ inputs.labels }}
sign-commits: ${{ inputs.sign_commits }}
delete-branch: true
add-paths: ${{ inputs.config_path }}

- name: Open unrestricted PR with updates
if: ${{ steps.diff.outputs.changed == 'true' && !inputs.restrict_paths }}
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
token: ${{ steps.app-token.outputs.token || github.token }}
branch: ${{ inputs.branch }}
title: ${{ inputs.title }}
commit-message: ${{ inputs.commit_message }}
body: ${{ steps.pr-body.outputs.body }}
labels: ${{ inputs.labels }}
sign-commits: ${{ inputs.sign_commits }}
delete-branch: true
Loading