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();