Compare commits

...

7 Commits

Author SHA1 Message Date
Daniel Dietzler 92293eba19 Merge branch 'main' into apk-build-deployment 2026-06-16 15:21:18 +02:00
Daniel Dietzler e70a1163f3 fix: too strict cron expression validation (#29138) 2026-06-16 13:20:40 +00:00
Daniel Dietzler a23a7c69ae fix: map settings (#29134) 2026-06-16 15:18:57 +02:00
Daniel Dietzler f21a753aff fix: integrity report checksum query (#29136) 2026-06-16 15:11:20 +02:00
Alex e93f0db224 Merge branch 'main' into apk-build-deployment 2026-06-12 14:10:24 -05:00
Alex Tran 46d8be8ffc different app name for staging 2026-06-05 16:16:03 -05:00
Alex Tran df051c24b3 chore: apk build deployment 2026-06-05 15:31:20 -05:00
15 changed files with 2915 additions and 2744 deletions
+54
View File
@@ -73,6 +73,7 @@ jobs:
needs: pre-job
permissions:
contents: read
deployments: write
pull-requests: write
if: ${{ github.actor != 'dependabot[bot]' && fromJSON(needs.pre-job.outputs.should_run).mobile == true }}
runs-on: mich
@@ -142,9 +143,18 @@ jobs:
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }}
IS_MAIN: ${{ github.ref == 'refs/heads/main' }}
IS_MAIN_DEPLOYMENT: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
IS_DEPLOYMENT_BUILD: ${{ (github.event_name == 'push' && github.ref == 'refs/heads/main') || (github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork) }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
if [[ $IS_DEPLOYMENT_BUILD == 'true' ]]; then
export ANDROID_APP_LABEL='Immich Staging'
fi
if [[ $IS_MAIN == 'true' ]]; then
if [[ $IS_MAIN_DEPLOYMENT == 'true' ]]; then
export ANDROID_APPLICATION_ID=app.immich.main
fi
flutter build apk --release
flutter build apk --release --split-per-abi --target-platform android-arm,android-arm64,android-x64
else
@@ -158,6 +168,50 @@ jobs:
name: release-apk-signed
path: mobile/build/app/outputs/flutter-apk/*.apk
- name: Publish Android APK deployment
if: ${{ (github.event_name == 'push' && github.ref == 'refs/heads/main') || (github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork) }}
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
APK_URL: ${{ steps.upload-apk.outputs.artifact-url }}
with:
script: |
const artifactUrl = process.env.APK_URL;
const isPullRequest = context.eventName === "pull_request";
const pullNumber = context.payload.pull_request?.number;
const environment = isPullRequest ? `mobile-android-apk-pr-${pullNumber}` : "mobile-android-apk";
const description = isPullRequest
? `Signed Android APK for PR #${pullNumber}`
: "Latest signed Android APK from main";
const ref = isPullRequest ? context.payload.pull_request.head.sha : context.sha;
if (!artifactUrl) {
throw new Error("The Android APK artifact URL was not generated");
}
const runUrl = `${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
const deployment = await github.rest.repos.createDeployment({
owner: context.repo.owner,
repo: context.repo.repo,
ref,
environment,
description,
auto_merge: false,
required_contexts: [],
production_environment: false,
transient_environment: isPullRequest,
});
await github.rest.repos.createDeploymentStatus({
owner: context.repo.owner,
repo: context.repo.repo,
deployment_id: deployment.data.id,
state: "success",
environment,
environment_url: artifactUrl,
log_url: runUrl,
description: "Signed APK artifact is ready for testing",
});
- name: Comment APK download link on PR
if: ${{ github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork }}
uses: immich-app/devtools/actions/sticky-comment@0135acd12ad9f3369b94a2aa3c0ae8c835a4e926 # sticky-comment-action-v1.0.0
+13
View File
@@ -82,6 +82,19 @@ url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/353224133"
version = "7.1.3-6"
backend = "github:jellyfin/jellyfin-ffmpeg"
[tools."github:jellyfin/jellyfin-ffmpeg".options]
asset_pattern = "jellyfin-ffmpeg_*_portable_linuxarm64-gpl.tar.xz"
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-arm64"]
checksum = "sha256:bea03c670e8cc5bfe9edc0c5d624d4735421610cef5e808db93e7d8596952886"
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linuxarm64-gpl.tar.xz"
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048876"
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-arm64-musl"]
checksum = "sha256:bea03c670e8cc5bfe9edc0c5d624d4735421610cef5e808db93e7d8596952886"
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linuxarm64-gpl.tar.xz"
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048876"
[[tools."github:webassembly/binaryen"]]
version = "version_124"
backend = "github:webassembly/binaryen"
+12 -1
View File
@@ -13,6 +13,16 @@ if (keystorePropertiesFile.exists()) {
keystorePropertiesFile.withInputStream { keystoreProperties.load(it) }
}
def androidApplicationId = System.getenv("ANDROID_APPLICATION_ID")?.trim()
if (!androidApplicationId) {
androidApplicationId = "app.alextran.immich"
}
def androidAppLabel = System.getenv("ANDROID_APP_LABEL")?.trim()
if (!androidAppLabel) {
androidAppLabel = "Immich"
}
android {
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
@@ -37,11 +47,12 @@ android {
}
defaultConfig {
applicationId "app.alextran.immich"
applicationId androidApplicationId
minSdk = 26
targetSdk = flutter.targetSdkVersion
versionCode flutter.versionCode
versionName flutter.versionName
manifestPlaceholders = [appLabel: androidAppLabel]
}
signingConfigs {
@@ -25,7 +25,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<application android:label="Immich" android:name=".ImmichApp" android:usesCleartextTraffic="true"
<application android:label="${appLabel}" android:name=".ImmichApp" android:usesCleartextTraffic="true"
android:icon="@mipmap/ic_launcher" android:requestLegacyExternalStorage="true"
android:largeHeap="true" android:enableOnBackInvokedCallback="false" android:allowBackup="false"
android:networkSecurityConfig="@xml/network_security_config">
-4
View File
@@ -18490,7 +18490,6 @@
"properties": {
"cronExpression": {
"description": "Cron expression",
"pattern": "(((\\d+,)+\\d+|(\\d+(\\/|-)\\d+)|\\d+|\\*) ?){5,7}",
"type": "string"
},
"enabled": {
@@ -25677,7 +25676,6 @@
"properties": {
"cronExpression": {
"description": "Cron expression for when the integrity check should run",
"pattern": "(((\\d+,)+\\d+|(\\d+(\\/|-)\\d+)|\\d+|\\*) ?){5,7}",
"type": "string"
},
"enabled": {
@@ -25710,7 +25708,6 @@
"properties": {
"cronExpression": {
"description": "Cron expression for when the integrity check should run",
"pattern": "(((\\d+,)+\\d+|(\\d+(\\/|-)\\d+)|\\d+|\\*) ?){5,7}",
"type": "string"
},
"enabled": {
@@ -25810,7 +25807,6 @@
"properties": {
"cronExpression": {
"description": "Cron expression",
"pattern": "(((\\d+,)+\\d+|(\\d+(\\/|-)\\d+)|\\d+|\\*) ?){5,7}",
"type": "string"
},
"enabled": {
+2794 -2714
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -65,3 +65,5 @@ preferWorkspacePackages: true
injectWorkspacePackages: true
shamefullyHoist: false
verifyDepsBeforeRun: install
minimumReleaseAgeExclude:
- '@immich/ui@0.81.1'
+11 -1
View File
@@ -1,3 +1,4 @@
import { validateCronExpression } from 'cron';
import { createZodDto } from 'nestjs-zod';
import { SystemConfig } from 'src/config';
import {
@@ -43,7 +44,16 @@ const JobSettingsSchema = z
const cronExpressionSchema = z
.string()
.regex(/(((\d+,)+\d+|(\d+(\/|-)\d+)|\d+|\*) ?){5,7}/, 'Invalid cron expression')
.superRefine((value, ctx) => {
const validated = validateCronExpression(value);
if (!validated.valid) {
ctx.addIssue({
code: 'custom',
message: `Invalid cron expression. ${validated.error?.message ?? ''}`,
input: value,
});
}
})
.describe('Cron expression');
const DatabaseBackupSchema = z
+3 -3
View File
@@ -129,10 +129,10 @@ from
and "integrity_report"."type" = $1
where
"asset"."deletedAt" is null
and "createdAt" >= $2
and "createdAt" <= $3
and "integrity_report"."createdAt" >= $2
and "integrity_report"."createdAt" <= $3
order by
"createdAt" asc
"integrity_report"."createdAt" asc
-- IntegrityRepository.streamIntegrityReports
select
@@ -177,9 +177,9 @@ export class IntegrityRepository {
'asset.id as assetId',
'integrity_report.id as reportId',
])
.$if(startMarker !== undefined, (qb) => qb.where('createdAt', '>=', startMarker!))
.$if(endMarker !== undefined, (qb) => qb.where('createdAt', '<=', endMarker!))
.orderBy('createdAt', 'asc')
.$if(startMarker !== undefined, (qb) => qb.where('integrity_report.createdAt', '>=', startMarker!))
.$if(endMarker !== undefined, (qb) => qb.where('integrity_report.createdAt', '<=', endMarker!))
.orderBy('integrity_report.createdAt', 'asc')
.stream();
}
@@ -319,14 +319,14 @@ describe(SystemConfigService.name, () => {
it('should accept valid cron expressions', async () => {
mocks.config.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
mocks.systemMetadata.readFile.mockResolvedValue(
JSON.stringify({ library: { scan: { cronExpression: '0 0 * * *' } } }),
JSON.stringify({ library: { scan: { cronExpression: '0 0 */3 * *' } } }),
);
await expect(sut.getSystemConfig()).resolves.toMatchObject({
library: {
scan: {
enabled: true,
cronExpression: '0 0 * * *',
cronExpression: '0 0 */3 * *',
},
},
});
+1 -1
View File
@@ -27,7 +27,7 @@
"@formatjs/icu-messageformat-parser": "^3.0.0",
"@immich/justified-layout-wasm": "^0.4.3",
"@immich/sdk": "workspace:*",
"@immich/ui": "^0.80.0",
"@immich/ui": "^0.81.1",
"@mapbox/mapbox-gl-rtl-text": "0.4.0",
"@mdi/js": "^7.4.47",
"@noble/hashes": "^2.2.0",
@@ -208,13 +208,13 @@
if (relativeDate) {
const duration = Duration.fromISO(relativeDate);
return {
fileCreatedAfter: duration.isValid ? DateTime.now().minus(duration).toISO() : undefined,
fileCreatedAfter: duration.isValid ? DateTime.now().minus(duration).toUTC().toISO() : undefined,
};
}
return {
fileCreatedAfter: dateAfter?.toUTC().toISO(),
fileCreatedBefore: dateBefore?.toUTC().toISO(),
fileCreatedAfter: dateAfter,
fileCreatedBefore: dateBefore,
};
}
@@ -243,7 +243,7 @@
}
const handleSettingsClick = async () => {
const settings = await modalManager.show(MapSettingsModal, { settings: { ...$mapSettings } });
const settings = await modalManager.show(MapSettingsModal);
if (settings) {
const shouldUpdate = !isEqual(omit(settings, 'allowDarkMode'), omit($mapSettings, 'allowDarkMode'));
$mapSettings = settings;
+13 -7
View File
@@ -1,17 +1,16 @@
<script lang="ts">
import type { MapSettings } from '$lib/stores/preferences.store';
import { mapSettings, type MapSettings } from '$lib/stores/preferences.store';
import { Button, DatePicker, Field, FormModal, Select, Stack, Switch } from '@immich/ui';
import { Duration } from 'luxon';
import { DateTime, Duration } from 'luxon';
import { t } from 'svelte-i18n';
import { fly } from 'svelte/transition';
type Props = {
settings: MapSettings;
onClose: (settings?: MapSettings) => void;
};
let { settings: initialValues, onClose }: Props = $props();
let settings = $state(initialValues);
let { onClose }: Props = $props();
let settings = $state({ ...$mapSettings });
let customDateRange = $state(!!settings.dateAfter || !!settings.dateBefore);
@@ -41,10 +40,17 @@
{#if customDateRange}
<div in:fly={{ y: 10, duration: 200 }} class="flex flex-col gap-4">
<Field label={$t('date_after')}>
<DatePicker bind:value={settings.dateAfter} maxDate={settings.dateBefore} />
<DatePicker
value={DateTime.fromISO(settings.dateAfter ?? '')}
maxDate={DateTime.fromISO(settings.dateBefore ?? '')}
onChange={(date) => (settings.dateAfter = date?.toUTC().toISO() ?? undefined)}
/>
</Field>
<Field label={$t('date_before')}>
<DatePicker bind:value={settings.dateBefore} />
<DatePicker
value={DateTime.fromISO(settings.dateBefore ?? '')}
onChange={(date) => (settings.dateBefore = date?.toUTC().toISO() ?? undefined)}
/>
</Field>
<div class="flex justify-center">
<Button
+2 -3
View File
@@ -1,4 +1,3 @@
import type { DateTime } from 'luxon';
import { persisted } from 'svelte-persisted-store';
import { browser } from '$app/environment';
import { defaultLang } from '$lib/constants';
@@ -27,8 +26,8 @@ export interface MapSettings {
withPartners: boolean;
withSharedAlbums: boolean;
relativeDate: string;
dateAfter?: DateTime<true>;
dateBefore?: DateTime<true>;
dateAfter?: string;
dateBefore?: string;
}
const defaultMapSettings = {