Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(release): automatic release v1.0.0 #1820

Merged
merged 25 commits into from
Dec 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
4943e7b
fix(deps): update dependency trpc-to-openapi to ^2.1.1 (#1788)
homarr-renovate[bot] Dec 27, 2024
c91aae5
fix(health-monitoring): add months to uptime, fix status update (#1792)
hillaliy Dec 27, 2024
fcab6a3
chore(lang): updated translations from crowdin
homarr-crowdin[bot] Dec 27, 2024
76abfca
chore(lang): updated translations from crowdin (#1786)
ajnart Dec 27, 2024
50746eb
chore(lang): updated translations from crowdin
homarr-crowdin[bot] Dec 28, 2024
abf7942
chore(lang): updated translations from crowdin
homarr-crowdin[bot] Dec 28, 2024
c8228b9
chore(lang): updated translations from crowdin (#1797)
ajnart Dec 28, 2024
5b04b93
chore(lang): updated translations from crowdin (#1802)
homarr-crowdin[bot] Dec 28, 2024
8f0945f
chore(lang): updated translations from crowdin (#1800)
ajnart Dec 28, 2024
6d90209
chore(deps): update pnpm to v9.15.2 (#1804)
homarr-renovate[bot] Dec 29, 2024
b52a8a7
chore(lang): updated translations from crowdin
homarr-crowdin[bot] Dec 29, 2024
75aefcb
fix(deps): update dependency octokit to ^4.0.3 (#1806)
homarr-renovate[bot] Dec 29, 2024
23c7d0b
fix(media-server): redesign widget (#1775)
hillaliy Dec 29, 2024
e8d2942
chore(lang): updated translations from crowdin
homarr-crowdin[bot] Dec 30, 2024
5cb726f
ci(docker): always push image (#1812)
Meierschlumpf Dec 30, 2024
eacda4c
fix(deps): update tiptap monorepo to v2.11.0 (#1813)
homarr-renovate[bot] Dec 30, 2024
598f004
fix(deps): update dependency typescript-eslint to ^8.19.0 (#1814)
homarr-renovate[bot] Dec 30, 2024
63786ee
chore(lang): updated translations from crowdin (#1803)
ajnart Dec 30, 2024
b8a155d
chore(lang): updated translations from crowdin
homarr-crowdin[bot] Dec 31, 2024
0ebf4bc
feat(widget): add minecraft server status widget (#1801)
Meierschlumpf Dec 31, 2024
e53b06e
chore(deps): update dependency concurrently to ^9.1.2 (#1818)
homarr-renovate[bot] Dec 31, 2024
91e1cef
feat: add request media (#1811)
manuel-rw Dec 31, 2024
56b57ad
fix(docker): replace anonymous docker volume with env variable for en…
Meierschlumpf Dec 31, 2024
aeb681a
feat(docker): add support for pgid and puid env variables (#1759)
Meierschlumpf Dec 31, 2024
f507645
feat(integration): add search engine creation (#1816)
manuel-rw Dec 31, 2024
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
13 changes: 8 additions & 5 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@
# This file will be committed to version control, so make sure not to have any secrets in it.
# If you are cloning this repo, create a copy of this file named `.env` and populate it with your secrets.

# The below secret is not used anywhere but required for Auth.js (Would encrypt JWTs and Mail hashes, both not used)
AUTH_SECRET="supersecret"

# The below secret is used to encrypt integration secrets in the database.
# It should be a 32-byte string, generated by running `openssl rand -hex 32` on Unix
# or starting the project without any (which will show a randomly generated one).
SECRET_ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000

# This is how you can use the sqlite driver:
DB_DRIVER='better-sqlite3'
DB_URL='FULL_PATH_TO_YOUR_SQLITE_DB_FILE'
Expand All @@ -20,11 +28,6 @@ DB_URL='FULL_PATH_TO_YOUR_SQLITE_DB_FILE'
# DB_PASSWORD='password'
# DB_NAME='name-of-database'


# You can generate the secret via 'openssl rand -base64 32' on Unix
# @see https://next-auth.js.org/configuration/options#secret
AUTH_SECRET='supersecret'

TURBO_TELEMETRY_DISABLED=1

# Configure logging to use winston logger
Expand Down
12 changes: 3 additions & 9 deletions .github/workflows/deployment-docker-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,6 @@ on:
required: false
default: true
description: Send notifications
push-image:
type: boolean
required: false
default: true
description: Push Docker Image

permissions:
contents: write
Expand Down Expand Up @@ -112,7 +107,6 @@ jobs:
NEXT_VERSION: ${{ needs.release.outputs.version }}
DEPLOY_LATEST: ${{ github.ref_name == 'main' }}
DEPLOY_BETA: ${{ github.ref_name == 'beta' }}
PUSH_IMAGE: ${{ github.event_name != 'workflow_dispatch' || github.events.inputs.push-image == true }}
steps:
- uses: actions/checkout@v4
with:
Expand Down Expand Up @@ -143,13 +137,13 @@ jobs:
${{ env.DEPLOY_LATEST == 'true' && 'type=raw,value=latest' || null }}
${{ env.DEPLOY_BETA == 'true' && 'type=raw,value=beta' || null }}
type=raw,value=${{ env.NEXT_VERSION }}
- name: Build and maybe push
- name: Build and push
id: buildPushAction
uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/arm64
context: .
push: ${{ env.PUSH_IMAGE }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
network: host
Expand All @@ -160,4 +154,4 @@ jobs:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@master
with:
args: "Deployment of image has completed for branch ${{ github.ref_name }}. Image ID is '${{ steps.buildPushAction.outputs.imageid }}'. ${{ env.PUSH_IMAGE == 'true' && '' || 'This was a dry run' }}"
args: "Deployment of image has completed for branch ${{ github.ref_name }}. Image ID is '${{ steps.buildPushAction.outputs.imageid }}'."
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,5 @@
"i18n-ally.localesPaths": [
"packages/translation/src/lang",
],
"i18n-ally.keystyle": "auto",
"i18n-ally.keystyle": "nested",
}
50 changes: 20 additions & 30 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -25,57 +25,47 @@
FROM base AS runner
WORKDIR /app

# gettext is required for envsubst
RUN apk add --no-cache redis nginx bash gettext su-exec
# gettext is required for envsubst, openssl for generating AUTH_SECRET, su-exec for running application as non-root
RUN apk add --no-cache redis nginx bash gettext su-exec openssl
RUN mkdir /appdata
VOLUME /appdata
RUN mkdir /secrets
VOLUME /secrets



RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

# Enable homarr cli
COPY --from=builder --chown=nextjs:nodejs /app/packages/cli/cli.cjs /app/apps/cli/cli.cjs
COPY --from=builder /app/packages/cli/cli.cjs /app/apps/cli/cli.cjs
RUN echo $'#!/bin/bash\ncd /app/apps/cli && node ./cli.cjs "$@"' > /usr/bin/homarr
RUN chmod +x /usr/bin/homarr

# Don't run production as root
RUN chown -R nextjs:nodejs /secrets
RUN mkdir -p /var/cache/nginx && chown -R nextjs:nodejs /var/cache/nginx && \
mkdir -p /var/log/nginx && chown -R nextjs:nodejs /var/log/nginx && \
mkdir -p /var/lib/nginx && chown -R nextjs:nodejs /var/lib/nginx && \
touch /run/nginx/nginx.pid && chown -R nextjs:nodejs /run/nginx/nginx.pid && \
mkdir -p /etc/nginx/templates /etc/nginx/ssl/certs && chown -R nextjs:nodejs /etc/nginx
RUN mkdir -p /var/cache/nginx && \
mkdir -p /var/log/nginx && \
mkdir -p /var/lib/nginx && \
touch /run/nginx/nginx.pid && \
mkdir -p /etc/nginx/templates /etc/nginx/ssl/certs

COPY --from=builder /app/apps/nextjs/next.config.mjs .
COPY --from=builder /app/apps/nextjs/package.json .

COPY --from=builder --chown=nextjs:nodejs /app/apps/tasks/tasks.cjs ./apps/tasks/tasks.cjs
COPY --from=builder --chown=nextjs:nodejs /app/apps/websocket/wssServer.cjs ./apps/websocket/wssServer.cjs
COPY --from=builder --chown=nextjs:nodejs /app/node_modules/better-sqlite3/build/Release/better_sqlite3.node /app/build/better_sqlite3.node
COPY --from=builder /app/apps/tasks/tasks.cjs ./apps/tasks/tasks.cjs
COPY --from=builder /app/apps/websocket/wssServer.cjs ./apps/websocket/wssServer.cjs
COPY --from=builder /app/node_modules/better-sqlite3/build/Release/better_sqlite3.node /app/build/better_sqlite3.node

COPY --from=builder --chown=nextjs:nodejs /app/packages/db/migrations ./db/migrations
COPY --from=builder /app/packages/db/migrations ./db/migrations

# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/apps/nextjs/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/apps/nextjs/.next/static ./apps/nextjs/.next/static
COPY --from=builder --chown=nextjs:nodejs /app/apps/nextjs/public ./apps/nextjs/public
COPY --chown=nextjs:nodejs scripts/run.sh ./run.sh
COPY scripts/entrypoint.sh ./entrypoint.sh
RUN chmod +x ./entrypoint.sh
COPY --chown=nextjs:nodejs scripts/generateRandomSecureKey.js ./generateRandomSecureKey.js
COPY --chown=nextjs:nodejs packages/redis/redis.conf /app/redis.conf
COPY --chown=nextjs:nodejs nginx.conf /etc/nginx/templates/nginx.conf
COPY --from=builder /app/apps/nextjs/.next/standalone ./
COPY --from=builder /app/apps/nextjs/.next/static ./apps/nextjs/.next/static
COPY --from=builder /app/apps/nextjs/public ./apps/nextjs/public
COPY scripts/run.sh ./run.sh
COPY --chmod=777 scripts/entrypoint.sh ./entrypoint.sh
COPY packages/redis/redis.conf /app/redis.conf
COPY nginx.conf /etc/nginx/templates/nginx.conf


ENV DB_URL='/appdata/db/db.sqlite'
ENV DB_DIALECT='sqlite'
ENV DB_DRIVER='better-sqlite3'
ENV AUTH_PROVIDERS='credentials'

Check warning on line 68 in Dockerfile

View workflow job for this annotation

GitHub Actions / Deploy docker image

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "AUTH_PROVIDERS") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/

ENTRYPOINT [ "/app/entrypoint.sh" ]
CMD ["sh", "run.sh"]
CMD ["sh", "run.sh"]
1 change: 1 addition & 0 deletions apps/nextjs/next.config.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Importing env files here to validate on build
import "@homarr/auth/env.mjs";
import "@homarr/db/env.mjs";
import "@homarr/common/env.mjs";

import MillionLint from "@million/lint";
import createNextIntlPlugin from "next-intl/plugin";
Expand Down
2 changes: 1 addition & 1 deletion apps/nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@
"@types/react": "^19.0.2",
"@types/react-dom": "^19.0.2",
"@types/swagger-ui-react": "^4.18.3",
"concurrently": "^9.1.1",
"concurrently": "^9.1.2",
"eslint": "^9.17.0",
"node-loader": "^2.1.0",
"prettier": "^3.4.2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
import { useCallback } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { Alert, Button, Fieldset, Group, SegmentedControl, Stack, Text, TextInput } from "@mantine/core";
import { Alert, Button, Checkbox, Fieldset, Group, SegmentedControl, Stack, Text, TextInput } from "@mantine/core";
import { IconInfoCircle } from "@tabler/icons-react";

import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
import type { IntegrationKind, IntegrationSecretKind } from "@homarr/definitions";
import { getAllSecretKindOptions, getIntegrationName } from "@homarr/definitions";
import { getAllSecretKindOptions, getIntegrationName, integrationDefs } from "@homarr/definitions";
import type { UseFormReturnType } from "@homarr/form";
import { useZodForm } from "@homarr/form";
import { convertIntegrationTestConnectionError } from "@homarr/integrations/client";
Expand Down Expand Up @@ -38,6 +38,7 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
kind,
value: "",
})),
attemptSearchEngineCreation: true,
},
});
const { mutateAsync, isPending } = clientApi.integration.create.useMutation();
Expand Down Expand Up @@ -78,6 +79,8 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
);
};

const supportsSearchEngine = integrationDefs[searchParams.kind].category.flat().includes("search");

return (
<form onSubmit={form.onSubmit((value) => void handleSubmitAsync(value))}>
<Stack>
Expand All @@ -104,6 +107,16 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
</Stack>
</Fieldset>

{supportsSearchEngine && (
<Checkbox
label={t("integration.field.attemptSearchEngineCreation.label")}
description={t("integration.field.attemptSearchEngineCreation.description", {
kind: getIntegrationName(searchParams.kind),
})}
{...form.getInputProps("attemptSearchEngineCreation", { type: "checkbox" })}
/>
)}

<Group justify="end" align="center">
<Button variant="default" component={Link} href="/manage/integrations">
{t("common.action.backToOverview")}
Expand Down
21 changes: 18 additions & 3 deletions e2e/shared/create-homarr-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,22 @@ export const createHomarrContainer = () => {
throw new Error("This test should only be run in CI or with a homarr image named 'homarr-e2e'");
}

return new GenericContainer("homarr-e2e")
.withExposedPorts(7575)
.withWaitStrategy(Wait.forHttp("/api/health/ready", 7575));
return withLogs(
new GenericContainer("homarr-e2e")
.withExposedPorts(7575)
.withEnvironment({
SECRET_ENCRYPTION_KEY: "0".repeat(64),
})
.withWaitStrategy(Wait.forHttp("/api/health/ready", 7575)),
);
};

export const withLogs = (container: GenericContainer) => {
container.withLogConsumer((stream) =>
stream
.on("data", (line) => console.log(line))
.on("err", (line) => console.error(line))
.on("end", () => console.log("Stream closed")),
);
return container;
};
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^2.1.8"
},
"packageManager": "[email protected].1",
"packageManager": "[email protected].2",
"engines": {
"node": ">=22.12.0"
},
Expand Down
3 changes: 2 additions & 1 deletion packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,11 @@
"@trpc/react-query": "next",
"@trpc/server": "next",
"dockerode": "^4.0.2",
"lodash.clonedeep": "^4.5.0",
"next": "^14.2.22",
"react": "^19.0.0",
"superjson": "2.2.2",
"trpc-to-openapi": "^2.1.0"
"trpc-to-openapi": "^2.1.1"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
Expand Down
44 changes: 44 additions & 0 deletions packages/api/src/router/integration/integration-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ import {
integrations,
integrationSecrets,
integrationUserPermissions,
searchEngines,
} from "@homarr/db/schema";
import type { IntegrationSecretKind } from "@homarr/definitions";
import {
getIconUrl,
getIntegrationKindsByCategory,
getPermissionsWithParents,
integrationDefs,
Expand Down Expand Up @@ -192,6 +194,18 @@ export const integrationRouter = createTRPCRouter({
})),
);
}

if (input.attemptSearchEngineCreation) {
const icon = getIconUrl(input.kind);
await ctx.db.insert(searchEngines).values({
id: createId(),
name: input.name,
integrationId,
type: "fromIntegration",
iconUrl: icon,
short: await getNextValidShortNameForSearchEngineAsync(ctx.db, input.name),
});
}
}),
update: protectedProcedure.input(validation.integration.update).mutation(async ({ ctx, input }) => {
await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.id), "full");
Expand Down Expand Up @@ -411,6 +425,36 @@ interface AddSecretInput {
value: string;
kind: IntegrationSecretKind;
}

const getNextValidShortNameForSearchEngineAsync = async (db: Database, integrationName: string) => {
const searchEngines = await db.query.searchEngines.findMany({
columns: {
short: true,
},
});

const usedShortNames = searchEngines.flatMap((searchEngine) => searchEngine.short.toLowerCase());
const nameByIntegrationName = integrationName.slice(0, 1).toLowerCase();

if (!usedShortNames.includes(nameByIntegrationName)) {
return nameByIntegrationName;
}

// 8 is max length constraint
for (let i = 2; i < 9999999; i++) {
const generatedName = `${nameByIntegrationName}${i}`;
if (usedShortNames.includes(generatedName)) {
continue;
}

return generatedName;
}

throw new Error(
"Unable to automatically generate a short name. All possible variations were exhausted. Please disable the automatic creation and choose one later yourself.",
);
};

const addSecretAsync = async (db: Database, input: AddSecretInput) => {
await db.insert(integrationSecrets).values({
kind: input.kind,
Expand Down
25 changes: 25 additions & 0 deletions packages/api/src/router/search-engine/search-engine-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import { TRPCError } from "@trpc/server";

import { createId, eq, like, sql } from "@homarr/db";
import { searchEngines } from "@homarr/db/schema";
import { integrationCreator } from "@homarr/integrations";
import { validation } from "@homarr/validation";

import { createOneIntegrationMiddleware } from "../../middlewares/integration";
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure } from "../../trpc";

export const searchEngineRouter = createTRPCRouter({
Expand Down Expand Up @@ -56,9 +58,32 @@ export const searchEngineRouter = createTRPCRouter({
search: protectedProcedure.input(validation.common.search).query(async ({ ctx, input }) => {
return await ctx.db.query.searchEngines.findMany({
where: like(searchEngines.short, `${input.query.toLowerCase().trim()}%`),
with: {
integration: {
columns: {
kind: true,
url: true,
id: true,
},
},
},
limit: input.limit,
});
}),
getMediaRequestOptions: protectedProcedure
.unstable_concat(createOneIntegrationMiddleware("query", "jellyseerr", "overseerr"))
.input(validation.common.mediaRequestOptions)
.query(async ({ ctx, input }) => {
const integration = integrationCreator(ctx.integration);
return await integration.getSeriesInformationAsync(input.mediaType, input.mediaId);
}),
requestMedia: protectedProcedure
.unstable_concat(createOneIntegrationMiddleware("interact", "jellyseerr", "overseerr"))
.input(validation.common.requestMedia)
.mutation(async ({ ctx, input }) => {
const integration = integrationCreator(ctx.integration);
return await integration.requestMediaAsync(input.mediaType, input.mediaId, input.seasons);
}),
create: permissionRequiredProcedure
.requiresPermission("search-engine-create")
.input(validation.searchEngine.manage)
Expand Down
Loading
Loading