diff --git a/.github/workflows/vsix.yml b/.github/workflows/vsix.yml new file mode 100644 index 0000000..99dbb86 --- /dev/null +++ b/.github/workflows/vsix.yml @@ -0,0 +1,61 @@ +name: Package VSIX + +on: + pull_request: + push: + branches: + - main + tags: + - "v*" + workflow_dispatch: + +jobs: + package: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci + - run: npm test + - run: npm run typecheck + - run: npm run package + - uses: actions/upload-artifact@v4 + with: + name: wgit-vsix + path: wgit-*.vsix + if-no-files-found: error + + release: + if: startsWith(github.ref, 'refs/tags/v') + needs: package + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + name: wgit-vsix + path: dist + - name: Require the tag to match the VSIX version + run: | + file=$(ls dist/wgit-*.vsix) + base=$(basename "$file" .vsix) + version=${base#wgit-} + tag=${GITHUB_REF_NAME#v} + test "$version" = "$tag" + - name: Publish GitHub Release + env: + GH_TOKEN: ${{ github.token }} + run: | + tag="$GITHUB_REF_NAME" + version="${tag#v}" + gh release create "$tag" "dist/wgit-${version}.vsix" \ + --repo "$GITHUB_REPOSITORY" \ + --title "WGit ${tag}" \ + --notes "Install wgit-${version}.vsix in Cursor or VS Code: Command Palette, then Extensions: Install from VSIX. After reload, type \`wg st\`. The status bar shows \`WGit: N\`." diff --git a/CHANGELOG.md b/CHANGELOG.md index 65dfc17..b3feaed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,5 +16,6 @@ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht - Dirty working tree policy: `skip` (default), `stash`, or `fail`. One repository never aborts the batch. - Status bar item `WGit: N` and an output channel named `WGit`. - Commands `WG: Switch Branch (sb)`, `WG: Pull All (pa)`, `WG: Fetch All (fa)`, `WG: Status (st)`, and `WG: Refresh (rf)`. +- GitHub Actions publishes `wgit-.vsix` on the matching `v*` tag. [0.1.0]: https://github.com/alpdogan/wgit/releases/tag/v0.1.0 diff --git a/README.md b/README.md index 07ad4b7..6914940 100644 --- a/README.md +++ b/README.md @@ -5,16 +5,16 @@ Fast multi-root Git sync. Switch, fetch, and pull across every repository in the WGit is a VS Code and Cursor extension. You type `wg sb` and every repo moves to the branch you picked. A repo that only has `origin/dev` gets a local `dev` that tracks it. A repo that cannot move is skipped, with a reason, and the rest of the batch still runs. ``` -repo | before | after | action | result | error -already-on | dev | dev | already-on | ok | -admin-panel-auth-service | master | dev | create-track | ok | -no-dev | master | master | skip: missing-remote-branch | skipped | -dirty-repo | master | master | skip: dirty | skipped | +repo | before | after | action | result | error +already-on | dev | dev | already-on | ok | +example-service | master | dev | create-track | ok | +no-dev | master | master | skip: missing-remote-branch | skipped | +dirty-repo | master | master | skip: dirty | skipped | ``` ## Why -Multi-root workspaces drift. Most repos sit on `dev`. One of them, `admin-panel-auth-service`, is still on `master`. `origin/dev` exists. There is no local `dev`. “Switch all to dev” should create that tracking branch. It should not leave the repo on `master` and hide the reason in a toast. +Multi-root workspaces drift. Most repos sit on `dev`. One of them is still on `master`. `origin/dev` exists. There is no local `dev`. “Switch all to dev” should create that tracking branch. It should not leave that repo on `master` and hide the reason in a toast. WGit plans the operation before it changes any branch, then prints one row per repository. @@ -74,29 +74,31 @@ Dirty means `git status --porcelain` is not empty, including untracked files. A WGit does not activate in an untrusted workspace, and it does not run in virtual workspaces. Git has to be on `PATH`. Fetch and pull use `GIT_TERMINAL_PROMPT=0`, so a missing credential fails that repository instead of hanging the batch on a prompt. -## Install from VSIX +## Install from GitHub -WGit targets VS Code and Cursor (`engines.vscode` `^1.85.0`). The 0.1 package is installed from a VSIX. Registry publishing is documented below and is not required to try it. - -```bash -git clone https://github.com/alpdogan/wgit.git -cd wgit -npm install -npm run package -``` - -`npm run package` writes `wgit-0.1.0.vsix` in this directory. +Download [`wgit-0.1.0.vsix`](https://github.com/alpdogan/wgit/releases/download/v0.1.0/wgit-0.1.0.vsix) from the [v0.1.0 release](https://github.com/alpdogan/wgit/releases/tag/v0.1.0). That file is attached when the `v0.1.0` tag is pushed. GitHub Actions runs the tests, builds the VSIX, and publishes the release. In Cursor or VS Code: 1. Open the Command Palette. 2. Run **Extensions: Install from VSIX...**. -3. Choose `wgit-0.1.0.vsix`. +3. Choose the downloaded `wgit-0.1.0.vsix`. 4. Reload the window when the editor asks. 5. Open a multi-root workspace, then run **WG: Status (st)** or type `wg st`. Trust the workspace when asked. The status bar should show `WGit: N` for the repositories it found. +To build the same file yourself: + +```bash +git clone https://github.com/alpdogan/wgit.git +cd wgit +npm install +npm run package +``` + +`npm run package` writes `wgit-0.1.0.vsix` in this directory. WGit targets VS Code and Cursor (`engines.vscode` `^1.85.0`). + ### Open VSX, then Marketplace Publishing is manual. Create an [Open VSX](https://open-vsx.org/) access token and, only when you intend to publish: diff --git a/test/acceptance.test.ts b/test/acceptance.test.ts index d6f478f..98afccd 100644 --- a/test/acceptance.test.ts +++ b/test/acceptance.test.ts @@ -29,18 +29,18 @@ describe('acceptance: switch a multi-root workspace to dev', () => { const dirty = seedDirty(root); const repos: NamedRepo[] = [ { path: alreadyOn, name: 'already-on' }, - { path: authService, name: 'admin-panel-auth-service' }, + { path: authService, name: 'example-service' }, { path: missing, name: 'no-dev' }, { path: dirty, name: 'dirty-repo' }, ]; - const beforeFetch = await loadRepoStatus(authService, 'admin-panel-auth-service', 'origin'); + const beforeFetch = await loadRepoStatus(authService, 'example-service', 'origin'); assert.equal(beforeFetch?.remoteBranches.includes('dev'), false); assert.equal(beforeFetch?.branch, 'master'); const fetched = await fetchRepos(repos, 'origin', config.maxParallel); assert.equal(fetched.every((row) => row.result === 'ok'), true); - const afterFetch = await loadRepoStatus(authService, 'admin-panel-auth-service', 'origin'); + const afterFetch = await loadRepoStatus(authService, 'example-service', 'origin'); assert.equal(afterFetch?.remoteBranches.includes('dev'), true); assert.equal(afterFetch?.localBranches.includes('dev'), false); @@ -67,7 +67,7 @@ describe('acceptance: switch a multi-root workspace to dev', () => { await switchRepos(repos, 'dev', config), ); assertRow(first, 'already-on', { action: 'already-on', result: 'ok', after: 'dev' }); - assertRow(first, 'admin-panel-auth-service', { action: 'create-track', result: 'ok', after: 'dev' }); + assertRow(first, 'example-service', { action: 'create-track', result: 'ok', after: 'dev' }); assertRow(first, 'no-dev', { action: 'skip: missing-remote-branch', result: 'skipped', after: 'master' }); assertRow(first, 'dirty-repo', { action: 'skip: dirty', result: 'skipped', after: 'master' }); @@ -82,7 +82,7 @@ describe('acceptance: switch a multi-root workspace to dev', () => { const second = await switchRepos(repos, 'dev', config); assertRow(second, 'already-on', { action: 'already-on', result: 'ok', after: 'dev' }); - assertRow(second, 'admin-panel-auth-service', { action: 'already-on', result: 'ok', after: 'dev' }); + assertRow(second, 'example-service', { action: 'already-on', result: 'ok', after: 'dev' }); assertRow(second, 'no-dev', { action: 'skip: missing-remote-branch', result: 'skipped', after: 'master' }); assertRow(second, 'dirty-repo', { action: 'skip: dirty', result: 'skipped', after: 'master' }); assert.equal(fs.readFileSync(path.join(dirty, 'readme.txt'), 'utf8'), 'local edit\n'); @@ -229,7 +229,7 @@ function seedRemoteOnlyDev(root: string): string { git(seed, ['checkout', '-b', 'dev']); commitFile(seed, 'b.txt', 'dev\n', 'dev'); git(seed, ['push', '-u', 'origin', 'dev']); - const repo = cloneFrom(root, origin, 'admin-panel-auth-service'); + const repo = cloneFrom(root, origin, 'example-service'); if (currentBranch(repo) !== 'master') { git(repo, ['checkout', 'master']); } diff --git a/test/report.test.ts b/test/report.test.ts index f4829a5..3c8d7f5 100644 --- a/test/report.test.ts +++ b/test/report.test.ts @@ -8,7 +8,7 @@ describe('reports', () => { it('renders the switch table and keeps a failed fetch on a non-ok row', () => { const table = formatReport([ { - repo: 'admin-panel-auth-service', + repo: 'example-service', before: 'master', after: 'dev', action: 'create-track', @@ -17,7 +17,7 @@ describe('reports', () => { }, ]); assert.match(table, /repo\s+\|\s+before\s+\|\s+after\s+\|\s+action\s+\|\s+result\s+\|\s+error/); - assert.match(table, /admin-panel-auth-service\s+\|\s+master\s+\|\s+dev\s+\|\s+create-track\s+\|\s+ok/); + assert.match(table, /example-service\s+\|\s+master\s+\|\s+dev\s+\|\s+create-track\s+\|\s+ok/); const merged = mergeFetchErrors( [