chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
name: Backend Blocking IO
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main", "2.0.x-dev"]
|
||||
paths:
|
||||
- "backend/**"
|
||||
- ".github/workflows/backend-blocking-io-tests.yml"
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
paths:
|
||||
- "backend/**"
|
||||
- ".github/workflows/backend-blocking-io-tests.yml"
|
||||
|
||||
concurrency:
|
||||
group: blocking-io-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
backend-blocking-io:
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.draft == false
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v3
|
||||
|
||||
- name: Install backend dependencies
|
||||
working-directory: backend
|
||||
run: uv sync --group dev
|
||||
|
||||
- name: Run blocking IO regression tests
|
||||
working-directory: backend
|
||||
run: make test-blocking-io
|
||||
@@ -0,0 +1,40 @@
|
||||
name: Unit Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ 'main', '2.0.x-dev' ]
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
|
||||
concurrency:
|
||||
group: unit-tests-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
backend-unit-tests:
|
||||
if: github.event.pull_request.draft == false
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
|
||||
- name: Install backend dependencies
|
||||
working-directory: backend
|
||||
run: uv sync --group dev
|
||||
|
||||
- name: Run unit tests of backend
|
||||
working-directory: backend
|
||||
run: make test
|
||||
@@ -0,0 +1,87 @@
|
||||
name: Publish Helm Chart
|
||||
|
||||
# Publishes the DeerFlow Helm chart as an OCI artifact to GHCR alongside the
|
||||
# container images (see container.yaml). Triggers on the same `v*` tags.
|
||||
#
|
||||
# On pull requests touching the chart or config.example.yaml, `validate-chart`
|
||||
# runs lint + template render + a config_version drift check (the chart's
|
||||
# embedded config_version must not lag config.example.yaml) so a broken or
|
||||
# stale chart fails the PR, not the release.
|
||||
#
|
||||
# Users then install with:
|
||||
# helm install deer-flow oci://ghcr.io/${{ owner }}/deer-flow --version <ver>
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
pull_request:
|
||||
paths:
|
||||
- "deploy/helm/deer-flow/**"
|
||||
- "config.example.yaml"
|
||||
- ".github/workflows/chart.yaml"
|
||||
- "scripts/check_config_version.sh"
|
||||
|
||||
jobs:
|
||||
validate-chart:
|
||||
# Runs on PRs and release tags: catch a broken render or a stale
|
||||
# config_version before merge / publish. A broken chart published under a
|
||||
# vX.Y.Z tag is an immutable OCI artifact (GHCR won't let you overwrite
|
||||
# --version), so a regression must fail here, not on install.
|
||||
if: github.event_name == 'pull_request' || startsWith(github.ref, 'refs/tags/v')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3
|
||||
|
||||
# ubuntu-latest ships with helm 3 preinstalled - no setup-helm action needed.
|
||||
- name: Lint chart
|
||||
run: helm lint deploy/helm/deer-flow
|
||||
- name: Validate templates render
|
||||
run: helm template deer-flow deploy/helm/deer-flow --include-crds >/dev/null
|
||||
|
||||
# The chart's `config:` block embeds a config_version that must not fall
|
||||
# behind config.example.yaml. A stale version is silent in-cluster (the
|
||||
# image ships no example to compare against, so _check_config_version
|
||||
# never warns) but means the chart's config is authored against an older
|
||||
# schema. Bump it in values.yaml and the README example. config_version
|
||||
# gates no runtime behavior - it only drives the outdated-warning - so a
|
||||
# bare version bump needs no field changes. Logic lives in
|
||||
# scripts/check_config_version.sh (shared with nightly.yaml).
|
||||
- name: config_version drift check
|
||||
run: bash scripts/check_config_version.sh
|
||||
|
||||
verify-versions:
|
||||
# Gate the release: every version source must match the v* tag. A forgotten
|
||||
# bump in Chart.yaml, pyproject.toml, or package.json fails here and skips
|
||||
# the publish. See scripts/verify_versions.sh.
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: ./.github/workflows/verify-versions.yml
|
||||
|
||||
publish-chart:
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
needs: [verify-versions, validate-chart]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3
|
||||
|
||||
- name: Log in to GHCR
|
||||
run: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | \
|
||||
helm registry login ghcr.io -u ${{ github.actor }} --password-stdin
|
||||
|
||||
- name: Package chart
|
||||
run: helm package deploy/helm/deer-flow --destination ./packages
|
||||
|
||||
- name: Push chart to GHCR
|
||||
run: |
|
||||
for pkg in ./packages/*.tgz; do
|
||||
echo "--- pushing $pkg"
|
||||
helm push "$pkg" oci://ghcr.io/${{ github.repository_owner }}
|
||||
done
|
||||
@@ -0,0 +1,162 @@
|
||||
name: Publish Containers
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
jobs:
|
||||
verify-versions:
|
||||
# Gate the release: every version source must match the v* tag. A forgotten
|
||||
# bump in Chart.yaml, pyproject.toml, or package.json fails here and skips
|
||||
# all image builds. See scripts/verify_versions.sh.
|
||||
uses: ./.github/workflows/verify-versions.yml
|
||||
|
||||
backend-container:
|
||||
needs: verify-versions
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
attestations: write
|
||||
id-token: write
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}-backend
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 #v3.4.0
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 #v5.7.0
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=tag
|
||||
type=ref,event=branch
|
||||
type=sha
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
- name: Build and push Docker image
|
||||
id: push
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 #v6.18.0
|
||||
with:
|
||||
context: .
|
||||
file: backend/Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
# Bake the `postgres` extra into the published image so multi-replica
|
||||
# deployments (K8s/Helm) can use shared Postgres persistence instead of
|
||||
# file-based SQLite. sqlite/redis-only single-replica setups still work;
|
||||
# this only adds the Postgres driver. See backend/Dockerfile `UV_EXTRAS`.
|
||||
build-args: |
|
||||
UV_EXTRAS=postgres
|
||||
|
||||
- name: Generate artifact attestation
|
||||
uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be #v2.4.0
|
||||
with:
|
||||
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
|
||||
subject-digest: ${{ steps.push.outputs.digest }}
|
||||
push-to-registry: true
|
||||
|
||||
frontend-container:
|
||||
needs: verify-versions
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
attestations: write
|
||||
id-token: write
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}-frontend
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 #v3.4.0
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 #v5.7.0
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=tag
|
||||
type=ref,event=branch
|
||||
type=sha
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
- name: Build and push Docker image
|
||||
id: push
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 #v6.18.0
|
||||
with:
|
||||
context: .
|
||||
file: frontend/Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
- name: Generate artifact attestation
|
||||
uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be #v2.4.0
|
||||
with:
|
||||
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
|
||||
subject-digest: ${{ steps.push.outputs.digest }}
|
||||
push-to-registry: true
|
||||
|
||||
provisioner-container:
|
||||
needs: verify-versions
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
attestations: write
|
||||
id-token: write
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}-provisioner
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 #v3.4.0
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 #v5.7.0
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=tag
|
||||
type=ref,event=branch
|
||||
type=sha
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
- name: Build and push Docker image
|
||||
id: push
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 #v6.18.0
|
||||
with:
|
||||
context: docker/provisioner
|
||||
file: docker/provisioner/Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
- name: Generate artifact attestation
|
||||
uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be #v2.4.0
|
||||
with:
|
||||
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
|
||||
subject-digest: ${{ steps.push.outputs.digest }}
|
||||
push-to-registry: true
|
||||
@@ -0,0 +1,63 @@
|
||||
name: E2E Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ 'main', '2.0.x-dev' ]
|
||||
paths:
|
||||
- 'frontend/**'
|
||||
- '.github/workflows/e2e-tests.yml'
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
paths:
|
||||
- 'frontend/**'
|
||||
- '.github/workflows/e2e-tests.yml'
|
||||
|
||||
concurrency:
|
||||
group: e2e-tests-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
e2e-tests:
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Use pinned pnpm version
|
||||
run: corepack prepare pnpm@10.26.2 --activate
|
||||
|
||||
- name: Install frontend dependencies
|
||||
working-directory: frontend
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Install Playwright Chromium
|
||||
working-directory: frontend
|
||||
run: npx playwright install chromium --with-deps
|
||||
|
||||
- name: Run E2E tests
|
||||
working-directory: frontend
|
||||
run: pnpm exec playwright test
|
||||
env:
|
||||
SKIP_ENV_VALIDATION: '1'
|
||||
|
||||
- name: Upload Playwright report
|
||||
uses: actions/upload-artifact@v4
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: playwright-report
|
||||
path: frontend/playwright-report/
|
||||
retention-days: 7
|
||||
@@ -0,0 +1,43 @@
|
||||
name: Frontend Unit Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ 'main', '2.0.x-dev' ]
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
|
||||
concurrency:
|
||||
group: frontend-unit-tests-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
frontend-unit-tests:
|
||||
if: github.event.pull_request.draft == false
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Use pinned pnpm version
|
||||
run: corepack prepare pnpm@10.26.2 --activate
|
||||
|
||||
- name: Install frontend dependencies
|
||||
working-directory: frontend
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Run unit tests of frontend
|
||||
working-directory: frontend
|
||||
run: make test
|
||||
@@ -0,0 +1,38 @@
|
||||
name: Label Sync
|
||||
|
||||
# Keeps repository labels in sync with the declarative source of truth
|
||||
# (.github/labels.yml). Runs whenever that file changes on main, and can be
|
||||
# triggered manually. Additive/update-only — never deletes labels.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- ".github/labels.yml"
|
||||
- "scripts/sync_labels.py"
|
||||
- ".github/workflows/label-sync.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
|
||||
concurrency:
|
||||
group: label-sync
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
|
||||
- name: Sync labels
|
||||
run: uv run --with pyyaml python scripts/sync_labels.py
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
@@ -0,0 +1,78 @@
|
||||
name: Lint Check
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ 'main', '2.0.x-dev' ]
|
||||
pull_request:
|
||||
branches: [ '*' ]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
lint-backend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
|
||||
- name: Check uv.lock is in sync
|
||||
working-directory: backend
|
||||
run: uv lock --check
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: backend
|
||||
run: |
|
||||
uv sync --group dev
|
||||
|
||||
- name: Lint backend
|
||||
working-directory: backend
|
||||
run: make lint
|
||||
|
||||
lint-frontend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Use pinned pnpm version
|
||||
run: corepack prepare pnpm@10.26.2 --activate
|
||||
|
||||
- name: Install frontend dependencies
|
||||
run: |
|
||||
cd frontend
|
||||
pnpm install --frozen-lockfile
|
||||
|
||||
- name: Check frontend formatting
|
||||
run: |
|
||||
cd frontend
|
||||
pnpm format
|
||||
|
||||
- name: Run frontend linting
|
||||
run: |
|
||||
cd frontend
|
||||
pnpm lint
|
||||
|
||||
- name: Check TypeScript types
|
||||
run: |
|
||||
cd frontend
|
||||
pnpm typecheck
|
||||
|
||||
- name: Build frontend
|
||||
run: |
|
||||
cd frontend
|
||||
BETTER_AUTH_SECRET=local-dev-secret pnpm build
|
||||
@@ -0,0 +1,234 @@
|
||||
name: Nightly Build
|
||||
|
||||
# Nightly build of the three DeerFlow container images (backend, frontend,
|
||||
# provisioner) and the Helm chart, published to GHCR from the default branch.
|
||||
#
|
||||
# This mirrors the tag-driven release workflows (container.yaml + chart.yaml)
|
||||
# but trades the `v*` tag for date-based tags, since nightly ships unreleased
|
||||
# `main`. The `verify-versions` gate is intentionally skipped - there is no tag
|
||||
# to match - and `latest` is left untouched so it keeps tracking the last `v*`
|
||||
# release. Images are amd64-only to match the release builds.
|
||||
#
|
||||
# Artifacts (under the running repo's owner):
|
||||
# ghcr.io/<owner>/deer-flow-{backend,frontend,provisioner}:nightly
|
||||
# ghcr.io/<owner>/deer-flow-{backend,frontend,provisioner}:nightly-YYYYMMDD
|
||||
# oci://ghcr.io/<owner>/deer-flow chart version <base>-nightly.YYYYMMDD-<sha>
|
||||
#
|
||||
# The nightly chart defaults image.tag=nightly and image.registry=ghcr.io/<owner>
|
||||
# (patched in-workflow, never committed), so installing it pulls the matching
|
||||
# nightly images with no values overrides.
|
||||
#
|
||||
# Restricted to the upstream repo: every job is gated on
|
||||
# `github.repository == 'bytedance/deer-flow'`, so a scheduled run or manual
|
||||
# dispatch on a fork skips all jobs rather than pushing to the fork's own GHCR
|
||||
# namespace. Scheduled workflows also only fire on the default branch.
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 16 * * *" # 16:00 UTC daily (adjust as needed)
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: nightly
|
||||
# Don't cancel a running nightly - a mid-cancel leaves a half-pushed set.
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
if: github.repository == 'bytedance/deer-flow'
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
date: ${{ steps.date.outputs.date }}
|
||||
nightly_version: ${{ steps.nightly_version.outputs.nightly_version }}
|
||||
steps:
|
||||
- name: Set nightly date (UTC)
|
||||
id: date
|
||||
# Shared by build-images (image tag) and publish-chart (chart version)
|
||||
# so the two stay in sync for a given run.
|
||||
run: echo "date=$(date -u +%Y%m%d)" >> "$GITHUB_OUTPUT"
|
||||
- name: Checkout repository
|
||||
# Needed to read Chart.yaml's base version for the nightly version
|
||||
# string computed below.
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3
|
||||
- name: Compute nightly version
|
||||
id: nightly_version
|
||||
# Single source of truth for the nightly version string:
|
||||
# <base>-nightly.<YYYYMMDD>-<short_sha> (mirrors the chart's nightly
|
||||
# scheme). build-images injects it into the frontend image (About-page
|
||||
# version) and publish-chart stamps it on Chart.yaml, so the version
|
||||
# users see and the chart version can't drift apart.
|
||||
run: |
|
||||
set -euo pipefail
|
||||
BASE=$(grep -m1 '^version:' deploy/helm/deer-flow/Chart.yaml | awk '{print $2}')
|
||||
# A malformed/missing Chart.yaml version would yield an empty BASE ->
|
||||
# `-nightly.<date>-<sha>` (invalid semver) that now flows into both
|
||||
# the chart publish and the frontend About-page version. Fail loudly.
|
||||
# `pipefail` matters: without it the pipeline's exit code is awk's,
|
||||
# so `set -e` alone wouldn't catch a grep miss.
|
||||
test -n "$BASE" || { echo "::error::empty base version from Chart.yaml"; exit 1; }
|
||||
SHORT_SHA="${GITHUB_SHA::7}"
|
||||
NIGHTLY="${BASE}-nightly.${{ steps.date.outputs.date }}-${SHORT_SHA}"
|
||||
echo "nightly_version=${NIGHTLY}" >> "$GITHUB_OUTPUT"
|
||||
echo "Nightly version: ${NIGHTLY}"
|
||||
|
||||
validate-chart:
|
||||
if: github.repository == 'bytedance/deer-flow'
|
||||
# Catch a broken render or a stale config_version before publish. A broken
|
||||
# chart published under an OCI version is immutable (GHCR won't let you
|
||||
# overwrite --version), so a regression must fail here, not on install.
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3
|
||||
|
||||
- name: Lint chart
|
||||
run: helm lint deploy/helm/deer-flow
|
||||
- name: Validate templates render
|
||||
run: helm template deer-flow deploy/helm/deer-flow --include-crds >/dev/null
|
||||
|
||||
# The chart's `config:` block embeds a config_version that must not fall
|
||||
# behind config.example.yaml. Shared with chart.yaml via
|
||||
# scripts/check_config_version.sh so the two workflows can't drift.
|
||||
- name: config_version drift check
|
||||
run: bash scripts/check_config_version.sh
|
||||
|
||||
build-images:
|
||||
needs: prepare
|
||||
if: github.repository == 'bytedance/deer-flow'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
attestations: write
|
||||
id-token: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- component: backend
|
||||
context: .
|
||||
file: backend/Dockerfile
|
||||
# Bake the `postgres` extra so multi-replica (K8s/Helm) deployments
|
||||
# can use shared Postgres. Matches container.yaml's release image.
|
||||
build-args: "UV_EXTRAS=postgres"
|
||||
- component: frontend
|
||||
context: .
|
||||
file: frontend/Dockerfile
|
||||
build-args: ""
|
||||
- component: provisioner
|
||||
context: docker/provisioner
|
||||
file: docker/provisioner/Dockerfile
|
||||
build-args: ""
|
||||
env:
|
||||
IMAGE_NAME: ${{ github.repository }}-${{ matrix.component }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 #v3.4.0
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 #v5.7.0
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
# `nightly` rolls forward each run; `nightly-YYYYMMDD` is pinned to a
|
||||
# day but mutable within it (a same-day re-dispatch overwrites it);
|
||||
# `sha-<short>` is the only truly immutable tag. No `latest` (that
|
||||
# stays on v* releases) and no branch/tag refs.
|
||||
tags: |
|
||||
type=raw,value=nightly
|
||||
type=raw,value=nightly-${{ needs.prepare.outputs.date }}
|
||||
type=sha
|
||||
- name: Build and push Docker image
|
||||
id: push
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 #v6.18.0
|
||||
with:
|
||||
context: ${{ matrix.context }}
|
||||
file: ${{ matrix.file }}
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
# APP_VERSION is consumed only by the frontend Dockerfile (it stamps
|
||||
# the About-page version); backend/provisioner Dockerfiles don't
|
||||
# declare it. Passed via build-arg because build-push-action doesn't
|
||||
# forward host env into the BuildKit build.
|
||||
build-args: |
|
||||
${{ matrix.build-args }}
|
||||
${{ matrix.component == 'frontend' && format('APP_VERSION={0}', needs.prepare.outputs.nightly_version) || '' }}
|
||||
- name: Generate artifact attestation
|
||||
uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be #v2.4.0
|
||||
with:
|
||||
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
subject-digest: ${{ steps.push.outputs.digest }}
|
||||
push-to-registry: true
|
||||
|
||||
publish-chart:
|
||||
# Ship the chart only after images build - it references all three, so a
|
||||
# failed image build withholds the chart rather than publishing one that
|
||||
# would fail to pull in-cluster.
|
||||
needs: [prepare, validate-chart, build-images]
|
||||
if: github.repository == 'bytedance/deer-flow'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
env:
|
||||
OWNER: ${{ github.repository_owner }}
|
||||
NIGHTLY: ${{ needs.prepare.outputs.nightly_version }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3
|
||||
|
||||
- name: Patch chart to a nightly version + nightly image defaults
|
||||
# Bumps Chart.yaml version/appVersion to the nightly version computed in
|
||||
# the prepare job (<base>-nightly.<date>-<sha>, a valid semver
|
||||
# prerelease; the short SHA makes each dispatch's version unique, so a
|
||||
# same-day re-dispatch re-publishes cleanly - OCI chart versions are
|
||||
# immutable and otherwise can't be overwritten). The same string is
|
||||
# injected into the frontend image, so the About-page version and the
|
||||
# chart version match. Repoints the chart's default image registry/tag
|
||||
# at the nightly build. Patches are in-workflow only - nothing is
|
||||
# committed back.
|
||||
run: |
|
||||
set -eu
|
||||
CHART=deploy/helm/deer-flow
|
||||
echo "Nightly chart version: ${NIGHTLY}"
|
||||
sed -i "s|^version:.*|version: ${NIGHTLY}|" "$CHART/Chart.yaml"
|
||||
sed -i "s|^appVersion:.*|appVersion: \"${NIGHTLY}\"|" "$CHART/Chart.yaml"
|
||||
sed -i "s|^ registry: \"\".*| registry: \"ghcr.io/${OWNER}\"|" "$CHART/values.yaml"
|
||||
sed -i 's|^ tag: "latest"| tag: "nightly"|' "$CHART/values.yaml"
|
||||
# Gate the patches: sed exits 0 even on zero matches, so a drifted
|
||||
# Chart.yaml/values.yaml would otherwise ship a chart that silently
|
||||
# pulls the wrong (release `latest`) images. Fail loudly if any patch
|
||||
# missed.
|
||||
grep -q "^version: ${NIGHTLY}$" "$CHART/Chart.yaml" || { echo "::error::Chart.yaml version sed did not apply"; exit 1; }
|
||||
grep -q "^appVersion: \"${NIGHTLY}\"$" "$CHART/Chart.yaml" || { echo "::error::Chart.yaml appVersion sed did not apply"; exit 1; }
|
||||
grep -q '^ registry: "ghcr.io/' "$CHART/values.yaml" || { echo "::error::values.yaml registry sed did not apply"; exit 1; }
|
||||
grep -q '^ tag: "nightly"' "$CHART/values.yaml" || { echo "::error::values.yaml tag sed did not apply"; exit 1; }
|
||||
echo "--- Chart.yaml (head) ---"; sed -n '1,7p' "$CHART/Chart.yaml"
|
||||
echo "--- image block ---"; sed -n '11,14p' "$CHART/values.yaml"
|
||||
|
||||
- name: Lint patched chart
|
||||
run: helm lint deploy/helm/deer-flow
|
||||
|
||||
- name: Log in to GHCR
|
||||
run: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | \
|
||||
helm registry login ghcr.io -u ${{ github.actor }} --password-stdin
|
||||
|
||||
- name: Package and push chart
|
||||
run: |
|
||||
helm package deploy/helm/deer-flow --destination ./packages
|
||||
for pkg in ./packages/*.tgz; do
|
||||
echo "--- pushing $pkg"
|
||||
helm push "$pkg" oci://ghcr.io/${{ github.repository_owner }}
|
||||
done
|
||||
@@ -0,0 +1,108 @@
|
||||
name: Replay E2E (front-back contract)
|
||||
|
||||
# Guards the front-back contract via record/replay (no API key in CI):
|
||||
# Layer 1 — backend golden: replay a recorded trace through the real gateway,
|
||||
# assert the SSE event sequence matches the committed golden.
|
||||
# Layer 2 — full-stack render: real Next.js frontend + real gateway (replay
|
||||
# model) + Chromium; assert the replayed turns render in the browser.
|
||||
# Triggered by changes on EITHER side of the contract so a backend change can no
|
||||
# longer pass without the frontend-facing checks running.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main", "2.0.x-dev"]
|
||||
paths:
|
||||
- "frontend/**"
|
||||
- "backend/app/gateway/**"
|
||||
- "backend/packages/harness/**"
|
||||
- "backend/tests/fixtures/replay/**"
|
||||
- "backend/tests/replay_provider.py"
|
||||
- "backend/tests/_replay_fixture.py"
|
||||
- "backend/tests/seed_runs_router.py"
|
||||
- "backend/tests/test_replay_golden.py"
|
||||
- "backend/scripts/run_replay_gateway.py"
|
||||
- ".github/workflows/replay-e2e.yml"
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
paths:
|
||||
- "frontend/**"
|
||||
- "backend/app/gateway/**"
|
||||
- "backend/packages/harness/**"
|
||||
- "backend/tests/fixtures/replay/**"
|
||||
- "backend/tests/replay_provider.py"
|
||||
- "backend/tests/_replay_fixture.py"
|
||||
- "backend/tests/seed_runs_router.py"
|
||||
- "backend/tests/test_replay_golden.py"
|
||||
- "backend/scripts/run_replay_gateway.py"
|
||||
- ".github/workflows/replay-e2e.yml"
|
||||
|
||||
concurrency:
|
||||
group: replay-e2e-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
backend-replay-golden:
|
||||
name: Layer 1 — backend golden (no API key)
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.draft == false
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
- name: Install backend dependencies
|
||||
working-directory: backend
|
||||
run: uv sync --group dev
|
||||
- name: Replay golden (backend SSE contract)
|
||||
working-directory: backend
|
||||
run: PYTHONPATH=. uv run pytest tests/test_replay_golden.py -v
|
||||
|
||||
fullstack-replay-render:
|
||||
name: Layer 2 — full-stack render (no API key)
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.draft == false
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
- name: Install backend dependencies (replay gateway)
|
||||
working-directory: backend
|
||||
run: uv sync --group dev
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
- name: Use pinned pnpm version
|
||||
run: corepack prepare pnpm@10.26.2 --activate
|
||||
- name: Install frontend dependencies
|
||||
working-directory: frontend
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Install Playwright Chromium
|
||||
working-directory: frontend
|
||||
run: npx playwright install chromium --with-deps
|
||||
- name: Full-stack replay render (DOM assertions are the gate)
|
||||
working-directory: frontend
|
||||
run: pnpm exec playwright test -c playwright.real-backend.config.ts
|
||||
- name: Upload report + render artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: replay-render
|
||||
path: |
|
||||
frontend/playwright-report/
|
||||
frontend/test-results/
|
||||
retention-days: 7
|
||||
@@ -0,0 +1,70 @@
|
||||
name: Skill Review CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main", "2.0.x-dev"]
|
||||
paths:
|
||||
- "skills/public/**"
|
||||
- "backend/packages/harness/deerflow/skills/review/**"
|
||||
- "contracts/skill_review/**"
|
||||
- "scripts/review_changed_public_skills.py"
|
||||
- "backend/pyproject.toml"
|
||||
- "backend/uv.lock"
|
||||
- ".github/workflows/skill-review-ci.yml"
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
paths:
|
||||
- "skills/public/**"
|
||||
- "backend/packages/harness/deerflow/skills/review/**"
|
||||
- "contracts/skill_review/**"
|
||||
- "scripts/review_changed_public_skills.py"
|
||||
- "backend/pyproject.toml"
|
||||
- "backend/uv.lock"
|
||||
- ".github/workflows/skill-review-ci.yml"
|
||||
|
||||
concurrency:
|
||||
group: skill-review-ci-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
skill-review:
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.draft == false
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
|
||||
- name: Install backend dependencies
|
||||
working-directory: backend
|
||||
run: uv sync --group dev
|
||||
|
||||
- name: Review changed public skills (pull request)
|
||||
if: github.event_name == 'pull_request'
|
||||
working-directory: backend
|
||||
run: |
|
||||
uv run python ../scripts/review_changed_public_skills.py \
|
||||
--base-ref "${{ github.event.pull_request.base.sha }}" \
|
||||
--head-ref "${{ github.event.pull_request.head.sha }}"
|
||||
|
||||
- name: Review changed public skills (push)
|
||||
if: github.event_name == 'push'
|
||||
working-directory: backend
|
||||
run: |
|
||||
uv run python ../scripts/review_changed_public_skills.py \
|
||||
--before "${{ github.event.before }}" \
|
||||
--after "${{ github.event.after }}"
|
||||
@@ -0,0 +1,223 @@
|
||||
name: Triage
|
||||
|
||||
# One workflow for all event-driven PR/issue labeling. Replaces the former
|
||||
# pr-labeler / pr-triage / issue-triage workflows (and drops actions/labeler).
|
||||
#
|
||||
# Design notes:
|
||||
# * All jobs are pure-metadata: they read changed-file lists / PR fields / the
|
||||
# review payload via the API and write labels. PR code is NEVER checked out
|
||||
# or executed, so pull_request_target is safe here.
|
||||
# * Each job only reconciles labels in namespaces IT owns
|
||||
# (area:* / size/* / risk:* / needs-validation). It never touches labels
|
||||
# applied by maintainers or other tools (bug, priority, etc.). first-time-
|
||||
# contributor and reviewing are add-only.
|
||||
# * State is read LIVE (listFiles + listLabelsOnIssue) at run time, not from
|
||||
# the (stale) event payload, so rapid synchronize events converge instead
|
||||
# of thrashing.
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
# ── PR: area / size / risk / needs-validation / first-time ─────────────────
|
||||
pr-labels:
|
||||
if: github.event_name == 'pull_request_target' && github.event.pull_request.draft == false
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: triage-pr-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- name: Apply PR labels from live state
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const pr = context.payload.pull_request;
|
||||
const { owner, repo } = context.repo;
|
||||
const num = pr.number;
|
||||
|
||||
// ---- live changed files ----
|
||||
const files = await github.paginate(github.rest.pulls.listFiles, {
|
||||
owner, repo, pull_number: num, per_page: 100,
|
||||
});
|
||||
const paths = files.map(f => f.filename);
|
||||
const m = (re) => paths.some(p => re.test(p));
|
||||
|
||||
// ---- area: replaces .github/labeler.yml (path -> area) ----
|
||||
const AREA_RULES = [
|
||||
['area:frontend', [/^frontend\//]],
|
||||
['area:backend', [/^backend\/app\//, /^backend\/packages\/harness\/deerflow\/(runtime|persistence|config|tools|guardrails|tracing|models|utils|uploads)\//]],
|
||||
['area:agents', [/^backend\/packages\/harness\/deerflow\/(agents|subagents|reflection)\//, /(^|\/)langgraph\.json$/, /^backend\/.*\/prompts\//]],
|
||||
['area:sandbox', [/^docker\//, /^backend\/packages\/harness\/deerflow\/sandbox\//, /(^|\/)Dockerfile$/]],
|
||||
['area:skills', [/^skills\//, /^backend\/packages\/harness\/deerflow\/skills\//, /^frontend\/src\/core\/skills\//]],
|
||||
['area:mcp', [/^backend\/packages\/harness\/deerflow\/mcp\//, /^frontend\/src\/core\/mcp\//]],
|
||||
['area:ci', [/^\.github\//, /^scripts\//]],
|
||||
['area:docs', [/^docs\//, /\.mdx?$/]],
|
||||
['area:deps', [/(^|\/)(pyproject\.toml|uv\.lock|package\.json|pnpm-lock\.yaml)$/]],
|
||||
];
|
||||
const areaLabels = AREA_RULES
|
||||
.filter(([, res]) => res.some(re => m(re)))
|
||||
.map(([label]) => label);
|
||||
|
||||
// ---- size: additions+deletions, excluding lockfiles/snapshots ----
|
||||
const EXCLUDE_SIZE = /(^|\/)(uv\.lock|pnpm-lock\.yaml|package-lock\.json)$|\.snap$/;
|
||||
const churn = files
|
||||
.filter(f => !EXCLUDE_SIZE.test(f.filename))
|
||||
.reduce((s, f) => s + (f.additions || 0) + (f.deletions || 0), 0);
|
||||
const sizeLabel =
|
||||
churn < 20 ? 'size/XS' :
|
||||
churn < 100 ? 'size/S' :
|
||||
churn < 300 ? 'size/M' :
|
||||
churn < 700 ? 'size/L' : 'size/XL';
|
||||
|
||||
// ---- risk ----
|
||||
const docsOnly = paths.length > 0 && paths.every(p =>
|
||||
/\.(md|mdx|txt)$/i.test(p) || p.startsWith('docs/') ||
|
||||
/\.(png|jpe?g|gif|svg|webp|ico)$/i.test(p));
|
||||
const highRisk =
|
||||
m(/^backend\/app\/gateway\//) ||
|
||||
m(/^backend\/packages\/harness\/deerflow\/(agents|subagents|sandbox)\//) ||
|
||||
m(/(^|\/)langgraph\.json$/) ||
|
||||
m(/(^|\/)(auth|authz|security)/i) ||
|
||||
m(/(pyproject\.toml|uv\.lock|package\.json|pnpm-lock\.yaml)$/) ||
|
||||
m(/^docker\//) ||
|
||||
m(/^\.github\/workflows\//);
|
||||
const riskLabel = docsOnly ? 'risk:low' : (highRisk ? 'risk:high' : 'risk:medium');
|
||||
|
||||
// ---- needs-validation: front/back contract surface ----
|
||||
const contract =
|
||||
m(/^backend\/app\/gateway\//) ||
|
||||
m(/^backend\/packages\/harness\/deerflow\/(agents|subagents)\//) ||
|
||||
m(/(^|\/)langgraph\.json$/) ||
|
||||
m(/^frontend\/src\/core\/(api|threads|messages)\//);
|
||||
|
||||
// ---- live current labels (NOT the stale event payload) ----
|
||||
const current = (await github.paginate(github.rest.issues.listLabelsOnIssue, {
|
||||
owner, repo, issue_number: num, per_page: 100,
|
||||
})).map(l => l.name);
|
||||
const hasSkip = current.includes('skip-validation');
|
||||
|
||||
// Reconcile ONLY namespaces we own; never touch others.
|
||||
const owned = (n) =>
|
||||
n.startsWith('area:') || n.startsWith('size/') ||
|
||||
n.startsWith('risk:') || n === 'needs-validation';
|
||||
const desired = new Set([...areaLabels, sizeLabel, riskLabel]);
|
||||
if (contract && !hasSkip) desired.add('needs-validation');
|
||||
|
||||
const toRemove = current.filter(n => owned(n) && !desired.has(n));
|
||||
const toAdd = [...desired].filter(n => !current.includes(n));
|
||||
|
||||
// first-time-contributor: add-only, on opened, real users only.
|
||||
if (context.payload.action === 'opened' &&
|
||||
pr.user.type === 'User' &&
|
||||
['FIRST_TIME_CONTRIBUTOR', 'FIRST_TIMER'].includes(pr.author_association) &&
|
||||
!current.includes('first-time-contributor')) {
|
||||
toAdd.push('first-time-contributor');
|
||||
}
|
||||
|
||||
for (const name of toRemove) {
|
||||
try {
|
||||
await github.rest.issues.removeLabel({ owner, repo, issue_number: num, name });
|
||||
} catch (e) {
|
||||
if (e.status !== 404) throw e;
|
||||
}
|
||||
}
|
||||
if (toAdd.length) {
|
||||
await github.rest.issues.addLabels({ owner, repo, issue_number: num, labels: toAdd });
|
||||
}
|
||||
core.info(`area=[${areaLabels.join(',')}] ${sizeLabel} ${riskLabel} churn=${churn} ` +
|
||||
`validation=${desired.has('needs-validation')} ` +
|
||||
`(+${toAdd.join(',') || '-'} / -${toRemove.join(',') || '-'})`);
|
||||
|
||||
# ── PR: reviewing label on a maintainer's human review ─────────────────────
|
||||
reviewing:
|
||||
if: github.event_name == 'pull_request_review'
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: triage-review-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: false
|
||||
steps:
|
||||
- name: Add reviewing label for maintainer reviews
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const num = context.payload.pull_request.number;
|
||||
const review = context.payload.review;
|
||||
const assoc = review.author_association; // payload field; no API call
|
||||
const type = review.user && review.user.type;
|
||||
|
||||
// author_association is NONE for every automated reviewer
|
||||
// (Copilot, CodeRabbit, Codex, Sourcery, ...), so this allowlist
|
||||
// drops them all without a denylist — and never calls the
|
||||
// collaborators API that 404s on "Copilot is not a user".
|
||||
// user.type === 'User' guards the rare bot-added-as-collaborator case.
|
||||
if (!['OWNER', 'MEMBER', 'COLLABORATOR'].includes(assoc) || type !== 'User') {
|
||||
core.info(`reviewer ${review.user && review.user.login} assoc=${assoc} type=${type}; skipping.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const labels = (await github.paginate(github.rest.issues.listLabelsOnIssue, {
|
||||
owner, repo, issue_number: num, per_page: 100,
|
||||
})).map(l => l.name);
|
||||
if (labels.includes('reviewing')) {
|
||||
core.info('Already labeled reviewing; skipping.');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await github.rest.issues.addLabels({
|
||||
owner, repo, issue_number: num, labels: ['reviewing'],
|
||||
});
|
||||
core.info('Added "reviewing".');
|
||||
} catch (e) {
|
||||
if (e.status === 403) core.info('No permission to label (expected on some fork PRs).');
|
||||
else throw e;
|
||||
}
|
||||
|
||||
# ── Issue: needs-triage on every new issue ────────────────────────────────
|
||||
issue-triage:
|
||||
if: github.event_name == 'issues'
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: triage-issue-${{ github.event.issue.number }}
|
||||
cancel-in-progress: false
|
||||
steps:
|
||||
- name: Add needs-triage label
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const issue_number = context.payload.issue.number;
|
||||
|
||||
// Read live labels (not the event payload) so labels added at creation
|
||||
// time via the API or by another automation are seen — consistent with
|
||||
// the live-state reads in the PR jobs above.
|
||||
const current = (await github.paginate(github.rest.issues.listLabelsOnIssue, {
|
||||
owner, repo, issue_number, per_page: 100,
|
||||
})).map(l => l.name);
|
||||
if (current.includes('needs-triage')) {
|
||||
core.info('Issue already has needs-triage; nothing to do.');
|
||||
return;
|
||||
}
|
||||
// Self-heal: create the label if it does not exist yet.
|
||||
try {
|
||||
await github.rest.issues.createLabel({
|
||||
owner, repo, name: 'needs-triage', color: 'fef2c0',
|
||||
description: 'Awaiting maintainer triage',
|
||||
});
|
||||
} catch (e) {
|
||||
if (e.status !== 422) throw e; // 422 = already exists
|
||||
}
|
||||
await github.rest.issues.addLabels({
|
||||
owner, repo, issue_number, labels: ['needs-triage'],
|
||||
});
|
||||
core.info(`Added needs-triage to #${issue_number}.`);
|
||||
@@ -0,0 +1,27 @@
|
||||
name: Verify Versions
|
||||
|
||||
# Reusable workflow: checks that every project version source agrees with the
|
||||
# git tag that triggered the release. Called by chart.yaml and container.yaml
|
||||
# on v* tags so a forgotten version bump blocks the entire release (container
|
||||
# images + chart), not just the chart.
|
||||
#
|
||||
# Sources verified: deploy/helm/deer-flow/Chart.yaml (version + appVersion),
|
||||
# backend/pyproject.toml, frontend/package.json. Logic lives in
|
||||
# scripts/verify_versions.sh so it can also be run locally.
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
verify-versions:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3
|
||||
|
||||
- name: Verify all version sources match tag
|
||||
run: |
|
||||
TAG_VERSION="${GITHUB_REF_NAME#v}"
|
||||
bash scripts/verify_versions.sh "$TAG_VERSION"
|
||||
Reference in New Issue
Block a user