diff --git a/.github/workflows/publish-to-npm.yml b/.github/workflows/publish-to-npm.yml index 51d8391..c5ef324 100644 --- a/.github/workflows/publish-to-npm.yml +++ b/.github/workflows/publish-to-npm.yml @@ -1,5 +1,6 @@ name: Publish to NPM +# user creates a GH release with a new `vX.X.X` tag to trigger this workflow on: push: tags: @@ -14,21 +15,26 @@ permissions: jobs: publish-to-npm: runs-on: ubuntu-latest + # use github environment so we can have an approval policy for workflow runs environment: publish steps: - name: Check out repository - uses: actions/checkout@v6 + # general policy: pin `uses` dependencies to commit hashes to avoid tag-poisoning attacks + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: + # throw away GH token after this step so future steps can't use it persist-credentials: false - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version-file: .nvmrc registry-url: https://registry.npmjs.org + # don't use NPM cache when installing, to avoid possibility of cache-poisoning attacks during publish package-manager-cache: false - name: Verify release version + # fail & warn if release git tag doesn't match version in `package.json` run: >- node -e " const { version } = require('./package.json'); @@ -39,22 +45,24 @@ jobs: " - name: Install dependencies + # use `npm ci` for reproducible installs, fails if package & lock files are out of sync + # install without running lifecycle scripts, then run only scripts approved in package.json's `allowScripts` run: | npm ci --ignore-scripts - # isolated-vm requires its lifecycle scripts to build the native addon. - npm rebuild isolated-vm - - - name: Test - run: npm test + npm rebuild --strict-allow-scripts - name: Build run: npm run build + - name: Test + run: npm test + - name: Verify package contents + # list all files in the published package, for logs/traceability run: npm pack --dry-run - name: Publish env: - # git tags like `v1.0.0-rc.0` will be tagged as `next` version in NPM instead of `latest` + # pre-release git tags like `v1.0.0-rc.0` will be tagged as `next` version in NPM instead of `latest` NPM_DIST_TAG: ${{ contains(github.ref_name, '-') && 'next' || 'latest' }} run: npm publish --tag "$NPM_DIST_TAG" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 57c5327..adcaf0c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,27 +19,31 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out repository - uses: actions/checkout@v6 + # general policy: pin `uses` dependencies to commit hashes to avoid tag-poisoning attacks + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: + # throw away GH token after this step so future steps can't use it persist-credentials: false - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version-file: .nvmrc + registry-url: https://registry.npmjs.org cache: npm - cache-dependency-path: package-lock.json - - name: Install Dependencies + - name: Install dependencies + # use `npm ci` for reproducible installs, fails if package & lock files are out of sync + # install without running lifecycle scripts, then run only scripts approved in package.json's `allowScripts` run: | npm ci --ignore-scripts - # isolated-vm requires its lifecycle scripts to build the native addon. - npm rebuild isolated-vm + npm rebuild --strict-allow-scripts + + - name: Build + run: npm run build - name: Test id: test - env: - NO_COLOR: 1 run: npm run test:ci - name: Add Test Summary @@ -50,7 +54,7 @@ jobs: - name: Upload Test Results if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: test-results-${{ github.run_id }} path: test-output.txt diff --git a/package-lock.json b/package-lock.json index bb73405..b7c3352 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { - "name": "@nasa-jpl/aerie-ts-user-code-runner", - "version": "1.0.0", + "name": "@nasa-jpl/plandev-ts-user-code-runner", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@nasa-jpl/aerie-ts-user-code-runner", - "version": "1.0.0", + "name": "@nasa-jpl/plandev-ts-user-code-runner", + "version": "1.1.0", "license": "MIT", "dependencies": { "isolated-vm": "6.0.2", diff --git a/package.json b/package.json index 4977844..a710464 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { - "name": "@nasa-jpl/aerie-ts-user-code-runner", - "version": "1.0.0", + "name": "@nasa-jpl/plandev-ts-user-code-runner", + "version": "1.1.0", "description": "A simple way to safely run user code written in Typescript.", "repository": { "type": "git", - "url": "https://github.com/NASA-AMMOS/aerie-ts-user-code-runner" + "url": "https://github.com/NASA-AMMOS/plandev-ts-user-code-runner" }, "main": "build/UserCodeRunner.js", "type": "module", @@ -44,5 +44,14 @@ "fsevents@2.3.3": true, "isolated-vm@6.0.2": true, "esbuild@0.28.2": true + }, + "publishConfig": { + "access": "public" + }, + "devEngines": { + "packageManager": { + "name": "npm", + "onFail": "warn" + } } } diff --git a/test/UserCodeRunner.spec.ts b/test/UserCodeRunner.spec.ts index 3b1c84f..96e7fdb 100644 --- a/test/UserCodeRunner.spec.ts +++ b/test/UserCodeRunner.spec.ts @@ -20,16 +20,16 @@ describe('behavior', () => { return Object.keys(globalThis).join(','); } `.trimTemplate(); - + const runner = new UserCodeRunner(); - + const result = await runner.executeUserCode( userCode, [], 'string', [], ); - + expect(result.isOk()).toBe(true); expect(result.unwrap()).toBe('__args,__result'); }); @@ -132,7 +132,7 @@ describe('behavior', () => { subroutine(); return thing + ' world'; } - + function subroutine() { throw new Error('This is a test error'); } @@ -186,7 +186,7 @@ describe('behavior', () => { export function throwingLibraryFunction(): void { throw new Error("Error in library code") } - + Object.assign(globalThis, { throwingLibraryFunction }); `.trimTemplate(), ts.ScriptTarget.ESNext, true), ], @@ -212,7 +212,7 @@ describe('behavior', () => { subroutine(); return thing + ' world'; } - + function subroutine() { throw new Error('This is a test error'); } @@ -247,7 +247,7 @@ describe('behavior', () => { subroutine(); return thing + ' world'; } - + function subroutine() { throw new Error('This is a test error'); } @@ -282,7 +282,7 @@ describe('behavior', () => { subroutine(); return thing + ' world'; } - + function subroutine() { throw new Error('This is a test error'); } @@ -317,7 +317,7 @@ describe('behavior', () => { subroutine(); return thing + ' world'; } - + function subroutine() { throw new Error('This is a test error'); } @@ -354,7 +354,7 @@ describe('behavior', () => { subroutine(); return thing + ' world'; } - + function subroutine() { throw new Error('This is a test error'); } @@ -588,7 +588,7 @@ describe('behavior', () => { const myExpansion = (props: ExpansionProps): ExpansionReturn => { const { activity } = props; const { biteSize } = activity.attributes.arguments; - + return [ AVS_DMP_ADC_SNAPSHOT(biteSize) ]; @@ -638,7 +638,7 @@ describe('behavior', () => { const myExpansion = function(props: ExpansionProps): ExpansionReturn { const { activity } = props; const { biteSize } = activity.attributes.arguments; - + return [ AVS_DMP_ADC_SNAPSHOT(biteSize) ]; @@ -686,7 +686,7 @@ describe('behavior', () => { export default function MyDSLFunction(thing: string): string { return thing + ' world'; } - + throw new Error('This is a test error'); `.trimTemplate(); @@ -782,7 +782,7 @@ This is a test error subroutine(); return thing + ' world'; } - + function subroutine() { throw new Error('This is a test error'); } @@ -818,7 +818,7 @@ This is a test error await subroutine(); return thing + ' world'; } - + async function subroutine() { throw new Error('This is a test error'); } @@ -872,7 +872,7 @@ This is a test error export function throwingLibraryFunction(): void { throw new Error("Error in library code") } - + Object.assign(globalThis, { throwingLibraryFunction }); `.trimTemplate(), ts.ScriptTarget.ESNext, true), ], @@ -898,7 +898,7 @@ This is a test error subroutine(); return thing + ' world'; } - + function subroutine() { throw new Error('This is a test error'); } @@ -933,7 +933,7 @@ This is a test error subroutine(); return thing + ' world'; } - + function subroutine() { throw new Error('This is a test error'); } @@ -1053,7 +1053,7 @@ This is a test error }); describe('regression tests', () => { - test('Aerie command expansion throw Regression Test', async () => { + test('PlanDev command expansion throw Regression Test', async () => { const userCode = ` export default function SingleCommandExpansion(props: { activity: ActivityType }): Command { const duration = Temporal.Duration.from('PT1H'); @@ -1086,7 +1086,7 @@ describe('regression tests', () => { return new Duration(); } } - + Object.defineProperty(globalThis, 'Temporal', { value: { Duration }, writable: false, @@ -1105,7 +1105,7 @@ describe('regression tests', () => { }); }) - test('Aerie undefined node test', async () => { + test('PlanDev undefined node test', async () => { const userCode = ` export default function BakeBananaBreadExpansionLogic( props: { @@ -1186,7 +1186,7 @@ describe('regression tests', () => { }); }); - test('Aerie Scheduler test', async () => { + test('PlanDev Scheduler test', async () => { const userCode = ` export default function myGoal() { return myHelper(ActivityTemplates.PeelBanana({ @@ -1245,7 +1245,7 @@ describe('regression tests', () => { }); }); - test('Aerie Scheduler TS2345 regression test', async () => { + test('PlanDev Scheduler TS2345 regression test', async () => { const userCode = ` export default function myGoal() { return myHelper(ActivityTemplates.PeelBanana({ peelDirection: 'fromStem' })) @@ -1294,7 +1294,7 @@ describe('regression tests', () => { }); }); - test("Aerie Scheduler wrong return type no annotation regression test", async () => { + test("PlanDev Scheduler wrong return type no annotation regression test", async () => { const userCode = ` export default function myGoal() { return 5 @@ -1454,7 +1454,7 @@ describe('regression tests', () => { }); }); - test('Aerie command expansion invalid count regression test', async () => { + test('PlanDev command expansion invalid count regression test', async () => { const userCode = ` export default function SingleCommandExpansion(): ExpansionReturn { return DDM_CLOSE_OPEN_SELECT_DP; @@ -1516,7 +1516,7 @@ describe('regression tests', () => { }); }); - test('Aerie scheduler unmapped harness error on missing property return type', async () => { + test('PlanDev scheduler unmapped harness error on missing property return type', async () => { const userCode = ` interface FakeGoal { and(...others: FakeGoal[]): FakeGoal; @@ -1568,7 +1568,7 @@ describe('regression tests', () => { }); }); - test('Aerie incorrect stack frame assumption regression test', async () => { + test('PlanDev incorrect stack frame assumption regression test', async () => { const userCode = ` export default () => { return Real.Resource("state of charge").lessThan(0.3).split(0)