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` (fallback)
1. Agent picks the best available Node handler: `Node24_1` → `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 @@ -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
Expand Down Expand Up @@ -131,7 +131,7 @@ vi.mock('../../shared/http-range');

## Adding a New Task

1. Create `tasks/<task-name>/task.json` — unique GUID, Node24_1 + Node20_1 handlers
1. Create `tasks/<task-name>/task.json` — unique GUID, Node24_1 + Node20_1 + Node20 handlers
2. Create `tasks/<task-name>/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`)
Expand All @@ -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
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 handlers
1. Create `tasks/<task-name>/task.json` with a unique GUID, Node24_1 + Node20_1 + Node20 handlers
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 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
7 changes: 5 additions & 2 deletions tasks/detect-tfm-bc-artifact/task.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@
},
"Node20_1": {
"target": "dist/index.js"
}
}
},
"Node20": {
"target": "dist/index.js"
}
}
}
7 changes: 5 additions & 2 deletions tasks/detect-tfm-marketplace/task.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@
},
"Node20_1": {
"target": "dist/index.js"
}
}
},
"Node20": {
"target": "dist/index.js"
}
}
}
7 changes: 5 additions & 2 deletions tasks/detect-tfm-nuget-devtools/task.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@
},
"Node20_1": {
"target": "dist/index.js"
}
}
},
"Node20": {
"target": "dist/index.js"
}
}
}
9 changes: 6 additions & 3 deletions tasks/download/task.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@
},
"Node20_1": {
"target": "dist/index.js"
}
}
}
},
"Node20": {
"target": "dist/index.js"
}
}
}
7 changes: 5 additions & 2 deletions tasks/install-analyzers/task.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@
},
"Node20_1": {
"target": "dist/index.js"
}
}
},
"Node20": {
"target": "dist/index.js"
}
}
}
113 changes: 57 additions & 56 deletions tests/scaffold.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});