diff --git a/.github/ARCHITECTURE.md b/.github/ARCHITECTURE.md index 7b236aa..60595ea 100644 --- a/.github/ARCHITECTURE.md +++ b/.github/ARCHITECTURE.md @@ -254,7 +254,7 @@ PR / Feature Branch Push to main Tag v* On the Azure DevOps agent: -1. Agent picks the best available Node handler: `Node24_1` → `Node20_1` (fallback) +1. Agent picks the best available Node handler: `Node24_1` → `Node20_1` → `Node20` (fallback) 2. Agent runs `tasks//dist/index.js` 3. `index.ts` calls `task-runner.ts` → `run()` 4. Task-runner reads inputs via `tl.getInput()`, executes logic, sets outputs via `tl.setVariable()` diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index ff36014..0ac13de 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -78,7 +78,7 @@ Every task follows the same pattern: ### task.json Each task has a `task.json` defining its Azure DevOps contract: -- Must include both `Node24_1` (primary) and `Node20_1` (fallback) in `execution` +- Must include both `Node24_1` (primary), `Node20_1`, and `Node20` (fallback) in `execution` - Task `id` is a stable GUID (never changes) - Task `Major` version only bumps for breaking YAML contract changes - `Minor` and `Patch` are stamped by CI via inline `jq` in the workflow YAML @@ -131,7 +131,7 @@ vi.mock('../../shared/http-range'); ## Adding a New Task -1. Create `tasks//task.json` — unique GUID, Node24_1 + Node20_1 handlers +1. Create `tasks//task.json` — unique GUID, Node24_1 + Node20_1 + Node20 handlers 2. Create `tasks//src/index.ts` and `src/task-runner.ts` 3. Add the task name to the `tasks` array in `esbuild.config.mjs` 4. Add entries in `vss-extension.json` `files` and `contributions` arrays (and `vss-extension.dev.json`) @@ -153,7 +153,7 @@ TypeScript and vitest both use the `@shared/*` alias for imports from `shared/`: ## Common Pitfalls -- **Missing Node handler**: every `task.json` needs both `Node24_1` and `Node20_1` execution entries +- **Missing Node handler**: every `task.json` needs `Node24_1`, `Node20_1`, and `Node20` execution entries - **Shared modules aren't runtime-shared**: they're bundled into each task by esbuild. No `node_modules` sharing at runtime. - **Output variables need `isOutput: true`**: the 4th argument to `tl.setVariable()` must be `true` for downstream tasks to read the value - **Don't commit `tasks/*/dist/`**: these are gitignored build artifacts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 52508fc..d265097 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -71,7 +71,7 @@ npx vitest --watch # Watch mode ### Adding a New Task -1. Create `tasks//task.json` with a unique GUID, Node24_1 + Node20_1 handlers +1. Create `tasks//task.json` with a unique GUID, Node24_1 + Node20_1 + Node20 handlers 2. Create `tasks//src/index.ts` (entry point) and `src/task-runner.ts` (orchestrator) 3. Add task to `esbuild.config.mjs` `tasks` array 4. Add task to `vss-extension.json` `files` and `contributions` arrays @@ -219,5 +219,5 @@ Before submitting a PR: - [ ] `.vsix` packages (`npm run package`) - [ ] New code has tests (TDD — write tests first) - [ ] Shared module changes tested across all affected tasks -- [ ] `task.json` has both `Node24_1` and `Node20_1` execution handlers +- [ ] `task.json` has `Node24_1`, `Node20_1`, and `Node20` execution handlers - [ ] No secrets or credentials in source code diff --git a/tasks/detect-tfm-bc-artifact/task.json b/tasks/detect-tfm-bc-artifact/task.json index c7ca068..108f340 100644 --- a/tasks/detect-tfm-bc-artifact/task.json +++ b/tasks/detect-tfm-bc-artifact/task.json @@ -40,6 +40,9 @@ }, "Node20_1": { "target": "dist/index.js" - } - } + }, + "Node20": { + "target": "dist/index.js" + } + } } diff --git a/tasks/detect-tfm-marketplace/task.json b/tasks/detect-tfm-marketplace/task.json index 96bf1d9..9c800ef 100644 --- a/tasks/detect-tfm-marketplace/task.json +++ b/tasks/detect-tfm-marketplace/task.json @@ -56,6 +56,9 @@ }, "Node20_1": { "target": "dist/index.js" - } - } + }, + "Node20": { + "target": "dist/index.js" + } + } } diff --git a/tasks/detect-tfm-nuget-devtools/task.json b/tasks/detect-tfm-nuget-devtools/task.json index f674c53..0372dc2 100644 --- a/tasks/detect-tfm-nuget-devtools/task.json +++ b/tasks/detect-tfm-nuget-devtools/task.json @@ -40,6 +40,9 @@ }, "Node20_1": { "target": "dist/index.js" - } - } + }, + "Node20": { + "target": "dist/index.js" + } + } } diff --git a/tasks/download/task.json b/tasks/download/task.json index c095224..1df928e 100644 --- a/tasks/download/task.json +++ b/tasks/download/task.json @@ -92,6 +92,9 @@ }, "Node20_1": { "target": "dist/index.js" - } - } -} + }, + "Node20": { + "target": "dist/index.js" + } + } +} \ No newline at end of file diff --git a/tasks/install-analyzers/task.json b/tasks/install-analyzers/task.json index ea57fa6..12017ab 100644 --- a/tasks/install-analyzers/task.json +++ b/tasks/install-analyzers/task.json @@ -99,6 +99,9 @@ }, "Node20_1": { "target": "dist/index.js" - } - } + }, + "Node20": { + "target": "dist/index.js" + } + } } diff --git a/tests/scaffold.test.ts b/tests/scaffold.test.ts index b12edb4..96f19e1 100644 --- a/tests/scaffold.test.ts +++ b/tests/scaffold.test.ts @@ -2,80 +2,81 @@ import { describe, it, expect } from 'vitest'; import * as fs from 'fs'; import * as path from 'path'; import { - TFM_PREFERENCE, - AL_COMPILER_DLL, - NUGET_PACKAGE_NAME, - NUGET_FLAT_CONTAINER, - VS_MARKETPLACE_API, - AL_EXTENSION_ID, - VSIX_DLL_PATH, + TFM_PREFERENCE, + AL_COMPILER_DLL, + NUGET_PACKAGE_NAME, + NUGET_FLAT_CONTAINER, + VS_MARKETPLACE_API, + AL_EXTENSION_ID, + VSIX_DLL_PATH, } from '@alcops/core'; const ROOT = path.resolve(__dirname, '..'); const TASKS_DIR = path.resolve(ROOT, 'tasks'); describe('scaffold: shared types', () => { - it('should export TFM_PREFERENCE with net8.0 and netstandard2.1', () => { - expect(TFM_PREFERENCE).toContain('net8.0'); - expect(TFM_PREFERENCE).toContain('netstandard2.1'); - }); + it('should export TFM_PREFERENCE with net8.0 and netstandard2.1', () => { + expect(TFM_PREFERENCE).toContain('net8.0'); + expect(TFM_PREFERENCE).toContain('netstandard2.1'); + }); - it('should export AL_COMPILER_DLL', () => { - expect(AL_COMPILER_DLL).toBe('Microsoft.Dynamics.Nav.CodeAnalysis.dll'); - }); + it('should export AL_COMPILER_DLL', () => { + expect(AL_COMPILER_DLL).toBe('Microsoft.Dynamics.Nav.CodeAnalysis.dll'); + }); - it('should export NUGET constants', () => { - expect(NUGET_PACKAGE_NAME).toBe('ALCops.Analyzers'); - expect(NUGET_FLAT_CONTAINER).toContain('api.nuget.org'); - }); + it('should export NUGET constants', () => { + expect(NUGET_PACKAGE_NAME).toBe('ALCops.Analyzers'); + expect(NUGET_FLAT_CONTAINER).toContain('api.nuget.org'); + }); - it('should export VS Marketplace constants', () => { - expect(VS_MARKETPLACE_API).toContain('marketplace.visualstudio.com'); - expect(AL_EXTENSION_ID).toBe('ms-dynamics-smb.al'); - expect(VSIX_DLL_PATH).toContain('Analyzers/'); - }); + it('should export VS Marketplace constants', () => { + expect(VS_MARKETPLACE_API).toContain('marketplace.visualstudio.com'); + expect(AL_EXTENSION_ID).toBe('ms-dynamics-smb.al'); + expect(VSIX_DLL_PATH).toContain('Analyzers/'); + }); }); describe('scaffold: task.json files', () => { - const taskDirs = [ - 'download', - 'install-analyzers', - 'detect-tfm-bc-artifact', - 'detect-tfm-nuget-devtools', - 'detect-tfm-marketplace', - ]; + const taskDirs = [ + 'download', + 'install-analyzers', + 'detect-tfm-bc-artifact', + 'detect-tfm-nuget-devtools', + 'detect-tfm-marketplace', + ]; - for (const taskDir of taskDirs) { - it(`should have a valid task.json for ${taskDir}`, () => { - const taskJsonPath = path.join(TASKS_DIR, taskDir, 'task.json'); - expect(fs.existsSync(taskJsonPath)).toBe(true); + for (const taskDir of taskDirs) { + it(`should have a valid task.json for ${taskDir}`, () => { + const taskJsonPath = path.join(TASKS_DIR, taskDir, 'task.json'); + expect(fs.existsSync(taskJsonPath)).toBe(true); - const taskJson = JSON.parse(fs.readFileSync(taskJsonPath, 'utf-8')); - expect(taskJson.id).toBeTruthy(); - expect(taskJson.name).toBeTruthy(); - expect(taskJson.execution).toHaveProperty('Node24_1'); - expect(taskJson.execution).toHaveProperty('Node20_1'); - }); + const taskJson = JSON.parse(fs.readFileSync(taskJsonPath, 'utf-8')); + expect(taskJson.id).toBeTruthy(); + expect(taskJson.name).toBeTruthy(); + expect(taskJson.execution).toHaveProperty('Node24_1'); + expect(taskJson.execution).toHaveProperty('Node20_1'); + expect(taskJson.execution).toHaveProperty('Node20'); + }); - it(`should have src/index.ts for ${taskDir}`, () => { - const indexPath = path.join(TASKS_DIR, taskDir, 'src', 'index.ts'); - expect(fs.existsSync(indexPath)).toBe(true); - }); - } + it(`should have src/index.ts for ${taskDir}`, () => { + const indexPath = path.join(TASKS_DIR, taskDir, 'src', 'index.ts'); + expect(fs.existsSync(indexPath)).toBe(true); + }); + } }); describe('scaffold: test fixtures', () => { - const FIXTURES_DIR = path.resolve(__dirname, 'fixtures'); + const FIXTURES_DIR = path.resolve(__dirname, 'fixtures'); - it('should have compiler-net80 fixture DLL', () => { - const dll = path.join(FIXTURES_DIR, 'compiler-net80', 'Microsoft.Dynamics.Nav.CodeAnalysis.dll'); - expect(fs.existsSync(dll)).toBe(true); - expect(fs.statSync(dll).size).toBeGreaterThan(0); - }); + it('should have compiler-net80 fixture DLL', () => { + const dll = path.join(FIXTURES_DIR, 'compiler-net80', 'Microsoft.Dynamics.Nav.CodeAnalysis.dll'); + expect(fs.existsSync(dll)).toBe(true); + expect(fs.statSync(dll).size).toBeGreaterThan(0); + }); - it('should have compiler-netstandard21 fixture DLL', () => { - const dll = path.join(FIXTURES_DIR, 'compiler-netstandard21', 'Microsoft.Dynamics.Nav.CodeAnalysis.dll'); - expect(fs.existsSync(dll)).toBe(true); - expect(fs.statSync(dll).size).toBeGreaterThan(0); - }); + it('should have compiler-netstandard21 fixture DLL', () => { + const dll = path.join(FIXTURES_DIR, 'compiler-netstandard21', 'Microsoft.Dynamics.Nav.CodeAnalysis.dll'); + expect(fs.existsSync(dll)).toBe(true); + expect(fs.statSync(dll).size).toBeGreaterThan(0); + }); });