Skip to content
This repository was archived by the owner on Nov 3, 2025. It is now read-only.
Open
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
1 change: 1 addition & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- @azure Rule - Use Azure Best Practices: When generating code for Azure, running terminal commands for Azure, or performing operations related to Azure, invoke your `azure_development-get_best_practices` tool if available.
92 changes: 50 additions & 42 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,44 +1,52 @@
# ------------------------------
# Stage 1: Build the application
# 1) Frontend Build (Remix)
# ------------------------------
FROM node:20-alpine AS builder

# Definimos NODE_ENV=development para garantir que devDependencies sejam instaladas
ENV NODE_ENV=development
WORKDIR /app

# Copia somente os arquivos de pacote, para aproveitar cache de build
COPY package*.json ./
COPY server/package*.json ./server/

# Instala todas as dependências (prod + dev)
RUN npm install

# Copia todo o restante do código
COPY . .

# Executa o build (Remix + Tailwind + etc.)
RUN npm run build:all

# ------------------------------
# Stage 2: Runtime
# ------------------------------
FROM node:20-alpine AS runtime

ENV NODE_ENV=production
ENV PORT=80
WORKDIR /app

# Copiamos os package.jsons para instalar apenas as dependências de produção
COPY package*.json ./
COPY server/package*.json ./server/
RUN npm install --omit=dev

# Copiamos do “builder” somente os artefatos gerados e o que for preciso
COPY --from=builder /app/build ./build
COPY --from=builder /app/server/dist ./server/dist
COPY --from=builder /app/public ./public

EXPOSE 80
CMD [ "node", "server/dist/index.js" ]

FROM node:20-alpine AS frontend
WORKDIR /app

COPY package*.json ./
RUN npm install

COPY tsconfig.json remix.config.js vite.config.ts ./
COPY app ./app
COPY public ./public

# Build do Frontend - Remix
RUN npm run build

# ------------------------------
# 2) Backend Build (Express + TS)
# ------------------------------
FROM node:20-alpine AS backend
WORKDIR /server

COPY server/. .

RUN npm install \
&& npm run clean \
&& npm run build

# ------------------------------
# 3) Image Production
# ------------------------------

FROM node:20-alpine AS runtime
WORKDIR /app

ENV NODE_ENV=production
ENV PORT=80

COPY package.json ./
COPY server/package.json ./server/
RUN npm install --omit=dev \
&& cd server && npm install --omit=dev

# Frontend
COPY --from=frontend /app/build ./build
COPY --from=frontend /app/public ./public

# Backend
COPY --from=backend /server/dist ./server/dist

EXPOSE 80
CMD ["node", "server/dist/index.js"]
8 changes: 4 additions & 4 deletions app/routes/generate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Form, useActionData, useNavigation } from '@remix-run/react';
import { useEffect, useState, Suspense, lazy } from 'react';
import ToneSelector from '~/components/ToneSelector';
import EnhancedTextInput from '~/components/EnhancedTextInput';
import { azureOpenAIService } from '../../lib/services/openai-service';
import { azureOpenAIService } from '../services/openai-service.server';

const PreviewCard = lazy(() => import('~/components/PreviewCard'));
const SuccessNotification = lazy(
Expand All @@ -27,8 +27,8 @@ type ActionData = {

export const action: ActionFunction = async ({ request }) => {
const formData = await request.formData();
const topic = formData.get('topic');
const tone = formData.get('tone');
const topic = formData.get('topic') as string;
const tone = formData.get('tone') as string;
const keywords = formData.get('keywords');

if (!topic || typeof topic !== 'string') {
Expand All @@ -43,7 +43,7 @@ export const action: ActionFunction = async ({ request }) => {
try {
const generatedContent = await azureOpenAIService.generateMicroblogContent(
topic,
tone?.toString() || 'casual',
tone || 'casual',
keywords?.toString()
);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import {
ChatCompletionSystemMessageParam,
ChatCompletionUserMessageParam
} from "openai/resources/index.mjs";

import { AzureOpenAI } from "openai";
/*import type {
ChatCompletionSystemMessageParam,
ChatCompletionUserMessageParam,
ChatCompletionAssistantMessageParam
} from "openai/resources/index.mjs";*/
import type {
ChatCompletionMessageParam,
ChatCompletionSystemMessageParam,
ChatCompletionUserMessageParam,
} from "openai/resources/chat/completions";
import "dotenv/config";

interface GeneratedContent {
Expand All @@ -18,9 +25,11 @@ interface ToneGuidelines {
class AzureOpenAIService {
private client: AzureOpenAI;
private readonly toneGuidelines: ToneGuidelines;
private readonly deploymentName: string;

constructor() {
this.validateEnvVariables();
this.deploymentName = process.env.AZURE_OPENAI_DEPLOYMENT_NAME!;
Copy link

Copilot AI Apr 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The AZURE_OPENAI_DEPLOYMENT_NAME environment variable is used with a non-null assertion without prior validation. Consider adding an explicit check (similar to AZURE_OPENAI_API_KEY) to ensure it is defined.

Suggested change
this.deploymentName = process.env.AZURE_OPENAI_DEPLOYMENT_NAME!;
this.deploymentName = process.env.AZURE_OPENAI_DEPLOYMENT_NAME;

Copilot uses AI. Check for mistakes.

this.client = new AzureOpenAI({
apiKey: process.env.AZURE_OPENAI_API_KEY,
Expand Down Expand Up @@ -52,8 +61,8 @@ class AzureOpenAIService {
};

const result = await this.client.chat.completions.create({
messages: [systemMessage, userMessage],
model: "", // The model is specified in the deployment
model: this.deploymentName,
messages: [systemMessage, userMessage] as ChatCompletionMessageParam[],
temperature: 0.7,
max_tokens: 500,
response_format: { type: "json_object" },
Expand Down
3 changes: 2 additions & 1 deletion infra/abbreviations.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@
"keyVaultVaults": "kv",
"managedIdentityUserAssignedIdentities": "id",
"cognitiveServicesAccounts": "cog",
"portalDashboards": "dash"
"portalDashboards": "dash",
"networkVirtualNetworks": "vnet"
}
127 changes: 71 additions & 56 deletions infra/app/microblog-app.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,12 @@ param containerAppsEnvironmentName string
param applicationInsightsName string
param exists bool = false

// Key Vault parameters (maintained for future use)
// Key Vault parameters
param keyVaultName string
param openAiApiKeySecretName string = 'AZURE-OPENAI-API-KEY'
param openAiEndpointSecretName string = 'AZURE-OPENAI-ENDPOINT'

// Direct credential parameters
@secure()
param azureOpenAIApiKey string = ''
@secure()
param azureOpenAIEndpoint string = ''

@description('Whether the deployment is running on GitHub Actions')
param runningOnGh string = ''

@description('Id of the user or app to assign application roles')
param principalId string = ''
param openAiApiKeySecretName string = 'azure-openai-api-key'
param openAiEndpointSecretName string = 'azure-openai-endpoint'
param openAiDeploymentNameSecretName string = 'azure-openai-deployment-name'
param openAiApiVersionSecretName string = 'azure-openai-api-version'

@secure()
param appDefinition object
Expand Down Expand Up @@ -68,7 +58,7 @@ module acrPullRole '../shared/role.bicep' = {
params: {
principalId: identity.properties.principalId
// AcrPull role definition ID
roleDefinitionId: '7f951dda-4ed3-4680-a7ca-43fe172d538d'
roleDefinitionId: '7f951dda-4ed3-4680-a7ca-43fe172d538d'
principalType: 'ServicePrincipal'
}
}
Expand Down Expand Up @@ -97,11 +87,11 @@ module fetchLatestImage '../modules/fetch-container-image.bicep' = {
resource app 'Microsoft.App/containerApps@2023-05-02-preview' = {
name: name
location: location
tags: union(tags, {'azd-service-name': 'microblog-ai-remix'})
tags: union(tags, { 'azd-service-name': 'microblog-ai-remix' })
dependsOn: [acrPullRole, kvRoleAssignment]
identity: {
type: 'SystemAssigned,UserAssigned'
userAssignedIdentities: {'${identity.id}': {}}
userAssignedIdentities: { '${identity.id}': {} }
}
properties: {
managedEnvironmentId: containerAppsEnvironment.id
Expand All @@ -117,50 +107,75 @@ resource app 'Microsoft.App/containerApps@2023-05-02-preview' = {
identity: identity.id
}
]
secrets: union([
// Direct value secrets
{
name: 'azure-openai-api-key'
value: !empty(azureOpenAIApiKey) ? azureOpenAIApiKey : 'placeholder-value'
}
{
name: 'azure-openai-endpoint'
value: !empty(azureOpenAIEndpoint) ? azureOpenAIEndpoint : 'https://placeholder-endpoint.openai.azure.com'
}
], map(secrets, secret => {
name: secret.secretRef
value: secret.value
}))
secrets: union(
[
// Key Vault referenced secrets
{
name: 'azure-openai-api-key'
keyVaultUrl: '${keyVault.properties.vaultUri}secrets/${openAiApiKeySecretName}'
identity: identity.id
}
{
name: 'azure-openai-endpoint'
keyVaultUrl: '${keyVault.properties.vaultUri}secrets/${openAiEndpointSecretName}'
identity: identity.id
}
{
name: 'azure-openai-deployment-name'
keyVaultUrl: '${keyVault.properties.vaultUri}secrets/${openAiDeploymentNameSecretName}'
identity: identity.id
}
{
name: 'azure-openai-api-version'
keyVaultUrl: '${keyVault.properties.vaultUri}secrets/${openAiApiVersionSecretName}'
identity: identity.id
}
],
map(secrets, secret => {
name: secret.secretRef
value: secret.value
})
)
}
template: {
containers: [
{
image: fetchLatestImage.outputs.?containers[?0].?image ?? 'mcr.microsoft.com/hello-world:latest'
name: 'main'
env: union([
{
name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
value: applicationInsights.properties.ConnectionString
}
{
name: 'PORT'
value: '80'
}
// Environment variables referencing secrets
{
name: 'AZURE_OPENAI_API_KEY'
secretRef: 'azure-openai-api-key'
}
{
name: 'AZURE_OPENAI_ENDPOINT'
secretRef: 'azure-openai-endpoint'
}
],
env,
map(secrets, secret => {
name: secret.name
secretRef: secret.secretRef
}))
env: union(
[
{
name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
value: applicationInsights.properties.ConnectionString
}
{
name: 'PORT'
value: '80'
}
// Environment variables referencing secrets
{
name: 'AZURE_OPENAI_API_KEY'
secretRef: 'azure-openai-api-key'
}
{
name: 'AZURE_OPENAI_ENDPOINT'
secretRef: 'azure-openai-endpoint'
}
{
name: 'AZURE_OPENAI_DEPLOYMENT_NAME'
secretRef: 'azure-openai-deployment-name'
}
{
name: 'AZURE_OPENAI_API_VERSION'
secretRef: 'azure-openai-api-version'
}
],
env,
map(secrets, secret => {
name: secret.name
secretRef: secret.secretRef
})
)
resources: {
cpu: json('1.0')
memory: '2.0Gi'
Expand Down
Loading