RELEASE — Publish (preview + production) #159
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: RELEASE — Publish (preview + production) | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| checks_profile: | |
| description: "Checks — Profile (full=all standard checks, fast=skip slow builds/smoke, none=skip all checks, custom=use toggles below)" | |
| required: true | |
| type: choice | |
| default: full | |
| options: | |
| - full | |
| - fast | |
| - none | |
| - custom | |
| run_providers: | |
| description: "Checks — Run providers contract suite (requires provider secrets)" | |
| required: true | |
| default: false | |
| type: boolean | |
| providers_preset: | |
| description: "Checks (PROVIDERS) — Preset" | |
| required: true | |
| default: all | |
| type: choice | |
| options: | |
| - all | |
| - claude | |
| - codex | |
| - opencode | |
| providers_tier: | |
| description: "Checks (PROVIDERS) — Tier" | |
| required: true | |
| default: smoke | |
| type: choice | |
| options: | |
| - smoke | |
| - extended | |
| custom_checks: | |
| description: "Checks (CUST) — Comma-separated toggles used only when checks_profile=custom: e2e_core,e2e_core_slow,server_db_contract,stress,cli_smoke_linux,build_website,build_docs" | |
| required: true | |
| default: e2e_core,server_db_contract,cli_smoke_linux,build_website,build_docs | |
| type: string | |
| release_verify_profile: | |
| description: "Verify — Post-build release verification (none=skip, smoke=installers+binaries, full=include self-host E2E). Preview-only; runs best-effort after publishing." | |
| required: true | |
| type: choice | |
| default: none | |
| options: | |
| - none | |
| - smoke | |
| - full | |
| dry_run: | |
| description: "Safety — Run checks and show plan, but do not push/deploy" | |
| required: true | |
| default: false | |
| type: boolean | |
| environment: | |
| description: "Deploy — Target environment (deploy/<env>/*)" | |
| required: true | |
| type: choice | |
| default: preview | |
| options: | |
| - preview | |
| - production | |
| deploy_targets: | |
| description: "Deploy — Comma-separated targets: ui,server,website,docs,cli,stack,server_runner" | |
| required: true | |
| default: ui,server,website,docs | |
| type: string | |
| force_deploy: | |
| description: "Deploy — Force promote + deploy for enabled targets even if no relevant changes are detected (useful after infra failures)" | |
| required: true | |
| default: false | |
| type: boolean | |
| ui_expo_action: | |
| description: "Deploy — UI Expo action (none|ota|native|native_submit) for the selected environment" | |
| required: true | |
| default: none | |
| type: choice | |
| options: | |
| - none | |
| - ota | |
| - native | |
| - native_submit | |
| ui_expo_builder: | |
| description: "Deploy — UI native build runner (eas_cloud uses Expo build servers; eas_local builds on GitHub runners)" | |
| required: true | |
| default: eas_cloud | |
| type: choice | |
| options: | |
| - eas_cloud | |
| - eas_local | |
| ui_expo_profile: | |
| description: "Deploy — UI EAS build profile (auto recommended). Use *-apk profiles to produce installable Android APKs and publish to GitHub Releases." | |
| required: true | |
| default: auto | |
| type: choice | |
| options: | |
| - auto | |
| - preview | |
| - preview-apk | |
| - production | |
| - production-apk | |
| ui_expo_platform: | |
| description: "Deploy — UI Expo native platform" | |
| required: true | |
| default: all | |
| type: choice | |
| options: | |
| - ios | |
| - android | |
| - all | |
| desktop_mode: | |
| description: "Deploy — Desktop artifacts mode" | |
| required: true | |
| type: choice | |
| default: none | |
| options: | |
| - none | |
| - build_only | |
| - build_and_publish | |
| bump: | |
| description: "Version bump (Preset) — Default bump for changed components. Preview bumps commit to dev; production bumps commit to dev then promote dev->main." | |
| required: true | |
| type: choice | |
| default: none | |
| options: | |
| - none | |
| - patch | |
| - minor | |
| - major | |
| bump_app_override: | |
| description: "Version bump override (App) — preset=use bump preset; none=disable bump" | |
| required: true | |
| type: choice | |
| default: preset | |
| options: | |
| - preset | |
| - none | |
| - patch | |
| - minor | |
| - major | |
| bump_cli_override: | |
| description: "Version bump override (CLI) — preset=use bump preset; none=disable bump" | |
| required: true | |
| type: choice | |
| default: preset | |
| options: | |
| - preset | |
| - none | |
| - patch | |
| - minor | |
| - major | |
| bump_stack_override: | |
| description: "Version bump override (Stack) — preset=use bump preset; none=disable bump" | |
| required: true | |
| type: choice | |
| default: preset | |
| options: | |
| - preset | |
| - none | |
| - patch | |
| - minor | |
| - major | |
| confirm: | |
| description: "Confirm — Choose the exact action for this environment" | |
| required: true | |
| type: choice | |
| options: | |
| - release preview from dev | |
| - release dev to main | |
| - reset main from dev | |
| release_message: | |
| description: "Release message — Optional note to include in Expo OTA metadata and GitHub rolling release notes" | |
| required: false | |
| default: "" | |
| type: string | |
| permissions: | |
| contents: write | |
| actions: read | |
| concurrency: | |
| group: release-unified | |
| cancel-in-progress: false | |
| jobs: | |
| release_actor_guard: | |
| name: Release actor guard | |
| permissions: | |
| contents: read | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Authorize release actor | |
| uses: ./.github/actions/release-actor-guard | |
| with: | |
| team_slug: release-admins | |
| ci: | |
| name: CI Checks (tests.yml) | |
| needs: [release_actor_guard] | |
| if: inputs.checks_profile != 'none' | |
| uses: ./.github/workflows/tests.yml | |
| with: | |
| run_ui: true | |
| run_server: true | |
| run_cli: true | |
| run_stack: true | |
| run_typecheck: true | |
| run_cli_daemon_e2e: true | |
| run_e2e_core: ${{ inputs.checks_profile == 'full' || (inputs.checks_profile == 'custom' && (contains(format(',{0},', inputs.custom_checks), ',e2e_core,') || contains(format(',{0},', inputs.custom_checks), ',e2e_core_slow,'))) }} | |
| run_e2e_core_slow: ${{ inputs.checks_profile == 'full' || (inputs.checks_profile == 'custom' && contains(format(',{0},', inputs.custom_checks), ',e2e_core_slow,')) }} | |
| run_server_db_contract: ${{ inputs.checks_profile == 'full' || (inputs.checks_profile == 'custom' && contains(format(',{0},', inputs.custom_checks), ',server_db_contract,')) }} | |
| run_providers: false | |
| providers_preset: ${{ inputs.providers_preset }} | |
| providers_tier: ${{ inputs.providers_tier }} | |
| run_stress: ${{ inputs.checks_profile == 'custom' && contains(format(',{0},', inputs.custom_checks), ',stress,') }} | |
| run_release_contracts: true | |
| run_installers_smoke: false | |
| run_binary_smoke: false | |
| run_self_host_systemd: false | |
| run_self_host_launchd: false | |
| run_self_host_schtasks: false | |
| run_self_host_daemon: false | |
| providers: | |
| name: Providers Checks (providers-contracts.yml) | |
| needs: [release_actor_guard] | |
| if: inputs.checks_profile != 'none' && inputs.run_providers == true | |
| uses: ./.github/workflows/providers-contracts.yml | |
| secrets: inherit | |
| with: | |
| preset: ${{ inputs.providers_preset }} | |
| tier: ${{ inputs.providers_tier }} | |
| strict_keys: false | |
| flake_retry: true | |
| plan: | |
| name: Release Plan (inputs + changes + bump/publish decisions) | |
| needs: [release_actor_guard, ci, providers] | |
| if: ${{ always() && needs.release_actor_guard.result == 'success' && (needs.ci.result == 'success' || needs.ci.result == 'skipped') && (needs.providers.result == 'success' || needs.providers.result == 'skipped') }} | |
| runs-on: ubuntu-latest | |
| outputs: | |
| changed_ui: ${{ steps.plan.outputs.changed_ui }} | |
| changed_cli: ${{ steps.plan.outputs.changed_cli }} | |
| changed_server: ${{ steps.plan.outputs.changed_server }} | |
| changed_website: ${{ steps.plan.outputs.changed_website }} | |
| changed_docs: ${{ steps.plan.outputs.changed_docs }} | |
| changed_shared: ${{ steps.plan.outputs.changed_shared }} | |
| changed_stack: ${{ steps.plan.outputs.changed_stack }} | |
| bump_app: ${{ steps.bump_plan.outputs.bump_app }} | |
| bump_cli: ${{ steps.bump_plan.outputs.bump_cli }} | |
| bump_stack: ${{ steps.bump_plan.outputs.bump_stack }} | |
| bump_server: ${{ steps.bump_plan.outputs.bump_server }} | |
| bump_website: ${{ steps.bump_plan.outputs.bump_website }} | |
| should_bump: ${{ steps.bump_plan.outputs.should_bump }} | |
| publish_cli: ${{ steps.bump_plan.outputs.publish_cli }} | |
| publish_stack: ${{ steps.bump_plan.outputs.publish_stack }} | |
| publish_server: ${{ steps.bump_plan.outputs.publish_server }} | |
| steps: | |
| - name: Validate inputs | |
| run: | | |
| set -euo pipefail | |
| confirm="${{ inputs.confirm }}" | |
| env_name="${{ inputs.environment }}" | |
| mode="" | |
| if [ "$env_name" = "preview" ]; then | |
| if [ "$confirm" != "release preview from dev" ]; then | |
| echo "Confirmation mismatch for preview releases." >&2 | |
| echo "Expected: release preview from dev" >&2 | |
| echo "Got: $confirm" >&2 | |
| exit 1 | |
| fi | |
| mode="preview_release" | |
| elif [ "$confirm" = "release dev to main" ]; then | |
| mode="fast_forward" | |
| elif [ "$confirm" = "reset main from dev" ]; then | |
| mode="reset" | |
| else | |
| echo "Unknown confirmation phrase: $confirm" >&2 | |
| exit 1 | |
| fi | |
| # Validate deploy_targets extras (cli/stack are publishable targets). | |
| raw_targets="${{ inputs.deploy_targets }}" | |
| norm="$(printf '%s' "${raw_targets}" | tr '[:upper:]' '[:lower:]' | tr -d ' \t\r\n')" | |
| if [ -n "${norm:-}" ]; then | |
| IFS=',' read -r -a parts <<< "${norm}" | |
| for p in "${parts[@]}"; do | |
| [ -z "${p:-}" ] && continue | |
| case "${p}" in | |
| ui|server|website|docs|cli|stack|server_runner) ;; | |
| *) | |
| echo "Unknown deploy_targets entry: '${p}'. Expected: ui,server,website,docs,cli,stack,server_runner" >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| done | |
| fi | |
| - name: Enforce trusted refs for manual dispatch | |
| run: | | |
| set -euo pipefail | |
| if [ "${GITHUB_EVENT_NAME}" != "workflow_dispatch" ]; then | |
| exit 0 | |
| fi | |
| case "${GITHUB_REF_NAME}" in | |
| dev|main) ;; | |
| *) | |
| echo "Refusing workflow_dispatch from untrusted ref '${GITHUB_REF_NAME}'. Use dev or main." >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| - name: Checkout dev | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: dev | |
| fetch-depth: 0 | |
| - name: Setup Node | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22.x | |
| cache: yarn | |
| cache-dependency-path: yarn.lock | |
| - name: Enable Corepack (Yarn) | |
| if: ${{ (inputs.checks_profile == 'full') || (inputs.checks_profile == 'custom' && (contains(format(',{0},', inputs.custom_checks), ',build_website,') || contains(format(',{0},', inputs.custom_checks), ',build_docs,') || contains(format(',{0},', inputs.custom_checks), ',cli_smoke_linux,'))) }} | |
| run: | | |
| corepack enable | |
| corepack prepare yarn@1.22.22 --activate | |
| - name: Install dependencies | |
| if: ${{ (inputs.checks_profile == 'full') || (inputs.checks_profile == 'custom' && (contains(format(',{0},', inputs.custom_checks), ',build_website,') || contains(format(',{0},', inputs.custom_checks), ',build_docs,') || contains(format(',{0},', inputs.custom_checks), ',cli_smoke_linux,'))) }} | |
| env: | |
| YARN_PRODUCTION: "false" | |
| npm_config_production: "false" | |
| run: yarn install --frozen-lockfile --ignore-engines | |
| - name: Test + typecheck | |
| if: inputs.checks_profile != 'none' | |
| run: | | |
| set -euo pipefail | |
| echo "CI checks ran in job 'ci' (tests/typecheck/e2e/providers/stress as configured)." | |
| - name: Build website | |
| if: ${{ (inputs.checks_profile == 'full') || (inputs.checks_profile == 'custom' && contains(format(',{0},', inputs.custom_checks), ',build_website,')) }} | |
| run: yarn website:build | |
| - name: Build docs | |
| if: ${{ (inputs.checks_profile == 'full') || (inputs.checks_profile == 'custom' && contains(format(',{0},', inputs.custom_checks), ',build_docs,')) }} | |
| run: yarn docs:build | |
| - name: CLI smoke test (Linux) | |
| if: ${{ (inputs.checks_profile == 'full') || (inputs.checks_profile == 'custom' && contains(format(',{0},', inputs.custom_checks), ',cli_smoke_linux,')) }} | |
| run: | | |
| set -euo pipefail | |
| yarn workspace @happier-dev/cli build | |
| PACKAGE_NAME="$(cd apps/cli && npm pack --silent --pack-destination /tmp)" | |
| PACKAGE_FILE="/tmp/${PACKAGE_NAME}" | |
| if [ ! -f "${PACKAGE_FILE}" ]; then | |
| echo "Unable to find packed CLI tarball at ${PACKAGE_FILE}" >&2 | |
| exit 1 | |
| fi | |
| SMOKE_PREFIX="$(mktemp -d)" | |
| SMOKE_HOME="$(mktemp -d)" | |
| npm install -g --prefix "${SMOKE_PREFIX}" "${PACKAGE_FILE}" | |
| HAPPIER_BIN="${SMOKE_PREFIX}/bin/happier" | |
| if [ ! -x "${HAPPIER_BIN}" ]; then | |
| echo "Expected CLI binary not found at ${HAPPIER_BIN}" >&2 | |
| exit 1 | |
| fi | |
| HAPPIER_HOME_DIR="${SMOKE_HOME}" timeout 30s "${HAPPIER_BIN}" --help | |
| HAPPIER_HOME_DIR="${SMOKE_HOME}" timeout 10s "${HAPPIER_BIN}" --version | |
| DOCTOR_OUTPUT="$(HAPPIER_HOME_DIR="${SMOKE_HOME}" timeout 10s "${HAPPIER_BIN}" doctor --help)" | |
| printf '%s\n' "${DOCTOR_OUTPUT}" | |
| DAEMON_HELP_OUTPUT="$(HAPPIER_HOME_DIR="${SMOKE_HOME}" timeout 10s "${HAPPIER_BIN}" daemon --help)" | |
| printf '%s\n' "${DAEMON_HELP_OUTPUT}" | |
| printf '%s' "${DAEMON_HELP_OUTPUT}" | grep -q 'Daemon management' | |
| - name: Compute changed components (main..dev) | |
| id: plan | |
| run: | | |
| set -euo pipefail | |
| git fetch origin main dev --prune --tags | |
| dev_sha="$(git rev-parse HEAD)" | |
| main_sha="$(git rev-parse origin/main)" | |
| node scripts/release/compute-changed-components.mjs \ | |
| --base "$main_sha" \ | |
| --head "$dev_sha" \ | |
| --out "$GITHUB_OUTPUT" | |
| - name: Release plan summary | |
| run: | | |
| set -euo pipefail | |
| { | |
| echo "## Release plan (dev → main)" | |
| echo "" | |
| echo "- checks_profile: \`${{ inputs.checks_profile }}\`" | |
| echo "- custom_checks: \`${{ inputs.custom_checks }}\`" | |
| echo "- environment: \`${{ inputs.environment }}\`" | |
| echo "- confirm: \`${{ inputs.confirm }}\`" | |
| echo "- dry_run: \`${{ inputs.dry_run }}\`" | |
| echo "- deploy_targets: \`${{ inputs.deploy_targets }}\`" | |
| echo "- bump: \`${{ inputs.bump }}\`" | |
| echo "- bump_app_override: \`${{ inputs.bump_app_override }}\`" | |
| echo "- bump_cli_override: \`${{ inputs.bump_cli_override }}\`" | |
| echo "- bump_stack_override: \`${{ inputs.bump_stack_override }}\`" | |
| echo "- ui_expo_action: \`${{ inputs.ui_expo_action }}\` (builder=\`${{ inputs.ui_expo_builder }}\`, platform=\`${{ inputs.ui_expo_platform }}\`, profile=\`${{ inputs.ui_expo_profile }}\`)" | |
| echo "- desktop_mode: \`${{ inputs.desktop_mode }}\`" | |
| echo "" | |
| echo "- commits to release (main..dev): \`${{ steps.plan.outputs.commit_count }}\`" | |
| echo "" | |
| echo "### Changed components (main..dev)" | |
| echo "- ui: \`${{ steps.plan.outputs.changed_ui }}\`" | |
| echo "- cli: \`${{ steps.plan.outputs.changed_cli }}\`" | |
| echo "- server: \`${{ steps.plan.outputs.changed_server }}\`" | |
| echo "- stack: \`${{ steps.plan.outputs.changed_stack }}\`" | |
| echo "- website: \`${{ steps.plan.outputs.changed_website }}\`" | |
| echo "- docs: \`${{ steps.plan.outputs.changed_docs }}\`" | |
| echo "- shared (agents/protocol): \`${{ steps.plan.outputs.changed_shared }}\`" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Resolve publish + bump plan | |
| id: bump_plan | |
| env: | |
| ENVIRONMENT: ${{ inputs.environment }} | |
| BUMP_PRESET: ${{ inputs.bump }} | |
| BUMP_APP_OVERRIDE: ${{ inputs.bump_app_override }} | |
| BUMP_CLI_OVERRIDE: ${{ inputs.bump_cli_override }} | |
| BUMP_STACK_OVERRIDE: ${{ inputs.bump_stack_override }} | |
| DEPLOY_TARGETS: ${{ inputs.deploy_targets }} | |
| CHANGED_UI: ${{ steps.plan.outputs.changed_ui }} | |
| CHANGED_CLI: ${{ steps.plan.outputs.changed_cli }} | |
| CHANGED_SERVER: ${{ steps.plan.outputs.changed_server }} | |
| CHANGED_WEBSITE: ${{ steps.plan.outputs.changed_website }} | |
| CHANGED_STACK: ${{ steps.plan.outputs.changed_stack }} | |
| CHANGED_SHARED: ${{ steps.plan.outputs.changed_shared }} | |
| run: | | |
| set -euo pipefail | |
| normalize_csv() { | |
| printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | tr -d ' \t\r\n' | |
| } | |
| resolve_override() { | |
| local override="$1" | |
| local preset="$2" | |
| if [ "$override" = "preset" ]; then | |
| printf '%s' "$preset" | |
| return 0 | |
| fi | |
| printf '%s' "$override" | |
| } | |
| should_bump_component() { | |
| local changed="$1" | |
| local bump="$2" | |
| if [ "$changed" != "true" ]; then | |
| printf '%s' "none" | |
| return 0 | |
| fi | |
| printf '%s' "$bump" | |
| } | |
| bump_preset="${BUMP_PRESET}" | |
| publish_cli="false" | |
| publish_stack="false" | |
| publish_server="false" | |
| pt="$(normalize_csv "${DEPLOY_TARGETS}")" | |
| if [ -n "${pt:-}" ]; then | |
| IFS=',' read -r -a parts <<< "${pt}" | |
| for p in "${parts[@]}"; do | |
| [ -z "${p:-}" ] && continue | |
| if [ "$p" = "cli" ]; then publish_cli="true"; fi | |
| if [ "$p" = "stack" ]; then publish_stack="true"; fi | |
| if [ "$p" = "server_runner" ]; then publish_server="true"; fi | |
| done | |
| fi | |
| app_override="$(resolve_override "${BUMP_APP_OVERRIDE}" "${bump_preset}")" | |
| cli_override="$(resolve_override "${BUMP_CLI_OVERRIDE}" "${bump_preset}")" | |
| stack_override="$(resolve_override "${BUMP_STACK_OVERRIDE}" "${bump_preset}")" | |
| changed_app="false" | |
| if [ "${CHANGED_UI}" = "true" ] || [ "${CHANGED_SHARED}" = "true" ]; then | |
| changed_app="true" | |
| fi | |
| changed_cli="false" | |
| if [ "${CHANGED_CLI}" = "true" ] || [ "${CHANGED_SHARED}" = "true" ]; then | |
| changed_cli="true" | |
| fi | |
| changed_stack="false" | |
| if [ "${CHANGED_STACK}" = "true" ] || [ "${CHANGED_SHARED}" = "true" ]; then | |
| changed_stack="true" | |
| fi | |
| changed_server="false" | |
| if [ "${CHANGED_SERVER}" = "true" ] || [ "${CHANGED_SHARED}" = "true" ]; then | |
| changed_server="true" | |
| fi | |
| bump_app="$(should_bump_component "${changed_app}" "${app_override}")" | |
| bump_cli="$(should_bump_component "${changed_cli}" "${cli_override}")" | |
| bump_stack="$(should_bump_component "${changed_stack}" "${stack_override}")" | |
| bump_server="$(should_bump_component "${changed_server}" "${bump_preset}")" | |
| bump_website="$(should_bump_component "${CHANGED_WEBSITE}" "${bump_preset}")" | |
| if [ "${ENVIRONMENT}" = "production" ]; then | |
| if [ "${publish_cli}" = "true" ] && [ "${bump_cli}" = "none" ]; then | |
| dev_version="$(node -p 'require("./apps/cli/package.json").version' | tr -d '\n' || true)" | |
| main_version="$(git show origin/main:apps/cli/package.json | node -e 'const fs=require("node:fs"); const raw=fs.readFileSync(0,"utf8"); process.stdout.write(String(JSON.parse(raw).version||""))')" | |
| if [ -z "${dev_version:-}" ] || [ -z "${main_version:-}" ]; then | |
| echo "Unable to resolve cli versions for production validation." >&2 | |
| exit 1 | |
| fi | |
| if [ "${dev_version}" = "${main_version}" ]; then | |
| echo "Refusing production deploy_targets includes cli without a version change (dev and main both at ${dev_version}). Set bump!=none or bump_cli_override!=none." >&2 | |
| exit 1 | |
| fi | |
| fi | |
| if [ "${publish_stack}" = "true" ] && [ "${bump_stack}" = "none" ]; then | |
| dev_version="$(node -p 'require("./apps/stack/package.json").version' | tr -d '\n' || true)" | |
| main_version="$(git show origin/main:apps/stack/package.json | node -e 'const fs=require("node:fs"); const raw=fs.readFileSync(0,"utf8"); process.stdout.write(String(JSON.parse(raw).version||""))')" | |
| if [ -z "${dev_version:-}" ] || [ -z "${main_version:-}" ]; then | |
| echo "Unable to resolve stack versions for production validation." >&2 | |
| exit 1 | |
| fi | |
| if [ "${dev_version}" = "${main_version}" ]; then | |
| echo "Refusing production deploy_targets includes stack without a version change (dev and main both at ${dev_version}). Set bump!=none or bump_stack_override!=none." >&2 | |
| exit 1 | |
| fi | |
| fi | |
| if [ "${publish_server}" = "true" ] && [ "${bump_server}" = "none" ]; then | |
| runner_dev_pkg="./packages/relay-server/package.json" | |
| if [ ! -f "${runner_dev_pkg}" ]; then | |
| echo "Unable to resolve server runner package.json (expected ${runner_dev_pkg})." >&2 | |
| exit 1 | |
| fi | |
| dev_version="$(node -p "require('${runner_dev_pkg}').version" 2>/dev/null | tr -d '\n' || true)" | |
| main_version="$(git show 'origin/main:packages/relay-server/package.json' 2>/dev/null | node -e 'const fs=require("node:fs"); const raw=fs.readFileSync(0,"utf8"); try { process.stdout.write(String(JSON.parse(raw).version||"")) } catch { process.stdout.write("") }' || true)" | |
| if [ -n "${main_version:-}" ] && [ -n "${dev_version:-}" ] && [ "${dev_version}" = "${main_version}" ]; then | |
| echo "Refusing production deploy_targets includes server without a version change (dev and main both at ${dev_version}). Set bump!=none." >&2 | |
| exit 1 | |
| fi | |
| fi | |
| fi | |
| should_bump="false" | |
| for v in "${bump_app}" "${bump_cli}" "${bump_stack}" "${bump_server}" "${bump_website}"; do | |
| if [ "${v}" != "none" ]; then | |
| should_bump="true" | |
| break | |
| fi | |
| done | |
| echo "publish_cli=${publish_cli}" >> "$GITHUB_OUTPUT" | |
| echo "publish_stack=${publish_stack}" >> "$GITHUB_OUTPUT" | |
| echo "publish_server=${publish_server}" >> "$GITHUB_OUTPUT" | |
| echo "bump_app=${bump_app}" >> "$GITHUB_OUTPUT" | |
| echo "bump_cli=${bump_cli}" >> "$GITHUB_OUTPUT" | |
| echo "bump_stack=${bump_stack}" >> "$GITHUB_OUTPUT" | |
| echo "bump_server=${bump_server}" >> "$GITHUB_OUTPUT" | |
| echo "bump_website=${bump_website}" >> "$GITHUB_OUTPUT" | |
| echo "should_bump=${should_bump}" >> "$GITHUB_OUTPUT" | |
| { | |
| echo "" | |
| echo "### Effective bump plan" | |
| echo "- publish_cli: \`${publish_cli}\`" | |
| echo "- publish_stack: \`${publish_stack}\`" | |
| echo "- publish_server: \`${publish_server}\`" | |
| echo "- bump preset: \`${bump_preset}\`" | |
| echo "- bump_app: \`${bump_app}\`" | |
| echo "- bump_cli: \`${bump_cli}\`" | |
| echo "- bump_stack: \`${bump_stack}\`" | |
| echo "- bump_server: \`${bump_server}\`" | |
| echo "- bump_website: \`${bump_website}\`" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| deploy_plan: | |
| name: deploy_plan (source → deploy branches) | |
| runs-on: ubuntu-latest | |
| environment: release-shared | |
| if: >- | |
| always() && needs.plan.result == 'success' && ( | |
| contains(format(',{0},', inputs.deploy_targets), ',server,') || | |
| contains(format(',{0},', inputs.deploy_targets), ',website,') || | |
| contains(format(',{0},', inputs.deploy_targets), ',docs,') || | |
| (inputs.environment == 'production' && contains(format(',{0},', inputs.deploy_targets), ',ui,')) | |
| ) | |
| needs: [plan, bump_versions_dev, promote_main] | |
| outputs: | |
| deploy_ui_needed: ${{ steps.plan.outputs.deploy_ui_needed }} | |
| deploy_server_needed: ${{ steps.plan.outputs.deploy_server_needed }} | |
| deploy_website_needed: ${{ steps.plan.outputs.deploy_website_needed }} | |
| deploy_docs_needed: ${{ steps.plan.outputs.deploy_docs_needed }} | |
| steps: | |
| - name: Create GitHub App token | |
| id: app_token | |
| uses: actions/create-github-app-token@v1 | |
| with: | |
| app-id: ${{ secrets.RELEASE_BOT_APP_ID }} | |
| private-key: ${{ secrets.RELEASE_BOT_PRIVATE_KEY }} | |
| - name: Checkout release source | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ inputs.environment == 'production' && 'main' || 'dev' }} | |
| fetch-depth: 0 | |
| token: ${{ steps.app_token.outputs.token }} | |
| - name: Compute deploy plan (deploy/<env>/* behind release source?) | |
| id: plan | |
| env: | |
| ENVIRONMENT: ${{ inputs.environment }} | |
| SOURCE_REF: ${{ inputs.environment == 'production' && 'main' || 'dev' }} | |
| FORCE_DEPLOY: ${{ inputs.force_deploy }} | |
| DEPLOY_UI: ${{ contains(format(',{0},', inputs.deploy_targets), ',ui,') }} | |
| DEPLOY_SERVER: ${{ contains(format(',{0},', inputs.deploy_targets), ',server,') }} | |
| DEPLOY_WEBSITE: ${{ contains(format(',{0},', inputs.deploy_targets), ',website,') }} | |
| DEPLOY_DOCS: ${{ contains(format(',{0},', inputs.deploy_targets), ',docs,') }} | |
| run: | | |
| set -euo pipefail | |
| env_name="${ENVIRONMENT}" | |
| force_deploy="${FORCE_DEPLOY}" | |
| source_ref="${SOURCE_REF}" | |
| source_sha="$(git rev-parse HEAD)" | |
| git fetch origin "${source_ref}" --prune --tags | |
| source_sha="$(git rev-parse "origin/${source_ref}")" | |
| ui_ref="deploy/${env_name}/ui" | |
| server_ref="deploy/${env_name}/server" | |
| website_ref="deploy/${env_name}/website" | |
| docs_ref="deploy/${env_name}/docs" | |
| git fetch origin "${ui_ref}" "${server_ref}" "${website_ref}" "${docs_ref}" --prune || true | |
| plan_one() { | |
| local key="$1" | |
| local deploy_ref="$2" | |
| local enabled="$3" | |
| shift 3 | |
| local -a patterns=("$@") | |
| local deploy_sha="" | |
| deploy_sha="$(git rev-parse "origin/${deploy_ref}" 2>/dev/null || true)" | |
| if [ -z "${deploy_sha:-}" ]; then | |
| local needed=false | |
| if [ "${enabled}" = "true" ] && [ "${force_deploy}" = "true" ]; then | |
| needed=true | |
| fi | |
| printf -v "${key}_needed" '%s' "${needed}" | |
| printf -v "${key}_commits" '%s' "0" | |
| printf -v "${key}_relevant_changes" '%s' "false" | |
| echo "${key}_needed=${needed}" >> "$GITHUB_OUTPUT" | |
| echo "${key}_commits=0" >> "$GITHUB_OUTPUT" | |
| echo "${key}_relevant_changes=false" >> "$GITHUB_OUTPUT" | |
| return 0 | |
| fi | |
| local commits="" | |
| commits="$(git rev-list --count "${deploy_sha}..${source_sha}" 2>/dev/null || echo 0)" | |
| local base="" | |
| base="$(git merge-base "${deploy_sha}" "${source_sha}" 2>/dev/null || true)" | |
| if [ -z "${base:-}" ]; then | |
| base="${deploy_sha}" | |
| fi | |
| local changed=false | |
| if [ "${commits}" != "0" ]; then | |
| git diff --name-only "${base}..${source_sha}" > "/tmp/changed_${key}.txt" || true | |
| while IFS= read -r p; do | |
| for pat in "${patterns[@]}"; do | |
| case "$p" in | |
| $pat) changed=true ;; | |
| *) ;; | |
| esac | |
| if [ "${changed}" = "true" ]; then | |
| break | |
| fi | |
| done | |
| if [ "${changed}" = "true" ]; then | |
| break | |
| fi | |
| done < "/tmp/changed_${key}.txt" | |
| fi | |
| local needed=false | |
| if [ "${enabled}" = "true" ] && [ "${force_deploy}" = "true" ]; then | |
| needed=true | |
| elif [ "${enabled}" = "true" ] && [ "${commits}" != "0" ] && [ "${changed}" = "true" ]; then | |
| needed=true | |
| fi | |
| printf -v "${key}_needed" '%s' "${needed}" | |
| printf -v "${key}_commits" '%s' "${commits}" | |
| printf -v "${key}_relevant_changes" '%s' "${changed}" | |
| echo "${key}_needed=${needed}" >> "$GITHUB_OUTPUT" | |
| echo "${key}_commits=${commits}" >> "$GITHUB_OUTPUT" | |
| echo "${key}_relevant_changes=${changed}" >> "$GITHUB_OUTPUT" | |
| } | |
| plan_one "deploy_ui" "${ui_ref}" "${DEPLOY_UI}" "apps/ui/*" "packages/agents/*" "packages/protocol/*" | |
| plan_one "deploy_server" "${server_ref}" "${DEPLOY_SERVER}" "apps/server/*" "packages/relay-server/*" "packages/agents/*" "packages/protocol/*" | |
| plan_one "deploy_website" "${website_ref}" "${DEPLOY_WEBSITE}" "apps/website/*" "scripts/release/installers/*" "scripts/release/sync-installers.mjs" | |
| plan_one "deploy_docs" "${docs_ref}" "${DEPLOY_DOCS}" "apps/docs/*" | |
| { | |
| echo "## Deploy plan (source → deploy branches)" | |
| echo "" | |
| echo "- environment: \`${env_name}\`" | |
| echo "- source ref: \`${source_ref}\`" | |
| echo "- origin/${source_ref}: \`${source_sha}\`" | |
| echo "" | |
| echo "| component | enabled | deploy ref | commits behind | relevant changes | will run |" | |
| echo "|---|---:|---|---:|---:|---:|" | |
| echo "| ui | \`${DEPLOY_UI}\` | \`${ui_ref}\` | \`${deploy_ui_commits}\` | \`${deploy_ui_relevant_changes}\` | \`${deploy_ui_needed}\` |" | |
| echo "| server | \`${DEPLOY_SERVER}\` | \`${server_ref}\` | \`${deploy_server_commits}\` | \`${deploy_server_relevant_changes}\` | \`${deploy_server_needed}\` |" | |
| echo "| website | \`${DEPLOY_WEBSITE}\` | \`${website_ref}\` | \`${deploy_website_commits}\` | \`${deploy_website_relevant_changes}\` | \`${deploy_website_needed}\` |" | |
| echo "| docs | \`${DEPLOY_DOCS}\` | \`${docs_ref}\` | \`${deploy_docs_commits}\` | \`${deploy_docs_relevant_changes}\` | \`${deploy_docs_needed}\` |" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| promote_main: | |
| # GitHub skips dependent jobs when a needed job is skipped unless we use always(). | |
| # In production, bump_versions_dev is frequently skipped (bump=none), but we still | |
| # must promote dev -> main so downstream deploy/publish jobs can run. | |
| if: always() && needs.plan.result == 'success' && (needs.bump_versions_dev.result == 'success' || needs.bump_versions_dev.result == 'skipped') && inputs.dry_run != true && inputs.environment == 'production' | |
| needs: [plan, bump_versions_dev] | |
| uses: ./.github/workflows/promote-branch.yml | |
| secrets: inherit | |
| with: | |
| source: dev | |
| target: main | |
| mode: ${{ inputs.confirm == 'reset main from dev' && 'reset' || 'fast_forward' }} | |
| dry_run: ${{ inputs.dry_run }} | |
| allow_reset: ${{ inputs.confirm == 'reset main from dev' }} | |
| confirm: ${{ inputs.confirm == 'reset main from dev' && 'reset main from dev' || 'promote main from dev' }} | |
| bump_versions_dev: | |
| if: inputs.dry_run != true && needs.plan.outputs.should_bump == 'true' | |
| needs: [plan] | |
| runs-on: ubuntu-latest | |
| environment: release-shared | |
| steps: | |
| - name: Create GitHub App token | |
| id: app_token | |
| uses: actions/create-github-app-token@v1 | |
| with: | |
| app-id: ${{ secrets.RELEASE_BOT_APP_ID }} | |
| private-key: ${{ secrets.RELEASE_BOT_PRIVATE_KEY }} | |
| - name: Checkout dev | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: dev | |
| fetch-depth: 0 | |
| token: ${{ steps.app_token.outputs.token }} | |
| - name: Setup Node | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22.x | |
| cache: yarn | |
| cache-dependency-path: yarn.lock | |
| - name: Enable Corepack (Yarn) | |
| run: | | |
| corepack enable | |
| corepack prepare yarn@1.22.22 --activate | |
| - name: Install dependencies | |
| env: | |
| YARN_PRODUCTION: "false" | |
| npm_config_production: "false" | |
| run: yarn install --frozen-lockfile --ignore-engines | |
| - name: Apply version bumps (single commit) | |
| id: bumps | |
| run: | | |
| set -euo pipefail | |
| bumped=() | |
| if [ "${{ needs.plan.outputs.bump_app }}" != "none" ]; then | |
| node scripts/release/bump-version.mjs --component app --bump "${{ needs.plan.outputs.bump_app }}" >/tmp/bump_app.txt | |
| bumped+=("app") | |
| fi | |
| if [ "${{ needs.plan.outputs.bump_server }}" != "none" ]; then | |
| node scripts/release/bump-version.mjs --component server --bump "${{ needs.plan.outputs.bump_server }}" >/tmp/bump_server.txt | |
| bumped+=("server") | |
| fi | |
| if [ "${{ needs.plan.outputs.bump_website }}" != "none" ]; then | |
| node scripts/release/bump-version.mjs --component website --bump "${{ needs.plan.outputs.bump_website }}" >/tmp/bump_website.txt | |
| bumped+=("website") | |
| fi | |
| if [ "${{ needs.plan.outputs.bump_cli }}" != "none" ]; then | |
| node scripts/release/bump-version.mjs --component cli --bump "${{ needs.plan.outputs.bump_cli }}" >/tmp/bump_cli.txt | |
| bumped+=("cli") | |
| fi | |
| if [ "${{ needs.plan.outputs.bump_stack }}" != "none" ]; then | |
| node scripts/release/bump-version.mjs --component stack --bump "${{ needs.plan.outputs.bump_stack }}" >/tmp/bump_stack.txt | |
| bumped+=("stack") | |
| fi | |
| if [ "${#bumped[@]}" -eq 0 ]; then | |
| echo "No version bumps requested." >> "$GITHUB_STEP_SUMMARY" | |
| echo "did_bump=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| echo "did_bump=true" >> "$GITHUB_OUTPUT" | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git add apps/ui/package.json apps/ui/app.config.js apps/server/package.json apps/website/package.json apps/cli/package.json apps/stack/package.json packages/relay-server/package.json || true | |
| # Also pick up any tauri version edits if present. | |
| if [ -d apps/ui/src-tauri ]; then | |
| git add apps/ui/src-tauri || true | |
| fi | |
| git commit -m "chore(release): bump versions (${bumped[*]})" | |
| git push origin HEAD:dev | |
| deploy_ui: | |
| if: always() && needs.plan.result == 'success' && inputs.dry_run != true && contains(format(',{0},', inputs.deploy_targets), ',ui,') && (needs.deploy_plan.outputs.deploy_ui_needed == 'true' || needs.plan.outputs.bump_app != 'none' || inputs.force_deploy == true || inputs.ui_expo_action != 'none') && (needs.bump_versions_dev.result == 'success' || needs.bump_versions_dev.result == 'skipped') && (inputs.environment != 'production' || needs.promote_main.result == 'success') | |
| needs: [plan, bump_versions_dev, promote_main, deploy_plan] | |
| permissions: | |
| contents: write | |
| actions: read | |
| uses: ./.github/workflows/promote-ui.yml | |
| secrets: inherit | |
| with: | |
| environment: ${{ inputs.environment }} | |
| force_deploy: ${{ inputs.force_deploy }} | |
| source_ref: ${{ inputs.environment == 'production' && 'main' || 'dev' }} | |
| allow_cross_promote: false | |
| bump: none | |
| deploy_web: ${{ inputs.environment == 'production' }} | |
| expo_action: ${{ inputs.ui_expo_action }} | |
| expo_builder: ${{ inputs.ui_expo_builder }} | |
| expo_profile: ${{ inputs.ui_expo_profile }} | |
| expo_platform: ${{ inputs.ui_expo_platform }} | |
| desktop_build: ${{ inputs.desktop_mode != 'none' }} | |
| desktop_publish_release: ${{ inputs.desktop_mode == 'build_and_publish' }} | |
| run_tests: false | |
| expo_update_message: ${{ inputs.release_message }} | |
| deploy_server: | |
| if: always() && needs.plan.result == 'success' && inputs.dry_run != true && contains(format(',{0},', inputs.deploy_targets), ',server,') && (needs.deploy_plan.outputs.deploy_server_needed == 'true' || needs.plan.outputs.bump_server != 'none' || inputs.force_deploy == true) && (needs.bump_versions_dev.result == 'success' || needs.bump_versions_dev.result == 'skipped') && (inputs.environment != 'production' || needs.promote_main.result == 'success') | |
| needs: [plan, bump_versions_dev, promote_main, deploy_plan] | |
| permissions: | |
| contents: write | |
| actions: read | |
| uses: ./.github/workflows/promote-server.yml | |
| secrets: inherit | |
| with: | |
| environment: ${{ inputs.environment }} | |
| force_deploy: ${{ inputs.force_deploy }} | |
| source_ref: ${{ inputs.environment == 'production' && 'main' || 'dev' }} | |
| allow_cross_promote: false | |
| bump: none | |
| publish_runtime_release: false | |
| run_tests: false | |
| publish_server_runtime: | |
| if: always() && needs.plan.result == 'success' && inputs.dry_run != true && inputs.environment == 'preview' && needs.plan.outputs.publish_server == 'true' && (needs.bump_versions_dev.result == 'success' || needs.bump_versions_dev.result == 'skipped') | |
| needs: [plan, bump_versions_dev] | |
| permissions: | |
| contents: write | |
| uses: ./.github/workflows/publish-server-runtime.yml | |
| secrets: inherit | |
| with: | |
| channel: preview | |
| source_ref: dev | |
| allow_stable: false | |
| release_message: ${{ inputs.release_message }} | |
| publish_ui_web: | |
| if: always() && needs.plan.result == 'success' && inputs.dry_run != true && inputs.environment == 'preview' && contains(format(',{0},', inputs.deploy_targets), ',ui,') && (needs.bump_versions_dev.result == 'success' || needs.bump_versions_dev.result == 'skipped') | |
| needs: [plan, bump_versions_dev] | |
| permissions: | |
| contents: write | |
| uses: ./.github/workflows/publish-ui-web.yml | |
| secrets: inherit | |
| with: | |
| channel: preview | |
| source_ref: dev | |
| allow_stable: false | |
| release_message: ${{ inputs.release_message }} | |
| publish_docker: | |
| if: always() && needs.plan.result == 'success' && inputs.dry_run != true && inputs.environment == 'preview' && (inputs.force_deploy == true || needs.plan.outputs.changed_ui == 'true' || needs.plan.outputs.changed_server == 'true' || needs.plan.outputs.changed_cli == 'true' || needs.plan.outputs.changed_stack == 'true' || needs.plan.outputs.changed_shared == 'true') && (needs.bump_versions_dev.result == 'success' || needs.bump_versions_dev.result == 'skipped') | |
| needs: [plan, bump_versions_dev] | |
| permissions: | |
| contents: read | |
| uses: ./.github/workflows/publish-docker.yml | |
| secrets: inherit | |
| with: | |
| channel: preview | |
| source_ref: dev | |
| push_latest: true | |
| build_relay: ${{ inputs.force_deploy == true || needs.plan.outputs.changed_ui == 'true' || needs.plan.outputs.changed_server == 'true' || needs.plan.outputs.changed_shared == 'true' }} | |
| build_devcontainer: ${{ inputs.force_deploy == true || needs.plan.outputs.changed_cli == 'true' || needs.plan.outputs.changed_stack == 'true' || needs.plan.outputs.changed_shared == 'true' }} | |
| deploy_website: | |
| if: always() && needs.plan.result == 'success' && inputs.dry_run != true && contains(format(',{0},', inputs.deploy_targets), ',website,') && (needs.deploy_plan.outputs.deploy_website_needed == 'true' || needs.plan.outputs.bump_website != 'none' || inputs.force_deploy == true) && (needs.bump_versions_dev.result == 'success' || needs.bump_versions_dev.result == 'skipped') && (inputs.environment != 'production' || needs.promote_main.result == 'success') | |
| needs: [plan, bump_versions_dev, promote_main, deploy_plan] | |
| permissions: | |
| contents: read | |
| actions: read | |
| uses: ./.github/workflows/promote-website.yml | |
| secrets: inherit | |
| with: | |
| environment: ${{ inputs.environment }} | |
| force_deploy: ${{ inputs.force_deploy }} | |
| source_ref: ${{ inputs.environment == 'production' && 'main' || 'dev' }} | |
| allow_cross_promote: false | |
| bump: none | |
| run_build: true | |
| deploy_docs: | |
| if: always() && needs.plan.result == 'success' && inputs.dry_run != true && contains(format(',{0},', inputs.deploy_targets), ',docs,') && (needs.deploy_plan.outputs.deploy_docs_needed == 'true' || inputs.force_deploy == true) && (needs.bump_versions_dev.result == 'success' || needs.bump_versions_dev.result == 'skipped') && (inputs.environment != 'production' || needs.promote_main.result == 'success') | |
| needs: [plan, bump_versions_dev, promote_main, deploy_plan] | |
| permissions: | |
| contents: read | |
| actions: read | |
| uses: ./.github/workflows/promote-docs.yml | |
| secrets: inherit | |
| with: | |
| environment: ${{ inputs.environment }} | |
| force_deploy: ${{ inputs.force_deploy }} | |
| source_ref: ${{ inputs.environment == 'production' && 'main' || 'dev' }} | |
| allow_cross_promote: false | |
| run_build: true | |
| publish_npm: | |
| if: always() && needs.plan.result == 'success' && inputs.dry_run != true && (needs.plan.outputs.publish_cli == 'true' || needs.plan.outputs.publish_stack == 'true' || needs.plan.outputs.publish_server == 'true') && (needs.bump_versions_dev.result == 'success' || needs.bump_versions_dev.result == 'skipped') && (inputs.environment != 'production' || needs.promote_main.result == 'success') | |
| needs: [plan, bump_versions_dev, promote_main] | |
| permissions: | |
| contents: write | |
| id-token: write | |
| uses: ./.github/workflows/release-npm.yml | |
| secrets: inherit | |
| with: | |
| publish_cli: ${{ needs.plan.outputs.publish_cli == 'true' }} | |
| publish_stack: ${{ needs.plan.outputs.publish_stack == 'true' }} | |
| publish_server: ${{ needs.plan.outputs.publish_server == 'true' }} | |
| channel: ${{ inputs.environment }} | |
| source_ref: ${{ inputs.environment == 'production' && 'main' || 'dev' }} | |
| npm_tag: ${{ inputs.environment == 'production' && 'latest' || 'next' }} | |
| run_tests: false | |
| release_message: ${{ inputs.release_message }} | |
| release_verify: | |
| if: always() && needs.publish_npm.result == 'success' && inputs.environment == 'preview' && inputs.release_verify_profile != 'none' | |
| needs: [publish_npm] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| actions: write | |
| steps: | |
| - name: Trigger release verify workflow (best-effort) | |
| env: | |
| REPO: ${{ github.repository }} | |
| REF: ${{ github.ref_name }} | |
| TOKEN: ${{ github.token }} | |
| RUN_SELF_HOST: ${{ inputs.release_verify_profile == 'full' }} | |
| run: | | |
| set -euo pipefail | |
| payload="$( | |
| cat <<JSON | |
| { | |
| "ref": "${REF}", | |
| "inputs": { | |
| "channel": "preview", | |
| "run_installers_smoke": "true", | |
| "run_binary_smoke": "true", | |
| "run_self_host_systemd": "${RUN_SELF_HOST}", | |
| "run_self_host_launchd": "${RUN_SELF_HOST}", | |
| "run_self_host_schtasks": "${RUN_SELF_HOST}", | |
| "run_self_host_daemon": "${RUN_SELF_HOST}" | |
| } | |
| } | |
| JSON | |
| )" | |
| echo "Dispatching release-verify.yml for preview (profile: ${{ inputs.release_verify_profile }})" | |
| if ! curl -fsSL \ | |
| -X POST \ | |
| -H "Authorization: Bearer ${TOKEN}" \ | |
| -H "Accept: application/vnd.github+json" \ | |
| "https://api.github.com/repos/${REPO}/actions/workflows/release-verify.yml/dispatches" \ | |
| -d "${payload}"; then | |
| echo "::warning::Failed to dispatch release-verify.yml (continuing)" | |
| fi | |
| sync_dev: | |
| if: inputs.dry_run != true && inputs.environment == 'production' && (vars.RELEASE_SYNC_DEV_FROM_MAIN == '' || vars.RELEASE_SYNC_DEV_FROM_MAIN == 'true') | |
| needs: [plan, bump_versions_dev, promote_main] | |
| uses: ./.github/workflows/promote-branch.yml | |
| secrets: inherit | |
| with: | |
| source: main | |
| target: dev | |
| mode: fast_forward | |
| dry_run: false | |
| allow_reset: false | |
| confirm: promote dev from main |