feat: add pnpm-install as a shared composite action

Lifts ptpa_pl's local .gitea/actions/pnpm-install into this repo, parametrised
enough for repositories we have not met yet: working-directory, install args,
pnpm version and an opt-in store cache.

Cache is off by default. ADR-0014 pins actions/cache to major 3 and no workflow
on this instance has exercised the cache server yet - a shared action that a
dozen jobs hang off is the wrong place to premiere that dependency.

Two traps found while testing against ptpa_pl's lockfile in node:24-bookworm-slim,
both avoided by pinning the store instead of reading `pnpm store path`:
that command defaults to <workspace>/.pnpm-store, inside the working tree where a
committing job could sweep it up, and it exits 0 while printing its error to
stdout, which would have fed actions/cache a garbage path silently.

The version input also carries COREPACK_ENABLE_PROJECT_SPEC=0, without which a
packageManager field outranks `corepack prepare --activate` and the input would
do nothing at all - no effect, no error.
This commit is contained in:
2026-08-10 23:32:51 +02:00
parent 2c9ebc0e04
commit e8cc842238
3 changed files with 180 additions and 0 deletions
+4
View File
@@ -9,6 +9,7 @@ przy pobieraniu akcji, patrz „Dostęp" niżej.
| Akcja | Do czego | | Akcja | Do czego |
| --- | --- | | --- | --- |
| [`pnpm-install`](pnpm-install/) | corepack + `pnpm install` z lockfile'a, opcjonalny cache store'a |
| [`resolve-buildx-cache`](resolve-buildx-cache/) | `DOCKER_CACHE_*` z runnera → `driver-opts` i refy cache'a dla buildx | | [`resolve-buildx-cache`](resolve-buildx-cache/) | `DOCKER_CACHE_*` z runnera → `driver-opts` i refy cache'a dla buildx |
## Jak używać ## Jak używać
@@ -47,6 +48,9 @@ Ustawienia nie ma w REST API — trzeba wyklikać.
Aktualnie nadane: `cierzniak-it`. Aktualnie nadane: `cierzniak-it`.
Do nadania przy najbliższej migracji: **`ptpa`** (`website`) — pierwszy konsument
`pnpm-install`, dziś bez dostępu.
Kandydaci do dodania, gdy akcja pojedzie szerzej (repo z buildxem w CI): Kandydaci do dodania, gdy akcja pojedzie szerzej (repo z buildxem w CI):
`leczenieran` (`wizytowka`, `elektroniczna-dokumentacja-medyczna`), `leczenieran` (`wizytowka`, `elektroniczna-dokumentacja-medyczna`),
`welding-technology-review` (`witryna-internetowa`), `ekoinbud` (`production-system`). `welding-technology-review` (`witryna-internetowa`), `ekoinbud` (`production-system`).
+98
View File
@@ -0,0 +1,98 @@
# pnpm-install
Aktywuje pnpm przez corepack i instaluje zależności z lockfile'a.
```yaml
- uses: actions/checkout@v7
- uses: https://git.cierzniak.it/gitea-runner/actions/pnpm-install@v1
- run: pnpm run lint:js
```
Podprojekt w monorepo, z cache'em store'a:
```yaml
- uses: https://git.cierzniak.it/gitea-runner/actions/pnpm-install@v1
with:
working-directory: ./wizard
cache: 'true'
```
`args` zastępuje **całą** listę flag, więc `--frozen-lockfile` trzeba powtórzyć:
```yaml
- uses: https://git.cierzniak.it/gitea-runner/actions/pnpm-install@v1
with:
args: --frozen-lockfile --ignore-scripts
```
## Wejście
| Input | Wymagany | Domyślnie | Co to |
| --- | --- | --- | --- |
| `working-directory` | nie | `.` | Katalog z `package.json` i `pnpm-lock.yaml` |
| `version` | nie | — | Wersja pnpm. Pusta bierze ją z pola `packageManager`; podana **nadpisuje to pole** |
| `args` | nie | `--frozen-lockfile` | Pełna lista argumentów `pnpm install` |
| `cache` | nie | `false` | `'true'` cache'uje store pnpm między jobami |
Outputów nie ma.
Node i corepack pochodzą z obrazu joba — akcja nie stawia żadnego z nich. Sprawdzone na
`git.cierzniak.it/gitea-runner/node:24` (Node 24.18.1, corepack 0.35.0).
## Dlaczego tak
**`args` to jeden input, nie `frozen-lockfile` plus `extra-args`.** Kto go podaje, przejmuje
całą listę flag. Dwa inputy wymagałyby reguły, co się dzieje przy `frozen-lockfile: true`
i `args: --no-frozen-lockfile` — tu takiego stanu nie ma.
**Wersja pnpm domyślnie z `packageManager`.** To pole jest już źródłem prawdy dla corepacka
lokalnie, a wpisanie jej drugi raz do workflow tworzy dwa źródła, które rozjeżdżają się po
pierwszym bumpie. Input `version` jest przede wszystkim dla repozytoriów bez tego pola — bez
niego samo `corepack enable` nie ma czego aktywować.
**`version` dokłada `COREPACK_ENABLE_PROJECT_SPEC=0`, bo inaczej nic by nie robił.** Samo
`corepack prepare pnpm@X --activate` ustawia tylko wersję *domyślną*, do której corepack sięga
przy braku `packageManager`; obecne pole ją przebija. Sprawdzone: w projekcie z
`packageManager: pnpm@9.15.9` po `prepare pnpm@10.20.0 --activate` dalej odpowiada `9.15.9`
(`COREPACK_ENABLE_STRICT=0` też nie pomaga — dopiero `COREPACK_ENABLE_PROJECT_SPEC=0` daje
`10.20.0`). Bez tego input byłby cichym no-opem: ustawiasz, nic się nie zmienia, zero błędu.
Cena jest taka, że przy podanym `version` pole `packageManager` przestaje obowiązywać —
dlatego zostawiaj go pustym wszędzie, gdzie to pole istnieje.
**Cache jest opt-in i domyślnie wyłączony.** [ADR-0014](https://git.cierzniak.it/gitea-runner/images/src/branch/main/docs/adr/0014-akcje-cache-i-artefaktow-na-v3.md)
trzyma `actions/cache` na majorze 3, bo od v4.2 akcja mówi protokołem cache service v2,
którego Gitea nie implementuje ([go-gitea#33393](https://github.com/go-gitea/gitea/issues/33393)).
Akcja wymaga też skonfigurowanego serwera cache'a — kontener joba i kontener runnera siedzą
w różnych przestrzeniach sieciowych, więc bez tego zwraca `ETIMEDOUT`. Runnery tej instancji
mają `cache.enabled: true` i `external_server`, ale **żaden workflow jeszcze z tego nie
korzystał**. Domyślnie wyłączony cache pozwala to sprawdzić na jednym repo, zamiast robić
premierę na wszystkich naraz.
**Klucz cache'a liczy `sha256sum`, nie `hashFiles()`.** `hashFiles` to funkcja natywna
GitHuba i jej zachowanie w `act_runner` byłoby kolejną niewiadomą dokładaną do i tak
niesprawdzonego tutaj `actions/cache`. Do tego sklejanie ścieżki z `working-directory` daje
wzorce w rodzaju `.//pnpm-lock.yaml`. `sha256sum` jest w każdym obrazie z
`gitea-runner/images` i widać w logu, co policzył. `runner.os` w kluczu idzie za wzorcem
oficjalnej `pnpm/action-setup`.
**Store jest przypięty do `~/.pnpm-store`, zamiast odczytany z `pnpm store path`.** Dwa
powody, oba sprawdzone w obrazie `node:24-bookworm-slim`:
1. Domyślnie store ląduje w **`<workspace>/.pnpm-store/v3`**, czyli w drzewie roboczym —
pnpm celowo szuka miejsca na tym samym systemie plików co projekt, a `/workspace` jest
bind-mountem. Job, który commituje (jak `check-pchia` w `ptpa_pl`), ma tam minę.
2. `pnpm store path` przy błędzie **kończy się kodem 0 i wypisuje błąd na stdout**
(`EROFS: read-only file system…`). `path=` w `GITHUB_OUTPUT` dostałoby wtedy śmieci,
a `actions/cache` cache'owałby nieistniejącą ścieżkę — po cichu, bez czerwonego joba.
Przestawia go wyłącznie `npm_config_store_dir`; **`PNPM_STORE_DIR` nie jest ustawieniem pnpm**
i nie działa. Zmienna leci przez `$GITHUB_ENV`, żeby dosięgła też kroku instalacji, i tylko
przy włączonym cache — przy `cache: false` rozmieszczenie store'a zostaje takie, jakie pnpm
wybiera dziś, więc obecni konsumenci nic nie odczuwają.
Cache obejmuje `~/.pnpm-store`, a nie podkatalog z wersją, bo ten zależy od majora pnpm —
sprawdzone: `v3` przy pnpm 9, `v10` przy pnpm 10.
**Akcja obsługuje wyłącznie pnpm.** Wariant wykrywający lockfile i wołający npm albo yarn
rozmyłby kontrakt na trzy ścieżki o różnych flagach, różnym modelu cache'a i różnych trybach
błędu — z czego dwie nikt by nie testował. Gdy npm będzie potrzebny, dostanie osobną akcję.
+78
View File
@@ -0,0 +1,78 @@
name: Install pnpm dependencies
description: >-
Activates pnpm through corepack and installs dependencies from the lockfile. The version comes
from the packageManager field unless overridden; the store cache is opt-in.
inputs:
working-directory:
description: Directory holding package.json and pnpm-lock.yaml.
required: false
default: '.'
version:
description: >-
pnpm version. Empty takes it from the packageManager field; a value overrides that field
and stops it from being enforced.
required: false
default: ''
args:
description: Full argument list for `pnpm install`; setting it drops the default --frozen-lockfile.
required: false
default: --frozen-lockfile
cache:
description: Set to "true" to cache the pnpm store between jobs.
required: false
default: 'false'
runs:
using: composite
steps:
- name: Activate pnpm
shell: bash
working-directory: ${{ inputs.working-directory }}
env:
COREPACK_ENABLE_DOWNLOAD_PROMPT: '0'
VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
corepack enable
if [ -n "${VERSION}" ]; then
corepack prepare "pnpm@${VERSION}" --activate
# `prepare --activate` only sets the default corepack falls back to; a packageManager
# field outranks it, so without this the input would silently do nothing on every repo
# that has one. Written to GITHUB_ENV so the install step sees it too.
echo "COREPACK_ENABLE_PROJECT_SPEC=0" >> "$GITHUB_ENV"
fi
# The store is pinned instead of read back from `pnpm store path`, because that command
# defaults to <workspace>/.pnpm-store - inside the working tree, where a job that commits
# could sweep it up - and because it exits 0 while printing its error to stdout, which would
# silently feed garbage to actions/cache. Only npm_config_store_dir moves it; PNPM_STORE_DIR
# is not a pnpm setting. Set here rather than as a step env so it also reaches the install
# step, and only when caching is on, so the default path keeps pnpm's own store placement.
- name: Pin store and compute cache key
id: store
if: inputs.cache == 'true'
shell: bash
working-directory: ${{ inputs.working-directory }}
run: |
set -euo pipefail
echo "npm_config_store_dir=${HOME}/.pnpm-store" >> "$GITHUB_ENV"
echo "key=$(sha256sum pnpm-lock.yaml | cut -d ' ' -f 1)" >> "$GITHUB_OUTPUT"
# actions/cache stays on major 3: from v4.2 it speaks a cache service protocol Gitea does
# not implement. Read ADR-0014 in gitea-runner/images before letting Renovate bump this.
- name: Cache pnpm store
if: inputs.cache == 'true'
uses: actions/cache@v3
with:
path: ~/.pnpm-store
key: pnpm-store-${{ runner.os }}-${{ steps.store.outputs.key }}
restore-keys: pnpm-store-${{ runner.os }}-
- name: Install dependencies
shell: bash
working-directory: ${{ inputs.working-directory }}
env:
ARGS: ${{ inputs.args }}
# ARGS stays unquoted on purpose - it carries a list of flags, not a single argument.
run: pnpm install ${ARGS}