Skip to content

⚡ PR › Autoscan

⚡ PR › Autoscan #4

Workflow file for this run

# ---------------------------------------------------------------------------------------
# @parent : github workflow
# @desc : pull request autoscan
# @author : Aetherinox
# @url : https://github.com/Aetherinox
# ---------------------------------------------------------------------------------------
name: "⚡ PR › Autoscan"
run-name: "⚡ PR › Autoscan"
# ---------------------------------------------------------------------------------------
# triggers
# ---------------------------------------------------------------------------------------
on:
pull_request_target:
branches:
- main
- master
# ---------------------------------------------------------------------------------------
# environment variables
# ---------------------------------------------------------------------------------------
env:
LABEL_CHECK_CHANGES_REQ: AC ✦ Changes Required
LABEL_CHECK_REVIEW_READY: AC ✦ Passed
LABEL_CHECK_REBASE_REQ: AC ✦ Needs Rebase
LABEL_CHECK_SECURITY_ERR: AC ✦ Security Warning
LABEL_CHECK_STATUS_CHGMADE: AC ✦ Changes Made
LABEL_CHECK_STATUS_FAILED: AC ✦ Failed
LABEL_CHECK_SCAN_SKIPPED: AC ✦ Skipped Scan
LABEL_TYPE_PR: Type ◦ Pull Request
LABEL_TYPE_DEPENDENCY: Type ◦ Dependency
LABEL_TYPE_GITACTION: Type ◦ Git Action
ASSIGN_USER: Aetherinox
BOT_NAME_1: AdminServ
BOT_NAME_2: AdminServX
BOT_NAME_DEPENDABOT: dependabot[bot]
LABELS_JSON: |
[
{ "name": "AC ✦ Failed", "color": "d73a4a", "description": "Autocheck failed to run through a complete cycle, requires investigation" },
{ "name": "AC ✦ Passed", "color": "ccb11d", "description": "Ready to be reviewed" },
{ "name": "AC ✦ Changes Required", "color": "8F1784", "description": "Requires changes to be made to the package before being accepted" },
{ "name": "AC ✦ Review Required", "color": "8F1784", "description": "PR needs to be reviewed by another person, after the requested changes have been made" },
{ "name": "AC ✦ Needs Rebase", "color": "8F1784", "description": "Due to the permissions on the requesting repo, this pull request must be rebased by the author" },
{ "name": "AC ✦ Security Warning", "color": "761620", "description": "Does not conform to developer policies, or includes potentially dangerous code" },
{ "name": "AC ✦ Changes Made", "color": "8F1784", "description": "Requested changes have been made and are pending a re-scan" },
{ "name": "AC ✦ Skipped Scan", "color": "8F1784", "description": "Author has skipped code scan" },
{ "name": "Type ◦ Pull Request", "color": "8F1784", "description": "Normal pull request" },
{ "name": "Type ◦ Dependency", "color": "243759", "description": "Item is associated to dependency" }
]
# ---------------------------------------------------------------------------------------
# jobs
# ---------------------------------------------------------------------------------------
jobs:
pr-autoscan:
runs-on: ubuntu-latest
permissions:
contents: read
actions: read
issues: write
pull-requests: read
steps:
# ---------------------------------------------------------------------------------------
# action needed if using 'pull_request' and 'issue_comment'
# to get the pull request, you would normally use ${{ github.event.number }}
# however this isnt available for 'issue_comment'
# ---------------------------------------------------------------------------------------
- name: "🏷️ Verify Existing Labels"
id: task_autocheck_labels_verify
uses: actions/github-script@v7
with:
github-token: ${{ secrets.ADMINSERV_TOKEN_CL || github.token }}
script: |
const labels = JSON.parse( process.env.LABELS_JSON );
for ( const label of labels )
{
try
{
await github.rest.issues.createLabel(
{
owner: context.repo.owner,
repo: context.repo.repo,
name: label.name,
description: label.description || '',
color: label.color
});
}
catch ( err )
{
if ( err.status === 422 )
{
console.log( `Label '${label.name}' already exists. Skipping.` );
}
else
{
console.error( `Error creating label '${label.name}': ${err}` );
}
}
}
# ---------------------------------------------------------------------------------------
# set issue number
# ---------------------------------------------------------------------------------------
- name: "#️⃣ Issue number › Set"
uses: actions/github-script@v7
id: task_autocheck_issue_num_set
with:
github-token: ${{ secrets.ADMINSERV_TOKEN_CL || github.token }}
script: |
if ( context.issue.number )
{
// Return issue number if present
return context.issue.number;
}
else
{
// Otherwise return issue number from commit
return (
await github.rest.repos.listPullRequestsAssociatedWithCommit(
{
commit_sha: context.sha,
owner: context.repo.owner,
repo: context.repo.repo,
})
).data[ 0 ].number;
}
result-encoding: string
# ---------------------------------------------------------------------------------------
# print issue number
# ---------------------------------------------------------------------------------------
- name: "#️⃣ Issue number › Print"
id: task_autocheck_issue_num_get
run: |
echo '${{ steps.task_autocheck_issue_num_set.outputs.result }}'
# ---------------------------------------------------------------------------------------
# checkout
# ---------------------------------------------------------------------------------------
- name: "☑️ Checkout"
id: task_autoscan_checkout
uses: actions/checkout@v4
if: |
( github.event_name == 'pull_request_target' ) || ( github.event_name == 'pull_request' ) || ( github.event_name == 'issue_comment' && contains( github.event.comment.html_url, '/pull/' ) && contains( github.event.comment.body, '/rescan' ) )
with:
fetch-depth: 0
ref: "refs/pull/${{ steps.task_autocheck_issue_num_set.outputs.result }}/merge"
# ---------------------------------------------------------------------------------------
# nodejs
# ---------------------------------------------------------------------------------------
- name: "⚙️ Setup Node"
id: task_autocheck_nodejs
uses: actions/setup-node@v4
# ---------------------------------------------------------------------------------------
# get list of changed files
# ---------------------------------------------------------------------------------------
- name: Get changed files
id: task_autocheck_changed_files_get
uses: tj-actions/changed-files@v44
with:
separator: ","
# ---------------------------------------------------------------------------------------
# list of changed files
# ---------------------------------------------------------------------------------------
- name: "📄 List all added files"
id: task_autocheck_added_files_get
run: |
for file in ${CHANGED_FILES}; do
echo "$file was changed"
done
env:
ADDED_FILES: ${{ steps.task_autocheck_changed_files_get.outputs.added_files }}
MODIFIED_FILES: ${{ steps.task_autocheck_changed_files_get.outputs.modified_files }}
CHANGED_FILES: ${{ steps.task_autocheck_changed_files_get.outputs.all_changed_files }}
COUNT_ADDED: ${{ steps.task_autocheck_changed_files_get.outputs.added_files_count }}
COUNT_MODIFIED: ${{ steps.task_autocheck_changed_files_get.outputs.modified_files_count }}
COUNT_DELETED: ${{ steps.task_autocheck_changed_files_get.outputs.deleted_files_count }}
COUNT_RENAMED: ${{ steps.task_autocheck_changed_files_get.outputs.renamed_files_count }}
COUNT_COPIED: ${{ steps.task_autocheck_changed_files_get.outputs.copied_files_count }}
- name: "📂 List Directories"
id: task_autocheck_dirs_list
run: |
ls
# ---------------------------------------------------------------------------------------
# Run autocheck
# ---------------------------------------------------------------------------------------
- name: "☑️ Run Autocheck"
id: task_autocheck_run
uses: actions/github-script@v7
with:
github-token: ${{ secrets.ADMINSERV_TOKEN_CL || github.token }}
script: |
const fs = require( 'fs' );
const escape_html = ( unsafe ) => unsafe.replace( /&/g, '&amp;' ).replace( /</g, '&lt;' ).replace( />/g, '&gt;' ).replace( /"/g, '&quot;' ).replace( /'/g, '&#039;' );
const labels = [];
const files_List = `${{ steps.task_autocheck_changed_files_get.outputs.all_changed_files }}` || ''
const files_Array = files_List.split(',')
const branch_ref = `${ context.payload.pull_request.head.ref }`
let message = [ "\n<br />\n" ]
message.push ( "## Automatic Self-Check - #" + context.issue.number + "\n" );
message.push ( `The details of our automated scan for your pull request are listed below. If our scan detected errors, they must be corrected before this pull request will be advanced to the review stage:\n` );
message.push ( "\n<br />\n\n---\n\n<br />\n\n" );
message.push ( "### About\nThis pull request includes the following information:" );
let bHasError = false;
let bHasWarning = false;
let date = new Date( `${ context.payload.pull_request.created_at }` );
date.toISOString( )
const actor = '${{ github.actor }}';
const dateTimeformat = ( date ) =>
{
let month = date.getMonth( ) + 1;
month = month.toString( ).padStart( 2, '0' );
let day = date.getDate( ).toString( ).padStart( 2, '0' );
let year = date.getFullYear( ).toString( ).padStart( 2, '0' );
let hours = date.getHours();
let minutes = date.getMinutes();
let x = hours >= 12 ? 'PM' : 'AM';
hours = hours % 12;
hours = hours ? hours : 12;
minutes = minutes.toString( ).padStart( 2, '0' );
let mergeTime = month + '.' + day + '.' + year + ' ' + hours + ':' + minutes + ' ' + x;
return mergeTime;
}
let date_created = dateTimeformat( date ) + " UTC";
/*
context.payload.pull_request.base.repo.owner.login
*/
let md_table =
`
| Category | Value |
| --- | --- |
| Title | [ ` + context.payload.pull_request.title + ` ](https://github.com/` + context.repo.owner + `/` + context.repo.repo + `/pull/` + context.payload.pull_request.number + `) |
| Created | [ ` + date_created + ` ](https://worldtimebuddy.com) |
| ID | ` + context.payload.pull_request.html_url + ` |
| Author | [ ` + context.payload.pull_request.user.login + ` ](https://github.com/` + context.repo.owner + `/) |
| Repo | [ ` + context.repo.repo + ` ](https://github.com/` + context.repo.owner + `/` + context.repo.repo + `) |
| Branch | [ ` + context.payload.pull_request.head.ref + `](https://github.com/` + context.repo.owner + `/` + context.repo.repo + `/tree/` + context.payload.pull_request.head.ref + `) ⇁ [ ` + context.payload.pull_request.base.ref + `](https://github.com/` + context.repo.owner + `/` + context.repo.repo + `/tree/` + context.payload.pull_request.base.ref + `) |
| Added Files | ${{ steps.task_autocheck_changed_files_get.outputs.added_files_count }} |
| Modified Files | ${{ steps.task_autocheck_changed_files_get.outputs.all_modified_files_count }} |
| Renamed Files | ${{ steps.task_autocheck_changed_files_get.outputs.renamed_files_count }} |
| Copied Files | ${{ steps.task_autocheck_changed_files_get.outputs.deleted_files_count }} |
| Deleted Files | ${{ steps.task_autocheck_changed_files_get.outputs.deleted_files_count }} |
`;
message.push ( md_table );
let error_Generic = "\n" +
"- `MyPlugin`\n" +
"- `MyPluginSettings`\n" +
"- `SampleSettings`\n" +
"- `SampleSettingTab`\n" +
"- `SampleModal`\n"
let warn_BadWords = "\n" +
"- `General`\n" +
"- `Settings`\n"
/*
Loop files
*/
const files_skipped = [];
/*
List of files to skip check
Entries are CASE sensitive
For folders, append / at the end of the parent directory
*/
const type_dependency =
[
"dependabot/npm_and_yarn"
];
const type_gitaction =
[
"dependabot/github_actions"
];
const files_skipList =
[
".github",
".gitea",
".gitignore",
"LICENSE",
".md",
".yml",
"plugins.json",
"package.json",
"package-lock.json",
"rollup.config.js",
"index.js",
"Docs/",
"tests/"
];
for ( const file of files_Array )
{
const errors = [];
const addError = ( error ) =>
{
errors.push ( `:x: ${error}` );
console.log ( 'Found Issues: ' + error );
bHasError = true;
};
const warnings = [];
const addWarning = ( warning ) =>
{
warnings.push ( `:warning: ${warning}` );
console.log ( 'Found Warnings: ' + warning );
bHasWarning = true;
}
/*
Regex Searches
*/
const file_current = file;
const filesData = fs.readFileSync( file_current, 'utf8' );
const bContainsStyle = /([A-Za-z]+\.style\.[A-Za-z]+)/gi.test( filesData );
const bFuncFetch = /(fetch)\((.*)\)(\[([^\]]*)\])?/gim.test( filesData );
const bVar = /^(?:var|)\s(\w+)\s*=\s*/gm.test( filesData );
const bLookBehind = /\(\?<[=!].*?\)/gmi.test( filesData );
const bMarkdownHtmlNode = /new\s+NodeHtmlMarkdown/gmi.test( filesData );
const bAsTFile = /as\s+TFile/g.test( filesData );
const bAsTFolder = /as\s+TFolder/g.test( filesData );
const bAsAny = /\((.*? as Any\s*)\)/gi.test( filesData );
const bInnerHTML = /^\s?.*[a-zA-Z0-9_]+\.innerHTML*\s?.*$/gm.test( filesData );
const bOuterHTML = /^\s?.*[a-zA-Z0-9_]+\.outerHTML*\s?.*$/gm.test( filesData );
const bFuncConsoleLog = /(console.log)\((.*)\)(\[([^\]]*)\])?/gim.test( filesData );
const bFuncSetTimeout = /(setTimeout)\((.*)\)(\[([^\]]*)\])?/gim.test( filesData );
const bFuncFS_Chk1 = /(require)\s?\((\s?(?:'|")fs(?:'|"))\s?\)?/gim.test( filesData );
const bFuncFS_Chk2 = /from\s+(?:'|")fs(?:'|")\s?/gim.test( filesData );
const bFuncFS_ExistsSync = /(fs.existsSync)\((.*)\)(\[([^\]]*)\])?/gm.test( filesData );
const bFuncFS_MkdirSync = /(fs.mkdirSync)\((.*)\)(\[([^\]]*)\])?/gm.test( filesData );
const bFoundBadWord = /(?:'|").*(Settings|General).*(?:'|")?/gmi.test( filesData );
const bContainsGeneric = /(?:^|(?<= ))(MyPlugin|MyPluginSettings|SampleSettings|SampleSettingTab|SampleModal|Sample Plugin|my-plugin)(?:(?= )|$)/gim.test( filesData );
const check_depGetUnpinnedLeaf = "app.workspace.getUnpinnedLeaf"
const bFileSkip = files_skipList.some( s => s.includes( file_current ) || file_current.includes( s ) );
if ( bFileSkip == true )
{
files_skipped.push( file_current );
continue;
}
/*
Header
*/
message.push ( "\n<br />\n\n---\n\n<br />\n" );
message.push ( "### 📄 " + file_current + "\n" );
message = message.concat( warnings );
/*
Skip File
all contents in the array below will be skipped.
E.g: any file which resides in the .github folder will be skipped.
any file which ends in .yml will be skipped.
*/
/*
Using inline style
*/
if ( bContainsStyle == true )
{
addError( "Avoid assigning `inline styles` via JavaScript or in HTML. Move these styles to CSS so that they are adaptable by themes and other plugins." );
}
/*
Using var
*/
if ( bVar == true )
{
addError( "Change all instances of `var` to **const** or **let**. var has function-level scope, and leads to bugs." );
}
/*
Using lookbehind
*/
if ( bLookBehind == true )
{
addError( "Lookbehinds are not supported in iOS < 16.4" );
}
/*
As TFile
*/
if ( bAsTFile == true )
{
addError( "Do not cast `as TFile`, use `instanceof` instead to check if the item is actually a file / folder" );
}
/*
As TFolder
*/
if ( bAsTFolder == true )
{
addError( "Do not cast `as TFolder`, use `instanceof` instead to check if the item is actually a file / folder" );
}
/*
Casting to Any
*/
if ( bAsAny == true )
{
addError( "Do not cast to `Any`" );
}
/*
innerHTML
*/
if ( bInnerHTML == true )
{
addError( `Using \`innerHTML\` is a security risk.` );
}
/*
outerHTML
*/
if ( bOuterHTML == true )
{
addError( `Using \`outerHTML\` is a security risk.` );
}
/*
require("fs")
*/
if ( bFuncFS_Chk1 == true || bFuncFS_Chk2 == true )
{
addError( "`fs` import only available from Node.js runtime, this will throw errors for users running on mobile" );
}
/*
require("fs") / fs.existsSync
*/
if ( bFuncFS_ExistsSync == true )
{
addError( "`fs` import only available from Node.js runtime, this will throw errors for users running on mobile." );
}
/*
require("fs") / fs.mkdirSync
*/
if ( bFuncFS_MkdirSync == true )
{
addError( "`fs` import only available from Node.js runtime, this will throw errors for users running on mobile." );
}
/*
Generic Calls
*/
if ( bContainsGeneric == true )
{
addError( "Rename sample classes to something that makes sense. You are not allowed to have names such as: " + error_Generic );
}
/*
console.log found
*/
if ( bFuncConsoleLog == true )
{
addWarning( "Avoid unnecessary logging or ensure logging only occurs in development environment." );
}
/*
Bad words found
*/
if ( bFoundBadWord == true && file != "package.json" && file != "manifest.json" )
{
addWarning( "A restricted word was found in your code. Generic words are not allowed in strings such as: " + warn_BadWords );
}
if ( errors.length > 0 || warnings.length > 0 )
{
/*
Errors
*/
if ( errors.length > 0 )
{
message.push ( "\n\n\n> [!CAUTION]\n> Errors must be fixed prior to a pull request being reviewed and accepted.<br />The file `" + file + "` contains the following errors:\n\n<br>\n\n" );
message = message.concat( errors );
}
/*
Warnings
*/
if ( warnings.length > 0 )
{
if ( errors.length > 0 )
{
message.push ( "\n<br />\n<br />\n" )
}
message.push ( "\n\n\n> [!WARNING]\n> Warnings are suggestions that do not require fixing, but are recommended before this pull request is reviewed and accepted.<br />The file `" + file + "` contains the following warnings:\n\n<br>\n\n" );
message = message.concat( warnings );
}
}
else
{
message.push ( "\n\n\n> [!NOTE]\n> The file `" + file + "` contains no errors\n\n<br>\n\n" );
}
}
if ( files_skipped.length > 0 )
{
message.push ( "\n<br />\n\n---\n<br />\n" );
message.push ( "### ❌ Skipped Files\n" );
message.push ( "\n\n\n> [!TIP]\n> The following file(s) have been skipped:\n\n<br>\n\n" );
for ( const file_skipped of files_skipped )
{
message.push ( "- " + file_skipped );
}
}
/*
footer
*/
message.push ( "\n<br />\n\n---\n<br />\n" );
message.push ( `<sup>This check was done automatically. Do <b>NOT</b> open a new PR for re-validation. Instead, to trigger this check again, make a change to your PR and wait a few minutes, or close and re-open it.</sup>` );
/*
Has Errors
*/
if ( bHasError == true )
{
labels.push( "${{ env.LABEL_CHECK_STATUS_FAILED }}" );
core.setFailed( "Pull Request Failed Autocheck: " + context.issue.number + ": " + context.payload.pull_request.title + "." );
}
/*
No Errors
*/
if ( bHasError == false )
{
/*
change pr title
*/
const pr_title = `${ context.payload.pull_request.title }`;
const pr_title_append = `PR ${ context.issue.number }:`;
if ( !pr_title.startsWith( pr_title_append ) )
{
await github.rest.pulls.update(
{
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
title: `${ pr_title_append } ${ context.payload.pull_request.title }`
} );
}
if ( !context.payload.pull_request.labels.filter( label => label.name === "${{ env.LABEL_CHECK_CHANGES_REQ }}" ).length > 0 )
labels.push( "${{ env.LABEL_CHECK_REVIEW_READY }}" );
}
/*
Determine Labels
*/
const bGitaction = type_gitaction.some( s => s.includes( branch_ref ) || branch_ref.includes( s ) );
const bDependency = type_dependency.some( s => s.includes( branch_ref ) || branch_ref.includes( s ) );
if ( actor == "${{ env.BOT_NAME_DEPENDABOT }}" && bDependency )
labels.push( "${{ env.LABEL_TYPE_DEPENDENCY }}" );
else if ( actor == "${{ env.BOT_NAME_DEPENDABOT }}" && bGitaction )
labels.push( "${{ env.LABEL_TYPE_GITACTION }}" );
if ( context.payload.pull_request.labels.filter( label => label.name === "${{ env.LABEL_CHECK_CHANGES_REQ }}" ).length > 0 )
labels.push( "${{ env.LABEL_CHECK_CHANGES_REQ }}" );
if (context.payload.pull_request.labels.filter(label => label.name === "${{ env.LABEL_CHECK_REBASE_REQ }}" ).length > 0 )
labels.push( "${{ env.LABEL_CHECK_REBASE_REQ }}" );
if ( context.payload.pull_request.labels.filter(label => label.name === "${{ env.LABEL_CHECK_SECURITY_ERR }}" ).length > 0 )
labels.push( "${{ env.LABEL_CHECK_SECURITY_ERR }}" );
if (context.payload.pull_request.labels.filter( label => label.name === "${{ env.LABEL_CHECK_STATUS_CHGMADE }}" ).length > 0 )
labels.push( "${{ env.LABEL_CHECK_STATUS_CHGMADE }}" );
if ( context.payload.pull_request.labels.filter( label => label.name === "${{ env.LABEL_CHECK_SCAN_SKIPPED }}" ).length > 0 )
labels.push( "${{ env.LABEL_CHECK_SCAN_SKIPPED }}" );
labels.push( "${{ env.LABEL_TYPE_PR }}" );
/*
Set Label
*/
await github.rest.issues.setLabels(
{
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels,
} );
/*
Create Comment
*/
await github.rest.issues.createComment(
{
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: message.join('\n'),
} );