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
142 changes: 142 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
name: Deploy

on:
workflow_call:
secrets:
SA_DEPLOYER_PRIVATE_KEY:
required: true
inputs:
revision_name:
type: string
required: true
sa_deployer_id:
description: Yandex Cloud service account Id
type: string
required: true
cloud_id:
description: Yandex Cloud Id
type: string
required: true
folder_name:
description: Yandex Cloud folder name
type: string
required: true
container_registry_id:
description: Yandex Cloud container registry id
type: string
required: true

jobs:
deploy:
timeout-minutes: 5
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Install Yandex Cloud dependencies
run: |
sudo apt-get update
sudo apt-get install -y bash curl jq gettext
# Install yq from GitHub releases (recommended on Ubuntu)
YQ_VERSION="v4.44.1"
sudo wget -q https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/yq_linux_amd64 -O /usr/local/bin/yq
sudo chmod +x /usr/local/bin/yq
# Install Yandex Cloud CLI
curl --fail -sSL -o install.sh https://storage.yandexcloud.net/yandexcloud-yc/install.sh
sudo bash install.sh -i /usr/local/yandex-cloud -n
sudo ln -s /usr/local/yandex-cloud/bin/yc /usr/local/bin/yc

# apk add yq --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community
# curl --fail -silent --location --remote-name https://storage.yandexcloud.net/yandexcloud-yc/install.sh
# bash install.sh -i /usr/local/yandex-cloud -n
# ln -s /usr/local/yandex-cloud/bin/yc /usr/local/bin/yc

- name: Create deployer private key json
id: create-json
uses: jsdaniell/create-json@1.1.2
with:
name: "key.json"
json: ${{ secrets.SA_DEPLOYER_PRIVATE_KEY }}

- name: Yandex Cloud Auth
run: |
yc config profile create sa-profile
yc config set service-account-key key.json

- name: Revision name
run: echo ${{ inputs.revision_name }}

- name: Create Docker Image
run: |
docker build -t web-react-app .
docker tag web-react-app cr.yandex/${{ inputs.container_registry_id }}/web-react-app:hello

- name: Create Container
id: create_container
run: |
# Try to get the container ID
container_info=$(yc serverless container get \
--name "${{ inputs.revision_name }}" \
--cloud-id "${{ inputs.cloud_id }}" \
--folder-name "${{ inputs.folder_name }}" \
--format json 2>/dev/null || true)

if [ -n "$container_info" ]; then
container_id=$(echo "$container_info" | yq .id)
echo "Container already exists with ID: $container_id"
else
export container_id=$(yc serverless container create \
--name ${{ inputs.revision_name }} \
--cloud-id ${{ inputs.cloud_id }} \
--folder-name "${{ inputs.folder_name }}" | yq .id)
echo "Created container with ID: $container_id"
fi

echo "container_id=$container_id" >> $GITHUB_OUTPUT

- name: Deploy Container Revision
run: |
yc serverless container revision deploy \
--container-name ${{ inputs.revision_name }} \
--image "cr.yandex/${{ inputs.container_registry_id }}/web-react-app:hello" \
--cores 1 \
--memory 512mb \
--concurrency 1 \
--execution-timeout 10s \
--cloud-id ${{ inputs.cloud_id }} \
--folder-name "${{ inputs.folder_name }}" \
--service-account-id ${{ inputs.sa_deployer_id }}

- name: Configure Api Gateway
run: |
export sa_id="${{ inputs.sa_deployer_id }}"
export container_id="${{ steps.create_container.outputs.container_id }}"
(cat apigw.yaml.j2 | envsubst) > apigw.yaml
cat apigw.yaml

# Try to get the api-gateway ID
gw_info=$(yc serverless api-gateway get \
--name ${{ inputs.revision_name }} \
--cloud-id ${{ inputs.cloud_id }} \
--folder-name "${{ inputs.folder_name }}"
--format json 2>/dev/null || true)

if [ -n "$gw_info" ]; then
gwdomain=$(echo "$gw_info" | yq .domain)
echo "API Gateway already exists with DOMAIN: $gwdomain"
else
yc serverless api-gateway create \
--name ${{ inputs.revision_name }} \
--spec=apigw.yaml \
--description "created from CI" \
--cloud-id ${{ inputs.cloud_id }} \
--folder-name "${{ inputs.folder_name }}"

gwdomain=$(yc serverless api-gateway get ${{ inputs.revision_name }} \
--cloud-id ${{ inputs.cloud_id }} \
--folder-name "${{ inputs.folder_name }}" | yq .domain)
echo "Created API Gateway with DOMAIN: $gwdomain"
fi

echo "https://"$gwdomain
17 changes: 17 additions & 0 deletions .github/workflows/pull-request-closed.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: Pull request (Closed)

on:
pull_request:
types: [ closed ]
workflow_dispatch:

jobs:
undeploy:
name: "Undeploy staging"
uses: ./.github/workflows/undeploy.yml
secrets:
SA_DEPLOYER_PRIVATE_KEY: ${{ secrets.SA_STAGING_DEPLOYER_PRIVATE_KEY }}
with:
revision_name: pr-${{ github.event.pull_request.number }}
cloud_id: b1g9utblfaj1k8am3nmg
folder_name: languages-learner
20 changes: 16 additions & 4 deletions .github/workflows/pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,22 @@ jobs:
name: "npm ci"
uses: ./.github/workflows/npm-ci.yml

build:
name: "Build"
needs: npm-ci
uses: ./.github/workflows/build.yml
# build:
# name: "Build"
# needs: npm-ci
# uses: ./.github/workflows/build.yml

deploy:
name: "Deploy staging"
uses: ./.github/workflows/deploy.yml
secrets:
SA_DEPLOYER_PRIVATE_KEY: ${{ secrets.SA_STAGING_DEPLOYER_PRIVATE_KEY }}
with:
revision_name: pr-${{ github.event.pull_request.number }}
cloud_id: b1g9utblfaj1k8am3nmg
folder_name: languages-learner
sa_deployer_id: ajelfqpn1n9hn128gsc4
container_registry_id: crp7sst4j2jnr7n52g3g

precommit-checks:
name: "Pre-commit checks"
Expand Down
92 changes: 92 additions & 0 deletions .github/workflows/undeploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
name: Undeploy

on:
workflow_call:
secrets:
SA_DEPLOYER_PRIVATE_KEY:
required: true
inputs:
revision_name:
type: string
required: true
cloud_id:
description: Yandex Cloud Id
type: string
required: true
folder_name:
description: Yandex Cloud folder name
type: string
required: true

jobs:
deploy:
timeout-minutes: 5
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Install Yandex Cloud dependencies
run: |
sudo apt-get update
sudo apt-get install -y bash curl jq gettext
# Install yq from GitHub releases (recommended on Ubuntu)
YQ_VERSION="v4.44.1"
sudo wget -q https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/yq_linux_amd64 -O /usr/local/bin/yq
sudo chmod +x /usr/local/bin/yq
# Install Yandex Cloud CLI
curl --fail -sSL -o install.sh https://storage.yandexcloud.net/yandexcloud-yc/install.sh
sudo bash install.sh -i /usr/local/yandex-cloud -n
sudo ln -s /usr/local/yandex-cloud/bin/yc /usr/local/bin/yc

- name: Create deployer private key json
id: create-json
uses: jsdaniell/create-json@1.1.2
with:
name: "key.json"
json: ${{ secrets.SA_DEPLOYER_PRIVATE_KEY }}

- name: Yandex Cloud Auth
run: |
yc config profile create sa-profile
yc config set service-account-key key.json

- name: Delete Api Gateway
run: |
# Try to get the api-gateway ID
gw_info=$(yc serverless api-gateway get \
--name ${{ inputs.revision_name }} \
--cloud-id ${{ inputs.cloud_id }} \
--folder-name "${{ inputs.folder_name }}"
--format json 2>/dev/null || true)

if [ -n "$gw_info" ]; then
yc serverless api-gateway delete \
--name ${{ inputs.revision_name }} \
--cloud-id ${{ inputs.cloud_id }} \
--folder-name "${{ inputs.folder_name }}"
echo "API Gateway ${{ inputs.revision_name }} deleted"
else
echo "API Gateway ${{ inputs.revision_name }} is not exist"
fi

- name: Delete Container
run: |
# Try to get the container ID
container_info=$(yc serverless container get \
--name "${{ inputs.revision_name }}" \
--cloud-id "${{ inputs.cloud_id }}" \
--folder-name "${{ inputs.folder_name }}" \
--format json 2>/dev/null || true)

if [ -n "$container_info" ]; then
yc serverless container delete \
--name "${{ inputs.revision_name }}" \
--cloud-id "${{ inputs.cloud_id }}" \
--folder-name "${{ inputs.folder_name }}"
echo "Container ${{ inputs.revision_name }} deleted"
else
echo "Container ${{ inputs.revision_name }} is not exist"
fi


2 changes: 2 additions & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export PRE_COMMIT=1

npx lint-staged
npm run i18n:manage
npm run sanitize-har
16 changes: 16 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
FROM node:20-alpine

WORKDIR /app

COPY package.json package-lock.json .npmrc ./
RUN npm ci

COPY . .

RUN npm run build

EXPOSE 8080

ENV NODE_ENV=production

CMD ["npm", "run", "preview"]
17 changes: 17 additions & 0 deletions apigw.yaml.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
openapi: 3.0.0
info:
title: Languages Learner PR Staging
version: 1.0.0
paths:
/{path+}:
x-yc-apigateway-any-method:
parameters:
- name: path
in: path
required: true
schema:
type: string
x-yc-apigateway-integration:
type: serverless_containers
container_id: $container_id
service_account_id: $sa_id
9 changes: 7 additions & 2 deletions scripts/manage-translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import process from "process";
import { type MessageDescriptor } from "react-intl";

// Configuration
const PRE_COMMIT = Boolean(process.env.PRE_COMMIT);
const LOCALES_DIR = path.relative(process.cwd(), path.resolve("src/locales"));
const EXTRACTED_FILE = path.join(LOCALES_DIR, "extracted.json");
const COMPILED_DIR = path.join(LOCALES_DIR, "compiled");
Expand All @@ -25,7 +26,9 @@ if (!fs.existsSync(COMPILED_DIR)) {
console.info("Extracting messages from source code...");
try {
execSync("npm run i18n:extract", { stdio: "inherit" });
execSync(`git add ${EXTRACTED_FILE}`, { stdio: "inherit" });
if (PRE_COMMIT) {
execSync(`git add ${EXTRACTED_FILE}`, { stdio: "inherit" });
}
console.info("✅ Messages extracted successfully");
} catch (error) {
console.error("❌ Failed to extract messages:", String(error));
Expand Down Expand Up @@ -87,7 +90,9 @@ for (const lang of LANGUAGES) {

// Write updated translations back to file
fs.writeFileSync(langFile, JSON.stringify(updatedTranslations, null, 2), "utf8");
execSync(`git add ${langFile}`, { stdio: "inherit" });
if (PRE_COMMIT) {
execSync(`git add ${langFile}`, { stdio: "inherit" });
}
if (newCount > 0) {
console.info(
`✅ Updated ${lang} translations (${existingCount} existing, ${newCount} new)`,
Expand Down
2 changes: 1 addition & 1 deletion src/server/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { userMiddleware } from "./middlewares/user";
dotenv.config();

const isProduction = process.env.NODE_ENV === "production";
const port = process.env.PORT || 5173;
const port = process.env.PORT || 8080;
const base = process.env.BASE || "/";

const createServer = async () => {
Expand Down
Loading