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//deer-flow-{backend,frontend,provisioner}:nightly # ghcr.io//deer-flow-{backend,frontend,provisioner}:nightly-YYYYMMDD # oci://ghcr.io//charts/deer-flow chart version -nightly.YYYYMMDD- # # The nightly chart defaults image.tag=nightly and image.registry=ghcr.io/ # (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: # -nightly.- (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.-` (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-` 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 (-nightly.-, 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 }}/charts done