Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
54d06b9
docs: add GitLab integration RFC
naokihaba Jun 28, 2026
024fde0
feat: add GitLab runtime template and bootstrap flow
naokihaba Jun 28, 2026
f222ba2
chore: include gitlab runtime tests in test config
naokihaba Jun 28, 2026
894abf1
docs: document GitLab integration usage and rollout details
naokihaba Jun 28, 2026
155cbb7
feat: add GitLab template bootstrap and documentation page
naokihaba Jun 28, 2026
277acaf
chore: remove generated GitLab runtime explanation page
naokihaba Jun 28, 2026
a2cc7ce
fix: align GitLab runtime bootstrap behavior
naokihaba Jun 28, 2026
ea2c661
refactor: compile GitLab runtime from TypeScript
naokihaba Jun 29, 2026
45e42bf
docs: document GitLab runtime architecture
naokihaba Jun 29, 2026
23b4700
fix: parse GitLab block YAML install args
naokihaba Jun 29, 2026
1b51955
Merge branch 'main' into main
naokihaba Jun 29, 2026
74479b7
fix: harden GitLab runtime parsing and downloads
naokihaba Jun 30, 2026
e666617
Merge branch 'main' into main
naokihaba Jun 30, 2026
1b922ca
fix: address GitLab review feedback
naokihaba Jun 30, 2026
c12d0e9
Merge remote-tracking branch 'upstream/main'
naokihaba Jul 1, 2026
12918b0
refactor: streamline temporary file handling in bootstrap script
naokihaba Jul 1, 2026
c789c15
refactor: enhance npmrc handling and improve downloadFile redirect logic
naokihaba Jul 1, 2026
84a0209
fix: use mktemp for bootstrap temporary file creation
naokihaba Jul 1, 2026
f5316dc
refactor: update import statements and improve temporary file handling
naokihaba Jul 1, 2026
8a8c877
refactor: enhance Vite+ installation logic and update description in …
naokihaba Jul 1, 2026
9cb7905
Merge remote-tracking branch 'upstream/main'
naokihaba Jul 4, 2026
4b0ba95
docs: add GitLab `before_script` usage instructions to README and set…
naokihaba Jul 4, 2026
88748cb
feat: update GitLab setup to use .setup-vp-bootstrap and add environm…
naokihaba Jul 4, 2026
2ed3691
build: regenerate dist bundle
naokihaba Jul 4, 2026
a05032c
feat: update setup-vp before_script to use heredoc for input variables
naokihaba Jul 4, 2026
a3251c9
fix: handle download failure in setup_vp_install_viteplus_from function
naokihaba Jul 4, 2026
1e6364f
feat: implement downloadFileWithShell function for curl and wget support
naokihaba Jul 4, 2026
4af9165
feat: add downloadFileWithShell function for curl and wget support
naokihaba Jul 4, 2026
64c3b91
fix: return 1 on download failure in setup_vp_install_viteplus_from f…
naokihaba Jul 4, 2026
79e3774
fix: prevent exposure of SETUP_VP_ENV_FILE to install subprocesses
naokihaba Jul 4, 2026
fa40c70
fix: remove SETUP_VP_ENV_FILE from environment in runInstall function
naokihaba Jul 4, 2026
361e74b
docs: update feature descriptions for clarity in README.md
naokihaba Jul 4, 2026
1eb8bfc
refactor(gitlab): simplify GitLab runtime and fix sfw version drift
fengmk2 Jul 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/renovate.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"customManagers": [
{
"customType": "regex",
"fileMatch": ["^src/install-sfw\\.ts$"],
"fileMatch": ["^src/install-sfw\\.ts$", "^src/gitlab/install-sfw\\.ts$"],
"matchStrings": ["const SFW_VERSION = \"(?<currentValue>v[^\"]+)\";"],
"depNameTemplate": "SocketDev/sfw-free",
"datasourceTemplate": "github-releases"
Expand Down
163 changes: 159 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
# setup-vp

GitHub Action to set up [Vite+](https://viteplus.dev) (`vp`) with dependency caching support.
GitHub Action and GitLab CI/CD remote template to set up [Vite+](https://viteplus.dev) (`vp`).

## Features

- Install Vite+ globally via official install scripts
- Optionally set up a specific Node.js version via `vp env use`
- Cache project dependencies with auto-detection of lock files
- GitHub Action: optionally set up a specific Node.js version via `vp env use`
- GitHub Action: cache project dependencies with auto-detection of lock files
- Optionally run `vp install` after setup
- Optionally wrap `vp install` with [Socket Firewall Free (`sfw`)](https://docs.socket.dev/docs/socket-firewall-free) to block malicious dependencies
- Support for all major package managers (npm, pnpm, yarn, bun)
- GitLab CI/CD support through a reusable `include:remote` template

## Usage

Expand Down Expand Up @@ -339,6 +340,160 @@ When `working-directory` is set, lockfile auto-detection runs in that directory.

When `cache-dependency-path` points to a lock file in a subdirectory, the action resolves the package-manager cache directory from that lock file's directory.

## GitLab CI/CD

setup-vp also provides a GitLab CI/CD remote template hosted from this GitHub repository. Because this repository is not a GitLab CI/CD component project, GitLab users should load it with `include:remote` instead of `include:component`.

See [GitLab integration notes](rfcs/gitlab-integration.md) for the design background, constraints, and follow-up work.

### Basic GitLab Usage

```yaml
include:
- remote: "https://raw.githubusercontent.com/voidzero-dev/setup-vp/v1/gitlab/setup-vp.yml"

test:
extends: .setup-vp
image: node:24
script:
- vp run test
```

### With GitLab Inputs

```yaml
include:
- remote: "https://raw.githubusercontent.com/voidzero-dev/setup-vp/v1/gitlab/setup-vp.yml"
inputs:
version: "latest"
working-directory: "web"
run-install: "true"

test:
extends: .setup-vp
image: node:24
script:
- vp run test
```

### With Pinned GitLab Runtime

When using an immutable tag or commit SHA, pin `setup-ref` to the same ref so the bootstrap and compiled runtime are downloaded from the same version as the included template:

```yaml
include:
- remote: "https://raw.githubusercontent.com/voidzero-dev/setup-vp/v1.0.0/gitlab/setup-vp.yml"
inputs:
setup-ref: "v1.0.0"

test:
extends: .setup-vp
image: node:24
script:
- vp run test
```

### With Existing GitLab `before_script`

GitLab replaces array keywords such as `before_script` when a job uses `extends`; it does not append them. If the job already needs setup commands, reference `.setup-vp-bootstrap` explicitly before the job-specific commands and configure setup-vp with variables:

```yaml
include:
- remote: "https://raw.githubusercontent.com/voidzero-dev/setup-vp/v1/gitlab/setup-vp.yml"

test:
image: node:24
variables:
SETUP_VP_VERSION: "latest"
SETUP_VP_RUN_INSTALL: "true"
SETUP_VP_SETUP_REF: "v1"
before_script:
- !reference [.setup-vp-bootstrap, before_script]
- npm config set //registry.example.com/:_authToken "$NODE_AUTH_TOKEN"
- corepack enable
script:
- vp run test
```

Use the same pattern when the project has `default:before_script`; put the shared setup commands in each job that needs them instead of relying on `.setup-vp` to append to the default array. The bootstrap variables match the GitLab inputs with `SETUP_VP_` prefixes, for example `SETUP_VP_WORKING_DIRECTORY`, `SETUP_VP_SFW`, `SETUP_VP_REGISTRY_URL`, and `SETUP_VP_SCOPE`.

### Advanced GitLab Run Install

```yaml
include:
- remote: "https://raw.githubusercontent.com/voidzero-dev/setup-vp/v1/gitlab/setup-vp.yml"
inputs:
run-install: |
- cwd: ./packages/app
args: ['--frozen-lockfile']
- cwd: ./packages/lib

test:
extends: .setup-vp
image: node:24
script:
- vp run test
```

### With GitLab Socket Firewall Free (sfw)

```yaml
include:
- remote: "https://raw.githubusercontent.com/voidzero-dev/setup-vp/v1/gitlab/setup-vp.yml"
inputs:
sfw: true
run-install: "true"

test:
extends: .setup-vp
image: node:24
script:
- vp run test
```

### With Private Registry

Pass `NODE_AUTH_TOKEN` as a GitLab CI/CD variable and set `registry-url` when the job needs an authenticated npm registry:

```yaml
include:
- remote: "https://raw.githubusercontent.com/voidzero-dev/setup-vp/v1/gitlab/setup-vp.yml"
inputs:
registry-url: "https://npm.pkg.github.com"
scope: "@myorg"

test:
extends: .setup-vp
image: node:24
variables:
NODE_AUTH_TOKEN: "$NPM_TOKEN"
script:
- vp run test
```

### GitLab Inputs

| Input | Description | Default |
| ------------------- | --------------------------------------------------------------------------------------------------------- | -------- |
| `version` | Version of Vite+ to install | `latest` |
| `working-directory` | Project directory used for relative paths and default `vp install` execution | `.` |
| `run-install` | String input for `vp install` after setup. Use `"true"`/`"false"` or a YAML object/list with `cwd`/`args` | `true` |
| `sfw` | Wrap `vp install` with [Socket Firewall Free](https://docs.socket.dev/docs/socket-firewall-free) | `false` |
| `registry-url` | Optional registry URL to write to a temporary `.npmrc` | |
| `scope` | Optional scope for authenticating against scoped registries | |
| `setup-ref` | setup-vp ref used to download the GitLab bootstrap and compiled runtime | `v1` |

### GitLab Notes

- Use a tag such as `v1` or `v1.0.0` in the remote URL instead of `main`.
- Pin `setup-ref` to the same tag or commit SHA as the remote URL when strict reproducibility is required.
- Quote GitLab string inputs such as `run-install: "false"`; unquoted booleans are rejected by GitLab before the setup runtime can parse them.
- GitLab 17.9+ users can add `integrity` to pin the remote file hash.
- The template expects a Unix-like runner image with Node.js, `bash`, and either `curl` or `wget`.
- The GitLab runtime source is TypeScript under `src/gitlab/`, but the template downloads and runs the `vp pack` generated JavaScript bundle from `dist/gitlab/index.mjs`.
- The GitLab template does not set up Node.js. Use a Node image such as `node:24`, or install Node.js before extending `.setup-vp`.
- The GitLab template intentionally does not expose `cache` or `cache-dependency-path` inputs. GitLab restores job cache before `before_script`, so this template cannot compute cache paths during setup and restore them for the same job. Configure GitLab `cache:` directly on the job when needed.

## Example Workflow

```yaml
Expand Down Expand Up @@ -395,7 +550,7 @@ vp install
### Before Committing

- Run `vp run check:fix` and `vp run build`
- The `dist/index.mjs` must be committed (it's the compiled action entry point)
- Generated files under `dist/` must be committed, including `dist/index.mjs` for the GitHub Action and `dist/gitlab/index.mjs` for the GitLab template
- Pre-commit hooks (via husky + lint-staged) will automatically run `vp check --fix` on staged files via `vpx lint-staged`

### Releasing
Expand Down
1 change: 1 addition & 0 deletions dist/gitlab/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import{get as e}from"node:http";import{pathToFileURL as t}from"node:url";import n from"node:path";import{createWriteStream as r,existsSync as i,mkdtempSync as a,statSync as o,writeFileSync as s}from"node:fs";import{tmpdir as c}from"node:os";import{get as l}from"node:https";import{spawnSync as u}from"node:child_process";import{chmod as d,mkdtemp as f}from"node:fs/promises";function shellQuote(e){return`'${String(e).replaceAll(`'`,`'\\''`)}'`}function exportShellEnv(e,t,n=process.env){!n.SETUP_VP_ENV_FILE||t===void 0||s(n.SETUP_VP_ENV_FILE,`export ${e}=${shellQuote(t)}\n`,{encoding:`utf8`,flag:`a`})}function run(e,t,n={}){let r=u(e,t,{stdio:`inherit`,...n});if(r.error)throw r.error;r.status!==0&&process.exit(r.status??1)}function commandPath(e){let t=u(`sh`,[`-c`,`command -v "$1"`,`sh`,e],{encoding:`utf8`});if(t.status===0)return t.stdout.trim()}function configureAuth(e,t,r=process.env){if(!e)return;let i;try{i=new URL(e)}catch{throw Error(`Invalid registry-url: "${e}". Must be a valid URL.`)}let o=i.href.endsWith(`/`)?i.href:`${i.href}/`,l=``;t&&(l=`${(t.startsWith(`@`)?t:`@${t}`).toLowerCase()}:`);let u=o.replace(/^\w+:/,``).toLowerCase(),d=a(n.join(c(),`setup-vp-npmrc-`)),f=n.join(d,`.npmrc`);return s(f,`${u}:_authToken=\${NODE_AUTH_TOKEN}\n${l}registry=${o}\n`,{encoding:`utf8`,mode:384}),r.NPM_CONFIG_USERCONFIG=f,r.PNPM_CONFIG_USERCONFIG=f,r.NODE_AUTH_TOKEN=r.NODE_AUTH_TOKEN||`XXXXX-XXXXX-XXXXX-XXXXX`,r===process.env&&(exportShellEnv(`NPM_CONFIG_USERCONFIG`,r.NPM_CONFIG_USERCONFIG,r),exportShellEnv(`PNPM_CONFIG_USERCONFIG`,r.PNPM_CONFIG_USERCONFIG,r),exportShellEnv(`NODE_AUTH_TOKEN`,r.NODE_AUTH_TOKEN,r)),f}const p=`v1.12.0`,m=`https://github.com/SocketDev/sfw-free/releases/download/${p}`;function isMuslLinux(){if(process.platform!==`linux`)return!1;try{let e=process.report?.getReport();if(e?.header&&!e.header.glibcVersionRuntime)return!0}catch{}return i(`/etc/alpine-release`)}function getSfwAssetName(e,t,n){if(e===`darwin`){if(t===`x64`)return`sfw-free-macos-x86_64`;if(t===`arm64`)return`sfw-free-macos-arm64`}if(e===`linux`){if(t===`x64`)return n?`sfw-free-musl-linux-x86_64`:`sfw-free-linux-x86_64`;if(t===`arm64`)return n?`sfw-free-musl-linux-arm64`:`sfw-free-linux-arm64`}throw Error(`Unsupported platform/arch for sfw: ${e}/${t}${e===`linux`?` (${n?`musl`:`glibc`})`:``}`)}function downloadFileWithShell(e,t,n){let r=Math.max(1,Math.ceil(n/1e3)),i=commandPath(`curl`);if(i){let n=u(i,[`-fsSL`,`--connect-timeout`,`5`,`--max-time`,String(r),e,`-o`,t],{stdio:`ignore`});if(n.status===0)return;throw n.error||Error(`curl failed while downloading ${e}`)}let a=commandPath(`wget`);if(a){let n=u(a,[`-q`,`-T`,String(r),`-t`,`2`,`-O`,t,e],{stdio:`ignore`});if(n.status===0)return;throw n.error||Error(`wget failed while downloading ${e}`)}throw Error(`curl or wget is required to download sfw`)}function downloadFile(t,n,i=0,a=6e4,o){if(i>5)return Promise.reject(Error(`too many redirects while downloading ${t}`));if(!o)return downloadFileWithShell(t,n,a),Promise.resolve();let s=o||(t.startsWith(`https:`)?l:e);return new Promise((e,c)=>{let l=!1,finish=t=>{l||(l=!0,clearTimeout(d),t?c(t):e())},u=s(t,e=>{let s=e.statusCode??0,c=e.headers.location;if(s>=300&&s<400&&c){e.resume(),downloadFile(new URL(c,t).toString(),n,i+1,a,o).then(()=>finish(),finish);return}if(s!==200){e.resume(),finish(Error(`download failed with HTTP ${s}: ${t}`));return}let l=r(n);e.pipe(l),l.on(`finish`,()=>l.close(()=>finish())),l.on(`error`,finish)}),d=setTimeout(()=>{u.destroy(Error(`download timed out after ${a}ms: ${t}`))},a);u.on(`error`,finish)})}async function setupSfw(e,t=process.env){if(t.SETUP_VP_SFW!==`true`)return`vp`;if(e.length===0)return console.log(`setup-vp: sfw was requested but run-install is disabled; sfw will not be invoked.`),`vp`;let r=commandPath(`sfw`);if(r)return console.log(`setup-vp: using existing sfw on PATH: ${r}`),`sfw`;let i=isMuslLinux(),a;try{a=getSfwAssetName(process.platform,process.arch,i)}catch{a=void 0}if(!a)return console.error(`setup-vp: sfw has no published binary for this runner's platform/architecture (process.platform=${process.platform}, process.arch=${process.arch}, musl=${i}) and none was found on PATH; falling back to plain vp install.`),`vp`;let o=await f(n.join(c(),`setup-vp-sfw-`)),s=n.join(o,`sfw`),l=`${m}/${a}`;for(let e=1;e<=2;e+=1)try{return console.log(`setup-vp: installing sfw ${p} from ${l}`),await downloadFile(l,s),await d(s,493),t.PATH=`${o}:${t.PATH||``}`,exportShellEnv(`PATH`,t.PATH,t),`sfw`}catch(t){if(e===2)throw t;await new Promise(e=>setTimeout(e,2e3))}throw Error(`failed to install sfw after retrying`)}function parseScalar(e){let t=String(e||``).trim();return t.startsWith(`"`)&&t.endsWith(`"`)||t.startsWith(`'`)&&t.endsWith(`'`)?t.slice(1,-1):t}function parseFlowArray(e){let t=String(e||``).trim();if(!t.startsWith(`[`)||!t.endsWith(`]`))throw Error(`args must be an array, got: ${e}`);let n=t.slice(1,-1).trim();if(!n)return[];let r=[],i=``,a=``,o=!1,pushCurrent=()=>{if(!i.trim())throw Error(`args flow array entries must be non-empty strings`);r.push(parseScalar(i)),i=``,o=!0};for(let e of n){if(a){e===a&&(a=``),i+=e;continue}if(e===`'`||e===`"`){a=e,i+=e;continue}if(e===`,`){pushCurrent();continue}i+=e}if(a)throw Error(`unterminated quoted string in args flow array`);if(i.trim())pushCurrent();else if(!o)throw Error(`args flow array entries must be non-empty strings`);return r}function parseKeyValue(e){let t=e.indexOf(`:`);if(!(t<0))return[e.slice(0,t).trim(),e.slice(t+1).trim()]}function countIndent(e){return e.length-e.trimStart().length}function parseBlockArray(e,t,n){let r=[],i=t;for(;i<e.length;){let t=e[i],a=countIndent(t),o=t.trimStart();if(a<=n)break;if(!o.startsWith(`-`))throw Error(`invalid args line: ${t}`);let s=o.slice(1).trim();if(!s)throw Error(`args entries must be strings: ${t}`);r.push(parseScalar(s)),i+=1}if(r.length===0)throw Error(`args must be an array`);return{values:r,nextIndex:i}}function assignValue(e,t,n){if(t===`cwd`)return e.cwd=parseScalar(n),!1;if(t===`args`)return n?(e.args=parseFlowArray(n),!1):!0;throw Error(`unsupported run-install key: ${t}`)}function isRecord(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function validateRunInstallEntry(e){if(!isRecord(e))throw Error(`run-install entries must be objects`);for(let t of Object.keys(e))if(t!==`cwd`&&t!==`args`)throw Error(`unsupported run-install key: ${t}`);let t={};if(e.cwd!==void 0){if(typeof e.cwd!=`string`)throw Error(`run-install.cwd must be a string`);t.cwd=e.cwd}if(e.args!==void 0){if(!Array.isArray(e.args)||e.args.some(e=>typeof e!=`string`))throw Error(`run-install.args must be an array of strings`);t.args=e.args}return t}function validateRunInstallInput(e){return e===null||typeof e==`boolean`?e:Array.isArray(e)?e.map(validateRunInstallEntry):validateRunInstallEntry(e)}function applyEntryLine(e,t,n,r,i,a){let o=parseKeyValue(t);if(!o)throw Error(`invalid run-install line: ${a}`);if(assignValue(e,o[0],o[1])){let t=parseBlockArray(n,r+1,i);return e.args=t.values,t.nextIndex-1}return r}function parseObject(e){let t={};for(let n=0;n<e.length;n+=1){let r=e[n],i=r.trim();!i||i.startsWith(`#`)||(n=applyEntryLine(t,i,e,n,countIndent(r),r))}return t}function parseYamlSubset(e){let t=e.split(/\r?\n/).filter(e=>e.trim()&&!e.trim().startsWith(`#`));if(t.length===0)return[];if(!t[0].trimStart().startsWith(`-`))return[parseObject(t)];let n=countIndent(t[0]),r=[],i;for(let e=0;e<t.length;e+=1){let a=t[e],o=countIndent(a),s=a.trimStart();if(o===n&&s.startsWith(`-`)){i&&r.push(i),i={};let n=s.slice(1).trim();n&&(e=applyEntryLine(i,n,t,e,o,a));continue}if(!i)throw Error(`invalid run-install line: ${a}`);e=applyEntryLine(i,s,t,e,o,a)}return i&&r.push(i),r}function parseRunInstall(e){let t=String(e||``).trim();return t?normalizeRunInstallInput(parseRunInstallInput(t)):[]}function parseRunInstallInput(e){try{return validateRunInstallInput(JSON.parse(e))}catch(e){if(!(e instanceof SyntaxError))throw formatRunInstallError(e)}try{return validateRunInstallInput(parseYamlSubset(e))}catch(e){throw formatRunInstallError(e)}}function normalizeRunInstallInput(e){return e?e===!0?[{}]:Array.isArray(e)?e:[e]:[]}function formatRunInstallError(e){return e instanceof Error?e:Error(String(e))}function runInstall(e,t,r){let i={...process.env};delete i.SETUP_VP_ENV_FILE;for(let a of e){let e=a.cwd?n.resolve(t,a.cwd):t,o=[`install`,...a.args||[]],s=r===`sfw`?[`vp`,...o]:o;console.log(`setup-vp: running ${r} ${s.join(` `)} in ${e}`),run(r,s,{cwd:e,env:i})}}function resolveProjectDir(e=process.env){let t=e.SETUP_VP_WORKING_DIRECTORY||`.`,r=n.isAbsolute(t)?t:n.join(e.CI_PROJECT_DIR||process.cwd(),t);try{if(!o(r).isDirectory())throw Error(`working-directory is not a directory: ${t} (resolved to ${r})`)}catch(e){throw e instanceof Error&&`code`in e&&e.code===`ENOENT`?Error(`working-directory not found: ${t} (resolved to ${r})`):e}return r}function fail(e){console.error(`setup-vp: ${e}`),process.exit(1)}async function main(){let e=resolveProjectDir(process.env);configureAuth(process.env.SETUP_VP_REGISTRY_URL||``,process.env.SETUP_VP_SCOPE||``);let t=parseRunInstall(process.env.SETUP_VP_RUN_INSTALL||`true`);runInstall(t,e,await setupSfw(t)),run(`vp`,[`--version`])}function isEntrypoint(e=process.argv[1],r=import.meta.url){return!!(e&&r===t(n.resolve(e)).href)}if(isEntrypoint())try{await main()}catch(e){fail(e instanceof Error?e.message:String(e))}export{isEntrypoint,main};
Loading
Loading