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
53 changes: 47 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@ name: CI
on:
push:
pull_request:
workflow_call:

permissions:
contents: read

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

jobs:
build-test:
Expand All @@ -12,31 +20,64 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-node@v4
with:
node-version: 20
node-version-file: .nvmrc
cache: npm
- name: Install
run: npm install --no-fund --no-audit
run: npm ci --no-fund --no-audit
- name: Lint
run: npm run lint
- name: Type check
run: npm run typecheck
- name: Build
run: npm run build
- name: Unit tests
run: npm run test:unit

e2e:
strategy:
fail-fast: false
matrix:
vscode: ['1.90.0', stable]
runs-on: ubuntu-latest
needs: build-test
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
Comment thread
Bowlerr marked this conversation as resolved.
Resolved
with:
persist-credentials: false
- uses: actions/setup-node@v4
with:
node-version: 20
node-version-file: .nvmrc
cache: npm
- name: Install
run: npm install --no-fund --no-audit
run: npm ci --no-fund --no-audit
- name: Build
run: npm run build
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
- name: E2E (harness)
run: npm run test:e2e
- name: E2E (VS Code and exported artifacts)
env:
OMV_E2E_VSCODE_VERSION: ${{ matrix.vscode }}
run: xvfb-run -a --server-args="-screen 0 1920x1080x24" npm run test:e2e
- name: Package extension
if: matrix.vscode == 'stable'
run: npm run package
- name: Upload verified package
if: matrix.vscode == 'stable'
uses: actions/upload-artifact@v4
with:
name: offline-markdown-preview-vsix
path: '*.vsix'
if-no-files-found: error
overwrite: true
- name: Upload E2E evidence
if: failure()
uses: actions/upload-artifact@v4
with:
name: e2e-failure-evidence-${{ matrix.vscode }}
path: test-results/
overwrite: true
188 changes: 188 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
name: Release

on:
push:
branches: [main]
paths: [package.json]
workflow_dispatch:

permissions:
contents: read

concurrency:
group: release
cancel-in-progress: false

jobs:
version:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
outputs:
changed: ${{ steps.version.outputs.changed }}
version: ${{ steps.version.outputs.version }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
- id: version
env:
EVENT_NAME: ${{ github.event_name }}
BEFORE_SHA: ${{ github.event.before }}
run: |
version=$(node -p "require('./package.json').version")
echo "version=$version" >> "$GITHUB_OUTPUT"
if [ "$EVENT_NAME" = workflow_dispatch ]; then
echo "changed=true" >> "$GITHUB_OUTPUT"
else
previous=$(git show "$BEFORE_SHA:package.json" | node -e "let text='';process.stdin.on('data',chunk=>text+=chunk).on('end',()=>console.log(JSON.parse(text).version))")
if [ "$version" != "$previous" ]; then
echo "changed=true" >> "$GITHUB_OUTPUT"
else
echo "changed=false" >> "$GITHUB_OUTPUT"
fi
fi

verify:
needs: version
if: needs.version.outputs.changed == 'true'
uses: ./.github/workflows/ci.yml

github-release:
needs: [version, verify]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/download-artifact@v4
with:
name: offline-markdown-preview-vsix
- name: Extract release notes
run: |
node <<'JS'
const fs = require('node:fs');
const version = require('./package.json').version;
const section = fs.readFileSync('CHANGELOG.md', 'utf8').split(`## ${version}\n`)[1]?.split('\n## ')[0]?.trim();
if (!section) throw new Error(`Missing changelog for ${version}`);
fs.writeFileSync('release-notes.md', section + '\n');
JS
- name: Create or verify immutable release artifact
env:
GH_TOKEN: ${{ github.token }}
RELEASE_VERSION: ${{ needs.version.outputs.version }}
run: |
file="offline-markdown-preview-$RELEASE_VERSION.vsix"
if gh api "repos/$GITHUB_REPOSITORY/commits/$RELEASE_VERSION" --jq .sha > tag-sha.txt 2> tag-error.txt; then
tag_sha=$(cat tag-sha.txt)
elif grep -q 'HTTP 404' tag-error.txt; then
tag_sha=''
else
cat tag-error.txt >&2
exit 1
fi
if [ -n "$tag_sha" ] && [ "$tag_sha" != "$GITHUB_SHA" ]; then
echo '::error::Release version already points to a different source revision. Bump the version.'
exit 1
fi
if gh release view "$RELEASE_VERSION" >/dev/null 2>&1; then
is_draft=$(gh release view "$RELEASE_VERSION" --json isDraft --jq .isDraft)
if [ "$is_draft" = true ]; then
echo '::error::An existing draft release needs source verification and publication or removal before retrying.'
exit 1
fi
if [ "$tag_sha" != "$GITHUB_SHA" ]; then
echo '::error::Existing release has no matching source tag.'
exit 1
fi
asset_names=$(gh release view "$RELEASE_VERSION" --json assets --jq '.assets[].name')
if printf '%s\n' "$asset_names" | grep -Fxq "$file"; then
mkdir existing-release
gh release download "$RELEASE_VERSION" --pattern "$file" --dir existing-release
# Reuse published bytes on retries: ZIP timestamps vary between builds.
cp "existing-release/$file" "$file"
else
# Recover a release whose tag/notes were created before an upload failed.
gh release upload "$RELEASE_VERSION" "$file"
fi
else
gh release create "$RELEASE_VERSION" "$file" --target "$GITHUB_SHA" --title "$RELEASE_VERSION" --notes-file release-notes.md
fi
sha256sum "$file" >> "$GITHUB_STEP_SUMMARY"
- name: Preserve canonical release artifact for both registries
uses: actions/upload-artifact@v4
with:
name: release-vsix
path: '*.vsix'
if-no-files-found: error
overwrite: true

marketplace:
needs: [version, github-release]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm
- run: npm ci --no-fund --no-audit
- uses: actions/download-artifact@v4
with:
name: release-vsix
- name: Publish verified VSIX to Marketplace
env:
VSCE_PAT: ${{ secrets.VSCE_PAT }}
RELEASE_VERSION: ${{ needs.version.outputs.version }}
run: |
if [ -z "$VSCE_PAT" ]; then
echo '::error::Configure repository secret VSCE_PAT with publishing access for bowlerr.'
exit 1
fi
npx --no-install vsce publish --packagePath "offline-markdown-preview-$RELEASE_VERSION.vsix" --skip-duplicate
- name: Verify Marketplace version
env:
RELEASE_VERSION: ${{ needs.version.outputs.version }}
run: |
for attempt in 1 2 3 4 5 6; do
if npx --no-install vsce show bowlerr.offline-markdown-preview --json > marketplace.json && node -e "const p=require('./marketplace.json');process.exit(p.versions.some(v=>v.version===process.env.RELEASE_VERSION)?0:1)"; then exit 0; fi
sleep 10
done
echo '::error::Marketplace has not reported the published version.'
exit 1

open-vsx:
needs: [version, github-release]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm
- run: npm ci --no-fund --no-audit
- uses: actions/download-artifact@v4
with:
name: release-vsix
- name: Publish verified VSIX to Open VSX
env:
OVSX_PAT: ${{ secrets.OVSX_PAT }}
RELEASE_VERSION: ${{ needs.version.outputs.version }}
run: |
if [ -z "$OVSX_PAT" ]; then
echo '::error::Configure repository secret OVSX_PAT with publishing access for bowlerr.'
exit 1
fi
npx --no-install ovsx publish "offline-markdown-preview-$RELEASE_VERSION.vsix" --skip-duplicate
- name: Verify Open VSX version
env:
RELEASE_VERSION: ${{ needs.version.outputs.version }}
run: |
curl --fail --silent --show-error --retry 5 --retry-all-errors --retry-delay 10 "https://open-vsx.org/api/bowlerr/offline-markdown-preview/$RELEASE_VERSION" -o open-vsx.json
node -e "const p=require('./open-vsx.json');if(p.version!==process.env.RELEASE_VERSION)throw new Error('Open VSX version mismatch')"
8 changes: 7 additions & 1 deletion .vscodeignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ test/**
test-results/**
node_modules/**/.cache/**
*.vsix
*.map
**/*.map
**/*.d.ts
package-lock.json
tsconfig.json
vitest.config.ts
Expand All @@ -19,5 +20,10 @@ yarn.lock
.eslintrc.cjs
.prettierrc
.DS_Store
images/*.gif
docs/RELEASE-PLAN.md
.nvmrc
.gitignore
**/.gitkeep
vitest.config.*
playwright.config.*
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
# Changelog

## 0.5.0

- Search fenced code and phrases spanning inline formatting, with previous-match keyboard navigation, clear no-results feedback, and accessible controls
- Preserve the reading position during live updates and keyboard focus in the contents list
- Add keyboard and button controls for diagram panning, improve focus indicators and image actions, and respect reduced-motion preferences
- Improve Mermaid connector and arrow contrast in the preview and exported documents
- Preserve the preview background and text colors in standalone exports so translucent diagram backgrounds retain their contrast
- Serialize preview rendering so rapid edits cannot mix diagram/math output from different document versions
- Export only a completed snapshot of the requested document, including when exporting before the preview has opened; report a clear error if rendering cannot finish
- Apply offline Content Security Policy to HTML/PDF exports, retaining the remote-image opt-in and preventing document scripts from running in exported files
- Reject forged internal image attributes and unauthorized local image paths, including symlink escapes, before opening or embedding files
- Bound remote-image downloads by the configured image size and a 30-second timeout
- Update affected rendering and validation dependencies, add strict TypeScript checking, and make dependency installs reproducible with one npm lockfile
- Replace fixture-only E2E checks with real VS Code preview, search, navigation, diagram, HTML-export and PDF-export journeys
- Minify production bundles and exclude demonstration GIFs from the installed extension package

## 0.4.0

- Resolve percent-encoded local image paths, including filenames with spaces and reserved characters, while rejecting malformed or unsafe encoded separators
Expand Down
25 changes: 24 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,32 @@ Thank you for your interest in contributing. Pull requests, bug reports, and fea
- Add or update tests for bug fixes and new features when practical
- Keep user-facing docs/settings descriptions accurate

## Development and verification

Use the Node version in `.nvmrc` (`nvm use`), then `npm ci`. npm and `package-lock.json` are the canonical dependency workflow.

Run `npm run check` for lint, strict TypeScript checking, unit tests, and the production build. Run `npm run watch` for readable development bundles.

`npm run test:e2e` launches a real VS Code instance with an isolated temporary profile and workspace. It exercises rendering, keyboard navigation, search, live edits, offline behavior, and HTML/PDF export. Install the test browser with `npx playwright install chromium`; use `xvfb-run -a --server-args="-screen 0 1920x1080x24" npm run test:e2e` on headless Linux. A missing display or failed application launch fails the suite.

Set `VSCODE_EXECUTABLE_PATH` to test an existing VS Code executable, or `OMV_E2E_VSCODE_VERSION` to download a specific version. E2E traces and screenshots are written under `test-results/e2e`.

## Release process

1. Update the version in `package.json` and `package-lock.json`, and write the changelog and any behavior documentation.
2. Run `npm ci`, `npm run check`, `npm run test:e2e`, and `npm audit`. Review dependency advisories in context; do not silently force major upgrades.
3. Run `npm run package`, inspect the VSIX contents, and verify installation and key workflows. Demo GIFs stay in the repository/Marketplace README and are excluded from the installed package.
4. Commit the verified source and obtain passing CI/review. CI checks lint, strict types, build and units on Linux, Windows and macOS, plus real E2E journeys on stable and minimum-supported VS Code.
5. Merge the version change to `main`. The Release workflow repeats the gates, creates a version tag and GitHub release, and publishes the **same verified VSIX** to both registries using repository secrets `VSCE_PAT` and `OVSX_PAT`. Missing credentials fail the affected publishing job explicitly. A workflow retry reuses an existing GitHub release asset only when its tag matches the source revision.
6. Verify both registry versions and download metadata against the intended artifact. Report a partial publication explicitly if one registry fails. For manual recovery, download the existing GitHub release VSIX and use `npx --no-install vsce publish --packagePath offline-markdown-preview-VERSION.vsix` and `npx --no-install ovsx publish offline-markdown-preview-VERSION.vsix`, or upload that file in the publisher interface. Never commit or print credentials.

The dependency overrides intentionally share patched KaTeX between the preview and Mermaid while preserving Mermaid 11 layouts, and keep publishing tooling on Azure Identity 4.13.0 (the compatible MSAL 3 dependency line) until its newer dependency resolves GHSA-xcx6-jx8p-x3h5. Reassess these overrides with upstream updates and rendering/publishing checks.

The October 2026 audit reports no production advisories. Three moderate development dependency entries share GHSA-w5hq-g745-h8pq in MSAL's UUID 8 dependency; its affected v3/v5/v6 buffer functions are not used by MSAL's `uuid.v4()` call. These publishing tools are excluded from the VSIX. We retain the supported dependency contract instead of forcing UUID across major versions; reassess when Azure publishes compatible fixes.

## Reporting Issues

- Use the issue tracker for bugs and feature requests: `TODO: add GitHub issues URL`
- Use the [issue tracker](https://github.com/Bowlerr/offline-markdown-preview/issues) for bugs and feature requests.
- Include reproduction steps, sample Markdown (if relevant), screenshots, and logs/errors
- Note your VS Code version, OS, and extension version

Expand Down
Loading
Loading