Reusable GitHub Actions workflows for common CI/CD tasks.
Builds a Vite application and deploys it to GitHub Pages.
Usage:
name: Deploy to GitHub Pages
on:
push:
branches:
- main
workflow_dispatch:
# IMPORTANT: Define permissions and concurrency at the workflow level
# The reusable workflow inherits these settings
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
build-and-deploy:
# SECURITY: pin to a full commit SHA, not @main (see "Supply chain hardening" below).
# Resolve the SHA for a release with:
# gh api repos/agusgonzaleznic/github-reusable-workflows/commits/v1.0.0 --jq .sha
uses: agusgonzaleznic/github-reusable-workflows/.github/workflows/vite-build-deploy.yml@<full-commit-sha> # v1.0.0
# Pass permissions to the job
permissions:
contents: read
pages: write
id-token: write
with:
node-version: '22' # Optional, default: '22'
install-command: 'npm ci' # Optional, default: 'npm ci'
lint-command: 'npm run lint' # Optional, default: 'npm run lint'
build-command: 'npm run build' # Optional, default: 'npm run build'
artifact-path: 'dist' # Optional, default: 'dist'
run-lint: true # Optional, default: trueInputs:
| Input | Description | Required | Default |
|---|---|---|---|
node-version |
Node.js version to use | No | '22' |
install-command |
Command to install dependencies | No | 'npm ci' |
lint-command |
Command to lint the project | No | 'npm run lint' |
build-command |
Command to build the project | No | 'npm run build' |
artifact-path |
Path to the build output directory (relative path without ./) |
No | 'dist' |
run-lint |
Whether to run linting | No | true |
Required Repository Settings:
- Go to Repository → Settings → Pages
- Under "Build and deployment" section:
- Source dropdown: Select "GitHub Actions"
- Do NOT use "Deploy from a branch" (this will use Jekyll and ignore your workflow)
- Click Save
Why this matters: If set to "Deploy from a branch", GitHub will try to build your site with Jekyll from the branch, completely ignoring your pre-built artifact. Your workflow will appear to succeed, but the deployed site will be incorrect or broken.
Runs linting and build checks for pull requests on Vite applications.
Usage:
name: CI
on:
pull_request:
branches:
- main
workflow_dispatch:
jobs:
ci:
# SECURITY: pin to a full commit SHA, not @main (see "Supply chain hardening" below).
# Resolve the SHA for a release with:
# gh api repos/agusgonzaleznic/github-reusable-workflows/commits/v1.0.0 --jq .sha
uses: agusgonzaleznic/github-reusable-workflows/.github/workflows/vite-ci.yml@<full-commit-sha> # v1.0.0
with:
node-version: '22' # Optional, default: '22'
install-command: 'npm ci' # Optional, default: 'npm ci'
lint-command: 'npm run lint' # Optional, default: 'npm run lint'
build-command: 'npm run build' # Optional, default: 'npm run build'
artifact-path: 'dist' # Optional, default: 'dist'
verify-build-output: true # Optional, default: trueInputs:
| Input | Description | Required | Default |
|---|---|---|---|
node-version |
Node.js version to use | No | '22' |
install-command |
Command to install dependencies | No | 'npm ci' |
lint-command |
Command to lint the project | No | 'npm run lint' |
build-command |
Command to build the project | No | 'npm run build' |
artifact-path |
Path to the build output directory | No | 'dist' |
verify-build-output |
Whether to verify build output exists | No | true |
- All workflows use npm by default, but can be customized via inputs
- Workflows support Node.js caching for faster builds
- Both workflows are framework-agnostic and can work with any Vite-based project
- Security: pin the reusable workflow to a full commit SHA, never
@main(see Supply chain hardening) - Important: Use relative paths without
./prefix forartifact-path(e.g.,'dist'not'./dist') to ensure proper artifact upload with GitHub Actions - Reusable Workflow Pattern: The reusable workflows do NOT define their own
concurrencygroups — those must be defined in the calling workflow to avoid deadlock conflicts. They DO declare minimal least-privilegepermissionsper job; the caller must grant at least those scopes.
These workflows follow GitHub Actions supply-chain best practices:
- All actions are pinned to full commit SHAs (with the version in a comment), not mutable tags. A mutable tag such as
@v6can be silently repointed by anyone who compromises the action's repository — as happened in thetj-actions/changed-filesincident (March 2025). A commit SHA is immutable. - Least-privilege
GITHUB_TOKEN: each job declares the minimumpermissionsit needs (contents: readto build;pages: write+id-token: writeonly on the deploy job). persist-credentials: falseon checkout so the token is not written to.git/config, where a malicious dependency in a later step could read it.- Data inputs are passed via
env:and quoted: values used as data inrun:blocks (e.g.artifact-path) go throughenv:rather than being interpolated directly, removing that shell-injection class. - Dependabot (
.github/dependabot.yml) opens weekly PRs to bump the pinned SHAs when upstream ships patches.
The install-command, lint-command, and build-command inputs are executed as shell commands (run: ${{ inputs.*-command }}) — that is their purpose. They cannot be env:-quoted without breaking that contract. Do not wire untrusted or event-derived data (e.g. a PR title, branch name, or github.event.*) into these inputs, or into the calling workflow that forwards them — doing so is command injection inside the reusable workflow. Pass only static, trusted command strings.
The build-env-vars secret is appended verbatim to $GITHUB_ENV and is available to every later step (including dependency build code). GitHub masks the exact full secret string in logs, but not each individual KEY=value line within a multiline secret — avoid echo-ing those values in your build.
Consumers pin to commit SHAs of this repository, so this repo's integrity is the root of their trust. Pair SHA pinning with:
- Branch protection on
main: require a PR, a CODEOWNER approval (see.github/CODEOWNERS), and passing status checks before merge. - Tag protection (or immutable releases) so a published version tag can't be silently repointed.
- Reviewing Dependabot PRs before merge rather than auto-merging.
Pinning the actions inside these workflows only protects half the chain. Any workflow that calls these reusable workflows must itself pin to a full commit SHA of this repository — referencing @main means a compromise of this repo would immediately execute in the caller with the caller's secrets.
# ❌ Not safe — tracks a mutable branch
uses: agusgonzaleznic/github-reusable-workflows/.github/workflows/vite-ci.yml@main
# ✅ Safe — immutable commit, human-readable version in the comment
uses: agusgonzaleznic/github-reusable-workflows/.github/workflows/vite-ci.yml@<full-commit-sha> # v1.0.0Resolve the SHA for a release tag with:
gh api repos/agusgonzaleznic/github-reusable-workflows/commits/v1.0.0 --jq .shaIf the GitHub Pages artifact isn't uploading correctly:
-
Check the workflow logs - The build verification step shows exactly what will be uploaded
-
Verify artifact structure - The artifact should contain your site files at the root, not in a subdirectory
-
Expected artifact contents:
index.html (at root) assets/ favicon.ico ...other files... -
NOT this structure:
dist/ (don't want this!) index.html assets/ -
Path format: Use
artifact-path: 'dist'without./prefix -
Verify build output: Check that
npm run buildcreates files in the expected directory -
Review workflow logs: Look for the "Build Output Verification" section
-
"Jekyll is building my site" or "Workflow succeeds but site is broken"
- Cause: Repository is set to "Deploy from a branch" instead of "GitHub Actions"
- Solution:
- Go to Repository → Settings → Pages
- Change Source from "Deploy from a branch" to "GitHub Actions"
- Re-run the workflow
- Why: When set to "Deploy from a branch", GitHub ignores your artifact and tries to build with Jekyll from the branch
- Verification: Check your workflow logs - if you see Jekyll-related output, your settings are wrong
-
"Deadlock was detected for concurrency group 'pages'"
- This happens when both the calling workflow and reusable workflow define the same concurrency group
- Solution: Only define
concurrencyin the calling workflow, not in the reusable workflow - The reusable workflow now correctly inherits concurrency settings from the caller
-
404 on deployment: Check that
index.htmlis at the root of the artifact, not in a subdirectory -
Assets not loading: Verify your
vite.config.tshas the correctbasepath- User/org pages (
username.github.io):base: '/' - Project pages (
username.github.io/repo):base: '/repo/'
- User/org pages (
When adding new reusable workflows, ensure:
- Use
workflow_callas the trigger - Provide sensible default values for all inputs
- Document all inputs and outputs
- Include usage examples in this README