From da74eef72714f00e9c6a277a2f8b9f215be06e7e Mon Sep 17 00:00:00 2001 From: Mark Rhoades-Brown Date: Fri, 29 May 2026 22:58:14 +0100 Subject: [PATCH 1/3] ci: add conventional commits enforcement and auto-versioning --- .husky/commit-msg | 40 ++++++++++++++++++++++++++ manifest.json | 2 +- package-lock.json | 21 ++++++++++++-- package.json | 4 ++- version-bump.mjs | 72 +++++++++++++++++++++++++++++++++++++++-------- 5 files changed, 124 insertions(+), 15 deletions(-) create mode 100644 .husky/commit-msg diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100644 index 0000000..75f8a15 --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1,40 @@ +#!/usr/bin/env sh + +# Enforce conventional commit format: +# type(scope)?: subject +# +# Allowed types: +# feat, fix, chore, docs, style, refactor, perf, test, build, ci, revert +# +# Examples: +# feat: add multi-repo support +# fix(sync): handle empty tree response +# chore: release 0.5.0 [skip ci] +# feat!: redesign settings API (BREAKING CHANGE) + +commit_msg=$(cat "$1") + +# Allow merge commits and revert commits generated by git +if echo "$commit_msg" | grep -qE "^(Merge |Revert )"; then + exit 0 +fi + +pattern="^(feat|fix|chore|docs|style|refactor|perf|test|build|ci|revert)(\(.+\))?!?: .+" + +if ! echo "$commit_msg" | grep -qE "$pattern"; then + echo "" + echo "ERROR: Commit message does not follow Conventional Commits format." + echo "" + echo " Expected: (): " + echo "" + echo " Allowed types: feat, fix, chore, docs, style, refactor, perf, test, build, ci, revert" + echo "" + echo " Examples:" + echo " feat: add multi-repo support" + echo " fix(sync): handle empty tree response" + echo " docs: update README with versioning info" + echo "" + echo " Your message: $commit_msg" + echo "" + exit 1 +fi diff --git a/manifest.json b/manifest.json index dac0c0e..5f165ac 100644 --- a/manifest.json +++ b/manifest.json @@ -7,4 +7,4 @@ "author": "M Rhoades-Brown", "authorUrl": "https://github.com/rhoades-brown", "isDesktopOnly": false -} \ No newline at end of file +} diff --git a/package-lock.json b/package-lock.json index 6c69199..0007d5a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "github-octokit", - "version": "0.3.2", + "version": "0.4.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "github-octokit", - "version": "0.3.2", + "version": "0.4.1", "license": "MIT", "devDependencies": { "@types/jest": "^30.0.0", @@ -15,6 +15,7 @@ "eslint": "^10.4.1", "eslint-plugin-obsidianmd": "^0.3.0", "globals": "^17.6.0", + "husky": "^9.1.7", "jest": "^30.4.2", "obsidian": "^1.13.0", "octokit": "^5.0.5", @@ -5859,6 +5860,22 @@ "node": ">=10.17.0" } }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/ignore": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", diff --git a/package.json b/package.json index 3d68ca2..75832dd 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "test:coverage": "npx jest --coverage", "lint": "eslint . --ext .ts", "lint:fix": "eslint . --ext .ts --fix", - "version": "node version-bump.mjs && git add manifest.json versions.json" + "version": "node version-bump.mjs && git add manifest.json versions.json", + "prepare": "husky" }, "keywords": [], "author": "", @@ -24,6 +25,7 @@ "eslint": "^10.4.1", "eslint-plugin-obsidianmd": "^0.3.0", "globals": "^17.6.0", + "husky": "^9.1.7", "jest": "^30.4.2", "obsidian": "^1.13.0", "octokit": "^5.0.5", diff --git a/version-bump.mjs b/version-bump.mjs index 55d631f..7b37e90 100644 --- a/version-bump.mjs +++ b/version-bump.mjs @@ -1,17 +1,67 @@ import { readFileSync, writeFileSync } from "fs"; -const targetVersion = process.env.npm_package_version; +/** + * Version bump script with two modes: + * + * 1. npm version hook (default): + * Called automatically by `npm version` — reads the target version from + * process.env.npm_package_version and updates manifest.json + versions.json. + * + * 2. CI conventional-commits mode: + * Called with `--bump=major|minor|patch` — computes the next version from + * the current manifest.json version, then updates manifest.json, + * package.json, and versions.json. + */ -// read minAppVersion from manifest.json and bump version to target version +const bumpArg = process.argv.find((a) => a.startsWith("--bump=")); +let targetVersion; + +if (bumpArg) { + // CI mode: compute new version from current + bump type + const bumpType = bumpArg.split("=")[1]; + const current = JSON.parse(readFileSync("manifest.json", "utf8")).version; + const [major, minor, patch] = current.split(".").map(Number); + + switch (bumpType) { + case "major": + targetVersion = `${major + 1}.0.0`; + break; + case "minor": + targetVersion = `${major}.${minor + 1}.0`; + break; + case "patch": + targetVersion = `${major}.${minor}.${patch + 1}`; + break; + default: + console.error(`Unknown bump type: ${bumpType}`); + process.exit(1); + } + + // In CI mode also update package.json + const pkg = JSON.parse(readFileSync("package.json", "utf8")); + pkg.version = targetVersion; + writeFileSync("package.json", JSON.stringify(pkg, null, "\t") + "\n"); +} else { + // npm version hook mode — package.json is already bumped by npm + targetVersion = process.env.npm_package_version; +} + +if (!targetVersion) { + console.error( + "No target version. Use --bump=major|minor|patch or run via npm version.", + ); + process.exit(1); +} + +// Update manifest.json const manifest = JSON.parse(readFileSync("manifest.json", "utf8")); const { minAppVersion } = manifest; manifest.version = targetVersion; -writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t")); - -// update versions.json with target version and minAppVersion from manifest.json -// but only if the target version is not already in versions.json -const versions = JSON.parse(readFileSync('versions.json', 'utf8')); -if (!Object.values(versions).includes(minAppVersion)) { - versions[targetVersion] = minAppVersion; - writeFileSync('versions.json', JSON.stringify(versions, null, '\t')); -} +writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t") + "\n"); + +// Update versions.json — always add the new version entry +const versions = JSON.parse(readFileSync("versions.json", "utf8")); +versions[targetVersion] = minAppVersion; +writeFileSync("versions.json", JSON.stringify(versions, null, "\t") + "\n"); + +console.log(`Bumped version to ${targetVersion}`); From 03aa0b37b4ecfc770785d1dab8c76cd63852c5a3 Mon Sep 17 00:00:00 2001 From: Mark Rhoades-Brown Date: Fri, 29 May 2026 22:59:51 +0100 Subject: [PATCH 2/3] ci: auto-version releases from conventional commits on merge to main --- .github/workflows/ci.yml | 124 ++++++++++++++++++++++++++++----------- 1 file changed, 89 insertions(+), 35 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fd496a2..9486ab6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,12 +11,12 @@ on: - master jobs: + # ------------------------------------------------------------------ + # Build & test — runs on every push and PR + # ------------------------------------------------------------------ build: runs-on: ubuntu-latest - permissions: - contents: write - steps: - name: Checkout uses: actions/checkout@v4 @@ -39,30 +39,98 @@ jobs: - name: Run tests run: npm test - # Release step - only runs on push to main/master (not PRs) - - name: Get version from manifest - id: version + # ------------------------------------------------------------------ + # Release — only on push to main/master, after build passes. + # Analyses conventional commits since the last tag, bumps the + # version, commits the change, and creates a GitHub release. + # ------------------------------------------------------------------ + release: + if: github.event_name == 'push' + needs: build + runs-on: ubuntu-latest + + permissions: + contents: write + + steps: + - name: Checkout (full history) + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Skip if version-bump commit + id: skip run: | - VERSION=$(node -p "require('./manifest.json').version") - echo "version=$VERSION" >> $GITHUB_OUTPUT - echo "Release version: $VERSION" + MSG=$(git log -1 --pretty=format:"%s") + if echo "$MSG" | grep -q "\[skip ci\]"; then + echo "skip=true" >> $GITHUB_OUTPUT + echo "Skipping release — version-bump commit detected." + else + echo "skip=false" >> $GITHUB_OUTPUT + fi - - name: Check if release exists - if: github.event_name == 'push' - id: check_release + - name: Determine version bump from conventional commits + if: steps.skip.outputs.skip == 'false' + id: bump run: | - if gh release view "${{ steps.version.outputs.version }}" > /dev/null 2>&1; then - echo "exists=true" >> $GITHUB_OUTPUT - echo "Release ${{ steps.version.outputs.version }} already exists, skipping..." + LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") + if [ -z "$LATEST_TAG" ]; then + RANGE="HEAD" else - echo "exists=false" >> $GITHUB_OUTPUT - echo "Release ${{ steps.version.outputs.version }} does not exist, will create..." + RANGE="${LATEST_TAG}..HEAD" fi - env: - GH_TOKEN: ${{ github.token }} - - name: Create Release - if: github.event_name == 'push' && steps.check_release.outputs.exists == 'false' + COMMITS=$(git log $RANGE --pretty=format:"%s" --no-merges) + echo "Commits since ${LATEST_TAG:-beginning}:" + echo "$COMMITS" + + BUMP="none" + if echo "$COMMITS" | grep -qiE "(BREAKING[ -]CHANGE|^[a-z]+(\(.+\))?!:)"; then + BUMP="major" + elif echo "$COMMITS" | grep -qE "^feat(\(.+\))?!?:"; then + BUMP="minor" + elif echo "$COMMITS" | grep -qE "^fix(\(.+\))?!?:"; then + BUMP="patch" + fi + + echo "bump=$BUMP" >> $GITHUB_OUTPUT + echo "Determined bump type: $BUMP" + + - name: Setup Node.js + if: steps.skip.outputs.skip == 'false' && steps.bump.outputs.bump != 'none' + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + if: steps.skip.outputs.skip == 'false' && steps.bump.outputs.bump != 'none' + run: npm ci + + - name: Bump version + if: steps.skip.outputs.skip == 'false' && steps.bump.outputs.bump != 'none' + id: version + run: | + node version-bump.mjs --bump=${{ steps.bump.outputs.bump }} + VERSION=$(node -p "JSON.parse(require('fs').readFileSync('manifest.json','utf8')).version") + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "New version: $VERSION" + + - name: Rebuild with new version + if: steps.skip.outputs.skip == 'false' && steps.bump.outputs.bump != 'none' + run: npm run build + + - name: Commit and push version bump + if: steps.skip.outputs.skip == 'false' && steps.bump.outputs.bump != 'none' + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add manifest.json package.json versions.json package-lock.json + git commit -m "chore: release ${{ steps.version.outputs.version }} [skip ci]" + git push + + - name: Create release + if: steps.skip.outputs.skip == 'false' && steps.bump.outputs.bump != 'none' uses: softprops/action-gh-release@v2 with: tag_name: ${{ steps.version.outputs.version }} @@ -74,17 +142,3 @@ jobs: generate_release_notes: true draft: false prerelease: false - - - name: Create Pre-Release - if: github.event_name != 'push' - uses: softprops/action-gh-release@v2 - with: - tag_name: ${{ steps.version.outputs.version }}-${{ github.run_number }} - name: ${{ steps.version.outputs.version }}-${{ github.run_number }} - files: | - main.js - manifest.json - styles.css - generate_release_notes: true - draft: false - prerelease: true From 7747e3e839eb8c45900ca574c54c5db74af4ae01 Mon Sep 17 00:00:00 2001 From: Mark Rhoades-Brown Date: Fri, 29 May 2026 23:00:59 +0100 Subject: [PATCH 3/3] docs: Add dev docs and refactor the README --- DEVELOPMENT.md | 111 +++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 43 +------------------ 2 files changed, 113 insertions(+), 41 deletions(-) create mode 100644 DEVELOPMENT.md diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 0000000..aa7f551 --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,111 @@ +# Development + +## Getting started + +```bash +# Clone and install +git clone https://github.com/rhoades-brown/obsidian-github.git +cd obsidian-github +npm install + +# Build +npm run build + +# Run tests +npm test + +# Watch mode (rebuilds on file changes) +npm run dev + +# Lint +npm run lint +``` + +## Project structure + +```text +obsidian-github/ +├── main.ts # Plugin entry point +├── src/ +│ ├── services/ # Core services (GitHub API, Sync, Logger) +│ ├── views/ # UI components (DiffView, SyncView) +│ ├── ui/ # Settings tab, modals +│ └── utils/ # Utility functions (file, diff, encoding) +├── tests/ # Jest test suites +└── styles.css # Plugin styles +``` + +## Conventional commits + +All commit messages **must** follow the [Conventional Commits](https://www.conventionalcommits.org/) format: + +```text +(): +``` + +A [husky](https://typicode.github.io/husky/) commit-msg hook validates every commit locally. Allowed types: + +| Type | Purpose | +| ------ | --------- | +| `feat` | A new feature (triggers **minor** version bump) | +| `fix` | A bug fix (triggers **patch** version bump) | +| `docs` | Documentation only | +| `style` | Code style (formatting, semicolons, etc.) | +| `refactor` | Refactoring (no feature or fix) | +| `perf` | Performance improvement | +| `test` | Adding or updating tests | +| `build` | Build system or dependencies | +| `ci` | CI/CD changes | +| `chore` | Maintenance tasks | +| `revert` | Reverting a previous commit | + +Append `!` after the type (e.g. `feat!:`) or include `BREAKING CHANGE` in the commit body to trigger a **major** version bump. + +### Examples + +```text +feat: add multi-repo support +fix(sync): handle empty tree response +docs: update README with versioning info +chore: update dependencies +feat!: redesign settings API +``` + +## Versioning & releases + +Version bumping is fully automated — you never need to edit `manifest.json`, `package.json`, or `versions.json` manually. + +When a PR is merged to `main`, the CI workflow: + +1. Runs lint, build, and tests +2. Analyses commit messages since the last release tag +3. Determines the SemVer bump type (`major` / `minor` / `patch`) +4. Bumps the version in `manifest.json`, `package.json`, and `versions.json` via `version-bump.mjs` +5. Commits the version bump with `[skip ci]` to avoid re-triggering CI +6. Creates a GitHub release with `main.js`, `manifest.json`, and `styles.css` attached + +If no `feat:` or `fix:` commits are found since the last release, no version bump or release is created. + +### Manual version bumping + +If you ever need to bump the version manually (e.g. for a pre-release), you can use: + +```bash +npm version patch # 0.4.1 → 0.4.2 +npm version minor # 0.4.1 → 0.5.0 +npm version major # 0.4.1 → 1.0.0 +``` + +This triggers the `version` script in `package.json`, which runs `version-bump.mjs` to keep all three version files in sync. + +## Contributing + +Contributions are welcome! Please: + +1. Fork the repository +2. Create a feature branch (`git checkout -b feature/amazing-feature`) +3. Make your changes +4. Run tests (`npm test`) and lint (`npm run lint`) +5. Commit using conventional commits (`git commit -m 'feat: add amazing feature'`) +6. Push to the branch (`git push origin feature/amazing-feature`) +7. Open a Pull Request diff --git a/README.md b/README.md index dc1f580..f920063 100644 --- a/README.md +++ b/README.md @@ -147,36 +147,9 @@ Add custom patterns in Settings → GitHub Octokit → Ignore Patterns: - `*.tmp` - All .tmp files - `.obsidian/**` - All Obsidian settings (if desired) -## Development +## Development & contributing -```bash -# Clone and install -git clone https://github.com/rhoades-brown/obsidian-github.git -cd obsidian-github -npm install - -# Build -npm run build - -# Run tests -npm test - -# Watch mode (rebuilds on file changes) -npm run dev -``` - -### Project Structure - -```text -obsidian-github/ -├── main.ts # Plugin entry point -├── src/ -│ ├── services/ # Core services (GitHub API, Sync, Logger) -│ ├── views/ # UI components (DiffView, SyncView) -│ └── utils/ # Utility functions (file, diff, encoding) -├── tests/ # Jest test suites -└── styles.css # Plugin styles -``` +See [DEVELOPMENT.md](DEVELOPMENT.md) for build instructions, project structure, conventional commit guidelines, and the automated versioning workflow. ## Troubleshooting @@ -210,18 +183,6 @@ obsidian-github/ 3. Open the debug console (macOS → cmd+option+i; Windows → ctrl+shift+i) to see real-time logs 4. Or click "View Logs" in settings to see recent entries -## Contributing - -Contributions are welcome! Please: - -1. Fork the repository -2. Create a feature branch (`git checkout -b feature/amazing-feature`) -3. Make your changes -4. Run tests (`npm test`) -5. Commit your changes (`git commit -m 'Add amazing feature'`) -6. Push to the branch (`git push origin feature/amazing-feature`) -7. Open a Pull Request - ## License MIT