From 0bd352155584db50b6f112978648904d7506a982 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Sep 2025 13:52:47 +0000 Subject: [PATCH 1/3] Initial plan From 950ff294ddf0adda84144c2282c8b2296984c18c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Sep 2025 14:08:23 +0000 Subject: [PATCH 2/3] Implement custom setCancelled function with self-cancellation Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- ...est-safe-output-add-issue-comment.lock.yml | 30 +++++++-- .../test-safe-output-add-issue-label.lock.yml | 30 +++++++-- ...output-create-code-scanning-alert.lock.yml | 30 +++++++-- ...est-safe-output-create-discussion.lock.yml | 30 +++++++-- .../test-safe-output-create-issue.lock.yml | 55 ++++++++++++++-- ...reate-pull-request-review-comment.lock.yml | 30 +++++++-- ...t-safe-output-create-pull-request.lock.yml | 30 +++++++-- ...est-safe-output-push-to-pr-branch.lock.yml | 30 +++++++-- .../test-safe-output-update-issue.lock.yml | 30 +++++++-- pkg/workflow/compiler.go | 63 ++++++++++++++++-- pkg/workflow/js.go | 3 + pkg/workflow/js/check_permissions.cjs | 26 +++++++- pkg/workflow/js/check_permissions.test.cjs | 5 ++ pkg/workflow/js/check_team_member.cjs | 29 +++++++++ pkg/workflow/js/check_team_member.test.cjs | 64 ++++++++++++++++++- 15 files changed, 437 insertions(+), 48 deletions(-) diff --git a/.github/workflows/test-safe-output-add-issue-comment.lock.yml b/.github/workflows/test-safe-output-add-issue-comment.lock.yml index c907e1ad540..c5b66f2d063 100644 --- a/.github/workflows/test-safe-output-add-issue-comment.lock.yml +++ b/.github/workflows/test-safe-output-add-issue-comment.lock.yml @@ -23,7 +23,11 @@ run-name: "Test Safe Output - Add Issue Comment" jobs: test-safe-output-add-issue-comment: runs-on: ubuntu-latest - permissions: read-all + permissions: + actions: write + contents: read + issues: read + pull-requests: read outputs: output: ${{ steps.collect_output.outputs.output }} steps: @@ -34,6 +38,23 @@ jobs: GITHUB_AW_REQUIRED_ROLES: admin,maintainer with: script: | + // Custom setCancelled function that uses self-cancellation + async function setCancelled(message) { + try { + // Cancel the current workflow run using GitHub Actions API + await github.rest.actions.cancelWorkflowRun({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: context.runId + }); + core.info(`Cancellation requested for this workflow run: ${message}`); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + core.warning(`Failed to cancel workflow run: ${errorMessage}`); + // Fallback to core.setCancelled if API call fails + core.setCancelled(message); + } + } async function main() { const { eventName } = context; // skip check for safe events @@ -52,7 +73,7 @@ jobs: core.error( "❌ Configuration error: Required permissions not specified. Contact repository administrator." ); - core.setCancelled( + await setCancelled( "Configuration error: Required permissions not specified" ); return; @@ -88,16 +109,17 @@ jobs: const errorMessage = repoError instanceof Error ? repoError.message : String(repoError); core.error(`Repository permission check failed: ${errorMessage}`); - core.setCancelled(`Repository permission check failed: ${errorMessage}`); + await setCancelled(`Repository permission check failed: ${errorMessage}`); return; } // Cancel the job when permission check fails core.warning( `❌ Access denied: Only authorized users can trigger this workflow. User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}` ); - core.setCancelled( + await setCancelled( `Access denied: User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}` ); + return; } await main(); - name: Checkout repository diff --git a/.github/workflows/test-safe-output-add-issue-label.lock.yml b/.github/workflows/test-safe-output-add-issue-label.lock.yml index de44b3b7577..35fd5beca22 100644 --- a/.github/workflows/test-safe-output-add-issue-label.lock.yml +++ b/.github/workflows/test-safe-output-add-issue-label.lock.yml @@ -25,7 +25,11 @@ run-name: "Test Safe Output - Add Issue Label" jobs: test-safe-output-add-issue-label: runs-on: ubuntu-latest - permissions: read-all + permissions: + actions: write + contents: read + issues: read + pull-requests: read outputs: output: ${{ steps.collect_output.outputs.output }} steps: @@ -36,6 +40,23 @@ jobs: GITHUB_AW_REQUIRED_ROLES: admin,maintainer with: script: | + // Custom setCancelled function that uses self-cancellation + async function setCancelled(message) { + try { + // Cancel the current workflow run using GitHub Actions API + await github.rest.actions.cancelWorkflowRun({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: context.runId + }); + core.info(`Cancellation requested for this workflow run: ${message}`); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + core.warning(`Failed to cancel workflow run: ${errorMessage}`); + // Fallback to core.setCancelled if API call fails + core.setCancelled(message); + } + } async function main() { const { eventName } = context; // skip check for safe events @@ -54,7 +75,7 @@ jobs: core.error( "❌ Configuration error: Required permissions not specified. Contact repository administrator." ); - core.setCancelled( + await setCancelled( "Configuration error: Required permissions not specified" ); return; @@ -90,16 +111,17 @@ jobs: const errorMessage = repoError instanceof Error ? repoError.message : String(repoError); core.error(`Repository permission check failed: ${errorMessage}`); - core.setCancelled(`Repository permission check failed: ${errorMessage}`); + await setCancelled(`Repository permission check failed: ${errorMessage}`); return; } // Cancel the job when permission check fails core.warning( `❌ Access denied: Only authorized users can trigger this workflow. User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}` ); - core.setCancelled( + await setCancelled( `Access denied: User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}` ); + return; } await main(); - name: Checkout repository diff --git a/.github/workflows/test-safe-output-create-code-scanning-alert.lock.yml b/.github/workflows/test-safe-output-create-code-scanning-alert.lock.yml index 32568d9cde1..1ca1b3290ae 100644 --- a/.github/workflows/test-safe-output-create-code-scanning-alert.lock.yml +++ b/.github/workflows/test-safe-output-create-code-scanning-alert.lock.yml @@ -27,7 +27,11 @@ run-name: "Test Safe Output - Create Code Scanning Alert" jobs: test-safe-output-create-code-scanning-alert: runs-on: ubuntu-latest - permissions: read-all + permissions: + actions: write + contents: read + issues: read + pull-requests: read outputs: output: ${{ steps.collect_output.outputs.output }} steps: @@ -38,6 +42,23 @@ jobs: GITHUB_AW_REQUIRED_ROLES: admin,maintainer with: script: | + // Custom setCancelled function that uses self-cancellation + async function setCancelled(message) { + try { + // Cancel the current workflow run using GitHub Actions API + await github.rest.actions.cancelWorkflowRun({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: context.runId + }); + core.info(`Cancellation requested for this workflow run: ${message}`); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + core.warning(`Failed to cancel workflow run: ${errorMessage}`); + // Fallback to core.setCancelled if API call fails + core.setCancelled(message); + } + } async function main() { const { eventName } = context; // skip check for safe events @@ -56,7 +77,7 @@ jobs: core.error( "❌ Configuration error: Required permissions not specified. Contact repository administrator." ); - core.setCancelled( + await setCancelled( "Configuration error: Required permissions not specified" ); return; @@ -92,16 +113,17 @@ jobs: const errorMessage = repoError instanceof Error ? repoError.message : String(repoError); core.error(`Repository permission check failed: ${errorMessage}`); - core.setCancelled(`Repository permission check failed: ${errorMessage}`); + await setCancelled(`Repository permission check failed: ${errorMessage}`); return; } // Cancel the job when permission check fails core.warning( `❌ Access denied: Only authorized users can trigger this workflow. User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}` ); - core.setCancelled( + await setCancelled( `Access denied: User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}` ); + return; } await main(); - name: Checkout repository diff --git a/.github/workflows/test-safe-output-create-discussion.lock.yml b/.github/workflows/test-safe-output-create-discussion.lock.yml index da147359f55..5705b159e79 100644 --- a/.github/workflows/test-safe-output-create-discussion.lock.yml +++ b/.github/workflows/test-safe-output-create-discussion.lock.yml @@ -22,7 +22,11 @@ run-name: "Test Safe Output - Create Discussion" jobs: test-safe-output-create-discussion: runs-on: ubuntu-latest - permissions: read-all + permissions: + actions: write + contents: read + issues: read + pull-requests: read outputs: output: ${{ steps.collect_output.outputs.output }} steps: @@ -33,6 +37,23 @@ jobs: GITHUB_AW_REQUIRED_ROLES: admin,maintainer with: script: | + // Custom setCancelled function that uses self-cancellation + async function setCancelled(message) { + try { + // Cancel the current workflow run using GitHub Actions API + await github.rest.actions.cancelWorkflowRun({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: context.runId + }); + core.info(`Cancellation requested for this workflow run: ${message}`); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + core.warning(`Failed to cancel workflow run: ${errorMessage}`); + // Fallback to core.setCancelled if API call fails + core.setCancelled(message); + } + } async function main() { const { eventName } = context; // skip check for safe events @@ -51,7 +72,7 @@ jobs: core.error( "❌ Configuration error: Required permissions not specified. Contact repository administrator." ); - core.setCancelled( + await setCancelled( "Configuration error: Required permissions not specified" ); return; @@ -87,16 +108,17 @@ jobs: const errorMessage = repoError instanceof Error ? repoError.message : String(repoError); core.error(`Repository permission check failed: ${errorMessage}`); - core.setCancelled(`Repository permission check failed: ${errorMessage}`); + await setCancelled(`Repository permission check failed: ${errorMessage}`); return; } // Cancel the job when permission check fails core.warning( `❌ Access denied: Only authorized users can trigger this workflow. User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}` ); - core.setCancelled( + await setCancelled( `Access denied: User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}` ); + return; } await main(); - name: Checkout repository diff --git a/.github/workflows/test-safe-output-create-issue.lock.yml b/.github/workflows/test-safe-output-create-issue.lock.yml index a3fa7a5eaef..5ab4d6a708a 100644 --- a/.github/workflows/test-safe-output-create-issue.lock.yml +++ b/.github/workflows/test-safe-output-create-issue.lock.yml @@ -19,7 +19,11 @@ run-name: "Test Safe Output - Create Issue" jobs: test-safe-output-create-issue: runs-on: ubuntu-latest - permissions: read-all + permissions: + actions: write + contents: read + issues: read + pull-requests: read outputs: output: ${{ steps.collect_output.outputs.output }} steps: @@ -30,6 +34,23 @@ jobs: GITHUB_AW_REQUIRED_ROLES: admin,maintainer with: script: | + // Custom setCancelled function that uses self-cancellation + async function setCancelled(message) { + try { + // Cancel the current workflow run using GitHub Actions API + await github.rest.actions.cancelWorkflowRun({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: context.runId + }); + core.info(`Cancellation requested for this workflow run: ${message}`); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + core.warning(`Failed to cancel workflow run: ${errorMessage}`); + // Fallback to core.setCancelled if API call fails + core.setCancelled(message); + } + } async function main() { const { eventName } = context; // skip check for safe events @@ -48,7 +69,7 @@ jobs: core.error( "❌ Configuration error: Required permissions not specified. Contact repository administrator." ); - core.setCancelled( + await setCancelled( "Configuration error: Required permissions not specified" ); return; @@ -84,16 +105,17 @@ jobs: const errorMessage = repoError instanceof Error ? repoError.message : String(repoError); core.error(`Repository permission check failed: ${errorMessage}`); - core.setCancelled(`Repository permission check failed: ${errorMessage}`); + await setCancelled(`Repository permission check failed: ${errorMessage}`); return; } // Cancel the job when permission check fails core.warning( `❌ Access denied: Only authorized users can trigger this workflow. User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}` ); - core.setCancelled( + await setCancelled( `Access denied: User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}` ); + return; } await main(); - name: Checkout repository @@ -1096,6 +1118,7 @@ jobs: needs: test-safe-output-create-issue runs-on: ubuntu-latest permissions: + actions: write contents: read issues: write timeout-minutes: 10 @@ -1110,6 +1133,23 @@ jobs: GITHUB_AW_REQUIRED_ROLES: admin,maintainer with: script: | + // Custom setCancelled function that uses self-cancellation + async function setCancelled(message) { + try { + // Cancel the current workflow run using GitHub Actions API + await github.rest.actions.cancelWorkflowRun({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: context.runId + }); + core.info(`Cancellation requested for this workflow run: ${message}`); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + core.warning(`Failed to cancel workflow run: ${errorMessage}`); + // Fallback to core.setCancelled if API call fails + core.setCancelled(message); + } + } async function main() { const { eventName } = context; // skip check for safe events @@ -1128,7 +1168,7 @@ jobs: core.error( "❌ Configuration error: Required permissions not specified. Contact repository administrator." ); - core.setCancelled( + await setCancelled( "Configuration error: Required permissions not specified" ); return; @@ -1164,16 +1204,17 @@ jobs: const errorMessage = repoError instanceof Error ? repoError.message : String(repoError); core.error(`Repository permission check failed: ${errorMessage}`); - core.setCancelled(`Repository permission check failed: ${errorMessage}`); + await setCancelled(`Repository permission check failed: ${errorMessage}`); return; } // Cancel the job when permission check fails core.warning( `❌ Access denied: Only authorized users can trigger this workflow. User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}` ); - core.setCancelled( + await setCancelled( `Access denied: User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}` ); + return; } await main(); - name: Create Output Issue diff --git a/.github/workflows/test-safe-output-create-pull-request-review-comment.lock.yml b/.github/workflows/test-safe-output-create-pull-request-review-comment.lock.yml index 2bf4e7d95aa..a06cd9204aa 100644 --- a/.github/workflows/test-safe-output-create-pull-request-review-comment.lock.yml +++ b/.github/workflows/test-safe-output-create-pull-request-review-comment.lock.yml @@ -21,7 +21,11 @@ run-name: "Test Safe Output - Create Pull Request Review Comment" jobs: test-safe-output-create-pull-request-review-comment: runs-on: ubuntu-latest - permissions: read-all + permissions: + actions: write + contents: read + issues: read + pull-requests: read outputs: output: ${{ steps.collect_output.outputs.output }} steps: @@ -32,6 +36,23 @@ jobs: GITHUB_AW_REQUIRED_ROLES: admin,maintainer with: script: | + // Custom setCancelled function that uses self-cancellation + async function setCancelled(message) { + try { + // Cancel the current workflow run using GitHub Actions API + await github.rest.actions.cancelWorkflowRun({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: context.runId + }); + core.info(`Cancellation requested for this workflow run: ${message}`); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + core.warning(`Failed to cancel workflow run: ${errorMessage}`); + // Fallback to core.setCancelled if API call fails + core.setCancelled(message); + } + } async function main() { const { eventName } = context; // skip check for safe events @@ -50,7 +71,7 @@ jobs: core.error( "❌ Configuration error: Required permissions not specified. Contact repository administrator." ); - core.setCancelled( + await setCancelled( "Configuration error: Required permissions not specified" ); return; @@ -86,16 +107,17 @@ jobs: const errorMessage = repoError instanceof Error ? repoError.message : String(repoError); core.error(`Repository permission check failed: ${errorMessage}`); - core.setCancelled(`Repository permission check failed: ${errorMessage}`); + await setCancelled(`Repository permission check failed: ${errorMessage}`); return; } // Cancel the job when permission check fails core.warning( `❌ Access denied: Only authorized users can trigger this workflow. User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}` ); - core.setCancelled( + await setCancelled( `Access denied: User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}` ); + return; } await main(); - name: Checkout repository diff --git a/.github/workflows/test-safe-output-create-pull-request.lock.yml b/.github/workflows/test-safe-output-create-pull-request.lock.yml index 1b5832803c7..71d4c8a7526 100644 --- a/.github/workflows/test-safe-output-create-pull-request.lock.yml +++ b/.github/workflows/test-safe-output-create-pull-request.lock.yml @@ -20,7 +20,11 @@ run-name: "Test Safe Output - Create Pull Request" jobs: test-safe-output-create-pull-request: runs-on: ubuntu-latest - permissions: read-all + permissions: + actions: write + contents: read + issues: read + pull-requests: read outputs: output: ${{ steps.collect_output.outputs.output }} steps: @@ -31,6 +35,23 @@ jobs: GITHUB_AW_REQUIRED_ROLES: admin,maintainer with: script: | + // Custom setCancelled function that uses self-cancellation + async function setCancelled(message) { + try { + // Cancel the current workflow run using GitHub Actions API + await github.rest.actions.cancelWorkflowRun({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: context.runId + }); + core.info(`Cancellation requested for this workflow run: ${message}`); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + core.warning(`Failed to cancel workflow run: ${errorMessage}`); + // Fallback to core.setCancelled if API call fails + core.setCancelled(message); + } + } async function main() { const { eventName } = context; // skip check for safe events @@ -49,7 +70,7 @@ jobs: core.error( "❌ Configuration error: Required permissions not specified. Contact repository administrator." ); - core.setCancelled( + await setCancelled( "Configuration error: Required permissions not specified" ); return; @@ -85,16 +106,17 @@ jobs: const errorMessage = repoError instanceof Error ? repoError.message : String(repoError); core.error(`Repository permission check failed: ${errorMessage}`); - core.setCancelled(`Repository permission check failed: ${errorMessage}`); + await setCancelled(`Repository permission check failed: ${errorMessage}`); return; } // Cancel the job when permission check fails core.warning( `❌ Access denied: Only authorized users can trigger this workflow. User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}` ); - core.setCancelled( + await setCancelled( `Access denied: User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}` ); + return; } await main(); - name: Checkout repository diff --git a/.github/workflows/test-safe-output-push-to-pr-branch.lock.yml b/.github/workflows/test-safe-output-push-to-pr-branch.lock.yml index 36d3c29b7dc..9c69dc28a50 100644 --- a/.github/workflows/test-safe-output-push-to-pr-branch.lock.yml +++ b/.github/workflows/test-safe-output-push-to-pr-branch.lock.yml @@ -21,7 +21,11 @@ run-name: "Test Safe Output - Push to PR Branch" jobs: test-safe-output-push-to-pr-branch: runs-on: ubuntu-latest - permissions: read-all + permissions: + actions: write + contents: read + issues: read + pull-requests: read outputs: output: ${{ steps.collect_output.outputs.output }} steps: @@ -32,6 +36,23 @@ jobs: GITHUB_AW_REQUIRED_ROLES: admin,maintainer with: script: | + // Custom setCancelled function that uses self-cancellation + async function setCancelled(message) { + try { + // Cancel the current workflow run using GitHub Actions API + await github.rest.actions.cancelWorkflowRun({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: context.runId + }); + core.info(`Cancellation requested for this workflow run: ${message}`); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + core.warning(`Failed to cancel workflow run: ${errorMessage}`); + // Fallback to core.setCancelled if API call fails + core.setCancelled(message); + } + } async function main() { const { eventName } = context; // skip check for safe events @@ -50,7 +71,7 @@ jobs: core.error( "❌ Configuration error: Required permissions not specified. Contact repository administrator." ); - core.setCancelled( + await setCancelled( "Configuration error: Required permissions not specified" ); return; @@ -86,16 +107,17 @@ jobs: const errorMessage = repoError instanceof Error ? repoError.message : String(repoError); core.error(`Repository permission check failed: ${errorMessage}`); - core.setCancelled(`Repository permission check failed: ${errorMessage}`); + await setCancelled(`Repository permission check failed: ${errorMessage}`); return; } // Cancel the job when permission check fails core.warning( `❌ Access denied: Only authorized users can trigger this workflow. User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}` ); - core.setCancelled( + await setCancelled( `Access denied: User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}` ); + return; } await main(); - name: Checkout repository diff --git a/.github/workflows/test-safe-output-update-issue.lock.yml b/.github/workflows/test-safe-output-update-issue.lock.yml index 8a12be65c00..e21428d3215 100644 --- a/.github/workflows/test-safe-output-update-issue.lock.yml +++ b/.github/workflows/test-safe-output-update-issue.lock.yml @@ -20,7 +20,11 @@ run-name: "Test Safe Output - Update Issue" jobs: test-safe-output-update-issue: runs-on: ubuntu-latest - permissions: read-all + permissions: + actions: write + contents: read + issues: read + pull-requests: read outputs: output: ${{ steps.collect_output.outputs.output }} steps: @@ -31,6 +35,23 @@ jobs: GITHUB_AW_REQUIRED_ROLES: admin,maintainer with: script: | + // Custom setCancelled function that uses self-cancellation + async function setCancelled(message) { + try { + // Cancel the current workflow run using GitHub Actions API + await github.rest.actions.cancelWorkflowRun({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: context.runId + }); + core.info(`Cancellation requested for this workflow run: ${message}`); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + core.warning(`Failed to cancel workflow run: ${errorMessage}`); + // Fallback to core.setCancelled if API call fails + core.setCancelled(message); + } + } async function main() { const { eventName } = context; // skip check for safe events @@ -49,7 +70,7 @@ jobs: core.error( "❌ Configuration error: Required permissions not specified. Contact repository administrator." ); - core.setCancelled( + await setCancelled( "Configuration error: Required permissions not specified" ); return; @@ -85,16 +106,17 @@ jobs: const errorMessage = repoError instanceof Error ? repoError.message : String(repoError); core.error(`Repository permission check failed: ${errorMessage}`); - core.setCancelled(`Repository permission check failed: ${errorMessage}`); + await setCancelled(`Repository permission check failed: ${errorMessage}`); return; } // Cancel the job when permission check fails core.warning( `❌ Access denied: Only authorized users can trigger this workflow. User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}` ); - core.setCancelled( + await setCancelled( `Access denied: User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}` ); + return; } await main(); - name: Checkout repository diff --git a/pkg/workflow/compiler.go b/pkg/workflow/compiler.go index 5a0d253b6e6..07de406b4c5 100644 --- a/pkg/workflow/compiler.go +++ b/pkg/workflow/compiler.go @@ -2018,11 +2018,18 @@ func (c *Compiler) buildTaskJob(data *WorkflowData, frontmatter map[string]any) steps = append(steps, " run: echo \"Task job executed - conditions satisfied\"\n") } + // Determine permissions needed for task job + var permissions string + if needsPermissionCheck || data.Command != "" { + // Add actions: write permission for self-cancellation when team member checks are present + permissions = "permissions:\n actions: write" + } + job := &Job{ Name: "task", If: data.If, // Use the existing condition (which may include alias checks) RunsOn: "runs-on: ubuntu-latest", - Permissions: "", // No permissions needed - task job does not require content access + Permissions: permissions, Steps: steps, Outputs: outputs, } @@ -2039,7 +2046,7 @@ core.setOutput("is_team_member", "true"); console.log("Permission check skipped - 'roles: all' specified");` } - // Use the embedded check_permissions.cjs script + // Use the embedded check_permissions.cjs script for flexible permission checking // The GITHUB_AW_REQUIRED_ROLES environment variable is set via the env field return checkPermissionsScript } @@ -2101,11 +2108,17 @@ func (c *Compiler) buildAddReactionJob(data *WorkflowData, taskJobCreated bool, depends = []string{"task"} // Depend on the task job only if it exists } + // Determine permissions - add actions: write if permission checks are added + permissions := "permissions:\n issues: write\n pull-requests: write" + if !taskJobCreated && c.needsPermissionChecks(data) { + permissions = "permissions:\n actions: write\n issues: write\n pull-requests: write" + } + job := &Job{ Name: "add_reaction", If: reactionCondition.Render(), RunsOn: "runs-on: ubuntu-latest", - Permissions: "permissions:\n issues: write\n pull-requests: write", + Permissions: permissions, Steps: steps, Outputs: outputs, Needs: depends, @@ -2186,11 +2199,17 @@ func (c *Compiler) buildCreateOutputIssueJob(data *WorkflowData, mainJobName str jobCondition = "" // No conditional execution } + // Determine permissions - add actions: write if permission checks are added + permissions := "permissions:\n contents: read\n issues: write" + if !taskJobCreated && c.needsPermissionChecks(data) { + permissions = "permissions:\n actions: write\n contents: read\n issues: write" + } + job := &Job{ Name: "create_issue", If: jobCondition, RunsOn: "runs-on: ubuntu-latest", - Permissions: "permissions:\n contents: read\n issues: write", + Permissions: permissions, TimeoutMinutes: 10, // 10-minute timeout as required Steps: steps, Outputs: outputs, @@ -2660,11 +2679,45 @@ func (c *Compiler) buildMainJob(data *WorkflowData, jobName string, taskJobCreat jobCondition = data.If // Use the original If condition from the workflow data } + // Determine permissions - modify permissions to include actions: write if permission checks are added + permissions := data.Permissions + if !taskJobCreated { + var needsPermissionCheck bool + // Check if permission checks are needed using frontmatter if available + if frontmatter != nil { + needsPermissionCheck = c.needsPermissionChecksWithFrontmatter(data, frontmatter) + } else { + needsPermissionCheck = c.needsPermissionChecks(data) + } + + if needsPermissionCheck { + // Add actions: write permission for self-cancellation + if permissions == "" || permissions == "permissions: read-all" { + permissions = "permissions:\n actions: write\n contents: read\n issues: read\n pull-requests: read" + } else { + // Parse existing permissions and add actions: write if not present + if !strings.Contains(permissions, "actions:") { + // Insert actions: write at the beginning of the permissions block + lines := strings.Split(permissions, "\n") + if len(lines) > 0 && strings.Contains(lines[0], "permissions:") { + // Insert after the permissions: line + newLines := []string{lines[0], " actions: write"} + newLines = append(newLines, lines[1:]...) + permissions = strings.Join(newLines, "\n") + } else { + // Fallback: prepend to existing permissions + permissions = "permissions:\n actions: write\n" + strings.TrimPrefix(permissions, "permissions:\n") + } + } + } + } + } + job := &Job{ Name: jobName, If: jobCondition, RunsOn: c.indentYAMLLines(data.RunsOn, " "), - Permissions: c.indentYAMLLines(data.Permissions, " "), + Permissions: c.indentYAMLLines(permissions, " "), Steps: steps, Needs: depends, Outputs: outputs, diff --git a/pkg/workflow/js.go b/pkg/workflow/js.go index 758cf14cf9d..154ed7897f7 100644 --- a/pkg/workflow/js.go +++ b/pkg/workflow/js.go @@ -51,6 +51,9 @@ var addReactionAndEditCommentScript string //go:embed js/check_permissions.cjs var checkPermissionsScript string +//go:embed js/check_team_member.cjs +var checkTeamMemberScript string + //go:embed js/parse_claude_log.cjs var parseClaudeLogScript string diff --git a/pkg/workflow/js/check_permissions.cjs b/pkg/workflow/js/check_permissions.cjs index e2ec20060bb..1b14376fe56 100644 --- a/pkg/workflow/js/check_permissions.cjs +++ b/pkg/workflow/js/check_permissions.cjs @@ -1,3 +1,22 @@ +// Custom setCancelled function that uses self-cancellation +async function setCancelled(message) { + try { + // Cancel the current workflow run using GitHub Actions API + await github.rest.actions.cancelWorkflowRun({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: context.runId + }); + + core.info(`Cancellation requested for this workflow run: ${message}`); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + core.warning(`Failed to cancel workflow run: ${errorMessage}`); + // Fallback to core.setCancelled if API call fails + core.setCancelled(message); + } +} + async function main() { const { eventName } = context; @@ -19,7 +38,7 @@ async function main() { core.error( "❌ Configuration error: Required permissions not specified. Contact repository administrator." ); - core.setCancelled( + await setCancelled( "Configuration error: Required permissions not specified" ); return; @@ -60,7 +79,7 @@ async function main() { const errorMessage = repoError instanceof Error ? repoError.message : String(repoError); core.error(`Repository permission check failed: ${errorMessage}`); - core.setCancelled(`Repository permission check failed: ${errorMessage}`); + await setCancelled(`Repository permission check failed: ${errorMessage}`); return; } @@ -68,8 +87,9 @@ async function main() { core.warning( `❌ Access denied: Only authorized users can trigger this workflow. User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}` ); - core.setCancelled( + await setCancelled( `Access denied: User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}` ); + return; } await main(); diff --git a/pkg/workflow/js/check_permissions.test.cjs b/pkg/workflow/js/check_permissions.test.cjs index 4cfa98faf39..73916a72cce 100644 --- a/pkg/workflow/js/check_permissions.test.cjs +++ b/pkg/workflow/js/check_permissions.test.cjs @@ -10,6 +10,7 @@ const mockCore = { setFailed: vi.fn(), setCancelled: vi.fn(), setError: vi.fn(), + info: vi.fn(), }; const mockGithub = { @@ -17,6 +18,9 @@ const mockGithub = { repos: { getCollaboratorPermissionLevel: vi.fn(), }, + actions: { + cancelWorkflowRun: vi.fn(), + }, }, }; @@ -27,6 +31,7 @@ const mockContext = { owner: "testowner", repo: "testrepo", }, + runId: 12345, }; // Set up global variables diff --git a/pkg/workflow/js/check_team_member.cjs b/pkg/workflow/js/check_team_member.cjs index 3a342bae627..a84b52e8c53 100644 --- a/pkg/workflow/js/check_team_member.cjs +++ b/pkg/workflow/js/check_team_member.cjs @@ -1,3 +1,22 @@ +// Custom setCancelled function that uses self-cancellation +async function setCancelled(message) { + try { + // Cancel the current workflow run using GitHub Actions API + await github.rest.actions.cancelWorkflowRun({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: context.runId + }); + + core.info(`Cancellation requested for this workflow run: ${message}`); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + core.warning(`Failed to cancel workflow run: ${errorMessage}`); + // Fallback to core.setCancelled if API call fails + core.setCancelled(message); + } +} + async function main() { const actor = context.actor; const { owner, repo } = context.repo; @@ -29,6 +48,16 @@ async function main() { core.warning(`Repository permission check failed: ${errorMessage}`); } + // Team membership check failed - use self-cancellation + const failureMessage = `Access denied: User '${actor}' is not authorized to trigger this workflow. Only admin or maintainer users can run this workflow.`; + core.warning(`❌ ${failureMessage}`); + + await setCancelled(failureMessage); + + // Set output for any dependent steps that might check before cancellation takes effect core.setOutput("is_team_member", "false"); + + // Return to finish the script + return; } await main(); diff --git a/pkg/workflow/js/check_team_member.test.cjs b/pkg/workflow/js/check_team_member.test.cjs index ad3a5db524f..2c3501aa031 100644 --- a/pkg/workflow/js/check_team_member.test.cjs +++ b/pkg/workflow/js/check_team_member.test.cjs @@ -7,6 +7,8 @@ const mockCore = { setOutput: vi.fn(), warning: vi.fn(), error: vi.fn(), + info: vi.fn(), + setCancelled: vi.fn(), }; const mockGithub = { @@ -14,6 +16,9 @@ const mockGithub = { repos: { getCollaboratorPermissionLevel: vi.fn(), }, + actions: { + cancelWorkflowRun: vi.fn(), + }, }, }; @@ -23,6 +28,7 @@ const mockContext = { owner: "testowner", repo: "testrepo", }, + runId: 12345, }; // Set up global variables @@ -145,11 +151,13 @@ describe("check_team_member.cjs", () => { consoleSpy.mockRestore(); }); - it("should set is_team_member to false for read permission", async () => { + it("should use self-cancellation when team membership fails", async () => { mockGithub.rest.repos.getCollaboratorPermissionLevel.mockResolvedValue({ data: { permission: "read" }, }); + mockGithub.rest.actions.cancelWorkflowRun.mockResolvedValue({}); + const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); // Execute the script @@ -169,6 +177,60 @@ describe("check_team_member.cjs", () => { expect(consoleSpy).toHaveBeenCalledWith( "Repository permission level: read" ); + + // Check that self-cancellation was attempted + expect(mockGithub.rest.actions.cancelWorkflowRun).toHaveBeenCalledWith({ + owner: "testowner", + repo: "testrepo", + run_id: 12345, + }); + + expect(mockCore.info).toHaveBeenCalledWith( + expect.stringContaining("Cancellation requested for this workflow run") + ); + expect(mockCore.warning).toHaveBeenCalledWith( + expect.stringContaining("Access denied: User 'testuser' is not authorized") + ); + expect(mockCore.setOutput).toHaveBeenCalledWith("is_team_member", "false"); + + consoleSpy.mockRestore(); + }); + + it("should fallback to core.setCancelled when API call fails", async () => { + mockGithub.rest.repos.getCollaboratorPermissionLevel.mockResolvedValue({ + data: { permission: "read" }, + }); + + const apiError = new Error("API Error: Forbidden"); + mockGithub.rest.actions.cancelWorkflowRun.mockRejectedValue(apiError); + + const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + + // Execute the script + await eval(`(async () => { ${checkTeamMemberScript} })()`); + + expect( + mockGithub.rest.repos.getCollaboratorPermissionLevel + ).toHaveBeenCalledWith({ + owner: "testowner", + repo: "testrepo", + username: "testuser", + }); + + // Check that self-cancellation was attempted but failed + expect(mockGithub.rest.actions.cancelWorkflowRun).toHaveBeenCalledWith({ + owner: "testowner", + repo: "testrepo", + run_id: 12345, + }); + + // Check that fallback occurred + expect(mockCore.warning).toHaveBeenCalledWith( + "Failed to cancel workflow run: API Error: Forbidden" + ); + expect(mockCore.setCancelled).toHaveBeenCalledWith( + expect.stringContaining("Access denied: User 'testuser' is not authorized") + ); expect(mockCore.setOutput).toHaveBeenCalledWith("is_team_member", "false"); consoleSpy.mockRestore(); From 8b7d9e87b0f2c90f7a7830893898c709e7aaf72d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Sep 2025 14:36:38 +0000 Subject: [PATCH 3/3] Complete implementation of custom setCancelled function with self-cancellation Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- ...est-safe-output-add-issue-comment.lock.yml | 97 +------ .../test-safe-output-add-issue-label.lock.yml | 97 +------ ...output-create-code-scanning-alert.lock.yml | 97 +------ ...est-safe-output-create-discussion.lock.yml | 97 +------ .../test-safe-output-create-issue.lock.yml | 189 +------------ ...reate-pull-request-review-comment.lock.yml | 97 +------ ...t-safe-output-create-pull-request.lock.yml | 97 +------ ...est-safe-output-push-to-pr-branch.lock.yml | 97 +------ .../test-safe-output-update-issue.lock.yml | 97 +------ pkg/workflow/actions_write_permission_test.go | 259 ++++++++++++++++++ pkg/workflow/compiler.go | 85 +++--- pkg/workflow/compiler_test.go | 34 ++- pkg/workflow/js.go | 3 - pkg/workflow/js/check_permissions.cjs | 11 +- pkg/workflow/js/check_team_member.cjs | 17 +- pkg/workflow/js/check_team_member.test.cjs | 9 +- pkg/workflow/js/compute_text.cjs | 2 - pkg/workflow/js/compute_text.test.cjs | 9 +- pkg/workflow/js/sanitize_output.cjs | 2 - pkg/workflow/js/sanitize_output.test.cjs | 21 +- pkg/workflow/output_test.go | 7 +- 21 files changed, 390 insertions(+), 1034 deletions(-) create mode 100644 pkg/workflow/actions_write_permission_test.go diff --git a/.github/workflows/test-safe-output-add-issue-comment.lock.yml b/.github/workflows/test-safe-output-add-issue-comment.lock.yml index c5b66f2d063..16f01d096dd 100644 --- a/.github/workflows/test-safe-output-add-issue-comment.lock.yml +++ b/.github/workflows/test-safe-output-add-issue-comment.lock.yml @@ -23,105 +23,10 @@ run-name: "Test Safe Output - Add Issue Comment" jobs: test-safe-output-add-issue-comment: runs-on: ubuntu-latest - permissions: - actions: write - contents: read - issues: read - pull-requests: read + permissions: read-all outputs: output: ${{ steps.collect_output.outputs.output }} steps: - - name: Check team membership for workflow - id: check-team-member - uses: actions/github-script@v7 - env: - GITHUB_AW_REQUIRED_ROLES: admin,maintainer - with: - script: | - // Custom setCancelled function that uses self-cancellation - async function setCancelled(message) { - try { - // Cancel the current workflow run using GitHub Actions API - await github.rest.actions.cancelWorkflowRun({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: context.runId - }); - core.info(`Cancellation requested for this workflow run: ${message}`); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - core.warning(`Failed to cancel workflow run: ${errorMessage}`); - // Fallback to core.setCancelled if API call fails - core.setCancelled(message); - } - } - async function main() { - const { eventName } = context; - // skip check for safe events - const safeEvents = ["workflow_dispatch", "workflow_run", "schedule"]; - if (safeEvents.includes(eventName)) { - console.log(`✅ Event ${eventName} does not require validation`); - return; - } - const actor = context.actor; - const { owner, repo } = context.repo; - const requiredPermissionsEnv = process.env.GITHUB_AW_REQUIRED_ROLES; - const requiredPermissions = requiredPermissionsEnv - ? requiredPermissionsEnv.split(",").filter(p => p.trim() !== "") - : []; - if (!requiredPermissions || requiredPermissions.length === 0) { - core.error( - "❌ Configuration error: Required permissions not specified. Contact repository administrator." - ); - await setCancelled( - "Configuration error: Required permissions not specified" - ); - return; - } - // Check if the actor has the required repository permissions - try { - console.log( - `Checking if user '${actor}' has required permissions for ${owner}/${repo}` - ); - console.log(`Required permissions: ${requiredPermissions.join(", ")}`); - const repoPermission = - await github.rest.repos.getCollaboratorPermissionLevel({ - owner: owner, - repo: repo, - username: actor, - }); - const permission = repoPermission.data.permission; - console.log(`Repository permission level: ${permission}`); - // Check if user has one of the required permission levels - for (const requiredPerm of requiredPermissions) { - if ( - permission === requiredPerm || - (requiredPerm === "maintainer" && permission === "maintain") - ) { - console.log(`✅ User has ${permission} access to repository`); - return; - } - } - console.log( - `User permission '${permission}' does not meet requirements: ${requiredPermissions.join(", ")}` - ); - } catch (repoError) { - const errorMessage = - repoError instanceof Error ? repoError.message : String(repoError); - core.error(`Repository permission check failed: ${errorMessage}`); - await setCancelled(`Repository permission check failed: ${errorMessage}`); - return; - } - // Cancel the job when permission check fails - core.warning( - `❌ Access denied: Only authorized users can trigger this workflow. User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}` - ); - await setCancelled( - `Access denied: User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}` - ); - return; - } - await main(); - name: Checkout repository uses: actions/checkout@v5 - name: Setup agent output diff --git a/.github/workflows/test-safe-output-add-issue-label.lock.yml b/.github/workflows/test-safe-output-add-issue-label.lock.yml index 35fd5beca22..d75b2e327fd 100644 --- a/.github/workflows/test-safe-output-add-issue-label.lock.yml +++ b/.github/workflows/test-safe-output-add-issue-label.lock.yml @@ -25,105 +25,10 @@ run-name: "Test Safe Output - Add Issue Label" jobs: test-safe-output-add-issue-label: runs-on: ubuntu-latest - permissions: - actions: write - contents: read - issues: read - pull-requests: read + permissions: read-all outputs: output: ${{ steps.collect_output.outputs.output }} steps: - - name: Check team membership for workflow - id: check-team-member - uses: actions/github-script@v7 - env: - GITHUB_AW_REQUIRED_ROLES: admin,maintainer - with: - script: | - // Custom setCancelled function that uses self-cancellation - async function setCancelled(message) { - try { - // Cancel the current workflow run using GitHub Actions API - await github.rest.actions.cancelWorkflowRun({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: context.runId - }); - core.info(`Cancellation requested for this workflow run: ${message}`); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - core.warning(`Failed to cancel workflow run: ${errorMessage}`); - // Fallback to core.setCancelled if API call fails - core.setCancelled(message); - } - } - async function main() { - const { eventName } = context; - // skip check for safe events - const safeEvents = ["workflow_dispatch", "workflow_run", "schedule"]; - if (safeEvents.includes(eventName)) { - console.log(`✅ Event ${eventName} does not require validation`); - return; - } - const actor = context.actor; - const { owner, repo } = context.repo; - const requiredPermissionsEnv = process.env.GITHUB_AW_REQUIRED_ROLES; - const requiredPermissions = requiredPermissionsEnv - ? requiredPermissionsEnv.split(",").filter(p => p.trim() !== "") - : []; - if (!requiredPermissions || requiredPermissions.length === 0) { - core.error( - "❌ Configuration error: Required permissions not specified. Contact repository administrator." - ); - await setCancelled( - "Configuration error: Required permissions not specified" - ); - return; - } - // Check if the actor has the required repository permissions - try { - console.log( - `Checking if user '${actor}' has required permissions for ${owner}/${repo}` - ); - console.log(`Required permissions: ${requiredPermissions.join(", ")}`); - const repoPermission = - await github.rest.repos.getCollaboratorPermissionLevel({ - owner: owner, - repo: repo, - username: actor, - }); - const permission = repoPermission.data.permission; - console.log(`Repository permission level: ${permission}`); - // Check if user has one of the required permission levels - for (const requiredPerm of requiredPermissions) { - if ( - permission === requiredPerm || - (requiredPerm === "maintainer" && permission === "maintain") - ) { - console.log(`✅ User has ${permission} access to repository`); - return; - } - } - console.log( - `User permission '${permission}' does not meet requirements: ${requiredPermissions.join(", ")}` - ); - } catch (repoError) { - const errorMessage = - repoError instanceof Error ? repoError.message : String(repoError); - core.error(`Repository permission check failed: ${errorMessage}`); - await setCancelled(`Repository permission check failed: ${errorMessage}`); - return; - } - // Cancel the job when permission check fails - core.warning( - `❌ Access denied: Only authorized users can trigger this workflow. User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}` - ); - await setCancelled( - `Access denied: User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}` - ); - return; - } - await main(); - name: Checkout repository uses: actions/checkout@v5 - name: Setup agent output diff --git a/.github/workflows/test-safe-output-create-code-scanning-alert.lock.yml b/.github/workflows/test-safe-output-create-code-scanning-alert.lock.yml index 1ca1b3290ae..497504f30c8 100644 --- a/.github/workflows/test-safe-output-create-code-scanning-alert.lock.yml +++ b/.github/workflows/test-safe-output-create-code-scanning-alert.lock.yml @@ -27,105 +27,10 @@ run-name: "Test Safe Output - Create Code Scanning Alert" jobs: test-safe-output-create-code-scanning-alert: runs-on: ubuntu-latest - permissions: - actions: write - contents: read - issues: read - pull-requests: read + permissions: read-all outputs: output: ${{ steps.collect_output.outputs.output }} steps: - - name: Check team membership for workflow - id: check-team-member - uses: actions/github-script@v7 - env: - GITHUB_AW_REQUIRED_ROLES: admin,maintainer - with: - script: | - // Custom setCancelled function that uses self-cancellation - async function setCancelled(message) { - try { - // Cancel the current workflow run using GitHub Actions API - await github.rest.actions.cancelWorkflowRun({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: context.runId - }); - core.info(`Cancellation requested for this workflow run: ${message}`); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - core.warning(`Failed to cancel workflow run: ${errorMessage}`); - // Fallback to core.setCancelled if API call fails - core.setCancelled(message); - } - } - async function main() { - const { eventName } = context; - // skip check for safe events - const safeEvents = ["workflow_dispatch", "workflow_run", "schedule"]; - if (safeEvents.includes(eventName)) { - console.log(`✅ Event ${eventName} does not require validation`); - return; - } - const actor = context.actor; - const { owner, repo } = context.repo; - const requiredPermissionsEnv = process.env.GITHUB_AW_REQUIRED_ROLES; - const requiredPermissions = requiredPermissionsEnv - ? requiredPermissionsEnv.split(",").filter(p => p.trim() !== "") - : []; - if (!requiredPermissions || requiredPermissions.length === 0) { - core.error( - "❌ Configuration error: Required permissions not specified. Contact repository administrator." - ); - await setCancelled( - "Configuration error: Required permissions not specified" - ); - return; - } - // Check if the actor has the required repository permissions - try { - console.log( - `Checking if user '${actor}' has required permissions for ${owner}/${repo}` - ); - console.log(`Required permissions: ${requiredPermissions.join(", ")}`); - const repoPermission = - await github.rest.repos.getCollaboratorPermissionLevel({ - owner: owner, - repo: repo, - username: actor, - }); - const permission = repoPermission.data.permission; - console.log(`Repository permission level: ${permission}`); - // Check if user has one of the required permission levels - for (const requiredPerm of requiredPermissions) { - if ( - permission === requiredPerm || - (requiredPerm === "maintainer" && permission === "maintain") - ) { - console.log(`✅ User has ${permission} access to repository`); - return; - } - } - console.log( - `User permission '${permission}' does not meet requirements: ${requiredPermissions.join(", ")}` - ); - } catch (repoError) { - const errorMessage = - repoError instanceof Error ? repoError.message : String(repoError); - core.error(`Repository permission check failed: ${errorMessage}`); - await setCancelled(`Repository permission check failed: ${errorMessage}`); - return; - } - // Cancel the job when permission check fails - core.warning( - `❌ Access denied: Only authorized users can trigger this workflow. User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}` - ); - await setCancelled( - `Access denied: User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}` - ); - return; - } - await main(); - name: Checkout repository uses: actions/checkout@v5 - name: Setup agent output diff --git a/.github/workflows/test-safe-output-create-discussion.lock.yml b/.github/workflows/test-safe-output-create-discussion.lock.yml index 5705b159e79..895df572536 100644 --- a/.github/workflows/test-safe-output-create-discussion.lock.yml +++ b/.github/workflows/test-safe-output-create-discussion.lock.yml @@ -22,105 +22,10 @@ run-name: "Test Safe Output - Create Discussion" jobs: test-safe-output-create-discussion: runs-on: ubuntu-latest - permissions: - actions: write - contents: read - issues: read - pull-requests: read + permissions: read-all outputs: output: ${{ steps.collect_output.outputs.output }} steps: - - name: Check team membership for workflow - id: check-team-member - uses: actions/github-script@v7 - env: - GITHUB_AW_REQUIRED_ROLES: admin,maintainer - with: - script: | - // Custom setCancelled function that uses self-cancellation - async function setCancelled(message) { - try { - // Cancel the current workflow run using GitHub Actions API - await github.rest.actions.cancelWorkflowRun({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: context.runId - }); - core.info(`Cancellation requested for this workflow run: ${message}`); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - core.warning(`Failed to cancel workflow run: ${errorMessage}`); - // Fallback to core.setCancelled if API call fails - core.setCancelled(message); - } - } - async function main() { - const { eventName } = context; - // skip check for safe events - const safeEvents = ["workflow_dispatch", "workflow_run", "schedule"]; - if (safeEvents.includes(eventName)) { - console.log(`✅ Event ${eventName} does not require validation`); - return; - } - const actor = context.actor; - const { owner, repo } = context.repo; - const requiredPermissionsEnv = process.env.GITHUB_AW_REQUIRED_ROLES; - const requiredPermissions = requiredPermissionsEnv - ? requiredPermissionsEnv.split(",").filter(p => p.trim() !== "") - : []; - if (!requiredPermissions || requiredPermissions.length === 0) { - core.error( - "❌ Configuration error: Required permissions not specified. Contact repository administrator." - ); - await setCancelled( - "Configuration error: Required permissions not specified" - ); - return; - } - // Check if the actor has the required repository permissions - try { - console.log( - `Checking if user '${actor}' has required permissions for ${owner}/${repo}` - ); - console.log(`Required permissions: ${requiredPermissions.join(", ")}`); - const repoPermission = - await github.rest.repos.getCollaboratorPermissionLevel({ - owner: owner, - repo: repo, - username: actor, - }); - const permission = repoPermission.data.permission; - console.log(`Repository permission level: ${permission}`); - // Check if user has one of the required permission levels - for (const requiredPerm of requiredPermissions) { - if ( - permission === requiredPerm || - (requiredPerm === "maintainer" && permission === "maintain") - ) { - console.log(`✅ User has ${permission} access to repository`); - return; - } - } - console.log( - `User permission '${permission}' does not meet requirements: ${requiredPermissions.join(", ")}` - ); - } catch (repoError) { - const errorMessage = - repoError instanceof Error ? repoError.message : String(repoError); - core.error(`Repository permission check failed: ${errorMessage}`); - await setCancelled(`Repository permission check failed: ${errorMessage}`); - return; - } - // Cancel the job when permission check fails - core.warning( - `❌ Access denied: Only authorized users can trigger this workflow. User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}` - ); - await setCancelled( - `Access denied: User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}` - ); - return; - } - await main(); - name: Checkout repository uses: actions/checkout@v5 - name: Setup agent output diff --git a/.github/workflows/test-safe-output-create-issue.lock.yml b/.github/workflows/test-safe-output-create-issue.lock.yml index 5ab4d6a708a..4ffcdca9c39 100644 --- a/.github/workflows/test-safe-output-create-issue.lock.yml +++ b/.github/workflows/test-safe-output-create-issue.lock.yml @@ -19,105 +19,10 @@ run-name: "Test Safe Output - Create Issue" jobs: test-safe-output-create-issue: runs-on: ubuntu-latest - permissions: - actions: write - contents: read - issues: read - pull-requests: read + permissions: read-all outputs: output: ${{ steps.collect_output.outputs.output }} steps: - - name: Check team membership for workflow - id: check-team-member - uses: actions/github-script@v7 - env: - GITHUB_AW_REQUIRED_ROLES: admin,maintainer - with: - script: | - // Custom setCancelled function that uses self-cancellation - async function setCancelled(message) { - try { - // Cancel the current workflow run using GitHub Actions API - await github.rest.actions.cancelWorkflowRun({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: context.runId - }); - core.info(`Cancellation requested for this workflow run: ${message}`); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - core.warning(`Failed to cancel workflow run: ${errorMessage}`); - // Fallback to core.setCancelled if API call fails - core.setCancelled(message); - } - } - async function main() { - const { eventName } = context; - // skip check for safe events - const safeEvents = ["workflow_dispatch", "workflow_run", "schedule"]; - if (safeEvents.includes(eventName)) { - console.log(`✅ Event ${eventName} does not require validation`); - return; - } - const actor = context.actor; - const { owner, repo } = context.repo; - const requiredPermissionsEnv = process.env.GITHUB_AW_REQUIRED_ROLES; - const requiredPermissions = requiredPermissionsEnv - ? requiredPermissionsEnv.split(",").filter(p => p.trim() !== "") - : []; - if (!requiredPermissions || requiredPermissions.length === 0) { - core.error( - "❌ Configuration error: Required permissions not specified. Contact repository administrator." - ); - await setCancelled( - "Configuration error: Required permissions not specified" - ); - return; - } - // Check if the actor has the required repository permissions - try { - console.log( - `Checking if user '${actor}' has required permissions for ${owner}/${repo}` - ); - console.log(`Required permissions: ${requiredPermissions.join(", ")}`); - const repoPermission = - await github.rest.repos.getCollaboratorPermissionLevel({ - owner: owner, - repo: repo, - username: actor, - }); - const permission = repoPermission.data.permission; - console.log(`Repository permission level: ${permission}`); - // Check if user has one of the required permission levels - for (const requiredPerm of requiredPermissions) { - if ( - permission === requiredPerm || - (requiredPerm === "maintainer" && permission === "maintain") - ) { - console.log(`✅ User has ${permission} access to repository`); - return; - } - } - console.log( - `User permission '${permission}' does not meet requirements: ${requiredPermissions.join(", ")}` - ); - } catch (repoError) { - const errorMessage = - repoError instanceof Error ? repoError.message : String(repoError); - core.error(`Repository permission check failed: ${errorMessage}`); - await setCancelled(`Repository permission check failed: ${errorMessage}`); - return; - } - // Cancel the job when permission check fails - core.warning( - `❌ Access denied: Only authorized users can trigger this workflow. User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}` - ); - await setCancelled( - `Access denied: User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}` - ); - return; - } - await main(); - name: Checkout repository uses: actions/checkout@v5 - name: Setup agent output @@ -1118,7 +1023,6 @@ jobs: needs: test-safe-output-create-issue runs-on: ubuntu-latest permissions: - actions: write contents: read issues: write timeout-minutes: 10 @@ -1126,97 +1030,6 @@ jobs: issue_number: ${{ steps.create_issue.outputs.issue_number }} issue_url: ${{ steps.create_issue.outputs.issue_url }} steps: - - name: Check team membership for workflow - id: check-team-member - uses: actions/github-script@v7 - env: - GITHUB_AW_REQUIRED_ROLES: admin,maintainer - with: - script: | - // Custom setCancelled function that uses self-cancellation - async function setCancelled(message) { - try { - // Cancel the current workflow run using GitHub Actions API - await github.rest.actions.cancelWorkflowRun({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: context.runId - }); - core.info(`Cancellation requested for this workflow run: ${message}`); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - core.warning(`Failed to cancel workflow run: ${errorMessage}`); - // Fallback to core.setCancelled if API call fails - core.setCancelled(message); - } - } - async function main() { - const { eventName } = context; - // skip check for safe events - const safeEvents = ["workflow_dispatch", "workflow_run", "schedule"]; - if (safeEvents.includes(eventName)) { - console.log(`✅ Event ${eventName} does not require validation`); - return; - } - const actor = context.actor; - const { owner, repo } = context.repo; - const requiredPermissionsEnv = process.env.GITHUB_AW_REQUIRED_ROLES; - const requiredPermissions = requiredPermissionsEnv - ? requiredPermissionsEnv.split(",").filter(p => p.trim() !== "") - : []; - if (!requiredPermissions || requiredPermissions.length === 0) { - core.error( - "❌ Configuration error: Required permissions not specified. Contact repository administrator." - ); - await setCancelled( - "Configuration error: Required permissions not specified" - ); - return; - } - // Check if the actor has the required repository permissions - try { - console.log( - `Checking if user '${actor}' has required permissions for ${owner}/${repo}` - ); - console.log(`Required permissions: ${requiredPermissions.join(", ")}`); - const repoPermission = - await github.rest.repos.getCollaboratorPermissionLevel({ - owner: owner, - repo: repo, - username: actor, - }); - const permission = repoPermission.data.permission; - console.log(`Repository permission level: ${permission}`); - // Check if user has one of the required permission levels - for (const requiredPerm of requiredPermissions) { - if ( - permission === requiredPerm || - (requiredPerm === "maintainer" && permission === "maintain") - ) { - console.log(`✅ User has ${permission} access to repository`); - return; - } - } - console.log( - `User permission '${permission}' does not meet requirements: ${requiredPermissions.join(", ")}` - ); - } catch (repoError) { - const errorMessage = - repoError instanceof Error ? repoError.message : String(repoError); - core.error(`Repository permission check failed: ${errorMessage}`); - await setCancelled(`Repository permission check failed: ${errorMessage}`); - return; - } - // Cancel the job when permission check fails - core.warning( - `❌ Access denied: Only authorized users can trigger this workflow. User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}` - ); - await setCancelled( - `Access denied: User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}` - ); - return; - } - await main(); - name: Create Output Issue id: create_issue uses: actions/github-script@v7 diff --git a/.github/workflows/test-safe-output-create-pull-request-review-comment.lock.yml b/.github/workflows/test-safe-output-create-pull-request-review-comment.lock.yml index a06cd9204aa..485b3b7b802 100644 --- a/.github/workflows/test-safe-output-create-pull-request-review-comment.lock.yml +++ b/.github/workflows/test-safe-output-create-pull-request-review-comment.lock.yml @@ -21,105 +21,10 @@ run-name: "Test Safe Output - Create Pull Request Review Comment" jobs: test-safe-output-create-pull-request-review-comment: runs-on: ubuntu-latest - permissions: - actions: write - contents: read - issues: read - pull-requests: read + permissions: read-all outputs: output: ${{ steps.collect_output.outputs.output }} steps: - - name: Check team membership for workflow - id: check-team-member - uses: actions/github-script@v7 - env: - GITHUB_AW_REQUIRED_ROLES: admin,maintainer - with: - script: | - // Custom setCancelled function that uses self-cancellation - async function setCancelled(message) { - try { - // Cancel the current workflow run using GitHub Actions API - await github.rest.actions.cancelWorkflowRun({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: context.runId - }); - core.info(`Cancellation requested for this workflow run: ${message}`); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - core.warning(`Failed to cancel workflow run: ${errorMessage}`); - // Fallback to core.setCancelled if API call fails - core.setCancelled(message); - } - } - async function main() { - const { eventName } = context; - // skip check for safe events - const safeEvents = ["workflow_dispatch", "workflow_run", "schedule"]; - if (safeEvents.includes(eventName)) { - console.log(`✅ Event ${eventName} does not require validation`); - return; - } - const actor = context.actor; - const { owner, repo } = context.repo; - const requiredPermissionsEnv = process.env.GITHUB_AW_REQUIRED_ROLES; - const requiredPermissions = requiredPermissionsEnv - ? requiredPermissionsEnv.split(",").filter(p => p.trim() !== "") - : []; - if (!requiredPermissions || requiredPermissions.length === 0) { - core.error( - "❌ Configuration error: Required permissions not specified. Contact repository administrator." - ); - await setCancelled( - "Configuration error: Required permissions not specified" - ); - return; - } - // Check if the actor has the required repository permissions - try { - console.log( - `Checking if user '${actor}' has required permissions for ${owner}/${repo}` - ); - console.log(`Required permissions: ${requiredPermissions.join(", ")}`); - const repoPermission = - await github.rest.repos.getCollaboratorPermissionLevel({ - owner: owner, - repo: repo, - username: actor, - }); - const permission = repoPermission.data.permission; - console.log(`Repository permission level: ${permission}`); - // Check if user has one of the required permission levels - for (const requiredPerm of requiredPermissions) { - if ( - permission === requiredPerm || - (requiredPerm === "maintainer" && permission === "maintain") - ) { - console.log(`✅ User has ${permission} access to repository`); - return; - } - } - console.log( - `User permission '${permission}' does not meet requirements: ${requiredPermissions.join(", ")}` - ); - } catch (repoError) { - const errorMessage = - repoError instanceof Error ? repoError.message : String(repoError); - core.error(`Repository permission check failed: ${errorMessage}`); - await setCancelled(`Repository permission check failed: ${errorMessage}`); - return; - } - // Cancel the job when permission check fails - core.warning( - `❌ Access denied: Only authorized users can trigger this workflow. User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}` - ); - await setCancelled( - `Access denied: User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}` - ); - return; - } - await main(); - name: Checkout repository uses: actions/checkout@v5 - name: Setup agent output diff --git a/.github/workflows/test-safe-output-create-pull-request.lock.yml b/.github/workflows/test-safe-output-create-pull-request.lock.yml index 71d4c8a7526..002622b178c 100644 --- a/.github/workflows/test-safe-output-create-pull-request.lock.yml +++ b/.github/workflows/test-safe-output-create-pull-request.lock.yml @@ -20,105 +20,10 @@ run-name: "Test Safe Output - Create Pull Request" jobs: test-safe-output-create-pull-request: runs-on: ubuntu-latest - permissions: - actions: write - contents: read - issues: read - pull-requests: read + permissions: read-all outputs: output: ${{ steps.collect_output.outputs.output }} steps: - - name: Check team membership for workflow - id: check-team-member - uses: actions/github-script@v7 - env: - GITHUB_AW_REQUIRED_ROLES: admin,maintainer - with: - script: | - // Custom setCancelled function that uses self-cancellation - async function setCancelled(message) { - try { - // Cancel the current workflow run using GitHub Actions API - await github.rest.actions.cancelWorkflowRun({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: context.runId - }); - core.info(`Cancellation requested for this workflow run: ${message}`); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - core.warning(`Failed to cancel workflow run: ${errorMessage}`); - // Fallback to core.setCancelled if API call fails - core.setCancelled(message); - } - } - async function main() { - const { eventName } = context; - // skip check for safe events - const safeEvents = ["workflow_dispatch", "workflow_run", "schedule"]; - if (safeEvents.includes(eventName)) { - console.log(`✅ Event ${eventName} does not require validation`); - return; - } - const actor = context.actor; - const { owner, repo } = context.repo; - const requiredPermissionsEnv = process.env.GITHUB_AW_REQUIRED_ROLES; - const requiredPermissions = requiredPermissionsEnv - ? requiredPermissionsEnv.split(",").filter(p => p.trim() !== "") - : []; - if (!requiredPermissions || requiredPermissions.length === 0) { - core.error( - "❌ Configuration error: Required permissions not specified. Contact repository administrator." - ); - await setCancelled( - "Configuration error: Required permissions not specified" - ); - return; - } - // Check if the actor has the required repository permissions - try { - console.log( - `Checking if user '${actor}' has required permissions for ${owner}/${repo}` - ); - console.log(`Required permissions: ${requiredPermissions.join(", ")}`); - const repoPermission = - await github.rest.repos.getCollaboratorPermissionLevel({ - owner: owner, - repo: repo, - username: actor, - }); - const permission = repoPermission.data.permission; - console.log(`Repository permission level: ${permission}`); - // Check if user has one of the required permission levels - for (const requiredPerm of requiredPermissions) { - if ( - permission === requiredPerm || - (requiredPerm === "maintainer" && permission === "maintain") - ) { - console.log(`✅ User has ${permission} access to repository`); - return; - } - } - console.log( - `User permission '${permission}' does not meet requirements: ${requiredPermissions.join(", ")}` - ); - } catch (repoError) { - const errorMessage = - repoError instanceof Error ? repoError.message : String(repoError); - core.error(`Repository permission check failed: ${errorMessage}`); - await setCancelled(`Repository permission check failed: ${errorMessage}`); - return; - } - // Cancel the job when permission check fails - core.warning( - `❌ Access denied: Only authorized users can trigger this workflow. User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}` - ); - await setCancelled( - `Access denied: User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}` - ); - return; - } - await main(); - name: Checkout repository uses: actions/checkout@v5 - name: Configure Git credentials diff --git a/.github/workflows/test-safe-output-push-to-pr-branch.lock.yml b/.github/workflows/test-safe-output-push-to-pr-branch.lock.yml index 9c69dc28a50..8b6b7f89f67 100644 --- a/.github/workflows/test-safe-output-push-to-pr-branch.lock.yml +++ b/.github/workflows/test-safe-output-push-to-pr-branch.lock.yml @@ -21,105 +21,10 @@ run-name: "Test Safe Output - Push to PR Branch" jobs: test-safe-output-push-to-pr-branch: runs-on: ubuntu-latest - permissions: - actions: write - contents: read - issues: read - pull-requests: read + permissions: read-all outputs: output: ${{ steps.collect_output.outputs.output }} steps: - - name: Check team membership for workflow - id: check-team-member - uses: actions/github-script@v7 - env: - GITHUB_AW_REQUIRED_ROLES: admin,maintainer - with: - script: | - // Custom setCancelled function that uses self-cancellation - async function setCancelled(message) { - try { - // Cancel the current workflow run using GitHub Actions API - await github.rest.actions.cancelWorkflowRun({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: context.runId - }); - core.info(`Cancellation requested for this workflow run: ${message}`); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - core.warning(`Failed to cancel workflow run: ${errorMessage}`); - // Fallback to core.setCancelled if API call fails - core.setCancelled(message); - } - } - async function main() { - const { eventName } = context; - // skip check for safe events - const safeEvents = ["workflow_dispatch", "workflow_run", "schedule"]; - if (safeEvents.includes(eventName)) { - console.log(`✅ Event ${eventName} does not require validation`); - return; - } - const actor = context.actor; - const { owner, repo } = context.repo; - const requiredPermissionsEnv = process.env.GITHUB_AW_REQUIRED_ROLES; - const requiredPermissions = requiredPermissionsEnv - ? requiredPermissionsEnv.split(",").filter(p => p.trim() !== "") - : []; - if (!requiredPermissions || requiredPermissions.length === 0) { - core.error( - "❌ Configuration error: Required permissions not specified. Contact repository administrator." - ); - await setCancelled( - "Configuration error: Required permissions not specified" - ); - return; - } - // Check if the actor has the required repository permissions - try { - console.log( - `Checking if user '${actor}' has required permissions for ${owner}/${repo}` - ); - console.log(`Required permissions: ${requiredPermissions.join(", ")}`); - const repoPermission = - await github.rest.repos.getCollaboratorPermissionLevel({ - owner: owner, - repo: repo, - username: actor, - }); - const permission = repoPermission.data.permission; - console.log(`Repository permission level: ${permission}`); - // Check if user has one of the required permission levels - for (const requiredPerm of requiredPermissions) { - if ( - permission === requiredPerm || - (requiredPerm === "maintainer" && permission === "maintain") - ) { - console.log(`✅ User has ${permission} access to repository`); - return; - } - } - console.log( - `User permission '${permission}' does not meet requirements: ${requiredPermissions.join(", ")}` - ); - } catch (repoError) { - const errorMessage = - repoError instanceof Error ? repoError.message : String(repoError); - core.error(`Repository permission check failed: ${errorMessage}`); - await setCancelled(`Repository permission check failed: ${errorMessage}`); - return; - } - // Cancel the job when permission check fails - core.warning( - `❌ Access denied: Only authorized users can trigger this workflow. User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}` - ); - await setCancelled( - `Access denied: User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}` - ); - return; - } - await main(); - name: Checkout repository uses: actions/checkout@v5 - name: Configure Git credentials diff --git a/.github/workflows/test-safe-output-update-issue.lock.yml b/.github/workflows/test-safe-output-update-issue.lock.yml index e21428d3215..34d61157bb2 100644 --- a/.github/workflows/test-safe-output-update-issue.lock.yml +++ b/.github/workflows/test-safe-output-update-issue.lock.yml @@ -20,105 +20,10 @@ run-name: "Test Safe Output - Update Issue" jobs: test-safe-output-update-issue: runs-on: ubuntu-latest - permissions: - actions: write - contents: read - issues: read - pull-requests: read + permissions: read-all outputs: output: ${{ steps.collect_output.outputs.output }} steps: - - name: Check team membership for workflow - id: check-team-member - uses: actions/github-script@v7 - env: - GITHUB_AW_REQUIRED_ROLES: admin,maintainer - with: - script: | - // Custom setCancelled function that uses self-cancellation - async function setCancelled(message) { - try { - // Cancel the current workflow run using GitHub Actions API - await github.rest.actions.cancelWorkflowRun({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: context.runId - }); - core.info(`Cancellation requested for this workflow run: ${message}`); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - core.warning(`Failed to cancel workflow run: ${errorMessage}`); - // Fallback to core.setCancelled if API call fails - core.setCancelled(message); - } - } - async function main() { - const { eventName } = context; - // skip check for safe events - const safeEvents = ["workflow_dispatch", "workflow_run", "schedule"]; - if (safeEvents.includes(eventName)) { - console.log(`✅ Event ${eventName} does not require validation`); - return; - } - const actor = context.actor; - const { owner, repo } = context.repo; - const requiredPermissionsEnv = process.env.GITHUB_AW_REQUIRED_ROLES; - const requiredPermissions = requiredPermissionsEnv - ? requiredPermissionsEnv.split(",").filter(p => p.trim() !== "") - : []; - if (!requiredPermissions || requiredPermissions.length === 0) { - core.error( - "❌ Configuration error: Required permissions not specified. Contact repository administrator." - ); - await setCancelled( - "Configuration error: Required permissions not specified" - ); - return; - } - // Check if the actor has the required repository permissions - try { - console.log( - `Checking if user '${actor}' has required permissions for ${owner}/${repo}` - ); - console.log(`Required permissions: ${requiredPermissions.join(", ")}`); - const repoPermission = - await github.rest.repos.getCollaboratorPermissionLevel({ - owner: owner, - repo: repo, - username: actor, - }); - const permission = repoPermission.data.permission; - console.log(`Repository permission level: ${permission}`); - // Check if user has one of the required permission levels - for (const requiredPerm of requiredPermissions) { - if ( - permission === requiredPerm || - (requiredPerm === "maintainer" && permission === "maintain") - ) { - console.log(`✅ User has ${permission} access to repository`); - return; - } - } - console.log( - `User permission '${permission}' does not meet requirements: ${requiredPermissions.join(", ")}` - ); - } catch (repoError) { - const errorMessage = - repoError instanceof Error ? repoError.message : String(repoError); - core.error(`Repository permission check failed: ${errorMessage}`); - await setCancelled(`Repository permission check failed: ${errorMessage}`); - return; - } - // Cancel the job when permission check fails - core.warning( - `❌ Access denied: Only authorized users can trigger this workflow. User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}` - ); - await setCancelled( - `Access denied: User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}` - ); - return; - } - await main(); - name: Checkout repository uses: actions/checkout@v5 - name: Setup agent output diff --git a/pkg/workflow/actions_write_permission_test.go b/pkg/workflow/actions_write_permission_test.go new file mode 100644 index 00000000000..dd91f8acd1f --- /dev/null +++ b/pkg/workflow/actions_write_permission_test.go @@ -0,0 +1,259 @@ +package workflow + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +// TestActionsWritePermissionForSelfCancellation tests that actions: write permission +// is added to jobs that include team member checks for self-cancellation functionality +func TestActionsWritePermissionForSelfCancellation(t *testing.T) { + // Create temporary directory for test files + tmpDir, err := os.MkdirTemp("", "actions-write-permission-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + compiler := NewCompiler(false, "", "test") + + tests := []struct { + name string + frontmatter string + filename string + expectActionsWrite bool + jobName string + description string + }{ + { + name: "command workflow task job should have actions: write", + frontmatter: `--- +on: + command: + name: test-bot +tools: + github: + allowed: [list_issues] +--- + +# Command Workflow +Test workflow with command trigger.`, + filename: "command-workflow.md", + expectActionsWrite: true, + jobName: "task", + description: "Task job should have actions: write for self-cancellation", + }, + { + name: "push workflow main job should have actions: write", + frontmatter: `--- +on: + push: + branches: [main] +tools: + github: + allowed: [list_issues] +--- + +# Push Workflow +Test workflow with push trigger that needs permission checks.`, + filename: "push-workflow.md", + expectActionsWrite: true, + jobName: "push-workflow", + description: "Main job should have actions: write for permission checks", + }, + { + name: "workflow_dispatch should not have actions: write", + frontmatter: `--- +on: + workflow_dispatch: +tools: + github: + allowed: [list_issues] +--- + +# Workflow Dispatch +Test workflow with safe event.`, + filename: "workflow-dispatch.md", + expectActionsWrite: false, + jobName: "", + description: "Safe events should not need actions: write permission", + }, + { + name: "schedule workflow should not have actions: write", + frontmatter: `--- +on: + schedule: + - cron: "0 9 * * 1" +tools: + github: + allowed: [list_issues] +--- + +# Schedule Workflow +Test workflow with schedule trigger.`, + filename: "schedule-workflow.md", + expectActionsWrite: false, + jobName: "", + description: "Schedule events should not need actions: write permission", + }, + { + name: "roles: all should not have actions: write", + frontmatter: `--- +on: + push: + branches: [main] +roles: all +tools: + github: + allowed: [list_issues] +--- + +# Unrestricted Workflow +Test workflow with unrestricted access.`, + filename: "unrestricted-workflow.md", + expectActionsWrite: false, + jobName: "", + description: "Unrestricted workflows should not need actions: write permission", + }, + { + name: "main job should have actions: write when no task job", + frontmatter: `--- +on: + issues: + types: [opened] +tools: + github: + allowed: [list_issues] +--- + +# Issues Workflow +Test workflow with permission checks but no task job.`, + filename: "issues-workflow.md", + expectActionsWrite: true, + jobName: "issues-workflow", + description: "Main job should have actions: write for permission checks", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create test file + testFile := filepath.Join(tmpDir, tt.filename) + err := os.WriteFile(testFile, []byte(tt.frontmatter), 0644) + if err != nil { + t.Fatal(err) + } + + // Compile the workflow + err = compiler.CompileWorkflow(testFile) + if err != nil { + t.Fatalf("Failed to compile workflow: %v", err) + } + + // Read the generated lock file + lockFile := strings.TrimSuffix(testFile, ".md") + ".lock.yml" + lockContent, err := os.ReadFile(lockFile) + if err != nil { + t.Fatalf("Failed to read lock file: %v", err) + } + lockContentStr := string(lockContent) + + if tt.expectActionsWrite { + // Check that the specified job exists + if !strings.Contains(lockContentStr, " "+tt.jobName+":") { + t.Fatalf("Job '%s' not found in workflow", tt.jobName) + } + + // Check for actions: write permission anywhere in the workflow (should be in the right job) + if !strings.Contains(lockContentStr, "permissions:") || !strings.Contains(lockContentStr, "actions: write") { + t.Errorf("%s: Expected 'actions: write' permission in workflow but not found", tt.description) + } + + // Check that setCancelled function is present + if !strings.Contains(lockContentStr, "async function setCancelled(message)") { + t.Errorf("%s: Expected custom setCancelled function but not found", tt.description) + } + + // Check that github.rest.actions.cancelWorkflowRun is called + if !strings.Contains(lockContentStr, "github.rest.actions.cancelWorkflowRun") { + t.Errorf("%s: Expected self-cancellation API call but not found", tt.description) + } + + } else { + // Check that actions: write permission is not present + if strings.Contains(lockContentStr, "actions: write") { + t.Errorf("%s: Did not expect 'actions: write' permission but found it", tt.description) + } + } + }) + } +} + +// TestMainJobActionsWritePermission tests that the main job gets actions: write +// permission when permission checks are needed and no task job exists +func TestMainJobActionsWritePermission(t *testing.T) { + // Create temporary directory for test files + tmpDir, err := os.MkdirTemp("", "main-job-actions-write-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + compiler := NewCompiler(false, "", "test") + + testContent := `--- +on: + issues: + types: [opened] +tools: + github: + allowed: [list_issues] +--- + +# Issues Workflow +This workflow needs permission checks but has no task job.` + + // Create test file + testFile := filepath.Join(tmpDir, "issues-workflow.md") + err = os.WriteFile(testFile, []byte(testContent), 0644) + if err != nil { + t.Fatal(err) + } + + // Compile the workflow + err = compiler.CompileWorkflow(testFile) + if err != nil { + t.Fatalf("Failed to compile workflow: %v", err) + } + + // Read the generated lock file + lockFile := strings.TrimSuffix(testFile, ".md") + ".lock.yml" + lockContent, err := os.ReadFile(lockFile) + if err != nil { + t.Fatalf("Failed to read lock file: %v", err) + } + lockContentStr := string(lockContent) + + // Check that the main job has actions: write permission + // Look for the main job (should be "issues-workflow" based on filename) + if !strings.Contains(lockContentStr, " issues-workflow:") { + t.Fatal("Main job 'issues-workflow' not found in workflow") + } + + // Check for actions: write permission anywhere in the workflow + if !strings.Contains(lockContentStr, "permissions:") || !strings.Contains(lockContentStr, "actions: write") { + t.Error("Expected 'actions: write' permission in main job but not found") + } + + // Check that permission check step is present + if !strings.Contains(lockContentStr, "Check team membership for workflow") { + t.Error("Expected team membership check step in main job but not found") + } + + // Check that setCancelled function is present + if !strings.Contains(lockContentStr, "async function setCancelled(message)") { + t.Error("Expected custom setCancelled function but not found") + } +} diff --git a/pkg/workflow/compiler.go b/pkg/workflow/compiler.go index 07de406b4c5..bf806215200 100644 --- a/pkg/workflow/compiler.go +++ b/pkg/workflow/compiler.go @@ -122,33 +122,34 @@ func NewCompilerWithCustomOutput(verbose bool, engineOverride string, customOutp // WorkflowData holds all the data needed to generate a GitHub Actions workflow type WorkflowData struct { - Name string - FrontmatterName string // name field from frontmatter (for code scanning alert driver default) - On string - Permissions string - Network string // top-level network permissions configuration - Concurrency string - RunName string - Env string - If string - TimeoutMinutes string - CustomSteps string - PostSteps string // steps to run after AI execution - RunsOn string - Tools map[string]any - MarkdownContent string - AI string // "claude" or "codex" (for backwards compatibility) - EngineConfig *EngineConfig // Extended engine configuration - StopTime string - Command string // for /command trigger support - CommandOtherEvents map[string]any // for merging command with other events - AIReaction string // AI reaction type like "eyes", "heart", etc. - Jobs map[string]any // custom job configurations with dependencies - Cache string // cache configuration - NeedsTextOutput bool // whether the workflow uses ${{ needs.task.outputs.text }} - NetworkPermissions *NetworkPermissions // parsed network permissions - SafeOutputs *SafeOutputsConfig // output configuration for automatic output routes - Roles []string // permission levels required to trigger workflow + Name string + FrontmatterName string // name field from frontmatter (for code scanning alert driver default) + On string + Permissions string + Network string // top-level network permissions configuration + Concurrency string + RunName string + Env string + If string + TimeoutMinutes string + CustomSteps string + PostSteps string // steps to run after AI execution + RunsOn string + Tools map[string]any + MarkdownContent string + AI string // "claude" or "codex" (for backwards compatibility) + EngineConfig *EngineConfig // Extended engine configuration + StopTime string + Command string // for /command trigger support + CommandOtherEvents map[string]any // for merging command with other events + AIReaction string // AI reaction type like "eyes", "heart", etc. + Jobs map[string]any // custom job configurations with dependencies + Cache string // cache configuration + NeedsTextOutput bool // whether the workflow uses ${{ needs.task.outputs.text }} + NetworkPermissions *NetworkPermissions // parsed network permissions + SafeOutputs *SafeOutputsConfig // output configuration for automatic output routes + Roles []string // permission levels required to trigger workflow + HasExplicitGitHubTools bool // whether GitHub tools were explicitly configured (vs default) } // SafeOutputsConfig holds configuration for automatic output routes @@ -1287,6 +1288,23 @@ func (c *Compiler) applyDefaults(data *WorkflowData, markdownPath string) { if data.RunsOn == "" { data.RunsOn = "runs-on: ubuntu-latest" } + + // Store whether tools were explicitly configured before applying defaults + hasExplicitTools := len(data.Tools) > 0 + if hasExplicitTools { + // Check if github tools are explicitly configured with non-empty allowed list + if githubTools, hasGithub := data.Tools["github"]; hasGithub { + if githubMap, ok := githubTools.(map[string]any); ok { + if allowed, hasAllowed := githubMap["allowed"]; hasAllowed { + if allowedList, ok := allowed.([]any); ok && len(allowedList) > 0 { + // Mark that explicit GitHub tools are configured + data.HasExplicitGitHubTools = true + } + } + } + } + } + // Apply default tools data.Tools = c.applyDefaultTools(data.Tools, data.SafeOutputs) } @@ -1755,11 +1773,8 @@ func (c *Compiler) needsPermissionChecks(data *WorkflowData) bool { return false } - // Permission checks are needed by default unless workflow uses only safe events - // Safe events: workflow_dispatch, workflow_run, schedule - // For now, we'll implement a simple heuristic since we don't have frontmatter here - // We'll implement the full logic later when we have access to frontmatter - return true + // Only need permission checks if GitHub tools are explicitly configured + return data.HasExplicitGitHubTools } // needsPermissionChecksWithFrontmatter determines if the workflow needs permission checks with full context @@ -1769,12 +1784,16 @@ func (c *Compiler) needsPermissionChecksWithFrontmatter(data *WorkflowData, fron return false } + // Only need permission checks if GitHub tools are explicitly configured + if !data.HasExplicitGitHubTools { + return false + } + // Check if the workflow uses only safe events (only if frontmatter is available) if frontmatter != nil && c.hasSafeEventsOnly(data, frontmatter) { return false } - // Permission checks are needed by default for non-safe events return true } diff --git a/pkg/workflow/compiler_test.go b/pkg/workflow/compiler_test.go index b7ab335f592..2c42dad155a 100644 --- a/pkg/workflow/compiler_test.go +++ b/pkg/workflow/compiler_test.go @@ -4202,13 +4202,17 @@ This workflow should get default permissions applied automatically. lockContentStr := string(lockContent) // Verify that default permissions are present in the generated workflow + // This test workflow has explicit GitHub tools (tools.github.allowed), so it should + // get specific permissions including actions: write for self-cancellation, + // NOT the default "read-all" permissions expectedDefaultPermissions := []string{ - "read-all", + "actions: write", // Required for self-cancellation + "contents: read", // Basic read access } for _, expectedPerm := range expectedDefaultPermissions { if !strings.Contains(lockContentStr, expectedPerm) { - t.Errorf("Expected default permission '%s' not found in generated workflow.\nGenerated content:\n%s", expectedPerm, lockContentStr) + t.Errorf("Expected permission '%s' not found in generated workflow.\nGenerated content:\n%s", expectedPerm, lockContentStr) } } @@ -4255,13 +4259,25 @@ This workflow should get default permissions applied automatically. t.Fatal("Permissions section not found in main job") } - // Verify permissions is a map - permissionsValue, ok := permissions.(string) - if !ok { - t.Fatal("Permissions section is not a string") - } - if permissionsValue != "read-all" { - t.Fatal("Default permissions not read-all") + // Verify permissions is either a string or a map + if permissionsStr, ok := permissions.(string); ok { + // Handle string permissions (like "read-all") + if !strings.Contains(permissionsStr, "actions: write") { + t.Fatalf("Expected permissions to include 'actions: write' for workflows with explicit GitHub tools, got: %s", permissionsStr) + } + if !strings.Contains(permissionsStr, "contents: read") { + t.Fatalf("Expected permissions to include 'contents: read' for workflows with explicit GitHub tools, got: %s", permissionsStr) + } + } else if permissionsMap, ok := permissions.(map[string]interface{}); ok { + // Handle map permissions (YAML object) + if actionsVal, hasActions := permissionsMap["actions"]; !hasActions || actionsVal != "write" { + t.Fatalf("Expected permissions to include 'actions: write' for workflows with explicit GitHub tools, got: %v", permissionsMap) + } + if contentsVal, hasContents := permissionsMap["contents"]; !hasContents || contentsVal != "read" { + t.Fatalf("Expected permissions to include 'contents: read' for workflows with explicit GitHub tools, got: %v", permissionsMap) + } + } else { + t.Fatalf("Permissions section is neither a string nor a map, got type: %T, value: %v", permissions, permissions) } } diff --git a/pkg/workflow/js.go b/pkg/workflow/js.go index 154ed7897f7..758cf14cf9d 100644 --- a/pkg/workflow/js.go +++ b/pkg/workflow/js.go @@ -51,9 +51,6 @@ var addReactionAndEditCommentScript string //go:embed js/check_permissions.cjs var checkPermissionsScript string -//go:embed js/check_team_member.cjs -var checkTeamMemberScript string - //go:embed js/parse_claude_log.cjs var parseClaudeLogScript string diff --git a/pkg/workflow/js/check_permissions.cjs b/pkg/workflow/js/check_permissions.cjs index 1b14376fe56..f6559b276c2 100644 --- a/pkg/workflow/js/check_permissions.cjs +++ b/pkg/workflow/js/check_permissions.cjs @@ -1,19 +1,22 @@ -// Custom setCancelled function that uses self-cancellation +/** + * Custom setCancelled function that uses self-cancellation + * @param {string} message - The cancellation message + */ async function setCancelled(message) { try { // Cancel the current workflow run using GitHub Actions API await github.rest.actions.cancelWorkflowRun({ owner: context.repo.owner, repo: context.repo.repo, - run_id: context.runId + run_id: context.runId, }); core.info(`Cancellation requested for this workflow run: ${message}`); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); core.warning(`Failed to cancel workflow run: ${errorMessage}`); - // Fallback to core.setCancelled if API call fails - core.setCancelled(message); + // Fallback to core.setFailed if API call fails (since core.setCancelled doesn't exist in types) + core.setFailed(message); } } diff --git a/pkg/workflow/js/check_team_member.cjs b/pkg/workflow/js/check_team_member.cjs index a84b52e8c53..8610e78e901 100644 --- a/pkg/workflow/js/check_team_member.cjs +++ b/pkg/workflow/js/check_team_member.cjs @@ -1,19 +1,22 @@ -// Custom setCancelled function that uses self-cancellation +/** + * Custom setCancelled function that uses self-cancellation + * @param {string} message - The cancellation message + */ async function setCancelled(message) { try { // Cancel the current workflow run using GitHub Actions API await github.rest.actions.cancelWorkflowRun({ owner: context.repo.owner, repo: context.repo.repo, - run_id: context.runId + run_id: context.runId, }); core.info(`Cancellation requested for this workflow run: ${message}`); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); core.warning(`Failed to cancel workflow run: ${errorMessage}`); - // Fallback to core.setCancelled if API call fails - core.setCancelled(message); + // Fallback to core.setFailed if API call fails (since core.setCancelled doesn't exist in types) + core.setFailed(message); } } @@ -51,12 +54,12 @@ async function main() { // Team membership check failed - use self-cancellation const failureMessage = `Access denied: User '${actor}' is not authorized to trigger this workflow. Only admin or maintainer users can run this workflow.`; core.warning(`❌ ${failureMessage}`); - + await setCancelled(failureMessage); - + // Set output for any dependent steps that might check before cancellation takes effect core.setOutput("is_team_member", "false"); - + // Return to finish the script return; } diff --git a/pkg/workflow/js/check_team_member.test.cjs b/pkg/workflow/js/check_team_member.test.cjs index 2c3501aa031..e4a9017de50 100644 --- a/pkg/workflow/js/check_team_member.test.cjs +++ b/pkg/workflow/js/check_team_member.test.cjs @@ -9,6 +9,7 @@ const mockCore = { error: vi.fn(), info: vi.fn(), setCancelled: vi.fn(), + setFailed: vi.fn(), }; const mockGithub = { @@ -189,7 +190,9 @@ describe("check_team_member.cjs", () => { expect.stringContaining("Cancellation requested for this workflow run") ); expect(mockCore.warning).toHaveBeenCalledWith( - expect.stringContaining("Access denied: User 'testuser' is not authorized") + expect.stringContaining( + "Access denied: User 'testuser' is not authorized" + ) ); expect(mockCore.setOutput).toHaveBeenCalledWith("is_team_member", "false"); @@ -229,7 +232,9 @@ describe("check_team_member.cjs", () => { "Failed to cancel workflow run: API Error: Forbidden" ); expect(mockCore.setCancelled).toHaveBeenCalledWith( - expect.stringContaining("Access denied: User 'testuser' is not authorized") + expect.stringContaining( + "Access denied: User 'testuser' is not authorized" + ) ); expect(mockCore.setOutput).toHaveBeenCalledWith("is_team_member", "false"); diff --git a/pkg/workflow/js/compute_text.cjs b/pkg/workflow/js/compute_text.cjs index ed74e08fcb0..d0100a81e45 100644 --- a/pkg/workflow/js/compute_text.cjs +++ b/pkg/workflow/js/compute_text.cjs @@ -37,8 +37,6 @@ function sanitizeContent(content) { // XML tag neutralization - convert XML tags to parentheses format sanitized = convertXmlTagsToParentheses(sanitized); - - // URI filtering - replace non-https protocols with "(redacted)" // Step 1: Temporarily mark HTTPS URLs to protect them sanitized = sanitizeUrlProtocols(sanitized); diff --git a/pkg/workflow/js/compute_text.test.cjs b/pkg/workflow/js/compute_text.test.cjs index b72d9a03e57..5763606ee5d 100644 --- a/pkg/workflow/js/compute_text.test.cjs +++ b/pkg/workflow/js/compute_text.test.cjs @@ -100,7 +100,8 @@ describe("compute_text.cjs", () => { }); it("should handle self-closing XML tags without whitespace", () => { - const input = 'Self-closing:
'; + const input = + 'Self-closing:
'; const result = sanitizeContentFunction(input); expect(result).toContain("(br/)"); expect(result).toContain('(img src="test.jpg"/)'); @@ -108,7 +109,8 @@ describe("compute_text.cjs", () => { }); it("should handle self-closing XML tags with whitespace", () => { - const input = 'With spaces:
'; + const input = + 'With spaces:
'; const result = sanitizeContentFunction(input); expect(result).toContain("(br /)"); expect(result).toContain('(img src="test.jpg" /)'); @@ -116,7 +118,8 @@ describe("compute_text.cjs", () => { }); it("should handle XML tags with various whitespace patterns", () => { - const input = 'Various: content text'; + const input = + 'Various: content text'; const result = sanitizeContentFunction(input); expect(result).toContain('(div\tclass="test")content(/div)'); expect(result).toContain('(span\n id="test")text(/span)'); diff --git a/pkg/workflow/js/sanitize_output.cjs b/pkg/workflow/js/sanitize_output.cjs index 114a54eabf1..7d4166d3b11 100644 --- a/pkg/workflow/js/sanitize_output.cjs +++ b/pkg/workflow/js/sanitize_output.cjs @@ -37,8 +37,6 @@ function sanitizeContent(content) { // XML tag neutralization - convert XML tags to parentheses format sanitized = convertXmlTagsToParentheses(sanitized); - - // URI filtering - replace non-https protocols with "(redacted)" // Step 1: Temporarily mark HTTPS URLs to protect them sanitized = sanitizeUrlProtocols(sanitized); diff --git a/pkg/workflow/js/sanitize_output.test.cjs b/pkg/workflow/js/sanitize_output.test.cjs index 6fdf49008bf..9d196244bc1 100644 --- a/pkg/workflow/js/sanitize_output.test.cjs +++ b/pkg/workflow/js/sanitize_output.test.cjs @@ -90,7 +90,8 @@ describe("sanitize_output.cjs", () => { }); it("should handle self-closing XML tags without whitespace", () => { - const input = 'Self-closing:
'; + const input = + 'Self-closing:
'; const result = sanitizeContentFunction(input); expect(result).toContain("(br/)"); expect(result).toContain('(img src="test.jpg"/)'); @@ -98,7 +99,8 @@ describe("sanitize_output.cjs", () => { }); it("should handle self-closing XML tags with whitespace", () => { - const input = 'With spaces:
'; + const input = + 'With spaces:
'; const result = sanitizeContentFunction(input); expect(result).toContain("(br /)"); expect(result).toContain('(img src="test.jpg" /)'); @@ -106,7 +108,8 @@ describe("sanitize_output.cjs", () => { }); it("should handle XML tags with various whitespace patterns", () => { - const input = 'Various: content text'; + const input = + 'Various: content text'; const result = sanitizeContentFunction(input); expect(result).toContain('(div\tclass="test")content(/div)'); expect(result).toContain('(span\n id="test")text(/span)'); @@ -374,9 +377,7 @@ Special chars: \x00\x1F & "quotes" 'apostrophes' const result = sanitizeContentFunction(input); expect(result).toContain("https://github.com/user/repo-name_with.dots"); - expect(result).toContain( - "https://github.com/search?q=test&type=code" - ); // & not escaped + expect(result).toContain("https://github.com/search?q=test&type=code"); // & not escaped expect(result).toContain("https://github.com/user/repo#readme"); expect(result).toContain("https://github.dev:443/workspace"); expect(result).toContain("https://github.com/repo"); @@ -443,12 +444,8 @@ Special chars: \x00\x1F & "quotes" 'apostrophes' `; const result = sanitizeContentFunction(input); - expect(result).toContain( - "(xml attr=\"value & 'quotes'\")" - ); - expect(result).toContain( - "(![CDATA[(script)alert(\"xss\")(/script)]])" - ); + expect(result).toContain("(xml attr=\"value & 'quotes'\")"); + expect(result).toContain('(![CDATA[(script)alert("xss")(/script)]])'); expect(result).toContain( "(!-- comment with \"quotes\" & 'apostrophes' --)" ); diff --git a/pkg/workflow/output_test.go b/pkg/workflow/output_test.go index 6a712147c3d..bf1237e7fe8 100644 --- a/pkg/workflow/output_test.go +++ b/pkg/workflow/output_test.go @@ -261,10 +261,15 @@ This workflow tests the create-issue job generation. t.Error("Expected 10-minute timeout in create_issue job") } - if !strings.Contains(lockContent, "permissions:\n contents: read\n issues: write") { + if !strings.Contains(lockContent, "permissions:") || !strings.Contains(lockContent, "contents: read") || !strings.Contains(lockContent, "issues: write") { t.Error("Expected correct permissions in create_issue job") } + // Since this workflow has explicit GitHub tools, it should also have actions: write for self-cancellation + if !strings.Contains(lockContent, "actions: write") { + t.Error("Expected actions: write permission in create_issue job for workflows with explicit GitHub tools") + } + // Verify the job uses github-script if !strings.Contains(lockContent, "uses: actions/github-script@v7") { t.Error("Expected github-script action to be used in create_issue job")