diff --git a/.circleci/config.yml b/.circleci/config.yml index d2798c319911..72a40b1c00b1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -17,6 +17,10 @@ parameters: default: '' description: The PR number type: string + ghTrustedAuthor: + default: 'false' + description: Whether the PR author is a trusted author who should be allowed to persist to shared caches + type: string workflow: default: skipped description: Which workflow to run @@ -30,7 +34,7 @@ parameters: jobs: generate-and-run-config: - executor: + executor: name: node/default resource_class: large steps: @@ -44,7 +48,9 @@ jobs: - run: name: Generate config command: | - yarn dlx jiti ./scripts/ci/main.ts --workflow=<< pipeline.parameters.workflow >> + yarn dlx jiti ./scripts/ci/main.ts \ + --workflow=<< pipeline.parameters.workflow >> \ + --gh-trusted-author=<< pipeline.parameters.ghTrustedAuthor >> - continuation/continue: configuration_path: .circleci/config.generated.yml workflows: diff --git a/.github/actions/setup-node-and-install/action.yml b/.github/actions/setup-node-and-install/action.yml index d882d1c57068..754d7dcba690 100644 --- a/.github/actions/setup-node-and-install/action.yml +++ b/.github/actions/setup-node-and-install/action.yml @@ -19,8 +19,11 @@ runs: shell: bash run: npm install -g npm@latest - - name: Cache dependencies - uses: actions/cache@v4 + # Restore only — save is gated below. actions/cache's post-job save step uses a + # runner-internal token that bypasses `permissions:`, so splitting is the only + # reliable way to prevent pull_request_target runs from poisoning the shared cache. + - name: Restore cached dependencies + uses: actions/cache/restore@v4 with: path: | ~/.yarn/berry/cache @@ -39,3 +42,11 @@ runs: shell: bash working-directory: code run: yarn install + + - name: Save cached dependencies + if: github.event_name != 'pull_request_target' + uses: actions/cache/save@v4 + with: + path: | + ~/.yarn/berry/cache + key: yarn-v1-${{ hashFiles('yarn.lock') }} diff --git a/.github/workflows/trigger-circle-ci-workflow.yml b/.github/workflows/trigger-circle-ci-workflow.yml index 7b1cedda19f1..c2f7b79de58d 100644 --- a/.github/workflows/trigger-circle-ci-workflow.yml +++ b/.github/workflows/trigger-circle-ci-workflow.yml @@ -49,10 +49,30 @@ jobs: run: echo "workflow=merged" >> $GITHUB_ENV - if: github.event_name == 'pull_request_target' && (contains(github.event.pull_request.labels.*.name, 'ci:daily')) run: echo "workflow=daily" >> $GITHUB_ENV + - id: trusted-author + env: + EVENT_NAME: ${{ github.event_name }} + ASSOCIATION: ${{ github.event.pull_request.author_association }} + USER_TYPE: ${{ github.event.pull_request.user.type }} + USER_LOGIN: ${{ github.event.pull_request.user.login }} + run: | + # You can only push to `main` and `next` as a core team member, so the content is trustworthy. + if [ "$EVENT_NAME" = "push" ]; then + echo "result=true" >> $GITHUB_OUTPUT + # These commits are made by the release actions, which are gated to core team members. + elif [ "$USER_LOGIN" = "github-actions[bot]" ] && [ "$USER_TYPE" = "Bot" ]; then + echo "result=true" >> $GITHUB_OUTPUT + # Trusted members of the organization can also write to cache (core team, DX, and a few maintainers) + elif { [ "$ASSOCIATION" = "OWNER" ] || [ "$ASSOCIATION" = "MEMBER" ]; } && [ "$USER_TYPE" != "Bot" ]; then + echo "result=true" >> $GITHUB_OUTPUT + else + echo "result=false" >> $GITHUB_OUTPUT + fi outputs: workflow: ${{ env.workflow }} ghBaseBranch: ${{ github.event.pull_request.base.ref }} ghPrNumber: ${{ github.event.pull_request.number }} + ghTrustedAuthor: ${{ steps.trusted-author.outputs.result }} trigger-circle-ci-workflow: runs-on: ubuntu-latest diff --git a/scripts/ci/common-jobs.ts b/scripts/ci/common-jobs.ts index 75ce07d0b364..3b28d7928eaa 100644 --- a/scripts/ci/common-jobs.ts +++ b/scripts/ci/common-jobs.ts @@ -17,6 +17,7 @@ import { workflow, workspace, } from './utils/helpers.ts'; +import { isTrustedAuthor } from './utils/runtime.ts'; import { defineJob, defineNoOpJob } from './utils/types.ts'; const dirname = import.meta.dirname; @@ -29,7 +30,7 @@ export const build_linux = defineJob('Build (linux)', (workflowName) => ({ steps: [ git.checkout(), npm.install('.'), - cache.persist(CACHE_PATHS, CACHE_KEYS()[0]), + ...(isTrustedAuthor() ? [cache.persist(CACHE_PATHS, CACHE_KEYS()[0])] : []), git.check(), npm.check(), { diff --git a/scripts/ci/main.ts b/scripts/ci/main.ts index dbb5b6497a8d..acf3346131d2 100644 --- a/scripts/ci/main.ts +++ b/scripts/ci/main.ts @@ -25,6 +25,7 @@ import { executors } from './utils/executors.ts'; import { ensureRequiredJobs } from './utils/helpers.ts'; import { orbs } from './utils/orbs.ts'; import { parameters } from './utils/parameters.ts'; +import { setTrustedAuthor } from './utils/runtime.ts'; import type { JobImplementationObj, JobOrNoOpJob, @@ -149,11 +150,19 @@ console.log('--------------------------------'); program .description('Generate CircleCI config') .requiredOption('-w, --workflow ', 'Workflow to generate config for') + .option( + '--gh-trusted-author ', + 'Whether the pipeline can persist to shared caches', + 'false' + ) .parse(process.argv); +const opts = program.opts(); +setTrustedAuthor(opts.ghTrustedAuthor === 'true'); + await fs.writeFile( join(dirname, '../../.circleci/config.generated.yml'), - yml.stringify(generateConfig(program.opts().workflow), null, { + yml.stringify(generateConfig(opts.workflow), null, { lineWidth: 1200, indent: 4, }) diff --git a/scripts/ci/utils/parameters.ts b/scripts/ci/utils/parameters.ts index 1518f67a1f49..63fb13e5028a 100644 --- a/scripts/ci/utils/parameters.ts +++ b/scripts/ci/utils/parameters.ts @@ -10,6 +10,12 @@ export const parameters = { description: 'The PR number', type: 'string', }, + ghTrustedAuthor: { + default: 'false', + description: + 'Whether the pipeline is allowed to persist to shared caches (team member PRs and push events only)', + type: 'string', + }, workflow: { default: 'skipped', description: 'Which workflow to run', diff --git a/scripts/ci/utils/runtime.ts b/scripts/ci/utils/runtime.ts new file mode 100644 index 000000000000..bc4de12925f3 --- /dev/null +++ b/scripts/ci/utils/runtime.ts @@ -0,0 +1,9 @@ +let trustedAuthor = false; + +export function setTrustedAuthor(isTrusted: boolean): void { + trustedAuthor = isTrusted; +} + +export function isTrustedAuthor(): boolean { + return trustedAuthor; +}