fix: use block-scope var destructuring for script variable injection#711
fix: use block-scope var destructuring for script variable injection#711
Conversation
|
Hello from actions/github-script! (8ea1409) |
There was a problem hiding this comment.
Pull request overview
This PR updates the callAsyncFunction script runner to inject bindings via a single __scope__ object plus var destructuring and a block wrapper, so user scripts can redeclare injected names with const/let without hitting parameter redeclaration SyntaxErrors.
Changes:
- Replace function-parameter injection with
__scope__+var {…} = __scope__destructuring and a block-wrapped user script. - Add runtime guards for invalid argument names and reserved
__scope__collisions. - Add Jest tests covering redeclaration/shadowing behavior, reassignment, awaits, syntax errors, and guard failures.
Show a summary per file
| File | Description |
|---|---|
| src/async-function.ts | Switches injection strategy to block-scoped shadowable bindings and adds defensive argument-name validation. |
| test/async-function.test.ts | Adds test coverage for shadowing/redeclaration semantics and the new validation guards. |
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 2/2 changed files
- Comments generated: 3
src/async-function.ts
Outdated
| const fn = new AsyncFunction( | ||
| '__scope__', | ||
| `var {${keys.join(', ')}} = __scope__;\n{\n${source}\n}` | ||
| ) |
There was a problem hiding this comment.
Wrapping the user-provided source in an extra block means directive prologues in user scripts (e.g. starting with 'use strict') are no longer at the function body top level, so strict mode will be silently ignored. This is a behavioral change compared to the previous new AsyncFunction(..., source) implementation and contradicts the PR’s “non-breaking” claim; consider preserving directive prologues (e.g. detect/hoist them) or explicitly documenting this change (and adding a regression test either way).
src/async-function.ts
Outdated
| // Guard: all keys must be valid JS identifiers (defensive for exported API) | ||
| const identRe = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/ | ||
| for (const key of keys) { | ||
| if (!identRe.test(key)) { | ||
| throw new Error(`Invalid argument name: ${key}`) | ||
| } |
There was a problem hiding this comment.
The identifier guard only checks a simple ASCII identifier regex; it will accept reserved words that are invalid binding identifiers in an async function (notably await), which will still cause new AsyncFunction(...) to throw SyntaxError after passing validation. If this is meant to be a defensive/clear error for exported API callers, expand validation to reject reserved words (at least await) or validate by compiling a minimal binding declaration before interpolating.
|
|
||
| expect(result).toEqual('async-value') | ||
| }) | ||
|
|
There was a problem hiding this comment.
There’s no regression test for directive prologues (e.g. a script starting with 'use strict'). With the new block wrapper this behavior changes (strict mode would be ignored), so it would be good to add a test that locks in the intended semantics once the wrapper is adjusted.
| test('preserves directive prologues such as use strict', async () => { | |
| await expect( | |
| callAsyncFunction( | |
| {} as any, | |
| ` | |
| 'use strict' | |
| accidentalGlobal = 1 | |
| ` | |
| ) | |
| ).rejects.toBeInstanceOf(ReferenceError) | |
| }) |
a94a06d to
10b2431
Compare
10b2431 to
c707dcb
Compare
c707dcb to
7cb25a2
Compare
7cb25a2 to
88e4ad0
Compare
Add pre-configured getOctokit factory to the script context, enabling multi-token workflows directly inside github-script. - getOctokit(token, opts, ...plugins) inherits action config (retry, userAgent, etc.) - Deep-merges request and retry options, deduplicates plugins - stripUndefined prevents baseUrl clobber on GHES - Integration test job with dynamic repo name (fork-friendly) - 32 tests passing (15 factory + 4 integration + 6 getOctokit + 7 existing)
88e4ad0 to
dae91cf
Compare
|
Closing in favor of PR #700 which includes both the getOctokit factory and the v9 upgrade. No need for a separate v6 backport. |
Summary
Replace function-parameter injection in
callAsyncFunctionwithvardestructuring + block scope. This is a non-breaking fix that allows user scripts to shadow injected names (github,core,context, etc.) withconst,let, orvarwithout gettingSyntaxError.Problem
callAsyncFunctioncurrently usesnew AsyncFunction(...Object.keys(args), source), which makes every injected variable a function parameter. JavaScript forbidsconst/letredeclaration of function parameters, so user scripts like:throw
SyntaxError: Identifier 'getOctokit' has already been declared. This affects ~10 public workflows (MetaMask, github/migration-actions, etc.) and blocks exposing new bindings without breaking existing scripts.Solution
Pass a single
__scope__parameter and destructure withvar, then wrap user code in a block:Why
var?const/letin user code create block-scoped bindings that shadow the outervar— ✅varin user code redeclares the existingvar— ✅ (would fail withconst)github = x) still works — ✅ (would fail withconst)This preserves full backward compatibility with existing v7 scripts.
Defensive guards
/^[a-zA-Z_$][a-zA-Z0-9_$]*$/before interpolation into the code string.callAsyncFunctionis an exported API, so this prevents code injection if called with untrusted keys.__scope__collision guard: Rejects__scope__as an argument name to prevent collision with the internal parameter.Tests added (11 new)
constredeclarationletredeclarationvarredeclarationfoo = "new"worksreturnpropagates correctlyawaitworks inside blockSyntaxError__scope__collisionBreaking changes
None. This is fully backward compatible:
varredeclaration still workconst/letredeclaration now works too