Skip to content

feat(errors): typed errors with status and URL for error responses #27

feat(errors): typed errors with status and URL for error responses

feat(errors): typed errors with status and URL for error responses #27

Workflow file for this run

name: CI
on:
push:
branches: [main]
pull_request:
types: ['opened', 'synchronize']
branches: [master, main]
# Read-only token unless a job asks for more; none of these jobs writes.
permissions:
contents: read
jobs:
automated-test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18, 20, 22, 24]
environment:
name: Production
steps:
- uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Use test/build Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version < 22 && 22 || matrix.node-version }}
cache: 'pnpm'
- name: Install dependencies
run: pnpm i --frozen-lockfile
- name: Run `typecheck`
if: matrix.node-version >= 22
run: pnpm typecheck
- name: Run `lint`
if: matrix.node-version >= 22
run: pnpm lint
- name: Build package for entry tests
if: matrix.node-version >= 22
run: pnpm build
- name: Run `test`
if: matrix.node-version >= 22
run: pnpm test
env:
RECORD_NETWORK_REQUESTS: 'false'
MOCK_FETCH: 'true'
CREDENTIAL_ICLOUD_USERNAME: ${{secrets.CREDENTIAL_ICLOUD_USERNAME}}
CREDENTIAL_ICLOUD_APP_SPECIFIC_PASSWORD: ${{secrets.CREDENTIAL_ICLOUD_APP_SPECIFIC_PASSWORD}}
CREDENTIAL_GOOGLE_USERNAME: ${{secrets.CREDENTIAL_GOOGLE_USERNAME}}
CREDENTIAL_GOOGLE_REFRESH_TOKEN: ${{secrets.CREDENTIAL_GOOGLE_REFRESH_TOKEN}}
CREDENTIAL_GOOGLE_CLIENT_ID: ${{secrets.CREDENTIAL_GOOGLE_CLIENT_ID}}
CREDENTIAL_GOOGLE_CLIENT_SECRET: ${{secrets.CREDENTIAL_GOOGLE_CLIENT_SECRET}}
CREDENTIAL_FASTMAIL_USERNAME: ${{secrets.CREDENTIAL_FASTMAIL_USERNAME}}
CREDENTIAL_FASTMAIL_APP_PASSWORD: ${{secrets.CREDENTIAL_FASTMAIL_APP_PASSWORD}}
CREDENTIAL_NEXTCLOUD_USERNAME: ${{secrets.CREDENTIAL_NEXTCLOUD_USERNAME}}
CREDENTIAL_NEXTCLOUD_PASSWORD: ${{secrets.CREDENTIAL_NEXTCLOUD_PASSWORD}}
CREDENTIAL_NEXTCLOUD_SERVER_URL: ${{secrets.CREDENTIAL_NEXTCLOUD_SERVER_URL}}
- name: Check public consumers and examples
if: matrix.node-version >= 22
run: |
pnpm test:consumer
pnpm test:examples
pnpm exec knip --dependencies --workspace .
- name: Build package for runtime smoke test
if: matrix.node-version < 22
run: |
pnpm build
mkdir -p /tmp/tsdav-runtime
npm pack --pack-destination /tmp/tsdav-runtime > /tmp/tsdav-runtime/pack.out
- name: Use runtime Node.js
if: matrix.node-version < 22
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Test packed package on runtime Node.js
if: matrix.node-version < 22
run: |
mkdir -p /tmp/tsdav-runtime/consumer
cd /tmp/tsdav-runtime/consumer
echo '{ "name": "consumer", "version": "0.0.0", "private": true }' > package.json
# Installed under the name `tsdav`, the way consumers alias @philflow/tsdav.
npm install "tsdav@file:$(find /tmp/tsdav-runtime -maxdepth 1 -name 'philflow-tsdav-*.tgz' -print -quit)" --no-audit --no-fund
node -e "const mod = require('tsdav'); if (typeof mod.DAVClient !== 'function') process.exit(1)"
node --input-type=module -e "import { DAVClient } from 'tsdav'; if (typeof DAVClient !== 'function') process.exit(1)"
bun-smoke-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Use Node.js 22
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
- name: Install dependencies
run: pnpm i --frozen-lockfile
- name: Build
run: pnpm build
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Import + instantiate from ESM bundle under Bun
run: |
cat > .tsdav-bun-smoke.mjs <<'EOF'
import { DAVClient, createDAVClient, DAVNamespace } from './dist/tsdav.esm.js';
if (typeof DAVClient !== 'function') throw new Error('DAVClient missing');
if (typeof createDAVClient !== 'function') throw new Error('createDAVClient missing');
if (!DAVNamespace) throw new Error('DAVNamespace missing');
// Exercise the auth helper path that previously crashed under
// native ESM due to base-64's CJS shape.
new DAVClient({
serverUrl: 'http://example.com',
credentials: { username: 'u', password: 'p' },
});
console.log('bun-ok');
EOF
bun .tsdav-bun-smoke.mjs | tee /tmp/bun.out
grep -q '^bun-ok$' /tmp/bun.out
- name: Import from CJS bundle under Bun
run: |
cat > .tsdav-bun-cjs.cjs <<'EOF'
const { DAVClient } = require('./dist/tsdav.cjs.js');
if (typeof DAVClient !== 'function') throw new Error('DAVClient missing from CJS');
console.log('bun-cjs-ok');
EOF
bun .tsdav-bun-cjs.cjs | tee /tmp/bun-cjs.out
grep -q '^bun-cjs-ok$' /tmp/bun-cjs.out
deno-smoke-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Use Node.js 22
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
- name: Install dependencies
run: pnpm i --frozen-lockfile
- name: Build
run: pnpm build
- name: Setup Deno
uses: denoland/setup-deno@v2
with:
deno-version: v2.x
- name: Pack tarball from repo root
id: pack
run: |
# Pack using the real root package.json so that `files`, `main`,
# `module`, `types`, and `exports` resolve the way they would on
# the npm registry. `dist/package.json` is a stale informational
# copy with paths that are only valid from inside the repo root.
mkdir -p /tmp/deno-smoke
npm pack --pack-destination /tmp/deno-smoke > /tmp/pack.out
TARBALL=$(find /tmp/deno-smoke -maxdepth 1 -name 'philflow-tsdav-*.tgz' -print -quit)
echo "tarball=$TARBALL" >> "$GITHUB_OUTPUT"
- name: 'Import @philflow/tsdav under Deno via npm: specifier'
run: |
# Build a throwaway consumer workspace that installs the packed
# tarball into node_modules, then tell Deno to consume that
# existing node_modules via `nodeModulesDir: "manual"`. Using
# `--node-modules-dir=auto` here would make Deno resolve
# `npm:@philflow/tsdav` against the public registry (ignoring our local
# build), which defeats the whole point of the smoke test.
mkdir -p /tmp/deno-smoke/consumer
cd /tmp/deno-smoke/consumer
cat > package.json <<EOF
{
"name": "consumer",
"private": true,
"type": "module",
"dependencies": { "@philflow/tsdav": "file:${{ steps.pack.outputs.tarball }}" }
}
EOF
npm install --no-audit --no-fund --no-save --no-package-lock
# Sanity-check: the installed package must be the freshly packed
# version and must NOT drag in `cross-fetch`.
INSTALLED_VERSION=$(node -p "require('./node_modules/@philflow/tsdav/package.json').version")
echo "installed tsdav version: $INSTALLED_VERSION"
if [ -d node_modules/cross-fetch ]; then
echo "cross-fetch unexpectedly present in node_modules"
exit 1
fi
cat > deno.json <<'EOF'
{
"nodeModulesDir": "manual"
}
EOF
cat > smoke.ts <<'EOF'
import { DAVClient, createDAVClient, DAVNamespace } from 'npm:@philflow/tsdav';
if (typeof DAVClient !== 'function') throw new Error('DAVClient missing');
if (typeof createDAVClient !== 'function') throw new Error('createDAVClient missing');
if (!DAVNamespace) throw new Error('DAVNamespace missing');
new DAVClient({
serverUrl: 'http://example.com',
credentials: { username: 'u', password: 'p' },
});
console.log('deno-ok');
EOF
deno run --allow-read --allow-env smoke.ts | tee /tmp/deno.out
grep -q '^deno-ok$' /tmp/deno.out
docs-check:
runs-on: ubuntu-latest
defaults:
run:
working-directory: docs
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
cache-dependency-path: docs/pnpm-lock.yaml
- run: pnpm install --frozen-lockfile
- name: Install shared-source verification tools
working-directory: .
run: pnpm install --frozen-lockfile
- name: Check docs dependencies
working-directory: .
run: pnpm exec knip --dependencies --workspace docs
- run: pnpm test
- run: pnpm typecheck
- run: pnpm build