mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-23 05:51:39 +00:00
Add plugin upload workflow and related enhancements
- Introduced a new upload workflow for the Atlas plugin to Roblox using Lune and OpenCloud. - Added `UploadDetails.json` to store plugin metadata. - Updated `build.rs` to include `UploadDetails.json` in the plugin snapshot. - Enhanced `plugin.project.json` to reference the new `UploadDetails`. - Added a new submodule for `opencloud-execute` to facilitate plugin uploads. - Created scripts for publishing the plugin via both PowerShell and Bash. - Updated GitHub Actions workflow to include steps for installing Rokit and uploading the plugin. These changes aim to streamline the plugin release process and improve integration with Roblox's marketplace.
This commit is contained in:
parent
8cf140af3a
commit
4918fb80c7
16 changed files with 752 additions and 19 deletions
161
.cursor/plans/merge_plugin_upload_workflow_0943cf6c.plan.md
Normal file
161
.cursor/plans/merge_plugin_upload_workflow_0943cf6c.plan.md
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
---
|
||||
name: Merge plugin upload workflow
|
||||
overview: Manually apply the upstream commit "Upload plugin as part of release workflow" to the Atlas fork, adapting all file references and metadata to match the fork's diverged codebase.
|
||||
todos:
|
||||
- id: new-files
|
||||
content: Create .lune/.config.luau, .lune/scripts/plugin-upload.luau, .lune/upload-plugin.luau (adapted for atlas), plugin/UploadDetails.json (with Atlas values)
|
||||
status: completed
|
||||
- id: submodule
|
||||
content: Add opencloud-execute submodule at .lune/opencloud-execute
|
||||
status: completed
|
||||
- id: build-rs
|
||||
content: Add UploadDetails.json to plugin snapshot in build.rs
|
||||
status: completed
|
||||
- id: project-json
|
||||
content: Add UploadDetails node to plugin.project.json
|
||||
status: completed
|
||||
- id: rokit
|
||||
content: Add lune tool to rokit.toml
|
||||
status: completed
|
||||
- id: release-yml
|
||||
content: Add Roblox upload step to release.yml build-plugin job
|
||||
status: completed
|
||||
- id: plugin-rs
|
||||
content: Apply trivial test change in src/cli/plugin.rs
|
||||
status: completed
|
||||
- id: local-scripts
|
||||
content: Replace scripts/publish-plugin.ps1 and add scripts/publish-plugin.sh to use lune-based upload
|
||||
status: completed
|
||||
isProject: false
|
||||
---
|
||||
|
||||
# Merge Plugin Upload Release Workflow
|
||||
|
||||
Cherry-picking will produce conflicts in 6+ files due to heavy divergence. Manual application is the cleanest path.
|
||||
|
||||
## Upstream bugs to fix
|
||||
|
||||
**Swapped env vars in `release.yml`:** The upstream commit has `RBX_UNIVERSE_ID` mapped to `vars.PLUGIN_CI_PLACE_ID` and `RBX_PLACE_ID` mapped to `vars.PLUGIN_CI_UNIVERSE_ID` -- clearly crossed. We will use the correct mapping.
|
||||
|
||||
## Local scripts replacement
|
||||
|
||||
`scripts/publish-plugin.ps1` currently uses `cargo run -- upload`. Replace it (and add missing `.sh`) to use the same Lune-based upload pipeline locally. The `.lune/upload-plugin.luau` script already handles the local case (builds the plugin via `atlas build` when `CI != "true"`), so the wrapper scripts just load `.env` and run `lune run upload-plugin Atlas.rbxm`.
|
||||
|
||||
`.env` uses the same names as GitHub Actions secrets/vars:
|
||||
|
||||
```
|
||||
PLUGIN_UPLOAD_TOKEN=your-opencloud-api-key
|
||||
PLUGIN_CI_UNIVERSE_ID=your-universe-id
|
||||
PLUGIN_CI_PLACE_ID=your-place-id
|
||||
```
|
||||
|
||||
The local scripts map these to `RBX_API_KEY`/`RBX_UNIVERSE_ID`/`RBX_PLACE_ID` before calling lune, matching what the CI workflow `env:` block does.
|
||||
|
||||
## What the commit adds
|
||||
|
||||
Automated plugin upload to Roblox as part of the GitHub release workflow, using [Lune](https://lune-org.github.io/docs) + OpenCloud Luau Execute to publish the built `.rbxm` to Roblox's plugin marketplace.
|
||||
|
||||
## New files (create as-is, with Atlas adaptations)
|
||||
|
||||
### `.lune/.config.luau`
|
||||
|
||||
Lune config with strict mode and typedefs alias. Copy verbatim from upstream.
|
||||
|
||||
### `.lune/scripts/plugin-upload.luau`
|
||||
|
||||
Roblox engine script that deserializes the plugin `.rbxm`, reads `UploadDetails`, and calls `AssetService:CreateAssetVersionAsync`. Copy verbatim -- it's generic (reads values from UploadDetails at runtime).
|
||||
|
||||
### `.lune/upload-plugin.luau`
|
||||
|
||||
Lune entry point that reads the `.rbxm`, sends it to OpenCloud for execution. One adaptation needed: change `process.exec("rojo", ...)` to `process.exec("atlas", ...)` on the local-testing path (line 25 in upstream).
|
||||
|
||||
### `plugin/UploadDetails.json`
|
||||
|
||||
Atlas-specific values per user input:
|
||||
|
||||
```json
|
||||
{
|
||||
"assetId": 111151139098227,
|
||||
"name": "Atlas",
|
||||
"description": "The plugin portion of Atlas, a tool to enable professional tooling for Roblox developers.",
|
||||
"creatorId": 422368266,
|
||||
"creatorType": "Group"
|
||||
}
|
||||
```
|
||||
|
||||
## Submodule addition
|
||||
|
||||
### `.lune/opencloud-execute`
|
||||
|
||||
Add git submodule: `https://github.com/Dekkonot/opencloud-luau-execute-lune.git` at `.lune/opencloud-execute`. This also adds an entry to `[.gitmodules](.gitmodules)`.
|
||||
|
||||
## Existing file modifications
|
||||
|
||||
### `[.github/workflows/release.yml](.github/workflows/release.yml)`
|
||||
|
||||
Two additions to the `build-plugin` job:
|
||||
|
||||
1. **Add Rokit install step** (after "Install Rust", before "Build Atlas") so `lune` is on PATH:
|
||||
|
||||
```yaml
|
||||
- name: Install Rokit
|
||||
uses: CompeyDev/setup-rokit@v0.1.2
|
||||
```
|
||||
|
||||
1. **Add upload step** (after "Upload Plugin to Artifacts" at line 47) with **corrected** env var mapping (upstream had these swapped):
|
||||
|
||||
```yaml
|
||||
- name: Upload Plugin to Roblox
|
||||
env:
|
||||
RBX_API_KEY: ${{ secrets.PLUGIN_UPLOAD_TOKEN }}
|
||||
RBX_UNIVERSE_ID: ${{ vars.PLUGIN_CI_UNIVERSE_ID }}
|
||||
RBX_PLACE_ID: ${{ vars.PLUGIN_CI_PLACE_ID }}
|
||||
run: lune run upload-plugin Atlas.rbxm
|
||||
```
|
||||
|
||||
### `[build.rs](build.rs)`
|
||||
|
||||
Add `"UploadDetails.json"` entry to the plugin snapshot hashmap, after `"Version.txt"` on line 98:
|
||||
|
||||
```rust
|
||||
"UploadDetails.json" => snapshot_from_fs_path(&plugin_dir.join("UploadDetails.json"))?,
|
||||
```
|
||||
|
||||
### `[plugin.project.json](plugin.project.json)`
|
||||
|
||||
Add `UploadDetails` child node after `Version` (line 25):
|
||||
|
||||
```json
|
||||
"UploadDetails": {
|
||||
"$path": "plugin/UploadDetails.json"
|
||||
}
|
||||
```
|
||||
|
||||
### `[rokit.toml](rokit.toml)`
|
||||
|
||||
Add lune tool (latest version) after the existing entries:
|
||||
|
||||
```toml
|
||||
lune = "lune-org/lune@0.10.4"
|
||||
```
|
||||
|
||||
### `[src/cli/plugin.rs](src/cli/plugin.rs)`
|
||||
|
||||
Trivial test change on line 101: `assert!(initialize_plugin().is_ok())` becomes `let _ = initialize_plugin().unwrap();`
|
||||
|
||||
### `[scripts/publish-plugin.ps1](scripts/publish-plugin.ps1)`
|
||||
|
||||
Replace entirely. New version: load `.env`, export `RBX_API_KEY`/`RBX_UNIVERSE_ID`/`RBX_PLACE_ID`, run `lune run upload-plugin Atlas.rbxm`.
|
||||
|
||||
### `scripts/publish-plugin.sh` (new)
|
||||
|
||||
Bash equivalent: source `.env`, export vars, run `lune run upload-plugin Atlas.rbxm`.
|
||||
|
||||
## Required GitHub secrets/vars
|
||||
|
||||
After merging, the repo needs these configured in GitHub settings:
|
||||
|
||||
- **Secret:** `PLUGIN_UPLOAD_TOKEN` -- Roblox OpenCloud API key
|
||||
- **Variable:** `PLUGIN_CI_UNIVERSE_ID` -- Universe ID for the place used by OpenCloud Execute
|
||||
- **Variable:** `PLUGIN_CI_PLACE_ID` -- Place ID within that universe
|
||||
|
||||
390
.cursor/plans/rokit_install_audit_fixes_e465275a.plan.md
Normal file
390
.cursor/plans/rokit_install_audit_fixes_e465275a.plan.md
Normal file
|
|
@ -0,0 +1,390 @@
|
|||
---
|
||||
name: Rokit Install Audit Fixes
|
||||
overview: Fix the EPERM crash when deleting a globally installed atlas.exe, and harden the entire rokit installation pipeline in the VS Code extension for reliability across Windows, macOS, and Linux.
|
||||
todos:
|
||||
- id: bug1-elevated-delete
|
||||
content: Add elevatedDelete() helper and fix the 'Switch to Rokit' flow in openMenu.ts to handle EPERM/EACCES/EBUSY with platform-specific elevation
|
||||
status: completed
|
||||
- id: bug2-progress
|
||||
content: Wrap installRojo() in vscode.window.withProgress() with stage reporting
|
||||
status: completed
|
||||
- id: bug3-github-api
|
||||
content: Replace eryn.io proxy URL with official GitHub API URL in installRojo.ts
|
||||
status: completed
|
||||
- id: bug4-temp-cleanup
|
||||
content: Delete temp rokit binary after successful self-install
|
||||
status: completed
|
||||
- id: bug5-path-dedup
|
||||
content: Deduplicate PATH entry and verify rokit accessibility before rokit add
|
||||
status: completed
|
||||
- id: bug6-stream
|
||||
content: Replace manual stream piping with stream.promises.pipeline()
|
||||
status: completed
|
||||
- id: bug7-rokit-add-existing
|
||||
content: Use 'rokit add --global --force' + 'rokit trust' + verify atlas --version + non-fatal plugin install
|
||||
status: completed
|
||||
- id: feat-check-updates
|
||||
content: Add 'Check for Updates' menu item in openMenu.ts for rokit-managed installs, handle project-level + global rokit update, reinstall Studio plugin after update
|
||||
status: completed
|
||||
- id: feat-auto-update-check
|
||||
content: Add background update check on activation using rokit update --check (once per day, project + global), prompt user with Update button that triggers rokit update + plugin reinstall
|
||||
status: completed
|
||||
isProject: false
|
||||
---
|
||||
|
||||
# Rokit Installation Audit and Fixes
|
||||
|
||||
## Audit Findings
|
||||
|
||||
### How Rokit Installation Works (No Elevation Needed)
|
||||
|
||||
Rokit installs entirely to `~/.rokit/` (user-level directory). `rokit self-install` modifies user-level PATH environment variables (HKCU on Windows, shell rc files on Unix). **Neither rokit installation nor `rokit add` requires elevation.**
|
||||
|
||||
The only scenario requiring elevation is the "Switch to Rokit" migration flow, which tries to delete an existing globally installed `atlas.exe` from a protected directory like `C:\Program Files\Atlas` (written by [install.ps1](install.ps1)) or `/usr/local/bin/atlas` (written by [install.sh](install.sh)).
|
||||
|
||||
### Issues Found (by severity)
|
||||
|
||||
---
|
||||
|
||||
**BUG 1 -- CRITICAL: `fs.unlink` fails without elevation, crashes the entire flow**
|
||||
|
||||
In [vscode-rojo/src/commands/openMenu.ts](vscode-rojo/src/commands/openMenu.ts) line 133:
|
||||
|
||||
```typescript
|
||||
return fs.unlink(install.resolvedPath)
|
||||
```
|
||||
|
||||
When atlas.exe is in `C:\Program Files\Atlas` or `/usr/local/bin/`, this throws EPERM/EACCES. The `.then()` error handler on line 139 catches it but just shows a generic message and the flow dies. Additionally, on Windows the exe may be **locked by a running process** (atlas serve), causing EBUSY even with elevation.
|
||||
|
||||
**Fix:** Add an `elevatedDelete()` helper that:
|
||||
|
||||
1. Tries `fs.unlink()` first
|
||||
2. On EPERM/EACCES, uses platform-specific elevated delete:
|
||||
- **Windows:** `powershell -Command "Start-Process powershell -ArgumentList '-Command Remove-Item \"path\"' -Verb RunAs -Wait"` (triggers UAC prompt)
|
||||
- **macOS:** `osascript -e 'do shell script "rm path" with administrator privileges'` (triggers password dialog)
|
||||
- **Linux:** `pkexec rm path` (triggers PolicyKit dialog)
|
||||
3. On EBUSY (Windows exe locked), shows a specific error message telling the user to stop atlas serve first
|
||||
|
||||
---
|
||||
|
||||
**BUG 2 -- HIGH: No progress indicator during installation**
|
||||
|
||||
In [vscode-rojo/src/installRojo.ts](vscode-rojo/src/installRojo.ts) `installRojo()`:
|
||||
|
||||
The entire flow (download rokit zip ~3.5MB, extract, self-install, `rokit add` atlas) runs silently. Users see zero feedback and may think the extension froze.
|
||||
|
||||
**Fix:** Wrap the body of `installRojo()` in `vscode.window.withProgress()` with `ProgressLocation.Notification` and `cancellable: false`. Report progress at each stage:
|
||||
|
||||
- "Downloading Rokit..."
|
||||
- "Installing Rokit..."
|
||||
- "Installing Atlas via Rokit..."
|
||||
|
||||
---
|
||||
|
||||
**BUG 3 -- HIGH: Third-party release URL is a single point of failure**
|
||||
|
||||
In [vscode-rojo/src/installRojo.ts](vscode-rojo/src/installRojo.ts) line 113:
|
||||
|
||||
```typescript
|
||||
const latestReleaseResponse = await fetch(
|
||||
"https://latest-github-release.eryn.io/rojo-rbx/rokit",
|
||||
)
|
||||
```
|
||||
|
||||
This third-party proxy (`eryn.io`) is not under our control. If it goes down, installation breaks. The official rokit install scripts use the GitHub API directly.
|
||||
|
||||
**Fix:** Replace with the official GitHub API URL: `https://api.github.com/repos/rojo-rbx/rokit/releases/latest`. Verified: response schema is identical, both return the same `GitHubRelease` shape. Add an `Accept: application/vnd.github+json` header per GitHub API best practice.
|
||||
|
||||
---
|
||||
|
||||
**BUG 4 -- MEDIUM: Temp files never cleaned up**
|
||||
|
||||
In [vscode-rojo/src/installRojo.ts](vscode-rojo/src/installRojo.ts) line 143-170:
|
||||
|
||||
The rokit binary is downloaded to `os.tmpdir()` but never deleted after `rokit self-install` succeeds.
|
||||
|
||||
**Fix:** Add `fs.promises.unlink(tempPath).catch(() => {})` after the `exec(self-install)` call succeeds.
|
||||
|
||||
---
|
||||
|
||||
**BUG 5 -- MEDIUM: PATH update is fragile**
|
||||
|
||||
In [vscode-rojo/src/installRojo.ts](vscode-rojo/src/installRojo.ts) lines 177-181:
|
||||
|
||||
```typescript
|
||||
if ("PATH" in process.env) {
|
||||
const envPath = process.env.PATH!.split(path.delimiter)
|
||||
envPath.push(path.join(os.homedir(), ".rokit", "bin"))
|
||||
process.env.PATH = envPath.join(path.delimiter)
|
||||
}
|
||||
```
|
||||
|
||||
This only updates the extension's own process environment. But the subsequent `rokit add --global` on line 184 runs via `exec()` which inherits this env, so it should work. The concern is:
|
||||
|
||||
- It doesn't check if the path is already present (could add duplicates on repeated attempts)
|
||||
- The rokit binary path might differ on some systems
|
||||
|
||||
**Fix:** Check for duplicates before appending. Also verify the rokit binary is actually accessible after the PATH update by doing a quick `which("rokit")` check before proceeding to `rokit add`.
|
||||
|
||||
---
|
||||
|
||||
**BUG 6 -- LOW: Stream error handling is fragile**
|
||||
|
||||
In [vscode-rojo/src/installRojo.ts](vscode-rojo/src/installRojo.ts) lines 51-60, 149-163:
|
||||
|
||||
`promisifyStream` resolves on whichever of "close"/"finish"/"end" fires first. The pipeline/pipe chain has multiple potential failure modes that aren't properly propagated.
|
||||
|
||||
**Fix:** Use `stream.promises.pipeline()` (available in Node 15+, which VS Code ships with) instead of manually piping and tracking events. This gives proper error propagation and backpressure handling. Alternatively, simplify to `await promisify(stream.pipeline)(download.body!, unzipper.ParseOne(), fs.createWriteStream(tempPath))`.
|
||||
|
||||
---
|
||||
|
||||
**BUG 7 -- HIGH: `rokit add --global` crashes if atlas is already configured**
|
||||
|
||||
In [vscode-rojo/src/installRojo.ts](vscode-rojo/src/installRojo.ts) line 184:
|
||||
|
||||
```typescript
|
||||
await exec("rokit add --global UserGeneratedLLC/rojo atlas")
|
||||
```
|
||||
|
||||
This is the **only** action after rokit is confirmed installed. It fails if atlas is already in the project's `rokit.toml` or already added globally. The scenario:
|
||||
|
||||
1. Project has `rokit.toml` with atlas listed but `rokit install` hasn't been run yet
|
||||
2. `which("atlas")` finds the rokit shim at `~/.rokit/bin/atlas`
|
||||
3. `atlas --version` fails (rokit says tool not installed), stderr contains "rokit" -> `getRojoInstall` returns null
|
||||
4. User sees "Install Atlas now", clicks it
|
||||
5. `isRokitInstalled()` returns true (skips rokit download)
|
||||
6. `rokit add --global` fails because atlas is already configured -> **onboarding dies**
|
||||
|
||||
**Fix:** Use `--force` to handle "already exists", trust the tool, then verify and install the plugin:
|
||||
|
||||
```typescript
|
||||
await exec("rokit add --global --force UserGeneratedLLC/rojo atlas")
|
||||
await exec("rokit trust UserGeneratedLLC/rojo")
|
||||
await exec("atlas --version")
|
||||
|
||||
// Install the Studio plugin (non-fatal -- don't crash onboarding if Studio isn't installed)
|
||||
try {
|
||||
await exec("atlas plugin install")
|
||||
} catch (e: any) {
|
||||
vscode.window.showWarningMessage(
|
||||
`Atlas installed successfully, but the Studio plugin could not be installed: ${e.stderr || e}. ` +
|
||||
`You can install it later from the Atlas menu or by running "atlas plugin install".`
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
- `--force` on `rokit add` handles the "already exists" case cleanly -- no try/catch needed, it re-adds/updates regardless
|
||||
- `rokit trust` marks `UserGeneratedLLC/rojo` as trusted so rokit won't prompt the user for confirmation when running atlas (without trust, `rokit install` and first-run shims can block waiting for user input)
|
||||
- `atlas --version` verifies the tool is actually usable after add
|
||||
- `atlas plugin install` writes the embedded Studio plugin to disk (non-fatal if Studio isn't installed yet)
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
**FEATURE: "Check for Updates" menu item for Rokit-managed installs**
|
||||
|
||||
In [vscode-rojo/src/commands/openMenu.ts](vscode-rojo/src/commands/openMenu.ts), the `generateProjectMenu()` return array (line 315) has action items like Studio, Syncback, Sourcemap, Install Plugin. There is no way to update Atlas from the extension.
|
||||
|
||||
`generateProjectMenu()` already tracks `installType` (line 168) and whether it's `InstallType.rokit`. We use this to conditionally show the item.
|
||||
|
||||
**Implementation:**
|
||||
|
||||
1. Add a menu item in `generateProjectMenu()` return array, conditionally included when `installType === InstallType.rokit`:
|
||||
|
||||
```typescript
|
||||
...(installType === InstallType.rokit
|
||||
? [
|
||||
{
|
||||
label: "$(sync) Check for Updates",
|
||||
description: `Atlas v${allRojoVersions[0]}`,
|
||||
action: "checkUpdates",
|
||||
projectFile: projectFiles[0],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
```
|
||||
|
||||
1. Add `"checkUpdates"` case in the `onDidAccept` handler (after the `"install"` case). The handler needs to support both project-level `rokit.toml` and global installations:
|
||||
|
||||
```typescript
|
||||
case "checkUpdates": {
|
||||
if (!selectedItem.projectFile) return
|
||||
const updateFolder = path.dirname(selectedItem.projectFile.path.fsPath)
|
||||
|
||||
input.hide()
|
||||
|
||||
await vscode.window.withProgress(
|
||||
{ location: vscode.ProgressLocation.Notification, title: "Checking for Atlas updates..." },
|
||||
async (progress) => {
|
||||
// Try project-level update first, fall back to global
|
||||
let updateOutput: string
|
||||
try {
|
||||
const result = await exec("rokit update atlas", { cwd: updateFolder })
|
||||
updateOutput = result.stdout || result.stderr
|
||||
} catch {
|
||||
try {
|
||||
const result = await exec("rokit update --global atlas")
|
||||
updateOutput = result.stdout || result.stderr
|
||||
} catch (e: any) {
|
||||
vscode.window.showErrorMessage(`Could not update Atlas: ${e.stderr || e}`)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Re-install the Studio plugin so it matches the new binary.
|
||||
// The plugin is embedded in the atlas binary at compile time
|
||||
// (include_bytes! in src/cli/plugin.rs), so updating the binary
|
||||
// without re-installing leaves a stale AtlasManagedPlugin.rbxm
|
||||
// in Studio's plugins folder. Mismatched versions break live sync
|
||||
// due to protocol version enforcement.
|
||||
progress.report({ message: "Reinstalling Studio plugin..." })
|
||||
try {
|
||||
await exec("atlas plugin install")
|
||||
} catch (e: any) {
|
||||
vscode.window.showWarningMessage(
|
||||
`Atlas updated, but plugin reinstall failed: ${e.stderr || e}. ` +
|
||||
`Run "atlas plugin install" manually to avoid version mismatch.`
|
||||
)
|
||||
}
|
||||
|
||||
vscode.window.showInformationMessage(updateOutput.trim() || "Atlas is up to date.")
|
||||
},
|
||||
)
|
||||
break
|
||||
}
|
||||
```
|
||||
|
||||
The strategy: try `rokit update atlas` in the project directory first. If atlas isn't in the project's `rokit.toml`, this fails and we fall back to `rokit update --global atlas`. Rokit's output tells the user whether an update was applied or they're already current.
|
||||
|
||||
After a successful update, immediately runs `atlas plugin install` to overwrite the stale `AtlasManagedPlugin.rbxm` in Studio's plugins folder. The plugin is baked into the atlas binary via `include_bytes!` at compile time (`src/cli/plugin.rs:12`), so the `.rbxm` in Studio never auto-updates -- it must be explicitly reinstalled. If the plugin reinstall fails (e.g. Studio not installed), a warning is shown with manual instructions rather than failing the whole update.
|
||||
|
||||
---
|
||||
|
||||
**FEATURE: Background update check on extension activation**
|
||||
|
||||
In [vscode-rojo/src/extension.ts](vscode-rojo/src/extension.ts) `activate()`:
|
||||
|
||||
Rokit has `rokit update atlas --check` which checks for updates without installing. Supports both project-level (`rokit update atlas --check` in the project dir, reads `rokit.toml`) and global (`rokit update --global atlas --check`).
|
||||
|
||||
**Implementation:** Add a new file `vscode-rojo/src/checkForUpdates.ts` with a `checkForAtlasUpdates(context)` function, called fire-and-forget from `activate()`:
|
||||
|
||||
```typescript
|
||||
export async function checkForAtlasUpdates(context: vscode.ExtensionContext) {
|
||||
// Throttle: once per day
|
||||
const lastCheck = context.globalState.get<number>("atlas::lastUpdateCheck")
|
||||
const ONE_DAY = 24 * 60 * 60 * 1000
|
||||
if (lastCheck && Date.now() - lastCheck < ONE_DAY) return
|
||||
|
||||
// Check if rokit is available
|
||||
const rokitPath = await which("rokit").catch(() => null)
|
||||
if (!rokitPath) return
|
||||
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
|
||||
|
||||
// rokit update --check prints update info if available, or "up to date" if current.
|
||||
// Check both project-level and global.
|
||||
let checkOutput: string | null = null
|
||||
let updateScope: "project" | "global" = "global"
|
||||
|
||||
// Try project-level first (if workspace is open)
|
||||
if (workspaceFolder) {
|
||||
try {
|
||||
const result = await exec("rokit update atlas --check", { cwd: workspaceFolder })
|
||||
const output = (result.stdout + result.stderr).trim()
|
||||
if (output && !output.toLowerCase().includes("up to date")) {
|
||||
checkOutput = output
|
||||
updateScope = "project"
|
||||
}
|
||||
} catch {
|
||||
// atlas not in project rokit.toml -- fall through
|
||||
}
|
||||
}
|
||||
|
||||
// Try global if no project-level update found
|
||||
if (!checkOutput) {
|
||||
try {
|
||||
const result = await exec("rokit update --global atlas --check")
|
||||
const output = (result.stdout + result.stderr).trim()
|
||||
if (output && !output.toLowerCase().includes("up to date")) {
|
||||
checkOutput = output
|
||||
updateScope = "global"
|
||||
}
|
||||
} catch {
|
||||
// atlas not installed globally via rokit
|
||||
}
|
||||
}
|
||||
|
||||
context.globalState.update("atlas::lastUpdateCheck", Date.now())
|
||||
|
||||
if (!checkOutput) return
|
||||
|
||||
const choice = await vscode.window.showInformationMessage(
|
||||
`An Atlas update is available. ${checkOutput}`,
|
||||
"Update",
|
||||
"Dismiss",
|
||||
)
|
||||
|
||||
if (choice !== "Update") return
|
||||
|
||||
await vscode.window.withProgress(
|
||||
{ location: vscode.ProgressLocation.Notification, title: "Updating Atlas..." },
|
||||
async (progress) => {
|
||||
try {
|
||||
if (updateScope === "project" && workspaceFolder) {
|
||||
await exec("rokit update atlas", { cwd: workspaceFolder })
|
||||
} else {
|
||||
await exec("rokit update --global atlas")
|
||||
}
|
||||
} catch (e: any) {
|
||||
vscode.window.showErrorMessage(`Could not update Atlas: ${e.stderr || e}`)
|
||||
return
|
||||
}
|
||||
|
||||
progress.report({ message: "Reinstalling Studio plugin..." })
|
||||
try {
|
||||
await exec("atlas plugin install")
|
||||
} catch {
|
||||
vscode.window.showWarningMessage(
|
||||
"Atlas updated, but Studio plugin reinstall failed. Run 'atlas plugin install' manually.",
|
||||
)
|
||||
}
|
||||
|
||||
vscode.window.showInformationMessage("Atlas updated successfully.")
|
||||
},
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Called from `activate()` as fire-and-forget (no `await`) so it never blocks extension startup:
|
||||
|
||||
```typescript
|
||||
// In activate(), after all synchronous setup is done:
|
||||
checkForAtlasUpdates(context).catch(() => {})
|
||||
```
|
||||
|
||||
Key design decisions:
|
||||
|
||||
- **Uses `rokit update --check`**: Lets rokit handle version resolution natively -- no GitHub API calls, no manual semver parsing
|
||||
- **Project-level first, then global**: Checks the project's `rokit.toml` first (if workspace is open), falls back to global. When updating, uses the same scope that found the update
|
||||
- **Throttle**: Once per day via `globalState`, so frequent restarts don't spam the user
|
||||
- **Silent failures**: Every step is wrapped in try/catch that returns early. Missing rokit, no atlas configured, network errors -- all silently ignored
|
||||
- **Non-blocking**: Called without `await` so extension activation is never delayed
|
||||
|
||||
---
|
||||
|
||||
### Summary of What's Working Correctly
|
||||
|
||||
- Asset name regex (`-(?<platform>\w+)-(?<arch>\w+)\.zip$`) correctly matches all 6 rokit 1.2.0 release assets (linux/macos/windows x x86_64/aarch64, all `.zip`)
|
||||
- Platform/arch mapping covers all common Node.js platforms (win32, darwin, linux) and architectures (x64, arm64)
|
||||
- `rokit self-install` correctly sets up `~/.rokit/bin` in user PATH on all platforms
|
||||
- `chmod 0o755` is correctly applied on Unix after extraction
|
||||
- `getRojoInstall` correctly detects rokit-managed vs globally installed atlas
|
||||
|
||||
## Files to Change
|
||||
|
||||
- [vscode-rojo/src/installRojo.ts](vscode-rojo/src/installRojo.ts) -- Bugs 2, 3, 4, 5, 6, 7
|
||||
- [vscode-rojo/src/commands/openMenu.ts](vscode-rojo/src/commands/openMenu.ts) -- Bug 1, Check for Updates menu item
|
||||
- [vscode-rojo/src/checkForUpdates.ts](vscode-rojo/src/checkForUpdates.ts) -- NEW: background update check logic
|
||||
- [vscode-rojo/src/extension.ts](vscode-rojo/src/extension.ts) -- Call checkForAtlasUpdates() on activation
|
||||
|
||||
10
.github/workflows/release.yml
vendored
10
.github/workflows/release.yml
vendored
|
|
@ -28,6 +28,9 @@ jobs:
|
|||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Install Rokit
|
||||
uses: CompeyDev/setup-rokit@v0.1.2
|
||||
|
||||
- name: Build Atlas
|
||||
run: cargo build --release --locked
|
||||
|
||||
|
|
@ -46,6 +49,13 @@ jobs:
|
|||
name: Atlas.rbxm
|
||||
path: Atlas.rbxm
|
||||
|
||||
- name: Upload Plugin to Roblox
|
||||
env:
|
||||
RBX_API_KEY: ${{ secrets.PLUGIN_UPLOAD_TOKEN }}
|
||||
RBX_UNIVERSE_ID: ${{ vars.PLUGIN_CI_UNIVERSE_ID }}
|
||||
RBX_PLACE_ID: ${{ vars.PLUGIN_CI_PLACE_ID }}
|
||||
run: lune run upload-plugin Atlas.rbxm
|
||||
|
||||
build-vscode:
|
||||
needs: ["create-release"]
|
||||
name: Build VS Code Extension
|
||||
|
|
|
|||
3
.gitmodules
vendored
3
.gitmodules
vendored
|
|
@ -25,3 +25,6 @@
|
|||
[submodule "vscode-rojo"]
|
||||
path = vscode-rojo
|
||||
url = https://github.com/UserGeneratedLLC/vscode-rojo.git
|
||||
[submodule ".lune/opencloud-execute"]
|
||||
path = .lune/opencloud-execute
|
||||
url = https://github.com/Dekkonot/opencloud-luau-execute-lune.git
|
||||
|
|
|
|||
8
.lune/.config.luau
Normal file
8
.lune/.config.luau
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
return {
|
||||
luau = {
|
||||
languagemode = "strict",
|
||||
aliases = {
|
||||
lune = "~/.lune/.typedefs/0.10.4/",
|
||||
},
|
||||
},
|
||||
}
|
||||
1
.lune/opencloud-execute
Submodule
1
.lune/opencloud-execute
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 8ae86dd3ad35eb66b0a4edda7e8771a5986cd0c6
|
||||
51
.lune/scripts/plugin-upload.luau
Normal file
51
.lune/scripts/plugin-upload.luau
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
local args: any = ...
|
||||
assert(args, "no arguments passed to script")
|
||||
|
||||
local input: buffer = args.BinaryInput
|
||||
|
||||
local AssetService = game:GetService("AssetService")
|
||||
local SerializationService = game:GetService("SerializationService")
|
||||
local EncodingService = game:GetService("EncodingService")
|
||||
|
||||
local input_hash: buffer = EncodingService:ComputeBufferHash(input, Enum.HashAlgorithm.Sha256)
|
||||
local hex_hash: { string } = table.create(buffer.len(input_hash))
|
||||
for i = 0, buffer.len(input_hash) - 1 do
|
||||
table.insert(hex_hash, string.format("%02x", buffer.readu8(input_hash, i)))
|
||||
end
|
||||
|
||||
print(`Deserializing plugin file (size: {buffer.len(input)} bytes, hash: {table.concat(hex_hash, "")})`)
|
||||
local plugin = SerializationService:DeserializeInstancesAsync(input)[1]
|
||||
|
||||
local UploadDetails = require(plugin.UploadDetails) :: any
|
||||
local PLUGIN_ID = UploadDetails.assetId
|
||||
local PLUGIN_NAME = UploadDetails.name
|
||||
local PLUGIN_DESCRIPTION = UploadDetails.description
|
||||
local PLUGIN_CREATOR_ID = UploadDetails.creatorId
|
||||
local PLUGIN_CREATOR_TYPE = UploadDetails.creatorType
|
||||
|
||||
assert(typeof(PLUGIN_ID) == "number", "UploadDetails did not contain a number field 'assetId'")
|
||||
assert(typeof(PLUGIN_NAME) == "string", "UploadDetails did not contain a string field 'name'")
|
||||
assert(typeof(PLUGIN_DESCRIPTION) == "string", "UploadDetails did not contain a string field 'description'")
|
||||
assert(typeof(PLUGIN_CREATOR_ID) == "number", "UploadDetails did not contain a number field 'creatorId'")
|
||||
assert(typeof(PLUGIN_CREATOR_TYPE) == "string", "UploadDetails did not contain a string field 'creatorType'")
|
||||
assert(
|
||||
Enum.AssetCreatorType:FromName(PLUGIN_CREATOR_TYPE) ~= nil,
|
||||
"UploadDetails field 'creatorType' was not a valid member of Enum.AssetCreatorType"
|
||||
)
|
||||
|
||||
print(`Uploading to {PLUGIN_ID}`)
|
||||
print(`Plugin Name: {PLUGIN_NAME}`)
|
||||
print(`Plugin Description: {PLUGIN_DESCRIPTION}`)
|
||||
|
||||
local result, version_or_err = AssetService:CreateAssetVersionAsync(plugin, Enum.AssetType.Plugin, PLUGIN_ID, {
|
||||
["Name"] = PLUGIN_NAME,
|
||||
["Description"] = PLUGIN_DESCRIPTION,
|
||||
["CreatorId"] = PLUGIN_CREATOR_ID,
|
||||
["CreatorType"] = Enum.AssetCreatorType:FromName(PLUGIN_CREATOR_TYPE),
|
||||
})
|
||||
|
||||
if result ~= Enum.CreateAssetResult.Success then
|
||||
error(`Plugin failed to upload because: {result.Name} - {version_or_err}`)
|
||||
end
|
||||
|
||||
print(`Plugin uploaded successfully. New version is {version_or_err}.`)
|
||||
77
.lune/upload-plugin.luau
Normal file
77
.lune/upload-plugin.luau
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
local fs = require("@lune/fs")
|
||||
local process = require("@lune/process")
|
||||
local stdio = require("@lune/stdio")
|
||||
|
||||
local luau_execute = require("./opencloud-execute")
|
||||
|
||||
local UNIVERSE_ID = process.env["RBX_UNIVERSE_ID"]
|
||||
local PLACE_ID = process.env["RBX_PLACE_ID"]
|
||||
|
||||
local version_string = fs.readFile("plugin/Version.txt")
|
||||
local versions = { string.match(version_string, "^v?(%d+)%.(%d+)%.(%d+)(.*)$") }
|
||||
if versions[4] ~= "" then
|
||||
print("This release is a pre-release. Skipping uploading plugin.")
|
||||
process.exit(0)
|
||||
end
|
||||
|
||||
local plugin_path = process.args[1]
|
||||
assert(
|
||||
typeof(plugin_path) == "string",
|
||||
"no plugin path provided, expected usage is `lune run upload-plugin [PATH TO RBXM]`."
|
||||
)
|
||||
|
||||
if process.env["CI"] ~= "true" then
|
||||
local atlas = process.exec("atlas", { "build", "plugin.project.json", "--output", plugin_path })
|
||||
if not atlas.ok then
|
||||
stdio.ewrite("plugin upload failed because: could not build plugin.rbxm\n\n")
|
||||
stdio.ewrite(atlas.stderr)
|
||||
stdio.ewrite("\n")
|
||||
process.exit(1)
|
||||
end
|
||||
else
|
||||
assert(fs.isFile(plugin_path), `Plugin file did not exist at {plugin_path}`)
|
||||
end
|
||||
local plugin_content = fs.readFile(plugin_path)
|
||||
|
||||
local engine_script = fs.readFile(".lune/scripts/plugin-upload.luau")
|
||||
|
||||
print("Creating task to upload plugin")
|
||||
local task = luau_execute.create_task_latest(UNIVERSE_ID, PLACE_ID, engine_script, 300, false, plugin_content)
|
||||
|
||||
print("Waiting for task to finish")
|
||||
local success = luau_execute.await_finish(task)
|
||||
if not success then
|
||||
local error = luau_execute.get_error(task)
|
||||
assert(error, "could not fetch error from task")
|
||||
stdio.ewrite("plugin upload failed because: task did not finish successfully\n\n")
|
||||
stdio.ewrite(error.code)
|
||||
stdio.ewrite("\n")
|
||||
stdio.ewrite(error.message)
|
||||
stdio.ewrite("\n")
|
||||
process.exit(1)
|
||||
end
|
||||
|
||||
print("Output from task:\n")
|
||||
for _, log in luau_execute.get_structured_logs(task) do
|
||||
if log.messageType == "ERROR" then
|
||||
stdio.write(stdio.color("red"))
|
||||
stdio.write(log.message)
|
||||
stdio.write("\n")
|
||||
stdio.write(stdio.color("reset"))
|
||||
elseif log.messageType == "INFO" then
|
||||
stdio.write(stdio.color("cyan"))
|
||||
stdio.write(log.message)
|
||||
stdio.write("\n")
|
||||
stdio.write(stdio.color("reset"))
|
||||
elseif log.messageType == "WARNING" then
|
||||
stdio.write(stdio.color("yellow"))
|
||||
stdio.write(log.message)
|
||||
stdio.write("\n")
|
||||
stdio.write(stdio.color("reset"))
|
||||
else
|
||||
stdio.write(stdio.color("reset"))
|
||||
stdio.write(log.message)
|
||||
stdio.write("\n")
|
||||
stdio.write(stdio.color("reset"))
|
||||
end
|
||||
end
|
||||
1
build.rs
1
build.rs
|
|
@ -96,6 +96,7 @@ fn main() -> Result<(), anyhow::Error> {
|
|||
"src" => snapshot_from_fs_path(&plugin_dir.join("src"))?,
|
||||
"Packages" => snapshot_from_fs_path(&plugin_dir.join("Packages"))?,
|
||||
"Version.txt" => snapshot_from_fs_path(&plugin_dir.join("Version.txt"))?,
|
||||
"UploadDetails.json" => snapshot_from_fs_path(&plugin_dir.join("UploadDetails.json"))?,
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@
|
|||
},
|
||||
"Version": {
|
||||
"$path": "plugin/Version.txt"
|
||||
},
|
||||
"UploadDetails": {
|
||||
"$path": "plugin/UploadDetails.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
7
plugin/UploadDetails.json
Normal file
7
plugin/UploadDetails.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"assetId": 111151139098227,
|
||||
"name": "Atlas",
|
||||
"description": "The plugin portion of Atlas, a tool to enable professional tooling for Roblox developers.",
|
||||
"creatorId": 422368266,
|
||||
"creatorType": "Group"
|
||||
}
|
||||
|
|
@ -3,3 +3,4 @@ selene = "Kampfkarren/selene@0.30.0"
|
|||
stylua = "JohnnyMorganz/stylua@2.3.1"
|
||||
luau-lsp = "JohnnyMorganz/luau-lsp@1.62.0"
|
||||
run-in-roblox = "rojo-rbx/run-in-roblox@0.3.0"
|
||||
lune = "lune-org/lune@0.10.4"
|
||||
|
|
|
|||
|
|
@ -1,14 +1,9 @@
|
|||
# Build and publish the Rojo plugin to the Creator Store
|
||||
# Loads API key from .env file, ROBLOX_API_KEY env var, or -ApiKey argument.
|
||||
# Get an API key at https://create.roblox.com/credentials (assets read+write).
|
||||
|
||||
param(
|
||||
[string]$ApiKey
|
||||
)
|
||||
# Build and publish the Atlas plugin to the Creator Store via OpenCloud.
|
||||
# Loads env vars from .env file in the repo root.
|
||||
# Required .env vars: PLUGIN_UPLOAD_TOKEN, PLUGIN_CI_UNIVERSE_ID, PLUGIN_CI_PLACE_ID
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# Load .env file if it exists
|
||||
$envFile = Join-Path $PSScriptRoot "..\.env"
|
||||
if (Test-Path $envFile) {
|
||||
foreach ($line in Get-Content $envFile) {
|
||||
|
|
@ -22,16 +17,14 @@ if (Test-Path $envFile) {
|
|||
}
|
||||
}
|
||||
|
||||
# Resolve API key: argument > env var > .env (already loaded above)
|
||||
if (-not $ApiKey) {
|
||||
$ApiKey = $env:ROBLOX_API_KEY
|
||||
}
|
||||
|
||||
if (-not $ApiKey -or $ApiKey -eq "your-key-here") {
|
||||
Write-Host "Error: No API key provided." -ForegroundColor Red
|
||||
Write-Host "Set ROBLOX_API_KEY in .env, env var, or pass -ApiKey <key>" -ForegroundColor Yellow
|
||||
if (-not $env:PLUGIN_UPLOAD_TOKEN) {
|
||||
Write-Host "Error: PLUGIN_UPLOAD_TOKEN not set. Add it to .env or set as env var." -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
$env:RBX_API_KEY = $env:PLUGIN_UPLOAD_TOKEN
|
||||
$env:RBX_UNIVERSE_ID = $env:PLUGIN_CI_UNIVERSE_ID
|
||||
$env:RBX_PLACE_ID = $env:PLUGIN_CI_PLACE_ID
|
||||
|
||||
Write-Host "Building and uploading plugin..." -ForegroundColor Cyan
|
||||
cargo run -- upload plugin.project.json --asset_id 111151139098227
|
||||
lune run upload-plugin Atlas.rbxm
|
||||
|
|
|
|||
27
scripts/publish-plugin.sh
Executable file
27
scripts/publish-plugin.sh
Executable file
|
|
@ -0,0 +1,27 @@
|
|||
#!/usr/bin/env bash
|
||||
# Build and publish the Atlas plugin to the Creator Store via OpenCloud.
|
||||
# Loads env vars from .env file in the repo root.
|
||||
# Required .env vars: PLUGIN_UPLOAD_TOKEN, PLUGIN_CI_UNIVERSE_ID, PLUGIN_CI_PLACE_ID
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ENV_FILE="$SCRIPT_DIR/../.env"
|
||||
|
||||
if [ -f "$ENV_FILE" ]; then
|
||||
set -a
|
||||
source "$ENV_FILE"
|
||||
set +a
|
||||
fi
|
||||
|
||||
if [ -z "${PLUGIN_UPLOAD_TOKEN:-}" ]; then
|
||||
echo "Error: PLUGIN_UPLOAD_TOKEN not set. Add it to .env or set as env var." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export RBX_API_KEY="$PLUGIN_UPLOAD_TOKEN"
|
||||
export RBX_UNIVERSE_ID="${PLUGIN_CI_UNIVERSE_ID:-}"
|
||||
export RBX_PLACE_ID="${PLUGIN_CI_PLACE_ID:-}"
|
||||
|
||||
echo "Building and uploading plugin..."
|
||||
lune run upload-plugin Atlas.rbxm
|
||||
|
|
@ -99,5 +99,5 @@ fn uninstall_plugin() -> anyhow::Result<()> {
|
|||
|
||||
#[test]
|
||||
fn plugin_initialize() {
|
||||
assert!(initialize_plugin().is_ok())
|
||||
let _ = initialize_plugin().unwrap();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit e25d02caea9afbac353e19a82e72189348306e1b
|
||||
Subproject commit 383f174518c8ec25e03c18e3ed26a0af093ac6d2
|
||||
Loading…
Add table
Add a link
Reference in a new issue