From f27ba7ccb6e477e445ab7e43baa036fe979b2959 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20J=C3=A4derberg?= Date: Tue, 16 Dec 2025 11:42:51 +0000 Subject: [PATCH] Merged in chore/release-pipeline (pull request #3352) Add scripts for handling deployments Approved-by: Linus Flood --- README.md | 1 + apps/partner-sas/env/client.ts | 2 + apps/partner-sas/env/server.ts | 2 + apps/partner-sas/instrumentation-client.ts | 1 + apps/partner-sas/instrumentation.ts | 1 + apps/partner-sas/netlify.toml | 6 +- .../components/EnvironmentWatermark/index.tsx | 10 +- apps/scandic-web/env/client.ts | 2 + apps/scandic-web/env/server.ts | 2 + apps/scandic-web/instrumentation-client.ts | 1 + apps/scandic-web/instrumentation.ts | 1 + apps/scandic-web/netlify.toml | 6 +- docs/deploy.md | 29 ++++ package.json | 113 ++++++++-------- scripts/deploy/deploy.ts | 128 ++++++++++++++++++ 15 files changed, 242 insertions(+), 63 deletions(-) create mode 100644 docs/deploy.md create mode 100644 scripts/deploy/deploy.ts diff --git a/README.md b/README.md index f4adfa9f1..fd3cda493 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ For more details see the respective apps and packages' README files. ## More documentation +- [Deploy](./docs/deploy.md) - [Icons](./docs/icons.md) - [Payment](./docs/payment.md) - [Translations (i18n)](./docs/translations.md) diff --git a/apps/partner-sas/env/client.ts b/apps/partner-sas/env/client.ts index 59a78c8d2..21e43e577 100644 --- a/apps/partner-sas/env/client.ts +++ b/apps/partner-sas/env/client.ts @@ -5,11 +5,13 @@ export const env = createEnv({ client: { NEXT_PUBLIC_SENTRY_ENVIRONMENT: z.string().default("development"), NEXT_PUBLIC_SENTRY_CLIENT_SAMPLERATE: z.coerce.number().default(0.001), + NEXT_PUBLIC_RELEASE_TAG: z.string().optional(), }, emptyStringAsUndefined: true, runtimeEnv: { NEXT_PUBLIC_SENTRY_ENVIRONMENT: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT, NEXT_PUBLIC_SENTRY_CLIENT_SAMPLERATE: process.env.NEXT_PUBLIC_SENTRY_CLIENT_SAMPLERATE, + NEXT_PUBLIC_RELEASE_TAG: process.env.NEXT_PUBLIC_RELEASE_TAG, }, }) diff --git a/apps/partner-sas/env/server.ts b/apps/partner-sas/env/server.ts index 38c40213a..ffb776bdf 100644 --- a/apps/partner-sas/env/server.ts +++ b/apps/partner-sas/env/server.ts @@ -36,6 +36,7 @@ export const env = createEnv({ .refine((s) => s === "true" || s === "false") .transform((s) => s === "true") .default("false"), + RELEASE_TAG: z.string().optional(), }, emptyStringAsUndefined: true, runtimeEnv: { @@ -52,5 +53,6 @@ export const env = createEnv({ SENTRY_ENVIRONMENT: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT, SENTRY_SERVER_SAMPLERATE: process.env.SENTRY_SERVER_SAMPLERATE, REDEMPTION_ENABLED: process.env.REDEMPTION_ENABLED, + RELEASE_TAG: process.env.NEXT_PUBLIC_RELEASE_TAG, }, }) diff --git a/apps/partner-sas/instrumentation-client.ts b/apps/partner-sas/instrumentation-client.ts index 7348bdd5e..9efb6f6ba 100644 --- a/apps/partner-sas/instrumentation-client.ts +++ b/apps/partner-sas/instrumentation-client.ts @@ -12,6 +12,7 @@ Sentry.init({ denyUrls: denyUrls, // Disable logs for clients, will probably give us too much noise enableLogs: false, + release: env.NEXT_PUBLIC_RELEASE_TAG || undefined, beforeSendLog(log) { const ignoredLevels: (typeof log.level)[] = ["debug", "trace", "info"] if (ignoredLevels.includes(log.level)) { diff --git a/apps/partner-sas/instrumentation.ts b/apps/partner-sas/instrumentation.ts index 021dc2e19..143576eb2 100644 --- a/apps/partner-sas/instrumentation.ts +++ b/apps/partner-sas/instrumentation.ts @@ -21,5 +21,6 @@ async function configureSentry() { tracesSampleRate: env.SENTRY_SERVER_SAMPLERATE, denyUrls: denyUrls, enableLogs: true, + release: env.RELEASE_TAG || undefined, }) } diff --git a/apps/partner-sas/netlify.toml b/apps/partner-sas/netlify.toml index dff2ac5b2..dbf98833e 100644 --- a/apps/partner-sas/netlify.toml +++ b/apps/partner-sas/netlify.toml @@ -1,14 +1,14 @@ [build] -command = "yarn test --filter=@scandic-hotels/partner-sas && yarn build:sas" +command = "export NEXT_PUBLIC_RELEASE_TAG=$(git tag -l 'release-v*' | sort -V | tail -n 1) && yarn test --filter=@scandic-hotels/partner-sas && yarn build:sas" publish = "apps/partner-sas/.next" ignore = "if [ -z ${CACHED_COMMIT_REF+x} ] ; then echo 'no CACHED_COMMIT_REF found' && false ; else git diff --quiet $CACHED_COMMIT_REF $COMMIT_REF apps/partner-sas packages/booking-flow packages/common packages/trpc packages/design-system packages/typescript-config ; fi" [context.branch-deploy] -command = "yarn test --filter=@scandic-hotels/partner-sas && yarn build:sas" +command = "export NEXT_PUBLIC_RELEASE_TAG=$(git tag -l 'release-v*' | sort -V | tail -n 1) && yarn test --filter=@scandic-hotels/partner-sas && yarn build:sas" [context.deploy-preview] -command = "yarn test --filter=@scandic-hotels/partner-sas && yarn build:sas" +command = "export NEXT_PUBLIC_RELEASE_TAG=$(git tag -l 'release-v*' | sort -V | tail -n 1) && yarn test --filter=@scandic-hotels/partner-sas && yarn build:sas" [build.environment] # set TERM variable for terminal output diff --git a/apps/scandic-web/components/EnvironmentWatermark/index.tsx b/apps/scandic-web/components/EnvironmentWatermark/index.tsx index 0c5b703d0..933ab0455 100644 --- a/apps/scandic-web/components/EnvironmentWatermark/index.tsx +++ b/apps/scandic-web/components/EnvironmentWatermark/index.tsx @@ -27,11 +27,17 @@ export function EnvironmentWatermark() { } const { environment, name } = getEnvironmentName() - const displayValue = name === environment ? name : `${name} (${environment})` + const displayValues = [name] + if (name !== environment) { + displayValues.push(`(${environment})`) + } + if (env.NEXT_PUBLIC_RELEASE_TAG) { + displayValues.push(`[${env.NEXT_PUBLIC_RELEASE_TAG}]`) + } return (
- {displayValue} + {displayValues.join(" ")}
) } diff --git a/apps/scandic-web/env/client.ts b/apps/scandic-web/env/client.ts index f673282a0..58f1e2603 100644 --- a/apps/scandic-web/env/client.ts +++ b/apps/scandic-web/env/client.ts @@ -8,6 +8,7 @@ export const env = createEnv({ NEXT_PUBLIC_SENTRY_ENVIRONMENT: z.string().default("development"), NEXT_PUBLIC_SENTRY_CLIENT_SAMPLERATE: z.coerce.number().default(0.001), NEXT_PUBLIC_PUBLIC_URL: z.string().optional(), + NEXT_PUBLIC_RELEASE_TAG: z.string().optional(), }, emptyStringAsUndefined: true, runtimeEnv: { @@ -17,5 +18,6 @@ export const env = createEnv({ NEXT_PUBLIC_SENTRY_CLIENT_SAMPLERATE: process.env.NEXT_PUBLIC_SENTRY_CLIENT_SAMPLERATE, NEXT_PUBLIC_PUBLIC_URL: process.env.NEXT_PUBLIC_PUBLIC_URL, + NEXT_PUBLIC_RELEASE_TAG: process.env.NEXT_PUBLIC_RELEASE_TAG, }, }) diff --git a/apps/scandic-web/env/server.ts b/apps/scandic-web/env/server.ts index 492ce67c6..3edabcb44 100644 --- a/apps/scandic-web/env/server.ts +++ b/apps/scandic-web/env/server.ts @@ -112,6 +112,7 @@ export const env = createEnv({ .refine((s) => s === "true" || s === "false") .transform((s) => s === "true") .default("false"), + RELEASE_TAG: z.string().optional(), }, emptyStringAsUndefined: true, runtimeEnv: { @@ -168,5 +169,6 @@ export const env = createEnv({ NEW_STAYS_ON_MY_PAGES: process.env.NEW_STAYS_ON_MY_PAGES, SEO_INERT: process.env.SEO_INERT, ENABLE_PROFILE_CONSENT: process.env.ENABLE_PROFILE_CONSENT, + RELEASE_TAG: process.env.NEXT_PUBLIC_RELEASE_TAG, }, }) diff --git a/apps/scandic-web/instrumentation-client.ts b/apps/scandic-web/instrumentation-client.ts index 7a8277a4f..75d73d01e 100644 --- a/apps/scandic-web/instrumentation-client.ts +++ b/apps/scandic-web/instrumentation-client.ts @@ -12,6 +12,7 @@ Sentry.init({ denyUrls: denyUrls, // Disable logs for clients, will probably give us too much noise enableLogs: false, + release: env.NEXT_PUBLIC_RELEASE_TAG || undefined, beforeSendLog(log) { const ignoredLevels: (typeof log.level)[] = ["debug", "trace", "info"] if (ignoredLevels.includes(log.level)) { diff --git a/apps/scandic-web/instrumentation.ts b/apps/scandic-web/instrumentation.ts index 70cd4c263..fe6b467ac 100644 --- a/apps/scandic-web/instrumentation.ts +++ b/apps/scandic-web/instrumentation.ts @@ -22,5 +22,6 @@ async function configureSentry() { denyUrls: denyUrls, enableLogs: true, enableMetrics: true, + release: env.RELEASE_TAG || undefined, }) } diff --git a/apps/scandic-web/netlify.toml b/apps/scandic-web/netlify.toml index 693e1a5bb..70d95f978 100644 --- a/apps/scandic-web/netlify.toml +++ b/apps/scandic-web/netlify.toml @@ -1,14 +1,14 @@ [build] -command = "yarn test --filter=@scandic-hotels/scandic-web && yarn build:web" +command = "export NEXT_PUBLIC_RELEASE_TAG=$(git tag -l 'release-v*' | sort -V | tail -n 1) && yarn test --filter=@scandic-hotels/scandic-web && yarn build:web" publish = "apps/scandic-web/.next" ignore = "if [ -z ${CACHED_COMMIT_REF+x} ] ; then echo 'no CACHED_COMMIT_REF found' && false ; else git diff --quiet $CACHED_COMMIT_REF $COMMIT_REF apps/scandic-web packages/booking-flow packages/common packages/trpc packages/design-system packages/typescript-config ; fi" [context.branch-deploy] -command = "yarn test --filter=@scandic-hotels/scandic-web && yarn build:web" +command = "export NEXT_PUBLIC_RELEASE_TAG=$(git tag -l 'release-v*' | sort -V | tail -n 1) && yarn test --filter=@scandic-hotels/scandic-web && yarn build:web" [context.deploy-preview] -command = "yarn test --filter=@scandic-hotels/scandic-web && yarn build:web" +command = "export NEXT_PUBLIC_RELEASE_TAG=$(git tag -l 'release-v*' | sort -V | tail -n 1) && yarn test --filter=@scandic-hotels/scandic-web && yarn build:web" [[plugins]] package = "@netlify/plugin-nextjs" diff --git a/docs/deploy.md b/docs/deploy.md new file mode 100644 index 000000000..58da9a534 --- /dev/null +++ b/docs/deploy.md @@ -0,0 +1,29 @@ +## Deployment Script + +This script automates the deployment process for the application by force-pushing the current branch to specific environment branches. +Deploying to production must be done via a release-branch + +### Usage + +Be situated on the branch you want to deploy and then run the script using `yarn deploy:` + +```bash +yarn deploy:stage +``` + +or if you want to explicitly call it by using `yarn dlx tsx scripts/deploy/deploy.ts` (or `bun`/`npx tsx`) + +### Environments + +| Environment | Target Branch | Description | +| :---------- | :------------ | :----------------------------------------- | +| `test` | `test` | Deploys to the test environment. | +| `stage` | `stage` | Deploys to the staging environment. | +| `preprod` | `prod` | Deploys to the pre-production environment. | +| `prod` | `release` | Deploys to the production environment. | + +### Features + +- **Branch Validation**: Checks if the target environment is valid. +- **Tagging**: If deploying from a release branch (e.g., `release-v1.2.3`), it automatically creates and pushes a git tag (e.g., `v1.2.3`) if it doesn't exist. +- **Safety Check**: Prompts for confirmation before deploying. diff --git a/package.json b/package.json index e4b3d8700..b6ee0ee34 100644 --- a/package.json +++ b/package.json @@ -1,57 +1,60 @@ { - "name": "scandic", - "packageManager": "yarn@4.6.0", - "scripts": { - "build": "turbo run build --env-mode=loose", - "build:web": "turbo run build --filter=@scandic-hotels/scandic-web --env-mode=loose", - "build:sas": "turbo run build --filter=@scandic-hotels/partner-sas --env-mode=loose", - "lint": "turbo run lint", - "dev": "turbo run dev --output-logs new-only", - "dev:web": "turbo run dev --filter=@scandic-hotels/scandic-web --output-logs new-only", - "dev:ds": "turbo run dev --filter=@scandic-hotels/design-system --output-logs new-only", - "dev:sas": "turbo run dev --filter=@scandic-hotels/partner-sas --output-logs new-only", - "test": "turbo run test", - "format": "turbo run format", - "postinstall": "husky", - "icons:update": "jiti scripts/material-symbols-update.mts", - "check-types": "turbo run check-types", - "env:web": "node scripts/show-env.mjs scandic-web --missing", - "env:sas": "node scripts/show-env.mjs partner-sas --missing", - "i18n:extract": "formatjs extract \"{apps/scandic-web,apps/partner-sas,packages/booking-flow,packages/design-system}/{actions,app,components,constants,contexts,env,hooks,i18n,lib,middlewares,netlify,providers,server,services,stores,utils}/**/*.{ts,tsx}\" --format scripts/i18n/formatter.mjs --out-file scripts/i18n/extracted.json", - "i18n:upload": "jiti scripts/i18n/upload.ts", - "i18n:download": "jiti scripts/i18n/download.ts", - "i18n:compile": "formatjs compile-folder --ast --format scripts/i18n/formatter.mjs scripts/i18n/translations-all scripts/i18n/dictionaries", - "i18n:diff": "yarn i18n:extract && yarn i18n:pull && node scripts/i18n/diff.mjs", - "i18n:clean": "jiti scripts/i18n/clean.ts", - "i18n:distribute": "jiti scripts/i18n/distribute.ts scandic-web partner-sas", - "i18n:push": "yarn i18n:extract && yarn i18n:upload", - "i18n:pull": "yarn i18n:download && yarn i18n:compile && yarn i18n:distribute", - "i18n:sync": "yarn i18n:push && yarn i18n:pull", - "i18n:syncDefaultMessage": "yarn i18n:download && bun scripts/i18n/syncDefaultMessage/index.ts scripts/i18n/translations/en.json '{apps,packages}/**/*.{tsx,ts}' && yarn format --force" - }, - "workspaces": [ - "apps/*", - "packages/*" - ], - "devDependencies": { - "@eslint/compat": "^1.2.9", - "@formatjs/cli": "^6.7.1", - "@types/react": "19.2.7", - "@types/react-dom": "19.2.3", - "@typescript/native-preview": "^7.0.0-dev.20251104.1", - "@yarnpkg/types": "^4.0.1", - "commander": "^14.0.0", - "husky": "^9.1.7", - "jiti": "^1.21.0", - "lint-staged": "^15.2.2", - "prettier": "^3.6.2", - "turbo": "^2.6.1" - }, - "resolutions": { - "react": "~19.2.0", - "react-dom": "~19.2.0", - "vite": "^7.2.4", - "import-in-the-middle": "^1.14.2", - "@react-aria/overlays": "3.27.3" - } + "name": "scandic", + "packageManager": "yarn@4.6.0", + "scripts": { + "build": "turbo run build --env-mode=loose", + "build:web": "turbo run build --filter=@scandic-hotels/scandic-web --env-mode=loose", + "build:sas": "turbo run build --filter=@scandic-hotels/partner-sas --env-mode=loose", + "lint": "turbo run lint", + "dev": "turbo run dev --output-logs new-only", + "dev:web": "turbo run dev --filter=@scandic-hotels/scandic-web --output-logs new-only", + "dev:ds": "turbo run dev --filter=@scandic-hotels/design-system --output-logs new-only", + "dev:sas": "turbo run dev --filter=@scandic-hotels/partner-sas --output-logs new-only", + "test": "turbo run test", + "format": "turbo run format", + "postinstall": "husky", + "icons:update": "jiti scripts/material-symbols-update.mts", + "check-types": "turbo run check-types", + "env:web": "node scripts/show-env.mjs scandic-web --missing", + "env:sas": "node scripts/show-env.mjs partner-sas --missing", + "i18n:extract": "formatjs extract \"{apps/scandic-web,apps/partner-sas,packages/booking-flow,packages/design-system}/{actions,app,components,constants,contexts,env,hooks,i18n,lib,middlewares,netlify,providers,server,services,stores,utils}/**/*.{ts,tsx}\" --format scripts/i18n/formatter.mjs --out-file scripts/i18n/extracted.json", + "i18n:upload": "jiti scripts/i18n/upload.ts", + "i18n:download": "jiti scripts/i18n/download.ts", + "i18n:compile": "formatjs compile-folder --ast --format scripts/i18n/formatter.mjs scripts/i18n/translations-all scripts/i18n/dictionaries", + "i18n:diff": "yarn i18n:extract && yarn i18n:pull && node scripts/i18n/diff.mjs", + "i18n:clean": "jiti scripts/i18n/clean.ts", + "i18n:distribute": "jiti scripts/i18n/distribute.ts scandic-web partner-sas", + "i18n:push": "yarn i18n:extract && yarn i18n:upload", + "i18n:pull": "yarn i18n:download && yarn i18n:compile && yarn i18n:distribute", + "i18n:sync": "yarn i18n:push && yarn i18n:pull", + "i18n:syncDefaultMessage": "yarn i18n:download && bun scripts/i18n/syncDefaultMessage/index.ts scripts/i18n/translations/en.json '{apps,packages}/**/*.{tsx,ts}' && yarn format --force", + "deploy:stage": "yarn dlx tsx scripts/deploy/deploy.ts stage", + "deploy:preprod": "yarn dlx tsx scripts/deploy/deploy.ts preprod", + "deploy:prod": "yarn dlx tsx scripts/deploy/deploy.ts prod" + }, + "workspaces": [ + "apps/*", + "packages/*" + ], + "devDependencies": { + "@eslint/compat": "^1.2.9", + "@formatjs/cli": "^6.7.1", + "@types/react": "19.2.7", + "@types/react-dom": "19.2.3", + "@typescript/native-preview": "^7.0.0-dev.20251104.1", + "@yarnpkg/types": "^4.0.1", + "commander": "^14.0.0", + "husky": "^9.1.7", + "jiti": "^1.21.0", + "lint-staged": "^15.2.2", + "prettier": "^3.6.2", + "turbo": "^2.6.1" + }, + "resolutions": { + "react": "~19.2.0", + "react-dom": "~19.2.0", + "vite": "^7.2.4", + "import-in-the-middle": "^1.14.2", + "@react-aria/overlays": "3.27.3" + } } diff --git a/scripts/deploy/deploy.ts b/scripts/deploy/deploy.ts new file mode 100644 index 000000000..ede6deb91 --- /dev/null +++ b/scripts/deploy/deploy.ts @@ -0,0 +1,128 @@ +import { execSync } from "node:child_process"; +import { createInterface } from "node:readline"; + +// Configuration +const ENV_BRANCH_MAP: Record = { + test: "test", + stage: "stage", + preprod: "prod", + prod: "release", +}; + +const args = process.argv.slice(2); +const targetEnv = args[0]; + +// Validate input +if (!targetEnv) { + console.error("āŒ Error: Please provide an environment."); + console.log( + `Usage: yarn dlx tsx deploy.ts <${Object.keys(ENV_BRANCH_MAP).join("|")}>` + ); + process.exit(1); +} + +const targetBranch = ENV_BRANCH_MAP[targetEnv]; + +if (!targetBranch) { + console.error(`āŒ Error: Invalid environment '${targetEnv}'.`); + console.log( + `Available environments: ${Object.keys(ENV_BRANCH_MAP).join(", ")}` + ); + process.exit(1); +} + +// Git helper +const runGit = ( + command: string, + options: { stdio?: "inherit" | "ignore" | "pipe" } = {} +) => { + return execSync(command, { encoding: "utf-8", ...options }).trim(); +}; + +async function main() { + try { + // Get current branch + const currentBranch = runGit("git rev-parse --abbrev-ref HEAD"); + + console.log( + `\nšŸš€ Preparing to deploy branch '${currentBranch}' to '${targetEnv}' (target branch: '${targetBranch}')` + ); + + // Check for tagging requirement + let tagToCreate: string | null = null; + const releaseBranchRegex = /^release-(v\d+\.\d+\.\d+)$/; + const releaseBranchMatch = currentBranch.match(releaseBranchRegex); + + if (releaseBranchMatch) { + const tagName = releaseBranchMatch[1]; + try { + // Check if tag already exists locally or remote + // Checking local first + runGit(`git rev-parse ${tagName}`, { stdio: "ignore" }); + console.log(`ā„¹ļø Tag '${tagName}' already exists.`); + } catch { + // Tag doesn't exist + tagToCreate = tagName; + console.log(`✨ Will create and push tag: '${tagName}'`); + } + } + if (!releaseBranchMatch && targetEnv === "prod") { + console.warn( + "āš ļø Warning: Deploying to prod from a non-release branch. No version tag will be created." + ); + } + + // Confirmation prompt + const rl = createInterface({ + input: process.stdin, + output: process.stdout, + }); + + const confirmed = await new Promise((resolve) => { + rl.question( + `\nā“ Are you sure you want to deploy ${currentBranch} to ${targetEnv}? (y/n) `, + (answer) => { + rl.close(); + resolve(answer.toLowerCase() === "y"); + } + ); + }); + + if (!confirmed) { + console.log("🚫 Deployment aborted."); + process.exit(0); + } + + console.log("\nšŸ”„ Starting deployment..."); + + // 1. Create and push tag if needed + if (tagToCreate) { + console.log(`šŸ·ļø Creating tag ${tagToCreate}...`); + runGit(`git tag ${tagToCreate}`); + + console.log(`ā¬†ļø Pushing tag ${tagToCreate}...`); + runGit(`git push origin ${tagToCreate}`); + } + + // 2. Force push to target branch + console.log( + `šŸ”„ Force pushing '${currentBranch}' to 'origin/${targetBranch}'...` + ); + runGit( + `git push origin ${currentBranch}:${targetBranch} --force-with-lease`, + { + stdio: "inherit", + } + ); + + console.log(`\nāœ… Deployment to ${targetEnv} completed successfully!`); + } catch (error) { + console.error("\nāŒ Deployment failed."); + if (error instanceof Error) { + console.error(error.message); + } + process.exit(1); + } +} + +main();