diff --git a/.github/ARCHITECTURE.md b/.github/ARCHITECTURE.md index 60595ea..24a6b42 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` → `Node20` (fallback) +1. Agent picks the best available Node handler: `Node24` → `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 e852029..0d57eaa 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -69,7 +69,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), `Node20_1`, and `Node20` (fallback) in `execution` +- Must include `Node24` (primary), `Node20_1`, and `Node20` (fallback) in `execution`, plus a `minimumAgentVersion` of `3.224.1` - 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 @@ -123,7 +123,7 @@ vi.mock('@alcops/core', async (importOriginal) => { ## Adding a New Task -1. Create `tasks//task.json` — unique GUID, Node24_1 + Node20_1 + Node20 handlers +1. Create `tasks//task.json` — unique GUID, Node24 + Node20_1 + Node20 handlers, `minimumAgentVersion` `3.224.1` 2. Create `tasks//src/index.ts` (calls `run()`, guards with `.catch`) and `src/task-runner.ts` (reads inputs → calls `@alcops/core` → sets outputs) 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`) @@ -145,7 +145,7 @@ TypeScript and vitest both use the `@shared/*` alias for imports from `shared/`: ## Common Pitfalls -- **Missing Node handler**: every `task.json` needs `Node24_1`, `Node20_1`, and `Node20` execution entries +- **Missing/invalid Node handler**: every `task.json` needs `Node24`, `Node20_1`, and `Node20` execution entries (note: `Node24_1` is NOT a valid handler name — the correct key is `Node24`). Also set `minimumAgentVersion` `3.224.1` so old agents get a clear error. - **Shared modules aren't runtime-shared**: `shared/` helpers and `@alcops/core` are bundled into each task by esbuild. No `node_modules` sharing at runtime. - **Logic lives in `@alcops/core`, not here**: TFM detection, NuGet download, ZIP/PE parsing are all in the external package. Wrappers must stay thin — don't reimplement or duplicate core's validation (e.g. valid TFM lists) in the wrapper. - **Output variables need `isOutput: true`**: the 4th argument to `tl.setVariable()` must be `true` for downstream tasks to read the value diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d265097..804f6ed 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 + Node20 handlers +1. Create `tasks//task.json` with a unique GUID, Node24 + Node20_1 + Node20 handlers, and `minimumAgentVersion` `3.224.1` 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 `Node24_1`, `Node20_1`, and `Node20` execution handlers +- [ ] `task.json` has `Node24`, `Node20_1`, and `Node20` execution handlers (not `Node24_1`) and `minimumAgentVersion` `3.224.1` - [ ] No secrets or credentials in source code diff --git a/README.md b/README.md index 89ed91a..0942ae7 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,34 @@ Download ALCops code analyzers with automatic TFM detection. | `outputDir` | Full path to extracted analyzer DLLs directory | | `files` | Semicolon-separated list of analyzer DLL paths | +## Troubleshooting + +### "A supported task execution handler was not found ... not compatible with your current operating system" + +Despite mentioning the operating system, this Azure DevOps error is **not** about your OS. +It means your build agent is **too old** to recognize any of the task's Node execution +handlers. The tasks ship `Node24`, `Node20_1`, and `Node20` handlers and require an agent +of at least **v3.224.1** (the release that introduced the Node 20 handler). + +Fix it with any of the following: + +- **Upgrade the agent.** For self-hosted agents, update to the latest 3.x or 4.x agent. + Microsoft-hosted agents are always current. Self-hosted agents auto-update within a + major version but **not** across majors (v2→v3, v3→v4 require a manual upgrade). +- **Install a newer Node runner on the agent** by adding the + [`NodeTaskRunnerInstaller@0`](https://learn.microsoft.com/azure/devops/pipelines/tasks/reference/node-task-runner-installer-v0) + task before the ALCops task: + + ```yaml + - task: NodeTaskRunnerInstaller@0 + inputs: + runnerVersion: 20 + ``` + +- **On-premises Azure DevOps Server:** ensure the server (and its bundled agent) is recent + enough. Servers older than the agent v3.224.1 baseline cannot run these tasks; upgrade + the server or use the `NodeTaskRunnerInstaller@0` workaround above. + ## Development ### Prerequisites diff --git a/esbuild.config.mjs b/esbuild.config.mjs index ebc5ced..55c7e76 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -15,7 +15,7 @@ const sharedOptions = { bundle: true, format: 'cjs', platform: 'node', - target: 'node24', + target: 'node20', sourcemap: !isProduction, minify: isProduction, logLevel: 'info', diff --git a/tasks/detect-tfm-bc-artifact/task.json b/tasks/detect-tfm-bc-artifact/task.json index 108f340..cc84eb7 100644 --- a/tasks/detect-tfm-bc-artifact/task.json +++ b/tasks/detect-tfm-bc-artifact/task.json @@ -13,6 +13,7 @@ "Minor": 0, "Patch": 0 }, + "minimumAgentVersion": "3.224.1", "instanceNameFormat": "ALCops Detect TFM from BC Artifact", "inputs": [ { @@ -35,14 +36,15 @@ } ], "execution": { - "Node24_1": { + "Node24": { "target": "dist/index.js" }, "Node20_1": { "target": "dist/index.js" }, - "Node20": { - "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 9c800ef..fcc9741 100644 --- a/tasks/detect-tfm-marketplace/task.json +++ b/tasks/detect-tfm-marketplace/task.json @@ -13,6 +13,7 @@ "Minor": 0, "Patch": 0 }, + "minimumAgentVersion": "3.224.1", "instanceNameFormat": "ALCops Detect TFM from VS Marketplace", "inputs": [ { @@ -51,14 +52,15 @@ } ], "execution": { - "Node24_1": { + "Node24": { "target": "dist/index.js" }, "Node20_1": { "target": "dist/index.js" }, - "Node20": { - "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 0372dc2..c2d1e70 100644 --- a/tasks/detect-tfm-nuget-devtools/task.json +++ b/tasks/detect-tfm-nuget-devtools/task.json @@ -13,6 +13,7 @@ "Minor": 0, "Patch": 0 }, + "minimumAgentVersion": "3.224.1", "instanceNameFormat": "ALCops Detect TFM from NuGet DevTools", "inputs": [ { @@ -35,14 +36,15 @@ } ], "execution": { - "Node24_1": { + "Node24": { "target": "dist/index.js" }, "Node20_1": { "target": "dist/index.js" }, - "Node20": { - "target": "dist/index.js" - } - } + "Node20": { + "target": "dist/index.js" + } + } } + diff --git a/tasks/download/task.json b/tasks/download/task.json index 1df928e..369be5d 100644 --- a/tasks/download/task.json +++ b/tasks/download/task.json @@ -12,6 +12,7 @@ "Minor": 0, "Patch": 0 }, + "minimumAgentVersion": "3.224.1", "instanceNameFormat": "ALCops Download Analyzers $(version)", "inputs": [ { @@ -87,14 +88,14 @@ } ], "execution": { - "Node24_1": { + "Node24": { "target": "dist/index.js" }, "Node20_1": { "target": "dist/index.js" }, - "Node20": { - "target": "dist/index.js" - } - } -} \ No newline at end of file + "Node20": { + "target": "dist/index.js" + } + } +} diff --git a/tasks/install-analyzers/task.json b/tasks/install-analyzers/task.json index 12017ab..c4553c9 100644 --- a/tasks/install-analyzers/task.json +++ b/tasks/install-analyzers/task.json @@ -13,6 +13,7 @@ "Minor": 0, "Patch": 0 }, + "minimumAgentVersion": "3.224.1", "instanceNameFormat": "ALCops Install Analyzers $(version)", "inputs": [ { @@ -94,14 +95,15 @@ } ], "execution": { - "Node24_1": { + "Node24": { "target": "dist/index.js" }, "Node20_1": { "target": "dist/index.js" }, - "Node20": { - "target": "dist/index.js" - } - } + "Node20": { + "target": "dist/index.js" + } + } } + diff --git a/tests/scaffold.test.ts b/tests/scaffold.test.ts index 96f19e1..a6fe835 100644 --- a/tests/scaffold.test.ts +++ b/tests/scaffold.test.ts @@ -53,9 +53,15 @@ describe('scaffold: task.json files', () => { 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('Node24'); expect(taskJson.execution).toHaveProperty('Node20_1'); expect(taskJson.execution).toHaveProperty('Node20'); + // Guard against the invalid handler name reappearing (Node24_1 is not a + // recognized Azure Pipelines handler; the correct key is Node24). + expect(taskJson.execution).not.toHaveProperty('Node24_1'); + // minimumAgentVersion gives old agents a clear error instead of the cryptic + // "supported task execution handler was not found" message. + expect(taskJson.minimumAgentVersion).toBe('3.224.1'); }); it(`should have src/index.ts for ${taskDir}`, () => {