Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<task>/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()`
Expand Down
6 changes: 3 additions & 3 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -123,7 +123,7 @@ vi.mock('@alcops/core', async (importOriginal) => {

## Adding a New Task

1. Create `tasks/<task-name>/task.json` — unique GUID, Node24_1 + Node20_1 + Node20 handlers
1. Create `tasks/<task-name>/task.json` — unique GUID, Node24 + Node20_1 + Node20 handlers, `minimumAgentVersion` `3.224.1`
2. Create `tasks/<task-name>/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`)
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ npx vitest --watch # Watch mode

### Adding a New Task

1. Create `tasks/<task-name>/task.json` with a unique GUID, Node24_1 + Node20_1 + Node20 handlers
1. Create `tasks/<task-name>/task.json` with a unique GUID, Node24 + Node20_1 + Node20 handlers, and `minimumAgentVersion` `3.224.1`
2. Create `tasks/<task-name>/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
Expand Down Expand Up @@ -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
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion esbuild.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const sharedOptions = {
bundle: true,
format: 'cjs',
platform: 'node',
target: 'node24',
target: 'node20',
sourcemap: !isProduction,
minify: isProduction,
logLevel: 'info',
Expand Down
12 changes: 7 additions & 5 deletions tasks/detect-tfm-bc-artifact/task.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"Minor": 0,
"Patch": 0
},
"minimumAgentVersion": "3.224.1",
"instanceNameFormat": "ALCops Detect TFM from BC Artifact",
"inputs": [
{
Expand All @@ -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"
}
}
}

12 changes: 7 additions & 5 deletions tasks/detect-tfm-marketplace/task.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"Minor": 0,
"Patch": 0
},
"minimumAgentVersion": "3.224.1",
"instanceNameFormat": "ALCops Detect TFM from VS Marketplace",
"inputs": [
{
Expand Down Expand Up @@ -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"
}
}
}

12 changes: 7 additions & 5 deletions tasks/detect-tfm-nuget-devtools/task.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"Minor": 0,
"Patch": 0
},
"minimumAgentVersion": "3.224.1",
"instanceNameFormat": "ALCops Detect TFM from NuGet DevTools",
"inputs": [
{
Expand All @@ -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"
}
}
}

13 changes: 7 additions & 6 deletions tasks/download/task.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"Minor": 0,
"Patch": 0
},
"minimumAgentVersion": "3.224.1",
"instanceNameFormat": "ALCops Download Analyzers $(version)",
"inputs": [
{
Expand Down Expand Up @@ -87,14 +88,14 @@
}
],
"execution": {
"Node24_1": {
"Node24": {
"target": "dist/index.js"
},
"Node20_1": {
"target": "dist/index.js"
},
"Node20": {
"target": "dist/index.js"
}
}
}
"Node20": {
"target": "dist/index.js"
}
}
}
12 changes: 7 additions & 5 deletions tasks/install-analyzers/task.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"Minor": 0,
"Patch": 0
},
"minimumAgentVersion": "3.224.1",
"instanceNameFormat": "ALCops Install Analyzers $(version)",
"inputs": [
{
Expand Down Expand Up @@ -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"
}
}
}

8 changes: 7 additions & 1 deletion tests/scaffold.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`, () => {
Expand Down