Skip to content

Minimally scope permissions in GitHub Actions workflows#29

Draft
desrosj wants to merge 4 commits into
mainfrom
add/scoped-workflow-permissions
Draft

Minimally scope permissions in GitHub Actions workflows#29
desrosj wants to merge 4 commits into
mainfrom
add/scoped-workflow-permissions

Conversation

@desrosj

@desrosj desrosj commented May 14, 2026

Copy link
Copy Markdown
Member

This updates the GitHub Actions workflow files to:

  • Grant minimally-scoped permissions to each job to adhere to the principle of least privilege
  • Specify a timeout on each job to prevent runaway processes consuming too many minutes (the default is 360)

Once this PR is merged, the Settings -> Actions -> Workflow permissions setting can be changed by a repo admin to "Read repository contents and packages permissions".

For more information, see PRESS11-470.

References

Use of AI

Cursor was used with (Claude Opus 4.7 and Composer 2.0 at varying points) to analyze the repository and make the initial changes.

When this PR is marked "ready for review" it means that I have manually reviewed all permissions and timeouts that were changed and made any necessary adjustments.

As a part of the analysis, the following summary was created:

Status

Field Value
Workflows scanned 7 (auto-translate.yml, brand-plugin-test-playwright.yml, codecoverage-main.yml, i18n-crowdin-download.yml, i18n-crowdin-upload.yml, lint.yml, satis-webhook.yml)
Branch add/scoped-workflow-permissions (created)
Permissions commit 51dfb76
Timeouts commit 5514267

Top-level permissions: {}

Category Entry
Workflows that were missing the top-level permissions: {} directive (added in this run): auto-translate.yml
Workflows that were missing the top-level permissions: {} directive (added in this run): brand-plugin-test-playwright.yml
Workflows that were missing the top-level permissions: {} directive (added in this run): codecoverage-main.yml
Workflows that were missing the top-level permissions: {} directive (added in this run): i18n-crowdin-download.yml
Workflows that were missing the top-level permissions: {} directive (added in this run): i18n-crowdin-upload.yml
Workflows that were missing the top-level permissions: {} directive (added in this run): lint.yml
Workflows that were missing the top-level permissions: {} directive (added in this run): satis-webhook.yml

Job-level permissions: additions

Workflow file Summary Job Permissions / notes
codecoverage-main.yml 1 job was missing a scoped permissions: directive get-repo-name permissions: {} [3]
satis-webhook.yml 1 job was missing a scoped permissions: directive webhook contents: read [3]
lint.yml 1 job was missing a scoped permissions: directive phpcs contents: read [3]
i18n-crowdin-upload.yml 1 job was missing a scoped permissions: directive call-crowdin-upload-workflow contents: read [3]
i18n-crowdin-download.yml 1 job was missing a scoped permissions: directive call-crowdin-workflow contents: read [3]
brand-plugin-test-playwright.yml 0 jobs were missing a scoped permissions: directive (existing job-level entries were retained; setup was tightened — see corrections)
auto-translate.yml 0 jobs were missing a scoped permissions: directive (existing translate permissions retained; comments aligned)

Permissions corrections (previously incorrect)

# Correction
1 codecoverage-main.yml :: (workflow-wide default): BEFORE top-level contents: read -> AFTER top-level permissions: {} plus explicit job grants -- Avoid granting read to jobs that never clone the repo (e.g. get-repo-name); codecoverage still supplies contents: write and pull-requests: write for the reusable workflow -- [3]
2 brand-plugin-test-playwright.yml :: setup: BEFORE contents: read -> AFTER permissions: {} -- Job only derives the branch name from environment variables and does not run actions/checkout or call the GitHub API -- [3]
3 i18n-crowdin-download.yml :: call-crowdin-workflow: BEFORE top-level contents: write and pull-requests: write (inherited by the job) -> AFTER job-level contents: read -- Matches newfold-labs/workflows i18n-crowdin-download.yml, which checks out with persist-credentials: false and uses NEWFOLD_ACCESS_TOKEN as GITHUB_TOKEN for Crowdin’s PR creation -- [3]

timeout-minutes additions

Workflow Job Minutes / action Rationale
codecoverage-main.yml get-repo-name 10 Trivial metadata step; short cap prevents a stuck runner if something regresses.
codecoverage-main.yml codecoverage 60 Reusable codecoverage runs PHP/Codeception matrices and publishes to gh-pages; needs headroom over the inner job’s coverage work.
brand-plugin-test-playwright.yml setup 10 Branch-name extraction only; should finish quickly.
brand-plugin-test-playwright.yml bluehost 60 Reusable Playwright/plugin build is capped at 45 minutes upstream; 60 allows queue/network slack.
brand-plugin-test-playwright.yml bluehost-dev 60 Same rationale as bluehost for the develop-matrix variant.
satis-webhook.yml webhook 15 Checkout plus a single repository-dispatch call should complete well under this limit.
lint.yml phpcs 30 Composer install and PHPCS on changed PHP files typically finish sooner; 30 bounds Composer/cache edge cases.
i18n-crowdin-upload.yml call-crowdin-upload-workflow 20 Crowdin upload reusable job allows 15 minutes; slight buffer for queueing.
i18n-crowdin-download.yml call-crowdin-workflow 30 Crowdin download reusable job allows 20 minutes; extra margin for PR preparation.
auto-translate.yml translate 120 Reusable translation pipeline chains multiple long-running jobs (i18n prep, optional Azure translate, post-process, PR); 120 caps the composed run.

Notes / blockers

# Note
1 i18n-crowdin-download.yml only passes CROWDIN_PERSONAL_TOKEN while newfold-labs/workflows/.github/workflows/i18n-crowdin-download.yml@main declares a required NEWFOLD_ACCESS_TOKEN secret for Crowdin’s PR step; confirm org/repo secrets and secrets mapping so dispatch does not fail at runtime.
2 peter-evans/repository-dispatch in satis-webhook.yml uses secrets.WEBHOOK_TOKEN, not GITHUB_TOKEN; job contents: read is scoped to actions/checkout only.

desrosj and others added 4 commits May 14, 2026 17:26
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Align reusable-workflow callers with the audit requirement that job-level
permissions keys immediately follow runs-on or uses.
Add scripts/verify-workflow-compliance.rb and composer workflow-compliance.
@desrosj desrosj self-assigned this May 14, 2026
@desrosj desrosj requested a review from Copilot May 14, 2026 23:32

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR tightens security on GitHub Actions workflows by removing implicit broad permissions, adding a top-level permissions: {} default with narrowly-scoped job-level grants, and adding timeout-minutes caps to every job. It also introduces a Ruby verification script (and a workflow-compliance composer script) that enforces these conventions across the workflow files.

Changes:

  • Add top-level permissions: {} and tighten/relocate job-level permissions: to the minimum scopes in all seven workflows.
  • Add timeout-minutes to every job to bound runaway runs (replacing the 360-minute default).
  • Add scripts/verify-workflow-compliance.rb plus a composer workflow-compliance entry that checks for the audit comments, permissions: {} placement, presence of job permissions/timeouts, and correct YAML key ordering.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated no comments.

Show a summary per file
File Description
.github/workflows/auto-translate.yml Moves permissions after uses, adds 120m timeout, adds preamble.
.github/workflows/brand-plugin-test-playwright.yml Top-level permissions: {}, tightens setup to {}, repositions reusable-caller permissions after uses, adds timeouts.
.github/workflows/codecoverage-main.yml Replaces top-level contents: read with {}, adds job-level {} for get-repo-name, repositions write perms after uses for codecoverage, adds timeouts.
.github/workflows/i18n-crowdin-download.yml Drops top-level write perms, sets contents: read after uses, adds 30m timeout.
.github/workflows/i18n-crowdin-upload.yml Adds top-level {}, job-level contents: read after uses, adds 20m timeout.
.github/workflows/lint.yml Adds top-level {}, job-level contents: read, 30m timeout.
.github/workflows/satis-webhook.yml Adds top-level {}, job-level contents: read, 15m timeout.
scripts/verify-workflow-compliance.rb New Ruby auditor enforcing the preamble, permissions: {} placement, job permissions/timeouts, and key ordering.
composer.json Registers workflow-compliance composer script and its description.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants