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
155 changes: 17 additions & 138 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,168 +2,47 @@ name: CI

on:
push:
branches: [main]
pull_request:

concurrency:
group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
lint:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm run lint

build:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm run build

unit:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
verify-ubuntu:
name: verify (ubuntu-latest)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
node-version: 24
cache: npm
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- run: npm ci
- run: npm run lint
- run: npm run build
- name: Run unit tests
run: npm test

windows-qmd-smoke:
name: windows-qmd-smoke (PR #11 verification)
unit-windows:
name: unit (windows-latest)
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
node-version: 24
cache: npm
- run: npm ci

- name: Install qmd via npm
shell: pwsh
run: npm install -g @tobilu/qmd

- name: Ensure qmd prerequisites are on PATH
shell: pwsh
run: |
$npmPrefix = npm prefix -g
Write-Host "Adding to PATH: $npmPrefix"
Add-Content -Path $env:GITHUB_PATH -Value $npmPrefix
$gitUsrBin = Join-Path $env:ProgramFiles "Git\usr\bin"
if (Test-Path $gitUsrBin) {
Write-Host "Adding to PATH: $gitUsrBin"
Add-Content -Path $env:GITHUB_PATH -Value $gitUsrBin
}

- name: Confirm qmd is callable (via the same direct-node path the wrapper uses)
shell: pwsh
run: |
$prefix = (npm prefix -g).Trim()
Write-Host "--- npm prefix shims ($prefix) ---"
Get-ChildItem -Path $prefix -Filter "qmd*" -ErrorAction SilentlyContinue | Format-Table -AutoSize Name, Length
Write-Host "--- Get-Command qmd -All ---"
Get-Command qmd -All | Format-Table -AutoSize
# cmd-shim writes literal `/bin/sh` into both qmd.cmd and qmd.ps1 on
# Windows, so neither shim is callable directly. The extension bypasses
# this by invoking qmd's JS entry with node. Verify that path exists
# and works.
$qmdJs = Join-Path $prefix "node_modules\@tobilu\qmd\dist\cli\qmd.js"
Write-Host "--- expected qmd.js: $qmdJs ---"
if (-not (Test-Path $qmdJs)) {
Write-Error "qmd.js not found at $qmdJs"
exit 1
}
node $qmdJs --version
if ($LASTEXITCODE -ne 0) {
Write-Error "node $qmdJs --version failed (exit $LASTEXITCODE)"
exit 1
}

# Captures stock Node execFile behavior for the runner's qmd shim. This
# is informational because shim behavior varies by package manager.
- name: Smoke (informational) — stock Node execFile -> qmd
shell: pwsh
continue-on-error: true
run: |
node -e "const { execFile } = require('node:child_process'); execFile('qmd', ['--version'], (err, stdout, stderr) => { if (err) { console.error('STOCK execFile FAILED (expected on Windows):', err.code, err.message); process.exit(1); } console.log('STOCK execFile OK:', stdout.trim()); });"

# Exercises the extension's qmd execFile wrapper under Node. The
# setupQmdCollection() calls qmd via the wrapped execFile, so a
# successful run here verifies Windows shell invocation.
- name: Smoke — extension wrapper -> qmd (must succeed)
shell: pwsh
run: |
node --import tsx -e "import('./index.ts').then(async (m) => { m.ensureDirs(); const ok = await m.setupQmdCollection(); if (!ok) { console.error('setupQmdCollection returned false — wrapper did not fix qmd invocation'); process.exit(1); } console.log('OK: extension wrapper successfully invoked qmd through the Windows shell'); }).catch((e) => { console.error('FAILED:', e); process.exit(1); });"

# Verifies resolveMemoryDir() falls back to USERPROFILE when HOME
# is unset (the second bug PR #11 fixes). We import the module in a
# subprocess with HOME deliberately removed, then have it create the
# default memory directory and print its location.
- name: Smoke — resolveMemoryDir USERPROFILE fallback
shell: pwsh
run: |
Remove-Item Env:HOME -ErrorAction SilentlyContinue
Remove-Item Env:PI_MEMORY_DIR -ErrorAction SilentlyContinue
if (-not $env:USERPROFILE) { Write-Error "USERPROFILE is not set on this runner"; exit 1 }
$expectedPrefix = Join-Path $env:USERPROFILE ".pi\agent\memory"
Write-Host "Expected prefix: $expectedPrefix"
$actual = node --import tsx -e "import('./index.ts').then(m => { console.log(m.dailyPath('2026-01-01')); });"
Write-Host "ensureDirs/dailyPath returned: $actual"
if ($actual -notlike "$expectedPrefix*") {
Write-Error "FAILED: resolveMemoryDir did not honor USERPROFILE fallback. Got: $actual"
exit 1
}
if ($actual -like "*~\.pi*") {
Write-Error "FAILED: literal '~' subdirectory still present in resolved path"
exit 1
}
Write-Host "OK: memory directory resolved under USERPROFILE"

test:
runs-on: ubuntu-latest
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
PI_E2E_PROVIDER: openai
PI_E2E_MODEL: gpt-4o-mini
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: oven-sh/setup-bun@v2
with:
node-version: 20
cache: npm
bun-version: latest
- run: npm ci
- name: Run e2e tests
if: ${{ env.OPENAI_API_KEY != '' }}
- name: Run unit tests
run: npm test
- name: Skip e2e tests (missing OPENAI_API_KEY)
if: ${{ env.OPENAI_API_KEY == '' }}
run: echo "Skipping e2e tests. Add an OPENAI_API_KEY repository secret to enable."
31 changes: 31 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: E2E

on:
workflow_dispatch:

concurrency:
group: e2e-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
e2e:
runs-on: ubuntu-latest
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
PI_E2E_PROVIDER: openai
PI_E2E_MODEL: gpt-4o-mini
steps:
- name: Require API key
shell: bash
run: test -n "$OPENAI_API_KEY"
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
- run: npm ci
- name: Run e2e tests
run: npm run test:e2e
123 changes: 123 additions & 0 deletions .github/workflows/windows-qmd-smoke.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
name: Windows qmd smoke

on:
push:
branches: [main]
paths:
- index.ts
- package.json
- package-lock.json
- test/qmd-cache.ts
- .github/workflows/windows-qmd-smoke.yml
pull_request:
paths:
- index.ts
- package.json
- package-lock.json
- test/qmd-cache.ts
- .github/workflows/windows-qmd-smoke.yml
workflow_dispatch:

concurrency:
group: qmd-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
windows-qmd-smoke:
name: windows-qmd-smoke
runs-on: windows-latest
env:
QMD_VERSION: "2.5.3"
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci

- name: Set isolated qmd prefix
id: qmd-prefix
shell: pwsh
run: |
$prefix = Join-Path $env:RUNNER_TEMP "qmd"
"path=$prefix" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
"QMD_PREFIX=$prefix" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append

- name: Cache qmd installation
id: qmd-cache
uses: actions/cache@v4
with:
path: ${{ steps.qmd-prefix.outputs.path }}
key: qmd-${{ runner.os }}-node-22-${{ env.QMD_VERSION }}

- name: Install qmd via npm
if: steps.qmd-cache.outputs.cache-hit != 'true'
shell: pwsh
run: npm install --global --prefix "$env:QMD_PREFIX" "@tobilu/qmd@$env:QMD_VERSION"

- name: Add qmd and Git prerequisites to PATH
shell: pwsh
run: |
Write-Host "Adding to PATH: $env:QMD_PREFIX"
Add-Content -Path $env:GITHUB_PATH -Value $env:QMD_PREFIX
$gitUsrBin = Join-Path $env:ProgramFiles "Git\usr\bin"
if (Test-Path $gitUsrBin) {
Write-Host "Adding to PATH: $gitUsrBin"
Add-Content -Path $env:GITHUB_PATH -Value $gitUsrBin
}

- name: Confirm qmd is callable through its direct-node path
shell: pwsh
run: |
Write-Host "--- npm prefix shims ($env:QMD_PREFIX) ---"
Get-ChildItem -Path $env:QMD_PREFIX -Filter "qmd*" -ErrorAction SilentlyContinue | Format-Table -AutoSize Name, Length
Write-Host "--- Get-Command qmd -All ---"
Get-Command qmd -All | Format-Table -AutoSize
$qmdJs = Join-Path $env:QMD_PREFIX "node_modules\@tobilu\qmd\dist\cli\qmd.js"
Write-Host "--- expected qmd.js: $qmdJs ---"
if (-not (Test-Path $qmdJs)) {
Write-Error "qmd.js not found at $qmdJs"
exit 1
}
node $qmdJs --version
if ($LASTEXITCODE -ne 0) {
Write-Error "node $qmdJs --version failed (exit $LASTEXITCODE)"
exit 1
}

# Captures stock Node execFile behavior for the runner's qmd shim. This
# is informational because shim behavior varies by package manager.
- name: Smoke (informational) — stock Node execFile -> qmd
shell: pwsh
continue-on-error: true
run: |
node -e "const { execFile } = require('node:child_process'); execFile('qmd', ['--version'], (err, stdout, stderr) => { if (err) { console.error('STOCK execFile FAILED (expected on Windows):', err.code, err.message); process.exit(1); } console.log('STOCK execFile OK:', stdout.trim()); });"

- name: Smoke — extension wrapper -> qmd (must succeed)
shell: pwsh
run: |
node --import tsx -e "import('./index.ts').then(async (m) => { m.ensureDirs(); const ok = await m.setupQmdCollection(); if (!ok) { console.error('setupQmdCollection returned false — wrapper did not fix qmd invocation'); process.exit(1); } console.log('OK: extension wrapper successfully invoked qmd through the Windows shell'); }).catch((e) => { console.error('FAILED:', e); process.exit(1); });"

- name: Smoke — resolveMemoryDir USERPROFILE fallback
shell: pwsh
run: |
Remove-Item Env:HOME -ErrorAction SilentlyContinue
Remove-Item Env:PI_MEMORY_DIR -ErrorAction SilentlyContinue
if (-not $env:USERPROFILE) { Write-Error "USERPROFILE is not set on this runner"; exit 1 }
$expectedPrefix = Join-Path $env:USERPROFILE ".pi\agent\memory"
Write-Host "Expected prefix: $expectedPrefix"
$actual = node --import tsx -e "import('./index.ts').then(m => { console.log(m.dailyPath('2026-01-01')); });"
Write-Host "ensureDirs/dailyPath returned: $actual"
if ($actual -notlike "$expectedPrefix*") {
Write-Error "FAILED: resolveMemoryDir did not honor USERPROFILE fallback. Got: $actual"
exit 1
}
if ($actual -like "*~\.pi*") {
Write-Error "FAILED: literal '~' subdirectory still present in resolved path"
exit 1
}
Write-Host "OK: memory directory resolved under USERPROFILE"
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ follows [Semantic Versioning](https://semver.org/).

### Changed

- Reduced pull-request CI duplication and setup overhead by consolidating the
fast verification jobs, canceling superseded runs, moving API-backed e2e to
an explicit workflow, and caching the path-filtered Windows qmd smoke test.
- Migrated Pi runtime imports and peer dependencies from the retired
`@mariozechner` scope to `@earendil-works` 0.81.1+, including its unified
TypeBox exports and compatibility API. The minimum Node.js version is now
Expand Down
37 changes: 37 additions & 0 deletions test/unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,43 @@ describe("runtime package scope", () => {
});
});

describe("GitHub Actions workflows", () => {
const ciWorkflow = fs.readFileSync(new URL("../.github/workflows/ci.yml", import.meta.url), "utf-8");
const e2eWorkflow = fs.readFileSync(new URL("../.github/workflows/e2e.yml", import.meta.url), "utf-8");
const qmdWorkflow = fs.readFileSync(new URL("../.github/workflows/windows-qmd-smoke.yml", import.meta.url), "utf-8");

test("runs feature-branch CI once and cancels superseded runs", () => {
expect(ciWorkflow).toContain("push:\n branches: [main]\n pull_request:");
expect(ciWorkflow).toContain(
`group: ci-\${{ github.workflow }}-\${{ github.event.pull_request.number || github.ref }}`,
);
expect(ciWorkflow).toContain("cancel-in-progress: true");
});

test("installs once per OS for the fast verification path", () => {
expect(ciWorkflow.match(/- run: npm ci/g)).toHaveLength(2);
expect(ciWorkflow).toContain("name: verify (ubuntu-latest)");
expect(ciWorkflow).toContain("name: unit (windows-latest)");
expect(ciWorkflow).not.toContain("matrix:");
expect(ciWorkflow).not.toContain("windows-qmd-smoke");
expect(ciWorkflow).not.toContain("OPENAI_API_KEY");
});

test("pins and caches the path-filtered Windows qmd smoke", () => {
expect(qmdWorkflow).toContain("name: Windows qmd smoke");
expect(qmdWorkflow).toContain("paths:");
expect(qmdWorkflow).toContain('QMD_VERSION: "2.5.3"');
expect(qmdWorkflow).toContain("uses: actions/cache@v4");
expect(qmdWorkflow).toContain('"@tobilu/qmd@$env:QMD_VERSION"');
});

test("keeps API-backed e2e explicit and uses the e2e command", () => {
expect(e2eWorkflow).toContain("workflow_dispatch:");
expect(e2eWorkflow).not.toContain("pull_request:");
expect(e2eWorkflow).toContain("run: npm run test:e2e");
});
});

// We need to import the default export to register tools
import registerExtension from "../index.js";

Expand Down
Loading