chore: import upstream snapshot with attribution
Sync docs with Docusaurus / sync (push) Waiting to run
Tests / Check if changed (push) Waiting to run
Tests / format (push) Blocked by required conditions
Tests / check-imports (push) Blocked by required conditions
Tests / Unit / macos-latest (push) Blocked by required conditions
Tests / Unit / ubuntu-latest (push) Blocked by required conditions
Tests / Unit / windows-latest (push) Blocked by required conditions
Tests / mypy (push) Blocked by required conditions
Tests / Integration / ubuntu-latest (push) Blocked by required conditions
Tests / Integration / macos-latest (push) Blocked by required conditions
Tests / Integration / windows-latest (push) Blocked by required conditions
Tests / notify-slack-on-failure (push) Blocked by required conditions
Tests / Mark tests as completed (push) Blocked by required conditions
Docker image release / Build base image (push) Waiting to run
CodeQL / Analyze (python) (push) Has been cancelled
Update Platform Components Table / update (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:22:28 +08:00
commit c56bef871b
9296 changed files with 1854228 additions and 0 deletions
+14
View File
@@ -0,0 +1,14 @@
# See https://help.github.com/articles/about-codeowners/ for syntax
# Core Engineering will be the default owners for everything
# in the repo. Unless a later match takes precedence,
# @deepset-ai/core-engineering will be requested for review
# when someone opens a pull request.
* @deepset-ai/open-source-engineering
# Documentation
*.md @deepset-ai/documentation @deepset-ai/open-source-engineering
# Auto-synced API reference (no human reviewers needed, auto-approved by GitHub bot)
docs-website/reference/
docs-website/reference_versioned_docs/
+31
View File
@@ -0,0 +1,31 @@
---
name: Bug report
about: Errors you encountered
title: ''
labels: ''
assignees: ''
type: Bug
---
**Describe the bug**
A clear and concise description of what the bug is.
**Error message**
Error that was thrown (if available)
**Expected behavior**
A clear and concise description of what you expected to happen.
**Additional context**
Add any other context about the problem here, like document types / preprocessing steps / settings of reader etc.
**To Reproduce**
Steps to reproduce the behavior
**FAQ Check**
- [ ] Have you had a look at [our new FAQ page](https://docs.haystack.deepset.ai/docs/faq)?
**System:**
- OS:
- Haystack version:
+5
View File
@@ -0,0 +1,5 @@
blank_issues_enabled: true
contact_links:
- name: Something unclear? Just ask :)
url: https://github.com/deepset-ai/haystack/discussions/new
about: Start a Github discussion with your question
+21
View File
@@ -0,0 +1,21 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
type: Feature
---
**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.
+44
View File
@@ -0,0 +1,44 @@
version: 2
updates:
- package-ecosystem: 'github-actions'
directory: '/'
schedule:
interval: 'daily'
cooldown:
default-days: 1
- package-ecosystem: 'pip'
directory: '/'
schedule:
interval: 'daily'
cooldown:
default-days: 1
- package-ecosystem: 'npm'
directory: '/docs-website'
schedule:
interval: 'daily'
cooldown:
default-days: 1
# Tracks the digest-pinned `COPY --from` image (ghcr.io/astral-sh/uv) in
# docker/Dockerfile.base. Note: the python:3.12-slim base/builder digests are
# carried both as the build_image/base_image ARG defaults in Dockerfile.base
# and in docker/docker-bake.hcl (which overrides them at build time).
# Dependabot does NOT parse images that reach FROM via an ARG, nor the
# `docker run` image strings in workflows, so bump those by hand and keep the
# Dockerfile.base ARG defaults and docker-bake.hcl digests in sync.
- package-ecosystem: 'docker'
directory: '/docker'
schedule:
interval: 'daily'
cooldown:
default-days: 1
# Keeps the digest-pinned OSS-Fuzz base-builder in .clusterfuzzlite/Dockerfile fresh.
- package-ecosystem: 'docker'
directory: '/.clusterfuzzlite'
schedule:
interval: 'daily'
cooldown:
default-days: 1
+37
View File
@@ -0,0 +1,37 @@
# Release lines
1.x:
- base-branch: 'v1.x'
# Proposals
proposal:
- changed-files:
- any-glob-to-any-file: proposals/text/*
# Topics
topic:tests:
- changed-files:
- any-glob-to-any-file: ['test/**/*','test/*']
topic:docker:
- changed-files:
- any-glob-to-any-file: docker/*
topic:CI:
- changed-files:
- any-glob-to-any-file: ['.github/*','.github/**/*']
topic:DX:
- changed-files:
- any-glob-to-any-file: ["CONTRIBUTING.md", ".pre-commit-config.yaml",".gitignore"]
topic:build/distribution:
- changed-files:
- any-glob-to-any-file: pyproject.toml
topic:security:
- changed-files:
- any-glob-to-any-file: SECURITY.md
topic:core:
- changed-files:
- any-glob-to-any-file: haystack/core/**/*
+26
View File
@@ -0,0 +1,26 @@
### Related Issues
- fixes #issue-number
### Proposed Changes:
<!--- In case of a bug: Describe what caused the issue and how you solved it -->
<!--- In case of a feature: Describe what did you add and how it works -->
### How did you test it?
<!-- unit tests, integration tests, manual verification, instructions for manual tests -->
### Notes for the reviewer
<!-- E.g. point out section where the reviewer -->
### Checklist
- I have read the [contributors guidelines](https://github.com/deepset-ai/haystack/blob/main/CONTRIBUTING.md) and the [code of conduct](https://github.com/deepset-ai/haystack/blob/main/code_of_conduct.txt).
- I have updated the related issue with new insights and changes.
- I have added unit tests and updated the docstrings.
- I've used one of the [conventional commit types](https://www.conventionalcommits.org/en/v1.0.0/) for my PR title: `fix:`, `feat:`, `build:`, `chore:`, `ci:`, `docs:`, `style:`, `refactor:`, `perf:`, `test:` and added `!` in case the PR includes breaking changes.
- I have documented my code.
- I have added a release note file, following the [contributors guidelines](https://github.com/deepset-ai/haystack/blob/main/CONTRIBUTING.md#release-notes).
- I have run [pre-commit hooks](https://github.com/deepset-ai/haystack/blob/main/CONTRIBUTING.md#installation) and fixed any issue.
+85
View File
@@ -0,0 +1,85 @@
import importlib
import os
import sys
import traceback
from pathlib import Path
from haystack import logging # noqa: F401 # this is needed to avoid circular imports
def validate_module_imports(root_dir: str, exclude_subdirs: list[str] | None = None) -> tuple[list, list]:
"""
Recursively search for all Python modules and attempt to import them.
This includes both packages (directories with __init__.py) and individual Python files.
"""
imported = []
failed = []
exclude_subdirs = (exclude_subdirs or []) + ["__pycache__"]
# Add the root directory to the Python path
sys.path.insert(0, root_dir)
base_path = Path(root_dir)
for root, _, files in os.walk(root_dir):
if any(subdir in root for subdir in exclude_subdirs):
continue
# Convert path to module format
module_path = ".".join(Path(root).relative_to(base_path.parent).parts)
python_files = [f for f in files if f.endswith(".py")]
# Try importing package and individual files
for file in python_files:
try:
if file == "__init__.py":
module_to_import = module_path
else:
module_name = os.path.splitext(file)[0]
module_to_import = f"{module_path}.{module_name}" if module_path else module_name
importlib.import_module(module_to_import)
imported.append(module_to_import)
except Exception:
failed.append({"module": module_to_import, "traceback": traceback.format_exc()})
return imported, failed
def main() -> None:
"""
This script checks that all Haystack modules can be imported successfully.
This includes both packages and individual Python files.
This can detect several issues, such as:
- Syntax errors in Python files
- Missing dependencies
- Circular imports
- Incorrect type hints without forward references
"""
# Add any subdirectories you want to skip during import checks ("__pycache__" is skipped by default)
exclude_subdirs = ["testing"]
print("Checking imports from all Haystack modules...")
imported, failed = validate_module_imports(root_dir="haystack", exclude_subdirs=exclude_subdirs)
if not imported:
print("\nNO MODULES WERE IMPORTED")
sys.exit(1)
print(f"\nSUCCESSFULLY IMPORTED {len(imported)} MODULES")
if failed:
print(f"\nFAILED TO IMPORT {len(failed)} MODULES:")
for fail in failed:
print(f" - {fail['module']}")
print("\nERRORS:")
for fail in failed:
print(f" - {fail['module']}\n")
print(f" {fail['traceback']}\n\n")
sys.exit(1)
if __name__ == "__main__":
main()
@@ -0,0 +1,102 @@
"""
This script creates an unstable documentation version at the time of branch-off for a new Haystack release.
Between branch-off and the actual release, two unstable doc versions coexist.
If we branch off for 2.20, we have:
1. the target unstable version, 2.20-unstable (lives in docs-website/versioned_docs/version-2.20-unstable)
2. the next unstable version, 2.21-unstable (lives in docs-website/docs)
This script takes care of all the necessary updates to the documentation website.
"""
import argparse
import json
import os
import re
import shutil
import subprocess
import sys
import tempfile
VERSION_VALIDATOR = re.compile(r"^[0-9]+\.[0-9]+$")
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument(
"-v", "--new-version", help="The new unstable version that is being created (e.g. 2.20).", required=True
)
args = parser.parse_args()
if VERSION_VALIDATOR.match(args.new_version) is None:
sys.exit("Version must be formatted like so <major>.<minor>")
target_version = f"{args.new_version}" # e.g., "2.20" - the target release version
major, minor = args.new_version.split(".")
target_unstable = f"{target_version}-unstable" # e.g., "2.20-unstable"
next_unstable = f"{major}.{int(minor) + 1}-unstable" # e.g., "2.21-unstable" - next cycle
versions = [
folder.replace("version-", "")
for folder in os.listdir("docs-website/versioned_docs")
if os.path.isdir(os.path.join("docs-website/versioned_docs", folder))
]
# Check if the versions we're about to create already exist in versioned_docs
if target_version in versions:
sys.exit(f"{target_version} already exists (already released). Aborting.")
if target_unstable in versions:
print(f"{target_unstable} already exists. Nothing to do.")
sys.exit(0)
# Create new unstable from the currently existing one.
# The new unstable will be made stable at a later time by another workflow
print(f"Creating new unstable version {target_unstable} from main")
### Docusaurus updates
# copy docs to versioned_docs/version-target_unstable
shutil.copytree("docs-website/docs", f"docs-website/versioned_docs/version-{target_unstable}")
# copy reference to reference_versioned_docs/version-target_unstable
shutil.copytree("docs-website/reference", f"docs-website/reference_versioned_docs/version-{target_unstable}")
# generate versioned_sidebars/version-target_unstable-sidebars.json from the current sidebars.js
with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as tmp:
tmp_path = tmp.name
subprocess.run(
["node", "docs-website/scripts/extract_sidebar.mjs", "docs-website/sidebars.js", tmp_path], check=True
)
docs_sidebar_dest = f"docs-website/versioned_sidebars/version-{target_unstable}-sidebars.json"
shutil.move(tmp_path, docs_sidebar_dest)
# generate reference_versioned_sidebars/version-target_unstable-sidebars.json from the current reference-sidebars.js
ref_sidebar_dest = f"docs-website/reference_versioned_sidebars/version-{target_unstable}-sidebars.json"
with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as tmp:
tmp_path = tmp.name
subprocess.run(
["node", "docs-website/scripts/extract_sidebar.mjs", "docs-website/reference-sidebars.js", tmp_path], check=True
)
shutil.move(tmp_path, ref_sidebar_dest)
# add unstable version to versions.json
with open("docs-website/versions.json") as f:
versions_list = json.load(f)
versions_list.insert(0, target_unstable)
with open("docs-website/versions.json", "w") as f:
json.dump(versions_list, f)
# add unstable version to reference_versions.json
with open("docs-website/reference_versions.json") as f:
reference_versions_list = json.load(f)
reference_versions_list.insert(0, target_unstable)
with open("docs-website/reference_versions.json", "w") as f:
json.dump(reference_versions_list, f)
# in docusaurus.config.js, replace the target unstable version with the next unstable version
with open("docs-website/docusaurus.config.js") as f:
config = f.read()
config = config.replace(f"label: '{target_unstable}'", f"label: '{next_unstable}'")
with open("docs-website/docusaurus.config.js", "w") as f:
f.write(config)
+120
View File
@@ -0,0 +1,120 @@
"""
This script syncs the Haystack docs HTML files to the deepset workspace for search indexing.
It is used in the docs_search_sync.yml workflow.
1. Collects all HTML files from the docs and reference directories for the stable Haystack version.
2. Uploads the HTML files to the deepset workspace.
- A timestamp-based metadata field is used to track document versions in the workspace.
3. Deletes the old HTML files from the deepset workspace.
- Since most files are overwritten during upload, only a small number of deletions is expected.
- In case MAX_DELETIONS_SAFETY_LIMIT is exceeded, we block the deletion.
"""
import os
import sys
import time
from pathlib import Path
import requests
from deepset_cloud_sdk.workflows.sync_client.files import DeepsetCloudFile, WriteMode, list_files, upload_texts
DEEPSET_WORKSPACE_DOCS_SEARCH = os.environ["DEEPSET_WORKSPACE_DOCS_SEARCH"]
DEEPSET_API_KEY_DOCS_SEARCH = os.environ["DEEPSET_API_KEY_DOCS_SEARCH"]
# If there are more files to delete than this limit, it's likely that something went wrong in the upload process.
MAX_DELETIONS_SAFETY_LIMIT = 20
def collect_docs_files(version: int) -> list[DeepsetCloudFile]:
"""
Collect all HTML files from the docs and reference directories.
Returns a list of DeepsetCloudFile objects.
"""
repo_root = Path(__file__).parent.parent.parent
build_dir = repo_root / "docs-website" / "build"
# we want to exclude previous and temporarily unstable versions (2.x) and next version (next)
exclude = ("2.", "next")
files = []
for section in ("docs", "reference"):
for subfolder in (build_dir / section).iterdir():
if subfolder.is_dir() and not any(x in subfolder.name for x in exclude):
for html_file in subfolder.rglob("*.html"):
files.append(
DeepsetCloudFile(
# The build produces files like docs/agents/index.html or reference/agents-api/index.html.
# For file names, we want to use the parent directory name (agents.html or agents-api.html)
name=f"{html_file.parent.name}.html",
text=html_file.read_text(),
meta={
"type": "api-reference" if section == "reference" else "documentation",
"version": version,
},
)
)
return files
def delete_files(file_names: list[str]) -> None:
"""
Delete files from the deepset workspace.
"""
url = f"https://api.cloud.deepset.ai/api/v1/workspaces/{DEEPSET_WORKSPACE_DOCS_SEARCH}/files"
payload = {"names": file_names}
headers = {"Accept": "application/json", "Authorization": f"Bearer {DEEPSET_API_KEY_DOCS_SEARCH}"}
response = requests.delete(url, json=payload, headers=headers, timeout=300)
response.raise_for_status()
if __name__ == "__main__":
version = time.time_ns()
print(f"Docs version: {version}")
print("Collecting docs files from build directory")
dc_files = collect_docs_files(version)
print(f"Collected {len(dc_files)} docs files")
if len(dc_files) == 0:
print("No docs files found. Something is wrong. Exiting.")
sys.exit(1)
print("Uploading docs files to deepset")
summary = upload_texts(
workspace_name=DEEPSET_WORKSPACE_DOCS_SEARCH,
files=dc_files,
api_key=DEEPSET_API_KEY_DOCS_SEARCH,
blocking=True, # Very important to ensure that DC is up to date when we query for deletion
timeout_s=300,
show_progress=True,
write_mode=WriteMode.OVERWRITE,
enable_parallel_processing=True,
)
print(f"Uploaded docs files to deepset\n{summary}")
if summary.failed_upload_count > 0:
print("Failed to upload some docs files. Stopping to prevent risky deletion of old files.")
sys.exit(1)
print("Listing old docs files from deepset")
odata_filter = f"version lt '{version}'"
old_files_names = [
f.name
for batch in list_files(
workspace_name=DEEPSET_WORKSPACE_DOCS_SEARCH, api_key=DEEPSET_API_KEY_DOCS_SEARCH, odata_filter=odata_filter
)
for f in batch
]
print(f"Found {len(old_files_names)} old files to delete")
if len(old_files_names) > MAX_DELETIONS_SAFETY_LIMIT:
print(
f"Found >{MAX_DELETIONS_SAFETY_LIMIT} old files to delete. "
"Stopping because something could have gone wrong in the upload process."
)
sys.exit(1)
if len(old_files_names) > 0:
print("Deleting old docs files from deepset")
delete_files(old_files_names)
print("Deleted old docs files from deepset")
+46
View File
@@ -0,0 +1,46 @@
import ast
import hashlib
from collections.abc import Iterator
from pathlib import Path
def docstrings_checksum(python_files: Iterator[Path]) -> str:
"""
Calculate the checksum of the docstrings in the given Python files.
"""
files_content = (f.read_text() for f in python_files)
trees = (ast.parse(c) for c in files_content)
# Get all docstrings from async functions, functions,
# classes and modules definitions
docstrings = []
for tree in trees:
for node in ast.walk(tree):
if not isinstance(node, (ast.AsyncFunctionDef, ast.FunctionDef, ast.ClassDef, ast.Module)):
# Skip all node types that can't have docstrings to prevent failures
continue
docstring = ast.get_docstring(node)
if docstring:
docstrings.append(docstring)
# Sort them to be safe, since ast.walk() returns
# nodes in no specified order.
# See https://docs.python.org/3/library/ast.html#ast.walk
docstrings.sort()
return hashlib.md5(str(docstrings).encode("utf-8")).hexdigest()
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--root", help="Haystack root folder", required=True, type=Path)
args = parser.parse_args()
# Get all Haystack and rest_api python files
root: Path = args.root.absolute()
haystack_files = root.glob("haystack/**/*.py")
md5 = docstrings_checksum(haystack_files)
print(md5)
+171
View File
@@ -0,0 +1,171 @@
#!/bin/bash
# parse_validate_version.sh - Parse and validate version for release
#
# Usage: ./parse_validate_version.sh <version>
# Output: Writes to $GITHUB_OUTPUT if set, otherwise to stdout
#
# Example:
# ./parse_validate_version.sh v2.99.0-rc1
#
# This script is used in the release.yml workflow to parse and validate the version to be released.
# Covers several checks to prevent accidental releases of incorrect versions.
set -euo pipefail
# --- Helpers ---
fail() {
echo ""
echo -e "$1"
echo ""
exit 1
}
ok() {
echo "$1"
}
tag_exists() {
git tag -l "$1" | grep -q "^$1$"
}
branch_exists() {
git ls-remote --heads origin "$1" | grep -q "$1"
}
# --- Parse and validate version ---
VERSION="${1#v}" # Strip 'v' prefix
echo ""
echo "️ Validating: ${1}"
echo ""
if [[ ! "${VERSION}" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)(-rc([0-9]+))?$ ]]; then
fail "Invalid version format: $1\n\n"\
"Expected format: vMAJOR.MINOR.PATCH or vMAJOR.MINOR.PATCH-rcN\n"\
"Examples: v2.99.0-rc1, v2.99.0, v2.99.1-rc1"
fi
ok "Version format is valid"
MAJOR="${BASH_REMATCH[1]}"
MINOR="${BASH_REMATCH[2]}"
PATCH="${BASH_REMATCH[3]}"
RC_NUM="${BASH_REMATCH[5]:-0}"
if [[ "${RC_NUM}" == "0" && "${VERSION}" == *"-rc0" ]]; then
fail "Cannot release rc0\n\n"\
"rc0 is an internal marker created automatically during branch-off.\n"\
"Release candidates start at rc1."
fi
MAJOR_MINOR="${MAJOR}.${MINOR}"
RELEASE_BRANCH="v${MAJOR_MINOR}.x"
TAG="v${VERSION}"
IS_RC="false"
[[ "${RC_NUM}" != "0" ]] && IS_RC="true"
IS_FIRST_RC="false"
if [[ "${PATCH}" == "0" && "${RC_NUM}" == "1" ]]; then
IS_FIRST_RC="true"
fi
# 1. Tag must not already exist
if tag_exists "${TAG}"; then
fail "Version ${TAG} was already released\n\n"\
"Each version can only be released once.\n"\
"To publish changes, release the next RC or patch version."
fi
ok "Tag ${TAG} does not exist"
# 2. Checks based on release type
if [[ "${IS_FIRST_RC}" == "true" ]]; then
# First RC of minor: branch must NOT exist yet
if branch_exists "${RELEASE_BRANCH}"; then
fail "Branch ${RELEASE_BRANCH} already exists\n\n"\
"The first RC of a minor (e.g., v${MAJOR_MINOR}.0-rc1) creates the release branch.\n"\
"Since the branch exists, this minor was likely already started.\n"\
"Did you mean to release the next RC (rc2, rc3...) or a patch (v${MAJOR_MINOR}.1-rc1)?"
fi
ok "Branch ${RELEASE_BRANCH} does not exist"
# First RC of minor: VERSION.txt must contain rc0
EXPECTED="${MAJOR_MINOR}.0-rc0"
ACTUAL=$(cat VERSION.txt)
if [[ "${ACTUAL}" != "${EXPECTED}" ]]; then
ACTUAL_MINOR=$(echo "${ACTUAL}" | cut -d. -f1,2)
fail "Cannot release v${MAJOR_MINOR}.0-rc1 from this branch\n\n"\
"The main branch is prepared for version ${ACTUAL_MINOR}, not ${MAJOR_MINOR}.\n"\
"Check that you're releasing the correct version."
fi
ok "VERSION.txt = ${EXPECTED}"
else
# Not first RC: branch MUST exist
if ! branch_exists "${RELEASE_BRANCH}"; then
if [[ "${PATCH}" == "0" ]]; then
fail "Branch ${RELEASE_BRANCH} does not exist\n\n"\
"For subsequent RCs (rc2, rc3...), the release branch must already exist.\n"\
"Release the first RC (v${MAJOR_MINOR}.0-rc1) first to create the branch."
else
fail "Branch ${RELEASE_BRANCH} does not exist\n\n"\
"For patch releases, the release branch must already exist.\n"\
"The minor version (v${MAJOR_MINOR}.0) must be released before any patches."
fi
fi
ok "Branch ${RELEASE_BRANCH} exists"
# Subsequent RC (rc2, rc3...): previous RC must exist
if [[ "${RC_NUM}" -gt 1 ]]; then
PREV_RC_NUM=$((RC_NUM - 1))
PREV_TAG="v${MAJOR_MINOR}.${PATCH}-rc${PREV_RC_NUM}"
if ! tag_exists "${PREV_TAG}"; then
fail "Cannot release v${MAJOR_MINOR}.${PATCH}-rc${RC_NUM}\n\n"\
"Previous RC (${PREV_TAG}) was not found.\n"\
"RC versions must be sequential. Release rc${PREV_RC_NUM} first."
fi
ok "Previous tag ${PREV_TAG} exists"
fi
# Final release: at least one RC must exist
if [[ "${RC_NUM}" == "0" ]]; then
RC_TAGS=$(git tag -l "v${MAJOR_MINOR}.${PATCH}-rc*" | grep -v "\-rc0$" || true)
if [[ -z "${RC_TAGS}" ]]; then
fail "Cannot release stable version v${MAJOR_MINOR}.${PATCH}\n\n"\
"No release candidate found for this version.\n"\
"Stable releases require at least one RC first (e.g., v${MAJOR_MINOR}.${PATCH}-rc1)."
fi
LAST_RC=$(echo "${RC_TAGS}" | sort -V | tail -n1)
ok "Found RC: ${LAST_RC}"
# Check Tests workflow passed (only if credentials available)
if [[ -n "${GH_TOKEN:-}" && -n "${GITHUB_REPOSITORY:-}" ]]; then
RC_SHA=$(git rev-list -n 1 "${LAST_RC}")
RESULT=$(gh api "/repos/${GITHUB_REPOSITORY}/actions/runs?head_sha=${RC_SHA}&status=success" \
--jq '.workflow_runs[] | select(.name == "Tests") | .conclusion' 2>/dev/null || true)
if [[ -z "${RESULT}" ]]; then
fail "Cannot release stable version v${MAJOR_MINOR}.${PATCH}\n\n"\
"Tests did not pass on the last RC (${LAST_RC}).\n"\
"Wait for tests to complete, or release a new RC with fixes."
fi
ok "Tests passed on ${LAST_RC}"
fi
fi
fi
echo ""
ok "All validations passed!"
echo ""
# --- Output to GITHUB_OUTPUT (or stdout for local testing) ---
OUTPUT_FILE="${GITHUB_OUTPUT:-/dev/stdout}"
{
echo "version=${VERSION}"
echo "major_minor=${MAJOR_MINOR}"
echo "release_branch=${RELEASE_BRANCH}"
echo "is_rc=${IS_RC}"
echo "is_first_rc=${IS_FIRST_RC}"
} >> "${OUTPUT_FILE}"
+96
View File
@@ -0,0 +1,96 @@
#!/bin/bash
# prepare_release_notification.sh - Prepare Slack notification for release outcome
#
# Requires: VERSION, RUN_URL, HAS_FAILURE, GH_TOKEN, GITHUB_REPOSITORY
# Optional: IS_RC, IS_FIRST_RC, MAJOR_MINOR, GITHUB_URL, PYPI_URL, DOCKER_URL,
# BUMP_VERSION_PR_URL, DC_PIPELINE_TEMPLATES_PR_URL, DC_CUSTOM_NODES_PR_URL,
# HAYSTACK_RUNTIME_PR_URL, GITHUB_WORKSPACE
# Output: slack_payload.json
#
# This script is used in the release.yml workflow to prepare the notification payload
# sent to Slack after a release completes (success or failure).
# Text uses Slack mrkdwn format: *bold*, <url|label> for links.
set -euo pipefail
PAYLOAD_FILE="${GITHUB_WORKSPACE:-/tmp}/slack_payload.json"
write_payload() {
jq -n --arg text "$TXT" '{
text: $text,
blocks: [{ type: "section", text: { type: "mrkdwn", text: $text } }]
}' > "$PAYLOAD_FILE"
}
if [[ "${HAS_FAILURE}" == "true" ]]; then
TXT=":red_circle: Release *${VERSION}* failed"
TXT+=$'\n'"Check workflow run for details: <${RUN_URL}|View Logs>"
write_payload
exit 0
fi
# Success case
TXT=":white_check_mark: Release *${VERSION}* completed successfully"
# Add artifact URLs if available
if [[ -n "${GITHUB_URL:-}" || -n "${PYPI_URL:-}" || -n "${DOCKER_URL:-}" ]]; then
TXT+=$'\n\n:package: *Artifacts:*'
[[ -n "${GITHUB_URL:-}" ]] && TXT+=$'\n'"- <${GITHUB_URL}|Release notes (GitHub)>"
[[ -n "${PYPI_URL:-}" ]] && TXT+=$'\n'"- <${PYPI_URL}|PyPI>"
[[ -n "${DOCKER_URL:-}" ]] && TXT+=$'\n'"- <${DOCKER_URL}|Docker>"
fi
# For RCs, include link to the Tests workflow run
if [[ "${IS_RC:-}" == "true" ]]; then
COMMIT_SHA=$(gh api "repos/${GITHUB_REPOSITORY}/commits/${VERSION}" --jq '.sha' 2>/dev/null || echo "")
if [[ -n "${COMMIT_SHA}" ]]; then
TESTS_RUN=$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs?head_sha=${COMMIT_SHA}" \
--jq '.workflow_runs[] | select(.name == "Tests") | .html_url' 2>/dev/null | head -1 || echo "")
if [[ -n "${TESTS_RUN}" ]]; then
TXT+=$'\n\n'":test_tube: <${TESTS_RUN}|Haystack Tests>"
fi
fi
fi
# For first RC, include the PRs to merge from branch-off
if [[ "${IS_FIRST_RC:-}" == "true" && -n "${BUMP_VERSION_PR_URL:-}" ]]; then
TXT+=$'\n\n'":clipboard: *PRs to merge:*"
TXT+=$'\n'"- <${BUMP_VERSION_PR_URL}|Bump unstable version and create unstable docs>"
fi
# For RCs, include Platform test PRs
if [[ "${IS_RC:-}" == "true" ]]; then
PLATFORM_PRS=""
[[ -n "${DC_PIPELINE_TEMPLATES_PR_URL:-}" ]] && PLATFORM_PRS+=$'\n'"- <${DC_PIPELINE_TEMPLATES_PR_URL}|dc-pipeline-templates>"
[[ -n "${DC_CUSTOM_NODES_PR_URL:-}" ]] && PLATFORM_PRS+=$'\n'"- <${DC_CUSTOM_NODES_PR_URL}|deepset-cloud-custom-nodes>"
[[ -n "${HAYSTACK_RUNTIME_PR_URL:-}" ]] && PLATFORM_PRS+=$'\n'"- <${HAYSTACK_RUNTIME_PR_URL}|haystack-runtime>"
if [[ -n "${PLATFORM_PRS}" ]]; then
TXT+=$'\n\n'":factory: *Test PRs opened on Platform:*${PLATFORM_PRS}"
fi
fi
# For RCs, request Platform Engineering taking over testing
if [[ "${IS_RC:-}" == "true" ]]; then
TXT+=$'\n\n'"This release is marked as a Release Candidate."
TXT+=$'\n'"Notify #deepset-platform-engineering channel on Slack that the RC is available"
TXT+=" and that Platform tests pass/fail, linking the PRs opened on the Platform repositories."
fi
# For final minor releases (vX.Y.0), include the docs promotion PR
if [[ "${VERSION}" =~ ^v[0-9]+\.[0-9]+\.0$ && -n "${MAJOR_MINOR:-}" ]]; then
PROMOTE_DOCS_PR_URL=$(gh pr list --repo "${GITHUB_REPOSITORY}" \
--head "promote-unstable-docs-${MAJOR_MINOR}" --json url --jq '.[0].url' 2>/dev/null || echo "")
if [[ -n "${PROMOTE_DOCS_PR_URL}" ]]; then
TXT+=$'\n\n'":clipboard: *PRs to merge:*"
TXT+=$'\n'"- <${PROMOTE_DOCS_PR_URL}|Promote unstable docs>"
fi
fi
# For final releases (not RCs), include info about pushing release notes to website
if [[ "${IS_RC:-}" != "true" ]]; then
TXT+=$'\n\n'":memo: After refining and finalizing release notes, push them to Haystack website:"
TXT+=$'\n'"\`gh workflow run push_release_notes_to_website.yml -R deepset-ai/haystack -f version=${VERSION}\`"
fi
write_payload
@@ -0,0 +1,117 @@
"""
This script promotes an unstable documentation version to a stable version at the time of a new Haystack release.
To understand how unstable doc versions are created, see create_unstable_docs_docusaurus.py.
"""
import argparse
import json
import os
import re
import shutil
import sys
VERSION_VALIDATOR = re.compile(r"^[0-9]+\.[0-9]+$")
MAX_STABLE_VERSIONS = 5
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("-v", "--version", help="The version to promote to stable (e.g. 2.20).", required=True)
args = parser.parse_args()
if VERSION_VALIDATOR.match(args.version) is None:
sys.exit("Version must be formatted like so <major>.<minor>")
target_version = f"{args.version}" # e.g., "2.20" - the target release version
major, minor = args.version.split(".")
target_unstable = f"{target_version}-unstable" # e.g., "2.20-unstable"
previous_stable = f"{major}.{int(minor) - 1}" # e.g., "2.19" - previous stable release
versions = [
folder.replace("version-", "")
for folder in os.listdir("docs-website/versioned_docs")
if os.path.isdir(os.path.join("docs-website/versioned_docs", folder))
]
if target_version in versions:
sys.exit(f"{target_version} already exists (already released). Aborting.")
if target_unstable not in versions:
sys.exit(f"Can't find version {target_unstable} to promote to {target_version}")
print(f"Promoting unstable version {target_unstable} to stable version {target_version}")
### Docusaurus updates
# move versioned_docs/version-target_unstable to versioned_docs/version-target_version
shutil.move(
f"docs-website/versioned_docs/version-{target_unstable}",
f"docs-website/versioned_docs/version-{target_version}",
)
# move reference_versioned_docs/version-target_unstable to reference_versioned_docs/version-target_version
shutil.move(
f"docs-website/reference_versioned_docs/version-{target_unstable}",
f"docs-website/reference_versioned_docs/version-{target_version}",
)
# move versioned_sidebars/version-target_unstable-sidebars.json
# to versioned_sidebars/version-target_version-sidebars.json
shutil.move(
f"docs-website/versioned_sidebars/version-{target_unstable}-sidebars.json",
f"docs-website/versioned_sidebars/version-{target_version}-sidebars.json",
)
# move reference_versioned_sidebars/version-target_unstable-sidebars.json
# to reference_versioned_sidebars/version-target_version-sidebars.json
shutil.move(
f"docs-website/reference_versioned_sidebars/version-{target_unstable}-sidebars.json",
f"docs-website/reference_versioned_sidebars/version-{target_version}-sidebars.json",
)
# replace unstable version with stable version in versions.json
with open("docs-website/versions.json") as f:
versions_list = json.load(f)
versions_list[versions_list.index(target_unstable)] = target_version
with open("docs-website/versions.json", "w") as f:
json.dump(versions_list, f)
# replace unstable version with stable version in reference_versions.json
with open("docs-website/reference_versions.json") as f:
reference_versions_list = json.load(f)
reference_versions_list[reference_versions_list.index(target_unstable)] = target_version
with open("docs-website/reference_versions.json", "w") as f:
json.dump(reference_versions_list, f)
# in docusaurus.config.js, replace previous stable version with the target version
with open("docs-website/docusaurus.config.js") as f:
config = f.read()
config = config.replace(f"lastVersion: '{previous_stable}'", f"lastVersion: '{target_version}'") # "2.19" -> "2.20"
with open("docs-website/docusaurus.config.js", "w") as f:
f.write(config)
# regenerate vercel.json redirects for inactive versions (those beyond the top MAX_STABLE_VERSIONS)
with open("docs-website/versions.json") as f:
updated_versions = json.load(f)
stable_versions = [v for v in updated_versions if not v.endswith("-unstable")]
inactive_versions = stable_versions[MAX_STABLE_VERSIONS:]
redirects = []
for v in inactive_versions:
redirects.append({"source": f"/docs/{v}/:slug*", "destination": "/docs/:slug*", "permanent": True})
redirects.append({"source": f"/reference/{v}/:slug*", "destination": "/reference/:slug*", "permanent": True})
with open("docs-website/vercel.json") as f:
vercel_config = json.load(f)
existing_redirects = vercel_config.get("redirects", [])
existing_sources = {r.get("source") for r in existing_redirects}
for r in redirects:
if r["source"] not in existing_sources:
existing_redirects.append(r)
vercel_config["redirects"] = existing_redirects
with open("docs-website/vercel.json", "w") as f:
json.dump(vercel_config, f, indent=2)
f.write("\n")
print(f"Updated vercel.json with {len(redirects)} redirect(s) for inactive versions: {inactive_versions}")
@@ -0,0 +1,53 @@
import argparse
import re
import sys
from pathlib import Path
import toml
matcher = re.compile(r"farm-haystack\[(.+)\]")
parser = argparse.ArgumentParser(
prog="pyproject_to_requirements.py", description="Convert pyproject.toml to requirements.txt"
)
parser.add_argument("pyproject_path")
parser.add_argument("--extra", default="")
def resolve(target: str, extras: dict, results: set) -> None:
"""
Resolve the dependencies for a given target.
"""
if target not in extras:
results.add(target)
return
for t in extras[target]:
m = matcher.match(t)
if m:
for i in m.group(1).split(","):
resolve(i, extras, results)
else:
resolve(t, extras, results)
def main(pyproject_path: Path, extra: str = "") -> None:
"""
Convert a pyproject.toml file to a requirements.txt file.
"""
content = toml.load(pyproject_path)
# basic set of dependencies
deps = set(content["project"]["dependencies"])
if extra:
extras = content["project"]["optional-dependencies"]
resolve(extra, extras, deps)
sys.stdout.write("\n".join(sorted(deps)))
sys.stdout.write("\n")
if __name__ == "__main__":
args = parser.parse_args()
pyproject_path = Path(args.pyproject_path).absolute()
main(pyproject_path, args.extra)
+61
View File
@@ -0,0 +1,61 @@
#!/usr/bin/env python3
"""
Update the haystack-ai version in the deepset-cloud-custom-nodes uv.lock file.
Fetches sdist/wheel hashes from PyPI and updates the haystack-ai package entry,
preserving the existing lock file formatting.
"""
import argparse
import json
import sys
import urllib.request
import tomlkit
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("version", help="Version to update to (e.g. 2.26.1-rc1)")
parser.add_argument("lock_file", help="Path to uv.lock")
args = parser.parse_args()
# PEP 440 normalized version for filenames
new_version = args.version.replace("-", "")
# Fetch hashes from PyPI
pypi_data = json.load(urllib.request.urlopen(f"https://pypi.org/pypi/haystack-ai/{args.version}/json"))
sdist_sha = wheel_sha = None
for u in pypi_data["urls"]:
if u["packagetype"] == "sdist":
sdist_sha = u["digests"]["sha256"]
elif u["packagetype"] == "bdist_wheel":
wheel_sha = u["digests"]["sha256"]
if not sdist_sha or not wheel_sha:
sys.exit("Could not find sdist or wheel hashes on PyPI")
with open(args.lock_file) as f:
data = tomlkit.load(f)
found = False
for pkg in data["package"]:
if pkg["name"] == "haystack-ai":
old_version = pkg["version"]
pkg["version"] = new_version
pkg["sdist"]["url"] = pkg["sdist"]["url"].replace(old_version, new_version)
pkg["sdist"]["hash"] = f"sha256:{sdist_sha}"
wheel = pkg["wheels"][0]
wheel["url"] = wheel["url"].replace(old_version, new_version)
wheel["hash"] = f"sha256:{wheel_sha}"
found = True
print(f"Updated haystack-ai from {old_version} to {new_version}")
break
if not found:
sys.exit("haystack-ai package not found in uv.lock")
with open(args.lock_file, "w") as f:
tomlkit.dump(data, f)
+82
View File
@@ -0,0 +1,82 @@
#!/bin/bash
# wait_for_workflows.sh - Wait for tag-triggered workflows to complete
#
# Usage: ./wait_for_workflows.sh <tag> <workflow_name1> [workflow_name2] ...
# Requires: GH_TOKEN and GITHUB_REPOSITORY environment variables
#
# Example:
# ./wait_for_workflows.sh v2.19.0 "Project release on PyPi" "Docker image release"
#
# This script is used in the release.yml workflow to wait for the workflows triggered by a specific release tag to
# successfully complete.
# With the default values, we wait for 20 minutes
MAX_ATTEMPTS="${MAX_ATTEMPTS:-40}"
SLEEP_SECONDS="${SLEEP_SECONDS:-30}"
set -euo pipefail
if [[ -z "${GH_TOKEN:-}" ]] || [[ -z "${GITHUB_REPOSITORY:-}" ]]; then
echo "❌ GH_TOKEN and GITHUB_REPOSITORY must be set"
exit 1
fi
TAG="$1"
shift
WORKFLOWS=("$@")
# Get commit SHA from tag
TAG_SHA=$(git rev-list -n 1 "${TAG}" 2>/dev/null) || {
echo "❌ Tag ${TAG} not found"
exit 1
}
echo "Tag ${TAG} (commit: ${TAG_SHA:0:7})"
echo ""
wait_for_workflow() {
local name="$1"
echo "⏳ Waiting for: $name"
for ((i=1; i<=MAX_ATTEMPTS; i++)); do
jq_filter="[.workflow_runs[] | select(.head_sha == \"${TAG_SHA}\" and .name == \"${name}\")]
| sort_by(.created_at) | last"
result=$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs" \
--jq "$jq_filter" 2>/dev/null || echo "")
if [[ -z "$result" ]]; then
echo " Attempt $i/$MAX_ATTEMPTS: not started yet..."
sleep $SLEEP_SECONDS
continue
fi
status=$(echo "$result" | jq -r '.status')
conclusion=$(echo "$result" | jq -r '.conclusion')
if [[ "$status" == "completed" ]]; then
if [[ "$conclusion" == "success" ]]; then
echo "$name completed"
return 0
else
echo "$name failed: $conclusion"
return 1
fi
fi
echo " Attempt $i/$MAX_ATTEMPTS: $status..."
sleep $SLEEP_SECONDS
done
echo "$name: timeout after $((MAX_ATTEMPTS * SLEEP_SECONDS / 60)) minutes"
return 1
}
for workflow in "${WORKFLOWS[@]}"; do
wait_for_workflow "$workflow" || exit 1
done
echo ""
echo "✅ All workflows completed"
@@ -0,0 +1,35 @@
name: Approve and merge API reference sync PRs
# Automatically approve and merge API reference sync PRs from Haystack, Haystack Core Integrations,
# and Haystack Experimental
on:
pull_request:
branches:
- main
paths:
- "docs-website/reference/**"
- "docs-website/reference_versioned_docs/**"
permissions:
contents: read
env:
GH_TOKEN: ${{ github.token }}
jobs:
auto-approve-and-merge:
permissions:
contents: write
pull-requests: write
if: |
github.event.pull_request.user.login == 'HaystackBot' &&
startsWith(github.event.pull_request.head.ref, 'sync-docusaurus-api-reference') &&
github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-slim
steps:
- name: Approve PR
run: gh pr review --approve ${{ github.event.pull_request.number }} --repo ${{ github.repository }}
- name: Enable auto-merge
run: gh pr merge --squash --auto ${{ github.event.pull_request.number }} --repo ${{ github.repository }}
+99
View File
@@ -0,0 +1,99 @@
name: Branch off
on:
workflow_call:
outputs:
bump_version_pr_url:
description: 'URL of the bump version PR'
value: ${{ jobs.branch-off.outputs.bump_version_pr_url }}
env:
PYTHON_VERSION: "3.10"
permissions:
contents: read
jobs:
branch-off:
runs-on: ubuntu-slim
outputs:
bump_version_pr_url: ${{ steps.create-pr.outputs.pull-request-url }}
steps:
- name: Checkout this repo
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: main
# Persist the bot token so the branch/tag pushes below authenticate as
# HaystackBot (a ruleset-bypass actor) instead of the default
# github-actions[bot], which only has contents:read and is blocked by
# the release rulesets.
token: ${{ secrets.HAYSTACK_BOT_TOKEN }}
- name: Define all versions
id: versions
shell: bash
run: |
# example: 2.20.0-rc0 in VERSION.txt -> 2.20
echo "current_version_major_minor=$(cut -d "." -f 1,2 < VERSION.txt)" >> "$GITHUB_OUTPUT"
# example: 2.20.0-rc0 in VERSION.txt -> 2.21.0-rc0
echo "next_version_rc0=$(awk -F. '/[0-9]+\./{$2++;print}' OFS=. < VERSION.txt)" >> "$GITHUB_OUTPUT"
- name: Create release branch and tag
shell: bash
env:
# We use the HAYSTACK_BOT_TOKEN here so the PR created by the step will
# trigger required workflows and can be merged by anyone
GITHUB_TOKEN: ${{ secrets.HAYSTACK_BOT_TOKEN }}
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
# Create the release branch from main
git checkout -b v${{ steps.versions.outputs.current_version_major_minor }}.x
git push -u origin v${{ steps.versions.outputs.current_version_major_minor }}.x
# Tag the branch-off point with the next version rc0 to mark start of next dev cycle.
# The tag points to this commit (before VERSION.txt is bumped).
# This is intentional for reno to work properly.
git tag "v${{ steps.versions.outputs.next_version_rc0 }}" -m "v${{ steps.versions.outputs.next_version_rc0 }}"
git push --tags
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "${{ env.PYTHON_VERSION }}"
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "22"
- name: Prepare changes for main
shell: bash
run: |
git checkout main
# Bump VERSION.txt to next version rc0
echo "${{ steps.versions.outputs.next_version_rc0 }}" > VERSION.txt
# Generate unstable docs for Docusaurus
python ./.github/utils/create_unstable_docs_docusaurus.py --new-version ${{ steps.versions.outputs.current_version_major_minor }}
- name: Create PR to bump unstable version and create unstable docs
id: create-pr
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
token: ${{ secrets.HAYSTACK_BOT_TOKEN }}
commit-message: "Bump unstable version and create unstable docs"
branch: bump-version
base: main
title: "Bump unstable version and create unstable docs"
add-paths: |
VERSION.txt
docs-website
body: |
This PR:
- Bumps the unstable version to `${{ steps.versions.outputs.next_version_rc0 }}`
- Creates the unstable docs for Haystack ${{ steps.versions.outputs.current_version_major_minor }}
You can inspect the docs preview (two unstable versions will be available) and merge it.
labels: "ignore-for-release-notes"
reviewers: "${{ github.actor }}"
+57
View File
@@ -0,0 +1,57 @@
name: ClusterFuzzLite PR fuzzing
# Short, code-change-scoped fuzzing run on PRs that touch fuzzed code or the
# fuzzing setup. Catches regressions and crashes introduced by a change.
# Continuous/batch fuzzing can be added later as a separate scheduled workflow.
#
# Scoped to the modules the harnesses actually exercise rather than all of
# `haystack/**`, so the (build-heavy) job doesn't run on every library PR:
# - Pipeline.loads -> core/pipeline, core/serialization, marshal
# - Document.from_dict -> dataclasses
# - document_matches_filter -> utils/filters
# Deep, transitively-reached regressions are better caught by a scheduled batch
# run; random YAML/JSON essentially never reaches haystack/components.
on:
pull_request:
paths:
- "haystack/core/pipeline/**"
- "haystack/core/serialization.py"
- "haystack/core/errors.py"
- "haystack/marshal/**"
- "haystack/dataclasses/**"
- "haystack/utils/filters.py"
- "test/fuzz/**"
- ".clusterfuzzlite/**"
- ".github/workflows/cflite_pr.yml"
permissions:
contents: read
jobs:
pr-fuzzing:
runs-on: ubuntu-latest
concurrency:
group: cflite-pr-${{ github.event.pull_request.number }}
cancel-in-progress: true
steps:
- name: Build Fuzzers
id: build
uses: google/clusterfuzzlite/actions/build_fuzzers@884713a6c30a92e5e8544c39945cd7cb630abcd1 # v1
with:
language: python
sanitizer: address
# Needed so build_fuzzers can check out the PR base for `mode: code-change`
# diffing in the Run Fuzzers step below.
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Run Fuzzers
id: run
uses: google/clusterfuzzlite/actions/run_fuzzers@884713a6c30a92e5e8544c39945cd7cb630abcd1 # v1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
fuzz-seconds: 120
mode: code-change
sanitizer: address
# Crashes fail the job and are uploaded as artifacts. SARIF upload is
# disabled to keep the token least-privilege (no security-events: write).
output-sarif: false
+99
View File
@@ -0,0 +1,99 @@
name: Check API reference changes
on:
pull_request:
paths:
- "haystack/**/*.py"
- "pydoc/*.yml"
permissions:
contents: read
jobs:
test-api-reference-build:
runs-on: ubuntu-slim
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "3.13"
- name: Detect API reference changes
id: changed
shell: python
run: |
import os
import subprocess
from pathlib import Path
import sys
sys.path.insert(0, ".github/utils")
from docstrings_checksum import docstrings_checksum
def git(*args):
result = subprocess.run(["git", *args], capture_output=True, text=True)
return result.stdout.strip(), result.returncode
base_sha, _ = git("rev-parse", "HEAD^1")
diff_output, _ = git("diff", "--name-only", f"{base_sha}...HEAD")
changed_files = set(diff_output.splitlines())
needs_check = False
# If any pydoc config changed, always rebuild
if any(f.startswith("pydoc/") and f.endswith(".yml") for f in changed_files):
needs_check = True
# If Python files changed, compare docstring checksums
if not needs_check and any(f.startswith("haystack/") and f.endswith(".py") for f in changed_files):
runner_temp = os.environ["RUNNER_TEMP"]
base_worktree = os.path.join(runner_temp, "base")
_, rc = git("worktree", "add", base_worktree, base_sha)
pr_checksum = docstrings_checksum(Path(".").glob("haystack/**/*.py"))
base_checksum = ""
if rc == 0:
base_checksum = docstrings_checksum(Path(base_worktree).glob("haystack/**/*.py"))
if pr_checksum != base_checksum:
needs_check = True
print(f"API reference check needed: {needs_check}")
with open(os.environ["GITHUB_OUTPUT"], "a") as f:
f.write(f"needs_check={str(needs_check).lower()}\n")
- name: Install Hatch
if: steps.changed.outputs.needs_check == 'true'
run: |
python -m pip install --upgrade pip
pip install hatch --uploaded-prior-to=P1D
- name: Generate API references
if: steps.changed.outputs.needs_check == 'true'
run: hatch run docs
- name: Set up Node.js
if: steps.changed.outputs.needs_check == 'true'
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "22"
- name: Run Docusaurus md/mdx checker
if: steps.changed.outputs.needs_check == 'true'
working-directory: tmp_api_reference
run: |
# docusaurus-mdx-checker is a package that is not frequently updated. Its dependency katex sometimes ships a
# broken ESM build, where a __VERSION__ placeholder is left unresolved, causing a ReferenceError at import time.
# Node 22+ prefers ESM when available. We force CJS (CommonJS) resolution to use the working katex build.
# This should be safe because docusaurus-mdx-checker and its dependencies provide CJS builds.
export NODE_OPTIONS="--conditions=require"
npx docusaurus-mdx-checker@3.0.0 -v || {
echo ""
echo "For common MDX problems, see https://docusaurus.io/blog/preparing-your-site-for-docusaurus-v3#common-mdx-problems"
exit 1
}
+56
View File
@@ -0,0 +1,56 @@
name: CodeQL
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
permissions:
contents: read
# Cancel a superseded run for the same ref (e.g. a force-push to a PR) so we
# don't burn CI minutes analyzing commits that are already stale.
concurrency:
group: codeql-${{ github.ref }}
cancel-in-progress: true
jobs:
analyze:
name: Analyze (${{ matrix.language }})
runs-on: ubuntu-latest
timeout-minutes: 360
permissions:
# Required to upload code-scanning results to the Security tab.
security-events: write
# Required by codeql-action to read the workflow/CI config.
actions: read
contents: read
strategy:
fail-fast: false
matrix:
include:
# Haystack is a Python project; Python is interpreted, so no build is
# needed. Add `javascript-typescript` here if the docs-website JS code
# should also be scanned.
- language: python
build-mode: none
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
with:
category: "/language:${{ matrix.language }}"
+24
View File
@@ -0,0 +1,24 @@
name: Add comment about test coverage to PRs
on:
workflow_run:
workflows:
- "Tests"
types: [completed]
permissions:
contents: read
jobs:
comment:
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request'
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- uses: py-cov-action/python-coverage-comment-action@5d8df5979747514c914e1c5a12335a7cf9a2745f # v4.1
with:
GITHUB_TOKEN: ${{ github.token }}
GITHUB_PR_RUN_ID: ${{ github.event.workflow_run.id }}
MINIMUM_GREEN: 90
MINIMUM_ORANGE: 60
+88
View File
@@ -0,0 +1,88 @@
name: Docker image release
on:
workflow_dispatch:
push:
branches:
- main
paths:
- '.github/workflows/docker_release.yml'
- 'docker/**'
- 'haystack/**'
- 'pyproject.toml'
- 'VERSION.txt'
tags:
- "v2.[0-9]+.[0-9]+*"
env:
DOCKER_REPO_NAME: deepset/haystack
permissions:
contents: read
jobs:
build-and-push:
name: Build base image
runs-on: ubuntu-latest
if: github.repository_owner == 'deepset-ai'
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Set up QEMU
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- name: Login to DockerHub
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
username: ${{ secrets.DOCKER_HUB_USER }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: Docker meta
id: meta
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
images: $DOCKER_REPO_NAME
- name: Detect stable version
run: |
if [[ "${{ steps.meta.outputs.version }}" =~ ^v2\.[0-9]+\.[0-9]+$ ]]; then
echo "IS_STABLE=true" >> "$GITHUB_ENV"
echo "Stable version detected"
else
echo "Not a stable version"
fi
- name: Build base images
uses: docker/bake-action@d3418bd7d0e9324001bca92fa8ba175ea7e6dc9b # v7.3.0
env:
IMAGE_TAG_SUFFIX: ${{ steps.meta.outputs.version }}
HAYSTACK_VERSION: ${{ steps.meta.outputs.version }}
with:
source: ./docker
targets: base
push: true
- name: Test base image
run: |
EXPECTED_VERSION=$(cat VERSION.txt)
if [[ $EXPECTED_VERSION == *"-"* ]]; then
EXPECTED_VERSION=$(cut -d '-' -f 1 < VERSION.txt)$(cut -d '-' -f 2 < VERSION.txt)
fi
TAG="base-${{ steps.meta.outputs.version }}"
PLATFORM="linux/amd64"
VERSION=$(docker run --platform "$PLATFORM" --rm "deepset/haystack:$TAG" python -c"from haystack.version import __version__; print(__version__)")
[[ "$VERSION" = "$EXPECTED_VERSION" ]] || echo "::error 'Haystack version in deepset/haystack:$TAG image for $PLATFORM is different from expected'"
PLATFORM="linux/arm64"
VERSION=$(docker run --platform "$PLATFORM" --rm "deepset/haystack:$TAG" python -c"from haystack.version import __version__; print(__version__)")
[[ "$VERSION" = "$EXPECTED_VERSION" ]] || echo "::error 'Haystack version in deepset/haystack:$TAG image for $PLATFORM is different from expected'"
# Remove image after test to avoid filling the GitHub runner and prevent its failure
docker rmi "deepset/haystack:$TAG"
@@ -0,0 +1,74 @@
name: Test Python snippets in docs
on:
schedule:
- cron: "17 3 * * *" # daily at 03:17 UTC
workflow_dispatch:
inputs:
haystack_version:
description: "Haystack version to test against (e.g., 2.16.1, main)"
required: false
default: "main"
type: string
env:
HATCH_VERSION: "1.16.5"
PYTHON_VERSION: "3.11"
permissions:
contents: read
jobs:
test-docs-snippets:
runs-on: ubuntu-latest
timeout-minutes: 30
env:
AZURE_OPENAI_ENDPOINT: ${{ secrets.AZURE_OPENAI_ENDPOINT }}
AZURE_OPENAI_AD_TOKEN: ${{ secrets.AZURE_OPENAI_AD_TOKEN }}
AZURE_OPENAI_API_KEY: ${{ secrets.AZURE_OPENAI_API_KEY }}
CORE_AZURE_CS_ENDPOINT: ${{ secrets.CORE_AZURE_CS_ENDPOINT }}
HF_API_TOKEN: ${{ secrets.HUGGINGFACE_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
SERPERDEV_API_KEY: ${{ secrets.SERPERDEV_API_KEY }}
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Setup Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "3.11"
- name: Install system dependencies
run: sudo apt-get install -y ffmpeg
- name: Install Hatch
run: |
python -m pip install --upgrade pip
pip install hatch==${{ env.HATCH_VERSION }} --uploaded-prior-to=P1D
- name: Generate API reference for Docusaurus
run: hatch run docs
- name: Install base dependencies
run: |
python -m pip install --upgrade pip
pip install requests toml --uploaded-prior-to=P1D
- name: Run snippet tests (verbose)
shell: bash
run: |
hatch -e test env run -- python docs-website/scripts/test_python_snippets.py --verbose tmp_api_reference/
notify-slack-on-failure:
if: failure() && github.ref_name == 'main'
needs:
- test-docs-snippets
runs-on: ubuntu-slim
steps:
- uses: deepset-ai/notify-slack-action@a65def0c8bf91d6520286ab34280151c76a5a008 # v1.1.0
with:
slack-webhook-url: ${{ secrets.SLACK_WEBHOOK_URL_NOTIFICATIONS }}
+60
View File
@@ -0,0 +1,60 @@
name: Docs Search Sync
on:
workflow_dispatch: # Activate this workflow manually
schedule:
- cron: "0 1 * * *"
# Running this workflow multiple times in parallel can cause issues with files uploads and deletions.
concurrency:
group: docs-search-sync
cancel-in-progress: false
permissions:
contents: read
jobs:
docs-search-sync:
runs-on: ubuntu-latest
steps:
- name: Checkout Haystack repo
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Install Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "22"
- name: Install Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "3.12"
- name: Install Docusaurus and build docs-website
working-directory: docs-website
run: |
npm install --ignore-scripts
npm rebuild sharp
npm run build
- name: Install script dependencies
# sniffio is needed because of https://github.com/deepset-ai/deepset-cloud-sdk/issues/286
# we pin pyrate-limiter due to https://github.com/deepset-ai/deepset-cloud-sdk/issues/295
run: |
python -m pip install --upgrade pip
pip install deepset-cloud-sdk sniffio requests "pyrate-limiter<4" --uploaded-prior-to=P1D
- name: Update new docs to Search pipeline and remove outdated docs
env:
DEEPSET_WORKSPACE_DOCS_SEARCH: ${{ secrets.DEEPSET_WORKSPACE_DOCS_SEARCH }}
DEEPSET_API_KEY_DOCS_SEARCH: ${{ secrets.DEEPSET_API_KEY_DOCS_SEARCH }}
run: python ./.github/utils/docs_search_sync.py
- name: Notify Slack on nightly failure
if: failure() && github.event_name == 'schedule'
uses: deepset-ai/notify-slack-action@a65def0c8bf91d6520286ab34280151c76a5a008 # v1.1.0
with:
slack-webhook-url: ${{ secrets.SLACK_WEBHOOK_URL_NOTIFICATIONS }}
+70
View File
@@ -0,0 +1,70 @@
name: Detect docstrings edit
# This workflow runs in the *untrusted* pull_request context: it has a read-only
# token and no access to secrets, so checking out the PR head here is safe.
# It only computes whether docstrings changed and hands the result to the
# "Apply docstrings label" workflow (workflow_run), which holds the write token
# but never checks out untrusted code. See:
# https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/
on:
pull_request:
paths:
- "haystack/**/*.py"
permissions:
contents: read
env:
PYTHON_VERSION: "3.11"
jobs:
detect:
runs-on: ubuntu-slim
steps:
- name: Checkout base commit
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
ref: ${{ github.base_ref }}
- name: Setup Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "${{ env.PYTHON_VERSION }}"
- name: Get base docstrings
id: base-docstrings
run: |
CHECKSUM=$(python .github/utils/docstrings_checksum.py --root "${{ github.workspace }}")
echo "checksum=$CHECKSUM" >> "$GITHUB_OUTPUT"
- name: Checkout HEAD commit
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
ref: ${{ github.event.pull_request.head.sha }}
- name: Get HEAD docstrings
id: head-docstrings
run: |
CHECKSUM=$(python .github/utils/docstrings_checksum.py --root "${{ github.workspace }}")
echo "checksum=$CHECKSUM" >> "$GITHUB_OUTPUT"
- name: Write result
# The follow-up workflow reads these values; it never checks out the PR code.
run: |
mkdir -p result
echo "${{ github.event.pull_request.number }}" > result/pr_number
if [ "${{ steps.base-docstrings.outputs.checksum }}" != "${{ steps.head-docstrings.outputs.checksum }}" ]; then
echo "true" > result/should_label
else
echo "false" > result/should_label
fi
- name: Upload result
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: docstring-label-result
path: result/
retention-days: 1
@@ -0,0 +1,42 @@
name: Apply docstrings label
# Runs after "Detect docstrings edit" completes. This workflow holds the write
# token but only consumes the data artifact produced by the untrusted run — it
# never checks out PR code, so it is safe under the workflow_run trigger.
on:
workflow_run:
workflows: ["Detect docstrings edit"]
types:
- completed
permissions:
contents: read
jobs:
label:
runs-on: ubuntu-slim
if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success'
permissions:
contents: read
pull-requests: write
actions: read
steps:
- name: Download result
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: docstring-label-result
path: result
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Add label
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
SHOULD_LABEL=$(cat result/should_label)
PR_NUMBER=$(cat result/pr_number)
if [ "$SHOULD_LABEL" = "true" ]; then
gh pr edit "$PR_NUMBER" --repo "${{ github.repository }}" --add-label "type:documentation"
else
echo "Docstrings unchanged, nothing to label."
fi
+66
View File
@@ -0,0 +1,66 @@
name: Sync docs with Docusaurus
on:
workflow_dispatch:
push:
branches:
- main
paths:
- "pydoc/**"
- "haystack/**"
- ".github/workflows/docusaurus_sync.yml"
env:
HATCH_VERSION: "1.16.5"
PYTHON_VERSION: "3.11"
permissions:
contents: read
jobs:
sync:
runs-on: ubuntu-slim
permissions:
contents: write
steps:
- name: Checkout Haystack repo
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "${{ env.PYTHON_VERSION }}"
- name: Install Hatch
run: |
python -m pip install --upgrade pip
pip install hatch==${{ env.HATCH_VERSION }} --uploaded-prior-to=P1D
- name: Generate API reference for Docusaurus
run: hatch run docs
- name: Sync generated API reference to docs folder
run: |
SOURCE_PATH="tmp_api_reference"
DEST_PATH="docs-website/reference/haystack-api"
echo "Syncing from $SOURCE_PATH to $DEST_PATH"
mkdir -p $DEST_PATH
# Using rsync to copy files. This will also remove files in dest that are no longer in source.
rsync -av --delete --exclude='.git/' "$SOURCE_PATH/" "$DEST_PATH/"
- name: Create Pull Request
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
token: ${{ secrets.HAYSTACK_BOT_TOKEN }}
commit-message: "Sync Haystack API reference on Docusaurus"
branch: sync-docusaurus-api-reference
base: main
title: "docs: sync Haystack API reference on Docusaurus"
add-paths: |
docs-website/reference/haystack-api
body: |
This PR syncs the Haystack API reference on Docusaurus. Just approve and merge it.
+52
View File
@@ -0,0 +1,52 @@
name: end-to-end
on:
workflow_dispatch: # Activate this workflow manually
schedule:
- cron: "0 0 * * *"
pull_request:
types:
- opened
- reopened
- synchronize
paths:
- "e2e/**/*.py"
- ".github/workflows/e2e.yml"
env:
PYTHON_VERSION: "3.10"
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
HATCH_VERSION: "1.16.5"
# we use HF_TOKEN instead of HF_API_TOKEN to work around a Hugging Face bug
# see https://github.com/deepset-ai/haystack/issues/9552
HF_TOKEN: ${{ secrets.HUGGINGFACE_API_KEY }}
permissions:
contents: read
jobs:
run:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "${{ env.PYTHON_VERSION }}"
- name: Install Hatch
run: |
python -m pip install --upgrade pip
pip install hatch==${{ env.HATCH_VERSION }} --uploaded-prior-to=P1D
- name: Run tests
run: hatch run test:e2e
- name: Notify Slack on nightly failure
if: failure() && github.event_name == 'schedule'
uses: deepset-ai/notify-slack-action@a65def0c8bf91d6520286ab34280151c76a5a008 # v1.1.0
with:
slack-webhook-url: ${{ secrets.SLACK_WEBHOOK_URL_NOTIFICATIONS }}
+104
View File
@@ -0,0 +1,104 @@
name: Project release on Github
on:
workflow_dispatch: # this is useful to re-generate the release page without a new tag being pushed
push:
tags:
- "v2.[0-9]+.[0-9]+*"
# Ignore release versions tagged with -rc0 suffix
- "!v2.[0-9]+.[0-9]-rc0"
permissions:
contents: read
jobs:
generate-notes:
runs-on: ubuntu-latest
permissions:
contents: write # ncipollo/release-action creates the GitHub release
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-tags: true
fetch-depth: 0 # slow but needed by reno
- name: Parse version
id: version
run: |
echo "current_release=$(awk -F \\- '{print $1}' < VERSION.txt)" >> "$GITHUB_OUTPUT"
echo "current_pre_release=$(awk -F \\- '{print $2}' < VERSION.txt)" >> "$GITHUB_OUTPUT"
- name: Install reno
run: |
python -m pip install --upgrade pip
pip install "reno<5" --uploaded-prior-to=P1D
# Remove next version rc0 tag in the CI environment to prevent reno from assigning notes to future releases.
# This ensures release notes are correctly aggregated for the current version.
# This is a workaround. Can be removed if the release process is fully aligned with reno.
- name: Delete next version rc0 tag in the CI environment
run: |
# Parse version X.Y.Z and increment Y for next minor version
IFS='.' read -r MAJOR MINOR _ <<< "${{ steps.version.outputs.current_release }}"
NEXT_MINOR=$((MINOR + 1))
NEXT_TAG="v${MAJOR}.${NEXT_MINOR}.0-rc0"
if git rev-parse --verify "$NEXT_TAG" >/dev/null 2>&1; then
git tag -d "$NEXT_TAG"
echo "Deleted local tag $NEXT_TAG"
else
echo "Tag $NEXT_TAG does not exist locally"
fi
- name: Generate release notes
env:
EARLIEST_VERSION: v${{ steps.version.outputs.current_release }}-rc1
run: |
reno report --no-show-source --ignore-cache --earliest-version "$EARLIEST_VERSION" -o relnotes.rst
- name: Convert to Markdown
uses: docker://pandoc/core:3.8@sha256:c7127705b9d84c1b4acbbe411a86e3a6d67ac57870654dc378b6fe636ab747df
with:
args: "--from rst --to gfm --syntax-highlighting=none --wrap=none relnotes.rst -o relnotes.md"
# We copy the relnotes file since the original one cannot be modified due to permissions
- name: Copy relnotes file
run: |
cat relnotes.md > enhanced_relnotes.md
- name: Add contributor list
# only for minor releases and minor release candidates (not bugfix releases)
if: endsWith(steps.version.outputs.current_release, '.0')
env:
GH_TOKEN: ${{ github.token }}
START: v${{ steps.version.outputs.current_release }}-rc0
END: ${{ github.ref_name }}
run: |
JQ_EXPR='[.commits[].author.login]
| map(select(. != null and . != "HaystackBot" and . != "dependabot[bot]" and . != "github-actions[bot]"))
| unique
| sort_by(ascii_downcase)
| map("@\(.)")
| join(", ")'
CONTRIBUTORS=$(gh api "repos/deepset-ai/haystack/compare/$START...$END" \
--jq "$JQ_EXPR") || { echo "Unable to fetch contributors"; exit 1; }
{
echo ""
echo "## 💙 Big thank you to everyone who contributed to this release!"
echo ""
echo "$CONTRIBUTORS"
} >> enhanced_relnotes.md
- name: Debug
run: |
cat enhanced_relnotes.md
- uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1.21.0
with:
bodyFile: "enhanced_relnotes.md"
prerelease: ${{ steps.version.outputs.current_pre_release != '' }}
allowUpdates: true
+17
View File
@@ -0,0 +1,17 @@
name: "Labeler"
on:
- pull_request_target
permissions:
contents: read
jobs:
triage:
runs-on: ubuntu-slim
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/labeler@b8dd2d9be0f68b860e7dae5dae7d772984eacd6d # v6.2.0
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
+69
View File
@@ -0,0 +1,69 @@
name: License Compliance
on:
pull_request:
paths:
- "**/pyproject.toml"
- ".github/workflows/license_compliance.yml"
# Since we test PRs, there is no need to run the workflow at each
# merge on `main`. Let's use a cron job instead.
schedule:
- cron: "0 0 * * *" # every day at midnight
env:
PYTHON_VERSION: "3.10"
permissions:
contents: read
jobs:
license_check_direct:
name: Direct dependencies only
env:
REQUIREMENTS_FILE: requirements_direct.txt
runs-on: ubuntu-latest
steps:
- name: Checkout the code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Setup Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "${{ env.PYTHON_VERSION }}"
- name: Get direct dependencies
run: |
python -m pip install --upgrade pip
pip install toml --uploaded-prior-to=P1D
python .github/utils/pyproject_to_requirements.py pyproject.toml > ${{ env.REQUIREMENTS_FILE }}
- name: Check Licenses
id: license_check_report
uses: pilosus/action-pip-license-checker@e909b0226ff49d3235c99c4585bc617f49fff16a # v3.1.0
with:
github-token: ${{ secrets.GH_ACCESS_TOKEN }}
requirements: ${{ env.REQUIREMENTS_FILE }}
fail: "Copyleft,Other,Error"
# Exclusions in the vanilla distribution must be explicitly motivated
# - tqdm is MLP but there are no better alternatives
# - typing_extensions>=4.13.0 has a Python Software Foundation License 2.0 but pip-license-checker does not recognize it
# (https://github.com/pilosus/pip-license-checker/issues/143)
exclude: "(?i)^(tqdm|typing_extensions).*"
# We keep the license inventory on FOSSA
- name: Send license report to Fossa
uses: fossas/fossa-action@29693cc50323968e039056be419b32989fc5880c # v2.0.0
continue-on-error: true # not critical
with:
api-key: ${{ secrets.FOSSA_LICENSE_SCAN_TOKEN }}
- name: Print report
if: ${{ always() }}
run: echo "${{ steps.license_check_report.outputs.report }}"
- name: Notify Slack on failure
if: failure()
uses: deepset-ai/notify-slack-action@a65def0c8bf91d6520286ab34280151c76a5a008 # v1.1.0
with:
slack-webhook-url: ${{ secrets.SLACK_WEBHOOK_URL_NOTIFICATIONS }}
@@ -0,0 +1,51 @@
name: Nightly pre-release on PyPI
on:
schedule:
# Run at midnight UTC every day
- cron: "0 0 * * *"
workflow_dispatch:
env:
HATCH_VERSION: "1.16.5"
permissions:
contents: read
jobs:
nightly-release:
runs-on: ubuntu-latest
environment: pypi
permissions:
id-token: write
# Always build from main for consistency (scheduled and manual runs)
steps:
- name: Checkout main
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
ref: main
fetch-depth: 1
# Reads VERSION.txt, strips any -rcN suffix, and appends .devYYYYMMDDHHMMSS
# (e.g. 2.25.0.dev20250217000000) so each run gets a unique, PEP 440valid pre-release version.
- name: Set nightly version
id: set-version
run: |
BASE_VERSION=$(sed 's/-rc[0-9]*$//' VERSION.txt)
TIMESTAMP=$(date +%Y%m%d%H%M%S)
NIGHTLY_VERSION="${BASE_VERSION}.dev${TIMESTAMP}"
echo "version=${NIGHTLY_VERSION}" >> "$GITHUB_OUTPUT"
echo "${NIGHTLY_VERSION}" > VERSION.txt
echo "Building haystack-ai version: ${NIGHTLY_VERSION}"
- name: Install Hatch
run: |
python -m pip install --upgrade pip
pip install hatch==${{ env.HATCH_VERSION }} --uploaded-prior-to=P1D
- name: Build Haystack
run: hatch build
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
+19
View File
@@ -0,0 +1,19 @@
name: Track issues with Github project
on:
issues:
types:
- opened
permissions:
contents: read
jobs:
add-to-project:
name: Add new issues to project for triage
runs-on: ubuntu-slim
steps:
- uses: actions/add-to-project@5afcf98fcd03f1c2f92c3c83f58ae24323cc57fd # v2.0.0
with:
project-url: https://github.com/orgs/deepset-ai/projects/5
github-token: ${{ secrets.GH_PROJECT_PAT }}
@@ -0,0 +1,65 @@
name: Release new minor version docs
on:
push:
tags:
# Trigger this only for new minor version tags (e.g. v2.99.0)
- "v[0-9]+.[0-9]+.0"
# Exclude 1.x tags
- "!v1.[0-9]+.[0-9]+"
env:
PYTHON_VERSION: "3.10"
permissions:
contents: read
jobs:
promote:
runs-on: ubuntu-slim
steps:
- name: Checkout this repo
# use VERSION.txt file from main branch
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
ref: main
- name: Get version to release
id: version
shell: bash
# We only need `major.minor`. At this point, VERSION.txt contains the next version.
# For example, if we are releasing 2.20.0, VERSION.txt contains 2.21.0-rc0.
run: |
MAJOR=$(cut -d "." -f 1 < VERSION.txt)
MINOR=$(cut -d "." -f 2 < VERSION.txt)
MINOR=$((MINOR - 1))
echo "version=${MAJOR}.${MINOR}" >> "$GITHUB_OUTPUT"
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "${{ env.PYTHON_VERSION }}"
- name: Promote unstable docs for Docusaurus
run: |
python ./.github/utils/promote_unstable_docs_docusaurus.py --version ${{ steps.version.outputs.version }}
- name: Create Pull Request with Docusaurus docs updates
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
token: ${{ secrets.HAYSTACK_BOT_TOKEN }}
commit-message: "Promote unstable docs for Haystack ${{ steps.version.outputs.version }}"
branch: promote-unstable-docs-${{ steps.version.outputs.version }}
base: main
title: "docs: promote unstable docs for Haystack ${{ steps.version.outputs.version }}"
add-paths: |
docs-website
body: |
This PR promotes the unstable docs for Haystack ${{ steps.version.outputs.version }} to stable.
It is expected to run at the time of the release.
You can inspect the docs preview and merge it. There should now be only one unstable version representing the next (main) branch.
# This workflow is triggered by a tag pushed by the HaystackBot in release.yml > create-release-tag.
# GitHub requires reviewers to be different from the PR author, so setting `github.actor`
# would fail (it would request a review from the HaystackBot itself).
# So we don't set any reviewers and instead notify the Release Manager
# (see .github/utils/prepare_release_notification.sh).
@@ -0,0 +1,65 @@
name: Push release notes to website
on:
workflow_dispatch:
inputs:
version:
description: 'Haystack version (vX.Y.Z)'
required: true
type: string
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ inputs.version }}
permissions:
contents: read
jobs:
push-release-notes-to-website:
runs-on: ubuntu-slim
steps:
- name: Checkout Haystack home repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
repository: deepset-ai/haystack-home
- name: Get release notes and add frontmatter
id: release_notes
run: |
VERSION_NUMBER="${VERSION:1}"
RELEASE_DATE=$(gh release view "$VERSION" --repo deepset-ai/haystack --json publishedAt --jq '.publishedAt | split("T")[0]')
RELEASE_NOTES_PATH="content/release-notes/$VERSION_NUMBER.md"
{
echo "---"
echo "title: Haystack $VERSION_NUMBER"
echo "description: Release notes for Haystack $VERSION_NUMBER"
echo "toc: True"
echo "date: $RELEASE_DATE"
echo "last_updated: $RELEASE_DATE"
echo 'tags: ["Release Notes"]'
echo "link: https://github.com/deepset-ai/haystack/releases/tag/$VERSION"
echo "---"
echo ""
} > "$RELEASE_NOTES_PATH"
gh release view "$VERSION" --repo deepset-ai/haystack --json body --jq '.body' >> "$RELEASE_NOTES_PATH"
cat "$RELEASE_NOTES_PATH"
- name: Create Pull Request to Haystack Home
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
token: ${{ secrets.HAYSTACK_BOT_TOKEN }}
commit-message: "Add release notes for Haystack ${{ env.VERSION }}"
branch: add-release-notes-for-haystack-${{ env.VERSION }}
base: main
title: "docs: add release notes for Haystack ${{ env.VERSION }}"
add-paths: |
content/release-notes
body: |
This PR adds the release notes for Haystack ${{ env.VERSION }} to the website.
reviewers: "${{ github.actor }}"
+38
View File
@@ -0,0 +1,38 @@
name: Project release on PyPi
on:
push:
tags:
- "v[0-9]+.[0-9]+.[0-9]+*"
# We must not release versions tagged with -rc0 suffix
- "!v[0-9]+.[0-9]+.[0-9]-rc0"
env:
HATCH_VERSION: "1.16.5"
permissions:
contents: read
jobs:
release-on-pypi:
runs-on: ubuntu-latest
environment: pypi
permissions:
id-token: write
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Install Hatch
run: |
python -m pip install --upgrade pip
pip install hatch==${{ env.HATCH_VERSION }} --uploaded-prior-to=P1D
- name: Build Haystack
run: hatch build
- name: Publish on PyPi
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
+302
View File
@@ -0,0 +1,302 @@
name: Release
on:
workflow_dispatch:
inputs:
version:
description: 'Version to release (e.g., v2.99.0-rc1 or v2.99.0)'
required: true
type: string
# Only one release workflow runs at a time; additional runs are queued.
concurrency:
group: release
cancel-in-progress: false
permissions:
contents: read
jobs:
authorize:
# Releasing acts as HaystackBot (a ruleset-bypass actor), so the ruleset
# cannot enforce *who* started the release. Gate on the triggering user's
# role instead: only maintainers/admins may run this workflow, even though
# workflow_dispatch itself is available to anyone with write access.
runs-on: ubuntu-slim
steps:
- name: Verify the triggering user may release
env:
GH_TOKEN: ${{ secrets.HAYSTACK_BOT_TOKEN }}
ACTOR: ${{ github.triggering_actor }}
run: |
ROLE=$(gh api "repos/${{ github.repository }}/collaborators/${ACTOR}/permission" --jq '.role_name')
echo "::notice::${ACTOR} has repository role '${ROLE}'"
case "$ROLE" in
admin|maintain)
echo "✅ ${ACTOR} is authorized to trigger a release." ;;
*)
echo "::error::${ACTOR} has role '${ROLE}'. A release can only be triggered by a user with the 'maintain' or 'admin' role."
exit 1 ;;
esac
parse-validate-version:
needs: ["authorize"]
runs-on: ubuntu-slim
outputs:
version: ${{ steps.parse-validate.outputs.version }}
major_minor: ${{ steps.parse-validate.outputs.major_minor }}
release_branch: ${{ steps.parse-validate.outputs.release_branch }}
is_rc: ${{ steps.parse-validate.outputs.is_rc }}
is_first_rc: ${{ steps.parse-validate.outputs.is_first_rc }}
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0 # needed to fetch tags and branches
- name: Parse and validate version
id: parse-validate
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ inputs.version }}
run: .github/utils/parse_validate_version.sh "$VERSION"
branch-off:
needs: ["parse-validate-version"]
if: needs.parse-validate-version.outputs.is_first_rc == 'true'
uses: ./.github/workflows/branch_off.yml
# https://docs.github.com/en/actions/how-tos/reuse-automations/reuse-workflows#passing-secrets-to-nested-workflows
secrets: inherit
create-release-tag:
needs: ["parse-validate-version", "branch-off"]
if: always() && needs.parse-validate-version.result == 'success' && needs.branch-off.result != 'failure'
runs-on: ubuntu-slim
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0 # needed to fetch tags and branches
# use this token so the created tag triggers workflows (does not happen with the default github.token)
token: ${{ secrets.HAYSTACK_BOT_TOKEN }}
- name: Update VERSION.txt and create tag
env:
GITHUB_TOKEN: ${{ secrets.HAYSTACK_BOT_TOKEN }}
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git checkout ${{ needs.parse-validate-version.outputs.release_branch }}
git pull origin ${{ needs.parse-validate-version.outputs.release_branch }}
echo "${{ needs.parse-validate-version.outputs.version }}" > VERSION.txt
git add VERSION.txt
git commit -m "bump version to ${{ needs.parse-validate-version.outputs.version }}"
git push origin ${{ needs.parse-validate-version.outputs.release_branch }}
TAG="v${{ needs.parse-validate-version.outputs.version }}"
git tag -m "$TAG" "$TAG"
git push origin "$TAG"
check-artifacts:
needs: ["parse-validate-version", "create-release-tag"]
if: always() && needs.parse-validate-version.result == 'success' && needs.create-release-tag.result == 'success'
runs-on: ubuntu-latest
outputs:
github_url: ${{ steps.set-outputs.outputs.github_url }}
pypi_url: ${{ steps.set-outputs.outputs.pypi_url }}
docker_url: ${{ steps.set-outputs.outputs.docker_url }}
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ needs.parse-validate-version.outputs.version }}
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0 # needed to fetch tags and branches
- name: Wait for release workflows
run: |
.github/utils/wait_for_workflows.sh "v${{ env.VERSION }}" \
"Project release on PyPi" \
"Project release on Github" \
"Docker image release"
- name: Check artifacts
run: |
check() {
for _ in {1..5}; do curl -sf "$2" > /dev/null && echo "✅ $1" && return 0; sleep 30; done
echo "❌ $1 not found" && return 1
}
check "GitHub Release" "https://api.github.com/repos/${{ github.repository }}/releases/tags/v${{ env.VERSION }}"
check "PyPI package" "https://pypi.org/pypi/haystack-ai/${{ env.VERSION }}/json"
check "Docker image" "https://hub.docker.com/v2/repositories/deepset/haystack/tags/base-v${{ env.VERSION }}"
- name: Set artifact URLs
id: set-outputs
run: |
{
echo "github_url=https://github.com/${{ github.repository }}/releases/tag/v${{ env.VERSION }}"
echo "pypi_url=https://pypi.org/project/haystack-ai/${{ env.VERSION }}/"
echo "docker_url=https://hub.docker.com/r/deepset/haystack/tags?name=base-v${{ env.VERSION }}"
} >> "$GITHUB_OUTPUT"
bump-dc-pipeline-templates:
needs: ["parse-validate-version", "check-artifacts"]
if: always() && needs.check-artifacts.result == 'success' && needs.parse-validate-version.outputs.is_rc == 'true'
runs-on: ubuntu-slim
outputs:
pr_url: ${{ steps.create-pr.outputs.pull-request-url }}
env:
VERSION: ${{ needs.parse-validate-version.outputs.version }}
steps:
- name: Checkout dc-pipeline-templates
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
repository: deepset-ai/dc-pipeline-templates
token: ${{ secrets.HAYSTACK_BOT_TOKEN }}
- name: Update haystack pin
run: sed -i "s/haystack-ai>=.*/haystack-ai>=${VERSION}/" requirements-test.txt
- name: Create Pull Request
id: create-pr
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
token: ${{ secrets.HAYSTACK_BOT_TOKEN }}
commit-message: "Bump haystack to ${{ env.VERSION }}"
branch: bump-haystack-${{ env.VERSION }}
base: main
title: "chore: bump haystack to ${{ env.VERSION }}"
add-paths: requirements-test.txt
body: |
Bump haystack pin to `${{ env.VERSION }}` for platform testing.
bump-deepset-cloud-custom-nodes:
needs: ["parse-validate-version", "check-artifacts"]
if: always() && needs.check-artifacts.result == 'success' && needs.parse-validate-version.outputs.is_rc == 'true'
runs-on: ubuntu-slim
outputs:
pr_url: ${{ steps.create-pr.outputs.pull-request-url }}
env:
VERSION: ${{ needs.parse-validate-version.outputs.version }}
steps:
- name: Checkout haystack (for utils)
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
path: haystack
sparse-checkout: .github/utils/update_haystack_dc_custom_nodes.py
sparse-checkout-cone-mode: false
- name: Checkout deepset-cloud-custom-nodes
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
repository: deepset-ai/deepset-cloud-custom-nodes
token: ${{ secrets.HAYSTACK_BOT_TOKEN }}
path: deepset-cloud-custom-nodes
- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "3.13"
- name: Install tomlkit
run: |
python -m pip install --upgrade pip
pip install tomlkit --uploaded-prior-to=P1D
- name: Update haystack-ai in uv.lock
run: python haystack/.github/utils/update_haystack_dc_custom_nodes.py "${{ env.VERSION }}" deepset-cloud-custom-nodes/uv.lock
- name: Create Pull Request
id: create-pr
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
token: ${{ secrets.HAYSTACK_BOT_TOKEN }}
path: deepset-cloud-custom-nodes
commit-message: "Bump haystack to ${{ env.VERSION }}"
branch: bump-haystack-${{ env.VERSION }}
base: main
title: "chore: bump haystack to ${{ env.VERSION }}"
add-paths: uv.lock
body: |
Bump haystack pin to `${{ env.VERSION }}` for platform testing.
bump-haystack-runtime:
needs: ["parse-validate-version", "check-artifacts"]
if: always() && needs.check-artifacts.result == 'success' && needs.parse-validate-version.outputs.is_rc == 'true'
runs-on: ubuntu-slim
outputs:
pr_url: ${{ steps.wait-pr.outputs.pr_url }}
env:
VERSION: ${{ needs.parse-validate-version.outputs.version }}
steps:
- name: Trigger "Update package version" workflow
env:
GH_TOKEN: ${{ secrets.HAYSTACK_BOT_TOKEN }}
run: |
gh workflow run update-package-version.yaml \
-R deepset-ai/haystack-runtime \
-f haystack_version="${{ env.VERSION }}"
- name: Wait for PR
id: wait-pr
env:
GH_TOKEN: ${{ secrets.HAYSTACK_BOT_TOKEN }}
run: |
TRIGGER_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ)
for i in $(seq 1 30); do
PR_URL=$(gh pr list -R deepset-ai/haystack-runtime \
--head "bump/hs${{ env.VERSION }}" \
--json url,createdAt \
--jq ".[] | select(.createdAt >= \"$TRIGGER_TIME\") | .url")
if [[ -n "$PR_URL" ]]; then
echo "pr_url=$PR_URL" >> "$GITHUB_OUTPUT"
echo "Found PR: $PR_URL"
exit 0
fi
echo "Attempt $i: PR not found yet, waiting 10s..."
sleep 10
done
echo "PR not found after 5 minutes"
notify:
needs:
- "parse-validate-version"
- "branch-off"
- "create-release-tag"
- "check-artifacts"
- "bump-dc-pipeline-templates"
- "bump-deepset-cloud-custom-nodes"
- "bump-haystack-runtime"
if: always()
runs-on: ubuntu-slim
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Prepare release notification
env:
VERSION: ${{ inputs.version }}
GH_TOKEN: ${{ github.token }}
RUN_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
HAS_FAILURE: ${{ contains(needs.*.result, 'failure') }}
IS_RC: ${{ needs.parse-validate-version.outputs.is_rc }}
IS_FIRST_RC: ${{ needs.parse-validate-version.outputs.is_first_rc }}
MAJOR_MINOR: ${{ needs.parse-validate-version.outputs.major_minor }}
GITHUB_URL: ${{ needs.check-artifacts.outputs.github_url }}
PYPI_URL: ${{ needs.check-artifacts.outputs.pypi_url }}
DOCKER_URL: ${{ needs.check-artifacts.outputs.docker_url }}
BUMP_VERSION_PR_URL: ${{ needs.branch-off.outputs.bump_version_pr_url }}
DC_PIPELINE_TEMPLATES_PR_URL: ${{ needs.bump-dc-pipeline-templates.outputs.pr_url }}
DC_CUSTOM_NODES_PR_URL: ${{ needs.bump-deepset-cloud-custom-nodes.outputs.pr_url }}
HAYSTACK_RUNTIME_PR_URL: ${{ needs.bump-haystack-runtime.outputs.pr_url }}
run: .github/utils/prepare_release_notification.sh
- name: Send release notification to Slack
uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3
with:
webhook: ${{ secrets.SLACK_WEBHOOK_URL_RELEASE }}
webhook-type: incoming-webhook
payload-file-path: slack_payload.json
+85
View File
@@ -0,0 +1,85 @@
name: Check Release Notes
on:
pull_request:
types:
- opened
- reopened
- synchronize
- ready_for_review
- labeled
- unlabeled
paths:
- "**.py"
- "pyproject.toml"
- "!.github/**/*.py"
- "releasenotes/notes/*.yaml"
permissions:
contents: read
jobs:
reno:
runs-on: ubuntu-slim
env:
PYTHON_VERSION: "3.10"
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
# With the default value of 1, there are corner cases where tj-actions/changed-files
# fails with a `no merge base` error
fetch-depth: 0
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "${{ env.PYTHON_VERSION }}"
- name: Get release note files
id: changed-files
uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6
with:
files: releasenotes/notes/*.yaml
- name: Check release notes
if: steps.changed-files.outputs.any_changed == 'false' && !contains( github.event.pull_request.labels.*.name, 'ignore-for-release-notes')
run: |
# Check if any of the commit messages contain tags ci/docs/test
if git log --pretty=%s origin/main..HEAD | grep -E '^(ci:|docs:|test:)' > /dev/null; then
echo "Skipping release note check for commits with 'ci:', 'docs:', or 'test:' tags."
else
echo "::error::The release notes file is missing, please add one or attach the label 'ignore-for-release-notes' to this PR."
exit 1
fi
- name: Verify release notes formatting
if: steps.changed-files.outputs.any_changed == 'true' && !contains( github.event.pull_request.labels.*.name, 'ignore-for-release-notes')
run: |
python -m pip install --upgrade pip
pip install "reno<5" --uploaded-prior-to=P1D
reno lint . # it is not possible to pass a list of files to reno lint
- name: Check reStructuredText code formatting
if: steps.changed-files.outputs.any_changed == 'true' && !contains( github.event.pull_request.labels.*.name, 'ignore-for-release-notes')
shell: python
run: |
files = "${{ steps.changed-files.outputs.all_changed_files }}".split()
errors = []
for filepath in files:
with open(filepath) as f:
for line_no, line in enumerate(f, start=1):
# Check for triple backticks (Markdown code blocks)
if "```" in line:
err = (f"Format error in {filepath}:{line_no}: "
"Found triple backticks. Use reStructuredText code block directive instead: .. code:: python")
errors.append(err)
# Check for single backticks (Markdown inline code)
if "`" in line.replace("```", "").replace("``", ""):
err = (f"Format error in {filepath}:{line_no}: "
"Found single backticks. Use double backticks (``code``) for inline code.")
errors.append(err)
if errors:
raise Exception("\n".join(errors))
@@ -0,0 +1,26 @@
name: Check Release Notes
on:
pull_request:
types:
- opened
- reopened
- synchronize
- ready_for_review
- labeled
- unlabeled
paths-ignore:
- "**.py"
- "pyproject.toml"
- "!.github/**/*.py"
- "releasenotes/notes/*.yaml"
permissions:
contents: read
jobs:
reno:
runs-on: ubuntu-slim
steps:
- name: Skip mandatory job
run: echo "Skipped!"
+66
View File
@@ -0,0 +1,66 @@
# OpenSSF Scorecard
#
# Runs the OpenSSF Scorecard analyzer (https://github.com/ossf/scorecard) and
# publishes results to GitHub's Security tab via the code-scanning SARIF API.
#
# Why: Scorecard gives haystack maintainers a single weekly signal on
# supply-chain posture (branch protection, SAST coverage, dependency-update
# tool, signed releases, etc.) without changing any existing CI behaviour.
# It also lets downstream consumers verify the project's score on the
# OpenSSF deps.dev / Security Insights dashboards.
#
# Permissions are scoped to the minimum required by the action:
# - security-events: write -> upload SARIF
# - id-token: write -> publish to the public OpenSSF API (badge)
# - contents: read -> default repo read
# - actions: read -> read CI config for checks
#
# Default branch is 'main'.
name: OpenSSF Scorecard
on:
# Run weekly so results stay fresh, plus on branch-protection changes so the
# branch-protection check is re-evaluated immediately in the Security tab.
branch_protection_rule:
schedule:
- cron: '32 6 * * 1'
permissions: read-all
jobs:
analysis:
name: Scorecard analysis
runs-on: ubuntu-latest
permissions:
security-events: write
id-token: write
contents: read
actions: read
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Run analysis
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
with:
results_file: results.sarif
results_format: sarif
# Publish results to the OpenSSF public REST API so the badge in
# the README and deps.dev card stay up to date. Opt-in.
publish_results: true
- name: Upload artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: SARIF file
path: results.sarif
retention-days: 5
- name: Upload to code-scanning
uses: github/codeql-action/upload-sarif@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
with:
sarif_file: results.sarif
+130
View File
@@ -0,0 +1,130 @@
name: Slow Integration Tests
# The workflow will always run, but the actual tests will only execute when:
# - The workflow is triggered manually
# - The workflow is scheduled
# - The PR has the "run-slow-tests" label
# - The push is to a release branch
# - There are changes to relevant files.
# Note: If no conditions are met, the workflow will complete successfully without running tests
# to satisfy Branch Protection rules.
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HF_API_TOKEN: ${{ secrets.HUGGINGFACE_API_KEY }}
PYTHON_VERSION: "3.10"
HATCH_VERSION: "1.16.5"
HAYSTACK_MPS_ENABLED: false
HAYSTACK_XPU_ENABLED: false
on:
workflow_dispatch: # Activate this workflow manually
schedule:
- cron: "0 0 * * *"
push:
branches:
# release branches have the form v1.9.x
- "v[0-9].*[0-9].x"
pull_request:
types:
- opened
- reopened
- synchronize
- labeled
- unlabeled
permissions:
contents: read
jobs:
check-if-changed:
# This job checks if the relevant files have been changed.
# We check for changes in the check-if-changed job instead of using paths/paths-ignore at workflow level.
# This ensures the "Slow Integration Tests completed" job always runs, which is required by Branch Protection rules.
name: Check if changed
runs-on: ubuntu-slim
permissions:
pull-requests: read
# Specifying outputs is not needed to make the job work, but only to comply with actionlint
outputs:
changes: ${{ steps.changes.outputs.changes }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Check for changed code
id: changes
uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
with:
# List of Python files that trigger slow integration tests when modified
filters: |
changes:
- "haystack/components/evaluators/sas_evaluator.py"
- "haystack/components/generators/openai_dalle.py"
- "haystack/components/preprocessors/embedding_based_document_splitter.py"
- "haystack/components/retrievers/multi_query_embedding_retriever.py"
- "test/components/evaluators/test_sas_evaluator.py"
- "test/components/generators/test_openai_dalle.py"
- "test/components/preprocessors/test_embedding_based_document_splitter.py"
- "test/components/retrievers/test_multi_query_embedding_retriever.py"
slow-integration-tests:
name: Slow Tests / ${{ matrix.os }}
runs-on: ${{ matrix.os }}
needs: check-if-changed
timeout-minutes: 30
# Run tests if: manual trigger, scheduled, PR has label, release branch, or relevant files changed
if: |
github.event_name == 'workflow_dispatch' ||
github.event_name == 'schedule' ||
(github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'run-slow-tests')) ||
(github.event_name == 'push' && github.ref == 'refs/heads/v[0-9].*[0-9].x') ||
(needs.check-if-changed.outputs.changes == 'true')
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "${{ env.PYTHON_VERSION }}"
- name: Install Hatch
id: hatch
shell: bash
run: |
python -m pip install --upgrade pip
pip install hatch==${{ env.HATCH_VERSION }} --uploaded-prior-to=P1D
- name: Run tests
run: hatch run test:integration-only-slow
- name: Notify Slack on nightly failure
if: failure() && github.event_name == 'schedule'
uses: deepset-ai/notify-slack-action@a65def0c8bf91d6520286ab34280151c76a5a008 # v1.1.0
with:
slack-webhook-url: ${{ secrets.SLACK_WEBHOOK_URL_NOTIFICATIONS }}
slow-integration-tests-completed:
# This job always runs and succeeds if all tests succeed or are skipped. It is required by Branch Protection rules.
name: Slow Integration Tests completed
runs-on: ubuntu-slim
if: ${{ always() && !cancelled() }}
needs: slow-integration-tests
steps:
- name: Mark tests as completed
run: |
if [ "${{ needs.slow-integration-tests.result }}" = "failure" ]; then
echo "Slow Integration Tests failed!"
exit 1
else
echo "Slow Integration Tests completed!"
fi
+21
View File
@@ -0,0 +1,21 @@
name: 'Stalebot'
on:
schedule:
- cron: '30 1 * * *'
permissions:
contents: read
jobs:
makestale:
runs-on: ubuntu-slim
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@1e223db275d687790206a7acac4d1a11bd6fe629 # v10.4.0
with:
any-of-labels: 'proposal,information-needed'
stale-pr-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 10 days.'
days-before-stale: 30
days-before-close: 10
+343
View File
@@ -0,0 +1,343 @@
name: Tests
# The workflow will always run, but the actual tests will only execute when:
# - The workflow is triggered manually.
# - The push is to main or a release branch.
# - There are changes to relevant files on a pull request.
# Note: If no conditions are met, the workflow will complete successfully without running tests
# to satisfy Branch Protection rules.
on:
workflow_dispatch: # Activate this workflow manually
push:
branches:
- main
# release branches have the form v1.9.x
- "v[0-9].*[0-9].x"
# when we push, we do not need to satisfy Branch Protection rules, so we can ignore PRs that just change docs
paths-ignore:
- "docs/**"
- "docs-website/**"
pull_request:
types:
- opened
- reopened
- synchronize
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
CORE_AZURE_CS_ENDPOINT: ${{ secrets.CORE_AZURE_CS_ENDPOINT }}
CORE_AZURE_CS_API_KEY: ${{ secrets.CORE_AZURE_CS_API_KEY }}
AZURE_OPENAI_API_KEY: ${{ secrets.AZURE_OPENAI_API_KEY }}
AZURE_OPENAI_ENDPOINT: ${{ secrets.AZURE_OPENAI_ENDPOINT }}
HF_API_TOKEN: ${{ secrets.HUGGINGFACE_API_KEY }}
PYTHON_VERSION: "3.10"
HATCH_VERSION: "1.16.5"
permissions:
contents: read
jobs:
check-if-changed:
# This job checks if the relevant files have been changed.
# We check for changes in the check-if-changed job instead of using paths/paths-ignore at workflow level.
# This ensures the "Mark tests as completed" job always runs, which is required by Branch Protection rules.
name: Check if changed
runs-on: ubuntu-slim
permissions:
pull-requests: read
# Specifying outputs is not needed to make the job work, but only to comply with actionlint
outputs:
changes: ${{ steps.changes.outputs.changes }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Check for changed code
id: changes
uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
with:
filters: |
changes:
- "haystack/**/*.py"
- "test/**/*.py"
- "pyproject.toml"
- ".github/utils/*.py"
- "scripts/*.py"
format:
needs: check-if-changed
# Run tests if: manual trigger, push to main/release, or relevant files changed
if: |
github.event_name == 'workflow_dispatch' ||
github.event_name == 'push' ||
(needs.check-if-changed.outputs.changes == 'true')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "${{ env.PYTHON_VERSION }}"
- name: Install Hatch
run: |
python -m pip install --upgrade pip
pip install hatch==${{ env.HATCH_VERSION }} --uploaded-prior-to=P1D
- name: Ruff - check format and linting
run: hatch run fmt-check
- name: Check presence of license header
run: docker run --rm -v "$(pwd):/github/workspace" ghcr.io/korandoru/hawkeye:v6.5.1@sha256:0ebd72353053aa6d3f0cb44a6897786386347bd4636ffc9592f8e47c81ea2b9d check
check-imports:
needs: format
runs-on: ubuntu-slim
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "${{ env.PYTHON_VERSION }}"
- name: Install Hatch
run: |
python -m pip install --upgrade pip
pip install hatch==${{ env.HATCH_VERSION }} --uploaded-prior-to=P1D
- name: Check imports
run: hatch run python .github/utils/check_imports.py
unit-tests:
name: Unit / ${{ matrix.os }}
needs: format
timeout-minutes: 30
permissions:
contents: write
pull-requests: write
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
- windows-latest
- macos-latest
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: true # needed for python-coverage-comment-action
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "${{ env.PYTHON_VERSION }}"
- name: Install Hatch
id: hatch
shell: bash
run: |
python -m pip install --upgrade pip
pip install hatch==${{ env.HATCH_VERSION }} --uploaded-prior-to=P1D
echo "env=$(hatch env find test)" >> "$GITHUB_OUTPUT"
- name: Run
run: hatch run test:unit
- uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
id: cache
if: matrix.os == 'macos-latest'
with:
path: ${{ steps.hatch.outputs.env }}
key: ${{ runner.os }}-${{ github.sha }}
# On PR: posts coverage comment (directly on same-repo PRs; via artifact for fork PRs).
# On push to main: stores coverage baseline on the data branch.
# We only upload coverage for ubuntu-latest, as handling multiple OSes adds complexity for little gain.
- name: Coverage comment
id: coverage_comment
if: matrix.os == 'ubuntu-latest'
uses: py-cov-action/python-coverage-comment-action@5d8df5979747514c914e1c5a12335a7cf9a2745f # v4.1
with:
GITHUB_TOKEN: ${{ github.token }}
MINIMUM_GREEN: 90
MINIMUM_ORANGE: 60
- name: Upload coverage comment to be posted
if: matrix.os == 'ubuntu-latest' && github.event_name == 'pull_request' && steps.coverage_comment.outputs.COMMENT_FILE_WRITTEN == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: python-coverage-comment-action
path: python-coverage-comment-action.txt
mypy:
needs: unit-tests
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
# With the default value of 1, there are corner cases where tj-actions/changed-files
# fails with a `no merge base` error
fetch-depth: 0
- name: Get changed files
id: files
uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6
with:
files: |
**/*.py
pyproject.toml
files_ignore: |
test/**
.github/**
scripts/**
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
if: steps.files.outputs.any_changed == 'true'
with:
python-version: "${{ env.PYTHON_VERSION }}"
- name: Install Hatch
id: hatch
if: steps.files.outputs.any_changed == 'true'
run: |
python -m pip install --upgrade pip
pip install hatch==${{ env.HATCH_VERSION }} --uploaded-prior-to=P1D
echo "env=$(hatch env find test)" >> "$GITHUB_OUTPUT"
- name: Mypy
if: steps.files.outputs.any_changed == 'true'
run: |
mkdir .mypy_cache
hatch run test:types
integration-tests-linux:
name: Integration / ubuntu-latest
needs: unit-tests
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "${{ env.PYTHON_VERSION }}"
- name: Install Hatch
id: hatch
shell: bash
run: |
python -m pip install --upgrade pip
pip install hatch==${{ env.HATCH_VERSION }} --uploaded-prior-to=P1D
echo "env=$(hatch env find test)" >> "$GITHUB_OUTPUT"
- name: Run
run: hatch run test:integration-only-fast
integration-tests-macos:
name: Integration / macos-latest
needs: unit-tests
runs-on: macos-latest
timeout-minutes: 30
env:
HAYSTACK_MPS_ENABLED: false
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "${{ env.PYTHON_VERSION }}"
- name: Install Hatch
id: hatch
shell: bash
run: |
python -m pip install --upgrade pip
pip install hatch==${{ env.HATCH_VERSION }} --uploaded-prior-to=P1D
echo "env=$(hatch env find test)" >> "$GITHUB_OUTPUT"
- uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
id: cache
with:
path: ${{ steps.hatch.outputs.env }}
key: ${{ runner.os }}-${{ github.sha }}
- name: Run
run: hatch run test:integration-only-fast
integration-tests-windows:
name: Integration / windows-latest
needs: unit-tests
runs-on: windows-latest
timeout-minutes: 30
env:
HAYSTACK_XPU_ENABLED: false
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "${{ env.PYTHON_VERSION }}"
- name: Install Hatch
id: hatch
shell: bash
run: |
python -m pip install --upgrade pip
pip install hatch==${{ env.HATCH_VERSION }} --uploaded-prior-to=P1D
echo "env=$(hatch env find test)" >> "$GITHUB_OUTPUT"
- name: Run
run: hatch run test:integration-only-fast
notify-slack-on-failure:
if: failure() && github.ref_name == 'main'
needs:
- check-imports
- mypy
- integration-tests-linux
- integration-tests-macos
- integration-tests-windows
runs-on: ubuntu-slim
steps:
- uses: deepset-ai/notify-slack-action@a65def0c8bf91d6520286ab34280151c76a5a008 # v1.1.0
with:
slack-webhook-url: ${{ secrets.SLACK_WEBHOOK_URL_NOTIFICATIONS }}
tests-completed:
# This job always runs and succeeds if all tests succeed or are skipped. It is required by Branch Protection rules.
name: Mark tests as completed
runs-on: ubuntu-slim
if: ${{ always() && !cancelled() }}
needs:
- check-imports
- mypy
- integration-tests-linux
- integration-tests-macos
- integration-tests-windows
steps:
- name: Mark tests as completed
run: |
if [ "${{ needs.check-imports.result }}" = "failure" ] ||
[ "${{ needs.mypy.result }}" = "failure" ] ||
[ "${{ needs.integration-tests-linux.result }}" = "failure" ] ||
[ "${{ needs.integration-tests-macos.result }}" = "failure" ] ||
[ "${{ needs.integration-tests-windows.result }}" = "failure" ]; then
echo "Tests failed!"
exit 1
else
echo "Tests completed!"
fi
@@ -0,0 +1,99 @@
name: Update Platform Components Table
on:
push:
branches:
- main
workflow_dispatch:
concurrency:
group: update-platform-components
cancel-in-progress: false
env:
PYTHON_VERSION: "3.11"
OUTPUT_FILE: docs-website/docs/overview/platform-components.mdx
permissions:
contents: read
jobs:
update:
runs-on: ubuntu-slim
steps:
- name: Checkout deepset-ai/haystack
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
token: ${{ secrets.HAYSTACK_BOT_TOKEN }}
# Full history so git diff against HEAD works correctly
fetch-depth: 0
- name: Checkout deepset-ai/haystack-core-integrations
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
repository: deepset-ai/haystack-core-integrations
path: haystack-core-integrations
fetch-depth: 1
sparse-checkout: integrations
sparse-checkout-cone-mode: true
- name: Checkout platform repo (sparse — schema file only)
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
repository: ${{ secrets.SCHEMA_REPO }}
token: ${{ secrets.PLATFORM_REPO_PAT }}
path: platform-repo
sparse-checkout: |
${{ secrets.SCHEMA_REPO_PATH }}
sparse-checkout-cone-mode: false
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "${{ env.PYTHON_VERSION }}"
- name: Generate platform components table
run: |
python scripts/generate_platform_components_table.py \
--haystack-src . \
--integrations-src ./haystack-core-integrations \
--schema ./platform-repo/${{ secrets.SCHEMA_REPO_PATH }} \
--output ./${{ env.OUTPUT_FILE }}
- name: Compute PR body
id: pr_body
run: |
FILE="${{ env.OUTPUT_FILE }}"
if git ls-files --error-unmatch "$FILE" 2>/dev/null; then
ADDED=$(git diff HEAD -- "$FILE" | grep -cE '^\+\| `' || true)
REMOVED=$(git diff HEAD -- "$FILE" | grep -cE '^\-\| `' || true)
SUMMARY="Component rows changed: **+${ADDED}** added, **-${REMOVED}** removed."
else
SUMMARY="Initial generation of the platform components table."
fi
{
echo "body<<EOF"
echo "## Platform Components Table Update"
echo ""
echo "${SUMMARY}"
echo ""
echo "_Auto-generated by the [update-platform-components](https://github.com/deepset-ai/haystack/actions/workflows/update-platform-components.yml) workflow._"
echo "EOF"
} >> "$GITHUB_OUTPUT"
- name: Open or update pull request
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
token: ${{ secrets.HAYSTACK_BOT_TOKEN }}
commit-message: "chore: update platform components table"
branch: chore/update-platform-components
base: main
title: "chore: update platform components table"
add-paths: |
${{ env.OUTPUT_FILE }}
body: ${{ steps.pr_body.outputs.body }}
labels: |
documentation
ignore-for-release-notes
+30
View File
@@ -0,0 +1,30 @@
name: Github workflows linter
on:
pull_request:
paths:
- ".github/workflows/**"
permissions:
contents: read
jobs:
lint-workflows:
runs-on: ubuntu-slim
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
go-version: ">=1.24.0"
- name: Install actionlint
run: go install github.com/rhysd/actionlint/cmd/actionlint@v1.7.12
- name: Run actionlint
env:
SHELLCHECK_OPTS: --exclude=SC2102
run: actionlint