44# - [Super-Linter](https://github.com/super-linter/super-linter), with some opinionated defaults.
55# - [CodeQL](https://docs.github.com/en/code-security/code-scanning/introduction-to-code-scanning/about-code-scanning-with-codeql) to analyze the code.
66# - [Ratchet](https://github.com/sethvargo/ratchet) to check that GitHub Action versions are pinned.
7+ # - [Actions Permissions](https://github.com/GitHubSecurityLab/actions-permissions) to analyze and optimize workflow permissions.
78
89name : " Linter"
910on :
4142 ./action.yml
4243 ./.github/workflows/**/*.yml
4344 ./actions/**/*.yml
45+ workflow-files :
46+ description : |
47+ List of files or directories where GitHub workflows are located.
48+ Supports glob patterns.
49+ Leave empty to disable the check.
50+ type : string
51+ required : false
52+ default : |
53+ ./.github/workflows/*.yml
4454 lint-all :
4555 description : " Run checks on all files, not just the changed ones."
4656 type : boolean
@@ -136,10 +146,12 @@ jobs:
136146 with :
137147 category : " /language:${{matrix.language}}"
138148
139- actions-pinning :
140- name : 📌 Check GitHub Actions Pinning
149+ prepare-actions-linting :
141150 runs-on : ${{ fromJson(inputs.runs-on) }}
142- if : ${{ inputs.action-files }}
151+ if : ${{ inputs.action-files || inputs.workflow-files }}
152+ outputs :
153+ action-files : ${{ steps.get-files-to-lint.outputs.action-files }}
154+ workflow-names : ${{ steps.get-files-to-lint.outputs.workflow-names }}
143155 steps :
144156 - uses : actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
145157 with :
@@ -150,74 +162,139 @@ jobs:
150162 uses : tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
151163 if : ${{ inputs.lint-all == false }}
152164 with :
153- files : ${{ inputs.action-files }}
165+ files : |
166+ ${{ inputs.action-files }}
167+ ${{ inputs.workflow-files }}
154168 dir_names_exclude_current_dir : true
155169
156170 - id : get-files-to-lint
157171 uses : actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
158172 env :
159- CHANGED_FILES : ${{ toJSON(steps.changed-files.outputs.all_changed_and_modified_files) }};
173+ CHANGED_FILES_OUTPUT : ${{ toJSON(steps.changed-files.outputs.all_changed_and_modified_files) }};
160174 ACTION_FILES_INPUT : ${{ toJSON(inputs.action-files) }}
175+ WORKFLOW_FILES_INPUT : ${{ toJSON(inputs.workflow-files) }}
161176 with :
162177 script : |
163178 const fs = require("node:fs");
164179 const path = require("node:path");
165180
166- const changedFiles = process.env.CHANGED_FILES;
181+ const changedFilesOutput = process.env.CHANGED_FILES_OUTPUT;
182+ core.debug(`Changed files output: ${changedFilesOutput}`);
167183
168- let actionFiles = [] ;
169- if (changedFiles !== null) {
170- actionFiles = changedFiles.split(" ").filter(file => file && fs.existsSync(file));
171- } else {
172- const actionFilesInput = process.env.ACTION_FILES_INPUT ;
184+ const actionFilesInput = process.env.ACTION_FILES_INPUT ;
185+ core.debug(`Action files input: ${actionFilesInput}`);
186+
187+ const workflowFilesInput = process.env.WORKFLOW_FILES_INPUT;
188+ core.debug(`Workflow files input: ${workflowFilesInput}`) ;
173189
174- for (const actionFile of actionFilesInput.split("\n")) {
175- let sanitizedActionFile = actionFile.trim();
176- if (sanitizedActionFile === "") {
190+ function parseFilePatterns(filePatterns) {
191+ const patterns = [];
192+ for (const filePattern of filePatterns.split("\n")) {
193+ let sanitizedFilePattern = filePattern.trim();
194+ if (sanitizedFilePattern === "") {
177195 continue;
178196 }
179197
180- if (path.isAbsolute(sanitizedActionFile )) {
181- // Ensure actionFile is within the workspace
182- if (!sanitizedActionFile .startsWith(process.env.GITHUB_WORKSPACE)) {
183- return core.setFailed(`Action file / directory is not within the workspace: ${sanitizedActionFile }`);
198+ if (path.isAbsolute(sanitizedFilePattern )) {
199+ // Ensure filePattern is within the workspace
200+ if (!sanitizedFilePattern .startsWith(process.env.GITHUB_WORKSPACE)) {
201+ return core.setFailed(`File / directory is not within the workspace: ${sanitizedFilePattern }`);
184202 }
185203 } else {
186- sanitizedActionFile = path.join(process.env.GITHUB_WORKSPACE, sanitizedActionFile );
204+ sanitizedFilePattern = path.join(process.env.GITHUB_WORKSPACE, sanitizedFilePattern );
187205 }
188- actionFiles.push(sanitizedActionFile);
189- }
190-
191- if (actionFiles.length === 0) {
192- return core.setFailed("No action files to lint.");
193- }
194-
195- async function getActionFiles(actionFile) {
196- const globber = await glob.create(actionFile,{ matchactionFilesInput: false });
197- return await globber.glob();
206+ patterns.push(sanitizedFilePattern);
198207 }
208+ return patterns;
209+ }
199210
200- actionFiles = (await Promise.all(actionFiles.map(getActionFiles)))
211+ async function findFilesByPatterns(filePatterns) {
212+ const foundFiles = (await Promise.all(filePatterns.map(
213+ async (filePattern) => {
214+ const globber = await glob.create(filePattern, { excludeHiddenFiles: false });
215+ return await globber.glob();
216+ }
217+ )))
201218 .flat()
202219 .map((file) => path.relative(process.env.GITHUB_WORKSPACE, file));
203220
204- if (actionFiles.length === 0) {
221+ return [...new Set(foundFiles)];
222+ }
223+
224+ let changedFiles = null;
225+ if (changedFilesOutput) {
226+ changedFiles = changedFilesOutput.split(" ").filter(file => file && fs.existsSync(file));
227+ }
228+
229+ let workflowNames = [];
230+ const parsedWorkflowFiles = parseFilePatterns(workflowFilesInput);
231+ core.debug(`Parsed workflow files: ${parsedWorkflowFiles}`);
232+ let workflowFiles = await findFilesByPatterns(parsedWorkflowFiles);
233+ core.debug(`Workflow files: ${workflowFiles}`);
234+
235+ let actionFiles = [];
236+ if (changedFiles !== null) {
237+ actionFiles = changedFiles;
238+ workflowFiles = workflowFiles.filter(file => changedFiles.includes(file));
239+ } else {
240+ const parsedActionFiles = parseFilePatterns(actionFilesInput);
241+ core.debug(`Parsed action files: ${parsedActionFiles}`);
242+
243+ if (parsedActionFiles.length === 0) {
205244 return core.setFailed("No action files to lint.");
206245 }
246+
247+ actionFiles = await findFilesByPatterns(parsedActionFiles);
248+ core.debug(`Action files: ${actionFiles}`);
207249 }
208250
209- const files = actionFiles.map((file) => path.relative(process.env.GITHUB_WORKSPACE, file));
210- const filesOutput = [...new Set(files)].join(" ").trim();
251+ if (actionFiles.length > 0) {
252+ core.setOutput("action-files", actionFiles);
253+ }
211254
212- if (filesOutput.length === 0) {
213- return;
255+ for (const workflowFile of workflowFiles) {
256+ try {
257+ const workflowContent = fs.readFileSync(workflowFile, "utf8");
258+ const match = workflowContent.match(/name:\s*(.+)/);
259+ if (match) {
260+ workflowNames.push(match[1].trim());
261+ } else {
262+ workflowNames.push(path.basename(workflowFile, path.extname(workflowFile)));
263+ }
264+ } catch (error) {
265+ return core.setFailed(`Failed to read workflow file ${workflowFile}: ${error.message}`);
266+ }
267+ }
268+ workflowNames = [...new Set(workflowNames)];
269+ if (workflowNames.length > 0) {
270+ core.setOutput("workflow-names", JSON.stringify(workflowNames));
214271 }
215272
216- core.setOutput("files", filesOutput);
273+ actions-pinning :
274+ name : 📌 Check GitHub Actions Pinning
275+ needs : prepare-actions-linting
276+ runs-on : ${{ fromJson(inputs.runs-on) }}
277+ if : ${{ needs.prepare-actions-linting.outputs.action-files }}
278+ steps :
279+ - uses : actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
217280
218- - id : ratchet
219- # FIXME: should be updated by dependabot. See https://github.com/dependabot/dependabot-core/issues/8362
220- uses : " docker://ghcr.io/sethvargo/ratchet:0.11.3@sha256:242445a1c55430ad7477e6fcf2027c77d03f5760702537bca4cf622e7338fc81" # 0.11.3
221- if : ${{ steps.get-files-to-lint.outputs.files }}
281+ # FIXME: should be updated by dependabot. See https://github.com/dependabot/dependabot-core/issues/8362
282+ - uses : " docker://ghcr.io/sethvargo/ratchet:0.11.3@sha256:242445a1c55430ad7477e6fcf2027c77d03f5760702537bca4cf622e7338fc81" # 0.11.3
283+ if : ${{ needs.prepare-actions-linting.outputs.action-files }}
284+ with :
285+ args : " lint --format human --format actions ${{ needs.prepare-actions-linting.outputs.action-files }}"
286+
287+ permissions-analysis :
288+ name : 🔐 Workflow Permissions Analysis
289+ runs-on : ${{ fromJson(inputs.runs-on) }}
290+ needs : prepare-actions-linting
291+ if : ${{ needs.prepare-actions-linting.outputs.workflow-names }}
292+ permissions :
293+ actions : read
294+ strategy :
295+ matrix :
296+ name : ${{ fromJson(needs.prepare-actions-linting.outputs.workflow-names) }}
297+ steps :
298+ - uses : GitHubSecurityLab/actions-permissions/advisor@37c927c24552caa0ef6040ab0876db729cc12754 # v1.0.2-beta7
222299 with :
223- args : " lint --format human --format actions ${{ steps.get-files-to-lint.outputs.files }}"
300+ name : ${{ matrix.name }}
0 commit comments