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
134 changes: 134 additions & 0 deletions .claude/skills/release/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
---
name: release
description: Cut a tagged release of Vortex Studio - bump the version, open the develop-to-master release PR, merge it, tag master, publish the GitHub release, and merge master back into develop. Use whenever the user asks to release, cut a release, ship a version, tag a version, do a major/minor/patch release, or "do the whole release process". Covers where the version number lives and the exact branch, commit-message and tag conventions the repo already follows.
---

# Releasing Vortex Studio

The canonical rules live in `CONTRIBUTING.md` under "Releases". This skill is the
executable version of them plus the details that only show up when you actually do it.

Releases go out from `develop`. `master` is the released branch and is always tagged.

## Pick the number

From `CONTRIBUTING.md`:

- **last number** (1.0.2 → 1.0.3) for fixes only
- **middle number** (1.0.2 → 1.1.0) when there are new features
- **first number** (1.0.2 → 2.0.0) only when the saved map format changes in a way
older files or older builds cannot handle

Read the commits before deciding — do not take the user's word for "minor" if the log
shows a format change:

```bash
git log --oneline "$(git describe --tags --abbrev=0 master)"..develop --no-merges
```

If the range touches the map format — `resources/js/studio/vortexProject.js`, its
round-trip tests in `scripts/project.test.mjs`, or any migration under
`database/migrations` that rewrites saved parts — stop and confirm the major bump with
the user before continuing.

## Where the version lives

`package.json` is the only source of truth. Nothing else hardcodes it:

- `vite.config.js` reads `package.json` at build time into `__APP_VERSION__`
- `resources/js/studio/version.js` re-exports that as `APP_VERSION`, falling back to `'dev'`
- `MenuBar.jsx` shows it in Help → About; `StartScreen.jsx` shows it at the bottom of the start screen

So a release edits exactly two files, and the diff is three lines:

- `package.json` — the `"version"` key
- `package-lock.json` — the `"version"` at the top **and** the one under `packages.""`

**Do not bump `live-editing-server/package.json`.** It has sat at 1.0.0 through every
release so far and is versioned separately. Changing that is a decision for the user,
not a step in this process.

Do not run `npm version` — it makes a tag and a commit that do not match the conventions below.

## The process

Confirm the merge the release is built on actually landed on `develop` before starting.

### 1. Tests, before anything else

```bash
npm test # scripts/*.test.mjs
./vendor/bin/pest # tests/Feature
```

Both must be green. If either fails, stop and report — do not release around a failure.
Keep the counts, they go in the PR body.

### 2. Bump on `develop`

```bash
git checkout develop && git pull --ff-only
# edit package.json + package-lock.json
git add package.json package-lock.json
git commit -m "set the version to X.Y.Z"
git push origin develop
```

Commit message is exactly `set the version to X.Y.Z`, lowercase, no `v`.

### 3. Release PR

```bash
gh pr create --base master --head develop --title "release vX.Y.Z" --body "..."
```

Body: one line on why the middle/last number moved, an **In this release** list written
from the commit log in plain user-facing language, and a checklist of the steps below.

### 4. Merge, tag, publish

```bash
gh pr merge <n> --merge --subject "merge develop into master for vX.Y.Z" --body ""
git checkout master && git pull --ff-only
git tag vX.Y.Z && git push origin vX.Y.Z
```

A **merge commit**, never squash and never rebase — the tag has to sit on the merge
commit, the way `v1.0.2` (`3e5b4a2`) and `v1.1.0` (`ce380ec`) do. Tag name carries the
`v`, the version in `package.json` does not.

Then the GitHub release, matching the voice of the existing ones (`gh release view v1.1.0`):
a one-line summary of what kind of release it is, an **In this release** bullet list
ending with "the version bumped to X.Y.Z", an optional **Thanks** section crediting
outside contributors by handle and PR number, and a compare link last.

```bash
gh release create vX.Y.Z --title "vX.Y.Z" --notes "..."
```

### 5. Merge back — do not skip

```bash
git checkout develop && git pull --ff-only
git merge master --no-ff -m "merge master back into develop after vX.Y.Z"
git push origin develop
```

Without this the branches diverge and the next release PR carries stale history.
See `6051d7c` after v1.0.2 and `af6ffd9` after v1.1.0.

### 6. Verify

```bash
git diff origin/master origin/develop --stat # must be empty
git log --graph --oneline --all -8
gh release list | head -3
```

## Permission prompts

`gh pr merge` and `git checkout master` have been blocked by the auto-mode permission
classifier mid-release. That leaves the release half-finished — bumped and PR'd but not
tagged. If it happens, say plainly which step is blocked and print the remaining commands
rather than probing for a way around it; the user can run them with `!` or add a Bash
permission rule.
4 changes: 3 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ DB_CONNECTION=sqlite
# DB_PASSWORD=

SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_LIFETIME=43200
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
Expand Down Expand Up @@ -67,3 +67,5 @@ VITE_APP_NAME="${APP_NAME}"
VITE_LIVE_URL=ws://localhost:8787

LIVE_SECRET=

GITHUB_REPO=N7z/vortex-studio
65 changes: 65 additions & 0 deletions app/Http/Controllers/AboutController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

namespace App\Http\Controllers;

use App\Support\Cached;
use Illuminate\Support\Facades\Http;

class AboutController extends Controller
{
private const CONTRIBUTOR_TTL = 21600;

public function info()
{
$deployed = self::deployedAt();

return response()->json([
'deployed_at' => $deployed,
'deploy_uptime' => $deployed ? time() - $deployed : null,
]);
}

public function contributors()
{
$repo = config('services.github.repo');

if (! $repo) {
return response()->json(['contributors' => []]);
}

return response()->json([
'contributors' => Cached::remember("about_contributors_$repo", self::CONTRIBUTOR_TTL, function () use ($repo) {
try {
$r = Http::timeout(5)
->withHeaders(['Accept' => 'application/vnd.github+json'])
->get("https://api.github.com/repos/$repo/contributors", ['per_page' => 30]);

if (! $r->successful()) {
return [];
}

return collect($r->json())
->filter(fn ($c) => ($c['type'] ?? '') === 'User')
->map(fn ($c) => [
'name' => $c['login'],
'avatar' => $c['avatar_url'] ?? null,
'url' => $c['html_url'] ?? null,
'commits' => (int) ($c['contributions'] ?? 0),
])
->values()
->all();
} catch (\Throwable $e) {
return [];
}
}),
]);
}

private static function deployedAt(): ?int
{
$manifest = public_path('build/manifest.json');
$at = is_file($manifest) ? @filemtime($manifest) : false;

return $at === false ? null : $at;
}
}
2 changes: 1 addition & 1 deletion app/Http/Controllers/AccountController.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public function register(Request $request)
'password' => $data['password'],
]);

return $this->signIn($request, $user);
return $this->signIn($request, $user, true);
}

public function login(Request $request)
Expand Down
4 changes: 4 additions & 0 deletions config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
'key' => env('RESEND_API_KEY'),
],

'github' => [
'repo' => env('GITHUB_REPO', 'N7z/vortex-studio'),
],

// Shared with live-editing-server, which has no other way to know who a member is.
'live' => [
'secret' => env('LIVE_SECRET'),
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"wasmoon": "^1.16.0"
},
"name": "studio",
"version": "1.1.0",
"version": "1.2.0",
"description": "<p align=\"center\"><a href=\"https://laravel.com\" target=\"_blank\"><img src=\"https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg\" width=\"400\" alt=\"Laravel Logo\"></a></p>",
"main": "vite.config.js",
"directories": {
Expand Down
101 changes: 92 additions & 9 deletions resources/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -160,14 +160,6 @@ a.tool-btn { text-decoration: none; }
}
.help-close:hover { color: var(--ink); }

.about-pop { width: 300px; text-align: center; }
.about-pop .about-version {
margin: 2px 0 10px;
color: var(--dim);
font-variant-numeric: tabular-nums;
}
.about-pop a { color: var(--dim); }

.help-pop::-webkit-scrollbar { width: 8px; }
.help-pop::-webkit-scrollbar-track { background: transparent; }
.help-pop::-webkit-scrollbar-thumb { background: var(--line-mid); border-radius: 4px; }
Expand Down Expand Up @@ -1044,7 +1036,8 @@ a.tool-btn { text-decoration: none; }
.studio.mobile .arch-pop,
.studio.mobile .team-panel,
.studio.mobile .chat-panel,
.studio.mobile .stats-panel {
.studio.mobile .stats-panel,
.studio.mobile .about-panel {
left: 50%;
right: auto;
top: 10px;
Expand All @@ -1053,6 +1046,7 @@ a.tool-btn { text-decoration: none; }
max-height: calc(100% - 20px);
overflow-y: auto;
}
.studio.mobile .about-panel { aspect-ratio: auto; }
.studio.mobile .arch-head { cursor: default; }

.studio.mobile .start { display: flex; flex-direction: column; }
Expand Down Expand Up @@ -1666,6 +1660,95 @@ a.tool-btn { text-decoration: none; }
.stats-panel { width: 214px; top: 46px; right: 10px; }
.stats-panel .team-body { gap: 0; padding: 6px 10px 9px; }

.about-panel {
position: fixed;
top: 50%;
left: 50%;
right: auto;
transform: translate(-50%, -50%);
width: 440px;
max-width: calc(100vw - 32px);
aspect-ratio: 4 / 3;
max-height: calc(100vh - 32px);
display: flex;
flex-direction: column;
z-index: 50;
}
.about-panel .team-body {
flex: 1;
min-height: 0;
overflow-y: auto;
overscroll-behavior: contain;
gap: 0;
padding: 6px 10px 9px;
}
.about-title {
font-size: 15px;
font-weight: 600;
color: var(--ink-hi);
text-align: center;
margin-top: 6px;
}
.about-version {
text-align: center;
color: var(--dim);
font-variant-numeric: tabular-nums;
margin: 2px 0 8px;
}
.about-blurb { color: var(--dim); margin-bottom: 8px; line-height: 1.5; text-align: center; }
.about-blurb a { color: var(--dim); }
.about-subject {
color: var(--dim);
margin-top: 3px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.about-people { display: flex; flex-direction: column; gap: 6px; margin-top: 3px; }
.about-person {
display: flex;
align-items: center;
gap: 8px;
font-size: 11px;
padding: 2px 4px;
margin: 0 -4px;
border-radius: 4px;
text-decoration: none;
}
a.about-person:hover { background: var(--field-alt); }
a.about-person:hover .about-name { color: var(--ink-hi); }
.about-dot {
flex: none;
width: 22px;
height: 22px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 9px;
font-weight: 600;
color: var(--ink-white);
}
.about-name {
flex: 1;
min-width: 0;
color: var(--ink-mid);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.about-bar {
flex: none;
width: 76px;
height: 4px;
border-radius: 2px;
background: var(--line);
overflow: hidden;
}
.about-face { object-fit: cover; border: 1px solid var(--line); }
.about-bar i { display: block; height: 100%; background: var(--accent); }
.about-count { flex: none; color: var(--dim); font-variant-numeric: tabular-nums; }

.stat-group {
font-size: 10px;
font-weight: 600;
Expand Down
Loading