Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/labeler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
141 changes: 123 additions & 18 deletions .github/workflows/labeler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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/pr-size-labeler@v1.10.3
# 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/
90 changes: 90 additions & 0 deletions .github/workflows/pr_labeler.yml
Original file line number Diff line number Diff line change
@@ -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
});
Loading