This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
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:
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:
- 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`.
| `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`
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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};
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
feat: add GitLab CI/CD integration for setup-vp #97
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
feat: add GitLab CI/CD integration for setup-vp #97
Filter by extension
Viewed files
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
There are no files selected for viewing
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.