diff --git a/.gitignore b/.gitignore index c966ea8..1f3f1c3 100755 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -# .gitignore: +# .gitignore: static posts/*.md posts/*/*.md @@ -17,6 +17,11 @@ # Node.js node_modules/ +.yarn +.pnp.cjs +.pnp.loader.mjs +.yarn/ +.yarnrc.yml # Python *.pyc @@ -82,3 +87,5 @@ .last_successful_prepush_test certs/* test/logs/* + + diff --git a/Jenkinsfile b/Jenkinsfile index 4987c18..3da29a4 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -12,6 +12,7 @@ parameters { string(name: 'branch', defaultValue: 'refs/heads/main', description: 'Deployment branch') booleanParam(name: 'SKIP_TESTS', defaultValue: false, description: 'Skip integration tests') + booleanParam(name: 'HOTFIX_MODE', defaultValue: true, description: 'Skip cloning/installing; pull and restart only') } stages { @@ -28,9 +29,10 @@ stage('Execute Deployment') { steps { script { + def mode = params.HOTFIX_MODE ? "--hotfix" : "" def skipFlag = params.SKIP_TESTS ? "--skip-tests" : "" // Call the python binary inside the venv directly - sh "./.venv/bin/python3 -u ./deployment --config /srv/jasonpoage.com/deployment.lua --branch ${env.TARGET_BRANCH} ${skipFlag}" + sh "./.venv/bin/python3 -u ./deployment --config /srv/jasonpoage.com/deployment.lua --branch ${env.TARGET_BRANCH} ${skipFlag} ${mode}" } } } diff --git a/deployment/core/suite.py b/deployment/core/suite.py index 9a1f478..0e22a5b 100644 --- a/deployment/core/suite.py +++ b/deployment/core/suite.py @@ -41,6 +41,8 @@ parser = argparse.ArgumentParser(description="Blog Deployment Suite") parser.add_argument("--config", required=True) + parser.add_argument("--hotfix", action="store_true") + parser.add_argument("--full-pipeline", action="store_true") parser.add_argument("--branch", required=True) parser.add_argument( "--root", type=str, help="The root directory of the project" diff --git a/deployment/core/task_runner.py b/deployment/core/task_runner.py index e7f2a6c..5b06f4b 100755 --- a/deployment/core/task_runner.py +++ b/deployment/core/task_runner.py @@ -12,9 +12,11 @@ from core.tasks import ( GetDeploymentConfig, LoadServerConfig, + HotFix, YarnBuild, AtomicDeploy, HealthCheck, + PipelineSuccess, ) @@ -49,6 +51,7 @@ VerifySystemDependencies, GetDeploymentConfig, LoadServerConfig, + HotFix, EnsureBuildPaths, YarnBuild, TestRunner, @@ -115,6 +118,9 @@ # continue if task.run() is False: self.fail(f"Pipeline stopped at task: {task.name}") + except PipelineSuccess as e: + self.print(e) + break except ModuleNotFoundError as e: self.print(f" [ERROR] Task {task.name} failed: {e}") self.fail(f"Pipeline stopped at task: {self.last_task}") diff --git a/deployment/core/tasks.py b/deployment/core/tasks.py index e4d8a5b..dcab19d 100644 --- a/deployment/core/tasks.py +++ b/deployment/core/tasks.py @@ -109,6 +109,51 @@ self.fail(f"FAILED to parse {server_type} TOML: ", e) +class PipelineSuccess(Exception): + pass + + +class HotFix(SuiteTask): + """Bypasses the full build to update the current live deployment""" + + _stage = Stage.DEPLOY + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.name = "Hot fix" + + def _run(self): + if not self.get_arg("hotfix"): + return + + cfg = self.env.release + print(self.env) + # 1. Target the current active symlink + live_path = self.env.release.deploy_link + + self.print(f" [HOTFIX] Pulling latest changes into {live_path}") + + # 2. Pull changes + try: + self.sh( + "git pull origin " + self.env.deploy_branch, + cwd=live_path, + handle_exception=False, + ) + except: + self.sh("git fetch origin ", cwd=live_path) + self.sh("git reset --hard origin/" + self.env.deploy_branch, cwd=live_path) + + # 3. Quick Asset Rebuild (Skip yarn install unless package.json changed) + # We check for changes in package.json to decide if we need a full install + self.sh("yarn combine:css", cwd=live_path) + + # 4. Restart to pick up Node.js changes + self.sh(f"sudo systemctl restart {cfg.service_name}") + + raise PipelineSuccess("Hot fix applied successfully") + + class YarnBuild(SuiteTask): """Executes dependency installation and asset compilation""" diff --git a/deployment/lib/task_types.py b/deployment/lib/task_types.py index 122efa6..1dfb1db 100755 --- a/deployment/lib/task_types.py +++ b/deployment/lib/task_types.py @@ -151,8 +151,11 @@ self, cmd: str, cwd: Path | None = None, handle_exception=True, dry_run=None ): """Helper to run shell commands within the project context.""" + + if cwd is not None: + self.print(f" [CWD] {cwd}") + cwd = str(cwd or os.getcwd()) - # self.print(f" [CWD] {cwd}") self.msg(f" [EXEC] {cmd}") if self.do_dry_run() and dry_run is not False: return diff --git a/deployment/lib/types.py b/deployment/lib/types.py index e2ea767..1e9e919 100644 --- a/deployment/lib/types.py +++ b/deployment/lib/types.py @@ -24,6 +24,8 @@ pidfile: Path = Path() test_log: Path = Path() toml: dict = {} + release: dict = {} + testing: dict = {} def __init__(self, timestamp_format: str | None = None): self.workspace: Path = Path() diff --git a/src/middleware/errorHandler.js b/src/middleware/errorHandler.js index eb6e663..bee91cc 100644 --- a/src/middleware/errorHandler.js +++ b/src/middleware/errorHandler.js @@ -47,7 +47,11 @@ const errorContext = getErrorContext(code || statusCode); if (!isDev && !req?.isAuthenticated) { - res.customRedirect(`${ERROR_REDIRECT_PATH}/${errorContext.statusCode}`); + try { + res.customRedirect(`${ERROR_REDIRECT_PATH}/${errorContext.statusCode}`); + } catch (e) { + console.error("Critical error", errorContext); + } return; } @@ -73,7 +77,7 @@ const errorPageContext = await req.getBaseContext( req?.isAuthenticated, - context + context, ); res.status(errorContext.statusCode); try {