Merged in chore/release-pipeline (pull request #3352)

Add scripts for handling deployments

Approved-by: Linus Flood
This commit is contained in:
Joakim Jäderberg
2025-12-16 11:42:51 +00:00
parent bf7a2ac2fe
commit f27ba7ccb6
15 changed files with 242 additions and 63 deletions

View File

@@ -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)

View File

@@ -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,
},
})

View File

@@ -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,
},
})

View File

@@ -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)) {

View File

@@ -21,5 +21,6 @@ async function configureSentry() {
tracesSampleRate: env.SENTRY_SERVER_SAMPLERATE,
denyUrls: denyUrls,
enableLogs: true,
release: env.RELEASE_TAG || undefined,
})
}

View File

@@ -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

View File

@@ -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 (
<div className={variants({ environment: environment })}>
<span>{displayValue}</span>
<span>{displayValues.join(" ")}</span>
</div>
)
}

View File

@@ -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,
},
})

View File

@@ -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,
},
})

View File

@@ -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)) {

View File

@@ -22,5 +22,6 @@ async function configureSentry() {
denyUrls: denyUrls,
enableLogs: true,
enableMetrics: true,
release: env.RELEASE_TAG || undefined,
})
}

View File

@@ -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"

29
docs/deploy.md Normal file
View File

@@ -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:<environment>`
```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.

View File

@@ -27,7 +27,10 @@
"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"
"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/*",

128
scripts/deploy/deploy.ts Normal file
View File

@@ -0,0 +1,128 @@
import { execSync } from "node:child_process";
import { createInterface } from "node:readline";
// Configuration
const ENV_BRANCH_MAP: Record<string, string> = {
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<boolean>((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();