chore: import upstream snapshot with attribution
Deploy local.promptfoo.app / Deploy to Cloudflare Pages (push) Waiting to run
Test and Publish Multi-arch Docker Image / test (push) Waiting to run
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-amd64 platform:linux/amd64 runner:ubuntu-latest]) (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-arm64 platform:linux/arm64 runner:ubuntu-24.04-arm]) (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / merge-docker-digests (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / Attest Multi-arch Image (push) Blocked by required conditions
Validate Renovate Config / Validate Renovate Configuration (push) Waiting to run
CI / Shell Format Check (push) Has been cancelled
CI / Check Ruby (3.4) (push) Has been cancelled
CI / CI Config (push) Has been cancelled
CI / Test on Node ${{ matrix.node }} and ${{ matrix.os }}${{ matrix.shard && format(' (shard {0}/3)', matrix.shard) || '' }} (push) Has been cancelled
CI / Build on Node ${{ matrix.node }} (push) Has been cancelled
CI / Style Check (push) Has been cancelled
CI / Generate Assets (push) Has been cancelled
CI / Check Python (3.14) (push) Has been cancelled
CI / Check Python (3.9) (push) Has been cancelled
CI / Build Docs (push) Has been cancelled
CI / Code Scan Action (push) Has been cancelled
CI / Site tests (push) Has been cancelled
CI / webui tests (push) Has been cancelled
CI / Run Integration Tests (push) Has been cancelled
CI / Run Smoke Tests (push) Has been cancelled
CI / Go Tests (push) Has been cancelled
CI / Share Test (push) Has been cancelled
CI / Redteam (Production API) (push) Has been cancelled
CI / Redteam (Staging API) (push) Has been cancelled
CI / GitHub Actions Lint (push) Has been cancelled
CI / Check Ruby (3.0) (push) Has been cancelled
release-please / release-please (push) Has been cancelled
release-please / build (push) Has been cancelled
release-please / publish-npm (push) Has been cancelled
release-please / publish-npm-backfill (push) Has been cancelled
release-please / docker (push) Has been cancelled
release-please / publish-code-scan-action (push) Has been cancelled
release-please / attest-code-scan-action (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:24:08 +08:00
commit 0d3cb498a3
5438 changed files with 1316560 additions and 0 deletions
+27
View File
@@ -0,0 +1,27 @@
# AGENTS.md
Guidance for AI agents working on GitHub Actions workflows.
## Workflow Security
- Keep actions SHA-pinned. Never use floating tags (`@v4`) or branches (`@main`).
- **Do not expose write-capable tokens to third-party or PR-controlled code.** If any step runs untrusted code (`npm install`, `npx <tool>`, a formatter, a build tool), that step's `env:` must not contain `GH_TOKEN`, `GITHUB_TOKEN`, `NODE_AUTH_TOKEN`, or equivalent. Split the job into a tokenless do-the-work phase and a credentialed push/API phase.
- **Mint credentials after — not before — running PR-controlled code.** In any job that both runs PR-controlled code (`npm install`, `npx <tool>`, formatters, build tools) _and_ mints an App token, place `actions/create-github-app-token` in a later step gated by `if:` so the token does not exist in the workflow context during untrusted execution. If that isn't feasible (e.g., `googleapis/release-please-action` needs the token at entry to open the PR), split into two jobs: a tokenless do-the-work job that produces artifacts, and a credentialed push job that consumes them — and do not run other untrusted code in the credentialed job.
- **Disable git hooks for every credentialed git invocation.** `git` executes `.git/hooks/*` by default, and a PR can plant `pre-push` / `pre-commit` hooks that run with the surrounding step's env. Use `git -c core.hooksPath=/dev/null commit …` and `git -c core.hooksPath=/dev/null push …` whenever a hook would otherwise run under a step that holds a credential.
- **Treat PR-controlled formatter/tool config as untrusted code.** `.prettierrc.js`, `.eslint.config.js`, `.babelrc.js`, and many others are JavaScript and will execute. Run formatters with `--no-config --no-editorconfig` (or equivalent) so PR-controlled config is ignored. For `npm install`, always pass `--registry=https://registry.npmjs.org/` explicitly so a PR-controlled `.npmrc` cannot redirect the install, and prefer installing into an isolated directory (e.g., `$RUNNER_TEMP/tool-install`) _before_ checking out the PR tree.
- **Use `persist-credentials: false` on `actions/checkout`** when the workflow subsequently runs code or installs packages on a PR branch. Otherwise `.git/config` holds the token and any code can read it. Reattach credentials just-in-time on the specific git command with GitHub Git smart HTTP's Basic auth header, e.g. compute `basic_auth="$(printf 'x-access-token:%s' "${GH_TOKEN}" | base64 | tr -d '\n')"` and pass `git -c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${basic_auth}" …`.
- **Verify the commit scope before pushing.** When a formatter commits on your behalf, assert `git diff-tree --no-commit-id --name-only -r HEAD` contains only the expected paths before the push step runs.
- **Treat PR fields as untrusted input.** Branch names, titles, labels, file paths, etc. may contain shell metacharacters. Assign them to env vars (e.g., `HEAD_REF: ${{ github.event.pull_request.head.ref }}`) before referencing in `run:` — never interpolate `${{ … }}` directly into a shell command. actionlint enforces this.
- **Avoid `pull_request_target`** unless the workflow does not check out or execute PR-controlled code. Prefer `pull_request` with narrow permissions and `persist-credentials: false`.
- **Gate privileged workflows tighter than a branch-name prefix.** Require same-repo (`head.repo.full_name == github.repository`), the expected bot author (`user.type == 'Bot'` or a specific `user.login`), and only then the branch-naming convention.
- Set job-level `permissions: contents: read` by default; narrow GITHUB_TOKEN so write is only available via a separately-issued App token scoped to a single step.
- Add `timeout-minutes` to every job.
## Release Workflows
- Release/publish workflows must use workflow-level `concurrency` with `cancel-in-progress: false` so two pushes to `main` serialize rather than race a publish.
- Do not run CHANGELOG formatting inside `release-please.yml`. Release-please force-pushes its PR branch when new commits land on `main`, which wipes any formatter commit created inline in the release-please job. Keep release PR formatting in a separate `pull_request`-triggered workflow that self-heals after every force-push — and apply all the Workflow Security rules above, since the PR body/branch is user-controllable.
- For npm trusted publishing, set `permissions: id-token: write` on the publish job and do not configure long-lived npm tokens. When `actions/setup-node` is configured with `registry-url`, it writes a `${NODE_AUTH_TOKEN}` placeholder into `.npmrc` that prevents OIDC fallthrough — set `NODE_AUTH_TOKEN: ''` on the publish step to neutralize it (see [actions/setup-node#1440](https://github.com/actions/setup-node/issues/1440)).
- Pin exact versions for any tooling installed at workflow runtime (e.g., `prettier@3.8.1`, not `prettier@^3.8.1`; `npm install -g npm@11.11.0`, not `@latest`). Semver ranges mean a future compromised patch release can alter published artifacts.
- Least-privilege every job. `publish-npm` does not need `packages: write`; only Docker jobs pushing to ghcr.io do.
- **`actions/create-github-app-token` `permission-*` inputs are a subset-request, not a grant.** GitHub returns 422 if you request a permission the App installation was not already granted — even a permission release-please-action appears to use (e.g., `issues: write` for label creation). Before adding `permission-*` inputs, confirm every requested permission is already granted to the App installation in the GitHub App settings; otherwise inherit the installation's granted set by omitting the inputs.
+127
View File
@@ -0,0 +1,127 @@
# CODEOWNERS - Automatically request reviews from code owners
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
#
# Rules:
# - Last matching pattern takes precedence (most specific patterns at bottom)
# - Team ownership provides coverage and knowledge sharing
# - Individual ownership for specialized domains with clear expertise
#
# ============================================================================
# DEFAULT
# ============================================================================
* @promptfoo/engineering
# ============================================================================
# TOP-LEVEL DIRECTORIES
# ============================================================================
/docs/ @alandelong-oai @mldangelo-oai # Repository documentation
/examples/ @mldangelo-oai @ianw-oai # Example configurations
/helm/ @mldangelo-oai # Kubernetes Helm charts
# ============================================================================
# SOURCE - SHARED OWNERSHIP
# ============================================================================
/src/app/ @mldangelo-oai @wholley-oai @faizan-oai # Web UI
/src/server/ @mldangelo-oai @wholley-oai @faizan-oai # API server
/src/models/ @mldangelo-oai @wholley-oai @faizan-oai # Data models
/src/redteam/ @mldangelo-oai @zcrab-oai @wholley-oai # Red team
/src/commands/ @mldangelo-oai @zcrab-oai # CLI
/src/types/ @mldangelo-oai @daneschneider-oai @faizan-oai # Type definitions
# ============================================================================
# SOURCE - CORE MODULES
# ============================================================================
/src/assertions/ @mldangelo-oai @zcrab-oai # Assertion logic
/src/providers/ @mldangelo-oai @zcrab-oai # LLM providers (base)
/src/scheduler/ @mldangelo-oai # Eval scheduler
/src/database/ @mldangelo-oai @wholley-oai # Database utilities
/src/prompts/ @mldangelo-oai # Prompt handling
/src/util/ @mldangelo-oai # Utility functions
/src/python/ @mldangelo-oai # Python integration
/src/tracing/ @mldangelo-oai # Tracing & telemetry
# ============================================================================
# SOURCE - SPECIALIZED OWNERSHIP
# ============================================================================
/src/codeScan/ @daneschneider-oai # Code scanning
/code-scan-action/ @daneschneider-oai # GitHub Action for code scan
/src/validators/ @faizan-oai @mldangelo-oai # Validators
# ============================================================================
# PROVIDERS - SPECIALIZED OWNERSHIP
# ============================================================================
/src/providers/openai/ @mldangelo-oai # OpenAI provider
/src/providers/anthropic/ @mldangelo-oai # Anthropic provider
/src/providers/azure/ @zcrab-oai # Azure provider
/src/providers/http.ts @zcrab-oai @faizan-oai # HTTP provider
# ============================================================================
# TESTS
# ============================================================================
/test/ @mldangelo-oai # Test suite base
/test/codeScans/ @daneschneider-oai
/test/code-scan-action/ @daneschneider-oai # Tests for GitHub Action
/test/redteam/ @mldangelo-oai @zcrab-oai @wholley-oai
/test/validators/ @faizan-oai @mldangelo-oai
/test/providers/ @zcrab-oai @mldangelo-oai # Provider tests
# ============================================================================
# DOCUMENTATION
# ============================================================================
/site/ @ianw-oai @mldangelo-oai # Marketing site (infra/config)
/site/docs/ @ianw-oai @mldangelo-oai # Technical documentation
/site/src/pages/**/*.tsx @ianw-oai @mldangelo-oai # Landing page content
/site/src/pages/**/*.md @ianw-oai @mldangelo-oai # Landing page markdown
/site/blog/ @mldangelo-oai # Blog posts
/SECURITY.md @mldangelo-oai # Security policy
/CONTRIBUTING.md @mldangelo-oai # Contributing guide
/site/docs/contributing.md @mldangelo-oai # Contributing docs
# ============================================================================
# DEVELOPER PRODUCTIVITY
# ============================================================================
/.github/ @mldangelo-oai # GitHub project config
/scripts/ @mldangelo-oai # Build & dev scripts
/biome.json @mldangelo-oai # Linting config
/tsconfig.json @mldangelo-oai # TypeScript config
/tsdown.config.ts @mldangelo-oai # Build config
/vitest.config.ts @mldangelo-oai # Test config
/vitest.integration.config.ts @mldangelo-oai # Integration test config
/knip.json @faizan-oai # Dead code detection
/renovate.json @mldangelo-oai # Dependency updates
/package.json @mldangelo-oai # Dependencies & scripts
/release-please-config.json @mldangelo-oai # Release automation
# ============================================================================
# DATABASE
# ============================================================================
/drizzle/ @mldangelo-oai @wholley-oai @faizan-oai # Database migrations
# ============================================================================
# MODEL AUDIT (overrides broader patterns)
# ============================================================================
/src/commands/modelScan.ts @ychhabria # Model scan CLI command
/src/server/routes/modelAudit.ts @ychhabria # Model audit API route
/src/util/modelAuditCliParser.ts @ychhabria # CLI parser utility
/src/types/modelAudit.ts @ychhabria # Model audit types
/src/app/src/pages/model-audit/ @ychhabria # Model audit UI
/test/commands/modelScan.test.ts @ychhabria # CLI tests
/test/utils/modelAuditCliParser.test.ts @ychhabria # Parser tests
# ============================================================================
# AI AGENT INSTRUCTIONS (override all other patterns)
# ============================================================================
# These patterns match files anywhere in the repo and must come last
AGENTS.md @alandelong-oai @mldangelo-oai # AI agent instructions
CLAUDE.md @alandelong-oai @mldangelo-oai # Claude Code instructions
+1
View File
@@ -0,0 +1 @@
github: [typpo]
+33
View File
@@ -0,0 +1,33 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior, including example Promptfoo configurations if applicable:
1. Run '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**System information:**
If possible, please output the results of `promptfoo debug` and paste the output here.
- Promptfoo version:
**Additional context**
Add any other context about the problem here.
+19
View File
@@ -0,0 +1,19 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
+22
View File
@@ -0,0 +1,22 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
providers:
- echo
- echo
- id: echo
config:
label: 'test'
tags:
env: 'ci'
prompts:
- 'Summarize this: {{text}}'
- 'Summarize this: {{text}}'
- 'Do not Summarize this: {{text}}'
- 'This is the prompt {{text}}'
tests:
- vars:
text: 'The quick brown fox jumps over the lazy dog.'
assert:
- type: contains
value: 'quick brown fox'
+2
View File
@@ -0,0 +1,2 @@
minimumSeverity: medium
diffsOnly: true
+49
View File
@@ -0,0 +1,49 @@
#!/usr/bin/env bash
# Determine whether to do incremental or full review based on event context
# Outputs: scope, base_sha, head_sha to GITHUB_OUTPUT
#
# Required environment variables:
# EVENT_ACTION - GitHub event action (opened, synchronize, ready_for_review)
# EVENT_BEFORE - SHA before synchronize event
# EVENT_AFTER - SHA after synchronize event
# PR_BASE_SHA - PR base branch SHA
# PR_HEAD_SHA - PR head SHA
# GITHUB_OUTPUT - GitHub Actions output file
set -euo pipefail
# Validate SHA format (40 hex chars, not null SHA) and verify it exists
is_valid_sha() {
local sha="$1"
[[ "$sha" =~ ^[0-9a-f]{40}$ ]] &&
[[ "$sha" != "0000000000000000000000000000000000000000" ]] &&
git rev-parse --verify "$sha^{commit}" >/dev/null 2>&1
}
# Check if a commit is a merge commit (has 2+ parents)
# Used to detect "merge main into branch" which should trigger full review
is_merge_commit() {
local sha="$1"
git rev-parse --verify "$sha^2" >/dev/null 2>&1
}
# For synchronize events with valid SHAs and no merge commits, do incremental review
# Merge commits (e.g., merging main into branch) trigger full review to avoid
# reviewing main branch changes that aren't part of this PR
if [ "$EVENT_ACTION" = "synchronize" ] &&
is_valid_sha "$EVENT_BEFORE" &&
is_valid_sha "$EVENT_AFTER" &&
! is_merge_commit "$EVENT_AFTER"; then
{
echo "scope=incremental"
echo "base_sha=$EVENT_BEFORE"
echo "head_sha=$EVENT_AFTER"
} >>"$GITHUB_OUTPUT"
else
# Full review for: opened, ready_for_review, force push, or merge commits
{
echo "scope=full"
echo "base_sha=$PR_BASE_SHA"
echo "head_sha=$PR_HEAD_SHA"
} >>"$GITHUB_OUTPUT"
fi
+50
View File
@@ -0,0 +1,50 @@
name: 'Deploy local.promptfoo.app'
on:
push:
branches: ['main']
paths:
- 'src/app/**' # Only trigger on changes to frontend code
permissions:
contents: read
jobs:
deploy:
name: Deploy to Cloudflare Pages
timeout-minutes: 5
runs-on: ubuntu-latest
permissions:
contents: read
deployments: write
env:
VITE_PROMPTFOO_LAUNCHER: true
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
steps:
- name: Checkout repo
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- name: Use Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: '.nvmrc'
package-manager-cache: false
# Node 24 ships with npm 11.6.2 which has a lockfile compatibility bug (npm/cli#8669).
# Pinned exact (not @latest): npm 12.0.0 blocks postinstall scripts not covered by
# allowScripts, which silently skips e.g. the Playwright browser download.
- name: Upgrade npm
run: npm install -g npm@11.18.0
- name: Install Dependencies
run: npm ci
- name: Build
run: npm run build:app
- name: Deploy to Cloudflare
uses: cloudflare/wrangler-action@9acf94ace14e7dc412b076f2c5c20b8ce93c79cd # v3
with:
apiToken: ${{ env.CLOUDFLARE_API_TOKEN }}
accountId: ${{ env.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy dist/src/app/ --project-name=promptfoo-launcher
+443
View File
@@ -0,0 +1,443 @@
# This GitHub Action builds the Docker image Promptfoo,
# runs the Docker container, performs a health check to ensure the application
# is running correctly, and then publishes the image to GitHub Container Registry
# if all checks pass. This action is triggered by the release-please workflow,
# published human-created releases, workflow dispatch, pull requests to main that
# modify the Docker/runtime update contract, and matching pushes to main.
name: Test and Publish Multi-arch Docker Image
run-name: |
${{ (inputs.tag_name || github.event.release.tag_name) && 'Release' ||
github.event_name == 'workflow_dispatch' && 'Manual' ||
github.event_name == 'pull_request' && 'PR' ||
'Push' }}
Docker build for ${{ inputs.tag_name || github.event.release.tag_name || github.ref_name }}
${{ github.event_name == 'pull_request' && format('(PR #{0})', github.event.number) || '' }}
by @${{ github.actor }}
on:
workflow_call:
inputs:
tag_name:
description: 'The release tag name (e.g., 0.120.0)'
required: true
type: string
workflow_dispatch:
inputs:
tag_name:
description: 'Optional release tag to publish (e.g., 0.121.6)'
required: false
type: string
release:
types:
- published
pull_request:
branches:
- main
paths:
- '.github/workflows/docker.yml'
- 'Dockerfile'
- 'src/envars.ts'
- 'src/globalConfig/runtimeNoticeState.ts'
- 'src/runtimeCompatibility*.ts'
- 'src/server/routes/version*.ts'
- 'src/types/api/version.ts'
- 'src/types/runtimeCompatibility.ts'
- 'src/updates.ts'
- 'src/updates/**'
push:
branches:
- main
paths:
- '.github/workflows/docker.yml'
- 'Dockerfile'
- 'src/envars.ts'
- 'src/globalConfig/runtimeNoticeState.ts'
- 'src/runtimeCompatibility*.ts'
- 'src/server/routes/version*.ts'
- 'src/types/api/version.ts'
- 'src/types/runtimeCompatibility.ts'
- 'src/updates.ts'
- 'src/updates/**'
concurrency:
# Reusable workflow calls inherit the caller's github.workflow value. Keep this
# group Docker-specific so release-please callers are not cancelled mid-release.
group: ${{ github.event_name == 'pull_request' && format('docker-pr-{0}', github.ref) || format('docker-publish-{0}', github.repository) }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
RELEASE_TAG: ${{ inputs.tag_name || github.event.release.tag_name || '' }}
SOURCE_REF: ${{ inputs.tag_name || github.event.release.tag_name || github.ref }}
BUILD_VERSION: ${{ inputs.tag_name || github.event.release.tag_name || github.ref_name }}
BUILD_DATE: ${{ github.event_name == 'pull_request' && github.event.pull_request.updated_at || github.event_name == 'release' && github.event.release.published_at || github.event.head_commit.timestamp || github.event.repository.updated_at }}
# Fallback release paths build a specific tag outside release-please.yml.
IS_RELEASE_FALLBACK: ${{ (github.event_name == 'workflow_dispatch' && inputs.tag_name != '') || github.event_name == 'release' }}
permissions:
contents: read
jobs:
test:
# Bot-authored releases are already handled by release-please.yml after npm publish.
# The release event path is for human-published fallback releases and backfills.
if: github.event_name != 'release' || github.event.release.author.type != 'Bot'
runs-on: ubuntu-latest
timeout-minutes: 60
permissions:
contents: read
steps:
- name: Validate release tag
if: github.event_name == 'release' || env.RELEASE_TAG != ''
run: |
if [[ ! "$RELEASE_TAG" =~ ^[0-9]+\.[0-9]+\.[0-9]+([.-][A-Za-z0-9.-]+)?$ ]]; then
echo "::error::RELEASE_TAG '$RELEASE_TAG' is empty or invalid"
exit 1
fi
- name: Free disk space
run: |
sudo rm -rf /usr/share/dotnet
sudo rm -rf /usr/local/lib/android
sudo rm -rf /opt/ghc
sudo rm -rf "/usr/local/share/boost"
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
sudo apt-get clean
df -h
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ env.SOURCE_REF }}
persist-credentials: false
# github.sha is the trigger SHA; SOURCE_REF can point checkout at a different tag.
- name: Resolve source commit
id: source
run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- name: Set up QEMU
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
- name: Build official Docker image for testing
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7
with:
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache-linux-amd64
context: .
push: false
load: true
tags: local-test-image:official
platforms: linux/amd64
build-args: |
BUILD_VERSION=${{ env.BUILD_VERSION }}
BUILD_DATE=${{ env.BUILD_DATE }}
GIT_SHA=${{ steps.source.outputs.sha }}
PROMPTFOO_OFFICIAL_DOCKER_IMAGE=1
- name: Build custom Docker image for contract testing
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7
with:
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache-linux-amd64
context: .
push: false
load: true
tags: local-test-image:custom
platforms: linux/amd64
build-args: |
BUILD_VERSION=${{ env.BUILD_VERSION }}
BUILD_DATE=${{ env.BUILD_DATE }}
GIT_SHA=${{ steps.source.outputs.sha }}
PROMPTFOO_OFFICIAL_DOCKER_IMAGE=0
- name: Run Docker containers for testing
run: |
docker run -d --name promptfoo-container -p 3000:3000 local-test-image:official
docker run -d --name promptfoo-custom-container -p 3001:3000 local-test-image:custom
- name: Run health check
run: |
# Wait for both servers to be fully ready (including database migrations)
# before running any CLI commands that also trigger migrations.
for port in 3000 3001; do
HEALTH_CHECK_PASS=
for ((retry=1; retry<=10; retry++)); do
if curl -f "http://localhost:${port}/health"; then
echo -e "\nHealth check passed on port ${port}"
HEALTH_CHECK_PASS=true
break
else
echo "Health check on port ${port} failed, retrying in 2 seconds..."
sleep 2
fi
done
if [ -z "$HEALTH_CHECK_PASS" ]; then
echo -e "\nHealth check on port ${port} failed after multiple attempts" >&2
exit 1
fi
done
- name: Verify Python and promptfoo
run: |
echo "Checking Python version:"
if ! docker exec promptfoo-container python --version; then
echo "Python check failed"
exit 1
fi
echo "Checking promptfoo version:"
if ! docker exec promptfoo-container promptfoo --version; then
echo "promptfoo check failed"
exit 1
fi
echo "Checking pf alias:"
if ! docker exec promptfoo-container pf --version; then
echo "pf alias check failed"
exit 1
fi
echo "Checking container update markers and API guidance:"
test "$(docker exec promptfoo-container printenv PROMPTFOO_RUNNING_IN_DOCKER)" = "1"
test "$(docker exec promptfoo-container printenv PROMPTFOO_OFFICIAL_DOCKER_IMAGE)" = "1"
test "$(docker exec promptfoo-custom-container printenv PROMPTFOO_RUNNING_IN_DOCKER)" = "1"
test "$(docker exec promptfoo-custom-container printenv PROMPTFOO_OFFICIAL_DOCKER_IMAGE)" = "0"
curl -fsS http://localhost:3000/api/version | jq -e '
.commandType == "docker" and
.updateCommands.primary == "docker pull ghcr.io/promptfoo/promptfoo:latest"
'
curl -fsS http://localhost:3001/api/version | jq -e '
.commandType == "npm" and
.updateCommands.primary == "" and
.updateCommands.isCustomContainer == true
'
- name: Use Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: '.nvmrc'
cache: 'npm'
# Node 24 ships with npm 11.6.2 which has a lockfile compatibility bug (npm/cli#8669)
- name: Upgrade npm
run: npm install -g npm@11.11.0 --registry=https://registry.npmjs.org/
- name: run promptfoo eval
id: eval
env:
PROMPTFOO_REMOTE_API_BASE_URL: http://localhost:3000
PROMPTFOO_SHARING_APP_BASE_URL: http://localhost:3000
run: |
npm install --registry=https://registry.npmjs.org/
npm run local -- eval -c .github/assets/promptfooconfig.yaml --share
- name: Test that the eval results are uploaded
run: |
response=$(curl -s http://localhost:3000/api/results)
echo "Response: $response"
# Use jq to extract the array length
count=$(echo "$response" | jq '.data | length')
echo "Array Length: $count"
# Check if the count is exactly 1
if [ "$count" -ne 1 ]; then
echo "Error: Expected 1 entry, but got $count"
exit 1
fi
- name: Stop and remove Docker container
if: always()
run: docker rm -f promptfoo-container promptfoo-custom-container || true
- name: Clean up Docker images
if: always()
run: |
docker system prune -af
docker volume prune -f
df -h
build-docker-and-push-digests:
if: (github.event_name != 'pull_request')
strategy:
matrix:
platforms:
[
{ runner: ubuntu-latest, platform: linux/amd64, digest-suffix: linux-amd64 },
{ runner: ubuntu-24.04-arm, platform: linux/arm64, digest-suffix: linux-arm64 },
]
runs-on: ${{ matrix.platforms.runner }}
timeout-minutes: 60
needs: [test]
permissions:
contents: read
packages: write
steps:
- name: Validate release tag
if: github.event_name == 'release' || env.RELEASE_TAG != ''
run: |
if [[ ! "$RELEASE_TAG" =~ ^[0-9]+\.[0-9]+\.[0-9]+([.-][A-Za-z0-9.-]+)?$ ]]; then
echo "::error::RELEASE_TAG '$RELEASE_TAG' is empty or invalid"
exit 1
fi
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ env.SOURCE_REF }}
# github.sha is the trigger SHA; SOURCE_REF can point checkout at a different tag.
- name: Resolve source commit
id: source
run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- name: Set up QEMU
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
- name: Docker meta
id: meta
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
labels: |
org.opencontainers.image.revision=${{ steps.source.outputs.sha }}
- name: Log in to the Container registry
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push multi-arch Docker image
id: build
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7
with:
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache-${{ matrix.platforms.digest-suffix }}
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache-${{ matrix.platforms.digest-suffix }},mode=max
context: .
platforms: ${{ matrix.platforms.platform }}
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
BUILD_VERSION=${{ env.BUILD_VERSION }}
BUILD_DATE=${{ env.BUILD_DATE }}
GIT_SHA=${{ steps.source.outputs.sha }}
PROMPTFOO_OFFICIAL_DOCKER_IMAGE=${{ github.repository == 'promptfoo/promptfoo' && '1' || '0' }}
outputs: |
type=image,push-by-digest=true,name-canonical=true,push=true,annotation-index.org.opencontainers.image.description=promptfoo is a tool for testing evaluating and red-teaming LLM apps.
- name: Export digest
run: |
mkdir -p "${{ runner.temp }}/digests"
digest="${{ steps.build.outputs.digest }}"
touch "${{ runner.temp }}/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: digests-${{ matrix.platforms.digest-suffix }}
path: ${{ runner.temp }}/digests/*
if-no-files-found: error
retention-days: 1
merge-docker-digests:
runs-on: ubuntu-latest
timeout-minutes: 15
needs: [build-docker-and-push-digests]
permissions:
packages: write
outputs:
manifest_digest: ${{ steps.inspect.outputs.digest }}
steps:
- name: Download digests
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
path: ${{ runner.temp }}/digests
pattern: digests-*
merge-multiple: true
- name: Log in to the Container registry
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
- name: Docker meta
id: meta
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# Fallback release builds publish immutable version tags only so moving aliases stay on normal release paths.
tags: |
type=ref,event=branch,enable=${{ env.IS_RELEASE_FALLBACK != 'true' }}
type=ref,event=pr,enable=${{ env.IS_RELEASE_FALLBACK != 'true' }}
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}},enable=${{ env.IS_RELEASE_FALLBACK != 'true' }}
type=sha,enable=${{ env.IS_RELEASE_FALLBACK != 'true' }}
type=raw,value=${{ env.RELEASE_TAG }},enable=${{ env.RELEASE_TAG != '' }}
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' && env.IS_RELEASE_FALLBACK != 'true' }}
- name: Create manifest list and push
working-directory: ${{ runner.temp }}/digests
run: |
# shellcheck disable=SC2046,SC2091
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
- name: Inspect image
id: inspect
env:
IMAGE_TAG: ${{ env.RELEASE_TAG || steps.meta.outputs.version }}
run: |
IMAGE_REF="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${IMAGE_TAG}"
# Log full inspection details and extract digest
INSPECT_OUTPUT=$(docker buildx imagetools inspect "${IMAGE_REF}")
echo "$INSPECT_OUTPUT"
# Extract the manifest digest (first Digest: line)
DIGEST=$(echo "$INSPECT_OUTPUT" | grep -m1 "^Digest:" | awk '{print $2}')
if [ -z "$DIGEST" ]; then
echo "Error: Failed to extract manifest digest from inspect output"
exit 1
fi
echo "digest=$DIGEST" >> "$GITHUB_OUTPUT"
echo "Manifest digest: $DIGEST"
attest-docker-image:
name: 'Attest Multi-arch Image'
runs-on: ubuntu-latest
timeout-minutes: 15
needs: [merge-docker-digests]
permissions:
id-token: write
attestations: write
packages: write
steps:
- name: Log in to the Container registry
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Attest Build Provenance
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4
with:
subject-name: '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}'
subject-digest: '${{ needs.merge-docker-digests.outputs.manifest_digest }}'
push-to-registry: true
+38
View File
@@ -0,0 +1,38 @@
name: Compress Images
on:
pull_request:
# Run Image Actions when JPG, JPEG, PNG or WebP files are added or changed.
# See https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#onpushpull_requestpaths for reference.
paths:
- '**.jpg'
- '**.jpeg'
- '**.png'
- '**.webp'
permissions:
contents: read
jobs:
build:
# Only run on Pull Requests within the same repository, and not from forks.
if: github.event.pull_request.head.repo.full_name == github.repository
name: calibreapp/image-actions
permissions:
contents: write
pull-requests: write
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout Repo
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- name: Compress Images
uses: calibreapp/image-actions@f32575787d333b0579f0b7d506ff03be63a669d1 # 1.4.1
with:
# The `GITHUB_TOKEN` is automatically generated by GitHub and scoped only to the repository that is currently running the action. By default, the action cant update Pull Requests initiated from forked repositories.
# See https://docs.github.com/en/actions/reference/authentication-in-a-workflow and https://help.github.com/en/articles/virtual-environments-for-github-actions#token-permissions
githubToken: ${{ secrets.GITHUB_TOKEN }}
jpegQuality: '90'
jpegProgressive: false
pngQuality: '90'
webpQuality: '90'
+864
View File
@@ -0,0 +1,864 @@
name: CI
on:
pull_request:
push:
branches:
- main
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
VITE_TELEMETRY_DISABLED: 1
jobs:
ci-config:
name: CI Config
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
test-matrix: ${{ steps.set-matrix.outputs.test-matrix }}
build-matrix: ${{ steps.set-matrix.outputs.build-matrix }}
steps:
- name: Determine CI matrix
id: set-matrix
env:
EVENT_NAME: ${{ github.event_name }}
run: |
# Shared entries: all Linux versions + one Windows Node version (sharded)
base_entries='
{"node":"20.20","os":"ubuntu-latest","shard":""},
{"node":"22.22","os":"ubuntu-latest","shard":""},
{"node":"24.x","os":"ubuntu-latest","shard":""},
{"node":"26.x","os":"ubuntu-latest","shard":""},
{"node":"22.22","os":"windows-2025-vs2026","shard":1},
{"node":"22.22","os":"windows-2025-vs2026","shard":2},
{"node":"22.22","os":"windows-2025-vs2026","shard":3}
'
if [[ "$EVENT_NAME" == "pull_request" ]]; then
# PRs: add 1 macOS version only
extra_entries='
,{"node":"22.22","os":"macOS-latest","shard":""}
'
build_matrix='{"node":["20.20","24.x","26.x"]}'
else
# Main/dispatch: add all macOS versions + remaining Windows shards
extra_entries='
,{"node":"20.20","os":"macOS-latest","shard":""},
{"node":"22.22","os":"macOS-latest","shard":""},
{"node":"20.20","os":"windows-2025-vs2026","shard":1},
{"node":"20.20","os":"windows-2025-vs2026","shard":2},
{"node":"20.20","os":"windows-2025-vs2026","shard":3},
{"node":"24.x","os":"windows-2025-vs2026","shard":1},
{"node":"24.x","os":"windows-2025-vs2026","shard":2},
{"node":"24.x","os":"windows-2025-vs2026","shard":3},
{"node":"26.x","os":"windows-2025-vs2026","shard":1},
{"node":"26.x","os":"windows-2025-vs2026","shard":2},
{"node":"26.x","os":"windows-2025-vs2026","shard":3}
'
build_matrix='{"node":["20.20","22.22","24.x","26.x"]}'
fi
# Build the matrix JSON (shard:"" is falsy in GHA expressions, used to skip shard flags)
test_matrix=$(echo "{\"include\":[${base_entries}${extra_entries}]}" | jq -c .)
echo "test-matrix=$test_matrix" >> "$GITHUB_OUTPUT"
echo "build-matrix=$build_matrix" >> "$GITHUB_OUTPUT"
test:
needs: ci-config
name: Test on Node ${{ matrix.node }} and ${{ matrix.os }}${{ matrix.shard && format(' (shard {0}/3)', matrix.shard) || '' }}
# Cold-cache Node 26 installs can approach 20m before the test phase starts, so leave room for the test body.
timeout-minutes: 40
runs-on: ${{ matrix.os }}
permissions:
contents: read
checks: write
id-token: write # Required for OIDC authentication with Codecov
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.ci-config.outputs.test-matrix) }}
steps:
- name: Checkout repo
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
fetch-depth: 2
- name: Use Node ${{ matrix.node }}
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
# Keep matrix label at 20.20 for stable check names, but install 20.20.1 to satisfy engines.
node-version: ${{ matrix.node == '20.20' && '20.20.1' || matrix.node }}
cache: 'npm'
- name: Use Python 3.14
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: 3.14.4
# Ruby 4.0.1 is not yet available on Windows via setup-ruby, use 4.0.0 on Windows
- name: Use Ruby
uses: ruby/setup-ruby@4c56a21280b36d862b5fc31348f463d60bdc55d5 # v1
with:
ruby-version: ${{ startsWith(matrix.os, 'windows-') && '4.0.0' || '4.0.1' }}
# Cache node_modules to speed up installs (especially on Windows where npm ci is slow)
# Key includes OS and Node version since native modules are platform/version specific
- name: Cache node_modules
id: cache-node-modules
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5
with:
path: node_modules
key: node-modules-${{ runner.os }}-node${{ matrix.node }}-${{ hashFiles('package-lock.json') }}
- name: Install Dependencies (Node 26)
if: steps.cache-node-modules.outputs.cache-hit != 'true' && matrix.node == '26.x'
# Node 26 hosted installs stall when lifecycle scripts run during npm ci.
# Install the tree without scripts, then rebuild the native addon tests need.
run: |
npm ci --ignore-scripts
npm rebuild better-sqlite3
- name: Install Dependencies
if: steps.cache-node-modules.outputs.cache-hit != 'true' && matrix.node != '26.x'
run: npx -y npm@11.11.0 ci
- name: Test
# Treat a post-run forks-worker crash (all tests already passed, then the
# worker dies on teardown -> "Worker exited unexpectedly") as non-fatal in
# CI. Real test/assertion failures still fail; see vitest.config.ts.
env:
PROMPTFOO_IGNORE_UNHANDLED_TEST_ERRORS: 'true'
run: npm run test${{ matrix.shard && format(' -- --shard={0}/3', matrix.shard) || '' }}${{ matrix.os == 'ubuntu-latest' && matrix.node == '20.20' && !matrix.shard && ' -- --coverage' || '' }}
- name: Check Backend Coverage Ratchets
if: matrix.os == 'ubuntu-latest' && matrix.node == '20.20' && !matrix.shard
run: npm run test:coverage:ratchet -- --report backend
- name: Upload Backend Coverage to Codecov
if: matrix.os == 'ubuntu-latest' && matrix.node == '20.20' && !matrix.shard
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
files: ./coverage/coverage-final.json
flags: backend
name: backend-coverage
# Codecov upload is informational (coverage is gated in-repo, not by
# Codecov); keep it non-blocking so Codecov infra outages can't fail main.
fail_ci_if_error: false
use_oidc: true
# Pin the CLI version too: the action SHA pins the wrapper, but the CLI
# binary it downloads is otherwise unpinned (action default is latest).
version: v11.2.8
build:
needs: ci-config
name: Build on Node ${{ matrix.node }}
env:
PROMPTFOO_POSTHOG_KEY: ${{ secrets.PROMPTFOO_POSTHOG_KEY }}
# Cold-cache Node 26 installs can approach 20m before the build phase starts on GitHub-hosted runners.
timeout-minutes: 30
runs-on: ubuntu-latest
strategy:
matrix: ${{ fromJSON(needs.ci-config.outputs.build-matrix) }}
steps:
- name: Checkout repo
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- name: Use Node ${{ matrix.node }}
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
# Keep matrix label at 20.20 for stable check names, but install 20.20.1 to satisfy engines.
node-version: ${{ matrix.node == '20.20' && '20.20.1' || matrix.node }}
cache: 'npm'
- name: Install Dependencies
# The build does not need native dependency lifecycle scripts. Skipping them
# keeps the Node 26 build lane out of the install stall seen on hosted runners.
run: |
if [ "${{ matrix.node }}" = "26.x" ]; then
npm ci --ignore-scripts
else
npx -y npm@11.11.0 ci
fi
- name: Build
run: npm run build
- name: Check Telemetry
env:
EVENT_NAME: ${{ github.event_name }}
HEAD_REPO_FULL_NAME: ${{ github.event.pull_request.head.repo.full_name }}
REPOSITORY: ${{ github.repository }}
run: |
# Skip PostHog key check for forks as they don't have access to secrets
if [[ "$EVENT_NAME" == "pull_request" ]] && [[ "$HEAD_REPO_FULL_NAME" != "$REPOSITORY" ]]; then
echo "Skipping PostHog key check for fork PR"
elif [[ -z "$PROMPTFOO_POSTHOG_KEY" ]]; then
echo "PostHog key not available (running without secret), skipping check"
else
# PostHog key is injected at build time via tsup's define option
# Check that it's present in the built output (it will be inlined as a string)
if ! grep -rq '"phc_' dist/src/*.js; then
echo "Error: PostHog key not found in built output"
echo "Checking for POSTHOG_KEY patterns in built files:"
grep -r "POSTHOG_KEY" dist/src/*.js | head -10 || echo "No POSTHOG_KEY found"
exit 1
fi
echo "PostHog key replacement verified successfully"
fi
- name: Test Package Artifact
if: matrix.node == '20.20' || matrix.node == '26.x'
env:
PROMPTFOO_DISABLE_TELEMETRY: 1
run: npm run test:package-artifact
style-check:
name: Style Check
timeout-minutes: 5
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- name: Use Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: '.nvmrc'
cache: 'npm'
# Node 24 ships with npm 11.6.2 which has a lockfile compatibility bug (npm/cli#8669).
# Pinned exact (not @latest): npm 12.0.0 blocks postinstall scripts not covered by
# allowScripts, which silently skips e.g. the Playwright browser download.
- name: Upgrade npm
run: npm install -g npm@11.18.0
- name: Install Dependencies
run: |
npm ci
- name: Run Biome CI
run: |
npm run lint:ci
- name: Run Prettier Check
run: |
npm run format:check:prettier
- name: Check Dependency Versions
run: |
npx check-dependency-version-consistency
- name: Check for circular dependencies
run: |
# shellcheck disable=SC2046
npx madge $(git ls-files '*.ts') --circular
- name: Check architecture boundaries
run: |
npm run architecture:check
- name: Check for missing dependencies
run: |
npm run depcheck
- name: Check for orphaned files
run: |
npm run knip -- --include files
- name: Validate lockfile integrity
run: |
npx lockfile-lint --path package-lock.json --allowed-hosts npm --validate-https
shell-format:
name: Shell Format Check
timeout-minutes: 5
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- uses: luizm/action-sh-checker@883217215b11c1fabbf00eb1a9a041f62d74c744
env:
SHFMT_OPTS: '-i 2' # 2 space indent
with:
sh_checker_shellcheck_disable: true
assets:
name: Generate Assets
timeout-minutes: 5
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- name: Use Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: '.nvmrc'
cache: 'npm'
- name: Use Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: '3.13'
# Node 24 ships with npm 11.6.2 which has a lockfile compatibility bug (npm/cli#8669).
# Pinned exact (not @latest): npm 12.0.0 blocks postinstall scripts not covered by
# allowScripts, which silently skips e.g. the Playwright browser download.
- name: Upgrade npm
run: npm install -g npm@11.18.0
- name: Install Dependencies
run: npm ci
- name: Install ModelAudit schema dependencies
run: python3 -m pip install --disable-pip-version-check --no-deps -r scripts/modelaudit_schema_requirements.txt
- name: Generate JSON Schemas
run: |
npm run jsonSchema:generate
npm run modelAuditSchema:generate
- name: Check for changes
run: |
if [[ -n $(git status --porcelain) ]]; then
echo "Changes detected after generating assets:"
git status --porcelain
exit 1
else
echo "No changes detected."
fi
python:
name: Check Python
timeout-minutes: 5
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.9, 3.14]
steps:
- name: Checkout repo
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- name: Use Python ${{ matrix.python-version }}
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: ${{ matrix.python-version }}
- name: Install Dependencies
run: |
pip install ruff==0.15.1
- name: Check Formatting
run: |
ruff check --select F401,F841,I --fix
ruff format
git diff --exit-code || (echo "Files were modified by ruff. Please commit these changes." && exit 1)
- name: Run Tests
run: |
python -m unittest discover -s src/python -p '*_test.py'
docs:
name: Build Docs
timeout-minutes: 5
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- name: Use Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: '.nvmrc'
cache: 'npm'
# Node 24 ships with npm 11.6.2 which has a lockfile compatibility bug (npm/cli#8669).
# Pinned exact (not @latest): npm 12.0.0 blocks postinstall scripts not covered by
# allowScripts, which silently skips e.g. the Playwright browser download.
- name: Upgrade npm
run: npm install -g npm@11.18.0
- name: Install Dependencies
run: npm ci
- name: Type Check
working-directory: site
run: npm run typecheck
- name: Build Documentation
working-directory: site
run: npm run build
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
code-scan-action:
name: Code Scan Action
timeout-minutes: 5
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- name: Use Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: '.nvmrc'
cache: 'npm'
# Node 24 ships with npm 11.6.2 which has a lockfile compatibility bug (npm/cli#8669).
# Pinned exact (not @latest): npm 12.0.0 blocks postinstall scripts not covered by
# allowScripts, which silently skips e.g. the Playwright browser download.
- name: Upgrade npm
run: npm install -g npm@11.18.0
# Install root dependencies first (code-scan-action imports from ../../src/types/codeScan.ts which needs zod)
- name: Install Root Dependencies
run: npm ci
- name: Install Code Scan Action Dependencies
working-directory: code-scan-action
run: npm install
- name: Type Check
working-directory: code-scan-action
run: npm run tsc
- name: Build Action
working-directory: code-scan-action
run: npm run build
site-tests:
name: Site tests
timeout-minutes: 5
runs-on: ubuntu-latest
permissions:
contents: read
checks: write
id-token: write # Required for OIDC authentication with Codecov
steps:
- name: Checkout repo
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- name: Use Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: '.nvmrc'
cache: 'npm'
# Node 24 ships with npm 11.6.2 which has a lockfile compatibility bug (npm/cli#8669).
# Pinned exact (not @latest): npm 12.0.0 blocks postinstall scripts not covered by
# allowScripts, which silently skips e.g. the Playwright browser download.
- name: Upgrade npm
run: npm install -g npm@11.18.0
- name: Install Dependencies
run: npm ci
- name: Run Site Tests with Coverage
working-directory: site
run: npm run test:coverage
# Site has no coverage ratchet, so the Codecov upload is the only consumer of
# this report. Since that upload is now non-blocking, verify the artifact was
# produced here — otherwise a missing/misnamed report would fail silently.
- name: Verify site coverage report exists
run: test -s ./site/coverage/coverage-final.json
- name: Upload Site Coverage to Codecov
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
files: ./site/coverage/coverage-final.json
flags: site
name: site-coverage
# Codecov upload is informational (coverage is gated in-repo, not by
# Codecov); keep it non-blocking so Codecov infra outages can't fail main.
fail_ci_if_error: false
use_oidc: true
# Pin the CLI version too: the action SHA pins the wrapper, but the CLI
# binary it downloads is otherwise unpinned (action default is latest).
version: v11.2.8
webui:
name: webui tests
timeout-minutes: 10
runs-on: ubuntu-latest
permissions:
contents: read
checks: write
id-token: write # Required for OIDC authentication with Codecov
steps:
- name: Checkout repo
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
fetch-depth: 2
- name: Use Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: '.nvmrc'
cache: 'npm'
# Node 24 ships with npm 11.6.2 which has a lockfile compatibility bug (npm/cli#8669).
# Pinned exact (not @latest): npm 12.0.0 blocks postinstall scripts not covered by
# allowScripts, which silently skips e.g. the Playwright browser download.
- name: Upgrade npm
run: npm install -g npm@11.18.0
- name: Install Dependencies
run: npm ci
- name: Run App Browser Tests
run: npm run test:app:browser
- name: Run App Tests with Coverage
run: npm run test:coverage --prefix src/app
- name: Check Frontend Coverage Ratchets
run: npm run test:coverage:ratchet -- --report frontend
- name: Upload Frontend Coverage to Codecov
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
files: ./src/app/coverage/coverage-final.json
flags: frontend
name: frontend-coverage
# Codecov upload is informational (coverage is gated in-repo, not by
# Codecov); keep it non-blocking so Codecov infra outages can't fail main.
fail_ci_if_error: false
use_oidc: true
# Pin the CLI version too: the action SHA pins the wrapper, but the CLI
# binary it downloads is otherwise unpinned (action default is latest).
version: v11.2.8
integration-tests:
name: Run Integration Tests
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout repo
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- name: Use Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: '.nvmrc'
cache: 'npm'
# Node 24 ships with npm 11.6.2 which has a lockfile compatibility bug (npm/cli#8669).
# Pinned exact (not @latest): npm 12.0.0 blocks postinstall scripts not covered by
# allowScripts, which silently skips e.g. the Playwright browser download.
- name: Upgrade npm
run: npm install -g npm@11.18.0
- name: Use Python 3.14
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: 3.14.4
- name: Use Ruby
uses: ruby/setup-ruby@4c56a21280b36d862b5fc31348f463d60bdc55d5 # v1
with:
ruby-version: 4.0.1
- name: Install Dependencies
run: |
npm ci
- name: Run Integration Tests
run: npm run test:integration -- --coverage
smoke-tests:
name: Run Smoke Tests
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout repo
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- name: Use Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: '.nvmrc'
cache: 'npm'
# Node 24 ships with npm 11.6.2 which has a lockfile compatibility bug (npm/cli#8669).
# Pinned exact (not @latest): npm 12.0.0 blocks postinstall scripts not covered by
# allowScripts, which silently skips e.g. the Playwright browser download.
- name: Upgrade npm
run: npm install -g npm@11.18.0
- name: Use Python 3.14
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: 3.14.4
- name: Use Ruby
uses: ruby/setup-ruby@4c56a21280b36d862b5fc31348f463d60bdc55d5 # v1
with:
ruby-version: 4.0.1
- name: Install Dependencies
run: npm ci
- name: Build
run: npm run build
- name: Run Smoke Tests
run: npm run test:smoke
share-test:
name: Share Test
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- name: Checkout repo
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- name: Use Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: '.nvmrc'
cache: 'npm'
# Node 24 ships with npm 11.6.2 which has a lockfile compatibility bug (npm/cli#8669).
# Pinned exact (not @latest): npm 12.0.0 blocks postinstall scripts not covered by
# allowScripts, which silently skips e.g. the Playwright browser download.
- name: Upgrade npm
run: npm install -g npm@11.18.0
- name: Install Dependencies
run: npm ci
- name: Run local server
run: |
mkdir -p "$RUNNER_TEMP/promptfoo-share-test"
npm run build
server_log="$RUNNER_TEMP/promptfoo-share-test/server.log"
PROMPTFOO_CONFIG_DIR="$HOME/tmp" LOG_LEVEL=DEBUG API_PORT=8500 node dist/src/server/index.js >"$server_log" 2>&1 &
echo "SERVER_PID=$!" >> "$GITHUB_ENV"
echo "SERVER_LOG=$server_log" >> "$GITHUB_ENV"
- name: Wait for server to be ready
run: |
for i in $(seq 1 45); do
if ! kill -0 "$SERVER_PID" 2>/dev/null; then
echo "Server exited before becoming ready"
cat "$SERVER_LOG"
exit 1
fi
if curl -fsS -o /dev/null http://localhost:8500/health; then
echo "Server is ready"
exit 0
fi
echo "Waiting for server... attempt $i/45"
sleep 2
done
echo "Server failed to start"
cat "$SERVER_LOG"
exit 1
- name: run promptfoo eval
id: eval
run: |
PROMPTFOO_REMOTE_API_BASE_URL=http://localhost:8500 PROMPTFOO_SHARING_APP_BASE_URL=http://localhost:8500 node dist/src/main.js eval -c .github/assets/promptfooconfig.yaml --share
env:
PROMPTFOO_DISABLE_TELEMETRY: 1
- name: Test that the eval results are uploaded
run: |
response=$(curl -s http://localhost:8500/api/results)
echo "Response: $response"
# Use jq to extract the array length
count=$(echo "$response" | jq '.data | length')
echo "Array Length: $count"
# Check if the count is exactly 1
if [ "$count" -ne 1 ]; then
echo "Error: Expected 1 entry, but got $count"
exit 1
fi
- name: Dump server log on failure
if: failure()
run: |
if [ -f "$SERVER_LOG" ]; then
cat "$SERVER_LOG"
fi
- name: Share to cloud
if: env.PROMPTFOO_STAGING_API_KEY != ''
env:
PROMPTFOO_STAGING_API_KEY: ${{ secrets.PROMPTFOO_STAGING_API_KEY }}
run: |
node dist/src/main.js auth login -k ${{ secrets.PROMPTFOO_STAGING_API_KEY }} -h https://api.promptfoo-staging.app
node dist/src/main.js eval -c .github/assets/promptfooconfig.yaml --share
- name: Stop local server
if: always()
run: |
if [ -n "${SERVER_PID:-}" ] && kill -0 "$SERVER_PID" 2>/dev/null; then
kill "$SERVER_PID"
wait "$SERVER_PID" || true
fi
redteam:
name: Redteam (Production API)
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout repo
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- name: Use Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: '.nvmrc'
cache: 'npm'
# Node 24 ships with npm 11.6.2 which has a lockfile compatibility bug (npm/cli#8669).
# Pinned exact (not @latest): npm 12.0.0 blocks postinstall scripts not covered by
# allowScripts, which silently skips e.g. the Playwright browser download.
- name: Upgrade npm
run: npm install -g npm@11.18.0
- name: Install Dependencies
run: npm ci
- name: Run Redteam (Production API)
run: |
npm run test:redteam:integration
redteam-staging:
name: Redteam (Staging API)
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- name: Checkout repo
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- name: Use Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: '.nvmrc'
cache: 'npm'
# Node 24 ships with npm 11.6.2 which has a lockfile compatibility bug (npm/cli#8669).
# Pinned exact (not @latest): npm 12.0.0 blocks postinstall scripts not covered by
# allowScripts, which silently skips e.g. the Playwright browser download.
- name: Upgrade npm
run: npm install -g npm@11.18.0
- name: Install Dependencies
run: npm ci
# Need to build first so we can login
- name: Build
run: |
npm run build
- name: Login
if: env.PROMPTFOO_INTEGRATION_TEST_API_KEY != ''
env:
PROMPTFOO_INTEGRATION_TEST_API_KEY: ${{ secrets.PROMPTFOO_INTEGRATION_TEST_API_KEY }}
run: |
npm run bin auth login -- -k ${{ secrets.PROMPTFOO_INTEGRATION_TEST_API_KEY }} -h ${{ secrets.PROMPTFOO_INTEGRATION_TEST_API_HOST }}
- name: Run Redteam with Staging API
continue-on-error: true
run: |
npm run test:redteam:integration
actionlint:
name: GitHub Actions Lint
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
steps:
- name: Checkout repo
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- name: Install and run actionlint
run: |
bash <(curl -s https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)
./actionlint
ruby:
name: Check Ruby
timeout-minutes: 5
runs-on: ubuntu-latest
strategy:
matrix:
ruby-version: ['3.0', '3.4']
steps:
- name: Checkout repo
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- name: Use Ruby ${{ matrix.ruby-version }}
uses: ruby/setup-ruby@4c56a21280b36d862b5fc31348f463d60bdc55d5 # v1
with:
ruby-version: ${{ matrix.ruby-version }}
- name: Install Dependencies
run: |
gem install rubocop
- name: Check Formatting
run: |
rubocop --autocorrect-all src/ruby/
git diff --exit-code || (echo "Files were modified by rubocop. Please commit these changes." && exit 1)
- name: Test Ruby Wrapper
run: |
# Create a simple test script
cat > /tmp/test_script.rb << 'EOF'
def test_function(a, b)
{ "sum" => a + b, "product" => a * b }
end
EOF
# Create input JSON
echo '[2, 3]' > /tmp/input.json
# Run the wrapper
ruby src/ruby/wrapper.rb /tmp/test_script.rb test_function /tmp/input.json /tmp/output.json
# Verify output
cat /tmp/output.json
if ! grep -q '"sum":5' /tmp/output.json; then
echo "Error: Ruby wrapper test failed"
exit 1
fi
echo "Ruby wrapper test passed"
golang:
name: Go Tests
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout repo
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- name: Set up Go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
with:
go-version-file: src/golang/go.mod
check-latest: true
- name: Run wrapper tests
working-directory: src/golang
run: |
go test -v wrapper.go wrapper_test.go
+57
View File
@@ -0,0 +1,57 @@
name: Promptfoo Code Scan
on:
pull_request:
types: [opened, ready_for_review]
workflow_dispatch:
inputs:
pr_number:
description: 'PR number to scan'
required: true
permissions:
contents: read
jobs:
security-scan:
runs-on: ubuntu-latest
timeout-minutes: 15
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
PROMPTFOO_DISABLE_UPDATE: true
permissions:
contents: read
pull-requests: write
id-token: write
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
fetch-depth: 0
# Pin to 24.15.0 (not .nvmrc): Node 24.16.0 adds ~10s to every HTTPS
# request on Linux runners, dragging the scanner's uncached
# `npm install -g promptfoo` (~860 packages) into a 14-minute crawl that
# trips the job timeout. 24.17.0 still has the regression (PR #9859
# re-bumped it and every scan started timing out), so renovate.json
# disables Node updates for this file. Revisit — and re-enable that
# rule — when a newer Node resolves it.
- name: Use Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: '24.15.0'
- name: Run Promptfoo Code Scan
uses: promptfoo/code-scan-action@c4839dcb8623ca031ee6d271c7850dc71d994dd3 # v0
env:
# Keep the global CLI install on the first promptfoo release that retries
# transient repository MCP timeouts, while avoiding future dependency drift.
NPM_CONFIG_BEFORE: '2026-04-11T00:40:00.000Z'
# Avoid combining the workflow's before pin with this repo's project .npmrc
# min-release-age setting without changing nested npx package layout.
NPM_CONFIG_USERCONFIG: '/dev/null'
with:
min-severity: medium
config-path: .github/promptfoo-code-scan.yaml
enable-fork-prs: true
+149
View File
@@ -0,0 +1,149 @@
name: release-please-format
run-name: format CHANGELOG on release PR (${{ github.event.pull_request.head.ref }})
# Runs prettier on CHANGELOG files in release-please PRs. Lives in its own
# workflow (not inside release-please.yml) so it re-runs on every PR
# synchronize event — release-please force-pushes its PR branch when new
# commits land on main, which would wipe any formatting commit created
# inline in the release-please job.
on:
pull_request:
types: [opened, synchronize, reopened]
paths:
- '**/CHANGELOG.md'
permissions:
contents: read
jobs:
format:
# Branch prefix alone is not a trust boundary. Require: same-repo PR (forks
# cannot be legitimate release-please PRs), the exact release bot as
# author, and the release-please branch naming convention.
if: >-
github.event.pull_request.head.repo.full_name == github.repository &&
github.event.pull_request.user.login == 'promptfoobot[bot]' &&
startsWith(github.event.pull_request.head.ref, 'release-please--')
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
# Pin an explicit Node version so this step does not depend on a
# PR-controlled .nvmrc — the install below happens before checkout.
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 'lts/*'
# Isolated install before checkout so a PR-controlled .npmrc cannot
# redirect the registry or inject a package.
- name: Install prettier (outside PR checkout)
run: |
set -euo pipefail
install_dir="$RUNNER_TEMP/prettier-install"
mkdir -p "$install_dir"
cd "$install_dir"
npm install \
--no-save --no-audit --no-fund --ignore-scripts \
--registry=https://registry.npmjs.org/ \
prettier@3.8.1
echo "$install_dir/node_modules/.bin" >> "$GITHUB_PATH"
# persist-credentials: false keeps GITHUB_TOKEN out of .git/config so it
# can't be read via a PR-planted pre-push hook. The App token is not
# created until after the untrusted steps below.
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ github.event.pull_request.head.ref }}
fetch-depth: 0
persist-credentials: false
- name: Format and commit CHANGELOG files
id: format
env:
BASE_REF: ${{ github.event.pull_request.base.ref }}
run: |
set -euo pipefail
# Ensure base ref is available locally — actions/checkout with
# fetch-depth: 0 fetches history of the head ref, not other branches.
git fetch --no-tags --prune origin "${BASE_REF}"
# Capture diff output separately so a failing `git diff` does not get
# swallowed by `|| true` on the downstream grep pipeline.
changed_paths="$(git diff --name-only --diff-filter=ACMRT "origin/${BASE_REF}...HEAD")"
mapfile -t changelog_files < <(
printf '%s\n' "$changed_paths" | grep -E '(^|/)CHANGELOG\.md$' || true
)
if [[ "${#changelog_files[@]}" -eq 0 ]]; then
echo "No CHANGELOG files changed in release PR"
echo "has_changes=false" >> "$GITHUB_OUTPUT"
exit 0
fi
# --no-config and --no-editorconfig prevent prettier from loading a
# PR-controlled .prettierrc.js (which would execute arbitrary code) or
# .editorconfig that could change what gets written.
prettier --no-config --no-editorconfig --write "${changelog_files[@]}"
if git diff --quiet -- "${changelog_files[@]}"; then
echo "No formatting changes needed"
echo "has_changes=false" >> "$GITHUB_OUTPUT"
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add "${changelog_files[@]}"
# core.hooksPath=/dev/null prevents a PR-planted .git/hooks/pre-commit
# from tampering with the commit.
git -c core.hooksPath=/dev/null \
commit -m "chore: format CHANGELOG files with prettier"
echo "has_changes=true" >> "$GITHUB_OUTPUT"
# Refuse to push if the just-created commit touches anything other than
# CHANGELOG.md files. Runs before the App token is issued.
- name: Verify commit scope
if: steps.format.outputs.has_changes == 'true'
run: |
set -euo pipefail
mapfile -t touched < <(git diff-tree --no-commit-id --name-only -r HEAD)
# Match the selector used above (^|/)CHANGELOG\.md$ — basename only,
# so MY_CHANGELOG.md or CHANGELOG.md.bak are rejected.
for path in "${touched[@]}"; do
if [[ "$path" != "CHANGELOG.md" && "$path" != */CHANGELOG.md ]]; then
echo "::error::Refusing to push: commit touches non-CHANGELOG path: $path"
exit 1
fi
done
# Create the App token only after untrusted steps so prettier / npm
# install never had it in reach. Token inherits the App installation's
# granted permissions — do NOT add `permission-*` inputs here until the
# App installation is confirmed to grant every requested permission;
# see the matching note on release-please.yml.
- name: Create App token for push
if: steps.format.outputs.has_changes == 'true'
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
id: app-token
with:
app-id: ${{ vars.PROMPTFOOBOT_APP_ID }}
private-key: ${{ secrets.PROMPTFOOBOT_APP_PRIVATE_KEY }}
- name: Push formatted CHANGELOG commit
if: steps.format.outputs.has_changes == 'true'
env:
HEAD_REF: ${{ github.event.pull_request.head.ref }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
set -euo pipefail
# GitHub's Git smart HTTP endpoint expects Basic auth. Build the
# header just-in-time so credentials are scoped to this invocation
# instead of being persisted in .git/config.
basic_auth="$(printf 'x-access-token:%s' "${GH_TOKEN}" | base64 | tr -d '\n')"
git -c core.hooksPath=/dev/null \
-c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${basic_auth}" \
push origin "HEAD:${HEAD_REF}"
@@ -0,0 +1,76 @@
name: release-please-sha-drift
# Guards against the failure mode fixed in PR #9517: release-please's
# `last-release-sha` in release-please-config.json is a STATIC pin that floors
# release-please's GraphQL "fetch merge commits" walk. Nobody advances it
# automatically, so the scan window grows every release and eventually the
# GraphQL query times out ("Something went wrong while executing your query"),
# failing the release-please job.
#
# This check measures how far last-release-sha is behind HEAD and fails loudly
# while there is still plenty of headroom, turning a silent release-breaking
# timeout into an early, obvious signal. The fix when it fires is a one-line
# bump of last-release-sha to the OLDEST current package release commit (never
# newer than any package's last release, or that package's unreleased commits
# get dropped).
on:
schedule:
# Mondays 14:00 UTC — proactive, since drift accrues with no config change.
- cron: '0 14 * * 1'
workflow_dispatch:
pull_request:
branches:
- main
paths:
- 'release-please-config.json'
permissions:
contents: read
concurrency:
group: release-please-sha-drift-${{ github.ref }}
cancel-in-progress: true
jobs:
check-drift:
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- name: Checkout (full history)
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
# Need full history to measure the distance from last-release-sha to HEAD.
fetch-depth: 0
- name: Check last-release-sha drift
env:
# Fail before the GraphQL scan gets near the size that timed out (~468
# commits in the #9517 incident; 144 still scanned fine). 250 leaves
# margin to bump the pin without a fire drill.
MAX_DRIFT: '250'
run: |
set -euo pipefail
sha="$(jq -r '."last-release-sha" // empty' release-please-config.json)"
if [[ -z "$sha" ]]; then
echo "::notice::no last-release-sha configured in release-please-config.json; nothing to check"
exit 0
fi
if ! git cat-file -e "${sha}^{commit}" 2>/dev/null; then
echo "::error file=release-please-config.json::last-release-sha ${sha} is not a reachable commit"
exit 1
fi
behind="$(git rev-list --count "${sha}..HEAD")"
echo "last-release-sha ${sha} is ${behind} commits behind HEAD (threshold ${MAX_DRIFT})"
if (( behind > MAX_DRIFT )); then
echo "::error file=release-please-config.json::release-please last-release-sha is ${behind} commits behind HEAD (> ${MAX_DRIFT}). The release-please merge-commit scan will eventually time out (see PR #9517). Advance last-release-sha to the OLDEST current package release commit — never newer than any package's last release, or that package loses its unreleased commits."
exit 1
fi
echo "::notice::release-please last-release-sha drift OK (${behind} <= ${MAX_DRIFT})"
+423
View File
@@ -0,0 +1,423 @@
name: release-please
run-name: promptfoo release by @${{ github.actor }}
on:
push:
branches:
- main
workflow_dispatch:
inputs:
tag_name:
description: 'Optional existing release tag to publish to npm (e.g., 0.121.10)'
required: false
type: string
permissions:
contents: read
concurrency:
group: release-please-${{ github.ref }}
cancel-in-progress: false
jobs:
release-please:
if: github.event_name != 'workflow_dispatch' || inputs.tag_name == ''
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: write
pull-requests: write
outputs:
release_created: ${{ steps.release.outputs.release_created }}
tag_name: ${{ steps.release.outputs.tag_name }}
code_scan_action_release_created: ${{ steps.release.outputs['code-scan-action--release_created'] }}
code_scan_action_tag_name: ${{ steps.release.outputs['code-scan-action--tag_name'] }}
code_scan_action_version: ${{ steps.release.outputs['code-scan-action--version'] }}
steps:
# Use GitHub App token so the created PR triggers CI workflows
# (PRs created with GITHUB_TOKEN don't trigger workflows to prevent loops).
# Token inherits the App installation's granted permissions. Previous
# attempt to narrow via `permission-*` inputs failed with 422 because
# the PROMPTFOOBOT installation does not grant all of them (release-please
# PR #8796 landed green, then main broke on the next release-please run).
# If we want to narrow here, first widen the App installation permissions
# in the GitHub App settings, then re-add the `permission-*` inputs.
- uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
id: app-token
with:
app-id: ${{ vars.PROMPTFOOBOT_APP_ID }}
private-key: ${{ secrets.PROMPTFOOBOT_APP_PRIVATE_KEY }}
- uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0
id: release
with:
token: ${{ steps.app-token.outputs.token }}
build:
# release-please can create a release, then still fail while opening the
# next release PR. Preserve artifact publication when the release outputs
# prove a release was already created.
if: >-
${{
always() &&
!cancelled() &&
(
needs.release-please.outputs.release_created == 'true' ||
needs.release-please.outputs.code_scan_action_release_created == 'true'
)
}}
runs-on: ubuntu-latest
timeout-minutes: 10
needs: release-please
permissions:
contents: read
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: '.nvmrc'
cache: 'npm'
# Node 24 ships with npm 11.6.2 which has a lockfile compatibility bug (npm/cli#8669)
- run: npm install -g npm@11.11.0
- run: npm ci
- run: npm test
publish-npm:
if: >-
${{
always() &&
!cancelled() &&
needs.release-please.outputs.release_created == 'true' &&
needs.build.result == 'success'
}}
needs: [build, release-please]
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: '.nvmrc'
cache: 'npm'
registry-url: 'https://registry.npmjs.org'
# Node 24 ships with npm 11.6.2 which has a lockfile compatibility bug (npm/cli#8669)
- run: npm install -g npm@11.11.0
- run: npm ci
# Empty NODE_AUTH_TOKEN so npm uses OIDC trusted publishing instead of the
# placeholder token actions/setup-node writes into .npmrc (actions/setup-node#1440).
- run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ''
PROMPTFOO_POSTHOG_KEY: ${{ secrets.PROMPTFOO_POSTHOG_KEY }}
publish-npm-backfill:
# npm trusted publishing is bound to this workflow filename on npmjs.com,
# so manual backfills must stay inside release-please.yml.
if: github.event_name == 'workflow_dispatch' && inputs.tag_name != ''
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
id-token: write
env:
TAG_NAME: ${{ inputs.tag_name }}
steps:
- name: Validate release tag
run: |
if [[ ! "$TAG_NAME" =~ ^[0-9]+\.[0-9]+\.[0-9]+([.-][A-Za-z0-9.-]+)?$ ]]; then
echo "::error::TAG_NAME '$TAG_NAME' is empty or invalid"
exit 1
fi
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: refs/tags/${{ inputs.tag_name }}
- name: Verify package version matches tag
run: |
package_version="$(node -p "require('./package.json').version")"
if [[ "$package_version" != "$TAG_NAME" ]]; then
echo "::error::package.json version '$package_version' does not match tag '$TAG_NAME'"
exit 1
fi
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: '.nvmrc'
cache: 'npm'
registry-url: 'https://registry.npmjs.org'
# Node 24 ships with npm 11.6.2 which has a lockfile compatibility bug (npm/cli#8669)
- run: npm install -g npm@11.11.0
- run: npm ci
- name: Publish package
run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ''
PROMPTFOO_POSTHOG_KEY: ${{ secrets.PROMPTFOO_POSTHOG_KEY }}
docker:
if: >-
${{
always() &&
!cancelled() &&
needs.publish-npm.result == 'success' &&
needs.release-please.outputs.release_created == 'true'
}}
needs: [publish-npm, release-please]
permissions:
contents: read
packages: write
id-token: write
attestations: write
uses: ./.github/workflows/docker.yml
with:
tag_name: ${{ needs.release-please.outputs.tag_name }}
publish-code-scan-action:
if: >-
${{
always() &&
!cancelled() &&
needs.release-please.outputs.code_scan_action_release_created == 'true' &&
needs.build.result == 'success'
}}
runs-on: ubuntu-latest
timeout-minutes: 15
# Gate on `build` so root test failures block the mirror — code-scan-action
# imports shared source from ../../src, so broken root code means broken action.
# `publish-npm` is in `needs` for ordering only, not gating — see the
# npm-availability check below for the rationale.
needs: [release-please, build, publish-npm]
permissions:
contents: read
env:
# Plain release-artifact files mirrored into promptfoo/code-scan-action.
# `dist/` (a directory) and `.release-source.json` (generated below, not
# copied from source) are handled separately in each step.
MIRROR_ARTIFACT_FILES: |-
action.yml
README.md
CHANGELOG.md
steps:
# Token is scoped to the foreign repo (code-scan-action) via owner/
# repositories. Do NOT add `permission-*` inputs here until the App
# installation is confirmed to grant every requested permission — see
# the matching note on the release-please job above.
- name: Create app token for code-scan-action mirror
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
id: mirror-token
with:
app-id: ${{ vars.PROMPTFOOBOT_APP_ID }}
private-key: ${{ secrets.PROMPTFOOBOT_APP_PRIVATE_KEY }}
owner: promptfoo
repositories: code-scan-action
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: '.nvmrc'
cache: 'npm'
# Node 24 ships with npm 11.6.2 which has a lockfile compatibility bug (npm/cli#8669)
- run: npm install -g npm@11.11.0
# The action's runtime install is pinned to this commit's root package.json
# version, inlined into dist/ at build time. Refuse to mirror a release whose
# pinned scanner version never reached npm (e.g. a failed publish-npm awaiting
# backfill) — otherwise every consumer scan fails at install time with ETARGET
# (no matching version found).
# The registry is the ground truth (a publish-npm job result would wrongly
# block a version that was backfilled later); `needs: publish-npm` guarantees
# a same-run publish has finished before this check runs, and the retries
# absorb registry read-after-write lag for a just-published version.
- name: Verify pinned promptfoo version is published to npm
run: |
set -euo pipefail
version="$(node -p "require('./package.json').version")"
for attempt in 1 2 3 4 5; do
if npm view "promptfoo@${version}" version; then
exit 0
fi
if [[ "$attempt" -lt 5 ]]; then
echo "promptfoo@${version} not visible on npm (attempt ${attempt}/5); retrying in 30s"
sleep 30
fi
done
echo "::error::promptfoo@${version} is not published to npm; the mirrored action would fail at install time"
exit 1
# Install root deps first; code-scan-action imports shared source from ../../src
- run: npm ci
- name: Install Code Scan Action dependencies
working-directory: code-scan-action
run: npm ci
- name: Type Check Code Scan Action
working-directory: code-scan-action
run: npm run tsc
- name: Build Code Scan Action
working-directory: code-scan-action
run: npm run build
# Companion to the npm-availability gate above: that step validates the version
# read from package.json, this one asserts the built bundle actually embeds the
# same pin — so a future refactor of the pin source in main.ts cannot desync
# the gate from what ships.
- name: Verify built dist embeds the published scanner pin
run: |
set -euo pipefail
version="$(node -p "require('./package.json').version")"
count="$(grep -Fc "\"${version}\"" code-scan-action/dist/index.js || true)"
echo "dist/index.js embeds \"${version}\" ${count} time(s)"
if [[ "$count" -eq 0 ]]; then
echo "::error::dist/index.js does not embed promptfoo@${version}; the runtime pin has desynced from the npm-availability gate"
exit 1
fi
- name: Prepare mirror release payload
env:
CODE_SCAN_ACTION_VERSION: ${{ needs.release-please.outputs.code_scan_action_version }}
SOURCE_SHA: ${{ github.sha }}
SOURCE_TAG: ${{ needs.release-please.outputs.code_scan_action_tag_name }}
run: |
set -euo pipefail
export_dir="$RUNNER_TEMP/code-scan-action-export"
rm -rf "$export_dir"
mkdir -p "$export_dir"
mapfile -t mirror_artifact_files <<< "$MIRROR_ARTIFACT_FILES"
for file in "${mirror_artifact_files[@]}"; do
cp "code-scan-action/$file" "$export_dir/$file"
done
cp -R code-scan-action/dist "$export_dir/dist"
cat > "$export_dir/.release-source.json" <<JSON
{
"repository": "${{ github.repository }}",
"sourceSha": "$SOURCE_SHA",
"sourceTag": "$SOURCE_TAG",
"packagePath": "code-scan-action",
"version": "$CODE_SCAN_ACTION_VERSION"
}
JSON
# Hand the executable payload (dist/ plus the action.yml that selects the
# entrypoint) to attest-code-scan-action — see that job for why attestation
# is isolated from this one.
- name: Upload release payload for attestation
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: code-scan-action-release-payload
if-no-files-found: error
path: |
${{ runner.temp }}/code-scan-action-export/dist
${{ runner.temp }}/code-scan-action-export/action.yml
- name: Open mirror release PR
env:
GH_TOKEN: ${{ steps.mirror-token.outputs.token }}
CODE_SCAN_ACTION_VERSION: ${{ needs.release-please.outputs.code_scan_action_version }}
SOURCE_SHA: ${{ github.sha }}
SOURCE_TAG: ${{ needs.release-please.outputs.code_scan_action_tag_name }}
run: |
set -euo pipefail
branch="release/code-scan-action-v${CODE_SCAN_ACTION_VERSION}"
export_dir="$RUNNER_TEMP/code-scan-action-export"
mirror_dir="$RUNNER_TEMP/code-scan-action-mirror"
# Keep the app token out of the remote URL and out of .git/config;
# attach it per-command via `-c http.extraheader=...` and disable git
# hooks so nothing executes in the token's env. GitHub's Git smart
# HTTP endpoint expects Basic auth.
basic_auth="$(printf 'x-access-token:%s' "${GH_TOKEN}" | base64 | tr -d '\n')"
auth_header="http.https://github.com/.extraheader=AUTHORIZATION: basic ${basic_auth}"
rm -rf "$mirror_dir"
git -c "$auth_header" -c core.hooksPath=/dev/null clone \
"https://github.com/promptfoo/code-scan-action.git" "$mirror_dir"
cd "$mirror_dir"
git switch -C "$branch" origin/main
# Replace only the generated release-artifact paths. The mirror repo
# owns every other path (.github/, renovate.json, AGENTS.md, ...);
# syncing the whole tree with `rsync --delete` silently deleted those
# and broke the mirror's validate-release-pr check.
rm -rf dist
cp -R "$export_dir/dist" dist
mapfile -t mirror_artifact_files <<< "$MIRROR_ARTIFACT_FILES"
for file in "${mirror_artifact_files[@]}"; do
cp "$export_dir/$file" "$file"
done
cp "$export_dir/.release-source.json" .release-source.json
git add "${mirror_artifact_files[@]}" dist .release-source.json
if git diff --cached --quiet; then
echo "No mirror changes to publish for code-scan-action v${CODE_SCAN_ACTION_VERSION}"
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git -c core.hooksPath=/dev/null \
commit -m "Release code-scan-action v${CODE_SCAN_ACTION_VERSION}"
git -c "$auth_header" -c core.hooksPath=/dev/null \
push --force-with-lease origin "HEAD:$branch"
pr_body=$(cat <<BODY
Automated release mirror for \`@promptfoo/code-scan-action\` v${CODE_SCAN_ACTION_VERSION}.
Source: promptfoo/promptfoo@${SOURCE_SHA}
Source tag: ${SOURCE_TAG}
This PR is generated from the monorepo release workflow. The mirror repository validation workflow rebuilds from \`.release-source.json\` and checks that the generated artifacts match.
BODY
)
existing_pr="$(gh pr list --repo promptfoo/code-scan-action --head "$branch" --base main --state open --json number --jq '.[0].number // empty')"
if [[ -n "$existing_pr" ]]; then
gh pr edit "$existing_pr" --repo promptfoo/code-scan-action --title "Release code-scan-action v${CODE_SCAN_ACTION_VERSION}" --body "$pr_body"
else
gh pr create --repo promptfoo/code-scan-action --base main --head "$branch" --title "Release code-scan-action v${CODE_SCAN_ACTION_VERSION}" --body "$pr_body"
fi
attest-code-scan-action:
# Signed build provenance for the exact bytes mirrored into
# promptfoo/code-scan-action — both the dist/ bundle and the action.yml that
# selects the entrypoint — so consumers can verify the committed artifacts came
# from this workflow without re-running the build:
# gh attestation verify dist/index.js --repo promptfoo/promptfoo
# gh attestation verify action.yml --repo promptfoo/promptfoo
# Kept as a separate job so id-token/attestations permissions are confined to
# steps that never install packages or execute repository code — a compromised
# dependency lifecycle script in the publish job must not be able to mint OIDC
# tokens or sign attestations as this repository. Default `needs` gating (run
# only when the publish job succeeded) is exactly the behavior wanted here.
needs: [publish-code-scan-action]
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
id-token: write
attestations: write
steps:
- name: Download release payload
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: code-scan-action-release-payload
path: ${{ runner.temp }}/code-scan-action-attest
- name: Attest build provenance for mirrored artifacts
uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3.2.0
with:
subject-path: |
${{ runner.temp }}/code-scan-action-attest/dist/*
${{ runner.temp }}/code-scan-action-attest/action.yml
@@ -0,0 +1,138 @@
name: Tusk Test Runner - Vitest unit tests (src/app)
# Required for Tusk
permissions:
contents: read
on:
workflow_dispatch:
inputs:
runId:
description: 'Tusk Run ID'
required: true
tuskUrl:
description: 'Tusk server URL'
required: true
commitSha:
description: 'Commit SHA to checkout'
required: true
runnerIndexes:
description: 'Runner indexes'
required: false
default: '["1"]'
jobs:
test-action:
name: Tusk Test Runner
runs-on: ubuntu-latest
timeout-minutes: 8
strategy:
matrix:
runnerIndex: ${{ fromJson(github.event.inputs.runnerIndexes) }}
steps:
- name: Checkout
id: checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ github.event.inputs.commitSha }} # Required for Tusk to access files for the commit being tested
- name: Use Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: '.nvmrc'
cache: 'npm'
# Node 24 ships with npm 11.6.2 which has a lockfile compatibility bug (npm/cli#8669).
# Pinned exact (not @latest): npm 12.0.0 blocks postinstall scripts not covered by
# allowScripts, which silently skips e.g. the Playwright browser download.
- name: Upgrade npm
run: npm install -g npm@11.18.0
- name: Install Dependencies
run: |
npm ci
npm install @vitest/coverage-v8
- name: Start runner
id: test-action
uses: Use-Tusk/test-runner@c83efabca725843d6cdb5d3e9c4083baf7c8282e # v1
# See https://github.com/Use-Tusk/test-runner for full details and examples.
with:
# Required to use parallelization
runnerIndex: ${{ matrix.runnerIndex }}
# Required for the test runner, do not remove this input
runId: ${{ github.event.inputs.runId }}
# Required for the test runner, do not remove this input
tuskUrl: ${{ github.event.inputs.tuskUrl }}
# Required for the test runner, do not remove this input
commitSha: ${{ github.event.inputs.commitSha }}
# Your Tusk auth token. It is recommended to add it to your repo's secrets.
# Please adapt the secret name accordingly if you have named it differently.
authToken: ${{ secrets.TUSK_AUTH_TOKEN }}
# Vitest for the React app tests
testFramework: 'vitest'
# Test file regex to match Vitest test files in src/app
testFileRegex: '^src/app/.*\.(test|spec)\.(js|jsx|ts|tsx)$'
# This will be the working directory for all commands
appDir: 'src/app'
# Format with Biome (JS/TS/JSON) and Prettier (CSS/MD/YAML), then lint and type-check
lintScript: |
set -e
FILE_PATH="{{file}}"
# Validate no shell metacharacters in file path
if [[ "$FILE_PATH" =~ [\;\|\&\$\`\<\>\(\)] ]]; then
echo "Error: Invalid characters in file path: $FILE_PATH"
exit 1
fi
EXT="${FILE_PATH##*.}"
FULL_PATH="src/app/$FILE_PATH"
cd ../..
# Format based on file extension (use -- to prevent flag injection)
case "$EXT" in
js|jsx|mjs|cjs|ts|tsx|json)
npx @biomejs/biome format --write -- "$FULL_PATH"
npx @biomejs/biome lint --write -- "$FULL_PATH"
;;
css|scss|html|md|mdc|mdx|yaml|yml)
npx prettier --write -- "$FULL_PATH"
;;
*)
echo "Skipping unsupported file type: $FILE_PATH"
;;
esac
cd src/app
# Use incremental build to reduce latency for subsequent build checks
# However, this means that we need to run with concurrency of 1 (on Tusk)
npx tsc --build --noEmit --incremental
# The script to run Vitest tests for individual files
testScript: 'npx vitest run {{file}} --reporter=default'
coverageScript: |
npx vitest run {{testFilePaths}} \
--coverage \
--coverage.reportsDirectory=coverage \
--coverage.reporter=json-summary \
--coverage.reporter=json \
--coverage.reportOnFailure \
--coverage.include="**/*.{ts,tsx}" \
--coverage.exclude="**/*.{test,spec}.{ts,tsx}"
# Max concurrency is set to 1 because we need to run tsc with incremental build
maxConcurrency: 1
@@ -0,0 +1,112 @@
name: Tusk Test Runner - Vitest unit tests (test directory)
# Required for Tusk
permissions:
contents: read
on:
workflow_dispatch:
inputs:
runId:
description: 'Tusk Run ID'
required: true
tuskUrl:
description: 'Tusk server URL'
required: true
commitSha:
description: 'Commit SHA to checkout'
required: true
runnerIndexes:
description: 'Runner indexes'
required: false
default: '["1"]'
jobs:
test-action:
name: Tusk Test Runner
runs-on: ubuntu-latest
timeout-minutes: 10
env:
PROMPTFOO_DISABLE_TELEMETRY: 1
# Required for test parallelization where available, do not remove.
strategy:
matrix:
runnerIndex: ${{ fromJson(github.event.inputs.runnerIndexes) }}
steps:
- name: Checkout
id: checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ github.event.inputs.commitSha }} # Required for Tusk to access files for the commit being tested
- name: Use Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: '.nvmrc'
cache: 'npm'
# Node 24 ships with npm 11.6.2 which has a lockfile compatibility bug (npm/cli#8669).
# Pinned exact (not @latest): npm 12.0.0 blocks postinstall scripts not covered by
# allowScripts, which silently skips e.g. the Playwright browser download.
- name: Upgrade npm
run: npm install -g npm@11.18.0
- name: Install Dependencies
run: |
npm ci
- name: Start runner
id: test-action
uses: Use-Tusk/test-runner@c83efabca725843d6cdb5d3e9c4083baf7c8282e # v1
# See https://github.com/Use-Tusk/test-runner for full details and examples.
with:
# Required for the test runner, do not remove this input
runId: ${{ github.event.inputs.runId }}
# Required for the test runner, do not remove this input
tuskUrl: ${{ github.event.inputs.tuskUrl }}
# Required for the test runner, do not remove this input
commitSha: ${{ github.event.inputs.commitSha }}
# Your Tusk auth token. It is recommended to add it to your repo's secrets.
# Please adapt the secret name accordingly if you have named it differently.
authToken: ${{ secrets.TUSK_AUTH_TOKEN }}
testFramework: 'vitest'
testFileRegex: '^test/.*\.test\.(js|ts|tsx)$'
# Format with Biome (JS/TS/JSON) and Prettier (CSS/MD/YAML), then lint
lintScript: |
FILE="{{file}}"
# Validate no shell metacharacters in file path
if [[ "$FILE" =~ [\;\|\&\$\`\<\>\(\)] ]]; then
echo "Error: Invalid characters in file path: $FILE"
exit 1
fi
EXT="${FILE##*.}"
# Format based on file extension (use -- to prevent flag injection)
case "$EXT" in
js|jsx|mjs|cjs|ts|tsx|json)
npx @biomejs/biome check --write -- "$FILE"
;;
css|scss|html|md|mdc|mdx|yaml|yml)
npx prettier --write -- "$FILE"
;;
*)
echo "Skipping unsupported file type: $FILE"
;;
esac
testScript: 'npx vitest run {{file}} --reporter=verbose'
coverageScript: 'npx vitest run {{testFilePaths}} --coverage --coverage.reporter=json-summary --coverage.reporter=json'
# Required for test parallelization where available, do not remove.
runnerIndex: ${{ matrix.runnerIndex }}
+18
View File
@@ -0,0 +1,18 @@
name: Validate PR Title
on:
pull_request:
types: [opened, edited, synchronize]
permissions:
pull-requests: read
jobs:
validate-pr-title:
name: Validate PR Title
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -0,0 +1,28 @@
name: Validate Renovate Config
on:
pull_request:
paths:
- 'renovate.json'
- '.github/workflows/validate-renovate-config.yml'
push:
branches:
- main
paths:
- 'renovate.json'
- '.github/workflows/validate-renovate-config.yml'
permissions:
contents: read
jobs:
validate:
name: Validate Renovate Configuration
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- name: Validate renovate.json
run: npx --yes --package renovate -- renovate-config-validator