Compare commits

..

1 Commits

Author SHA1 Message Date
bo0tzz a9993428e5 fix: pass secrets to build-mobile 2026-06-12 17:27:53 +02:00
7 changed files with 27 additions and 168 deletions
+4
View File
@@ -12,6 +12,10 @@ on:
default: 'development'
type: string
secrets:
PUSH_O_MATIC_APP_CLIENT_ID:
required: true
PUSH_O_MATIC_APP_KEY:
required: true
KEY_JKS:
required: true
ALIAS:
+3 -9
View File
@@ -50,7 +50,6 @@ jobs:
outputs:
ref: ${{ steps.push-tag.outputs.commit_long_sha }}
version: ${{ steps.output.outputs.version }}
rc: ${{ steps.output.outputs.rc }}
permissions: {} # No job-level permissions are needed because it uses the app-token
steps:
- id: token
@@ -82,13 +81,7 @@ jobs:
run: pnpm --silent release -s "${SERVER_BUMP}" -m "${MOBILE_BUMP}"
- id: output
run: |
echo "version=$IMMICH_VERSION" >> $GITHUB_OUTPUT
if [[ "$IMMICH_VERSION" =~ -rc\.[0-9]+$ ]]; then
echo "rc=true" >> $GITHUB_OUTPUT
else
echo "rc=false" >> $GITHUB_OUTPUT
fi
run: echo "version=$IMMICH_VERSION" >> $GITHUB_OUTPUT
- name: Commit and tag
id: push-tag
@@ -106,6 +99,8 @@ jobs:
contents: read
pull-requests: write
secrets:
PUSH_O_MATIC_APP_CLIENT_ID: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
PUSH_O_MATIC_APP_KEY: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
KEY_JKS: ${{ secrets.KEY_JKS }}
ALIAS: ${{ secrets.ALIAS }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
@@ -152,7 +147,6 @@ jobs:
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
with:
draft: true
prerelease: ${{ needs.bump_version.outputs.rc }}
tag_name: ${{ needs.bump_version.outputs.version }}
token: ${{ steps.generate-token.outputs.token }}
generate_release_notes: true
+1 -1
View File
@@ -95,7 +95,7 @@ describe('/server', () => {
major: expect.any(Number),
minor: expect.any(Number),
patch: expect.any(Number),
prerelease: expect.anything(),
prerelease: null,
});
});
});
+2 -8
View File
@@ -70,10 +70,7 @@ class DeepLinkService {
if (assetRegex.hasMatch(path)) {
final assetId = assetRegex.firstMatch(path)?.group(1) ?? '';
// /albums/<albumId>/photos/<assetId> links carry the album context,
// which drives the like/comment UI in the viewer
final albumId = albumRegex.firstMatch(path)?.group(1);
return _buildAssetDeepLink(assetId, ref, albumId: albumId);
return _buildAssetDeepLink(assetId, ref);
}
if (albumRegex.hasMatch(path)) {
final albumId = albumRegex.firstMatch(path)?.group(1) ?? '';
@@ -110,19 +107,16 @@ class DeepLinkService {
return DriftMemoryRoute(memories: memories, memoryIndex: 0);
}
Future<PageRouteInfo?> _buildAssetDeepLink(String assetId, WidgetRef ref, {String? albumId}) async {
Future<PageRouteInfo?> _buildAssetDeepLink(String assetId, WidgetRef ref) async {
final asset = await _betaAssetService.getRemoteAsset(assetId);
if (asset == null) {
return null;
}
final album = albumId != null ? await _betaRemoteAlbumService.get(albumId) : null;
AssetViewer.setAsset(ref, asset);
return AssetViewerRoute(
initialIndex: 0,
timelineService: _betaTimelineFactory.fromAssets([asset], TimelineOrigin.deepLink),
currentAlbum: album,
);
}
@@ -1,140 +0,0 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/album/album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/timeline.model.dart';
import 'package:immich_mobile/domain/services/asset.service.dart';
import 'package:immich_mobile/domain/services/memory.service.dart';
import 'package:immich_mobile/domain/services/people.service.dart';
import 'package:immich_mobile/domain/services/remote_album.service.dart';
import 'package:immich_mobile/domain/services/timeline.service.dart';
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/services/deep_link.service.dart';
import 'package:mocktail/mocktail.dart';
class MockTimelineFactory extends Mock implements TimelineFactory {}
class MockAssetService extends Mock implements AssetService {}
class MockRemoteAlbumService extends Mock implements RemoteAlbumService {}
class MockDriftMemoryService extends Mock implements DriftMemoryService {}
class MockDriftPeopleService extends Mock implements DriftPeopleService {}
class MockPlatformDeepLink extends Mock implements PlatformDeepLink {}
class MockWidgetRef extends Mock implements WidgetRef {}
class MockAssetViewerStateNotifier extends Mock implements AssetViewerStateNotifier {}
const _assetId = 'aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb';
const _albumId = 'cccccccc-4444-5555-6666-dddddddddddd';
final _asset = RemoteAsset(
id: _assetId,
name: 'photo.jpg',
ownerId: 'user-1',
checksum: 'checksum-1',
type: AssetType.image,
createdAt: DateTime(2026, 6, 12),
updatedAt: DateTime(2026, 6, 12),
isEdited: false,
);
final _album = RemoteAlbum(
id: _albumId,
name: 'Shared Album',
ownerId: 'user-1',
description: '',
createdAt: DateTime(2026, 6, 12),
updatedAt: DateTime(2026, 6, 12),
isActivityEnabled: true,
isShared: true,
order: AlbumAssetOrder.asc,
assetCount: 1,
ownerName: 'Owner',
);
void main() {
late MockTimelineFactory timelineFactory;
late MockAssetService assetService;
late MockRemoteAlbumService remoteAlbumService;
late MockWidgetRef ref;
late List<TimelineService> createdTimelineServices;
late DeepLinkService sut;
setUp(() {
timelineFactory = MockTimelineFactory();
assetService = MockAssetService();
remoteAlbumService = MockRemoteAlbumService();
ref = MockWidgetRef();
createdTimelineServices = [];
when(() => timelineFactory.fromAssets(any(), TimelineOrigin.deepLink)).thenAnswer((invocation) {
final assets = List<BaseAsset>.from(invocation.positionalArguments[0] as List<BaseAsset>);
final timelineService = TimelineService((
assetSource: (index, count) async => assets.skip(index).take(count).toList(),
bucketSource: () => Stream.value([Bucket(assetCount: assets.length)]),
origin: TimelineOrigin.deepLink,
));
createdTimelineServices.add(timelineService);
return timelineService;
});
when(() => ref.read(assetViewerProvider.notifier)).thenReturn(MockAssetViewerStateNotifier());
sut = DeepLinkService(
timelineFactory,
assetService,
remoteAlbumService,
MockDriftMemoryService(),
MockDriftPeopleService(),
null,
);
addTearDown(() async {
for (final timelineService in createdTimelineServices) {
await timelineService.dispose();
}
});
});
PlatformDeepLink link(String path) {
final deepLink = MockPlatformDeepLink();
when(() => deepLink.uri).thenReturn(Uri.parse('https://my.immich.app$path'));
return deepLink;
}
test('album photo link carries the album into the viewer route', () async {
when(() => assetService.getRemoteAsset(_assetId)).thenAnswer((_) async => _asset);
when(() => remoteAlbumService.get(_albumId)).thenAnswer((_) async => _album);
final route = await sut.handleMyImmichApp(link('/albums/$_albumId/photos/$_assetId'), ref);
expect(route, isA<AssetViewerRoute>());
expect((route!.args as AssetViewerRouteArgs).currentAlbum, _album);
});
test('still opens the viewer when the album cannot be resolved', () async {
when(() => assetService.getRemoteAsset(_assetId)).thenAnswer((_) async => _asset);
when(() => remoteAlbumService.get(_albumId)).thenAnswer((_) async => null);
final route = await sut.handleMyImmichApp(link('/albums/$_albumId/photos/$_assetId'), ref);
expect(route, isA<AssetViewerRoute>());
expect((route!.args as AssetViewerRouteArgs).currentAlbum, isNull);
});
test('plain photo link has no album', () async {
when(() => assetService.getRemoteAsset(_assetId)).thenAnswer((_) async => _asset);
final route = await sut.handleMyImmichApp(link('/photos/$_assetId'), ref);
expect(route, isA<AssetViewerRoute>());
expect((route!.args as AssetViewerRouteArgs).currentAlbum, isNull);
verifyNever(() => remoteAlbumService.get(any()));
});
}
+16 -9
View File
@@ -1,6 +1,7 @@
import { DateTime } from 'luxon';
import { SemVer } from 'semver';
import { defaults } from 'src/config';
import { serverVersion } from 'src/constants';
import { ReleaseChannel } from 'src/dtos/system-config.dto';
import { CronJob, JobName, JobStatus, SystemMetadataKey } from 'src/enum';
import { VersionService } from 'src/services/version.service';
@@ -22,10 +23,16 @@ describe(VersionService.name, () => {
mocks.cron.update.mockResolvedValue();
});
vitest.mock(import('src/constants.js'), async (importOriginal) => ({
...(await importOriginal()),
serverVersion: new SemVer('v3.0.0'),
}));
beforeAll(() => {
vitest.mock(import('src/constants.js'), async () => ({
...(await vitest.importActual<typeof import('src/constants.js')>('src/constants.js')),
serverVersion: new SemVer('v3.0.0'),
}));
});
afterAll(() => {
vitest.unmock(import('src/constants.js'));
});
it('should work', () => {
expect(sut).toBeDefined();
@@ -46,7 +53,7 @@ describe(VersionService.name, () => {
mocks.versionHistory.getLatest.mockResolvedValue({
id: 'version-1',
createdAt: new Date(),
version: '3.0.0',
version: serverVersion.toString(),
});
await expect(sut.onBootstrap()).resolves.toBeUndefined();
expect(mocks.versionHistory.create).not.toHaveBeenCalled();
@@ -57,7 +64,7 @@ describe(VersionService.name, () => {
mocks.versionHistory.getLatest.mockResolvedValue({
id: 'version-1',
createdAt: new Date(),
version: '3.0.0',
version: serverVersion.toString(),
});
await sut.onBootstrap();
expect(mocks.cron.create).toHaveBeenCalledWith(
@@ -114,7 +121,7 @@ describe(VersionService.name, () => {
checkedAt: DateTime.utc().minus({ seconds: 60 }).toISO(),
releaseVersion: '1.0.0',
});
mocks.serverInfo.getLatestRelease.mockResolvedValue(mockVersionResponse('v3.0.0'));
mocks.serverInfo.getLatestRelease.mockResolvedValue(mockVersionResponse(serverVersion.toString()));
await expect(sut.handleVersionCheck()).resolves.toEqual(JobStatus.Success);
expect(mocks.serverInfo.getLatestRelease).toHaveBeenCalled();
});
@@ -128,11 +135,11 @@ describe(VersionService.name, () => {
});
it('should not notify if the version is equal', async () => {
mocks.serverInfo.getLatestRelease.mockResolvedValue(mockVersionResponse('v3.0.0'));
mocks.serverInfo.getLatestRelease.mockResolvedValue(mockVersionResponse(serverVersion.toString()));
await expect(sut.handleVersionCheck()).resolves.toEqual(JobStatus.Success);
expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.VersionCheckState, {
checkedAt: expect.any(String),
releaseVersion: 'v3.0.0',
releaseVersion: serverVersion.toString(),
});
expect(mocks.websocket.clientBroadcast).not.toHaveBeenCalled();
});
+1 -1
View File
@@ -69,7 +69,7 @@ export enum OpenQueryParam {
PURCHASE_SETTINGS = 'user-purchase-settings',
}
export const maximumLengthSearchPeople = 100;
export const maximumLengthSearchPeople = 1000;
// time to load the map before displaying the loading spinner
export const timeToLoadTheMap: number = 100;