diff --git a/.github/workflows/fork-ci.yml b/.github/workflows/fork-ci.yml index c27c9214aed..4dadd360d98 100644 --- a/.github/workflows/fork-ci.yml +++ b/.github/workflows/fork-ci.yml @@ -191,131 +191,3 @@ jobs: - name: Exercise release-only workflow steps run: node scripts/release-smoke.ts - - deployment_scope: - name: Classify Deployment Scope - # Two release tips during the cutover: the composed fork/integration tip - # (dispatched by the stack workflow) and fork/dev merge commits. Keep both - # until fork/integration is retired. - if: >- - (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/fork/integration') || - (github.event_name == 'push' && github.ref == 'refs/heads/fork/dev') - runs-on: ubuntu-24.04 - timeout-minutes: 5 - permissions: - actions: read - contents: read - outputs: - deploy: ${{ steps.classify.outputs.deploy }} - server: ${{ steps.classify.outputs.server }} - discord: ${{ steps.classify.outputs.discord }} - vscode: ${{ steps.classify.outputs.vscode }} - mobile: ${{ steps.classify.outputs.mobile }} - desktop: ${{ steps.classify.outputs.desktop }} - steps: - - name: Checkout integration source - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: Find previous successful release-tip CI - id: previous - env: - GH_TOKEN: ${{ github.token }} - # Compare against this branch's own history. Hardcoding - # fork/integration + workflow_dispatch here would make every fork/dev - # push diff against an unrelated tip and classify every component as - # changed. - SCOPE_BRANCH: ${{ github.ref_name }} - SCOPE_EVENT: ${{ github.event_name }} - run: | - previous_sha="$( - gh api --method GET \ - "repos/${GITHUB_REPOSITORY}/actions/workflows/fork-ci.yml/runs" \ - -f "branch=${SCOPE_BRANCH}" \ - -f "event=${SCOPE_EVENT}" \ - -f status=success \ - -f per_page=20 \ - --jq ".workflow_runs | map(select(.head_sha != \"${GITHUB_SHA}\")) | first | .head_sha // \"\"" - )" - echo "sha=${previous_sha}" >>"${GITHUB_OUTPUT}" - - - name: Classify changes since previous successful integration CI - id: classify - env: - PREVIOUS_SHA: ${{ steps.previous.outputs.sha }} - run: | - deploy=true - server=true - discord=true - vscode=true - mobile=true - desktop=true - if [[ "${PREVIOUS_SHA}" =~ ^[0-9a-f]{40}$ ]]; then - if ! git cat-file -e "${PREVIOUS_SHA}^{commit}" 2>/dev/null; then - git fetch --quiet origin "${PREVIOUS_SHA}" || true - fi - if git cat-file -e "${PREVIOUS_SHA}^{commit}" 2>/dev/null; then - classification="$( - scripts/classify-deployment-diff.sh "${PREVIOUS_SHA}" "${GITHUB_SHA}" - )" - printf '%s\n' "${classification}" >&2 - deploy="$(sed -n 's/^deploy=//p' <<<"${classification}")" - server="$(sed -n 's/^server=//p' <<<"${classification}")" - discord="$(sed -n 's/^discord=//p' <<<"${classification}")" - vscode="$(sed -n 's/^vscode=//p' <<<"${classification}")" - mobile="$(sed -n 's/^mobile=//p' <<<"${classification}")" - desktop="$(sed -n 's/^desktop=//p' <<<"${classification}")" - else - echo "Previous successful integration SHA is unavailable; deployment remains enabled." - fi - else - echo "No previous successful integration SHA; deployment remains enabled." - fi - echo "deploy=${deploy}" >>"${GITHUB_OUTPUT}" - echo "server=${server}" >>"${GITHUB_OUTPUT}" - echo "discord=${discord}" >>"${GITHUB_OUTPUT}" - echo "vscode=${vscode}" >>"${GITHUB_OUTPUT}" - echo "mobile=${mobile}" >>"${GITHUB_OUTPUT}" - echo "desktop=${desktop}" >>"${GITHUB_OUTPUT}" - - dispatch_mobile_releases: - name: Dispatch Mobile Releases - needs: [check, test, mobile_native_static_analysis, release_smoke, deployment_scope] - if: >- - ((github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/fork/integration') || - (github.event_name == 'push' && github.ref == 'refs/heads/fork/dev')) && - needs.deployment_scope.outputs.mobile == 'true' - runs-on: ubuntu-24.04 - timeout-minutes: 5 - permissions: - actions: write - contents: read - steps: - - name: Dispatch exact release SHA - env: - GH_TOKEN: ${{ github.token }} - # Dispatch against the branch this run is validating so the mobile - # workflows come from the same tip, not a stale fork/integration copy. - RELEASE_REF: ${{ github.ref_name }} - run: | - # mode=auto, not update: an OTA alone never reaches a phone when the - # native runtime changed, so the fingerprint decides between an update - # and a TestFlight build. iOS only, because Android has no keystore. - # release_branch must match: the mobile workflows check out that branch - # and verify the SHA is contained by it. Defaulting it to - # fork/integration there would reject every fork/dev SHA. - gh workflow run mobile-eas-production.yml \ - --repo "$GITHUB_REPOSITORY" \ - --ref "$RELEASE_REF" \ - -f mode=auto \ - -f platform=ios \ - -f sha="$GITHUB_SHA" \ - -f release_branch="$RELEASE_REF" \ - -f message="${RELEASE_REF} ${GITHUB_SHA}" - gh workflow run mobile-eas-development.yml \ - --repo "$GITHUB_REPOSITORY" \ - --ref "$RELEASE_REF" \ - -f platform=ios \ - -f sha="$GITHUB_SHA" \ - -f release_branch="$RELEASE_REF" diff --git a/.github/workflows/fork-release.yml b/.github/workflows/fork-release.yml new file mode 100644 index 00000000000..4bcd48a8d91 --- /dev/null +++ b/.github/workflows/fork-release.yml @@ -0,0 +1,158 @@ +name: Fork Release + +# Release actions, split out of Fork CI on purpose. +# +# Fork CI answers exactly one question: "is this SHA valid?" Its conclusion is +# what the deploy poller on the smart host promotes on, so a release action must +# never be able to turn it red — a failed EAS dispatch would otherwise strand +# server, Discord, desktop and VS Code deployment of a perfectly good commit. +# +# workflow_run gives the ordering for free: this only starts after Fork CI has +# already concluded success for a push to fork/dev, so nothing is released from a +# SHA that is not green, and a failure here is recorded against this run alone. +# +# NOTE: workflow_run only fires for workflow files present on the repository +# default branch. Until fork/dev is the default branch this workflow will not +# trigger, and mobile releases must be dispatched manually below. +on: + workflow_run: + workflows: ["Fork CI"] + types: [completed] + workflow_dispatch: + inputs: + sha: + description: "Exact green fork/dev SHA to release" + required: true + type: string + release_branch: + description: "Branch that must contain the SHA" + required: false + type: string + default: fork/dev + +concurrency: + # Per SHA: two releases of different commits may safely overlap, and a queued + # release must not be cancelled by a newer one — each SHA gets its own outcome. + group: fork-release-${{ github.event.workflow_run.head_sha || inputs.sha }} + cancel-in-progress: false + +jobs: + release: + name: Release + # Only a green push run on fork/dev. Pull-request and workflow_dispatch runs + # of Fork CI validate a tip that was never merged and must not release. + if: >- + github.event_name == 'workflow_dispatch' || + (github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.event == 'push' && + github.event.workflow_run.head_branch == 'fork/dev') + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + actions: write + contents: read + env: + RELEASE_SHA: ${{ github.event.workflow_run.head_sha || inputs.sha }} + RELEASE_REF: ${{ github.event.workflow_run.head_branch || inputs.release_branch }} + steps: + - name: Checkout release source + uses: actions/checkout@v6 + with: + ref: ${{ github.event.workflow_run.head_sha || inputs.sha }} + fetch-depth: 0 + + - name: Confirm the SHA is green + # workflow_dispatch bypasses the workflow_run gate, so re-check it here. + # Releasing an unvalidated SHA by hand is the one thing this split must + # not make easier. + env: + GH_TOKEN: ${{ github.token }} + run: | + green="$( + gh api --method GET \ + "repos/${GITHUB_REPOSITORY}/actions/workflows/fork-ci.yml/runs" \ + -f branch="${RELEASE_REF}" -f event=push -f status=success -f per_page=30 \ + --jq "[.workflow_runs[] | select(.head_sha == \"${RELEASE_SHA}\")] | length" + )" + if [[ "${green}" -lt 1 ]]; then + echo "error: no successful Fork CI push run for ${RELEASE_SHA} on ${RELEASE_REF}" >&2 + exit 1 + fi + echo "Fork CI is green for ${RELEASE_SHA}." + + - id: previous + name: Find previous released SHA + env: + GH_TOKEN: ${{ github.token }} + run: | + # Compare against the last SHA this workflow released, not the last one + # that merely passed CI: a commit whose release was skipped must still + # be represented in the next release's scope. + previous_sha="$( + gh api --method GET \ + "repos/${GITHUB_REPOSITORY}/actions/workflows/fork-release.yml/runs" \ + -f branch="${RELEASE_REF}" -f status=success -f per_page=30 \ + --jq "[.workflow_runs[] | select(.head_sha != \"${RELEASE_SHA}\")] | first | .head_sha // \"\"" + )" + echo "sha=${previous_sha}" >>"${GITHUB_OUTPUT}" + + - id: classify + name: Classify release scope + env: + PREVIOUS_SHA: ${{ steps.previous.outputs.sha }} + run: | + mobile=true + if [[ "${PREVIOUS_SHA}" =~ ^[0-9a-f]{40}$ ]]; then + if ! git cat-file -e "${PREVIOUS_SHA}^{commit}" 2>/dev/null; then + git fetch --quiet origin "${PREVIOUS_SHA}" || true + fi + if git cat-file -e "${PREVIOUS_SHA}^{commit}" 2>/dev/null; then + classification="$( + scripts/classify-deployment-diff.sh "${PREVIOUS_SHA}" "${RELEASE_SHA}" + )" + printf '%s\n' "${classification}" >&2 + mobile="$(sed -n 's/^mobile=//p' <<<"${classification}")" + [[ "${mobile}" =~ ^(true|false)$ ]] || mobile=true + else + echo "Previous released SHA unavailable; releasing every target." + fi + else + echo "No previous released SHA; releasing every target." + fi + echo "mobile=${mobile}" >>"${GITHUB_OUTPUT}" + + - name: Dispatch mobile releases + if: steps.classify.outputs.mobile == 'true' + env: + GH_TOKEN: ${{ github.token }} + run: | + # mode=auto, not update: an OTA alone never reaches a phone when the + # native runtime changed, so the fingerprint decides between an update + # and a TestFlight build. iOS only, because Android has no keystore. + gh workflow run mobile-eas-production.yml \ + --repo "$GITHUB_REPOSITORY" \ + --ref "$RELEASE_REF" \ + -f mode=auto \ + -f platform=ios \ + -f sha="$RELEASE_SHA" \ + -f release_branch="$RELEASE_REF" \ + -f message="${RELEASE_REF} ${RELEASE_SHA}" + gh workflow run mobile-eas-development.yml \ + --repo "$GITHUB_REPOSITORY" \ + --ref "$RELEASE_REF" \ + -f platform=ios \ + -f sha="$RELEASE_SHA" \ + -f release_branch="$RELEASE_REF" + + - name: Report + if: always() + run: | + { + printf '### Fork Release\n\n' + printf -- '- SHA: `%s`\n' "${RELEASE_SHA}" + printf -- '- Branch: `%s`\n' "${RELEASE_REF}" + printf -- '- Previous released: `%s`\n' "${{ steps.previous.outputs.sha || 'none' }}" + printf -- '- Mobile dispatched: `%s`\n' "${{ steps.classify.outputs.mobile }}" + printf -- '\nServer, Discord, desktop and VS Code are promoted by the smart-host poller\n' + printf -- 'from the green Fork CI run, independently of this workflow.\n' + } >>"${GITHUB_STEP_SUMMARY}"