This is an automated email from the ASF dual-hosted git repository.

dongjoon-hyun pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/spark-connect-swift.git


The following commit(s) were added to refs/heads/main by this push:
     new cc38900  [SPARK-57151] Run CI in forked repositories and report build 
status to PRs
cc38900 is described below

commit cc389008d25b088fced35773a18ce5104409c71a
Author: Dongjoon Hyun <[email protected]>
AuthorDate: Fri May 29 11:56:22 2026 -0700

    [SPARK-57151] Run CI in forked repositories and report build status to PRs
    
    ### What changes were proposed in this pull request?
    
    Make `build_and_test.yml` a reusable (`workflow_call`) workflow and add 
Apache
    Spark's fork-based CI status mechanism:
    
    - `build_main.yml` — runs on `push` to all branches and calls 
`build_and_test.yml`.
    - `notify_test_workflow.yml` — on `pull_request_target`, creates a `Build` 
check
      on the PR that points to the fork's run.
    - `update_build_status.yml` — every 15 minutes, syncs the `Build` check 
from the
      fork's run.
    - Add `images/workflow-enable-button.png` used by the notify message, and 
repoint
      the README badge and `AGENTS.md` to `build_main.yml`.
    
    Flow:
    
    ```mermaid
    flowchart TD
        subgraph fork["Forked repo (contributor)"]
            P([push to any branch]) --> BM["build_main.yml — job: Run"]
            BM -->|workflow_call| BAT["build_and_test.yml 
(reusable):<br/>License Check, build, integration tests"]
            BAT --> CR["check runs:<br/>Run / License Check, ..."]
        end
    
        subgraph up["Upstream repo (apache/spark-connect-swift)"]
            PRT([pull_request_target]) --> N["notify_test_workflow.yml"]
            SCH([schedule: every 15 min]) --> U["update_build_status.yml"]
            N -->|create| B(["Build check on PR"])
            U -->|"PATCH status / conclusion"| B
        end
    
        BM -.->|"① find run (id: build_main.yml)"| N
        CR -.->|"② link check-run view"| N
        BM -.->|"③ poll run status"| U
    ```
    
    ### Why are the changes needed?
    
    Like Apache Spark main repository, run the heavy test matrix in 
contributors' forks and mirror only a single `Build`
    status check upstream, instead of running the matrix in the upstream repo 
on every
    PR. This matches the Apache Spark main repository.
    
    ### Does this PR introduce _any_ user-facing change?
    
    No. CI and documentation only.
    
    ### How was this patch tested?
    
    Static verification of the cross-workflow wiring: the `Run / License Check`
    check-run name, the shared `Build` check name, and the `run_id` passed from
    `notify` to `update`. Full behavior takes effect only after merge to 
`main`, since
    the `pull_request_target`/`schedule` workflows and `main`-pinned URLs 
resolve from
    the default branch.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Claude Opus 4.8)
    
    Closes #399 from dongjoon-hyun/SPARK-57151.
    
    Authored-by: Dongjoon Hyun <[email protected]>
    Signed-off-by: Dongjoon Hyun <[email protected]>
---
 .github/workflows/build_and_test.yml               |   5 +-
 .github/workflows/build_main.yml                   |  29 ++++
 .../workflows/images/workflow-enable-button.png    | Bin 0 -> 79807 bytes
 .github/workflows/notify_test_workflow.yml         | 172 +++++++++++++++++++++
 .github/workflows/update_build_status.yml          | 111 +++++++++++++
 AGENTS.md                                          |   6 +-
 README.md                                          |   2 +-
 7 files changed, 318 insertions(+), 7 deletions(-)

diff --git a/.github/workflows/build_and_test.yml 
b/.github/workflows/build_and_test.yml
index 0d3e283..5f4506f 100644
--- a/.github/workflows/build_and_test.yml
+++ b/.github/workflows/build_and_test.yml
@@ -20,10 +20,7 @@
 name: Build and test
 
 on:
-  push:
-    branches: [ "main" ]
-  pull_request:
-    branches: [ "main" ]
+  workflow_call:
 
 jobs:
   license-check:
diff --git a/.github/workflows/build_main.yml b/.github/workflows/build_main.yml
new file mode 100644
index 0000000..370da00
--- /dev/null
+++ b/.github/workflows/build_main.yml
@@ -0,0 +1,29 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+name: "Build"
+
+on:
+  push:
+    branches: [ "**" ]
+
+jobs:
+  call-build-and-test:
+    name: Run
+    uses: ./.github/workflows/build_and_test.yml
diff --git a/.github/workflows/images/workflow-enable-button.png 
b/.github/workflows/images/workflow-enable-button.png
new file mode 100644
index 0000000..f7299f2
Binary files /dev/null and 
b/.github/workflows/images/workflow-enable-button.png differ
diff --git a/.github/workflows/notify_test_workflow.yml 
b/.github/workflows/notify_test_workflow.yml
new file mode 100644
index 0000000..beeed62
--- /dev/null
+++ b/.github/workflows/notify_test_workflow.yml
@@ -0,0 +1,172 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+# Intentionally has a general name.
+# because the test status check created in GitHub Actions
+# currently randomly picks any associated workflow.
+# So, the name was changed to make sense in that context too.
+# See also 
https://github.community/t/specify-check-suite-when-creating-a-checkrun/118380/10
+name: On pull request update
+on:
+  pull_request_target:
+    types: [opened, reopened, synchronize]
+
+jobs:
+  notify:
+    name: Notify test workflow
+    runs-on: ubuntu-slim
+    permissions:
+      actions: read
+      checks: write
+    steps:
+      - name: "Notify test workflow"
+        uses: actions/github-script@v9
+        with:
+          github-token: ${{ secrets.GITHUB_TOKEN }}
+          script: |
+            const endpoint = 'GET 
/repos/:owner/:repo/actions/workflows/:id/runs?&branch=:branch'
+            const check_run_endpoint = 'GET 
/repos/:owner/:repo/commits/:ref/check-runs?per_page=100'
+
+            // TODO: Should use pull_request.user and 
pull_request.user.repos_url?
+            // If a different person creates a commit to another forked repo,
+            // it wouldn't be able to detect.
+            const params = {
+              owner: context.payload.pull_request.head.repo.owner.login,
+              repo: context.payload.pull_request.head.repo.name,
+              id: 'build_main.yml',
+              branch: context.payload.pull_request.head.ref,
+            }
+            const check_run_params = {
+              owner: context.payload.pull_request.head.repo.owner.login,
+              repo: context.payload.pull_request.head.repo.name,
+              ref: context.payload.pull_request.head.ref,
+            }
+
+            console.log('Ref: ' + context.payload.pull_request.head.ref)
+            console.log('SHA: ' + context.payload.pull_request.head.sha)
+
+            // Wait 3 seconds to make sure the fork repository triggered a 
workflow.
+            await new Promise(r => setTimeout(r, 3000))
+
+            let runs
+            try {
+              runs = await github.request(endpoint, params)
+            } catch (error) {
+              console.error(error)
+              // Assume that runs were not found.
+            }
+
+            const name = 'Build'
+            const head_sha = context.payload.pull_request.head.sha
+            let status = 'queued'
+
+            if (!runs || runs.data.workflow_runs.length === 0) {
+              status = 'completed'
+              const conclusion = 'action_required'
+
+              github.rest.checks.create({
+                owner: context.repo.owner,
+                repo: context.repo.repo,
+                name: name,
+                head_sha: head_sha,
+                status: status,
+                conclusion: conclusion,
+                output: {
+                  title: 'Workflow run detection failed',
+                  summary: `
+            Unable to detect the workflow run for testing the changes in your 
PR.
+
+            1. If you did not enable GitHub Actions in your forked repository, 
please enable it by clicking the button as shown in the image below. See also 
[Managing Github Actions Settings for a 
repository](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/enabling-features-for-your-repository/managing-github-actions-settings-for-a-repository)
 for more details.
+            2. It is possible your branch is based on the old \`main\` branch 
in Apache Spark Connect Client for Swift, please sync your branch to the latest 
main branch. For example as below:
+                \`\`\`bash
+                git fetch upstream
+                git rebase upstream/main
+                git push origin YOUR_BRANCH --force
+                \`\`\``,
+                  images: [
+                    {
+                      alt: 'enabling workflows button',
+                      image_url: 
'https://raw.githubusercontent.com/apache/spark-connect-swift/main/.github/workflows/images/workflow-enable-button.png'
+                    }
+                  ]
+                }
+              })
+            } else {
+              const run_id = runs.data.workflow_runs[0].id
+
+              if (runs.data.workflow_runs[0].head_sha != 
context.payload.pull_request.head.sha) {
+                throw new Error('There was a new unsynced commit pushed. 
Please retrigger the workflow.');
+              }
+
+              // Here we get check run ID to provide Check run view instead of 
Actions view, see also SPARK-37879.
+              let retryCount = 0;
+              let check_run_head;
+              while (retryCount < 3) {
+                const check_runs = await github.request(check_run_endpoint, 
check_run_params);
+                check_run_head = check_runs.data.check_runs.find(r => r.name 
=== "Run / License Check");
+                if (check_run_head) {
+                  break;
+                }
+                retryCount++;
+                if (retryCount < 3) {
+                  await new Promise(resolve => setTimeout(resolve, 3000));
+                }
+              }
+              if (!check_run_head) {
+                throw new Error('Failed to retrieve check_run_head after 3 
attempts');
+              }
+
+              if (check_run_head.head_sha != 
context.payload.pull_request.head.sha) {
+                throw new Error('There was a new unsynced commit pushed. 
Please retrigger the workflow.');
+              }
+
+              const check_run_url = 'https://github.com/'
+                + context.payload.pull_request.head.repo.full_name
+                + '/runs/'
+                + check_run_head.id
+              console.log('Check run URL: ' + check_run_url)
+
+              const actions_url = 'https://github.com/'
+                + context.payload.pull_request.head.repo.full_name
+                + '/actions/runs/'
+                + run_id
+              console.log('Actions URL: ' + actions_url)
+
+              github.rest.checks.create({
+                owner: context.repo.owner,
+                repo: context.repo.repo,
+                name: name,
+                head_sha: head_sha,
+                status: status,
+                output: {
+                  title: 'Test results',
+                  summary: '[See test results](' + check_run_url + ')\n\n'
+                    + 'If the tests fail for reasons unrelated to this pull 
request, '
+                    + 'please rerun the workflow in your forked repository.\n'
+                    + 'If the failures are related to this pull request, '
+                    + 'please investigate them and push follow-up changes.',
+                  text: JSON.stringify({
+                    owner: context.payload.pull_request.head.repo.owner.login,
+                    repo: context.payload.pull_request.head.repo.name,
+                    run_id: run_id
+                  })
+                },
+                details_url: actions_url,
+              })
+            }
diff --git a/.github/workflows/update_build_status.yml 
b/.github/workflows/update_build_status.yml
new file mode 100644
index 0000000..26ab78f
--- /dev/null
+++ b/.github/workflows/update_build_status.yml
@@ -0,0 +1,111 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+name: Update build status workflow
+
+on:
+  schedule:
+  - cron: "*/15 * * * *"
+
+jobs:
+  update:
+    name: Update build status
+    runs-on: ubuntu-slim
+    permissions:
+      actions: read
+      checks: write
+    steps:
+      - name: "Update build status"
+        uses: actions/github-script@v9
+        with:
+          github-token: ${{ secrets.GITHUB_TOKEN }}
+          script: |
+            const endpoint = 'GET /repos/:owner/:repo/pulls?state=:state'
+            const params = {
+              owner: context.repo.owner,
+              repo: context.repo.repo,
+              state: 'open'
+            }
+
+            // See 
https://docs.github.com/en/graphql/reference/enums#mergestatestatus
+            const maybeReady = ['behind', 'clean', 'draft', 'has_hooks', 
'unknown', 'unstable'];
+
+            // Iterate open PRs
+            for await (const prs of github.paginate.iterator(endpoint,params)) 
{
+              // Each page
+              for await (const pr of prs.data) {
+                console.log('SHA: ' + pr.head.sha)
+                console.log('  Mergeable status: ' + pr.mergeable_state)
+                if (pr.mergeable_state == null || 
maybeReady.includes(pr.mergeable_state)) {
+                  const checkRuns = await github.request('GET 
/repos/{owner}/{repo}/commits/{ref}/check-runs', {
+                    owner: context.repo.owner,
+                    repo: context.repo.repo,
+                    ref: pr.head.sha
+                  })
+
+                  // Iterator GitHub Checks in the PR
+                  for await (const cr of checkRuns.data.check_runs) {
+                    if (cr.name == 'Build' && cr.conclusion != 
"action_required") {
+                      // text contains parameters to make request in JSON.
+                      const params = JSON.parse(cr.output.text)
+
+                      // Get the workflow run in the forked repository
+                      let run
+                      try {
+                        run = await github.request('GET 
/repos/{owner}/{repo}/actions/runs/{run_id}', params)
+                      } catch (error) {
+                        console.error(error)
+                        // Run not found. This can happen when the PR author 
removes GitHub Actions runs or
+                        // disables GitHub Actions.
+                        continue
+                      }
+
+                      // Keep syncing the status of the checks
+                      if (run.data.status == 'completed') {
+                        console.log('    Run ' + cr.id + ': set status (' + 
run.data.status + ') and conclusion (' + run.data.conclusion + ')')
+                        const response = await github.request('PATCH 
/repos/{owner}/{repo}/check-runs/{check_run_id}', {
+                          owner: context.repo.owner,
+                          repo: context.repo.repo,
+                          check_run_id: cr.id,
+                          output: cr.output,
+                          status: run.data.status,
+                          conclusion: run.data.conclusion,
+                          details_url: run.data.details_url
+                        })
+                      } else {
+                        // PATCH /check-runs accepts only queued | in_progress 
| completed,
+                        // but workflow_run may also report requested / 
waiting / pending.
+                        const status = run.data.status == 'in_progress' ? 
'in_progress' : 'queued'
+                        console.log('    Run ' + cr.id + ': set status (' + 
run.data.status + ' -> ' + status + ')')
+                        const response = await github.request('PATCH 
/repos/{owner}/{repo}/check-runs/{check_run_id}', {
+                          owner: context.repo.owner,
+                          repo: context.repo.repo,
+                          check_run_id: cr.id,
+                          output: cr.output,
+                          status: status,
+                          details_url: run.data.details_url
+                        })
+                      }
+
+                      break
+                    }
+                  }
+                }
+              }
+            }
diff --git a/AGENTS.md b/AGENTS.md
index 9ef64b4..955682e 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -46,8 +46,10 @@ catalog, ML) over gRPC, exchanging results in Apache Arrow 
format.
 - `Examples/` — runnable sample apps (`pi`, `spark-sql`, `stream`, `web`, 
`app`,
   `pyspark-connect`), each with its own `Package.swift` and `Dockerfile`.
 - `dev/` — Python maintainer scripts (JIRA + PR merge tooling).
-- `.github/workflows/build_and_test.yml` — CI: license check, multi-platform
-  build, and integration tests.
+- `.github/workflows/build_main.yml` — CI entry point: runs on push to all
+  branches and calls the reusable `build_and_test.yml` (license check,
+  multi-platform build, integration tests). `notify_test_workflow.yml` and
+  `update_build_status.yml` mirror forked-repo CI results onto PR checks.
 
 ## Build
 
diff --git a/README.md b/README.md
index 9833b0f..ea98bb8 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
 # Apache Spark Connect Client for Swift
 
 
[![Release](https://img.shields.io/github/v/release/apache/spark-connect-swift)](https://github.com/apache/spark-connect-swift/releases/latest)
-[![GitHub Actions 
Build](https://github.com/apache/spark-connect-swift/actions/workflows/build_and_test.yml/badge.svg)](https://github.com/apache/spark-connect-swift/blob/main/.github/workflows/build_and_test.yml)
+[![GitHub Actions 
Build](https://github.com/apache/spark-connect-swift/actions/workflows/build_main.yml/badge.svg)](https://github.com/apache/spark-connect-swift/blob/main/.github/workflows/build_main.yml)
 [![Swift Version 
Compatibility](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fapache%2Fspark-connect-swift%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/apache/spark-connect-swift)
 [![Platform 
Compatibility](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fapache%2Fspark-connect-swift%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/apache/spark-connect-swift)
 


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to