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

lupyuen pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/nuttx-apps.git


The following commit(s) were added to refs/heads/master by this push:
     new fe546acfc github/workflow: Sync the new PR Labeling Workflow from 
NuttX Repo to NuttX Apps
fe546acfc is described below

commit fe546acfc7c50bb2af89d8fc1c032952d3ab7616
Author: Lup Yuen Lee <[email protected]>
AuthorDate: Mon Feb 23 14:10:37 2026 +0800

    github/workflow: Sync the new PR Labeling Workflow from NuttX Repo to NuttX 
Apps
    
    This PR replicates the new PR Labeling Workflow from NuttX Repo to NuttX 
Apps Repo. For Future Syncing:
    - Copy from NuttX Repo to NuttX Apps: `.github/workflows/labeler.yml` and 
`.github/workflows/pr_labeler.yml`
    - Edit `.github/workflows/labeler.yml` and change `repository: 
apache/nuttx` to `repository: apache/nuttx-apps`
    - Don't overwrite `.github/labeler.yml` by NuttX Repo
    
    The new workflow reimplements PR Labeling with two triggers: pull_request 
and workflow_run. We no longer need pull_request_target, which is an unsafe 
trigger and may introduce security vulnerabilities.
    
    The New PR Labeler is explained here:
    - https://lupyuen.org/articles/prtarget
    - https://github.com/apache/nuttx/issues/18359
    
    Modified Files:
    
    `.github/workflows/labeler.yml`: Changed the (read-write) 
pull_request_target trigger to (read-only) pull_request trigger. Compute the 
Size Label (e.g. Size: XS) and Arch Labels (e.g. Area: Examples). Save the PR 
Labels into a PR Artifact.
    
    `.github/labeler.yml`: Added comment to clarify that NuttX PR Labeler only 
supports a subset of the `actions/labeler` syntax: `changed-files` and 
`any-glob-to-any-file`. Note: Don't overwrite this file by NuttX Repo.
    
    New Files:
    
    `.github/workflows/pr_labeler.yml`: Contains the workflow_run trigger, 
which is executed upon completion of the pull_request trigger. Download the PR 
Labels from the PR Artifact. Write the PR Labels into the PR.
    
    Signed-off-by: Lup Yuen Lee <[email protected]>
---
 .github/labeler.yml              |   3 +
 .github/workflows/labeler.yml    | 141 ++++++++++++++++++++++++++++++++++-----
 .github/workflows/pr_labeler.yml |  90 +++++++++++++++++++++++++
 3 files changed, 216 insertions(+), 18 deletions(-)

diff --git a/.github/labeler.yml b/.github/labeler.yml
index 88336e8ae..278cc551a 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -17,6 +17,9 @@
 # specific language governing permissions and limitations
 # under the License.
 #
+# Note: NuttX PR Labeler only supports a subset of the
+# `actions/labeler` syntax: `changed-files` and
+# `any-glob-to-any-file`. See .github/workflows/labeler.yml
 
 # add arch labels
 
diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml
index 1c38d38da..bf1c459ec 100644
--- a/.github/workflows/labeler.yml
+++ b/.github/workflows/labeler.yml
@@ -12,34 +12,139 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 #
+# This workflow will fetch the updated PR filenames, compute the Size Label
+# and Arch Labels, then save the PR Labels into a PR Artifact. The
+# PR Labels will be written to the PR inside the "workflow_run" trigger
+# (pr_labeler.yml), because this "pull_request" trigger has read-only
+# permission. Don't use "pull_request_target", it's unsafe.
+# See 
https://cwiki.apache.org/confluence/pages/viewpage.action?pageId=321719166#GitHubActionsSecurity-Buildstriggeredwithpull_request_target
 name: "Pull Request Labeler"
 on:
-  - pull_request_target
+  - pull_request
 
 jobs:
   labeler:
     permissions:
       contents: read
-      pull-requests: write
-      issues: write
+      pull-requests: read
+      issues: read
     runs-on: ubuntu-latest
     steps:
-      - name: Checkout repository
-        uses: actions/checkout@v6
+      # Checkout one file from our trusted source: .github/labeler.yml
+      # Never checkout and execute any untrusted code from the PR.
+      - name: Checkout labeler config
+        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # 
v6.0.2
+        with:
+          repository: apache/nuttx-apps
+          ref: master
+          path: labeler
+          fetch-depth: 1
+          persist-credentials: false
+          sparse-checkout: .github/labeler.yml
+          sparse-checkout-cone-mode: false
 
-      - name: Assign labels based on paths
-        uses: actions/labeler@main
+      # Fetch the updated PR filenames. Compute the Size Label and Arch Labels.
+      - name: Compute PR labels
+        uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd  
# v8.0.0
         with:
-          repo-token: "${{ secrets.GITHUB_TOKEN }}"
-          sync-labels: true
+          github-token: ${{ secrets.GITHUB_TOKEN }}
+          script: |
+            const owner = context.repo.owner;
+            const repo = context.repo.repo;
+            const pull_number = context.issue.number;
+
+            // Fetch the array of updated PR filenames:
+            // { status: 'added',    filename: 'arch/arm/test.txt',            
  additions: 3, deletions: 0,    changes: 3 }
+            // { status: 'removed',  filename: 
'Documentation/legacy_README.md', additions: 0, deletions: 2531, changes: 2531 }
+            // { status: 'modified', filename: 'Documentation/security.rst',   
  additions: 1, deletions: 0,    changes: 1 }
+            const listFilesOptions = github.rest.pulls.listFiles
+              .endpoint.merge({ owner, repo, pull_number });
+            const listFilesResponse = await github.paginate(listFilesOptions);
+
+            // Sum up the number of lines changed
+            const sizeFiles = listFilesResponse
+              .filter(f => (f.status != 'removed'));  // Ignore deleted files
+            var linesChanged = 0;
+            for (const file of sizeFiles) {
+              linesChanged += file.changes;
+            }
+            console.log({ linesChanged });
+
+            // Compute the Size Label
+            const sizeLabel =
+              (linesChanged   <=   10) ? 'Size: XS'
+              : (linesChanged <=  100) ? 'Size: S'
+              : (linesChanged <=  500) ? 'Size: M'
+              : (linesChanged <= 1000) ? 'Size: L'
+              : 'Size: XL';
+            var prLabels = [ sizeLabel ];
+
+            // Parse the Arch Label Patterns in .github/labeler.yml. Condense 
into:
+            // "Arch: arm":
+            // - any-glob-to-any-file: 'arch/arm/**'
+            // - any-glob-to-any-file: ...
+            const fs = require('fs');
+            const config = fs.readFileSync('labeler/.github/labeler.yml', 
'utf8')
+              .split('\n')             // Split by newline
+              .map(s => s.trim())      // Remove leading and trailing spaces
+              .filter(s => (s != ''))  // Remove empty lines
+              .filter(s => !s.startsWith('#'))                  // Remove 
comments
+              .filter(s => !s.startsWith('- changed-files:'));  // Remove 
"changed-files"
+
+            // Convert the Arch Label Patterns from config to archLabels.
+            // archLabels will contain the mappings for Arch Label and 
Filename Pattern:
+            // { label: "Arch: arm",   pattern: "arch/arm/.*"   },
+            // { label: "Arch: arm64", pattern: "arch/arm64/.*" }, ...
+            var archLabels = [];
+            var label = "";
+            for (const c of config) {
+              // Get the Arch Label
+              if (c.startsWith('"')) {    // "Arch: arm":
+                label = c.split('"')[1];  // Arch: arm
+
+              } else if (c.startsWith('- any-glob-to-any-file:')) {  // - 
any-glob-to-any-file: 'arch/arm/**'
+                // Convert the Glob Pattern to Regex Pattern
+                const pattern = c.split("'")[1]      // arch/arm/**
+                  .split('.').join('\\.')            // .  becomes \.
+                  .split('*').join('[^/]*')          // *  becomes [^/]*
+                  .split('[^/]*[^/]*').join('.*');   // ** becomes .*
+                archLabels.push({
+                  label,                        // Arch: arm
+                  pattern: '^' + pattern + '$'  // Match the Line Start and 
Line End
+                });
+
+              } else {
+                // We don't support all rules of `actions/labeler`
+                throw new Error('.github/labeler.yml should contain only 
changed-files and any-glob-to-any-file, not: ' + c);
+              }
+            }
+
+            // Search the filenames for matching Arch Labels
+            for (const archLabel of archLabels) {
+              if (prLabels.includes(archLabel.label)) {
+                break;
+              }
+              for (const file of listFilesResponse) {
+                const re = new RegExp(archLabel.pattern);
+                const match = re.test(file.filename);
+                if (match && !prLabels.includes(archLabel.label)) {
+                  prLabels.push(archLabel.label);
+                  break;
+                }
+              }
+            }
+            console.log({ prLabels });
+
+            // Save the PR Number and PR Labels into a PR Artifact
+            // e.g. 'Size: XS\nArch: avr\n'
+            const dir = 'pr';
+            fs.mkdirSync(dir);
+            fs.writeFileSync(dir + '/pr-id.txt', pull_number + '\n');
+            fs.writeFileSync(dir + '/pr-labels.txt', prLabels.join('\n') + 
'\n');
 
-      - name: Assign labels based on the PR's size
-        uses: codelytv/[email protected]
+      # Upload the PR Artifact as pr.zip
+      - name: Upload PR artifact
+        uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f 
 # v6.0.0
         with:
-          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-          ignore_file_deletions: true
-          xs_label: 'Size: XS'
-          s_label: 'Size: S'
-          m_label: 'Size: M'
-          l_label: 'Size: L'
-          xl_label: 'Size: XL'
+          name: pr
+          path: pr/
diff --git a/.github/workflows/pr_labeler.yml b/.github/workflows/pr_labeler.yml
new file mode 100644
index 000000000..f8af53e05
--- /dev/null
+++ b/.github/workflows/pr_labeler.yml
@@ -0,0 +1,90 @@
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed 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.
+#
+# This workflow will fetch the PR Labels from the PR Artifact, and write
+# the PR Labels into the PR. The workflow is called after the
+# "pull_request" trigger (labeler.yml). This "workflow_run" trigger uses a
+# GitHub Token with Write Permission, so we must never run any untrusted
+# code from the PR, and we must always extract and use the PR Artifact
+# safely. See 
https://cwiki.apache.org/confluence/pages/viewpage.action?pageId=321719166#GitHubActionsSecurity-Buildstriggeredwithworkflow_run
+name: "Set Pull Request Labels"
+on:
+  workflow_run:
+    workflows: ["Pull Request Labeler"]
+    types:
+      - completed
+
+jobs:
+  pr_labeler:
+    permissions:
+      contents: read
+      pull-requests: write
+      issues: write
+    runs-on: ubuntu-latest
+    if: >
+      github.event.workflow_run.event == 'pull_request' &&
+      github.event.workflow_run.conclusion == 'success'
+    steps:
+      # Download the PR Artifact, containing PR Number and PR Labels
+      - name: Download PR artifact
+        uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd  
# v8.0.0
+        with:
+          script: |
+            const artifacts = await 
github.rest.actions.listWorkflowRunArtifacts({
+              owner: context.repo.owner,
+              repo: context.repo.repo,
+              run_id: ${{ github.event.workflow_run.id }},
+            });
+            const matchArtifact = artifacts.data.artifacts.filter((artifact) 
=> {
+              return artifact.name == "pr"
+            })[0];
+            const download = await github.rest.actions.downloadArtifact({
+              owner: context.repo.owner,
+              repo: context.repo.repo,
+              artifact_id: matchArtifact.id,
+              archive_format: 'zip',
+            });
+            const fs = require('fs');
+            fs.writeFileSync('${{github.workspace}}/pr.zip', 
Buffer.from(download.data));
+
+      # Unzip the PR Artifact
+      - name: Unzip PR artifact
+        run: unzip pr.zip
+
+      # Write the PR Labels into the PR
+      - name: Write PR labels
+        uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd  
# v8.0.0
+        with:
+          github-token: ${{ secrets.GITHUB_TOKEN }}
+          script: |
+            const owner = context.repo.owner;
+            const repo = context.repo.repo;
+            const fs = require('fs');
+
+            // Read the PR Number and PR Labels from the PR Artifact
+            // e.g. 'Size: XS\nArch: avr\n'
+            const issue_number = Number(fs.readFileSync('pr-id.txt'));
+            const labels = fs.readFileSync('pr-labels.txt', 'utf8')
+              .split('\n')              // Split by newline
+              .filter(s => (s != ''));  // Remove empty lines
+            console.log({ issue_number, labels });
+
+            // Write the PR Labels into the PR
+            // e.g. [ 'Size: XS', 'Arch: avr' ]
+            await github.rest.issues.setLabels({
+              owner,
+              repo,
+              issue_number,
+              labels
+            });

Reply via email to