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
45 changes: 1 addition & 44 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ on:
tags:
- "v*.*.*"
- "!v*-nightly.*"
schedule:
- cron: "0 */3 * * *"
workflow_dispatch:
inputs:
channel:
Expand All @@ -27,49 +25,8 @@ permissions:
id-token: none

jobs:
check_changes:
name: Check for changes since last nightly
if: github.event_name == 'schedule'
runs-on: blacksmith-8vcpu-ubuntu-2404
outputs:
has_changes: ${{ steps.check.outputs.has_changes }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
sparse-checkout: |
/*
!/.repos/
sparse-checkout-cone-mode: false

- id: check
name: Compare HEAD to last nightly tag
run: |
last_nightly_tag=$(git tag --list 'v*-nightly.*' 'nightly-v*' --sort=-creatordate | head -n 1)
if [[ -z "$last_nightly_tag" ]]; then
echo "No previous nightly tag found. Proceeding with release."
echo "has_changes=true" >> "$GITHUB_OUTPUT"
exit 0
fi

last_nightly_sha=$(git rev-parse "$last_nightly_tag^{commit}")
head_sha=$(git rev-parse HEAD)

if [[ "$last_nightly_sha" == "$head_sha" ]]; then
echo "No changes on main since last nightly release ($last_nightly_tag). Skipping."
echo "has_changes=false" >> "$GITHUB_OUTPUT"
else
echo "Changes detected on main since $last_nightly_tag ($last_nightly_sha → $head_sha). Proceeding."
echo "has_changes=true" >> "$GITHUB_OUTPUT"
fi

preflight:
name: Preflight
needs: [check_changes]
if: |
!failure() && !cancelled() &&
(github.event_name != 'schedule' || needs.check_changes.outputs.has_changes == 'true')
runs-on: blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 10
outputs:
Expand Down Expand Up @@ -113,7 +70,7 @@ jobs:
NIGHTLY_SHA: ${{ github.sha }}
NIGHTLY_RUN_NUMBER: ${{ github.run_number }}
run: |
if [[ "${GITHUB_EVENT_NAME}" == "schedule" || ( "${GITHUB_EVENT_NAME}" == "workflow_dispatch" && "${DISPATCH_CHANNEL:-stable}" == "nightly" ) ]]; then
if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" && "${DISPATCH_CHANNEL:-stable}" == "nightly" ]]; then
nightly_date="$(date -u -d "$NIGHTLY_DATE" +%Y%m%d)"

node scripts/resolve-nightly-release.ts \
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ __screenshots__/
squashfs-root/
.vercel
.gstack/
/tmp/
dist-electron/
.electron-runtime/
.showcase/
Expand Down
2 changes: 2 additions & 0 deletions .vite-hooks/pre-push
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/usr/bin/env sh
vp run --workspace-root lastcode:ci:quick
51 changes: 7 additions & 44 deletions apps/desktop/src/app/DesktopAppIdentity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as PlatformError from "effect/PlatformError";

import type * as Electron from "electron";

Expand All @@ -20,9 +19,9 @@ const defaultEnvironmentInput = {
platform: "darwin",
processArch: "arm64",
appVersion: "1.2.3",
appPath: "/Applications/T3 Code.app/Contents/Resources/app.asar",
appPath: "/Applications/LastCode.app/Contents/Resources/app.asar",
isPackaged: true,
resourcesPath: "/Applications/T3 Code.app/Contents/Resources",
resourcesPath: "/Applications/LastCode.app/Contents/Resources",
runningUnderArm64Translation: false,
} satisfies DesktopEnvironment.MakeDesktopEnvironmentInput;

Expand All @@ -39,7 +38,7 @@ interface ElectronAppCalls {
const makeElectronAppLayer = (calls: ElectronAppCalls) =>
Layer.succeed(ElectronApp.ElectronApp, {
metadata: Effect.die("unexpected metadata read"),
name: Effect.succeed("T3 Code"),
name: Effect.succeed("LastCode"),
whenReady: Effect.void,
quit: Effect.void,
exit: () => Effect.void,
Expand Down Expand Up @@ -107,8 +106,6 @@ const withIdentity = <A, E, R>(
input: {
readonly calls?: ElectronAppCalls;
readonly environment?: TestEnvironmentInput;
readonly legacyPathExists?: boolean;
readonly legacyPathProbeError?: PlatformError.PlatformError;
readonly packageJson?: string;
readonly pngIconPath?: Option.Option<string>;
} = {},
Expand All @@ -124,12 +121,6 @@ const withIdentity = <A, E, R>(
DesktopAppIdentity.layer.pipe(
Layer.provideMerge(
FileSystem.layerNoop({
exists: (path) =>
input.legacyPathProbeError
? Effect.fail(input.legacyPathProbeError)
: Effect.succeed(
input.legacyPathExists === true && path.includes("T3 Code (Alpha)"),
),
readFileString: () =>
Effect.succeed(input.packageJson ?? '{"t3codeCommitHash":"abcdef1234567890"}'),
}),
Expand All @@ -143,45 +134,17 @@ const withIdentity = <A, E, R>(
};

describe("DesktopAppIdentity", () => {
it.effect("keeps using the legacy userData path when it already exists", () =>
it.effect("uses the isolated LastCode userData path", () =>
withIdentity(
Effect.gen(function* () {
const identity = yield* DesktopAppIdentity.DesktopAppIdentity;
const userDataPath = yield* identity.resolveUserDataPath;

assert.equal(userDataPath, "/Users/alice/Library/Application Support/T3 Code (Alpha)");
assert.equal(userDataPath, "/Users/alice/Library/Application Support/lastcode");
}),
{ legacyPathExists: true },
),
);

it.effect("preserves failures while inspecting the legacy userData path", () => {
const legacyPath = "/Users/alice/Library/Application Support/T3 Code (Alpha)";
const cause = PlatformError.systemError({
_tag: "PermissionDenied",
module: "FileSystem",
method: "exists",
description: "permission denied",
pathOrDescriptor: legacyPath,
});

return withIdentity(
Effect.gen(function* () {
const identity = yield* DesktopAppIdentity.DesktopAppIdentity;
const error = yield* identity.resolveUserDataPath.pipe(Effect.flip);

assert.instanceOf(error, DesktopAppIdentity.DesktopUserDataPathResolutionError);
assert.equal(error.legacyPath, legacyPath);
assert.strictEqual(error.cause, cause);
assert.equal(
error.message,
`Failed to inspect legacy desktop user-data path at "${legacyPath}".`,
);
}),
{ legacyPathProbeError: cause },
);
});

it.effect("configures app identity from the environment commit override", () => {
const calls: ElectronAppCalls = {
setAboutPanelOptions: [],
Expand All @@ -194,8 +157,8 @@ describe("DesktopAppIdentity", () => {
const identity = yield* DesktopAppIdentity.DesktopAppIdentity;
yield* identity.configure;

assert.deepEqual(calls.setName, ["T3 Code (Alpha)"]);
assert.equal(calls.setAboutPanelOptions[0]?.applicationName, "T3 Code (Alpha)");
assert.deepEqual(calls.setName, ["LastCode (Alpha)"]);
assert.equal(calls.setAboutPanelOptions[0]?.applicationName, "LastCode (Alpha)");
assert.equal(calls.setAboutPanelOptions[0]?.applicationVersion, "1.2.3");
assert.equal(calls.setAboutPanelOptions[0]?.version, "0123456789ab");
assert.deepEqual(calls.setDockIcon, ["/icon.png"]);
Expand Down
32 changes: 2 additions & 30 deletions apps/desktop/src/app/DesktopAppIdentity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,10 @@ const AppPackageMetadata = Schema.Struct({
});
const decodeAppPackageMetadata = Schema.decodeEffect(Schema.fromJsonString(AppPackageMetadata));

export class DesktopUserDataPathResolutionError extends Schema.TaggedErrorClass<DesktopUserDataPathResolutionError>()(
"DesktopUserDataPathResolutionError",
{
legacyPath: Schema.String,
cause: Schema.Defect(),
},
) {
override get message(): string {
return `Failed to inspect legacy desktop user-data path at "${this.legacyPath}".`;
}
}

export class DesktopAppIdentity extends Context.Service<
DesktopAppIdentity,
{
readonly resolveUserDataPath: Effect.Effect<string, DesktopUserDataPathResolutionError>;
readonly resolveUserDataPath: Effect.Effect<string>;
readonly configure: Effect.Effect<void>;
}
>()("@t3tools/desktop/app/DesktopAppIdentity") {}
Expand All @@ -47,23 +35,7 @@ const normalizeCommitHash = (value: string): Option.Option<string> => {

export const resolveUserDataPath = Effect.gen(function* () {
const environment = yield* DesktopEnvironment.DesktopEnvironment;
const fileSystem = yield* FileSystem.FileSystem;
const legacyPath = environment.path.join(
environment.appDataDirectory,
environment.legacyUserDataDirName,
);
const legacyPathExists = yield* fileSystem.exists(legacyPath).pipe(
Effect.mapError(
(cause) =>
new DesktopUserDataPathResolutionError({
legacyPath,
cause,
}),
),
);
return legacyPathExists
? legacyPath
: environment.path.join(environment.appDataDirectory, environment.userDataDirName);
return environment.path.join(environment.appDataDirectory, environment.userDataDirName);
}).pipe(Effect.withSpan("desktop.appIdentity.resolveUserDataPath"));

export const make = Effect.gen(function* () {
Expand Down
14 changes: 8 additions & 6 deletions apps/desktop/src/app/DesktopClerk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,7 @@ const makeDesktopClerkLayer = (isDevelopment = true, events: string[] = []) => {
stateDir: "/tmp/t3-state",
isDevelopment,
appDataDirectory: "/tmp/app-data",
userDataDirName: isDevelopment ? "t3code-dev" : "t3code",
legacyUserDataDirName: isDevelopment ? "T3 Code (Dev)" : "T3 Code (Alpha)",
userDataDirName: isDevelopment ? "lastcode-dev" : "lastcode",
path: { join: (...parts: ReadonlyArray<string>) => parts.join("/") },
} as unknown as DesktopEnvironment.DesktopEnvironment["Service"]);

Expand Down Expand Up @@ -91,15 +90,18 @@ describe("DesktopClerk", () => {
{
storage: storageAdapter,
passkeys: true,
renderer: { scheme: "t3code-dev", host: "app" },
renderer: { scheme: "lastcode-dev", host: "app" },
},
],
]);
assert.equal(cleanup.mock.calls.length, 1);
// The bridge acquires Electron's single-instance lock at creation, and
// the lock both lives in and creates the userData directory — so the
// real path must be set before the bridge exists.
assert.deepEqual(events, ["setPath:userData:/tmp/app-data/t3code-dev", "createClerkBridge"]);
assert.deepEqual(events, [
"setPath:userData:/tmp/app-data/lastcode-dev",
"createClerkBridge",
]);
storageMock.mockClear();
createClerkBridgeMock.mockClear();
});
Expand Down Expand Up @@ -210,8 +212,8 @@ describe("DesktopClerk", () => {
});

it.each([
{ isDevelopment: true, scheme: "t3code-dev" },
{ isDevelopment: false, scheme: "t3code" },
{ isDevelopment: true, scheme: "lastcode-dev" },
{ isDevelopment: false, scheme: "lastcode" },
])("configures the SDK with the $scheme renderer origin", ({ isDevelopment, scheme }) => {
const bridge = { cleanup: vi.fn(), isPrimaryInstance: true };
storageMock.mockReturnValue(storageAdapter);
Expand Down
9 changes: 3 additions & 6 deletions apps/desktop/src/app/DesktopClerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,9 @@ export const make = Effect.gen(function* () {
const environment = yield* DesktopEnvironment.DesktopEnvironment;
const electronApp = yield* ElectronApp.ElectronApp;

// Electron scopes the single-instance lock to the userData directory and
// creates that directory when the lock is acquired. The SDK bridge takes
// the lock at creation, so userData must already point at the real
// directory here — under the default productName-derived path, acquiring
// the lock would create "T3 Code (Alpha)" and make the legacy-install
// detection in resolveUserDataPath match on fresh installs.
// Electron scopes the single-instance lock to the userData directory. The
// SDK bridge takes the lock at creation, so point it at LastCode's isolated
// profile before creating the bridge.
const userDataPath = yield* DesktopAppIdentity.resolveUserDataPath;
yield* electronApp.setPath("userData", userDataPath);

Expand Down
8 changes: 4 additions & 4 deletions apps/desktop/src/app/DesktopEarlyElectronStartup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,20 +81,20 @@ describe("DesktopEarlyElectronStartup", () => {
});

assert.deepEqual(options, {
linuxWmClass: "t3code-dev",
linuxWmClass: "lastcode-dev",
passwordStore: "gnome-libsecret",
});
});

it("keeps implicit development state under ~/.t3/dev when T3CODE_HOME is unset", () => {
it("keeps implicit development state under ~/.lastcode/dev when T3CODE_HOME is unset", () => {
const preference = resolveEarlyLinuxPasswordStorePreference({
env: {
VITE_DEV_SERVER_URL: "http://127.0.0.1:5173",
},
homeDirectory: "/home/user",
joinPath,
readFileString: (path) => {
assert.equal(path, "/home/user/.t3/dev/desktop-settings.json");
assert.equal(path, "/home/user/.lastcode/dev/desktop-settings.json");
return JSON.stringify({ linuxPasswordStore: "kwallet" });
},
});
Expand All @@ -111,7 +111,7 @@ describe("DesktopEarlyElectronStartup", () => {
homeDirectory: "/home/user",
joinPath,
readFileString: (path) => {
assert.equal(path, "/home/user/.t3/dev/desktop-settings.json");
assert.equal(path, "/home/user/.lastcode/dev/desktop-settings.json");
return JSON.stringify({ linuxPasswordStore: "gnome-libsecret" });
},
});
Expand Down
5 changes: 4 additions & 1 deletion apps/desktop/src/app/DesktopEarlyElectronStartup.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { fromLenientJson } from "@t3tools/shared/schemaJson";
import { LASTCODE_DESKTOP_DISTRIBUTION } from "@t3tools/shared/desktopDistribution";
import * as Option from "effect/Option";
import * as Schema from "effect/Schema";

Expand Down Expand Up @@ -81,7 +82,9 @@ export function resolveEarlyLinuxElectronOptions(
): EarlyLinuxElectronOptions {
const preference = resolveEarlyLinuxPasswordStorePreference(input);
return {
linuxWmClass: isDevelopmentEnvironment(input.env) ? "t3code-dev" : "t3code",
linuxWmClass: isDevelopmentEnvironment(input.env)
? LASTCODE_DESKTOP_DISTRIBUTION.developmentExecutableName
: LASTCODE_DESKTOP_DISTRIBUTION.executableName,
passwordStore: resolveLinuxPasswordStoreSwitch({
preference,
env: input.env,
Expand Down
21 changes: 13 additions & 8 deletions apps/desktop/src/app/DesktopEnvironment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ const defaultInput = {
platform: "darwin",
processArch: "arm64",
appVersion: "0.0.22",
appPath: "/Applications/T3 Code.app/Contents/Resources/app.asar",
appPath: "/Applications/LastCode.app/Contents/Resources/app.asar",
isPackaged: false,
resourcesPath: "/Applications/T3 Code.app/Contents/Resources",
resourcesPath: "/Applications/LastCode.app/Contents/Resources",
runningUnderArm64Translation: false,
} satisfies DesktopEnvironment.MakeDesktopEnvironmentInput;

Expand Down Expand Up @@ -67,8 +67,10 @@ describe("DesktopEnvironment", () => {
assert.equal(environment.appRoot, "/repo");
assert.equal(environment.backendEntryPath, "/repo/apps/server/dist/bin.mjs");
assert.equal(environment.backendCwd, "/repo");
assert.equal(environment.appUserModelId, "com.t3tools.t3code.dev");
assert.equal(environment.linuxWmClass, "t3code-dev");
assert.equal(environment.appUserModelId, "codes.lastobelus.lastcode.dev");
assert.equal(environment.linuxWmClass, "lastcode-dev");
assert.equal(environment.userDataDirName, "lastcode-dev");
assert.equal(environment.displayName, "LastCode (Dev)");
assert.deepEqual(
Option.map(environment.devServerUrl, (url) => url.href),
Option.some("http://localhost:5173/"),
Expand Down Expand Up @@ -106,8 +108,11 @@ describe("DesktopEnvironment", () => {
);
const production = yield* makeEnvironment();

assert.equal(development.stateDir, "/Users/alice/.t3/dev");
assert.equal(production.stateDir, "/Users/alice/.t3/userdata");
assert.equal(development.stateDir, "/Users/alice/.lastcode/dev");
assert.equal(production.stateDir, "/Users/alice/.lastcode/userdata");
assert.equal(production.userDataDirName, "lastcode");
assert.equal(production.appUserModelId, "codes.lastobelus.lastcode");
assert.equal(production.displayName, "LastCode (Alpha)");
}),
);

Expand All @@ -116,12 +121,12 @@ describe("DesktopEnvironment", () => {
const environment = yield* makeEnvironment(
{},
{
T3CODE_DESKTOP_APP_USER_MODEL_ID: " com.t3tools.t3code.dev.local ",
T3CODE_DESKTOP_APP_USER_MODEL_ID: " codes.lastobelus.lastcode.dev.local ",
VITE_DEV_SERVER_URL: "http://localhost:5173",
},
);

assert.equal(environment.appUserModelId, "com.t3tools.t3code.dev.local");
assert.equal(environment.appUserModelId, "codes.lastobelus.lastcode.dev.local");
}),
);

Expand Down
Loading