mirror of
https://github.com/lukaszraczylo/shared-actions.git
synced 2026-11-03 07:44:04 +00:00
Compare commits
39
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8a86485a9 | ||
|
|
42c623fc6c | ||
|
|
3d5fe9d4f8 | ||
|
|
e70a775c27 | ||
|
|
87651c95dc | ||
|
|
38e5cb0968 | ||
|
|
31cb4a7f8b | ||
|
|
33fdaf6689 | ||
|
|
a0032ad2f8 | ||
|
|
b7bb0468b2 | ||
|
|
459d448941 | ||
|
|
c42cf115b8 | ||
|
|
ffbec959bc | ||
|
|
5cb1998345 | ||
|
|
852afeb5aa | ||
|
|
b23191de71 | ||
|
|
8f20f32137 | ||
|
|
26ad486b78 | ||
|
|
73983972db | ||
|
|
caaad0efab | ||
|
|
cce3d04588 | ||
|
|
b7c4914104 | ||
|
|
86ba34bab5 | ||
|
|
1aece66c7f | ||
|
|
2e49670452 | ||
|
|
85900e7304 | ||
|
|
319b564d14 | ||
|
|
f854725a97 | ||
|
|
b06a5bb1b1 | ||
|
|
bdbd313c0d | ||
|
|
5e0790e131 | ||
|
|
8126310820 | ||
|
|
114812c70e | ||
|
|
c12df22051 | ||
|
|
46d523860a | ||
|
|
6f8fec0956 | ||
|
|
94d1fe3409 | ||
|
|
dc0437151c | ||
|
|
aca729f8f7 |
@@ -19,7 +19,7 @@ runs:
|
|||||||
using: "composite"
|
using: "composite"
|
||||||
steps:
|
steps:
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v7
|
||||||
with:
|
with:
|
||||||
go-version: ${{ inputs.go-version }}
|
go-version: ${{ inputs.go-version }}
|
||||||
|
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ runs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ inputs.github-token }}
|
GITHUB_TOKEN: ${{ inputs.github-token }}
|
||||||
GORELEASER_KEY: ${{ inputs.goreleaser-key }}
|
GORELEASER_KEY: ${{ inputs.goreleaser-key }}
|
||||||
|
HOMEBREW_TAP_TOKEN: ${{ inputs.homebrew-tap-token }}
|
||||||
CGO_ENABLED: ${{ inputs.cgo-enabled }}
|
CGO_ENABLED: ${{ inputs.cgo-enabled }}
|
||||||
|
|
||||||
- name: Run GoReleaser Pro (merge)
|
- name: Run GoReleaser Pro (merge)
|
||||||
@@ -91,3 +92,4 @@ runs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ inputs.github-token }}
|
GITHUB_TOKEN: ${{ inputs.github-token }}
|
||||||
GORELEASER_KEY: ${{ inputs.goreleaser-key }}
|
GORELEASER_KEY: ${{ inputs.goreleaser-key }}
|
||||||
|
HOMEBREW_TAP_TOKEN: ${{ inputs.homebrew-tap-token }}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ runs:
|
|||||||
using: "composite"
|
using: "composite"
|
||||||
steps:
|
steps:
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: ${{ inputs.node-version }}
|
node-version: ${{ inputs.node-version }}
|
||||||
cache: "npm"
|
cache: "npm"
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ jobs:
|
|||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
lfs: ${{ inputs.lfs }}
|
lfs: ${{ inputs.lfs }}
|
||||||
@@ -51,7 +51,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v7
|
||||||
with:
|
with:
|
||||||
go-version: ${{ inputs.go-version }}
|
go-version: ${{ inputs.go-version }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ jobs:
|
|||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
lfs: ${{ inputs.lfs }}
|
lfs: ${{ inputs.lfs }}
|
||||||
@@ -92,7 +92,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v7
|
||||||
with:
|
with:
|
||||||
go-version: ${{ inputs.go-version }}
|
go-version: ${{ inputs.go-version }}
|
||||||
cache: true
|
cache: true
|
||||||
@@ -122,7 +122,7 @@ jobs:
|
|||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
lfs: ${{ inputs.lfs }}
|
lfs: ${{ inputs.lfs }}
|
||||||
@@ -136,7 +136,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v7
|
||||||
with:
|
with:
|
||||||
go-version: ${{ inputs.go-version }}
|
go-version: ${{ inputs.go-version }}
|
||||||
cache: true
|
cache: true
|
||||||
@@ -185,7 +185,7 @@ jobs:
|
|||||||
security-events: write
|
security-events: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
lfs: ${{ inputs.lfs }}
|
lfs: ${{ inputs.lfs }}
|
||||||
@@ -199,7 +199,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v7
|
||||||
with:
|
with:
|
||||||
go-version: ${{ inputs.go-version }}
|
go-version: ${{ inputs.go-version }}
|
||||||
cache: true
|
cache: true
|
||||||
@@ -226,7 +226,7 @@ jobs:
|
|||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
lfs: ${{ inputs.lfs }}
|
lfs: ${{ inputs.lfs }}
|
||||||
@@ -240,7 +240,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v7
|
||||||
with:
|
with:
|
||||||
go-version: ${{ inputs.go-version }}
|
go-version: ${{ inputs.go-version }}
|
||||||
cache: true
|
cache: true
|
||||||
@@ -264,7 +264,7 @@ jobs:
|
|||||||
security-events: write
|
security-events: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
lfs: ${{ inputs.lfs }}
|
lfs: ${{ inputs.lfs }}
|
||||||
|
|
||||||
@@ -277,7 +277,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v7
|
||||||
with:
|
with:
|
||||||
go-version: ${{ inputs.go-version }}
|
go-version: ${{ inputs.go-version }}
|
||||||
cache: true
|
cache: true
|
||||||
@@ -303,7 +303,7 @@ jobs:
|
|||||||
coverage: ${{ steps.coverage.outputs.coverage }}
|
coverage: ${{ steps.coverage.outputs.coverage }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
lfs: ${{ inputs.lfs }}
|
lfs: ${{ inputs.lfs }}
|
||||||
@@ -317,7 +317,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v7
|
||||||
with:
|
with:
|
||||||
go-version: ${{ inputs.go-version }}
|
go-version: ${{ inputs.go-version }}
|
||||||
cache: true
|
cache: true
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
if: inputs.preflight
|
if: inputs.preflight
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
lfs: false
|
lfs: false
|
||||||
|
|
||||||
@@ -119,6 +119,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
GORELEASER_KEY: ${{ secrets.GORELEASER_PRO }}
|
GORELEASER_KEY: ${{ secrets.GORELEASER_PRO }}
|
||||||
|
HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
|
||||||
|
|
||||||
test:
|
test:
|
||||||
name: Test
|
name: Test
|
||||||
@@ -127,7 +128,7 @@ jobs:
|
|||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
lfs: ${{ inputs.lfs }}
|
lfs: ${{ inputs.lfs }}
|
||||||
@@ -154,7 +155,7 @@ jobs:
|
|||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
lfs: ${{ inputs.lfs }}
|
lfs: ${{ inputs.lfs }}
|
||||||
|
|
||||||
@@ -167,7 +168,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: ${{ inputs.node-version }}
|
node-version: ${{ inputs.node-version }}
|
||||||
cache: "npm"
|
cache: "npm"
|
||||||
@@ -194,7 +195,7 @@ jobs:
|
|||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
lfs: ${{ inputs.lfs }}
|
lfs: ${{ inputs.lfs }}
|
||||||
@@ -229,7 +230,7 @@ jobs:
|
|||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
lfs: ${{ inputs.lfs }}
|
lfs: ${{ inputs.lfs }}
|
||||||
@@ -246,7 +247,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v7
|
||||||
with:
|
with:
|
||||||
go-version: ${{ inputs.go-version }}
|
go-version: ${{ inputs.go-version }}
|
||||||
cache: true
|
cache: true
|
||||||
@@ -285,6 +286,7 @@ jobs:
|
|||||||
cgo-enabled: "1"
|
cgo-enabled: "1"
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
goreleaser-key: ${{ secrets.GORELEASER_PRO }}
|
goreleaser-key: ${{ secrets.GORELEASER_PRO }}
|
||||||
|
homebrew-tap-token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
|
||||||
|
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v7
|
||||||
@@ -308,7 +310,7 @@ jobs:
|
|||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
lfs: ${{ inputs.lfs }}
|
lfs: ${{ inputs.lfs }}
|
||||||
@@ -322,7 +324,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v7
|
||||||
with:
|
with:
|
||||||
go-version: ${{ inputs.go-version }}
|
go-version: ${{ inputs.go-version }}
|
||||||
|
|
||||||
@@ -364,6 +366,7 @@ jobs:
|
|||||||
mode: merge
|
mode: merge
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
goreleaser-key: ${{ secrets.GORELEASER_PRO }}
|
goreleaser-key: ${{ secrets.GORELEASER_PRO }}
|
||||||
|
homebrew-tap-token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
|
||||||
|
|
||||||
- name: Rolling release
|
- name: Rolling release
|
||||||
if: inputs.rolling-release-tag != ''
|
if: inputs.rolling-release-tag != ''
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
if: inputs.preflight
|
if: inputs.preflight
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
lfs: false
|
lfs: false
|
||||||
|
|
||||||
@@ -104,7 +104,7 @@ jobs:
|
|||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
lfs: ${{ inputs.lfs }}
|
lfs: ${{ inputs.lfs }}
|
||||||
|
|
||||||
@@ -131,7 +131,7 @@ jobs:
|
|||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
lfs: ${{ inputs.lfs }}
|
lfs: ${{ inputs.lfs }}
|
||||||
@@ -162,7 +162,7 @@ jobs:
|
|||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
lfs: ${{ inputs.lfs }}
|
lfs: ${{ inputs.lfs }}
|
||||||
@@ -184,7 +184,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v7
|
||||||
with:
|
with:
|
||||||
go-version: ${{ inputs.go-version }}
|
go-version: ${{ inputs.go-version }}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,259 @@
|
|||||||
|
name: Node Pull Request
|
||||||
|
|
||||||
|
# Checks for a Node, Vue or Astro project: install without touching the lockfile,
|
||||||
|
# then run lint, typecheck, test and build. A script that does not exist is skipped.
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
node-version:
|
||||||
|
description: "Node.js version to use"
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: "22"
|
||||||
|
working-directory:
|
||||||
|
description: "Directory that holds package.json"
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: "."
|
||||||
|
workspace:
|
||||||
|
description: "The project is a pnpm workspace. working-directory then holds pnpm-workspace.yaml and pnpm-lock.yaml, and needs no package.json. Dependencies install once there, and scripts run in each of packages."
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
packages:
|
||||||
|
description: "With workspace, comma-separated workspace package directories, relative to working-directory."
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: ""
|
||||||
|
scripts:
|
||||||
|
description: "Comma-separated package.json scripts to run, in order. A script that does not exist is skipped."
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: "lint,typecheck,test,build"
|
||||||
|
pnpm-version:
|
||||||
|
description: "pnpm version to use, for a repo whose package.json has no packageManager field. Empty uses the packageManager field, or the newest pnpm."
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: ""
|
||||||
|
require-tests:
|
||||||
|
description: "Fail when package.json has no test script"
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
lfs:
|
||||||
|
description: "Enable Git LFS checkout (for repos with large files)"
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
npm-registry-scope:
|
||||||
|
description: "Scope served by a private npm registry, for example '@fortawesome'. Used only when npm-registry-url and the npm-registry-token secret are also set."
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: ""
|
||||||
|
npm-registry-url:
|
||||||
|
description: "URL of the private npm registry, for example 'https://npm.fontawesome.com/'. Used only when npm-registry-scope and the npm-registry-token secret are also set."
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: ""
|
||||||
|
runner:
|
||||||
|
description: "Runner label, or JSON object such as {\"group\":\"ci\",\"labels\":[\"linux\"]} to select a runner group. Falls back to vars.SHARED_ACTIONS_RUNNER, then ubuntu-latest."
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: ""
|
||||||
|
secrets:
|
||||||
|
npm-registry-token:
|
||||||
|
description: "Auth token for the private npm registry"
|
||||||
|
required: false
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: node-pr-${{ github.repository }}-${{ github.event_name }}-${{ github.head_ref || github.ref_name }}-${{ inputs.working-directory }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pre:
|
||||||
|
name: Detect duplicate run
|
||||||
|
runs-on: ${{ fromJSON(startsWith(inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest', '{') && (inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest') || format('"{0}"', inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest')) }}
|
||||||
|
timeout-minutes: 5
|
||||||
|
permissions:
|
||||||
|
pull-requests: read
|
||||||
|
outputs:
|
||||||
|
skip: ${{ steps.dup.outputs.skip }}
|
||||||
|
steps:
|
||||||
|
# A push to a branch with an open PR also fires pull_request; run only the PR one.
|
||||||
|
- name: Skip push run when a PR is open
|
||||||
|
id: dup
|
||||||
|
uses: actions/github-script@v9
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
let skip = false;
|
||||||
|
if (context.eventName === 'push') {
|
||||||
|
try {
|
||||||
|
const { data } = await github.rest.pulls.list({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
head: `${context.repo.owner}:${context.ref.replace('refs/heads/', '')}`,
|
||||||
|
state: 'open',
|
||||||
|
});
|
||||||
|
skip = data.length > 0;
|
||||||
|
} catch (e) {
|
||||||
|
core.warning(`PR lookup failed, running checks: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
core.setOutput('skip', String(skip));
|
||||||
|
if (skip) core.notice('Open PR found for this branch; checks run on the pull_request event.');
|
||||||
|
|
||||||
|
checks:
|
||||||
|
name: Checks (${{ inputs.working-directory }})
|
||||||
|
needs: pre
|
||||||
|
if: needs.pre.outputs.skip != 'true'
|
||||||
|
runs-on: ${{ fromJSON(startsWith(inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest', '{') && (inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest') || format('"{0}"', inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest')) }}
|
||||||
|
timeout-minutes: 30
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v7
|
||||||
|
with:
|
||||||
|
lfs: ${{ inputs.lfs }}
|
||||||
|
|
||||||
|
- name: Run workflow prepare script
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
if [ -f "./workflow-prepare.sh" ]; then
|
||||||
|
chmod +x ./workflow-prepare.sh
|
||||||
|
./workflow-prepare.sh
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Detect the package manager
|
||||||
|
id: detect
|
||||||
|
working-directory: ${{ inputs.working-directory }}
|
||||||
|
env:
|
||||||
|
WORKSPACE: ${{ inputs.workspace }}
|
||||||
|
PACKAGES: ${{ inputs.packages }}
|
||||||
|
run: |
|
||||||
|
if [ "$WORKSPACE" = true ]; then
|
||||||
|
if [ ! -f pnpm-workspace.yaml ] || [ ! -f pnpm-lock.yaml ]; then
|
||||||
|
echo "::error::workspace needs pnpm-workspace.yaml and pnpm-lock.yaml in ${{ inputs.working-directory }}."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
count=0
|
||||||
|
IFS=',' read -r -a packages <<<"$PACKAGES"
|
||||||
|
for package in "${packages[@]}"; do
|
||||||
|
package=$(echo "$package" | xargs)
|
||||||
|
[ -n "$package" ] || continue
|
||||||
|
if [ ! -f "$package/package.json" ]; then
|
||||||
|
echo "::error::packages lists $package, but ${{ inputs.working-directory }}/$package/package.json does not exist."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
count=$((count + 1))
|
||||||
|
done
|
||||||
|
if [ "$count" -eq 0 ]; then
|
||||||
|
echo "::error::workspace needs packages: the packages to check."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "pm=pnpm" >> "$GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if [ ! -f package.json ]; then
|
||||||
|
echo "::error::No package.json in ${{ inputs.working-directory }}."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
pm=npm
|
||||||
|
if [ -f pnpm-lock.yaml ]; then pm=pnpm
|
||||||
|
elif [ -f yarn.lock ]; then pm=yarn
|
||||||
|
fi
|
||||||
|
echo "pm=$pm" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v7
|
||||||
|
with:
|
||||||
|
node-version: ${{ inputs.node-version }}
|
||||||
|
|
||||||
|
# The npmrc lives in RUNNER_TEMP, not the checkout, so no git command can pick the token up.
|
||||||
|
- name: Configure the private npm registry
|
||||||
|
env:
|
||||||
|
NPM_SCOPE: ${{ inputs.npm-registry-scope }}
|
||||||
|
NPM_URL: ${{ inputs.npm-registry-url }}
|
||||||
|
NPM_TOKEN: ${{ secrets.npm-registry-token }}
|
||||||
|
run: |
|
||||||
|
[ -n "$NPM_SCOPE" ] && [ -n "$NPM_URL" ] && [ -n "$NPM_TOKEN" ] || exit 0
|
||||||
|
echo "::add-mask::$NPM_TOKEN"
|
||||||
|
url="${NPM_URL%/}/"
|
||||||
|
npmrc="$RUNNER_TEMP/private-registry.npmrc"
|
||||||
|
(umask 077 && printf '%s:registry=%s\n//%s:_authToken=%s\n' "$NPM_SCOPE" "$url" "${url#*://}" "$NPM_TOKEN" > "$npmrc")
|
||||||
|
echo "NPM_CONFIG_USERCONFIG=$npmrc" >> "$GITHUB_ENV"
|
||||||
|
echo "Using a private npm registry for $NPM_SCOPE."
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
working-directory: ${{ inputs.working-directory }}
|
||||||
|
env:
|
||||||
|
PM: ${{ steps.detect.outputs.pm }}
|
||||||
|
PNPM_VERSION: ${{ inputs.pnpm-version }}
|
||||||
|
run: |
|
||||||
|
# Install without updating the lockfile, so a lockfile that does not match package.json fails.
|
||||||
|
case "$PM" in
|
||||||
|
pnpm | yarn)
|
||||||
|
command -v corepack >/dev/null || npm install --global corepack
|
||||||
|
corepack enable "$PM"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
if [ "$PM" = pnpm ] && [ -n "$PNPM_VERSION" ]; then
|
||||||
|
corepack prepare "pnpm@$PNPM_VERSION" --activate
|
||||||
|
fi
|
||||||
|
case "$PM" in
|
||||||
|
pnpm) pnpm install --frozen-lockfile ;;
|
||||||
|
yarn) yarn install --immutable ;;
|
||||||
|
npm)
|
||||||
|
if [ -f package-lock.json ]; then npm ci; else npm install; fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
- name: Run scripts
|
||||||
|
working-directory: ${{ inputs.working-directory }}
|
||||||
|
env:
|
||||||
|
PM: ${{ steps.detect.outputs.pm }}
|
||||||
|
SCRIPTS: ${{ inputs.scripts }}
|
||||||
|
REQUIRE_TESTS: ${{ inputs.require-tests }}
|
||||||
|
WORKSPACE: ${{ inputs.workspace }}
|
||||||
|
PACKAGES: ${{ inputs.packages }}
|
||||||
|
run: |
|
||||||
|
# A single project is the one package ".". A workspace is each listed package.
|
||||||
|
packages=(.)
|
||||||
|
if [ "$WORKSPACE" = true ]; then
|
||||||
|
IFS=',' read -r -a packages <<<"$PACKAGES"
|
||||||
|
fi
|
||||||
|
if [ "$REQUIRE_TESTS" = true ]; then
|
||||||
|
for package in "${packages[@]}"; do
|
||||||
|
package=$(echo "$package" | xargs)
|
||||||
|
[ -n "$package" ] || continue
|
||||||
|
if ! jq -e '.scripts.test' "$package/package.json" >/dev/null; then
|
||||||
|
echo "::error::$package/package.json has no test script."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
ran=()
|
||||||
|
IFS=',' read -r -a scripts <<<"$SCRIPTS"
|
||||||
|
for script in "${scripts[@]}"; do
|
||||||
|
script=$(echo "$script" | xargs)
|
||||||
|
[ -n "$script" ] || continue
|
||||||
|
for package in "${packages[@]}"; do
|
||||||
|
package=$(echo "$package" | xargs)
|
||||||
|
[ -n "$package" ] || continue
|
||||||
|
where=""
|
||||||
|
[ "$package" = . ] || where=" ($package)"
|
||||||
|
if jq -e --arg s "$script" '.scripts[$s]' "$package/package.json" >/dev/null; then
|
||||||
|
echo "::group::$PM run $script$where"
|
||||||
|
(cd "$package" && "$PM" run "$script")
|
||||||
|
echo "::endgroup::"
|
||||||
|
ran+=("$script$where")
|
||||||
|
else
|
||||||
|
echo "::notice::No '$script' script$where, skipping."
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
if [ "${#ran[@]}" -eq 0 ]; then
|
||||||
|
echo "::warning::None of the scripts ($SCRIPTS) exist in package.json, so nothing was checked."
|
||||||
|
fi
|
||||||
|
echo "Ran: ${ran[*]:-none} (${PM})" >> "$GITHUB_STEP_SUMMARY"
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
name: Renovate Autoupdate
|
name: Renovate Autoupdate
|
||||||
|
|
||||||
# Renovate batches every Go and npm/pnpm/yarn update into one PR, the tests run
|
# Renovate prepares branches with every Go and npm/pnpm/yarn update and opens no PR.
|
||||||
# on that branch, and the PR merges only when they pass. Replaces go-autoupdate.
|
# Each branch is merged into the default branch and tested. Only when the tests
|
||||||
|
# pass does the workflow open a PR and merge it. Replaces go-autoupdate.
|
||||||
# A renovate.json in the calling repository adds to these defaults.
|
# A renovate.json in the calling repository adds to these defaults.
|
||||||
|
|
||||||
on:
|
on:
|
||||||
@@ -12,8 +13,13 @@ on:
|
|||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
default: ">=1.24"
|
default: ">=1.24"
|
||||||
|
go:
|
||||||
|
description: "Update and test the Go module. Turn off for a repo with no Go code."
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
go-working-directory:
|
go-working-directory:
|
||||||
description: "Directory that holds go.mod"
|
description: "Directory that holds go.mod. Only this module is updated and tested."
|
||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
default: "."
|
default: "."
|
||||||
@@ -22,8 +28,33 @@ on:
|
|||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
default: "22"
|
default: "22"
|
||||||
|
go-mod-tidy:
|
||||||
|
description: "Run go mod tidy after a Go update. Turn it off for an Encore app: Encore adds imports at build time, so tidy prunes go.sum entries the build needs."
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
|
go-prepare-command:
|
||||||
|
description: "Shell commands to run before the Go tests, for example installing a CLI. To put a tool on PATH, append its directory to $GITHUB_PATH."
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: ""
|
||||||
|
go-test-command:
|
||||||
|
description: "Replaces the default Go build and test commands, for example 'encore test ./...' for an Encore app. It runs in go-working-directory."
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: ""
|
||||||
|
build-tags:
|
||||||
|
description: "Build tags to pass to go build and go test (e.g., 'fts5'). The default commands then run with CGO_ENABLED=1. Ignored when go-test-command is set."
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: ""
|
||||||
|
node:
|
||||||
|
description: "Update and test the Node project. Turn off for a repo with no frontend to test."
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
node-working-directory:
|
node-working-directory:
|
||||||
description: "Directory that holds package.json"
|
description: "Directory that holds package.json. Only this project is updated and tested."
|
||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
default: "."
|
default: "."
|
||||||
@@ -32,16 +63,31 @@ on:
|
|||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
default: "lint,typecheck,test,build"
|
default: "lint,typecheck,test,build"
|
||||||
|
node-workspace:
|
||||||
|
description: "The Node project is a pnpm workspace. node-working-directory then holds pnpm-workspace.yaml and pnpm-lock.yaml, and needs no package.json. Dependencies install once there, and node-scripts run in each of node-packages."
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
node-packages:
|
||||||
|
description: "With node-workspace, comma-separated workspace package directories, relative to node-working-directory. Only these are updated and tested."
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: ""
|
||||||
|
pnpm-version:
|
||||||
|
description: "pnpm version to use, for a repo whose package.json has no packageManager field. Empty uses the packageManager field, or the newest pnpm."
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: ""
|
||||||
require-node-tests:
|
require-node-tests:
|
||||||
description: "Fail when a Node project has no test script, so an untested update never merges"
|
description: "Fail when a Node project has no test script, so an untested update never merges"
|
||||||
required: false
|
required: false
|
||||||
type: boolean
|
type: boolean
|
||||||
default: true
|
default: true
|
||||||
include-major:
|
major-updates:
|
||||||
description: "Put major updates in the batch. When false, each major update gets its own PR that is never merged automatically."
|
description: "What to do with major updates: 'separate' gives each its own branch, tested and merged on its own, 'batch' puts them in the one batch, 'ignore' skips them."
|
||||||
required: false
|
required: false
|
||||||
type: boolean
|
type: string
|
||||||
default: false
|
default: "separate"
|
||||||
minimum-release-age:
|
minimum-release-age:
|
||||||
description: "Skip releases younger than this, for example '2 days'"
|
description: "Skip releases younger than this, for example '2 days'"
|
||||||
required: false
|
required: false
|
||||||
@@ -52,31 +98,45 @@ on:
|
|||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
default: "44"
|
default: "44"
|
||||||
commit-subject:
|
release-workflow:
|
||||||
description: "Squash commit subject. Keep it free of release keywords such as 'major' or 'breaking', which semver-generator matches."
|
description: "Workflow file to dispatch after a merge, for example release.yaml. Give several as a comma-separated list. Each must have a workflow_dispatch trigger and the caller needs actions: write. Empty means no dispatch."
|
||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
default: "chore(deps): update dependencies"
|
default: ""
|
||||||
admin-merge:
|
admin-merge:
|
||||||
description: "Merge with --admin, which bypasses branch protection. The workflow token cannot do this, so leave it off unless you pass a token that can."
|
description: "Merge with --admin, which bypasses branch protection. The workflow token cannot do this, so leave it off unless you pass a token that can."
|
||||||
required: false
|
required: false
|
||||||
type: boolean
|
type: boolean
|
||||||
default: false
|
default: false
|
||||||
release-workflow:
|
log-level:
|
||||||
description: "Workflow file to dispatch after the merge, for example release.yaml. It must have a workflow_dispatch trigger and the caller needs actions: write. Empty means no dispatch."
|
description: "Renovate log level: info, debug or trace. Use debug to see why Renovate fails."
|
||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
default: ""
|
default: "info"
|
||||||
lfs:
|
lfs:
|
||||||
description: "Enable Git LFS checkout (for repos with large files)"
|
description: "Enable Git LFS checkout (for repos with large files)"
|
||||||
required: false
|
required: false
|
||||||
type: boolean
|
type: boolean
|
||||||
default: false
|
default: false
|
||||||
|
npm-registry-scope:
|
||||||
|
description: "Scope served by a private npm registry, for example '@fortawesome'. Used only when npm-registry-url and the npm-registry-token secret are also set."
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: ""
|
||||||
|
npm-registry-url:
|
||||||
|
description: "URL of the private npm registry, for example 'https://npm.fontawesome.com/'. Used only when npm-registry-scope and the npm-registry-token secret are also set."
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: ""
|
||||||
runner:
|
runner:
|
||||||
description: "Runner label, or JSON object such as {\"group\":\"ci\",\"labels\":[\"linux\"]} to select a runner group. Falls back to vars.SHARED_ACTIONS_RUNNER, then ubuntu-latest."
|
description: "Runner label, or JSON object such as {\"group\":\"ci\",\"labels\":[\"linux\"]} to select a runner group. Falls back to vars.SHARED_ACTIONS_RUNNER, then ubuntu-latest."
|
||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
default: ""
|
default: ""
|
||||||
|
secrets:
|
||||||
|
npm-registry-token:
|
||||||
|
description: "Auth token for the private npm registry"
|
||||||
|
required: false
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: renovate-autoupdate-${{ github.repository }}
|
group: renovate-autoupdate-${{ github.repository }}
|
||||||
@@ -84,110 +144,227 @@ concurrency:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
BATCH_BRANCH: deps-autoupdate/batch
|
BATCH_BRANCH: deps-autoupdate/batch
|
||||||
|
ISSUE_TITLE: Dependency updates failing tests
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
renovate:
|
renovate:
|
||||||
name: Update dependencies
|
name: Prepare the updates
|
||||||
runs-on: ${{ fromJSON(startsWith(inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest', '{') && (inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest') || format('"{0}"', inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest')) }}
|
runs-on: ${{ fromJSON(startsWith(inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest', '{') && (inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest') || format('"{0}"', inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest')) }}
|
||||||
timeout-minutes: 20
|
timeout-minutes: 45
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
# Renovate reads the repository's issues when it starts. A private repo refuses that without this.
|
||||||
|
issues: read
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
outputs:
|
outputs:
|
||||||
pr-number: ${{ steps.find-pr.outputs.pr-number }}
|
branches: ${{ steps.list.outputs.branches }}
|
||||||
|
base-sha: ${{ steps.base.outputs.sha }}
|
||||||
steps:
|
steps:
|
||||||
|
- name: Record the default branch commit
|
||||||
|
id: base
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
branch=$(gh api "repos/$GITHUB_REPOSITORY" --jq .default_branch)
|
||||||
|
echo "sha=$(gh api "repos/$GITHUB_REPOSITORY/commits/$branch" --jq .sha)" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Write Renovate config
|
- name: Write Renovate config
|
||||||
env:
|
env:
|
||||||
MIN_AGE: ${{ inputs.minimum-release-age }}
|
MIN_AGE: ${{ inputs.minimum-release-age }}
|
||||||
INCLUDE_MAJOR: ${{ inputs.include-major }}
|
MAJOR_UPDATES: ${{ inputs.major-updates }}
|
||||||
|
USE_GO: ${{ inputs.go }}
|
||||||
|
USE_NODE: ${{ inputs.node }}
|
||||||
|
GO_DIR: ${{ inputs.go-working-directory }}
|
||||||
|
NODE_DIR: ${{ inputs.node-working-directory }}
|
||||||
|
NODE_WORKSPACE: ${{ inputs.node-workspace }}
|
||||||
|
NODE_PACKAGES: ${{ inputs.node-packages }}
|
||||||
|
PNPM_VERSION: ${{ inputs.pnpm-version }}
|
||||||
|
GO_MOD_TIDY: ${{ inputs.go-mod-tidy }}
|
||||||
|
NPM_SCOPE: ${{ inputs.npm-registry-scope }}
|
||||||
|
NPM_URL: ${{ inputs.npm-registry-url }}
|
||||||
|
NPM_TOKEN: ${{ secrets.npm-registry-token }}
|
||||||
CONFIG_FILE: ${{ runner.temp }}/renovate-autoupdate-config.json
|
CONFIG_FILE: ${{ runner.temp }}/renovate-autoupdate-config.json
|
||||||
run: |
|
run: |
|
||||||
|
# The token goes to jq through the environment, never argv, and the config print below hides it.
|
||||||
|
export NPMRC=""
|
||||||
|
if [ -n "$NPM_SCOPE" ] && [ -n "$NPM_URL" ] && [ -n "$NPM_TOKEN" ]; then
|
||||||
|
echo "::add-mask::$NPM_TOKEN"
|
||||||
|
url="${NPM_URL%/}/"
|
||||||
|
NPMRC=$(printf '%s:registry=%s\n//%s:_authToken=%s\n' "$NPM_SCOPE" "$url" "${url#*://}" "$NPM_TOKEN")
|
||||||
|
# Renovate writes its npmrc option into the package directory, but a pnpm workspace install reads
|
||||||
|
# only the root, so the lockfile update gets a 404. The Renovate container shares /tmp with the host.
|
||||||
|
RENOVATE_NPMRC_FILE="/tmp/renovate-npmrc-$GITHUB_RUN_ID"
|
||||||
|
(umask 022 && printf '%s\n' "$NPMRC" > "$RENOVATE_NPMRC_FILE")
|
||||||
|
echo "RENOVATE_NPMRC_FILE=$RENOVATE_NPMRC_FILE" >> "$GITHUB_ENV"
|
||||||
|
export RENOVATE_NPMRC_FILE
|
||||||
|
fi
|
||||||
jq -n \
|
jq -n \
|
||||||
--arg repo "$GITHUB_REPOSITORY" \
|
--arg repo "$GITHUB_REPOSITORY" \
|
||||||
--arg age "$MIN_AGE" \
|
--arg age "$MIN_AGE" \
|
||||||
--arg branch_prefix "${BATCH_BRANCH%%/*}/" \
|
--arg branch_prefix "${BATCH_BRANCH%%/*}/" \
|
||||||
--argjson include_major "$INCLUDE_MAJOR" \
|
--arg majors "$MAJOR_UPDATES" \
|
||||||
|
--argjson use_go "$USE_GO" \
|
||||||
|
--argjson use_node "$USE_NODE" \
|
||||||
|
--arg go_dir "$GO_DIR" \
|
||||||
|
--arg node_dir "$NODE_DIR" \
|
||||||
|
--argjson node_workspace "$NODE_WORKSPACE" \
|
||||||
|
--arg node_packages "$NODE_PACKAGES" \
|
||||||
|
--arg pnpm_version "$PNPM_VERSION" \
|
||||||
|
--argjson go_tidy "$GO_MOD_TIDY" \
|
||||||
'
|
'
|
||||||
|
# A regex for files directly inside a directory, so only what the tests cover is updated.
|
||||||
|
def prefix($d): (if $d == "." or $d == "" then "" else ($d | ltrimstr("./") | rtrimstr("/")) + "/" end) | gsub("\\."; "\\.");
|
||||||
|
def pkgprefix($d; $p): prefix($d) + ($p | ltrimstr("./") | rtrimstr("/") | gsub("\\."; "\\.")) + "/";
|
||||||
|
# A workspace updates only its listed packages. The root lockfile is rewritten by pnpm with them.
|
||||||
|
def node_files:
|
||||||
|
if $node_workspace
|
||||||
|
then [$node_packages | split(",")[] | gsub("^\\s+|\\s+$"; "") | select(length > 0) | "/^" + pkgprefix($node_dir; .) + "package\\.json$/"]
|
||||||
|
else ["/^" + prefix($node_dir) + "package\\.json$/"]
|
||||||
|
end;
|
||||||
{
|
{
|
||||||
platform: "github",
|
platform: "github",
|
||||||
repositories: [$repo],
|
repositories: [$repo],
|
||||||
onboarding: false,
|
onboarding: false,
|
||||||
requireConfig: "optional",
|
requireConfig: "optional",
|
||||||
branchPrefix: $branch_prefix,
|
branchPrefix: $branch_prefix,
|
||||||
enabledManagers: ["gomod", "npm"],
|
enabledManagers: ((if $use_go then ["gomod"] else [] end) + (if $use_node then ["npm"] else [] end)),
|
||||||
|
# Renovate writes the lockfile with its own pnpm. Use the version the tests install with.
|
||||||
|
constraints: (if $pnpm_version != "" then {pnpm: $pnpm_version} else {} end),
|
||||||
|
gomod: {managerFilePatterns: ["/^" + prefix($go_dir) + "go\\.mod$/"]},
|
||||||
|
npm: {managerFilePatterns: (node_files + ["/^" + prefix($node_dir) + "pnpm-workspace\\.yaml$/"])},
|
||||||
dependencyDashboard: false,
|
dependencyDashboard: false,
|
||||||
|
# Branches only: with no dashboard, no PR is ever opened. The workflow opens it after the tests.
|
||||||
|
prCreation: "approval",
|
||||||
|
# A PR closed after a merge or by hand must not stop a later update of the same package.
|
||||||
|
recreateWhen: "always",
|
||||||
|
rebaseWhen: "behind-base-branch",
|
||||||
semanticCommits: "enabled",
|
semanticCommits: "enabled",
|
||||||
semanticCommitType: "chore",
|
semanticCommitType: "chore",
|
||||||
semanticCommitScope: "deps",
|
semanticCommitScope: "deps",
|
||||||
minimumReleaseAge: $age,
|
minimumReleaseAge: $age,
|
||||||
prConcurrentLimit: 0,
|
prConcurrentLimit: 0,
|
||||||
prHourlyLimit: 0,
|
prHourlyLimit: 0,
|
||||||
recreateWhen: "always",
|
branchConcurrentLimit: 0,
|
||||||
postUpdateOptions: ["gomodTidy"],
|
# A Go major update changes the import path: rewrite the imports, so the build tests the real update.
|
||||||
|
postUpdateOptions: ((if $go_tidy then ["gomodTidy"] else [] end) + ["gomodUpdateImportPaths"]),
|
||||||
statusCheckNames: {
|
statusCheckNames: {
|
||||||
artifactError: null,
|
artifactError: null,
|
||||||
configValidation: null,
|
configValidation: null,
|
||||||
mergeConfidence: null,
|
mergeConfidence: null,
|
||||||
minimumReleaseAge: null
|
minimumReleaseAge: null
|
||||||
},
|
},
|
||||||
packageRules: [
|
packageRules: (
|
||||||
({
|
[
|
||||||
matchManagers: ["gomod", "npm"],
|
({
|
||||||
groupName: "dependencies",
|
matchManagers: ["gomod", "npm"],
|
||||||
groupSlug: "batch"
|
groupName: "dependencies",
|
||||||
} + (if $include_major then {} else {matchUpdateTypes: ["minor", "patch", "pin", "pinDigest", "digest"]} end)),
|
groupSlug: "batch"
|
||||||
{matchManagers: ["gomod"], matchDepTypes: ["indirect"], enabled: true},
|
} + (if $majors == "batch" then {} else {matchUpdateTypes: ["minor", "patch", "pin", "pinDigest", "digest"]} end)),
|
||||||
{matchPackageNames: ["typescript"], allowedVersions: "<7"}
|
{matchManagers: ["gomod"], matchDepTypes: ["indirect"], enabled: true},
|
||||||
]
|
# The parent module chooses the major version of an indirect dependency.
|
||||||
}' > "$CONFIG_FILE"
|
{matchManagers: ["gomod"], matchDepTypes: ["indirect"], allowedVersions: "<{{add major 1}}.0.0"},
|
||||||
jq . "$CONFIG_FILE"
|
{matchPackageNames: ["typescript"], allowedVersions: "<7"}
|
||||||
|
]
|
||||||
|
# Rules that disable by update type are ignored, but a version cap works.
|
||||||
|
+ (if $majors == "ignore" then [{matchManagers: ["gomod", "npm"], allowedVersions: "<{{add major 1}}.0.0"}] else [] end)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
| if $ENV.NPMRC != "" then .npmrc = $ENV.NPMRC | .customEnvVariables = {NPM_CONFIG_USERCONFIG: $ENV.RENOVATE_NPMRC_FILE} else . end' > "$CONFIG_FILE"
|
||||||
|
jq 'if has("npmrc") then .npmrc = "<redacted>" else . end' "$CONFIG_FILE"
|
||||||
|
|
||||||
- name: Run Renovate
|
- name: Run Renovate
|
||||||
uses: renovatebot/[email protected].6
|
uses: renovatebot/[email protected].11
|
||||||
with:
|
with:
|
||||||
configurationFile: ${{ runner.temp }}/renovate-autoupdate-config.json
|
configurationFile: ${{ runner.temp }}/renovate-autoupdate-config.json
|
||||||
token: ${{ github.token }}
|
token: ${{ github.token }}
|
||||||
renovate-version: ${{ inputs.renovate-version }}
|
renovate-version: ${{ inputs.renovate-version }}
|
||||||
env:
|
env:
|
||||||
LOG_LEVEL: info
|
LOG_LEVEL: ${{ inputs.log-level }}
|
||||||
|
|
||||||
- name: Find the batch PR
|
- name: Remove the registry token file
|
||||||
id: find-pr
|
if: always() && env.RENOVATE_NPMRC_FILE != ''
|
||||||
|
run: rm -f "$RENOVATE_NPMRC_FILE"
|
||||||
|
|
||||||
|
- name: List the update branches
|
||||||
|
id: list
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
run: |
|
run: |
|
||||||
number=$(gh pr list --repo "$GITHUB_REPOSITORY" --head "$BATCH_BRANCH" --state open --json number --jq '.[0].number // empty')
|
prefix="${BATCH_BRANCH%%/*}/"
|
||||||
echo "pr-number=$number" >> "$GITHUB_OUTPUT"
|
all=$(gh api --paginate "repos/$GITHUB_REPOSITORY/branches" --jq '.[].name' | grep -F "$prefix" || true)
|
||||||
if [ -z "$number" ]; then
|
# The batch first, then each major update.
|
||||||
echo "No dependency updates to test."
|
ordered=$({ printf '%s\n' "$all" | grep -Fx "$BATCH_BRANCH" || true; printf '%s\n' "$all" | grep -vFx "$BATCH_BRANCH" || true; } | sed '/^$/d')
|
||||||
else
|
json=$(printf '%s\n' "$ordered" | jq -R . | jq -sc 'map(select(length > 0))')
|
||||||
echo "Batch PR: #$number"
|
echo "branches=$json" >> "$GITHUB_OUTPUT"
|
||||||
fi
|
echo "Update branches: $json"
|
||||||
|
|
||||||
test:
|
test-and-merge:
|
||||||
name: Test the update
|
name: Test and merge ${{ matrix.branch }}
|
||||||
needs: renovate
|
needs: renovate
|
||||||
if: needs.renovate.outputs.pr-number != ''
|
if: needs.renovate.outputs.branches != '[]'
|
||||||
runs-on: ${{ fromJSON(startsWith(inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest', '{') && (inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest') || format('"{0}"', inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest')) }}
|
runs-on: ${{ fromJSON(startsWith(inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest', '{') && (inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest') || format('"{0}"', inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest')) }}
|
||||||
timeout-minutes: 45
|
timeout-minutes: 60
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: write
|
||||||
|
issues: write
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
outputs:
|
strategy:
|
||||||
head-sha: ${{ steps.checkout-sha.outputs.sha }}
|
fail-fast: false
|
||||||
|
# One at a time, so each branch is tested on top of the ones merged before it.
|
||||||
|
max-parallel: 1
|
||||||
|
matrix:
|
||||||
|
branch: ${{ fromJSON(needs.renovate.outputs.branches) }}
|
||||||
|
env:
|
||||||
|
BRANCH: ${{ matrix.branch }}
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the batch branch
|
- name: Find the default branch
|
||||||
uses: actions/checkout@v6
|
id: default
|
||||||
|
run: echo "branch=$(gh api "repos/$GITHUB_REPOSITORY" --jq .default_branch)" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
# Not the triggering ref: a push or a manual run from another branch must still test the default branch.
|
||||||
|
- name: Checkout the default branch
|
||||||
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
ref: ${{ env.BATCH_BRANCH }}
|
ref: ${{ steps.default.outputs.branch }}
|
||||||
token: ${{ github.token }}
|
fetch-depth: 0
|
||||||
lfs: ${{ inputs.lfs }}
|
lfs: ${{ inputs.lfs }}
|
||||||
|
|
||||||
- name: Record the tested commit
|
- name: Merge the update branch locally
|
||||||
id: checkout-sha
|
id: merge
|
||||||
run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
run: |
|
||||||
|
git config user.name "Lukasz Raczylo"
|
||||||
|
git config user.email "[email protected]"
|
||||||
|
git fetch origin "$BRANCH"
|
||||||
|
echo "head=$(git rev-parse FETCH_HEAD)" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "subject=$(git log -1 --format=%s FETCH_HEAD)" >> "$GITHUB_OUTPUT"
|
||||||
|
if git merge-base --is-ancestor FETCH_HEAD HEAD; then
|
||||||
|
echo "::notice::$BRANCH is already in the default branch."
|
||||||
|
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||||
|
elif git merge --no-ff --no-commit FETCH_HEAD; then
|
||||||
|
# A lockfile that both sides changed merges as text and can be broken. Renovate regenerates it on the next run.
|
||||||
|
base=$(git merge-base HEAD FETCH_HEAD)
|
||||||
|
stale=""
|
||||||
|
for lock in pnpm-lock.yaml package-lock.json yarn.lock; do
|
||||||
|
for f in $(git ls-tree -r --name-only FETCH_HEAD | grep -E "(^|/)${lock//./\\.}$"); do
|
||||||
|
if ! git diff --quiet "$base" HEAD -- "$f" && ! git diff --quiet "$base" FETCH_HEAD -- "$f"; then stale="$stale $f"; fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
if [ -n "$stale" ]; then
|
||||||
|
git merge --abort
|
||||||
|
echo "::warning::$BRANCH and the default branch both changed${stale}. Renovate rebases it on the next run."
|
||||||
|
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
git merge --abort
|
||||||
|
echo "::warning::$BRANCH conflicts with the default branch. Renovate rebases it on the next run."
|
||||||
|
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Run workflow prepare script
|
- name: Run workflow prepare script
|
||||||
|
if: steps.merge.outputs.skip != 'true'
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
if [ -f "./workflow-prepare.sh" ]; then
|
if [ -f "./workflow-prepare.sh" ]; then
|
||||||
@@ -197,15 +374,42 @@ jobs:
|
|||||||
|
|
||||||
- name: Detect project types
|
- name: Detect project types
|
||||||
id: detect
|
id: detect
|
||||||
|
if: steps.merge.outputs.skip != 'true'
|
||||||
env:
|
env:
|
||||||
GO_DIR: ${{ inputs.go-working-directory }}
|
GO_DIR: ${{ inputs.go-working-directory }}
|
||||||
NODE_DIR: ${{ inputs.node-working-directory }}
|
NODE_DIR: ${{ inputs.node-working-directory }}
|
||||||
|
NODE_WORKSPACE: ${{ inputs.node-workspace }}
|
||||||
|
NODE_PACKAGES: ${{ inputs.node-packages }}
|
||||||
|
USE_GO: ${{ inputs.go }}
|
||||||
|
USE_NODE: ${{ inputs.node }}
|
||||||
run: |
|
run: |
|
||||||
go=false
|
go=false
|
||||||
node=false
|
node=false
|
||||||
pm=npm
|
pm=npm
|
||||||
if [ -f "$GO_DIR/go.mod" ]; then go=true; fi
|
if [ "$USE_GO" = true ] && [ -f "$GO_DIR/go.mod" ]; then go=true; fi
|
||||||
if [ -f "$NODE_DIR/package.json" ]; then
|
if [ "$USE_NODE" = true ] && [ "$NODE_WORKSPACE" = true ]; then
|
||||||
|
if [ ! -f "$NODE_DIR/pnpm-workspace.yaml" ] || [ ! -f "$NODE_DIR/pnpm-lock.yaml" ]; then
|
||||||
|
echo "::error::node-workspace needs pnpm-workspace.yaml and pnpm-lock.yaml in $NODE_DIR."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
IFS=',' read -r -a packages <<<"$NODE_PACKAGES"
|
||||||
|
count=0
|
||||||
|
for package in "${packages[@]}"; do
|
||||||
|
package=$(echo "$package" | xargs)
|
||||||
|
[ -n "$package" ] || continue
|
||||||
|
if [ ! -f "$NODE_DIR/$package/package.json" ]; then
|
||||||
|
echo "::error::node-packages lists $package, but $NODE_DIR/$package/package.json does not exist."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
count=$((count + 1))
|
||||||
|
done
|
||||||
|
if [ "$count" -eq 0 ]; then
|
||||||
|
echo "::error::node-workspace needs node-packages: the packages to update and test."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
node=true
|
||||||
|
pm=pnpm
|
||||||
|
elif [ "$USE_NODE" = true ] && [ -f "$NODE_DIR/package.json" ]; then
|
||||||
node=true
|
node=true
|
||||||
if [ -f "$NODE_DIR/pnpm-lock.yaml" ]; then pm=pnpm
|
if [ -f "$NODE_DIR/pnpm-lock.yaml" ]; then pm=pnpm
|
||||||
elif [ -f "$NODE_DIR/yarn.lock" ]; then pm=yarn
|
elif [ -f "$NODE_DIR/yarn.lock" ]; then pm=yarn
|
||||||
@@ -221,117 +425,326 @@ jobs:
|
|||||||
echo "pm=$pm"
|
echo "pm=$pm"
|
||||||
} >> "$GITHUB_OUTPUT"
|
} >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
# The merged go.mod can need a newer Go than the input allows, because a dependency update raises its go directive.
|
||||||
|
- name: Pick the Go version
|
||||||
|
id: goversion
|
||||||
|
if: steps.merge.outputs.skip != 'true' && steps.detect.outputs.go == 'true'
|
||||||
|
env:
|
||||||
|
GO_VERSION_INPUT: ${{ inputs.go-version }}
|
||||||
|
GO_DIR: ${{ inputs.go-working-directory }}
|
||||||
|
run: |
|
||||||
|
want=$(awk '$1=="go"{print $2; exit}' "$GO_DIR/go.mod")
|
||||||
|
floor=$(printf '%s' "$GO_VERSION_INPUT" | sed -E 's/^[^0-9]*//' | cut -d. -f1,2)
|
||||||
|
need=$(printf '%s' "$want" | cut -d. -f1,2)
|
||||||
|
version=$GO_VERSION_INPUT
|
||||||
|
if [ -n "$need" ] && [ "$(printf '%s\n%s\n' "$need" "${floor:-0}" | sort -V | tail -n 1)" != "${floor:-0}" ]; then
|
||||||
|
version=">=$want"
|
||||||
|
fi
|
||||||
|
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "::notice::Go $version (input '$GO_VERSION_INPUT', go.mod '$want')"
|
||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
if: steps.detect.outputs.go == 'true'
|
if: steps.merge.outputs.skip != 'true' && steps.detect.outputs.go == 'true'
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v7
|
||||||
with:
|
with:
|
||||||
go-version: ${{ inputs.go-version }}
|
go-version: ${{ steps.goversion.outputs.version }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
cache: true
|
cache: true
|
||||||
cache-dependency-path: ${{ inputs.go-working-directory }}/go.sum
|
cache-dependency-path: ${{ inputs.go-working-directory }}/go.sum
|
||||||
|
|
||||||
- name: Build and test Go
|
- name: Prepare Go
|
||||||
if: steps.detect.outputs.go == 'true'
|
if: steps.merge.outputs.skip != 'true' && steps.detect.outputs.go == 'true' && inputs.go-prepare-command != ''
|
||||||
working-directory: ${{ inputs.go-working-directory }}
|
working-directory: ${{ inputs.go-working-directory }}
|
||||||
|
env:
|
||||||
|
GO_PREPARE_COMMAND: ${{ inputs.go-prepare-command }}
|
||||||
run: |
|
run: |
|
||||||
go mod verify
|
set -o pipefail
|
||||||
go build ./...
|
bash -c "$GO_PREPARE_COMMAND" 2>&1 | tee -a "$RUNNER_TEMP/test-output.log"
|
||||||
go test -race -cover ./...
|
|
||||||
|
- name: Build and test Go
|
||||||
|
if: steps.merge.outputs.skip != 'true' && steps.detect.outputs.go == 'true'
|
||||||
|
working-directory: ${{ inputs.go-working-directory }}
|
||||||
|
env:
|
||||||
|
GO_TEST_COMMAND: ${{ inputs.go-test-command }}
|
||||||
|
TAGS: ${{ inputs.build-tags }}
|
||||||
|
GOTOOLCHAIN: auto
|
||||||
|
run: |
|
||||||
|
set -o pipefail
|
||||||
|
{
|
||||||
|
go mod verify
|
||||||
|
if [ -n "$GO_TEST_COMMAND" ]; then
|
||||||
|
bash -c "$GO_TEST_COMMAND"
|
||||||
|
elif [ -n "$TAGS" ]; then
|
||||||
|
export CGO_ENABLED=1
|
||||||
|
go build -tags "$TAGS" ./...
|
||||||
|
go test -tags "$TAGS" -race -cover ./...
|
||||||
|
else
|
||||||
|
go build ./...
|
||||||
|
go test -race -cover ./...
|
||||||
|
fi
|
||||||
|
} 2>&1 | tee -a "$RUNNER_TEMP/test-output.log"
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
if: steps.detect.outputs.node == 'true'
|
if: steps.merge.outputs.skip != 'true' && steps.detect.outputs.node == 'true'
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: ${{ inputs.node-version }}
|
node-version: ${{ inputs.node-version }}
|
||||||
|
|
||||||
|
# The npmrc lives in RUNNER_TEMP, not the checkout, so no git command can pick the token up.
|
||||||
|
- name: Configure the private npm registry
|
||||||
|
if: steps.merge.outputs.skip != 'true' && steps.detect.outputs.node == 'true'
|
||||||
|
env:
|
||||||
|
NPM_SCOPE: ${{ inputs.npm-registry-scope }}
|
||||||
|
NPM_URL: ${{ inputs.npm-registry-url }}
|
||||||
|
NPM_TOKEN: ${{ secrets.npm-registry-token }}
|
||||||
|
run: |
|
||||||
|
[ -n "$NPM_SCOPE" ] && [ -n "$NPM_URL" ] && [ -n "$NPM_TOKEN" ] || exit 0
|
||||||
|
echo "::add-mask::$NPM_TOKEN"
|
||||||
|
url="${NPM_URL%/}/"
|
||||||
|
npmrc="$RUNNER_TEMP/private-registry.npmrc"
|
||||||
|
(umask 077 && printf '%s:registry=%s\n//%s:_authToken=%s\n' "$NPM_SCOPE" "$url" "${url#*://}" "$NPM_TOKEN" > "$npmrc")
|
||||||
|
echo "NPM_CONFIG_USERCONFIG=$npmrc" >> "$GITHUB_ENV"
|
||||||
|
echo "Using a private npm registry for $NPM_SCOPE."
|
||||||
|
|
||||||
- name: Install Node dependencies
|
- name: Install Node dependencies
|
||||||
if: steps.detect.outputs.node == 'true'
|
if: steps.merge.outputs.skip != 'true' && steps.detect.outputs.node == 'true'
|
||||||
working-directory: ${{ inputs.node-working-directory }}
|
working-directory: ${{ inputs.node-working-directory }}
|
||||||
env:
|
env:
|
||||||
PM: ${{ steps.detect.outputs.pm }}
|
PM: ${{ steps.detect.outputs.pm }}
|
||||||
|
PNPM_VERSION: ${{ inputs.pnpm-version }}
|
||||||
run: |
|
run: |
|
||||||
# The lockfile must match what Renovate wrote: install without updating it.
|
set -o pipefail
|
||||||
case "$PM" in
|
{
|
||||||
pnpm | yarn)
|
# The lockfile must match what Renovate wrote: install without updating it.
|
||||||
command -v corepack >/dev/null || npm install --global corepack
|
case "$PM" in
|
||||||
corepack enable "$PM"
|
pnpm | yarn)
|
||||||
;;
|
command -v corepack >/dev/null || npm install --global corepack
|
||||||
esac
|
corepack enable "$PM"
|
||||||
case "$PM" in
|
;;
|
||||||
pnpm) pnpm install --frozen-lockfile ;;
|
esac
|
||||||
yarn) yarn install --immutable ;;
|
if [ "$PM" = pnpm ] && [ -n "$PNPM_VERSION" ]; then
|
||||||
npm)
|
corepack prepare "pnpm@$PNPM_VERSION" --activate
|
||||||
if [ -f package-lock.json ]; then npm ci; else npm install; fi
|
fi
|
||||||
;;
|
case "$PM" in
|
||||||
esac
|
pnpm) pnpm install --frozen-lockfile ;;
|
||||||
|
yarn) yarn install --immutable ;;
|
||||||
|
npm)
|
||||||
|
if [ -f package-lock.json ]; then npm ci; else npm install; fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
} 2>&1 | tee -a "$RUNNER_TEMP/test-output.log"
|
||||||
|
|
||||||
- name: Run Node scripts
|
- name: Run Node scripts
|
||||||
if: steps.detect.outputs.node == 'true'
|
if: steps.merge.outputs.skip != 'true' && steps.detect.outputs.node == 'true'
|
||||||
working-directory: ${{ inputs.node-working-directory }}
|
working-directory: ${{ inputs.node-working-directory }}
|
||||||
env:
|
env:
|
||||||
PM: ${{ steps.detect.outputs.pm }}
|
PM: ${{ steps.detect.outputs.pm }}
|
||||||
SCRIPTS: ${{ inputs.node-scripts }}
|
SCRIPTS: ${{ inputs.node-scripts }}
|
||||||
REQUIRE_TESTS: ${{ inputs.require-node-tests }}
|
REQUIRE_TESTS: ${{ inputs.require-node-tests }}
|
||||||
|
NODE_WORKSPACE: ${{ inputs.node-workspace }}
|
||||||
|
NODE_PACKAGES: ${{ inputs.node-packages }}
|
||||||
run: |
|
run: |
|
||||||
if [ "$REQUIRE_TESTS" = true ] && ! jq -e '.scripts.test' package.json >/dev/null; then
|
set -o pipefail
|
||||||
echo "::error::package.json has no test script. Add one, or set require-node-tests to false."
|
log="$RUNNER_TEMP/test-output.log"
|
||||||
exit 1
|
# A single project is the one package ".". A workspace is each listed package.
|
||||||
|
packages=(.)
|
||||||
|
if [ "$NODE_WORKSPACE" = true ]; then
|
||||||
|
IFS=',' read -r -a packages <<<"$NODE_PACKAGES"
|
||||||
|
fi
|
||||||
|
if [ "$REQUIRE_TESTS" = true ]; then
|
||||||
|
for package in "${packages[@]}"; do
|
||||||
|
package=$(echo "$package" | xargs)
|
||||||
|
[ -n "$package" ] || continue
|
||||||
|
if ! jq -e '.scripts.test' "$package/package.json" >/dev/null; then
|
||||||
|
echo "::error::$package/package.json has no test script. Add one, or set require-node-tests to false."
|
||||||
|
echo "$package/package.json has no test script, so the update was not tested." >>"$log"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
fi
|
fi
|
||||||
IFS=',' read -r -a scripts <<<"$SCRIPTS"
|
IFS=',' read -r -a scripts <<<"$SCRIPTS"
|
||||||
for script in "${scripts[@]}"; do
|
for script in "${scripts[@]}"; do
|
||||||
script=$(echo "$script" | xargs)
|
script=$(echo "$script" | xargs)
|
||||||
[ -n "$script" ] || continue
|
[ -n "$script" ] || continue
|
||||||
if jq -e --arg s "$script" '.scripts[$s]' package.json >/dev/null; then
|
for package in "${packages[@]}"; do
|
||||||
echo "::group::$PM run $script"
|
package=$(echo "$package" | xargs)
|
||||||
"$PM" run "$script"
|
[ -n "$package" ] || continue
|
||||||
echo "::endgroup::"
|
where=""
|
||||||
else
|
[ "$package" = . ] || where=" ($package)"
|
||||||
echo "::notice::No '$script' script, skipping."
|
if jq -e --arg s "$script" '.scripts[$s]' "$package/package.json" >/dev/null; then
|
||||||
fi
|
echo "::group::$PM run $script$where"
|
||||||
|
(cd "$package" && "$PM" run "$script") 2>&1 | tee -a "$log"
|
||||||
|
echo "::endgroup::"
|
||||||
|
else
|
||||||
|
echo "::notice::No '$script' script$where, skipping."
|
||||||
|
fi
|
||||||
|
done
|
||||||
done
|
done
|
||||||
|
|
||||||
- name: Report the failure on the PR
|
- name: Mark the tests as passed
|
||||||
if: failure()
|
if: steps.merge.outputs.skip != 'true'
|
||||||
|
run: touch "$RUNNER_TEMP/tests-passed"
|
||||||
|
|
||||||
|
- name: Open the PR and merge it
|
||||||
|
if: steps.merge.outputs.skip != 'true'
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ github.token }}
|
HEAD_SHA: ${{ steps.merge.outputs.head }}
|
||||||
PR: ${{ needs.renovate.outputs.pr-number }}
|
SUBJECT: ${{ steps.merge.outputs.subject }}
|
||||||
|
ADMIN: ${{ inputs.admin-merge && 'true' || '' }}
|
||||||
|
run: |
|
||||||
|
default=$(gh api "repos/$GITHUB_REPOSITORY" --jq .default_branch)
|
||||||
|
pr=$(gh pr list --repo "$GITHUB_REPOSITORY" --head "$BRANCH" --state open --json number --jq '.[0].number // empty')
|
||||||
|
if [ -z "$pr" ]; then
|
||||||
|
url=$(gh pr create --repo "$GITHUB_REPOSITORY" --head "$BRANCH" --base "$default" \
|
||||||
|
--title "$SUBJECT" \
|
||||||
|
--body "Automated dependency update. The tests passed on the result of merging this branch into $default.")
|
||||||
|
pr=${url##*/}
|
||||||
|
fi
|
||||||
|
# GitHub needs a moment to work out whether the PR can merge.
|
||||||
|
for _ in $(seq 1 12); do
|
||||||
|
state=$(gh pr view "$pr" --repo "$GITHUB_REPOSITORY" --json mergeable --jq .mergeable)
|
||||||
|
[ "$state" != UNKNOWN ] && break
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
if [ "$state" = CONFLICTING ]; then
|
||||||
|
gh pr close "$pr" --repo "$GITHUB_REPOSITORY" \
|
||||||
|
--comment "The tests passed, but the branch conflicts with $default. Renovate rebases it on the next run."
|
||||||
|
echo "::warning::$BRANCH conflicts with $default, so PR #$pr was closed. Renovate rebases it on the next run."
|
||||||
|
echo "\`$BRANCH\` passed its tests but conflicts with \`$default\`. Renovate rebases it on the next run." >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
# --match-head-commit refuses to merge anything newer than the commit that was tested.
|
||||||
|
# GitHub answers 5xx now and then; a failed call may still have merged, so check the state before retrying.
|
||||||
|
merged=false
|
||||||
|
for _ in 1 2 3; do
|
||||||
|
if gh pr merge "$pr" --repo "$GITHUB_REPOSITORY" --squash --delete-branch \
|
||||||
|
--match-head-commit "$HEAD_SHA" \
|
||||||
|
--subject "$SUBJECT" \
|
||||||
|
--body "Automated dependency update. All tests passed." \
|
||||||
|
${ADMIN:+--admin}; then
|
||||||
|
merged=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 10
|
||||||
|
[ "$(gh pr view "$pr" --repo "$GITHUB_REPOSITORY" --json state --jq .state)" = MERGED ] && { merged=true; break; }
|
||||||
|
done
|
||||||
|
if [ "$merged" != true ]; then
|
||||||
|
gh pr close "$pr" --repo "$GITHUB_REPOSITORY" --comment "The tests passed, but GitHub refused the merge. See the workflow run. The next run tries again."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Merged #$pr ($BRANCH)." >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
|
||||||
|
- name: Report the failure
|
||||||
|
if: failure()
|
||||||
|
# Best effort: the summary is always written, the issue needs issues: write.
|
||||||
|
continue-on-error: true
|
||||||
|
env:
|
||||||
|
HEAD_SHA: ${{ steps.merge.outputs.head }}
|
||||||
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||||
run: |
|
run: |
|
||||||
gh pr comment "$PR" --repo "$GITHUB_REPOSITORY" \
|
if [ -e "$RUNNER_TEMP/tests-passed" ]; then
|
||||||
--body "The dependency update failed its tests, so it was not merged. See $RUN_URL. Renovate retries on the next run."
|
echo "The tests passed for \`$BRANCH\`, but the merge failed. See the log above. The next run tries again." >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "The tests failed for \`$BRANCH\`, so no PR was opened. Renovate retries on the next run." >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
|
||||||
merge:
|
log="$RUNNER_TEMP/test-output.log"
|
||||||
name: Merge the update
|
esc=$(printf '\033')
|
||||||
needs: [renovate, test]
|
marker="<!-- renovate-autoupdate:$BRANCH -->"
|
||||||
if: needs.test.result == 'success'
|
body="$RUNNER_TEMP/failure-comment.md"
|
||||||
|
{
|
||||||
|
echo "$marker"
|
||||||
|
echo "### \`$BRANCH\` failed its tests"
|
||||||
|
echo
|
||||||
|
echo "Commit \`${HEAD_SHA:0:8}\`, [workflow run]($RUN_URL), $(date -u '+%Y-%m-%d %H:%M') UTC. No PR was opened."
|
||||||
|
echo
|
||||||
|
echo "<details open><summary>Test output, last lines</summary>"
|
||||||
|
echo
|
||||||
|
echo '`````'
|
||||||
|
if [ -s "$log" ]; then
|
||||||
|
sed -E "s/${esc}\[[0-9;]*[A-Za-z]//g" "$log" | tail -c 50000
|
||||||
|
else
|
||||||
|
echo "No output was captured. See the workflow run."
|
||||||
|
fi
|
||||||
|
echo '`````'
|
||||||
|
echo
|
||||||
|
echo "</details>"
|
||||||
|
} > "$body"
|
||||||
|
|
||||||
|
issue=$(gh issue list --repo "$GITHUB_REPOSITORY" --state open --search "\"$ISSUE_TITLE\" in:title" --json number,title \
|
||||||
|
--jq "[.[] | select(.title == \"$ISSUE_TITLE\")][0].number // empty")
|
||||||
|
if [ -z "$issue" ]; then
|
||||||
|
url=$(gh issue create --repo "$GITHUB_REPOSITORY" --title "$ISSUE_TITLE" \
|
||||||
|
--body "Updates that failed their tests, so no PR was opened. Each comment holds the output for one branch. Fix the cause on the branch or in the code. The workflow retries on every run, merges an update once it passes, and closes this issue when none fail.")
|
||||||
|
issue=${url##*/}
|
||||||
|
fi
|
||||||
|
id=$(gh api "repos/$GITHUB_REPOSITORY/issues/$issue/comments" --paginate \
|
||||||
|
--jq ".[] | select(.body | contains(\"$marker\")) | .id" | head -n 1)
|
||||||
|
if [ -n "$id" ]; then
|
||||||
|
gh api -X PATCH "repos/$GITHUB_REPOSITORY/issues/comments/$id" -F body=@"$body" >/dev/null
|
||||||
|
else
|
||||||
|
gh issue comment "$issue" --repo "$GITHUB_REPOSITORY" --body-file "$body"
|
||||||
|
fi
|
||||||
|
|
||||||
|
tidy-failures:
|
||||||
|
name: Tidy the failure issue
|
||||||
|
needs: [renovate, test-and-merge]
|
||||||
|
if: always() && needs.renovate.result == 'success'
|
||||||
|
runs-on: ${{ fromJSON(startsWith(inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest', '{') && (inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest') || format('"{0}"', inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest')) }}
|
||||||
|
timeout-minutes: 10
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
contents: read
|
||||||
|
steps:
|
||||||
|
- name: Remove fixed updates and close the issue when none fail
|
||||||
|
continue-on-error: true
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
issue=$(gh issue list --repo "$GITHUB_REPOSITORY" --state open --search "\"$ISSUE_TITLE\" in:title" --json number,title \
|
||||||
|
--jq "[.[] | select(.title == \"$ISSUE_TITLE\")][0].number // empty")
|
||||||
|
[ -n "$issue" ] || exit 0
|
||||||
|
branches=$(gh api --paginate "repos/$GITHUB_REPOSITORY/branches" --jq '.[].name')
|
||||||
|
remaining=0
|
||||||
|
while IFS=$'\t' read -r id branch; do
|
||||||
|
[ -n "$id" ] || continue
|
||||||
|
if printf '%s\n' "$branches" | grep -Fxq "$branch"; then
|
||||||
|
remaining=$((remaining + 1))
|
||||||
|
else
|
||||||
|
gh api -X DELETE "repos/$GITHUB_REPOSITORY/issues/comments/$id"
|
||||||
|
fi
|
||||||
|
done < <(gh api "repos/$GITHUB_REPOSITORY/issues/$issue/comments" --paginate \
|
||||||
|
--jq '.[] | select(.body | startswith("<!-- renovate-autoupdate:")) | [.id, (.body | split("\n")[0] | sub("^<!-- renovate-autoupdate:"; "") | sub(" -->$"; ""))] | @tsv')
|
||||||
|
if [ "$remaining" -eq 0 ]; then
|
||||||
|
gh issue close "$issue" --repo "$GITHUB_REPOSITORY" --comment "Every dependency update now passes its tests."
|
||||||
|
fi
|
||||||
|
|
||||||
|
release:
|
||||||
|
name: Dispatch the release
|
||||||
|
needs: [renovate, test-and-merge]
|
||||||
|
if: always() && needs.renovate.outputs.branches != '[]' && inputs.release-workflow != ''
|
||||||
runs-on: ${{ fromJSON(startsWith(inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest', '{') && (inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest') || format('"{0}"', inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest')) }}
|
runs-on: ${{ fromJSON(startsWith(inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest', '{') && (inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest') || format('"{0}"', inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest')) }}
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
permissions:
|
permissions:
|
||||||
actions: write
|
actions: write
|
||||||
contents: write
|
contents: read
|
||||||
pull-requests: write
|
|
||||||
steps:
|
steps:
|
||||||
- name: Squash-merge the PR
|
- name: Dispatch when the default branch moved
|
||||||
env:
|
|
||||||
# A push made with github.token does not trigger workflows, so the release is dispatched below.
|
|
||||||
GH_TOKEN: ${{ github.token }}
|
|
||||||
PR: ${{ needs.renovate.outputs.pr-number }}
|
|
||||||
HEAD_SHA: ${{ needs.test.outputs.head-sha }}
|
|
||||||
SUBJECT: ${{ inputs.commit-subject }}
|
|
||||||
ADMIN: ${{ inputs.admin-merge && 'true' || '' }}
|
|
||||||
run: |
|
|
||||||
# --match-head-commit refuses to merge anything newer than the commit that was tested.
|
|
||||||
gh pr merge "$PR" --repo "$GITHUB_REPOSITORY" --squash --delete-branch \
|
|
||||||
--match-head-commit "$HEAD_SHA" \
|
|
||||||
--subject "$SUBJECT" \
|
|
||||||
--body "Automated dependency update. All tests passed." \
|
|
||||||
${ADMIN:+--admin}
|
|
||||||
|
|
||||||
- name: Dispatch the release workflow
|
|
||||||
if: inputs.release-workflow != ''
|
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
RELEASE_WORKFLOW: ${{ inputs.release-workflow }}
|
RELEASE_WORKFLOW: ${{ inputs.release-workflow }}
|
||||||
|
BASE_SHA: ${{ needs.renovate.outputs.base-sha }}
|
||||||
run: |
|
run: |
|
||||||
|
# A push made with github.token does not trigger workflows, so dispatch the release explicitly.
|
||||||
branch=$(gh api "repos/$GITHUB_REPOSITORY" --jq .default_branch)
|
branch=$(gh api "repos/$GITHUB_REPOSITORY" --jq .default_branch)
|
||||||
gh workflow run "$RELEASE_WORKFLOW" --repo "$GITHUB_REPOSITORY" --ref "$branch"
|
now=$(gh api "repos/$GITHUB_REPOSITORY/commits/$branch" --jq .sha)
|
||||||
|
if [ "$now" = "$BASE_SHA" ]; then
|
||||||
|
echo "Nothing was merged, so no release."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
IFS=',' read -r -a workflows <<<"$RELEASE_WORKFLOW"
|
||||||
|
for workflow in "${workflows[@]}"; do
|
||||||
|
workflow=$(echo "$workflow" | xargs)
|
||||||
|
[ -n "$workflow" ] || continue
|
||||||
|
gh workflow run "$workflow" --repo "$GITHUB_REPOSITORY" --ref "$branch"
|
||||||
|
done
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
name: Validate
|
||||||
|
|
||||||
|
# Checks that a change to the workflows or composite actions still works. Renovate
|
||||||
|
# opens a PR for each action bump; this workflow is the gate those PRs must pass.
|
||||||
|
# The aggregate job "validate" is the one required status check.
|
||||||
|
|
||||||
|
# No paths filter: "validate" is a required status check, and a required check
|
||||||
|
# that never runs blocks a PR that touches only other files.
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: validate-${{ github.head_ref || github.ref_name }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
static:
|
||||||
|
name: Static checks
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 10
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
|
# actionlint, then every external `uses:` reference must resolve.
|
||||||
|
- name: Check workflows and action references
|
||||||
|
run: scripts/check-actions.sh --ci
|
||||||
|
|
||||||
|
selftest-go:
|
||||||
|
name: Self-test go-test action
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
|
# The local path runs the action as it is on this branch, not at @main.
|
||||||
|
- name: Run the go-test action on the fixture module
|
||||||
|
uses: ./.github/actions/go-test
|
||||||
|
with:
|
||||||
|
working-directory: testdata/selftest/go
|
||||||
|
|
||||||
|
selftest-node-build:
|
||||||
|
name: Self-test node-build action
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
|
- name: Run the node-build action on the fixture project
|
||||||
|
uses: ./.github/actions/node-build
|
||||||
|
with:
|
||||||
|
build-script: cd testdata/selftest/node && npm ci && npm run build
|
||||||
|
cache-dependency-path: testdata/selftest/node/package-lock.json
|
||||||
|
|
||||||
|
selftest-node-pr:
|
||||||
|
name: Self-test node-pr workflow
|
||||||
|
uses: ./.github/workflows/node-pr.yaml
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: read
|
||||||
|
with:
|
||||||
|
working-directory: testdata/selftest/node
|
||||||
|
require-tests: true
|
||||||
|
|
||||||
|
validate:
|
||||||
|
name: validate
|
||||||
|
if: always()
|
||||||
|
needs: [static, selftest-go, selftest-node-build, selftest-node-pr]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 5
|
||||||
|
steps:
|
||||||
|
- name: Require every check to pass
|
||||||
|
env:
|
||||||
|
RESULTS: ${{ join(needs.*.result, ' ') }}
|
||||||
|
run: |
|
||||||
|
echo "results: $RESULTS"
|
||||||
|
for r in $RESULTS; do
|
||||||
|
if [ "$r" != success ]; then
|
||||||
|
echo "::error::a validation job finished as '$r'"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
@@ -86,6 +86,52 @@ jobs:
|
|||||||
secrets: inherit
|
secrets: inherit
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### `node-pr.yaml`
|
||||||
|
|
||||||
|
Pull request checks for a Node, Vue or Astro project. It installs without changing the lockfile, then runs `lint`, `typecheck`, `test` and `build`. A script that does not exist is skipped. A push to a branch with an open PR skips the duplicate run, and stale runs on the same branch are cancelled. Call it once per project directory.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: Pull Request
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- "**"
|
||||||
|
- "!main"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
frontend:
|
||||||
|
uses: lukaszraczylo/shared-actions/.github/workflows/node-pr.yaml@main
|
||||||
|
with:
|
||||||
|
working-directory: web
|
||||||
|
```
|
||||||
|
|
||||||
|
**Inputs:**
|
||||||
|
| Input | Default | Description |
|
||||||
|
|-------|---------|-------------|
|
||||||
|
| `node-version` | `22` | Node.js version |
|
||||||
|
| `working-directory` | `.` | Directory that holds `package.json`. With `workspace`, the directory that holds `pnpm-workspace.yaml` and `pnpm-lock.yaml`. |
|
||||||
|
| `workspace` | `false` | pnpm workspace: install once in `working-directory`, then run `scripts` in each of `packages`. `working-directory` needs no `package.json`. |
|
||||||
|
| `packages` | | With `workspace`, comma-separated package directories relative to `working-directory`, for example `admin,public,packages/ui` |
|
||||||
|
| `scripts` | `lint,typecheck,test,build` | Scripts to run in order. A missing script is skipped. |
|
||||||
|
| `pnpm-version` | | pnpm version for a repo with no `packageManager` field. Empty uses the field, or the newest pnpm. |
|
||||||
|
| `require-tests` | `false` | Fail when there is no `test` script |
|
||||||
|
| `lfs` | `false` | Git LFS checkout |
|
||||||
|
| `npm-registry-scope` | | Scope served by a private npm registry, for example `@fortawesome`. |
|
||||||
|
| `npm-registry-url` | | URL of that registry, for example `https://npm.fontawesome.com/`. |
|
||||||
|
| `runner` | | Runner label or runner group, as in the other workflows |
|
||||||
|
|
||||||
|
pnpm is used when `pnpm-lock.yaml` exists, yarn for `yarn.lock`, otherwise npm. pnpm and yarn run through Corepack, so set `packageManager` in `package.json`. A workspace with no root `package.json` has no such field, so set `pnpm-version`. With `require-tests`, every listed package needs a `test` script.
|
||||||
|
|
||||||
|
**Private npm registry.** Set `npm-registry-scope`, `npm-registry-url` and the `npm-registry-token` secret together; with any one missing nothing changes. The token goes into an npmrc outside the checkout (`NPM_CONFIG_USERCONFIG`), is masked in logs, and works for npm, pnpm and yarn classic. Yarn 2+ ignores npmrc and needs its own `.yarnrc.yml`.
|
||||||
|
|
||||||
### `go-autoupdate.yaml`
|
### `go-autoupdate.yaml`
|
||||||
|
|
||||||
Automatic dependency updates for Go. See `renovate-autoupdate.yaml` for the Renovate-based replacement that also covers npm, pnpm and yarn.
|
Automatic dependency updates for Go. See `renovate-autoupdate.yaml` for the Renovate-based replacement that also covers npm, pnpm and yarn.
|
||||||
@@ -104,9 +150,10 @@ jobs:
|
|||||||
|
|
||||||
Daily dependency updates for Go and npm/pnpm/yarn projects, built on Renovate. It replaces `go-autoupdate.yaml`.
|
Daily dependency updates for Go and npm/pnpm/yarn projects, built on Renovate. It replaces `go-autoupdate.yaml`.
|
||||||
|
|
||||||
1. Renovate puts every minor and patch update (direct and indirect Go modules, npm, pnpm and yarn packages) in one PR on the branch `deps-autoupdate/batch`.
|
1. Renovate upgrades everything that can be upgraded on branches named `deps-autoupdate/*` and opens no PR. All minor and patch updates (direct and indirect Go modules, npm, pnpm and yarn packages) go on `deps-autoupdate/batch`. Each major update gets its own branch.
|
||||||
2. The workflow checks out that branch and runs the tests: `go build`, `go test -race -cover` for Go, and the `package.json` scripts for Node.
|
2. For each branch, one at a time, the workflow merges it into the default branch locally and runs the tests on that result: `go build` and `go test -race -cover` for Go, the `package.json` scripts for Node.
|
||||||
3. If everything passes, it squash-merges the PR. If a test fails, the PR stays open with a comment and Renovate retries on the next run.
|
3. If the tests pass, it opens a PR and squash-merges it. If they fail, no PR is opened. The test output goes into a comment on one sticky issue (see below), and Renovate retries on the next run.
|
||||||
|
4. If anything merged and `release-workflow` is set, it dispatches the release once.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
name: Update dependencies
|
name: Update dependencies
|
||||||
@@ -119,6 +166,7 @@ on:
|
|||||||
permissions:
|
permissions:
|
||||||
actions: write # only to dispatch release-workflow
|
actions: write # only to dispatch release-workflow
|
||||||
contents: write
|
contents: write
|
||||||
|
issues: write # only to report failing updates
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -129,27 +177,45 @@ jobs:
|
|||||||
release-workflow: release.yaml
|
release-workflow: release.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
The workflow finds `go.mod` and `package.json` itself and tests what exists. A repo with both, such as a Go backend with a Vue frontend, gets both tested in one run.
|
The workflow finds `go.mod` and `package.json` itself and tests what exists. A repo with both, such as a Go backend with a Vue frontend, gets both tested for every branch.
|
||||||
|
|
||||||
**Inputs:**
|
**Inputs:**
|
||||||
| Input | Default | Description |
|
| Input | Default | Description |
|
||||||
|-------|---------|-------------|
|
|-------|---------|-------------|
|
||||||
| `go-version` | `>=1.24` | Go version |
|
| `go-version` | `>=1.24` | Go version |
|
||||||
| `go-working-directory` | `.` | Directory that holds `go.mod` |
|
| `go` | `true` | Update and test the Go module. Turn off for a repo with no Go code. |
|
||||||
|
| `go-working-directory` | `.` | Directory that holds `go.mod`. Only this module is updated and tested. |
|
||||||
|
| `go-mod-tidy` | `true` | Run `go mod tidy` after a Go update. Turn it off for an Encore app, where tidy prunes `go.sum` entries the build needs. |
|
||||||
|
| `go-prepare-command` | | Shell commands to run before the Go tests, for example installing a CLI. Append a tool directory to `$GITHUB_PATH` to put it on `PATH`. |
|
||||||
|
| `go-test-command` | | Replaces the default `go build` and `go test`, for example `encore test ./...` for an Encore app. |
|
||||||
|
| `build-tags` | | Build tags for the default `go build` and `go test`, for example `fts5`. Also sets `CGO_ENABLED=1`. Ignored when `go-test-command` is set. |
|
||||||
| `node-version` | `22` | Node.js version |
|
| `node-version` | `22` | Node.js version |
|
||||||
| `node-working-directory` | `.` | Directory that holds `package.json` |
|
| `node` | `true` | Update and test the Node project. Turn off for a repo with no frontend to test. |
|
||||||
|
| `node-working-directory` | `.` | Directory that holds `package.json`. Only this project is updated and tested. With `node-workspace`, the directory that holds the workspace files. |
|
||||||
|
| `node-workspace` | `false` | pnpm workspace: `node-working-directory` holds `pnpm-workspace.yaml` and `pnpm-lock.yaml` and needs no `package.json`. Install once there, then run `node-scripts` in each of `node-packages`. |
|
||||||
|
| `node-packages` | | With `node-workspace`, comma-separated package directories relative to `node-working-directory`, for example `admin,public,packages/ui`. Only these are updated and tested. |
|
||||||
| `node-scripts` | `lint,typecheck,test,build` | Scripts to run in order. A missing script is skipped. |
|
| `node-scripts` | `lint,typecheck,test,build` | Scripts to run in order. A missing script is skipped. |
|
||||||
|
| `pnpm-version` | | pnpm version for a repo with no `packageManager` field. Empty uses the field, or the newest pnpm. |
|
||||||
| `require-node-tests` | `true` | Fail when there is no `test` script, so an untested update never merges |
|
| `require-node-tests` | `true` | Fail when there is no `test` script, so an untested update never merges |
|
||||||
| `include-major` | `false` | Put major updates in the batch. When `false`, each major gets its own PR that is never merged automatically. |
|
| `major-updates` | `separate` | `separate`: each major gets its own branch, tested and merged on its own. `batch`: majors join the batch. `ignore`: skip majors. |
|
||||||
| `minimum-release-age` | `2 days` | Skip releases younger than this |
|
| `minimum-release-age` | `2 days` | Skip releases younger than this |
|
||||||
| `renovate-version` | `44` | Renovate version |
|
| `renovate-version` | `44` | Renovate version |
|
||||||
| `commit-subject` | `chore(deps): update dependencies` | Squash commit subject |
|
| `release-workflow` | | Workflow file to dispatch after a merge, for example `release.yaml`. Give several as a comma-separated list. Each needs a `workflow_dispatch` trigger. Empty means no dispatch. |
|
||||||
| `release-workflow` | | Workflow file to dispatch after the merge, for example `release.yaml`. It needs a `workflow_dispatch` trigger. Empty means no dispatch. |
|
|
||||||
| `admin-merge` | `false` | Merge with `--admin`. The workflow token cannot bypass branch protection. |
|
| `admin-merge` | `false` | Merge with `--admin`. The workflow token cannot bypass branch protection. |
|
||||||
| `lfs` | `false` | Git LFS checkout |
|
| `lfs` | `false` | Git LFS checkout |
|
||||||
|
| `npm-registry-scope` | | Scope served by a private npm registry, for example `@fortawesome`. |
|
||||||
|
| `npm-registry-url` | | URL of that registry, for example `https://npm.fontawesome.com/`. |
|
||||||
| `runner` | | Runner label or runner group, as in the other workflows |
|
| `runner` | | Runner label or runner group, as in the other workflows |
|
||||||
|
|
||||||
**Package manager.** pnpm is used when `pnpm-lock.yaml` exists, yarn for `yarn.lock`, otherwise npm. The install step never changes the lockfile (`--frozen-lockfile`, `--immutable`, `npm ci`), so a lockfile that does not match `package.json` fails the run. pnpm and yarn run through Corepack, so set `packageManager` in `package.json`.
|
**Private npm registry.** Set `npm-registry-scope`, `npm-registry-url` and the `npm-registry-token` secret together; with any one missing nothing changes. Renovate and the install step both use it. The token goes into an npmrc outside the checkout (`NPM_CONFIG_USERCONFIG`), is masked in logs, and works for npm, pnpm and yarn classic. Yarn 2+ ignores npmrc and needs its own `.yarnrc.yml`.
|
||||||
|
|
||||||
|
**Failing updates.** The last 50 KB of the test output goes into a comment on one issue titled "Dependency updates failing tests". Each failing branch has its own comment, updated on every run, and a header with the commit and the run link. When a branch passes and merges, its comment is removed. When no failing branch is left, the issue closes. The output is also in the run summary. Without `issues: write` the run still works, but the issue is skipped.
|
||||||
|
|
||||||
|
**Only what is tested is updated.** Renovate manages one Go module and one Node project: the ones in `go-working-directory` and `node-working-directory`. A second `package.json` (docs, e2e tests, another frontend) or a second `go.mod` is left alone, because nothing would test its updates. In a workspace the same rule applies to packages: only those in `node-packages` are updated, plus `pnpm-workspace.yaml` and the root `pnpm-lock.yaml`.
|
||||||
|
|
||||||
|
**Branches run one at a time**, so each one is tested on top of the ones merged before it. A branch that conflicts with the default branch is skipped, and Renovate rebases it on the next run.
|
||||||
|
|
||||||
|
**Package manager.** pnpm is used when `pnpm-lock.yaml` exists, yarn for `yarn.lock`, otherwise npm. The install step never changes the lockfile (`--frozen-lockfile`, `--immutable`, `npm ci`), so a lockfile that does not match `package.json` fails the run. pnpm and yarn run through Corepack, so set `packageManager` in `package.json`. A workspace with no root `package.json` has no such field, so set `pnpm-version`. With `require-node-tests`, every listed package needs a `test` script.
|
||||||
|
|
||||||
**Per-project rules.** Put a `renovate.json` in the calling repository. Renovate reads it and merges it with the defaults, so no extra input is needed:
|
**Per-project rules.** Put a `renovate.json` in the calling repository. Renovate reads it and merges it with the defaults, so no extra input is needed:
|
||||||
|
|
||||||
@@ -164,10 +230,10 @@ The defaults already cap `typescript` below 7, which breaks `vue-tsc`.
|
|||||||
|
|
||||||
**Token.** No secret is needed. Everything runs inside the calling repository with the workflow's own token, which needs `contents: write` and `pull-requests: write`. Two repository settings matter:
|
**Token.** No secret is needed. Everything runs inside the calling repository with the workflow's own token, which needs `contents: write` and `pull-requests: write`. Two repository settings matter:
|
||||||
|
|
||||||
- Settings, Actions, General: tick "Allow GitHub Actions to create and approve pull requests". Without it Renovate cannot open the PR.
|
- Settings, Actions, General: tick "Allow GitHub Actions to create and approve pull requests". Without it the workflow cannot open the PR.
|
||||||
- A push made with the workflow token does not trigger other workflows, so a release workflow that runs on `push` will not start after the merge. Set `release-workflow` to dispatch it explicitly, and grant `actions: write`.
|
- A push made with the workflow token does not trigger other workflows, so a release workflow that runs on `push` will not start after a merge. Set `release-workflow` to dispatch it, and grant `actions: write`.
|
||||||
|
|
||||||
**Commit message.** The squash commit uses a fixed subject and body without the words `major`, `minor` or `breaking`, because `semver-generator` matches release keywords in commit messages.
|
**Commit message.** The squash commit subject is the Renovate commit message, for example `chore(deps): update dependencies`, and the body is fixed text. Neither contains `major` or `breaking`, which `semver-generator` matches in commit messages.
|
||||||
|
|
||||||
## Composite Actions
|
## Composite Actions
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"timezone": "Europe/London",
|
||||||
|
"enabledManagers": [
|
||||||
|
"github-actions"
|
||||||
|
],
|
||||||
|
"semanticCommits": "enabled",
|
||||||
|
"semanticCommitType": "chore",
|
||||||
|
"semanticCommitScope": "deps",
|
||||||
|
"dependencyDashboard": false,
|
||||||
|
"prConcurrentLimit": 0,
|
||||||
|
"prHourlyLimit": 0,
|
||||||
|
"minimumReleaseAge": "3 days",
|
||||||
|
"postUpgradeTasks": {
|
||||||
|
"commands": [
|
||||||
|
"scripts/check-actions.sh --renovate"
|
||||||
|
],
|
||||||
|
"fileFilters": [
|
||||||
|
".validation-failed"
|
||||||
|
],
|
||||||
|
"executionMode": "branch"
|
||||||
|
},
|
||||||
|
"packageRules": [
|
||||||
|
{
|
||||||
|
"description": "This repository's own composite actions are tracked at @main by design.",
|
||||||
|
"matchPackageNames": [
|
||||||
|
"lukaszraczylo/shared-actions"
|
||||||
|
],
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Tool versions in action inputs and defaults (Go, Node, GoReleaser) are policy, not pins. A bump would change what every consumer builds with.",
|
||||||
|
"matchDatasources": [
|
||||||
|
"golang-version",
|
||||||
|
"node-version"
|
||||||
|
],
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "The GoReleaser version input is policy, not a pin.",
|
||||||
|
"matchDepNames": [
|
||||||
|
"goreleaser/goreleaser"
|
||||||
|
],
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Minor and patch bumps merge on their own once the validate check is green.",
|
||||||
|
"matchUpdateTypes": [
|
||||||
|
"minor",
|
||||||
|
"patch",
|
||||||
|
"digest",
|
||||||
|
"pin"
|
||||||
|
],
|
||||||
|
"automerge": true,
|
||||||
|
"automergeType": "pr",
|
||||||
|
"platformAutomerge": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Every consumer calls these actions at @main, so a major bump waits for a human.",
|
||||||
|
"matchUpdateTypes": [
|
||||||
|
"major"
|
||||||
|
],
|
||||||
|
"automerge": false,
|
||||||
|
"addLabels": [
|
||||||
|
"major"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"statusCheckNames": {
|
||||||
|
"artifactError": null,
|
||||||
|
"configValidation": null,
|
||||||
|
"mergeConfidence": null,
|
||||||
|
"minimumReleaseAge": null
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+115
@@ -0,0 +1,115 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Static checks for the workflows and composite actions in this repository:
|
||||||
|
# 1. actionlint (schema, expressions, and shell steps through shellcheck when present)
|
||||||
|
# 2. every external `uses:` reference resolves to a real tag or branch
|
||||||
|
#
|
||||||
|
# Usage: scripts/check-actions.sh [--ci | --renovate]
|
||||||
|
# --ci exit 1 when a check fails (default)
|
||||||
|
# --renovate exit 0 and write .validation-failed when a check fails, so the
|
||||||
|
# Renovate runner deletes the branch before a PR can open
|
||||||
|
#
|
||||||
|
# Needs: bash, git, curl, tar, sha256sum or shasum. No other tools: actionlint is
|
||||||
|
# downloaded at a pinned version and checked against a pinned SHA-256.
|
||||||
|
set -u
|
||||||
|
|
||||||
|
mode=--ci
|
||||||
|
case "${1:-}" in
|
||||||
|
"" | --ci) mode=--ci ;;
|
||||||
|
--renovate) mode=--renovate ;;
|
||||||
|
*) echo "usage: $0 [--ci | --renovate]" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
ACTIONLINT_VERSION=1.7.12
|
||||||
|
ACTIONLINT_SHA256_AMD64=8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8
|
||||||
|
ACTIONLINT_SHA256_ARM64=325e971b6ba9bfa504672e29be93c24981eeb1c07576d730e9f7c8805afff0c6
|
||||||
|
marker=.validation-failed
|
||||||
|
|
||||||
|
cd "$(git rev-parse --show-toplevel)" || exit 2
|
||||||
|
|
||||||
|
fail=0
|
||||||
|
problem() {
|
||||||
|
echo "::error::$*" >&2
|
||||||
|
fail=1
|
||||||
|
}
|
||||||
|
|
||||||
|
sha256_of() {
|
||||||
|
if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | cut -d' ' -f1; else shasum -a 256 "$1" | cut -d' ' -f1; fi
|
||||||
|
}
|
||||||
|
|
||||||
|
install_actionlint() {
|
||||||
|
local os arch want url tmp
|
||||||
|
case "$(uname -s)" in Linux) os=linux ;; Darwin) os=darwin ;; *) return 1 ;; esac
|
||||||
|
case "$(uname -m)" in
|
||||||
|
x86_64 | amd64) arch=amd64; want=$ACTIONLINT_SHA256_AMD64 ;;
|
||||||
|
aarch64 | arm64) arch=arm64; want=$ACTIONLINT_SHA256_ARM64 ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
# The pinned checksums are for the linux builds only.
|
||||||
|
[ "$os" = linux ] || return 1
|
||||||
|
tmp=$(mktemp -d)
|
||||||
|
url="https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_${os}_${arch}.tar.gz"
|
||||||
|
curl -fsSL --retry 3 -o "$tmp/a.tgz" "$url" || return 1
|
||||||
|
[ "$(sha256_of "$tmp/a.tgz")" = "$want" ] || { echo "actionlint checksum mismatch" >&2; return 1; }
|
||||||
|
tar -xzf "$tmp/a.tgz" -C "$tmp" actionlint || return 1
|
||||||
|
echo "$tmp/actionlint"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_actionlint() {
|
||||||
|
local bin
|
||||||
|
if command -v actionlint >/dev/null 2>&1; then
|
||||||
|
bin=$(command -v actionlint)
|
||||||
|
else
|
||||||
|
bin=$(install_actionlint) || { problem "cannot install actionlint ${ACTIONLINT_VERSION}"; return; }
|
||||||
|
fi
|
||||||
|
# -shellcheck= disables the shellcheck pass when the binary is missing.
|
||||||
|
local opts=()
|
||||||
|
command -v shellcheck >/dev/null 2>&1 || opts+=(-shellcheck=)
|
||||||
|
# Info and style findings from shellcheck (for example SC2086 on intentional word
|
||||||
|
# splitting) do not fail the gate. Warnings and errors do. Composite actions are
|
||||||
|
# not linted by actionlint 1.7; workflows are.
|
||||||
|
opts+=(-ignore 'shellcheck reported issue.+SC[0-9]+:(info|style):')
|
||||||
|
if ! "$bin" "${opts[@]}" -color .github/workflows/*.y*ml; then
|
||||||
|
problem "actionlint reported problems"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Resolves every `uses: owner/repo[/path]@ref` that points outside this repository.
|
||||||
|
check_refs() {
|
||||||
|
local files uses ref repo path checked=""
|
||||||
|
files=$(find .github -type f \( -name '*.yml' -o -name '*.yaml' \))
|
||||||
|
uses=$(grep -hoE '^[[:space:]]*(-[[:space:]]+)?uses:[[:space:]]*[^[:space:]#]+' $files | sed -E 's/^[[:space:]]*(-[[:space:]]+)?uses:[[:space:]]*//' | sort -u)
|
||||||
|
while IFS= read -r u; do
|
||||||
|
[ -n "$u" ] || continue
|
||||||
|
case "$u" in ./* | docker://*) continue ;; esac
|
||||||
|
repo=${u%%@*}
|
||||||
|
ref=${u#*@}
|
||||||
|
[ "$repo" != "$u" ] || { problem "no @ref in 'uses: $u'"; continue; }
|
||||||
|
# owner/repo/sub/path -> owner/repo
|
||||||
|
path=$(echo "$repo" | cut -d/ -f1,2)
|
||||||
|
# This repository's own composite actions are tracked at @main by design.
|
||||||
|
[ "$path" = lukaszraczylo/shared-actions ] && continue
|
||||||
|
case " $checked " in *" $path@$ref "*) continue ;; esac
|
||||||
|
checked="$checked $path@$ref"
|
||||||
|
if ! git ls-remote --exit-code "https://github.com/$path" "refs/tags/$ref" "refs/heads/$ref" >/dev/null 2>&1; then
|
||||||
|
# A full commit SHA is not listed by ls-remote; accept it when the repository answers at all.
|
||||||
|
if echo "$ref" | grep -qE '^[0-9a-f]{40}$' && git ls-remote --exit-code "https://github.com/$path" HEAD >/dev/null 2>&1; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
problem "'uses: $u' does not resolve: no tag or branch $ref in $path"
|
||||||
|
fi
|
||||||
|
done <<<"$uses"
|
||||||
|
}
|
||||||
|
|
||||||
|
rm -f "$marker"
|
||||||
|
run_actionlint
|
||||||
|
check_refs
|
||||||
|
|
||||||
|
if [ "$fail" -ne 0 ]; then
|
||||||
|
if [ "$mode" = --renovate ]; then
|
||||||
|
echo "validation failed; writing $marker" >&2
|
||||||
|
printf 'actions validation failed\n' >"$marker"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "actions validation passed"
|
||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
// Package selftest is a fixture that the validate workflow runs the go-test action against.
|
||||||
|
package selftest
|
||||||
|
|
||||||
|
// Add returns the sum of a and b.
|
||||||
|
func Add(a, b int) int { return a + b }
|
||||||
Vendored
+9
@@ -0,0 +1,9 @@
|
|||||||
|
package selftest
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestAdd(t *testing.T) {
|
||||||
|
if got := Add(2, 3); got != 5 {
|
||||||
|
t.Fatalf("Add(2, 3) = %d, want 5", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
module example.com/selftest
|
||||||
|
|
||||||
|
go 1.24
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "selftest",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "selftest",
|
||||||
|
"version": "0.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+10
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"name": "selftest",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"lint": "node -e \"process.exit(0)\"",
|
||||||
|
"test": "node --test",
|
||||||
|
"build": "node -e \"console.log('built')\""
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert');
|
||||||
|
|
||||||
|
test('addition', () => {
|
||||||
|
assert.strictEqual(2 + 3, 5);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user