Skip to content
Draft
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
140 changes: 140 additions & 0 deletions .github/workflows/node-repair.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
name: Repair selected Node pins
'on':
workflow_call:
inputs:
request-id:
required: true
type: string
pull-request:
required: true
type: string
expected-head:
required: true
type: string
expected-base:
required: true
type: string
selections:
required: true
type: string
permissions:
contents: read
jobs:
guard:
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
pull-requests: read
steps:
- uses: $/node/repair
env:
GH_TOKEN: ${{ github.token }}
with:
operation: guard
request-id: ${{ inputs.request-id }}
pull-request: ${{ inputs.pull-request }}
expected-head: ${{ inputs.expected-head }}
expected-base: ${{ inputs.expected-base }}
selections: ${{ inputs.selections }}
prepare:
needs: guard
runs-on: ubuntu-latest
timeout-minutes: 60
permissions:
contents: read
env:
npm_config_ignore_pnpmfile: 'true'
pnpm_config_ignore_pnpmfile: 'true'
outputs:
candidate-artifact-id: ${{ steps.candidate.outputs.artifact-id }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
ref: ${{ inputs.expected-head }}
persist-credentials: false
fetch-depth: 0
- uses: $/node/repair
with:
operation: prepare
request-id: ${{ inputs.request-id }}
pull-request: ${{ inputs.pull-request }}
expected-head: ${{ inputs.expected-head }}
expected-base: ${{ inputs.expected-base }}
selections: ${{ inputs.selections }}
- name: Seal candidate before validation
id: candidate
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: node-repair-candidate-${{ inputs.request-id }}
path: ${{ runner.temp }}/node-repair/candidate/
if-no-files-found: error
validate:
needs: prepare
runs-on: ubuntu-latest
timeout-minutes: 60
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
ref: ${{ inputs.expected-head }}
persist-credentials: false
fetch-depth: 0
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
with:
artifact-ids: ${{ needs.prepare.outputs.candidate-artifact-id }}
path: ${{ runner.temp }}/node-repair/candidate
merge-multiple: true
- uses: $/node/repair
with:
operation: validate
request-id: ${{ inputs.request-id }}
pull-request: ${{ inputs.pull-request }}
expected-head: ${{ inputs.expected-head }}
expected-base: ${{ inputs.expected-base }}
selections: ${{ inputs.selections }}
- name: Retain repair evidence
if: ${{ always() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: node-repair-${{ inputs.request-id }}
path: ${{ runner.temp }}/node-repair/evidence/
if-no-files-found: warn
- name: Show repair evidence
if: ${{ always() }}
shell: bash
run: |
for file in "$RUNNER_TEMP/node-repair/evidence/"*.md; do
if [ -f "$file" ]; then cat "$file" >> "$GITHUB_STEP_SUMMARY"; fi
done
publish:
needs: [prepare, validate]
runs-on: ubuntu-latest
timeout-minutes: 10
concurrency:
group: node-dependency-proposal-${{ github.repository }}
cancel-in-progress: false
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
ref: ${{ inputs.expected-head }}
fetch-depth: 0
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
with:
artifact-ids: ${{ needs.prepare.outputs.candidate-artifact-id }}
path: ${{ runner.temp }}/node-repair/candidate
merge-multiple: true
- uses: $/node/repair
env:
GH_TOKEN: ${{ github.token }}
with:
operation: publish
request-id: ${{ inputs.request-id }}
pull-request: ${{ inputs.pull-request }}
expected-head: ${{ inputs.expected-head }}
expected-base: ${{ inputs.expected-base }}
selections: ${{ inputs.selections }}
18 changes: 18 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,24 @@ name: Validate shared GitHub Actions
permissions:
contents: read
jobs:
repair-app:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
with:
node-version: '24'
check-latest: true
- run: npm ci --ignore-scripts
working-directory: repair-app
- run: npm test
working-directory: repair-app
- run: npm run test:runtime
working-directory: repair-app
- run: npm run check
working-directory: repair-app
test:
runs-on: ubuntu-latest
steps:
Expand Down
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,17 @@ checks. With the released two-job caller, all audit kinds maintain
reruns update that PR rather than opening one PR per dependency. Reports show
proposed versions, validation, and blocked findings.

For separate security and routine-upgrade PRs, see the opt-in
[coordinated caller](docs/node-coordination.md). It validates dependency-file
The [App-based setup](docs/node-maintenance.md#set-it-up) includes App installation
and coordinated workflows for separate security and routine-upgrade PRs. The
[coordinated caller](docs/node-coordination.md) validates dependency-file
pushes and refreshes existing proposals after either PR merges. This new caller
requires a workflow revision containing that interface; existing callers keep
their combined-PR behavior.

The shared maintenance App supplies GitHub controls. Maintainer-selected
follow-up repairs are one use of that integration; tests and changes run in the
repository's Actions. See [reviewing findings and requesting repairs](docs/node-maintenance.md#review-findings-and-request-follow-up-repairs).

| Maintenance setup | Automatic security checks | Automatic routine upgrades |
| --- | --- | --- |
| Basic setup in the guide | Weekly | Disabled; run upgrades manually |
Expand Down Expand Up @@ -64,6 +69,7 @@ repositories at a time in one runner, with one combined report and issue.
- [Fleet setup checks: inventory, caller, results, and limits](docs/node-fleet-setup.md)
- [Maintenance schedules and manual audits](docs/audit-schedules.md)
- [Separate proposal channels and automatic refresh](docs/node-coordination.md)
- [Maintenance App operators: host the App and Cloudflare service](docs/node-maintenance-operator.md)
- [Shared defaults for multiple installations](docs/shared-defaults.md)
- [Maintenance behavior, reports, and permissions](node/maintenance.md)
- [Migrate an earlier Docusaurus installation](docs/node-migration.md)
Expand Down
2 changes: 1 addition & 1 deletion docs/audit-schedules.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ routine upgrades at 02:00 UTC on the first day of January, April, July, and
October. Both kinds obey the release cooldown and project constraints. Routine
upgrades also obey `maintenance.upgradeScope`.

In the [setup guide's caller](node-maintenance.md#3-add-the-caller-workflow), use:
In the [setup guide's caller](node-maintenance.md#4-add-the-workflow-files), use:

```yaml
'on':
Expand Down
2 changes: 1 addition & 1 deletion docs/node-coordination.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Do not point this caller at an older revision that lacks the workflow.

For manual-only routine upgrades, omit the quarterly entry. For no scheduled
work, remove `schedule`. Keep the manual input and push trigger in all cases.
4. Keep the existing PR-creation permission or optional App configuration.
4. Keep the existing PR-creation permission or optional publisher App configuration.
Replace the previous caller rather than installing a second scheduled file.
Keep the `dependency-audit` concurrency group with `cancel-in-progress: false`
and `queue: max` so a new request does not cancel a pending push check.
Expand Down
102 changes: 102 additions & 0 deletions docs/node-maintenance-operator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Operate the shared maintenance App

This guide is for the maintainer who hosts the shared GitHub App and Cloudflare
service. Repository users follow [the maintenance setup guide](node-maintenance.md); they
do not need to deploy this service or manage its credentials.

## Deploy the service

One public GitHub App and one Cloudflare Worker serve every installation. Only
the operator needs a Cloudflare account and App credentials. The Worker performs
authentication, consent checks, check updates, and workflow dispatch. It does
not install dependencies or run repository scripts.

1. Register a public GitHub App. Set its webhook URL to your Worker's
`https://<worker-host>/webhook`. Generate a strong webhook secret and App
private key. Request repository permissions: **Contents: read**, **Pull
requests: read**, **Checks: write**, and **Actions: write**. Metadata read
is implicit. Subscribe to **Pull request**, **Check run**, and **Workflow run**
events. No user authorization callback or external selection page is needed.
2. Run `npm ci --ignore-scripts` in `repair-app`. Use a supported Node 24 runtime.
Authenticate Wrangler with your Cloudflare account.
3. Create one D1 database with `npx wrangler d1 create node-dependency-repair`.
Put its returned ID in [wrangler.jsonc](../repair-app/wrangler.jsonc), along
with the GitHub App's numeric ID and slug. Do not put secrets in that file.
4. Apply the schema with `npx wrangler d1 execute node-dependency-repair
--remote --file schema.sql` from `repair-app`.
5. Convert GitHub's downloaded RSA key to unencrypted PKCS#8 for Web Crypto:
`openssl pkcs8 -topk8 -nocrypt -in github-app.pem -out worker-app.pem`.
Store it with `npx wrangler secret put APP_PRIVATE_KEY < worker-app.pem`.
Store the matching webhook secret with `npx wrangler secret put WEBHOOK_SECRET`.
Delete temporary key files according to your credential handling policy.
6. Run `npm test`, `npm run test:runtime`, and `npm run check`. The latter
bundles without deploying.
Deploy deliberately with `npx wrangler deploy`, then test GitHub's webhook
ping and one selected repository before inviting other installations.

## Prepare the user installation instructions

Keep the App's installation URL in your installation instructions. It depends
on the registered App's slug; this source repository does not yet name or host
a deployed shared service. Use a custom domain for a maintained production
service if appropriate. `workers.dev` is sufficient to try the integration.

Before publishing the user guide as generally available, supply the App
installation URL and a released workflow reference that includes the repair
workflow and report renderer. For a pilot, supply the reviewed reference
explicitly. Update the maintenance setup guide's availability notice when these are ready.

## Permissions and request handling

The Worker verifies each webhook's HMAC signature, fetches the current PR from
GitHub, checks write access for the person clicking, and binds consent to the
PR head, base, and exact selected versions. The runner independently checks the
same state. A read-only preparation job uploads the candidate before dependency
or project validation scripts run. Validation uses a separate runner without
write credentials. The publisher downloads the original candidate by the
preparation job's artifact ID. That sealed artifact also contains the audit
report. The publisher does not download validation-runner output; it links to
the Actions logs and reports the successful validation job. It checks the
candidate paths and scope without executing project scripts.

Candidate resolution disables lifecycle scripts and pnpmfile hooks. Repair
commits carry a trailer that normal audit publication checks before replacing
the branch. This preserves a pushed repair even if refreshing its PR report fails.

GitHub grants **Actions: write** per selected repository, not per workflow.
The service restricts itself to `dependency-repair.yml`, but that restriction is
in this code. The App has no contents-write permission and cannot push repair
commits itself; the repository's publisher job owns that permission.

## Stored data and operations

D1 stores request IDs, repository names, PR numbers, revisions, check IDs, and
run status for 90 days. It prevents duplicate webhook delivery from starting
the same request twice. A daily cleanup removes old records. Treat these records
as private installation data. Do not enable request-body or token logging.

Monitor delivery failures, failed Actions runs, Worker errors, D1 storage and
request limits, and App credential expiry or rotation. A failure after recording
a request but before confirmed dispatch needs inspection; the service favors
avoiding duplicate execution over automatically retrying an ambiguous POST.

Use the App's webhook delivery logs to investigate failures. Redelivering a
recorded request does not dispatch it again. Inspect the repository's Actions
runs before arranging a new attempt when dispatch could not be confirmed.

## Cost and capacity

Workers Free and Paid both support D1. Account quotas are shared by installations;
repair jobs consume the repository's normal Actions allocation. Workers Paid
starts at US$5 per account per month; D1 and other usage can add charges beyond
included allowances. Measure actual Worker CPU and database use during the pilot
before choosing the plan. Creating additional Workers does not multiply a free
account's quota.

References: [GitHub App installation](https://docs.github.com/en/apps/using-github-apps/installing-a-github-app-from-a-third-party),
[Checks API](https://docs.github.com/en/rest/checks/runs),
[workflow dispatch](https://docs.github.com/en/rest/actions/workflows#create-a-workflow-dispatch-event),
[webhook validation](https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries),
[Cloudflare pricing](https://developers.cloudflare.com/workers/platform/pricing/),
[D1 pricing](https://developers.cloudflare.com/d1/platform/pricing/),
and [Worker secrets](https://developers.cloudflare.com/workers/configuration/secrets/).
Loading
Loading