diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0061b25..afef57d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,17 +5,23 @@ on: branches: [main] pull_request: branches: [main] - types: [opened, synchronize, reopened, edited, ready_for_review] workflow_call: permissions: contents: read +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: ci: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 + with: + persist-credentials: false + submodules: recursive - uses: pnpm/action-setup@v5 - uses: actions/setup-node@v6 with: @@ -23,6 +29,12 @@ jobs: cache: pnpm - run: pnpm install --frozen-lockfile - run: pnpm build + - run: pnpm tsc + - run: pnpm lint + - name: Check formatting + run: pnpm exec oxfmt --check . + - run: pnpm test --coverage + # --- repository-specific steps (package-content check, size budget, e2e, docs, ...) go here --- - name: Verify published packages include dist run: | for pkg in packages/hdrify packages/cli packages/hdrify-react; do @@ -31,17 +43,11 @@ jobs: exit 1 } done - - name: Generate OpenCLI spec - run: node packages/cli/dist/index.js docgen --output packages/cli/opencli.json - - uses: bhouston/clidoc-action@v1 - with: - files: packages/cli/opencli.json - - run: pnpm tsc - - run: pnpm lint - - run: pnpm test --coverage - run: pnpm release:check - name: Bundle size budget run: pnpm size | tee -a "$GITHUB_STEP_SUMMARY" + - name: Package VS Code extension + run: pnpm --filter hdrify-vscode-extension run package - name: Audit dependencies id: audit continue-on-error: true @@ -54,32 +60,8 @@ jobs: name: coverage-${{ github.run_id }}-${{ github.job }} path: coverage/ - uses: codecov/codecov-action@v5 + if: ${{ !cancelled() && hashFiles('coverage/lcov.info') != '' }} with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage/lcov.info fail_ci_if_error: false - - contribution: - if: github.event_name == 'pull_request' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - uses: pnpm/action-setup@v5 - - uses: actions/setup-node@v6 - with: - node-version-file: '.nvmrc' - cache: pnpm - - run: pnpm install --frozen-lockfile - - name: Validate issue link - run: node scripts/check-pr.mjs - - name: Lint PR title - env: - PR_TITLE: ${{ github.event.pull_request.title }} - run: printf '%s\n' "$PR_TITLE" | pnpm exec commitlint - - name: Lint feature commits - env: - BASE_SHA: ${{ github.event.pull_request.base.sha }} - HEAD_SHA: ${{ github.event.pull_request.head.sha }} - run: pnpm exec commitlint --from "$BASE_SHA" --to "$HEAD_SHA" diff --git a/.github/workflows/pr-policy.yml b/.github/workflows/pr-policy.yml new file mode 100644 index 0000000..147ecd9 --- /dev/null +++ b/.github/workflows/pr-policy.yml @@ -0,0 +1,39 @@ +name: pr-policy + +on: + pull_request: + branches: [main] + types: [opened, edited, synchronize, reopened, ready_for_review] + +permissions: + contents: read + +concurrency: + group: pr-policy-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + contribution: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + persist-credentials: false + - uses: pnpm/action-setup@v5 + - uses: actions/setup-node@v6 + with: + node-version-file: '.nvmrc' + cache: pnpm + - run: pnpm install --frozen-lockfile --ignore-scripts + - name: Validate target branch and linked issue + run: node scripts/check-pr.mjs + - name: Lint PR title + env: + PR_TITLE: ${{ github.event.pull_request.title }} + run: printf '%s\n' "$PR_TITLE" | pnpm exec commitlint + - name: Lint PR commits + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: pnpm exec commitlint --from "$BASE_SHA" --to "$HEAD_SHA" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4e5891b..76debb7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,13 +8,13 @@ on: type: boolean default: false -concurrency: - group: npm-release - cancel-in-progress: false - permissions: contents: read +concurrency: + group: release + cancel-in-progress: false + jobs: guard: runs-on: ubuntu-latest @@ -24,30 +24,44 @@ jobs: run: | echo "::error::Release must be dispatched with ref=main (got ${{ github.ref }})." exit 1 + checks: needs: guard uses: ./.github/workflows/ci.yml secrets: inherit + release: - needs: checks + needs: [guard, checks] runs-on: ubuntu-latest + environment: npm permissions: contents: write id-token: write + env: + HUSKY: '0' steps: - uses: actions/checkout@v6 with: + ref: ${{ github.sha }} fetch-depth: 0 + persist-credentials: false + - name: Verify main has not advanced since dispatch + env: + GH_TOKEN: ${{ github.token }} + run: | + head="$(gh api "repos/${{ github.repository }}/git/ref/heads/main" --jq .object.sha)" + if [ "$head" != "$GITHUB_SHA" ]; then + echo "::error::main moved from $GITHUB_SHA to $head after dispatch; dispatch the release again." + exit 1 + fi - uses: pnpm/action-setup@v5 - uses: actions/setup-node@v6 with: node-version-file: '.nvmrc' cache: pnpm - run: pnpm install --frozen-lockfile - - name: Require an existing release baseline - run: git rev-parse --verify refs/tags/v1.1.4 - run: pnpm build - - name: Release packages and changelog + - name: Release run: pnpm release ${{ inputs.dry_run && '--dry-run' || '' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -55,4 +69,4 @@ jobs: OVSX_PAT: ${{ secrets.OVSX_PAT }} - name: Summary if: inputs.dry_run - run: echo "Dry run complete — no packages were published, no tag was created." >> "$GITHUB_STEP_SUMMARY" + run: echo "Dry run complete; nothing was published and no tag was created." >> "$GITHUB_STEP_SUMMARY" diff --git a/.oxfmtrc.json b/.oxfmtrc.json index e21de03..aee8a71 100644 --- a/.oxfmtrc.json +++ b/.oxfmtrc.json @@ -2,5 +2,5 @@ "$schema": "./node_modules/oxfmt/configuration_schema.json", "singleQuote": true, "printWidth": 120, - "ignorePatterns": ["submodule/**", "**/routeTree.gen.ts"] + "ignorePatterns": ["**/*.gen.ts"] } diff --git a/.oxlintrc.json b/.oxlintrc.json index c6eaaca..47687cf 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -8,13 +8,14 @@ "plugins": ["eslint", "typescript", "oxc", "unicorn", "react", "jsx-a11y"], "rules": { "eslint/no-underscore-dangle": "off", - "no-unused-private-class-members": "off", + "eslint/no-unused-private-class-members": "error", "react/react-in-jsx-scope": "off", + "typescript/no-explicit-any": "error", "unicorn/no-new-array": "off" }, "overrides": [ { - "files": ["**/*.test.ts", "**/*.test.tsx", "**/*.spec.ts", "**/*.spec.tsx"], + "files": ["**/*.test.ts", "**/*.test.tsx", "**/*.spec.ts", "**/*.spec.tsx", "**/test/**", "**/tests/**"], "plugins": ["eslint", "typescript", "oxc", "unicorn", "react", "jsx-a11y", "vitest"] } ] diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..b1ce03d --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +submodule/** diff --git a/package.json b/package.json index 56a488c..34fec34 100644 --- a/package.json +++ b/package.json @@ -8,9 +8,8 @@ "build": "pnpm -r build", "dev": "pnpm -r --parallel dev", "clean": "rimraf dist packages/**/dist .nx node_modules packages/**/node_modules", - "check": "pnpm lint && pnpm format", "tsc": "pnpm -r tsc", - "test": "pnpm vitest", + "test": "vitest run", "test:profile": "PROFILE=1 pnpm vitest run --no-coverage", "test:profile:main": "node --cpu-prof --cpu-prof-dir=./profile-output ./node_modules/vitest/vitest.mjs run --no-coverage", "profile:analyze": "node scripts/analyze-cpuprofile.mjs", @@ -21,8 +20,7 @@ "hdrify": "node ./packages/cli/dist/index.js", "release": "semantic-release", "release:check": "node --test scripts/release.test.mjs", - "size": "size-limit", - "format:check": "oxfmt --check ." + "size": "size-limit" }, "devDependencies": { "@anolilab/semantic-release-pnpm": "^8.1.25", @@ -77,7 +75,7 @@ } ], "engines": { - "node": ">=26.0.0" + "node": ">=22.0.0" }, "packageManager": "pnpm@11.27.0" } diff --git a/packages/hdrify-react/package.json b/packages/hdrify-react/package.json index 429f5a2..663cdab 100644 --- a/packages/hdrify-react/package.json +++ b/packages/hdrify-react/package.json @@ -49,6 +49,6 @@ "react-dom": "^18.0.0 || ^19.0.0" }, "engines": { - "node": ">=26.0.0" + "node": ">=22.0.0" } } diff --git a/packages/hdrify/package.json b/packages/hdrify/package.json index 1f47266..5428eb1 100644 --- a/packages/hdrify/package.json +++ b/packages/hdrify/package.json @@ -53,6 +53,6 @@ "exifreader": "^4.36.0" }, "engines": { - "node": ">=26.0.0" + "node": ">=22.0.0" } } diff --git a/scripts/release-vscode-extension.mjs b/scripts/release-vscode-extension.mjs index 7aba4e1..ab188a2 100644 --- a/scripts/release-vscode-extension.mjs +++ b/scripts/release-vscode-extension.mjs @@ -20,13 +20,6 @@ export function setVersion(version) { // and Open VSX (the registry Cursor and other VS Code-compatible editors // use). Requires VSCE_PAT / OVSX_PAT in the environment. export function publish() { - const missing = [!process.env.VSCE_PAT && 'VSCE_PAT', !process.env.OVSX_PAT && 'OVSX_PAT'].filter(Boolean); - if (missing.length > 0) { - throw new Error( - `Cannot publish the VS Code extension: ${missing.join(' and ')} ${missing.length > 1 ? 'are' : 'is'} not set. Configure ${missing.length > 1 ? 'them' : 'it'} as repository secret(s) (see RELEASING.md) before dispatching a release.`, - ); - } - execFileSync('pnpm', ['--filter', 'hdrify-vscode-extension', 'run', 'build'], { stdio: 'inherit' }); execFileSync('pnpm', ['--filter', 'hdrify-vscode-extension', 'exec', 'vsce', 'package', '--no-dependencies'], { stdio: 'inherit', diff --git a/scripts/release.test.mjs b/scripts/release.test.mjs index 831b012..bf53593 100644 --- a/scripts/release.test.mjs +++ b/scripts/release.test.mjs @@ -4,7 +4,6 @@ import { resolve } from 'node:path'; import test from 'node:test'; import { analyzeCommits } from '@semantic-release/commit-analyzer'; import { checkPullRequest } from './check-pr.mjs'; -import { publish as publishExtension } from './release-vscode-extension.mjs'; // Published via pnpm publish directly (see release.config.js): pnpm rewrites // workspace:* deps and packs the `files` field natively, so these packages @@ -41,18 +40,6 @@ for (const [message, expected] of [ }); } -test('VS Code extension publish fails fast with a clear error when VSCE_PAT/OVSX_PAT are unset', () => { - const { VSCE_PAT, OVSX_PAT } = process.env; - delete process.env.VSCE_PAT; - delete process.env.OVSX_PAT; - try { - assert.throws(() => publishExtension(), /VSCE_PAT and OVSX_PAT are not set/); - } finally { - if (VSCE_PAT !== undefined) process.env.VSCE_PAT = VSCE_PAT; - if (OVSX_PAT !== undefined) process.env.OVSX_PAT = OVSX_PAT; - } -}); - test('PR policy enforces issue link and integration target, not branch name', () => { const pr = { base: { ref: 'main' },