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

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


The following commit(s) were added to refs/heads/master by this push:
     new 4e9ab20b6ac github/workflow: Reimplement PR Labeling without 
pull_request_target
4e9ab20b6ac is described below

commit 4e9ab20b6ac26aaef6e4dd0e50b2bc4cc1e5e451
Author: Lup Yuen Lee <[email protected]>
AuthorDate: Tue Feb 10 09:46:49 2026 +0800

    github/workflow: Reimplement PR Labeling without pull_request_target
    
    ASF Infrastructure Team has flagged a GitHub Actions workflow policy 
violation, inside our PR Labeling. We must remove pull_request_target before 6 
Apr 2026, or ASF Infra will turn off all GitHub Builds: 
https://github.com/apache/nuttx/issues/18359
    
    This PR reimplements the 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.
    
    GitHub Actions `codelytv/pr-size-labeler` and `actions/labeler` don't work 
with the pull_request trigger, so we replaced them with our own code. The 
implementation is explained here: 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. Arch: arm). 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`
    
    ### 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    | 138 ++++++++++++++++++++++++++++++++++-----
 .github/workflows/pr_labeler.yml |  90 +++++++++++++++++++++++++
 3 files changed, 213 insertions(+), 18 deletions(-)

diff --git a/.github/labeler.yml b/.github/labeler.yml
index bd778f9ba2a..699f48b217f 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 1c38d38da6e..da967babdae 100644
--- a/.github/workflows/labeler.yml
+++ b/.github/workflows/labeler.yml
@@ -12,34 +12,136 @@
 # 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
+          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, pattern });
+
+              } 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 00000000000..f8af53e0501
--- /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