name: Build and Push Docker Images on Tag on: push: tags: - "*" workflow_dispatch: # Set restrictive default permissions for all jobs. Jobs that need more permissions # should explicitly declare them. permissions: # Required to checkout the repo on private repos (no-op on public) contents: read # Required for OIDC authentication with AWS id-token: write # zizmor: ignore[excessive-permissions] env: EDGE_TAG: ${{ startsWith(github.ref_name, 'nightly-latest') || github.ref_name == 'edge' }} jobs: # Determine which components to build based on the tag determine-builds: # NOTE: Github-hosted runners have about 20s faster queue times and are preferred here. runs-on: ubuntu-slim timeout-minutes: 90 outputs: build-desktop: ${{ steps.check.outputs.build-desktop }} build-web: ${{ steps.check.outputs.build-web }} build-web-cloud: ${{ steps.check.outputs.build-web-cloud }} build-backend: ${{ steps.check.outputs.build-backend }} build-model-server: ${{ steps.check.outputs.build-model-server }} is-cloud-tag: ${{ steps.check.outputs.is-cloud-tag }} is-beta: ${{ steps.check.outputs.is-beta }} is-beta-standalone: ${{ steps.check.outputs.is-beta-standalone }} is-stable: ${{ steps.check.outputs.is-stable }} is-latest: ${{ steps.check.outputs.is-latest }} is-test-run: ${{ steps.check.outputs.is-test-run }} sanitized-tag: ${{ steps.check.outputs.sanitized-tag }} short-sha: ${{ steps.check.outputs.short-sha }} steps: - name: Checkout (for git tags) uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # ratchet:actions/checkout@v6 with: persist-credentials: false fetch-depth: 0 fetch-tags: true - name: Setup uv uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # ratchet:astral-sh/setup-uv@v10.0.1 with: version: "0.11.25" enable-cache: false - name: Install ods from uv.lock run: uv sync --frozen --no-install-project --only-group ods - name: Check which components to build and version info id: check env: EVENT_NAME: ${{ github.event_name }} run: | set -eo pipefail # `ods release classify` owns the routing rules; see # tools/ods/internal/release/classify.go. The checkout above fetches # all tags, which the "latest" decision needs. uv run --no-sync ods release classify \ --ref "${GITHUB_REF_NAME}" \ --sha "${GITHUB_SHA}" \ --event "${EVENT_NAME}" >> "$GITHUB_OUTPUT" # Block the release when dependencies carry unignored critical vulnerabilities. # Runs the same `ods audit` gate used on PRs and nightly. Always runs (no skip # condition) so the build jobs that depend on it aren't skipped on test runs; # the audit itself degrades gracefully when the S3 allowlist or Dependabot API # is unavailable. audit-gate: needs: [determine-builds] runs-on: ubuntu-latest timeout-minutes: 20 permissions: contents: read # checkout + read lockfiles id-token: write # OIDC for fetching the S3 allowlist and the registry credentials vulnerability-alerts: read # read Dependabot alerts security-events: write # SARIF uploads of the image audits steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # ratchet:actions/checkout@v6 with: persist-credentials: false - name: Setup uv uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # ratchet:astral-sh/setup-uv@v10.0.1 with: version: "0.11.25" enable-cache: false - name: Install ods from uv.lock run: uv sync --frozen --no-install-project --only-group ods - name: Configure AWS credentials continue-on-error: true # the audit still runs (with no suppressions) if creds are unavailable uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # ratchet:aws-actions/configure-aws-credentials@v6.2.3 with: role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }} aws-region: us-east-2 - name: Run dependency audit env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: uv run --no-sync ods audit --fail-on=critical # The same registry credentials the builds use, fetched the same way, so # the bases scanned are the bases shipped. Missing credentials fail the # gate, since the builds would ship hardened bases the gate never saw. - name: Get AWS Secrets uses: aws-actions/aws-secretsmanager-get-secrets@2cb1a461cbd4865ac4299648312e4704c646cd53 with: secret-ids: | DOCKER_USERNAME, deploy/docker-username DOCKER_TOKEN, deploy/docker-token parse-json-secrets: true - name: Login to Docker Hub uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 with: username: ${{ env.DOCKER_USERNAME }} password: ${{ env.DOCKER_TOKEN }} - name: Login to the hardened base images uses: ./.github/actions/dhi-base-images with: docker-username: ${{ env.DOCKER_USERNAME }} docker-token: ${{ env.DOCKER_TOKEN }} # Everything a shipped image holds outside its lockfiles: the pinned # runtime base for web and model-server, the backend's and the sandbox's # os stage built here. All four are scanned before the step fails, so one run shows # every blocker, and --strict keeps an empty scan from passing. - name: Run base image audit run: | status=0 for component in web model-server backend sandbox; do image="$(.github/scripts/base-image-ref.sh "${component}")" uv run --no-sync ods audit image "${image}" --fail-on=critical --strict --format=sarif,text > "image-audit-${component}.sarif" || status=1 if [ ! -s "image-audit-${component}.sarif" ]; then # $schema is a literal SARIF key, not a shell variable. # shellcheck disable=SC2016 echo '{"$schema":"https://json.schemastore.org/sarif-2.1.0.json","version":"2.1.0","runs":[]}' > "image-audit-${component}.sarif" fi # A stage built here is named "name:tag" with no registry path, which # the Security tab reads as a URI scheme and rejects; point its # findings at the Dockerfile the stage comes from. case "${component}" in backend) dockerfile=backend/Dockerfile ;; sandbox) dockerfile=backend/onyx/server/features/build/sandbox/image/Dockerfile ;; *) dockerfile="" ;; esac if [ -n "${dockerfile}" ]; then jq --arg uri "${dockerfile}" '(.runs[]?.results[]?.locations[]?.physicalLocation.artifactLocation.uri) |= $uri' \ "image-audit-${component}.sarif" > "image-audit-${component}.sarif.tmp" mv "image-audit-${component}.sarif.tmp" "image-audit-${component}.sarif" fi done exit "${status}" # Rolling main-HEAD builds map to refs/heads/main so the Security tab stays # authoritative for main; test runs upload against their own ref. Release # tags sit on older commits, so they gate without uploading. - name: Upload the web image audit to the GitHub Security tab id: upload-web continue-on-error: true # reporting never blocks a release; the gate's verdict is the step above if: >- always() && ((env.EDGE_TAG == 'true' && needs.determine-builds.outputs.is-test-run != 'true') || needs.determine-builds.outputs.is-test-run == 'true') uses: github/codeql-action/upload-sarif@ba454b8ab46733eb6145342877cd148270bb77ab with: sarif_file: image-audit-web.sarif category: image-audit-web ref: ${{ needs.determine-builds.outputs.is-test-run == 'true' && github.ref || 'refs/heads/main' }} sha: ${{ github.sha }} - name: Upload the model-server image audit to the GitHub Security tab id: upload-model-server continue-on-error: true # reporting never blocks a release; the gate's verdict is the step above if: >- always() && ((env.EDGE_TAG == 'true' && needs.determine-builds.outputs.is-test-run != 'true') || needs.determine-builds.outputs.is-test-run == 'true') uses: github/codeql-action/upload-sarif@ba454b8ab46733eb6145342877cd148270bb77ab with: sarif_file: image-audit-model-server.sarif category: image-audit-model-server ref: ${{ needs.determine-builds.outputs.is-test-run == 'true' && github.ref || 'refs/heads/main' }} sha: ${{ github.sha }} - name: Upload the backend image audit to the GitHub Security tab id: upload-backend continue-on-error: true # reporting never blocks a release; the gate's verdict is the step above if: >- always() && ((env.EDGE_TAG == 'true' && needs.determine-builds.outputs.is-test-run != 'true') || needs.determine-builds.outputs.is-test-run == 'true') uses: github/codeql-action/upload-sarif@ba454b8ab46733eb6145342877cd148270bb77ab with: sarif_file: image-audit-backend.sarif category: image-audit-backend ref: ${{ needs.determine-builds.outputs.is-test-run == 'true' && github.ref || 'refs/heads/main' }} sha: ${{ github.sha }} - name: Upload the sandbox image audit to the GitHub Security tab id: upload-sandbox continue-on-error: true # reporting never blocks a release; the gate's verdict is the step above if: >- always() && ((env.EDGE_TAG == 'true' && needs.determine-builds.outputs.is-test-run != 'true') || needs.determine-builds.outputs.is-test-run == 'true') uses: github/codeql-action/upload-sarif@ba454b8ab46733eb6145342877cd148270bb77ab with: sarif_file: image-audit-sandbox.sarif category: image-audit-sandbox ref: ${{ needs.determine-builds.outputs.is-test-run == 'true' && github.ref || 'refs/heads/main' }} sha: ${{ github.sha }} # A rejected upload leaves the Security tab stale without failing the # job, so it is called out where the run is read. - name: Report failed uploads if: always() && (steps.upload-web.outcome == 'failure' || steps.upload-model-server.outcome == 'failure' || steps.upload-backend.outcome == 'failure' || steps.upload-sandbox.outcome == 'failure') env: WEB: ${{ steps.upload-web.outcome }} MODEL_SERVER: ${{ steps.upload-model-server.outcome }} BACKEND: ${{ steps.upload-backend.outcome }} SANDBOX: ${{ steps.upload-sandbox.outcome }} run: | failed="" [ "${WEB}" = failure ] && failed="${failed} web" [ "${MODEL_SERVER}" = failure ] && failed="${failed} model-server" [ "${BACKEND}" = failure ] && failed="${failed} backend" [ "${SANDBOX}" = failure ] && failed="${failed} sandbox" echo "::warning::The Security tab upload failed for:${failed}. The tab is stale until the next nightly upload succeeds." echo "Security tab upload failed for:${failed}" >> "${GITHUB_STEP_SUMMARY}" # Validate that a pushed release tag is versioned correctly, via # `ods release --check`. A cloud tag must be the tag `ods release cloud` # would have cut: its commit on origin/main, its base matching the release # branches, its counter in sequence. Stable (vX.Y.Z) and beta # (vX.Y.Z-beta.N) tags must sit on their release/vX.Y branch with the # patch or counter in sequence; a beta must precede its stable release. # # Deliberately gated on the raw ref name, not on computed tag-type outputs: # a tag that is neither a valid cloud nor stable tag fails loudly here # instead of being skipped by a wrongly computed output. check-version-tag: runs-on: ubuntu-slim timeout-minutes: 10 if: ${{ !startsWith(github.ref_name, 'nightly-latest') && github.ref_name != 'edge' && github.event_name != 'workflow_dispatch' }} steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # ratchet:actions/checkout@v6 with: persist-credentials: false fetch-depth: 0 - name: Setup uv uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # ratchet:astral-sh/setup-uv@v10.0.1 with: version: "0.11.25" # NOTE: This isn't caching much and zizmor suggests this could be poisoned, so disable. enable-cache: false - name: Install ods from uv.lock run: uv sync --frozen --no-install-project --only-group ods - name: Validate the tag is versioned correctly run: | uv run --no-sync ods release --check --ref "$GITHUB_REF_NAME" notify-slack-on-tag-check-failure: needs: - check-version-tag if: always() && needs.check-version-tag.result == 'failure' && github.event_name != 'workflow_dispatch' runs-on: ubuntu-slim timeout-minutes: 10 environment: release steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # ratchet:actions/checkout@v6 with: persist-credentials: false - name: Send Slack notification uses: ./.github/actions/slack-notify with: webhook-url: ${{ secrets.MONITOR_DEPLOYMENTS_WEBHOOK }} failed-jobs: "• check-version-tag" title: "🚨 Version Tag Check Failed" ref-name: ${{ github.ref_name }} # Create GitHub release first, before desktop builds start. # This ensures all desktop matrix jobs upload to the same release instead of # racing to create duplicate releases. create-release: needs: - determine-builds - audit-gate if: needs.determine-builds.outputs.build-desktop == 'true' runs-on: ubuntu-slim timeout-minutes: 10 permissions: contents: write outputs: release-id: ${{ steps.create-release.outputs.id }} steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # ratchet:actions/checkout@v6 with: persist-credentials: false - name: Determine release tag id: release-tag env: IS_TEST_RUN: ${{ needs.determine-builds.outputs.is-test-run }} SHORT_SHA: ${{ needs.determine-builds.outputs.short-sha }} run: | if [ "${IS_TEST_RUN}" == "true" ]; then echo "tag=v0.0.0-dev+${SHORT_SHA}" >> "$GITHUB_OUTPUT" else echo "tag=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT" fi - name: Create GitHub Release id: create-release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAG: ${{ steps.release-tag.outputs.tag }} run: | # Look up an existing release (including drafts) by tag name. # # NOTE: a draft release does not create the underlying git tag until # it is published, so `GET /releases/tags/{tag}` (and # `gh release view `) returns 404 for drafts. We must list # releases instead — that endpoint DOES include drafts — and match # on tag_name. get_release_id() { gh api --paginate "repos/${GITHUB_REPOSITORY}/releases" \ --jq ".[] | select(.tag_name == \"${TAG}\") | .id" | head -n1 } release_id=$(get_release_id) if [ -z "$release_id" ]; then release_id=$(gh api --method POST "repos/${GITHUB_REPOSITORY}/releases" \ -f tag_name="$TAG" \ -f name="$TAG" \ -f body="See the assets to download this version and install." \ -F draft=true \ --jq '.id') fi if [ -z "$release_id" ]; then echo "::error::Failed to resolve release id for tag ${TAG}" exit 1 fi echo "id=${release_id}" >> "$GITHUB_OUTPUT" build-desktop: needs: - determine-builds - create-release if: needs.determine-builds.outputs.build-desktop == 'true' permissions: id-token: write contents: write actions: read strategy: fail-fast: false matrix: include: - platform: "macos-latest" # Build a universal image for macOS. args: "--target universal-apple-darwin" rust-targets: "aarch64-apple-darwin,x86_64-apple-darwin" - platform: "ubuntu-24.04" args: "--bundles deb,rpm" - platform: "ubuntu-24.04-arm" # Only available in public repos. args: "--bundles deb,rpm" - platform: "windows-latest" args: "" # Cross-compiled Windows ARM64 build. Only the NSIS bundle supports # ARM64 (WiX/MSI does not), hence --bundles nsis. - platform: "windows-latest" args: "--target aarch64-pc-windows-msvc --bundles nsis" rust-targets: "aarch64-pc-windows-msvc" runs-on: ${{ matrix.platform }} timeout-minutes: 90 environment: release steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # ratchet:actions/checkout@v7.0.1 with: # NOTE: persist-credentials is needed for tauri-action to upload assets to GitHub releases. persist-credentials: true # zizmor: ignore[artipacked] - name: Configure AWS credentials if: startsWith(matrix.platform, 'macos-') uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c with: role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }} aws-region: us-east-2 - name: Get AWS Secrets if: startsWith(matrix.platform, 'macos-') uses: aws-actions/aws-secretsmanager-get-secrets@2cb1a461cbd4865ac4299648312e4704c646cd53 with: secret-ids: | APPLE_ID, deploy/apple-id APPLE_PASSWORD, deploy/apple-password APPLE_CERTIFICATE, deploy/apple-certificate APPLE_CERTIFICATE_PASSWORD, deploy/apple-certificate-password KEYCHAIN_PASSWORD, deploy/keychain-password APPLE_TEAM_ID, deploy/apple-team-id parse-json-secrets: true - name: install dependencies (ubuntu only) if: startsWith(matrix.platform, 'ubuntu-') run: | sudo apt-get update sudo apt-get install -y \ build-essential \ libglib2.0-dev \ libgirepository1.0-dev \ libgtk-3-dev \ libjavascriptcoregtk-4.1-dev \ libwebkit2gtk-4.1-dev \ libayatana-appindicator3-dev \ gobject-introspection \ pkg-config \ curl \ xdg-utils - name: Setup Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # ratchet:oven-sh/setup-bun@v2 # zizmor: ignore[cache-poisoning] with: bun-version: "1.3.13" - name: install Rust stable uses: dtolnay/rust-toolchain@6d9817901c499d6b02debbb57edb38d33daa680b # zizmor: ignore[impostor-commit] with: # Extra targets for jobs that cross-compile (macOS universal, Windows ARM64). # Jobs building only for the host architecture leave this empty. targets: ${{ matrix.rust-targets || '' }} - name: install frontend dependencies run: bun install --frozen-lockfile - name: Inject version (Unix) if: runner.os != 'Windows' working-directory: ./desktop env: SHORT_SHA: ${{ needs.determine-builds.outputs.short-sha }} IS_TEST_RUN: ${{ needs.determine-builds.outputs.is-test-run }} run: | if [ "${IS_TEST_RUN}" == "true" ]; then VERSION="0.0.0-dev+${SHORT_SHA}" else VERSION="${GITHUB_REF_NAME#v}" fi echo "Injecting version: $VERSION" # Update Cargo.toml sed "s/^version = .*/version = \"$VERSION\"/" src-tauri/Cargo.toml > src-tauri/Cargo.toml.tmp mv src-tauri/Cargo.toml.tmp src-tauri/Cargo.toml # Update tauri.conf.json jq --arg v "$VERSION" '.version = $v' src-tauri/tauri.conf.json > src-tauri/tauri.conf.json.tmp mv src-tauri/tauri.conf.json.tmp src-tauri/tauri.conf.json # Update package.json jq --arg v "$VERSION" '.version = $v' package.json > package.json.tmp mv package.json.tmp package.json echo "Versions set to: $VERSION" - name: Inject version (Windows) if: runner.os == 'Windows' working-directory: ./desktop shell: pwsh env: IS_TEST_RUN: ${{ needs.determine-builds.outputs.is-test-run }} run: | # Windows MSI requires numeric-only build metadata, so we skip the SHA suffix if ($env:IS_TEST_RUN -eq "true") { $VERSION = "0.0.0" } else { # Strip 'v' prefix and any pre-release suffix (e.g., -beta.13) for MSI compatibility $VERSION = "$env:GITHUB_REF_NAME" -replace '^v', '' -replace '-.*$', '' } Write-Host "Injecting version: $VERSION" # Update Cargo.toml $cargo = Get-Content src-tauri/Cargo.toml -Raw $cargo = $cargo -replace '(?m)^version = .*', "version = `"$VERSION`"" Set-Content src-tauri/Cargo.toml $cargo -NoNewline # Update tauri.conf.json $json = Get-Content src-tauri/tauri.conf.json | ConvertFrom-Json $json.version = $VERSION $json | ConvertTo-Json -Depth 100 | Set-Content src-tauri/tauri.conf.json # Update package.json $pkg = Get-Content package.json | ConvertFrom-Json $pkg.version = $VERSION $pkg | ConvertTo-Json -Depth 100 | Set-Content package.json Write-Host "Versions set to: $VERSION" - name: Import Apple Developer Certificate if: startsWith(matrix.platform, 'macos-') run: | echo $APPLE_CERTIFICATE | base64 --decode > certificate.p12 security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain security default-keychain -s build.keychain security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain security set-keychain-settings -t 3600 -u build.keychain security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain security find-identity -v -p codesigning build.keychain - name: Verify Certificate if: startsWith(matrix.platform, 'macos-') run: | CERT_INFO=$(security find-identity -v -p codesigning build.keychain | grep -E "(Developer ID Application|Apple Distribution|Apple Development)" | head -n 1) CERT_ID=$(echo "$CERT_INFO" | awk -F'"' '{print $2}') echo "CERT_ID=$CERT_ID" >> $GITHUB_ENV echo "Certificate imported." - uses: tauri-apps/tauri-action@1deb371b0cd8bd54025b384f1cd735e725c4060f # ratchet:tauri-apps/tauri-action@action-v1.0.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} APPLE_ID: ${{ env.APPLE_ID }} APPLE_PASSWORD: ${{ env.APPLE_PASSWORD }} APPLE_SIGNING_IDENTITY: ${{ env.CERT_ID }} APPLE_TEAM_ID: ${{ env.APPLE_TEAM_ID }} with: projectPath: desktop # Use the release created by the create-release job to avoid race conditions # when multiple matrix jobs try to create/update the same release simultaneously releaseId: ${{ needs.create-release.outputs.release-id }} releaseAssetNamePattern: "[name]_[arch][ext]" args: ${{ matrix.args }} build-web-amd64: needs: - determine-builds - audit-gate if: needs.determine-builds.outputs.build-web == 'true' runs-on: - runs-on - runner=4cpu-linux-x64 - run-id=${{ github.run_id }}-web-amd64 - extras=ecr-cache timeout-minutes: 90 environment: release outputs: digest: ${{ steps.build.outputs.digest }} env: REGISTRY_IMAGE: onyxdotapp/onyx-web-server steps: - uses: runs-on/action@4e5f72399b6b17f2e79c511c1b38a315a64d22dc - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # ratchet:actions/checkout@v6 with: persist-credentials: false - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c with: role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }} aws-region: us-east-2 - name: Get AWS Secrets uses: aws-actions/aws-secretsmanager-get-secrets@2cb1a461cbd4865ac4299648312e4704c646cd53 with: secret-ids: | DOCKER_USERNAME, deploy/docker-username DOCKER_TOKEN, deploy/docker-token parse-json-secrets: true - name: Docker meta id: meta uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 with: images: ${{ needs.determine-builds.outputs.is-test-run == 'true' && env.RUNS_ON_ECR_CACHE || env.REGISTRY_IMAGE }} flavor: | latest=false - name: Set up Docker Buildx uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # ratchet:docker/setup-buildx-action@v4 - name: Login to Docker Hub uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 with: username: ${{ env.DOCKER_USERNAME }} password: ${{ env.DOCKER_TOKEN }} # web/Dockerfile defaults to the public Node bases. Release builds ship on the # hardened DHI equivalents, passed as build args below. - name: Resolve Docker Hardened Image bases uses: ./.github/actions/dhi-base-images with: docker-username: ${{ env.DOCKER_USERNAME }} docker-token: ${{ env.DOCKER_TOKEN }} - name: Build and push AMD64 id: build uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a with: context: ./web file: ./web/Dockerfile platforms: linux/amd64 labels: ${{ steps.meta.outputs.labels }} build-args: | ONYX_VERSION=${{ github.ref_name }} ${{ env.DHI_NODE_BUILD_ARGS }} NODE_OPTIONS=--max-old-space-size=8192 cache-from: | type=registry,ref=${{ env.REGISTRY_IMAGE }}:edge type=registry,ref=${{ env.REGISTRY_IMAGE }}:latest cache-to: type=inline outputs: type=image,name=${{ needs.determine-builds.outputs.is-test-run == 'true' && env.RUNS_ON_ECR_CACHE || env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true no-cache: ${{ vars.DOCKER_NO_CACHE == 'true' }} build-web-arm64: needs: - determine-builds - audit-gate if: needs.determine-builds.outputs.build-web == 'true' runs-on: - runs-on - runner=4cpu-linux-arm64 - run-id=${{ github.run_id }}-web-arm64 - extras=ecr-cache timeout-minutes: 90 environment: release outputs: digest: ${{ steps.build.outputs.digest }} env: REGISTRY_IMAGE: onyxdotapp/onyx-web-server steps: - uses: runs-on/action@4e5f72399b6b17f2e79c511c1b38a315a64d22dc - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # ratchet:actions/checkout@v6 with: persist-credentials: false - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c with: role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }} aws-region: us-east-2 - name: Get AWS Secrets uses: aws-actions/aws-secretsmanager-get-secrets@2cb1a461cbd4865ac4299648312e4704c646cd53 with: secret-ids: | DOCKER_USERNAME, deploy/docker-username DOCKER_TOKEN, deploy/docker-token parse-json-secrets: true - name: Docker meta id: meta uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 with: images: ${{ needs.determine-builds.outputs.is-test-run == 'true' && env.RUNS_ON_ECR_CACHE || env.REGISTRY_IMAGE }} flavor: | latest=false - name: Set up Docker Buildx uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # ratchet:docker/setup-buildx-action@v4 - name: Login to Docker Hub uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 with: username: ${{ env.DOCKER_USERNAME }} password: ${{ env.DOCKER_TOKEN }} # web/Dockerfile defaults to the public Node bases. Release builds ship on the # hardened DHI equivalents, passed as build args below. - name: Resolve Docker Hardened Image bases uses: ./.github/actions/dhi-base-images with: docker-username: ${{ env.DOCKER_USERNAME }} docker-token: ${{ env.DOCKER_TOKEN }} - name: Build and push ARM64 id: build uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a with: context: ./web file: ./web/Dockerfile platforms: linux/arm64 labels: ${{ steps.meta.outputs.labels }} build-args: | ONYX_VERSION=${{ github.ref_name }} ${{ env.DHI_NODE_BUILD_ARGS }} NODE_OPTIONS=--max-old-space-size=8192 cache-from: | type=registry,ref=${{ env.REGISTRY_IMAGE }}:edge type=registry,ref=${{ env.REGISTRY_IMAGE }}:latest cache-to: type=inline outputs: type=image,name=${{ needs.determine-builds.outputs.is-test-run == 'true' && env.RUNS_ON_ECR_CACHE || env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true no-cache: ${{ vars.DOCKER_NO_CACHE == 'true' }} merge-web: needs: - determine-builds - build-web-amd64 - build-web-arm64 if: always() && needs.build-web-amd64.result == 'success' && needs.build-web-arm64.result == 'success' runs-on: - runs-on - runner=2cpu-linux-x64 - run-id=${{ github.run_id }}-merge-web - extras=ecr-cache timeout-minutes: 90 environment: release env: REGISTRY_IMAGE: onyxdotapp/onyx-web-server steps: - uses: runs-on/action@4e5f72399b6b17f2e79c511c1b38a315a64d22dc - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c with: role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }} aws-region: us-east-2 - name: Get AWS Secrets uses: aws-actions/aws-secretsmanager-get-secrets@2cb1a461cbd4865ac4299648312e4704c646cd53 with: secret-ids: | DOCKER_USERNAME, deploy/docker-username DOCKER_TOKEN, deploy/docker-token parse-json-secrets: true - name: Set up Docker Buildx uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # ratchet:docker/setup-buildx-action@v4 - name: Login to Docker Hub uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 with: username: ${{ env.DOCKER_USERNAME }} password: ${{ env.DOCKER_TOKEN }} - name: Docker meta id: meta uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 with: images: ${{ needs.determine-builds.outputs.is-test-run == 'true' && env.RUNS_ON_ECR_CACHE || env.REGISTRY_IMAGE }} flavor: | latest=false tags: | type=raw,value=${{ needs.determine-builds.outputs.is-test-run == 'true' && format('web-{0}', needs.determine-builds.outputs.sanitized-tag) || github.ref_name }} type=raw,value=${{ needs.determine-builds.outputs.is-test-run != 'true' && needs.determine-builds.outputs.is-latest == 'true' && 'latest' || '' }} type=raw,value=${{ needs.determine-builds.outputs.is-test-run != 'true' && env.EDGE_TAG == 'true' && 'edge' || '' }} type=raw,value=${{ needs.determine-builds.outputs.is-test-run != 'true' && needs.determine-builds.outputs.is-beta == 'true' && 'beta' || '' }} type=semver,pattern={{version}},enable=${{ needs.determine-builds.outputs.is-stable == 'true' }} type=semver,pattern={{major}}.{{minor}},enable=${{ needs.determine-builds.outputs.is-stable == 'true' }} type=semver,pattern={{major}},enable=${{ needs.determine-builds.outputs.is-stable == 'true' }} # Same tag set with a -dev suffix. Only the backend has a genuinely different dev image # (runtime + debugging tools); the web -dev tags point at the same manifest as their # plain counterparts so that one version string (IMAGE_TAG / helm global.version) selects # a whole deployment. The suffix is spelled out per tag rather than via flavor `suffix=` # so that disabled (empty) raw tags stay empty instead of degenerating to a bare "-dev". - name: Docker meta (dev) id: meta-dev uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 with: images: ${{ needs.determine-builds.outputs.is-test-run == 'true' && env.RUNS_ON_ECR_CACHE || env.REGISTRY_IMAGE }} flavor: | latest=false tags: | type=raw,value=${{ needs.determine-builds.outputs.is-test-run == 'true' && format('web-{0}-dev', needs.determine-builds.outputs.sanitized-tag) || format('{0}-dev', github.ref_name) }} type=raw,value=${{ needs.determine-builds.outputs.is-test-run != 'true' && needs.determine-builds.outputs.is-latest == 'true' && 'latest-dev' || '' }} type=raw,value=${{ needs.determine-builds.outputs.is-test-run != 'true' && env.EDGE_TAG == 'true' && 'edge-dev' || '' }} type=raw,value=${{ needs.determine-builds.outputs.is-test-run != 'true' && needs.determine-builds.outputs.is-beta == 'true' && 'beta-dev' || '' }} type=semver,pattern={{version}}-dev,enable=${{ needs.determine-builds.outputs.is-stable == 'true' }} type=semver,pattern={{major}}.{{minor}}-dev,enable=${{ needs.determine-builds.outputs.is-stable == 'true' }} type=semver,pattern={{major}}-dev,enable=${{ needs.determine-builds.outputs.is-stable == 'true' }} - name: Create and push manifest env: IMAGE_REPO: ${{ needs.determine-builds.outputs.is-test-run == 'true' && env.RUNS_ON_ECR_CACHE || env.REGISTRY_IMAGE }} AMD64_DIGEST: ${{ needs.build-web-amd64.outputs.digest }} ARM64_DIGEST: ${{ needs.build-web-arm64.outputs.digest }} META_TAGS: ${{ steps.meta.outputs.tags }} META_DEV_TAGS: ${{ steps.meta-dev.outputs.tags }} run: | IMAGES="${IMAGE_REPO}@${AMD64_DIGEST} ${IMAGE_REPO}@${ARM64_DIGEST}" docker buildx imagetools create \ $(printf '%s\n%s\n' "${META_TAGS}" "${META_DEV_TAGS}" | sed '/^$/d' | xargs -I {} echo -t {}) \ $IMAGES build-web-cloud-amd64: needs: - determine-builds - audit-gate if: needs.determine-builds.outputs.build-web-cloud == 'true' runs-on: - runs-on - runner=4cpu-linux-x64 - run-id=${{ github.run_id }}-web-cloud-amd64 - extras=ecr-cache timeout-minutes: 90 environment: release outputs: digest: ${{ steps.build.outputs.digest }} env: REGISTRY_IMAGE: onyxdotapp/onyx-web-server steps: - uses: runs-on/action@4e5f72399b6b17f2e79c511c1b38a315a64d22dc - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # ratchet:actions/checkout@v6 with: persist-credentials: false - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c with: role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }} aws-region: us-east-2 - name: Get AWS Secrets uses: aws-actions/aws-secretsmanager-get-secrets@2cb1a461cbd4865ac4299648312e4704c646cd53 with: secret-ids: | DOCKER_USERNAME, deploy/docker-username DOCKER_TOKEN, deploy/docker-token parse-json-secrets: true - name: Docker meta id: meta uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 with: images: ${{ needs.determine-builds.outputs.is-test-run == 'true' && env.RUNS_ON_ECR_CACHE || env.REGISTRY_IMAGE }} flavor: | latest=false - name: Set up Docker Buildx uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # ratchet:docker/setup-buildx-action@v4 - name: Login to Docker Hub uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 with: username: ${{ env.DOCKER_USERNAME }} password: ${{ env.DOCKER_TOKEN }} # web/Dockerfile defaults to the public Node bases. Release builds ship on the # hardened DHI equivalents, passed as build args below. - name: Resolve Docker Hardened Image bases uses: ./.github/actions/dhi-base-images with: docker-username: ${{ env.DOCKER_USERNAME }} docker-token: ${{ env.DOCKER_TOKEN }} - name: Build and push AMD64 id: build uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a with: context: ./web file: ./web/Dockerfile platforms: linux/amd64 labels: ${{ steps.meta.outputs.labels }} build-args: | ONYX_VERSION=${{ github.ref_name }} ${{ env.DHI_NODE_BUILD_ARGS }} NEXT_PUBLIC_CLOUD_ENABLED=true WEB_FRAME_PROTECTION_ENABLED=false NEXT_PUBLIC_POSTHOG_KEY=${{ secrets.POSTHOG_KEY }} NEXT_PUBLIC_POSTHOG_HOST=${{ secrets.POSTHOG_HOST }} NEXT_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }} NEXT_PUBLIC_RECAPTCHA_SITE_KEY=${{ vars.NEXT_PUBLIC_RECAPTCHA_SITE_KEY }} NEXT_PUBLIC_GTM_ENABLED=true NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED=true NEXT_PUBLIC_INCLUDE_ERROR_POPUP_SUPPORT_LINK=true NODE_OPTIONS=--max-old-space-size=8192 SENTRY_RELEASE=${{ github.sha }} secrets: | sentry_auth_token=${{ secrets.SENTRY_AUTH_TOKEN }} cache-from: type=registry,ref=${{ env.REGISTRY_IMAGE }}:latest cache-to: type=inline outputs: type=image,name=${{ needs.determine-builds.outputs.is-test-run == 'true' && env.RUNS_ON_ECR_CACHE || env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true no-cache: ${{ vars.DOCKER_NO_CACHE == 'true' }} build-web-cloud-arm64: needs: - determine-builds - audit-gate if: needs.determine-builds.outputs.build-web-cloud == 'true' runs-on: - runs-on - runner=4cpu-linux-arm64 - run-id=${{ github.run_id }}-web-cloud-arm64 - extras=ecr-cache timeout-minutes: 90 environment: release outputs: digest: ${{ steps.build.outputs.digest }} env: REGISTRY_IMAGE: onyxdotapp/onyx-web-server steps: - uses: runs-on/action@4e5f72399b6b17f2e79c511c1b38a315a64d22dc - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # ratchet:actions/checkout@v6 with: persist-credentials: false - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c with: role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }} aws-region: us-east-2 - name: Get AWS Secrets uses: aws-actions/aws-secretsmanager-get-secrets@2cb1a461cbd4865ac4299648312e4704c646cd53 with: secret-ids: | DOCKER_USERNAME, deploy/docker-username DOCKER_TOKEN, deploy/docker-token parse-json-secrets: true - name: Docker meta id: meta uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 with: images: ${{ needs.determine-builds.outputs.is-test-run == 'true' && env.RUNS_ON_ECR_CACHE || env.REGISTRY_IMAGE }} flavor: | latest=false - name: Set up Docker Buildx uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # ratchet:docker/setup-buildx-action@v4 - name: Login to Docker Hub uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 with: username: ${{ env.DOCKER_USERNAME }} password: ${{ env.DOCKER_TOKEN }} # web/Dockerfile defaults to the public Node bases. Release builds ship on the # hardened DHI equivalents, passed as build args below. - name: Resolve Docker Hardened Image bases uses: ./.github/actions/dhi-base-images with: docker-username: ${{ env.DOCKER_USERNAME }} docker-token: ${{ env.DOCKER_TOKEN }} - name: Build and push ARM64 id: build uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a with: context: ./web file: ./web/Dockerfile platforms: linux/arm64 labels: ${{ steps.meta.outputs.labels }} build-args: | ONYX_VERSION=${{ github.ref_name }} ${{ env.DHI_NODE_BUILD_ARGS }} NEXT_PUBLIC_CLOUD_ENABLED=true WEB_FRAME_PROTECTION_ENABLED=false NEXT_PUBLIC_POSTHOG_KEY=${{ secrets.POSTHOG_KEY }} NEXT_PUBLIC_POSTHOG_HOST=${{ secrets.POSTHOG_HOST }} NEXT_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }} NEXT_PUBLIC_RECAPTCHA_SITE_KEY=${{ vars.NEXT_PUBLIC_RECAPTCHA_SITE_KEY }} NEXT_PUBLIC_GTM_ENABLED=true NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED=true NEXT_PUBLIC_INCLUDE_ERROR_POPUP_SUPPORT_LINK=true NODE_OPTIONS=--max-old-space-size=8192 SENTRY_RELEASE=${{ github.sha }} secrets: | sentry_auth_token=${{ secrets.SENTRY_AUTH_TOKEN }} cache-from: type=registry,ref=${{ env.REGISTRY_IMAGE }}:latest cache-to: type=inline outputs: type=image,name=${{ needs.determine-builds.outputs.is-test-run == 'true' && env.RUNS_ON_ECR_CACHE || env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true no-cache: ${{ vars.DOCKER_NO_CACHE == 'true' }} merge-web-cloud: needs: - determine-builds - build-web-cloud-amd64 - build-web-cloud-arm64 if: always() && needs.build-web-cloud-amd64.result == 'success' && needs.build-web-cloud-arm64.result == 'success' runs-on: - runs-on - runner=2cpu-linux-x64 - run-id=${{ github.run_id }}-merge-web-cloud - extras=ecr-cache timeout-minutes: 90 environment: release env: REGISTRY_IMAGE: onyxdotapp/onyx-web-server steps: - uses: runs-on/action@4e5f72399b6b17f2e79c511c1b38a315a64d22dc - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c with: role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }} aws-region: us-east-2 - name: Get AWS Secrets uses: aws-actions/aws-secretsmanager-get-secrets@2cb1a461cbd4865ac4299648312e4704c646cd53 with: secret-ids: | DOCKER_USERNAME, deploy/docker-username DOCKER_TOKEN, deploy/docker-token parse-json-secrets: true - name: Set up Docker Buildx uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # ratchet:docker/setup-buildx-action@v4 - name: Login to Docker Hub uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 with: username: ${{ env.DOCKER_USERNAME }} password: ${{ env.DOCKER_TOKEN }} - name: Docker meta id: meta uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 with: images: ${{ needs.determine-builds.outputs.is-test-run == 'true' && env.RUNS_ON_ECR_CACHE || env.REGISTRY_IMAGE }} flavor: | latest=false tags: | type=raw,value=${{ needs.determine-builds.outputs.is-test-run == 'true' && format('web-cloud-{0}', needs.determine-builds.outputs.sanitized-tag) || github.ref_name }} # -dev twin of the tag above, pointing at the same manifest — see the merge-web dev note. # Cloud tags build the web image here rather than in merge-web, so without this a cloud # deployment couldn't set a single -dev version across all of its images. - name: Docker meta (dev) id: meta-dev uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 with: images: ${{ needs.determine-builds.outputs.is-test-run == 'true' && env.RUNS_ON_ECR_CACHE || env.REGISTRY_IMAGE }} flavor: | latest=false tags: | type=raw,value=${{ needs.determine-builds.outputs.is-test-run == 'true' && format('web-cloud-{0}-dev', needs.determine-builds.outputs.sanitized-tag) || format('{0}-dev', github.ref_name) }} - name: Create and push manifest env: IMAGE_REPO: ${{ needs.determine-builds.outputs.is-test-run == 'true' && env.RUNS_ON_ECR_CACHE || env.REGISTRY_IMAGE }} AMD64_DIGEST: ${{ needs.build-web-cloud-amd64.outputs.digest }} ARM64_DIGEST: ${{ needs.build-web-cloud-arm64.outputs.digest }} META_TAGS: ${{ steps.meta.outputs.tags }} META_DEV_TAGS: ${{ steps.meta-dev.outputs.tags }} run: | IMAGES="${IMAGE_REPO}@${AMD64_DIGEST} ${IMAGE_REPO}@${ARM64_DIGEST}" docker buildx imagetools create \ $(printf '%s\n%s\n' "${META_TAGS}" "${META_DEV_TAGS}" | sed '/^$/d' | xargs -I {} echo -t {}) \ $IMAGES build-backend-amd64: needs: - determine-builds - audit-gate if: needs.determine-builds.outputs.build-backend == 'true' runs-on: - runs-on - runner=2cpu-linux-x64 - run-id=${{ github.run_id }}-backend-amd64 - extras=ecr-cache timeout-minutes: 90 environment: release outputs: digest: ${{ steps.build.outputs.digest }} digest-dev: ${{ steps.build-dev.outputs.digest }} env: REGISTRY_IMAGE: onyxdotapp/onyx-backend steps: - uses: runs-on/action@4e5f72399b6b17f2e79c511c1b38a315a64d22dc - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # ratchet:actions/checkout@v6 with: persist-credentials: false - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c with: role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }} aws-region: us-east-2 - name: Get AWS Secrets uses: aws-actions/aws-secretsmanager-get-secrets@2cb1a461cbd4865ac4299648312e4704c646cd53 with: secret-ids: | DOCKER_USERNAME, deploy/docker-username DOCKER_TOKEN, deploy/docker-token parse-json-secrets: true - name: Docker meta id: meta uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 with: images: ${{ needs.determine-builds.outputs.is-test-run == 'true' && env.RUNS_ON_ECR_CACHE || env.REGISTRY_IMAGE }} flavor: | latest=false - name: Set up Docker Buildx uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # ratchet:docker/setup-buildx-action@v4 - name: Login to Docker Hub uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 with: username: ${{ env.DOCKER_USERNAME }} password: ${{ env.DOCKER_TOKEN }} - name: Build and push AMD64 id: build uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a with: context: ./backend file: ./backend/Dockerfile target: runtime platforms: linux/amd64 labels: ${{ steps.meta.outputs.labels }} build-args: | ONYX_VERSION=${{ github.ref_name }} cache-from: | type=registry,ref=${{ env.REGISTRY_IMAGE }}:edge type=registry,ref=${{ env.REGISTRY_IMAGE }}:latest cache-to: type=inline outputs: type=image,name=${{ needs.determine-builds.outputs.is-test-run == 'true' && env.RUNS_ON_ECR_CACHE || env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true no-cache: ${{ vars.DOCKER_NO_CACHE == 'true' }} # Dev variant: runtime + interactive debugging tools, published with a -dev tag suffix by # merge-backend. Reuses the runtime stage from the build above via the local BuildKit # cache, so this only builds and pushes the extra apt layer. - name: Build and push AMD64 dev id: build-dev uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a with: context: ./backend file: ./backend/Dockerfile target: dev platforms: linux/amd64 labels: ${{ steps.meta.outputs.labels }} build-args: | ONYX_VERSION=${{ github.ref_name }} cache-from: | type=registry,ref=${{ env.REGISTRY_IMAGE }}:edge-dev type=registry,ref=${{ env.REGISTRY_IMAGE }}:edge type=registry,ref=${{ env.REGISTRY_IMAGE }}:latest cache-to: type=inline outputs: type=image,name=${{ needs.determine-builds.outputs.is-test-run == 'true' && env.RUNS_ON_ECR_CACHE || env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true no-cache: ${{ vars.DOCKER_NO_CACHE == 'true' }} build-backend-arm64: needs: - determine-builds - audit-gate if: needs.determine-builds.outputs.build-backend == 'true' runs-on: - runs-on - runner=2cpu-linux-arm64 - run-id=${{ github.run_id }}-backend-arm64 - extras=ecr-cache timeout-minutes: 90 environment: release outputs: digest: ${{ steps.build.outputs.digest }} digest-dev: ${{ steps.build-dev.outputs.digest }} env: REGISTRY_IMAGE: onyxdotapp/onyx-backend steps: - uses: runs-on/action@4e5f72399b6b17f2e79c511c1b38a315a64d22dc - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # ratchet:actions/checkout@v6 with: persist-credentials: false - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c with: role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }} aws-region: us-east-2 - name: Get AWS Secrets uses: aws-actions/aws-secretsmanager-get-secrets@2cb1a461cbd4865ac4299648312e4704c646cd53 with: secret-ids: | DOCKER_USERNAME, deploy/docker-username DOCKER_TOKEN, deploy/docker-token parse-json-secrets: true - name: Docker meta id: meta uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 with: images: ${{ needs.determine-builds.outputs.is-test-run == 'true' && env.RUNS_ON_ECR_CACHE || env.REGISTRY_IMAGE }} flavor: | latest=false - name: Set up Docker Buildx uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # ratchet:docker/setup-buildx-action@v4 - name: Login to Docker Hub uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 with: username: ${{ env.DOCKER_USERNAME }} password: ${{ env.DOCKER_TOKEN }} - name: Build and push ARM64 id: build uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a with: context: ./backend file: ./backend/Dockerfile target: runtime platforms: linux/arm64 labels: ${{ steps.meta.outputs.labels }} build-args: | ONYX_VERSION=${{ github.ref_name }} cache-from: | type=registry,ref=${{ env.REGISTRY_IMAGE }}:edge type=registry,ref=${{ env.REGISTRY_IMAGE }}:latest cache-to: type=inline outputs: type=image,name=${{ needs.determine-builds.outputs.is-test-run == 'true' && env.RUNS_ON_ECR_CACHE || env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true no-cache: ${{ vars.DOCKER_NO_CACHE == 'true' }} # Dev variant: runtime + interactive debugging tools, published with a -dev tag suffix by # merge-backend. Reuses the runtime stage from the build above via the local BuildKit # cache, so this only builds and pushes the extra apt layer. - name: Build and push ARM64 dev id: build-dev uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a with: context: ./backend file: ./backend/Dockerfile target: dev platforms: linux/arm64 labels: ${{ steps.meta.outputs.labels }} build-args: | ONYX_VERSION=${{ github.ref_name }} cache-from: | type=registry,ref=${{ env.REGISTRY_IMAGE }}:edge-dev type=registry,ref=${{ env.REGISTRY_IMAGE }}:edge type=registry,ref=${{ env.REGISTRY_IMAGE }}:latest cache-to: type=inline outputs: type=image,name=${{ needs.determine-builds.outputs.is-test-run == 'true' && env.RUNS_ON_ECR_CACHE || env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true no-cache: ${{ vars.DOCKER_NO_CACHE == 'true' }} merge-backend: needs: - determine-builds - build-backend-amd64 - build-backend-arm64 if: always() && needs.build-backend-amd64.result == 'success' && needs.build-backend-arm64.result == 'success' runs-on: - runs-on - runner=2cpu-linux-x64 - run-id=${{ github.run_id }}-merge-backend - extras=ecr-cache timeout-minutes: 90 environment: release env: REGISTRY_IMAGE: onyxdotapp/onyx-backend steps: - uses: runs-on/action@4e5f72399b6b17f2e79c511c1b38a315a64d22dc - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c with: role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }} aws-region: us-east-2 - name: Get AWS Secrets uses: aws-actions/aws-secretsmanager-get-secrets@2cb1a461cbd4865ac4299648312e4704c646cd53 with: secret-ids: | DOCKER_USERNAME, deploy/docker-username DOCKER_TOKEN, deploy/docker-token parse-json-secrets: true - name: Set up Docker Buildx uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # ratchet:docker/setup-buildx-action@v4 - name: Login to Docker Hub uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 with: username: ${{ env.DOCKER_USERNAME }} password: ${{ env.DOCKER_TOKEN }} - name: Docker meta id: meta uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 with: images: ${{ needs.determine-builds.outputs.is-test-run == 'true' && env.RUNS_ON_ECR_CACHE || env.REGISTRY_IMAGE }} flavor: | latest=false tags: | type=raw,value=${{ needs.determine-builds.outputs.is-test-run == 'true' && format('backend-{0}', needs.determine-builds.outputs.sanitized-tag) || github.ref_name }} type=raw,value=${{ needs.determine-builds.outputs.is-test-run != 'true' && needs.determine-builds.outputs.is-latest == 'true' && 'latest' || '' }} type=raw,value=${{ needs.determine-builds.outputs.is-test-run != 'true' && env.EDGE_TAG == 'true' && 'edge' || '' }} type=raw,value=${{ needs.determine-builds.outputs.is-test-run != 'true' && needs.determine-builds.outputs.is-beta-standalone == 'true' && 'beta' || '' }} type=semver,pattern={{version}},enable=${{ needs.determine-builds.outputs.is-stable == 'true' }} type=semver,pattern={{major}}.{{minor}},enable=${{ needs.determine-builds.outputs.is-stable == 'true' }} type=semver,pattern={{major}},enable=${{ needs.determine-builds.outputs.is-stable == 'true' }} # Same tag set with a -dev suffix, for the dev image variant (runtime + debugging tools). # The suffix is spelled out per tag rather than via flavor `suffix=` so that disabled # (empty) raw tags stay empty instead of degenerating to a bare "-dev" tag. - name: Docker meta (dev) id: meta-dev uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 with: images: ${{ needs.determine-builds.outputs.is-test-run == 'true' && env.RUNS_ON_ECR_CACHE || env.REGISTRY_IMAGE }} flavor: | latest=false tags: | type=raw,value=${{ needs.determine-builds.outputs.is-test-run == 'true' && format('backend-{0}-dev', needs.determine-builds.outputs.sanitized-tag) || format('{0}-dev', github.ref_name) }} type=raw,value=${{ needs.determine-builds.outputs.is-test-run != 'true' && needs.determine-builds.outputs.is-latest == 'true' && 'latest-dev' || '' }} type=raw,value=${{ needs.determine-builds.outputs.is-test-run != 'true' && env.EDGE_TAG == 'true' && 'edge-dev' || '' }} type=raw,value=${{ needs.determine-builds.outputs.is-test-run != 'true' && needs.determine-builds.outputs.is-beta-standalone == 'true' && 'beta-dev' || '' }} type=semver,pattern={{version}}-dev,enable=${{ needs.determine-builds.outputs.is-stable == 'true' }} type=semver,pattern={{major}}.{{minor}}-dev,enable=${{ needs.determine-builds.outputs.is-stable == 'true' }} type=semver,pattern={{major}}-dev,enable=${{ needs.determine-builds.outputs.is-stable == 'true' }} - name: Create and push manifest env: IMAGE_REPO: ${{ needs.determine-builds.outputs.is-test-run == 'true' && env.RUNS_ON_ECR_CACHE || env.REGISTRY_IMAGE }} AMD64_DIGEST: ${{ needs.build-backend-amd64.outputs.digest }} ARM64_DIGEST: ${{ needs.build-backend-arm64.outputs.digest }} META_TAGS: ${{ steps.meta.outputs.tags }} run: | IMAGES="${IMAGE_REPO}@${AMD64_DIGEST} ${IMAGE_REPO}@${ARM64_DIGEST}" docker buildx imagetools create \ $(printf '%s\n' "${META_TAGS}" | xargs -I {} echo -t {}) \ $IMAGES - name: Create and push dev manifest env: IMAGE_REPO: ${{ needs.determine-builds.outputs.is-test-run == 'true' && env.RUNS_ON_ECR_CACHE || env.REGISTRY_IMAGE }} AMD64_DIGEST: ${{ needs.build-backend-amd64.outputs.digest-dev }} ARM64_DIGEST: ${{ needs.build-backend-arm64.outputs.digest-dev }} META_TAGS: ${{ steps.meta-dev.outputs.tags }} run: | IMAGES="${IMAGE_REPO}@${AMD64_DIGEST} ${IMAGE_REPO}@${ARM64_DIGEST}" docker buildx imagetools create \ $(printf '%s\n' "${META_TAGS}" | xargs -I {} echo -t {}) \ $IMAGES build-model-server-amd64: needs: - determine-builds - audit-gate if: needs.determine-builds.outputs.build-model-server == 'true' runs-on: - runs-on - runner=2cpu-linux-x64 - run-id=${{ github.run_id }}-model-server-amd64 # Spot can place this job on an instance with no local NVMe, which leaves docker on the # root volume. The ~7gb amd64 venv, copied again by the final stage, needs the headroom. - volume=60gb - extras=ecr-cache timeout-minutes: 90 environment: release outputs: digest: ${{ steps.build.outputs.digest }} env: REGISTRY_IMAGE: onyxdotapp/onyx-model-server steps: - uses: runs-on/action@4e5f72399b6b17f2e79c511c1b38a315a64d22dc - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # ratchet:actions/checkout@v6 with: persist-credentials: false - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c with: role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }} aws-region: us-east-2 - name: Get AWS Secrets uses: aws-actions/aws-secretsmanager-get-secrets@2cb1a461cbd4865ac4299648312e4704c646cd53 with: secret-ids: | DOCKER_USERNAME, deploy/docker-username DOCKER_TOKEN, deploy/docker-token parse-json-secrets: true - name: Docker meta id: meta uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 with: images: ${{ needs.determine-builds.outputs.is-test-run == 'true' && env.RUNS_ON_ECR_CACHE || env.REGISTRY_IMAGE }} flavor: | latest=false - name: Set up Docker Buildx uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # ratchet:docker/setup-buildx-action@v4 with: buildkitd-flags: ${{ vars.DOCKER_DEBUG == 'true' && '--debug' || '' }} - name: Login to Docker Hub uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 with: username: ${{ env.DOCKER_USERNAME }} password: ${{ env.DOCKER_TOKEN }} # Dockerfile.model_server defaults to the public Python bases. Release builds ship # on the hardened DHI equivalents, passed as build args below. - name: Resolve Docker Hardened Image bases uses: ./.github/actions/dhi-base-images with: docker-username: ${{ env.DOCKER_USERNAME }} docker-token: ${{ env.DOCKER_TOKEN }} - name: Build and push AMD64 id: build uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a env: DEBUG: ${{ vars.DOCKER_DEBUG == 'true' && 1 || 0 }} with: context: ./backend file: ./backend/Dockerfile.model_server platforms: linux/amd64 labels: ${{ steps.meta.outputs.labels }} build-args: | ONYX_VERSION=${{ github.ref_name }} ${{ env.DHI_PYTHON_BUILD_ARGS }} cache-from: | type=registry,ref=${{ env.REGISTRY_IMAGE }}:edge type=registry,ref=${{ env.REGISTRY_IMAGE }}:latest cache-to: type=inline outputs: type=image,name=${{ needs.determine-builds.outputs.is-test-run == 'true' && env.RUNS_ON_ECR_CACHE || env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true no-cache: ${{ env.EDGE_TAG != 'true' && vars.MODEL_SERVER_NO_CACHE == 'true' }} build-model-server-arm64: needs: - determine-builds - audit-gate if: needs.determine-builds.outputs.build-model-server == 'true' runs-on: - runs-on - runner=2cpu-linux-arm64 - run-id=${{ github.run_id }}-model-server-arm64 - volume=60gb - extras=ecr-cache timeout-minutes: 90 environment: release outputs: digest: ${{ steps.build.outputs.digest }} env: REGISTRY_IMAGE: onyxdotapp/onyx-model-server steps: - uses: runs-on/action@4e5f72399b6b17f2e79c511c1b38a315a64d22dc - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # ratchet:actions/checkout@v6 with: persist-credentials: false - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c with: role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }} aws-region: us-east-2 - name: Get AWS Secrets uses: aws-actions/aws-secretsmanager-get-secrets@2cb1a461cbd4865ac4299648312e4704c646cd53 with: secret-ids: | DOCKER_USERNAME, deploy/docker-username DOCKER_TOKEN, deploy/docker-token parse-json-secrets: true - name: Docker meta id: meta uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 with: images: ${{ needs.determine-builds.outputs.is-test-run == 'true' && env.RUNS_ON_ECR_CACHE || env.REGISTRY_IMAGE }} flavor: | latest=false - name: Set up Docker Buildx uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # ratchet:docker/setup-buildx-action@v4 with: buildkitd-flags: ${{ vars.DOCKER_DEBUG == 'true' && '--debug' || '' }} - name: Login to Docker Hub uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 with: username: ${{ env.DOCKER_USERNAME }} password: ${{ env.DOCKER_TOKEN }} # Dockerfile.model_server defaults to the public Python bases. Release builds ship # on the hardened DHI equivalents, passed as build args below. - name: Resolve Docker Hardened Image bases uses: ./.github/actions/dhi-base-images with: docker-username: ${{ env.DOCKER_USERNAME }} docker-token: ${{ env.DOCKER_TOKEN }} - name: Build and push ARM64 id: build uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a env: DEBUG: ${{ vars.DOCKER_DEBUG == 'true' && 1 || 0 }} with: context: ./backend file: ./backend/Dockerfile.model_server platforms: linux/arm64 labels: ${{ steps.meta.outputs.labels }} build-args: | ONYX_VERSION=${{ github.ref_name }} ${{ env.DHI_PYTHON_BUILD_ARGS }} cache-from: | type=registry,ref=${{ env.REGISTRY_IMAGE }}:edge type=registry,ref=${{ env.REGISTRY_IMAGE }}:latest cache-to: type=inline outputs: type=image,name=${{ needs.determine-builds.outputs.is-test-run == 'true' && env.RUNS_ON_ECR_CACHE || env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true no-cache: ${{ env.EDGE_TAG != 'true' && vars.MODEL_SERVER_NO_CACHE == 'true' }} merge-model-server: needs: - determine-builds - build-model-server-amd64 - build-model-server-arm64 if: always() && needs.build-model-server-amd64.result == 'success' && needs.build-model-server-arm64.result == 'success' runs-on: - runs-on - runner=2cpu-linux-x64 - run-id=${{ github.run_id }}-merge-model-server - extras=ecr-cache timeout-minutes: 90 environment: release env: REGISTRY_IMAGE: onyxdotapp/onyx-model-server steps: - uses: runs-on/action@4e5f72399b6b17f2e79c511c1b38a315a64d22dc - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c with: role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }} aws-region: us-east-2 - name: Get AWS Secrets uses: aws-actions/aws-secretsmanager-get-secrets@2cb1a461cbd4865ac4299648312e4704c646cd53 with: secret-ids: | DOCKER_USERNAME, deploy/docker-username DOCKER_TOKEN, deploy/docker-token parse-json-secrets: true - name: Set up Docker Buildx uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # ratchet:docker/setup-buildx-action@v4 - name: Login to Docker Hub uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 with: username: ${{ env.DOCKER_USERNAME }} password: ${{ env.DOCKER_TOKEN }} - name: Docker meta id: meta uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 with: images: ${{ needs.determine-builds.outputs.is-test-run == 'true' && env.RUNS_ON_ECR_CACHE || env.REGISTRY_IMAGE }} flavor: | latest=false tags: | type=raw,value=${{ needs.determine-builds.outputs.is-test-run == 'true' && format('model-server-{0}', needs.determine-builds.outputs.sanitized-tag) || github.ref_name }} type=raw,value=${{ needs.determine-builds.outputs.is-test-run != 'true' && needs.determine-builds.outputs.is-latest == 'true' && 'latest' || '' }} type=raw,value=${{ needs.determine-builds.outputs.is-test-run != 'true' && env.EDGE_TAG == 'true' && 'edge' || '' }} type=raw,value=${{ needs.determine-builds.outputs.is-test-run != 'true' && needs.determine-builds.outputs.is-beta-standalone == 'true' && 'beta' || '' }} type=semver,pattern={{version}},enable=${{ needs.determine-builds.outputs.is-stable == 'true' }} type=semver,pattern={{major}}.{{minor}},enable=${{ needs.determine-builds.outputs.is-stable == 'true' }} type=semver,pattern={{major}},enable=${{ needs.determine-builds.outputs.is-stable == 'true' }} # -dev twins of the tags above, pointing at the same manifest — see the merge-web dev note. - name: Docker meta (dev) id: meta-dev uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 with: images: ${{ needs.determine-builds.outputs.is-test-run == 'true' && env.RUNS_ON_ECR_CACHE || env.REGISTRY_IMAGE }} flavor: | latest=false tags: | type=raw,value=${{ needs.determine-builds.outputs.is-test-run == 'true' && format('model-server-{0}-dev', needs.determine-builds.outputs.sanitized-tag) || format('{0}-dev', github.ref_name) }} type=raw,value=${{ needs.determine-builds.outputs.is-test-run != 'true' && needs.determine-builds.outputs.is-latest == 'true' && 'latest-dev' || '' }} type=raw,value=${{ needs.determine-builds.outputs.is-test-run != 'true' && env.EDGE_TAG == 'true' && 'edge-dev' || '' }} type=raw,value=${{ needs.determine-builds.outputs.is-test-run != 'true' && needs.determine-builds.outputs.is-beta-standalone == 'true' && 'beta-dev' || '' }} type=semver,pattern={{version}}-dev,enable=${{ needs.determine-builds.outputs.is-stable == 'true' }} type=semver,pattern={{major}}.{{minor}}-dev,enable=${{ needs.determine-builds.outputs.is-stable == 'true' }} type=semver,pattern={{major}}-dev,enable=${{ needs.determine-builds.outputs.is-stable == 'true' }} - name: Create and push manifest env: IMAGE_REPO: ${{ needs.determine-builds.outputs.is-test-run == 'true' && env.RUNS_ON_ECR_CACHE || env.REGISTRY_IMAGE }} AMD64_DIGEST: ${{ needs.build-model-server-amd64.outputs.digest }} ARM64_DIGEST: ${{ needs.build-model-server-arm64.outputs.digest }} META_TAGS: ${{ steps.meta.outputs.tags }} META_DEV_TAGS: ${{ steps.meta-dev.outputs.tags }} run: | IMAGES="${IMAGE_REPO}@${AMD64_DIGEST} ${IMAGE_REPO}@${ARM64_DIGEST}" docker buildx imagetools create \ $(printf '%s\n%s\n' "${META_TAGS}" "${META_DEV_TAGS}" | sed '/^$/d' | xargs -I {} echo -t {}) \ $IMAGES prepare-sandbox: needs: - determine-builds - audit-gate if: needs.determine-builds.outputs.is-test-run == 'true' || (needs.determine-builds.outputs.is-stable == 'true' || needs.determine-builds.outputs.is-beta-standalone == 'true' || needs.determine-builds.outputs.is-cloud-tag == 'true' || startsWith(github.ref_name, 'nightly-latest') || github.ref_name == 'edge') runs-on: ubuntu-slim timeout-minutes: 10 environment: release permissions: contents: read id-token: write outputs: context-tag: ${{ steps.context.outputs.context-tag }} context-exists: ${{ steps.exists.outputs.context-exists }} env: REGISTRY_IMAGE: onyxdotapp/sandbox SANDBOX_CONTEXT: backend/onyx/server/features/build/sandbox/image steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # ratchet:actions/checkout@v6 with: persist-credentials: false - name: Compute sandbox context tag id: context run: | set -euo pipefail digest="$(git ls-files -s -z "$SANDBOX_CONTEXT" | sort -z | sha256sum | cut -d' ' -f1)" echo "context-tag=ctx-${digest:0:20}" >> "$GITHUB_OUTPUT" - name: Configure AWS credentials if: needs.determine-builds.outputs.is-test-run != 'true' && vars.DOCKER_NO_CACHE != 'true' uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c with: role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }} aws-region: us-east-2 - name: Get AWS Secrets if: needs.determine-builds.outputs.is-test-run != 'true' && vars.DOCKER_NO_CACHE != 'true' uses: aws-actions/aws-secretsmanager-get-secrets@2cb1a461cbd4865ac4299648312e4704c646cd53 with: secret-ids: | DOCKER_USERNAME, deploy/docker-username DOCKER_TOKEN, deploy/docker-token parse-json-secrets: true - name: Login to Docker Hub if: needs.determine-builds.outputs.is-test-run != 'true' && vars.DOCKER_NO_CACHE != 'true' uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 with: username: ${{ env.DOCKER_USERNAME }} password: ${{ env.DOCKER_TOKEN }} - name: Check for existing context image id: exists env: CONTEXT_TAG: ${{ steps.context.outputs.context-tag }} DOCKER_NO_CACHE: ${{ vars.DOCKER_NO_CACHE == 'true' && 'true' || 'false' }} IS_TEST_RUN: ${{ needs.determine-builds.outputs.is-test-run }} run: | if [ "${IS_TEST_RUN}" = "true" ] || [ "${DOCKER_NO_CACHE}" = "true" ]; then echo "context-exists=false" >> "$GITHUB_OUTPUT" exit 0 fi if docker buildx imagetools inspect "${REGISTRY_IMAGE}:${CONTEXT_TAG}" >/dev/null 2>&1; then echo "context-exists=true" >> "$GITHUB_OUTPUT" else echo "context-exists=false" >> "$GITHUB_OUTPUT" fi build-sandbox-amd64: needs: - determine-builds - prepare-sandbox if: needs.prepare-sandbox.result == 'success' && needs.prepare-sandbox.outputs.context-exists != 'true' runs-on: - runs-on - runner=4cpu-linux-x64 - run-id=${{ github.run_id }}-sandbox-amd64 - extras=ecr-cache timeout-minutes: 90 environment: release permissions: contents: read id-token: write outputs: digest: ${{ steps.build.outputs.digest }} env: REGISTRY_IMAGE: onyxdotapp/sandbox steps: - uses: runs-on/action@4e5f72399b6b17f2e79c511c1b38a315a64d22dc - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # ratchet:actions/checkout@v6 with: persist-credentials: false - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c with: role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }} aws-region: us-east-2 - name: Get AWS Secrets uses: aws-actions/aws-secretsmanager-get-secrets@2cb1a461cbd4865ac4299648312e4704c646cd53 with: secret-ids: | DOCKER_USERNAME, deploy/docker-username DOCKER_TOKEN, deploy/docker-token parse-json-secrets: true - name: Docker meta id: meta uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 with: images: ${{ needs.determine-builds.outputs.is-test-run == 'true' && env.RUNS_ON_ECR_CACHE || env.REGISTRY_IMAGE }} flavor: | latest=false - name: Set up Docker Buildx uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # ratchet:docker/setup-buildx-action@v4 - name: Login to Docker Hub uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 with: username: ${{ env.DOCKER_USERNAME }} password: ${{ env.DOCKER_TOKEN }} - name: Build and push AMD64 id: build uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a with: context: ./backend/onyx/server/features/build/sandbox/image file: ./backend/onyx/server/features/build/sandbox/image/Dockerfile platforms: linux/amd64 labels: ${{ steps.meta.outputs.labels }} cache-from: | type=registry,ref=${{ env.BASE_IMAGE_REGISTRY }}/onyxdotapp/sandbox:edge type=registry,ref=${{ env.BASE_IMAGE_REGISTRY }}/onyxdotapp/sandbox:latest cache-to: type=inline outputs: type=image,name=${{ needs.determine-builds.outputs.is-test-run == 'true' && env.RUNS_ON_ECR_CACHE || env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true no-cache: ${{ vars.DOCKER_NO_CACHE == 'true' }} build-sandbox-arm64: needs: - determine-builds - prepare-sandbox if: needs.prepare-sandbox.result == 'success' && needs.prepare-sandbox.outputs.context-exists != 'true' runs-on: - runs-on - runner=4cpu-linux-arm64 - run-id=${{ github.run_id }}-sandbox-arm64 - extras=ecr-cache timeout-minutes: 90 environment: release permissions: contents: read id-token: write outputs: digest: ${{ steps.build.outputs.digest }} env: REGISTRY_IMAGE: onyxdotapp/sandbox steps: - uses: runs-on/action@4e5f72399b6b17f2e79c511c1b38a315a64d22dc - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # ratchet:actions/checkout@v6 with: persist-credentials: false - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c with: role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }} aws-region: us-east-2 - name: Get AWS Secrets uses: aws-actions/aws-secretsmanager-get-secrets@2cb1a461cbd4865ac4299648312e4704c646cd53 with: secret-ids: | DOCKER_USERNAME, deploy/docker-username DOCKER_TOKEN, deploy/docker-token parse-json-secrets: true - name: Docker meta id: meta uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 with: images: ${{ needs.determine-builds.outputs.is-test-run == 'true' && env.RUNS_ON_ECR_CACHE || env.REGISTRY_IMAGE }} flavor: | latest=false - name: Set up Docker Buildx uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # ratchet:docker/setup-buildx-action@v4 - name: Login to Docker Hub uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 with: username: ${{ env.DOCKER_USERNAME }} password: ${{ env.DOCKER_TOKEN }} - name: Build and push ARM64 id: build uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a with: context: ./backend/onyx/server/features/build/sandbox/image file: ./backend/onyx/server/features/build/sandbox/image/Dockerfile platforms: linux/arm64 labels: ${{ steps.meta.outputs.labels }} cache-from: | type=registry,ref=${{ env.BASE_IMAGE_REGISTRY }}/onyxdotapp/sandbox:edge type=registry,ref=${{ env.BASE_IMAGE_REGISTRY }}/onyxdotapp/sandbox:latest cache-to: type=inline outputs: type=image,name=${{ needs.determine-builds.outputs.is-test-run == 'true' && env.RUNS_ON_ECR_CACHE || env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true no-cache: ${{ vars.DOCKER_NO_CACHE == 'true' }} merge-sandbox: needs: - determine-builds - prepare-sandbox - build-sandbox-amd64 - build-sandbox-arm64 if: always() && needs.prepare-sandbox.result == 'success' && (needs.prepare-sandbox.outputs.context-exists == 'true' || (needs.build-sandbox-amd64.result == 'success' && needs.build-sandbox-arm64.result == 'success')) runs-on: - runs-on - runner=2cpu-linux-x64 - run-id=${{ github.run_id }}-merge-sandbox - extras=ecr-cache timeout-minutes: 90 environment: release permissions: id-token: write env: REGISTRY_IMAGE: onyxdotapp/sandbox steps: - uses: runs-on/action@4e5f72399b6b17f2e79c511c1b38a315a64d22dc - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c with: role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }} aws-region: us-east-2 - name: Get AWS Secrets uses: aws-actions/aws-secretsmanager-get-secrets@2cb1a461cbd4865ac4299648312e4704c646cd53 with: secret-ids: | DOCKER_USERNAME, deploy/docker-username DOCKER_TOKEN, deploy/docker-token parse-json-secrets: true - name: Set up Docker Buildx uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # ratchet:docker/setup-buildx-action@v4 - name: Login to Docker Hub uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 with: username: ${{ env.DOCKER_USERNAME }} password: ${{ env.DOCKER_TOKEN }} - name: Docker meta id: meta uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 with: images: ${{ needs.determine-builds.outputs.is-test-run == 'true' && env.RUNS_ON_ECR_CACHE || env.REGISTRY_IMAGE }} flavor: | latest=false tags: | type=raw,value=${{ needs.determine-builds.outputs.is-test-run == 'true' && format('sandbox-{0}', needs.determine-builds.outputs.sanitized-tag) || github.ref_name }} type=raw,value=${{ needs.determine-builds.outputs.is-test-run != 'true' && needs.determine-builds.outputs.is-latest == 'true' && 'latest' || '' }} type=raw,value=${{ needs.determine-builds.outputs.is-test-run != 'true' && env.EDGE_TAG == 'true' && 'edge' || '' }} type=raw,value=${{ needs.determine-builds.outputs.is-test-run != 'true' && needs.determine-builds.outputs.is-beta-standalone == 'true' && 'beta' || '' }} type=semver,pattern={{version}},enable=${{ needs.determine-builds.outputs.is-stable == 'true' }} type=semver,pattern={{major}}.{{minor}},enable=${{ needs.determine-builds.outputs.is-stable == 'true' }} type=semver,pattern={{major}},enable=${{ needs.determine-builds.outputs.is-stable == 'true' }} # -dev twins of the tags above, pointing at the same manifest — see the merge-web dev note. # The sandbox image resolves off the same version knob as the rest of the deployment # (IMAGE_TAG in compose, global.version in the helm chart), so it needs the twins too. - name: Docker meta (dev) id: meta-dev uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 with: images: ${{ needs.determine-builds.outputs.is-test-run == 'true' && env.RUNS_ON_ECR_CACHE || env.REGISTRY_IMAGE }} flavor: | latest=false tags: | type=raw,value=${{ needs.determine-builds.outputs.is-test-run == 'true' && format('sandbox-{0}-dev', needs.determine-builds.outputs.sanitized-tag) || format('{0}-dev', github.ref_name) }} type=raw,value=${{ needs.determine-builds.outputs.is-test-run != 'true' && needs.determine-builds.outputs.is-latest == 'true' && 'latest-dev' || '' }} type=raw,value=${{ needs.determine-builds.outputs.is-test-run != 'true' && env.EDGE_TAG == 'true' && 'edge-dev' || '' }} type=raw,value=${{ needs.determine-builds.outputs.is-test-run != 'true' && needs.determine-builds.outputs.is-beta-standalone == 'true' && 'beta-dev' || '' }} type=semver,pattern={{version}}-dev,enable=${{ needs.determine-builds.outputs.is-stable == 'true' }} type=semver,pattern={{major}}.{{minor}}-dev,enable=${{ needs.determine-builds.outputs.is-stable == 'true' }} type=semver,pattern={{major}}-dev,enable=${{ needs.determine-builds.outputs.is-stable == 'true' }} - name: Create and push manifest env: IMAGE_REPO: ${{ needs.determine-builds.outputs.is-test-run == 'true' && env.RUNS_ON_ECR_CACHE || env.REGISTRY_IMAGE }} CONTEXT_TAG: ${{ needs.prepare-sandbox.outputs.context-tag }} AMD64_DIGEST: ${{ needs.build-sandbox-amd64.outputs.digest }} ARM64_DIGEST: ${{ needs.build-sandbox-arm64.outputs.digest }} META_TAGS: ${{ steps.meta.outputs.tags }} META_DEV_TAGS: ${{ steps.meta-dev.outputs.tags }} DOCKER_NO_CACHE: ${{ vars.DOCKER_NO_CACHE == 'true' && 'true' || 'false' }} IS_TEST_RUN: ${{ needs.determine-builds.outputs.is-test-run }} run: | mapfile -t tags < <(printf '%s\n%s\n' "${META_TAGS}" "${META_DEV_TAGS}" | sed '/^$/d') tag_args=() for tag in "${tags[@]}"; do tag_args+=("-t" "$tag") done if [[ "$IS_TEST_RUN" == "true" ]]; then if [[ -z "$AMD64_DIGEST" || -z "$ARM64_DIGEST" ]]; then echo "::error::Missing sandbox image digest(s); cannot create test manifest" exit 1 fi images="${IMAGE_REPO}@${AMD64_DIGEST} ${IMAGE_REPO}@${ARM64_DIGEST}" docker buildx imagetools create \ "${tag_args[@]}" \ $images elif [[ "$DOCKER_NO_CACHE" != "true" ]] && docker buildx imagetools inspect "${REGISTRY_IMAGE}:${CONTEXT_TAG}" >/dev/null 2>&1; then docker buildx imagetools create \ "${tag_args[@]}" \ "${REGISTRY_IMAGE}:${CONTEXT_TAG}" else if [[ -z "$AMD64_DIGEST" || -z "$ARM64_DIGEST" ]]; then echo "::error::Missing sandbox image digest(s); cannot create ${CONTEXT_TAG}" exit 1 fi tag_args=("-t" "${REGISTRY_IMAGE}:${CONTEXT_TAG}" "${tag_args[@]}") images="${IMAGE_REPO}@${AMD64_DIGEST} ${IMAGE_REPO}@${ARM64_DIGEST}" docker buildx imagetools create \ "${tag_args[@]}" \ $images fi dispatch-cloud-deployment: needs: - determine-builds - merge-web-cloud - merge-backend - merge-model-server - merge-sandbox if: >- always() && needs.merge-web-cloud.result == 'success' && needs.merge-backend.result == 'success' && needs.merge-model-server.result == 'success' && needs.merge-sandbox.result == 'success' && needs.determine-builds.outputs.is-cloud-tag == 'true' && needs.determine-builds.outputs.is-test-run != 'true' # NOTE: Github-hosted runners have about 20s faster queue times and are preferred here. runs-on: ubuntu-slim timeout-minutes: 10 permissions: {} steps: - name: Resolve dispatch target id: target env: CLOUD_DEPLOYMENT_REPO: ${{ vars.CLOUD_DEPLOYMENT_REPO }} run: | if [ -z "$CLOUD_DEPLOYMENT_REPO" ]; then echo "::error::CLOUD_DEPLOYMENT_REPO is not set" exit 1 fi echo "owner=${CLOUD_DEPLOYMENT_REPO%%/*}" >> "$GITHUB_OUTPUT" echo "repo=${CLOUD_DEPLOYMENT_REPO#*/}" >> "$GITHUB_OUTPUT" - name: Mint GitHub App installation token id: app-token uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 with: client-id: ${{ vars.CLOUD_DEPLOYMENT_APP_ID }} private-key: ${{ secrets.CLOUD_DEPLOYMENT_APP_PRIVATE_KEY }} # Scope the token to the same repo the dispatch targets, derived from CLOUD_DEPLOYMENT_REPO. owner: ${{ steps.target.outputs.owner }} repositories: ${{ steps.target.outputs.repo }} permission-contents: write - name: Trigger new-cloud-image dispatch env: GH_TOKEN: ${{ steps.app-token.outputs.token }} CLOUD_DEPLOYMENT_REPO: ${{ vars.CLOUD_DEPLOYMENT_REPO }} VERSION: ${{ github.ref_name }} run: | gh api "repos/${CLOUD_DEPLOYMENT_REPO}/dispatches" \ -f event_type=new-cloud-image \ -F "client_payload[version]=${VERSION}" # A failed audit usually means a new advisory, so the CVE Alerts run opens # its tracking issue, fix PR, and Slack post. Every release tag also gets a # run once its images are published, since a release branch has no schedule # and that run is what closes the branch's issues after a fix ships. dispatch-cve-alerts: needs: [determine-builds, audit-gate, merge-web, merge-backend, merge-model-server] if: >- always() && !cancelled() && needs.determine-builds.outputs.is-test-run != 'true' && (needs.audit-gate.result == 'failure' || startsWith(github.ref_name, 'v')) runs-on: ubuntu-slim timeout-minutes: 5 permissions: actions: write # dispatch cve-alerts.yml contents: read # look up the release branch steps: # A release tag's findings live on its release branch, so the run scans # that branch. Anything else, such as the nightly tag, scans main. - name: Dispatch CVE Alerts env: GH_TOKEN: ${{ github.token }} REPO: ${{ github.repository }} TAG: ${{ github.ref_name }} SERVER_URL: ${{ github.server_url }} RUN_ID: ${{ github.run_id }} run: | ref=main if [[ "${TAG}" =~ ^v([0-9]+\.[0-9]+)\. ]] && gh api "repos/${REPO}/branches/release/v${BASH_REMATCH[1]}" > /dev/null 2>&1; then ref="release/v${BASH_REMATCH[1]}" fi gh workflow run cve-alerts.yml --repo "${REPO}" --ref "${ref}" -f "source-run=${SERVER_URL}/${REPO}/actions/runs/${RUN_ID}" notify-slack-on-failure: needs: - determine-builds - audit-gate - create-release - build-desktop - build-web-amd64 - build-web-arm64 - merge-web - build-web-cloud-amd64 - build-web-cloud-arm64 - merge-web-cloud - build-backend-amd64 - build-backend-arm64 - merge-backend - build-model-server-amd64 - build-model-server-arm64 - merge-model-server - prepare-sandbox - build-sandbox-amd64 - build-sandbox-arm64 - merge-sandbox if: always() && (needs.audit-gate.result == 'failure' || needs.create-release.result == 'failure' || needs.build-desktop.result == 'failure' || needs.build-web-amd64.result == 'failure' || needs.build-web-arm64.result == 'failure' || needs.merge-web.result == 'failure' || needs.build-web-cloud-amd64.result == 'failure' || needs.build-web-cloud-arm64.result == 'failure' || needs.merge-web-cloud.result == 'failure' || needs.build-backend-amd64.result == 'failure' || needs.build-backend-arm64.result == 'failure' || needs.merge-backend.result == 'failure' || needs.build-model-server-amd64.result == 'failure' || needs.build-model-server-arm64.result == 'failure' || needs.merge-model-server.result == 'failure' || needs.prepare-sandbox.result == 'failure' || needs.build-sandbox-amd64.result == 'failure' || needs.build-sandbox-arm64.result == 'failure' || needs.merge-sandbox.result == 'failure') && needs.determine-builds.outputs.is-test-run != 'true' # NOTE: Github-hosted runners have about 20s faster queue times and are preferred here. runs-on: ubuntu-slim timeout-minutes: 90 environment: release steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # ratchet:actions/checkout@v6 with: persist-credentials: false - name: Determine failed jobs id: failed-jobs shell: bash run: | FAILED_JOBS="" if [ "${NEEDS_AUDIT_GATE_RESULT}" == "failure" ]; then FAILED_JOBS="${FAILED_JOBS}• audit-gate\\n" fi if [ "${NEEDS_CREATE_RELEASE_RESULT}" == "failure" ]; then FAILED_JOBS="${FAILED_JOBS}• create-release\\n" fi if [ "${NEEDS_BUILD_DESKTOP_RESULT}" == "failure" ]; then FAILED_JOBS="${FAILED_JOBS}• build-desktop\\n" fi if [ "${NEEDS_BUILD_WEB_AMD64_RESULT}" == "failure" ]; then FAILED_JOBS="${FAILED_JOBS}• build-web-amd64\\n" fi if [ "${NEEDS_BUILD_WEB_ARM64_RESULT}" == "failure" ]; then FAILED_JOBS="${FAILED_JOBS}• build-web-arm64\\n" fi if [ "${NEEDS_MERGE_WEB_RESULT}" == "failure" ]; then FAILED_JOBS="${FAILED_JOBS}• merge-web\\n" fi if [ "${NEEDS_BUILD_WEB_CLOUD_AMD64_RESULT}" == "failure" ]; then FAILED_JOBS="${FAILED_JOBS}• build-web-cloud-amd64\\n" fi if [ "${NEEDS_BUILD_WEB_CLOUD_ARM64_RESULT}" == "failure" ]; then FAILED_JOBS="${FAILED_JOBS}• build-web-cloud-arm64\\n" fi if [ "${NEEDS_MERGE_WEB_CLOUD_RESULT}" == "failure" ]; then FAILED_JOBS="${FAILED_JOBS}• merge-web-cloud\\n" fi if [ "${NEEDS_BUILD_BACKEND_AMD64_RESULT}" == "failure" ]; then FAILED_JOBS="${FAILED_JOBS}• build-backend-amd64\\n" fi if [ "${NEEDS_BUILD_BACKEND_ARM64_RESULT}" == "failure" ]; then FAILED_JOBS="${FAILED_JOBS}• build-backend-arm64\\n" fi if [ "${NEEDS_MERGE_BACKEND_RESULT}" == "failure" ]; then FAILED_JOBS="${FAILED_JOBS}• merge-backend\\n" fi if [ "${NEEDS_BUILD_MODEL_SERVER_AMD64_RESULT}" == "failure" ]; then FAILED_JOBS="${FAILED_JOBS}• build-model-server-amd64\\n" fi if [ "${NEEDS_BUILD_MODEL_SERVER_ARM64_RESULT}" == "failure" ]; then FAILED_JOBS="${FAILED_JOBS}• build-model-server-arm64\\n" fi if [ "${NEEDS_MERGE_MODEL_SERVER_RESULT}" == "failure" ]; then FAILED_JOBS="${FAILED_JOBS}• merge-model-server\\n" fi if [ "${NEEDS_PREPARE_SANDBOX_RESULT}" == "failure" ]; then FAILED_JOBS="${FAILED_JOBS}• prepare-sandbox\\n" fi if [ "${NEEDS_BUILD_SANDBOX_AMD64_RESULT}" == "failure" ]; then FAILED_JOBS="${FAILED_JOBS}• build-sandbox-amd64\\n" fi if [ "${NEEDS_BUILD_SANDBOX_ARM64_RESULT}" == "failure" ]; then FAILED_JOBS="${FAILED_JOBS}• build-sandbox-arm64\\n" fi if [ "${NEEDS_MERGE_SANDBOX_RESULT}" == "failure" ]; then FAILED_JOBS="${FAILED_JOBS}• merge-sandbox\\n" fi # Remove trailing \n and set output FAILED_JOBS=$(printf '%s' "$FAILED_JOBS" | sed 's/\\n$//') echo "jobs=$FAILED_JOBS" >> "$GITHUB_OUTPUT" env: NEEDS_AUDIT_GATE_RESULT: ${{ needs.audit-gate.result }} NEEDS_CREATE_RELEASE_RESULT: ${{ needs.create-release.result }} NEEDS_BUILD_DESKTOP_RESULT: ${{ needs.build-desktop.result }} NEEDS_BUILD_WEB_AMD64_RESULT: ${{ needs.build-web-amd64.result }} NEEDS_BUILD_WEB_ARM64_RESULT: ${{ needs.build-web-arm64.result }} NEEDS_MERGE_WEB_RESULT: ${{ needs.merge-web.result }} NEEDS_BUILD_WEB_CLOUD_AMD64_RESULT: ${{ needs.build-web-cloud-amd64.result }} NEEDS_BUILD_WEB_CLOUD_ARM64_RESULT: ${{ needs.build-web-cloud-arm64.result }} NEEDS_MERGE_WEB_CLOUD_RESULT: ${{ needs.merge-web-cloud.result }} NEEDS_BUILD_BACKEND_AMD64_RESULT: ${{ needs.build-backend-amd64.result }} NEEDS_BUILD_BACKEND_ARM64_RESULT: ${{ needs.build-backend-arm64.result }} NEEDS_MERGE_BACKEND_RESULT: ${{ needs.merge-backend.result }} NEEDS_BUILD_MODEL_SERVER_AMD64_RESULT: ${{ needs.build-model-server-amd64.result }} NEEDS_BUILD_MODEL_SERVER_ARM64_RESULT: ${{ needs.build-model-server-arm64.result }} NEEDS_MERGE_MODEL_SERVER_RESULT: ${{ needs.merge-model-server.result }} NEEDS_PREPARE_SANDBOX_RESULT: ${{ needs.prepare-sandbox.result }} NEEDS_BUILD_SANDBOX_AMD64_RESULT: ${{ needs.build-sandbox-amd64.result }} NEEDS_BUILD_SANDBOX_ARM64_RESULT: ${{ needs.build-sandbox-arm64.result }} NEEDS_MERGE_SANDBOX_RESULT: ${{ needs.merge-sandbox.result }} - name: Send Slack notification uses: ./.github/actions/slack-notify with: webhook-url: ${{ secrets.MONITOR_DEPLOYMENTS_WEBHOOK }} failed-jobs: ${{ steps.failed-jobs.outputs.jobs }} title: "🚨 Deployment Workflow Failed" ref-name: ${{ github.ref_name }}