diff --git a/dev-utils/README.md b/dev-utils/README.md new file mode 100644 index 00000000..705fa4bc --- /dev/null +++ b/dev-utils/README.md @@ -0,0 +1,3 @@ +# dev-utils + +Utilities for assisting project development. diff --git a/dev-utils/deploy-dev-env.sh b/dev-utils/deploy-dev-env.sh new file mode 100755 index 00000000..4d8cabae --- /dev/null +++ b/dev-utils/deploy-dev-env.sh @@ -0,0 +1,224 @@ +#!/usr/bin/env bash +# Deploy environment for developing the project +# +# Copyright 2025 林博仁(Buo-ren Lin) +# SPDX-License-Identifier: MPL-2.0 + +set_opts=( + # Terminate script execution when an unhandled error occurs + -o errexit + -o errtrace + + # Terminate script execution when an unset parameter variable is + # referenced + -o nounset +) +if ! set "${set_opts[@]}"; then + printf \ + 'Error: Unable to configure the defensive interpreter behaviors.\n' \ + 1>&2 + exit 1 +fi + +required_commands=( + apt-get + dpkg + realpath +) +flag_required_command_check_failed=false +for command in "${required_commands[@]}"; do + if ! command -v "${command}" >/dev/null; then + flag_required_command_check_failed=true + printf \ + 'Error: This program requires the "%s" command to be available in your command search PATHs.\n' \ + "${command}" \ + 1>&2 + fi +done +if test "${flag_required_command_check_failed}" == true; then + printf \ + 'Error: Required command check failed, please check your installation.\n' \ + 1>&2 + exit 1 +fi + +if test -v BASH_SOURCE; then + # Convenience variables may not need to be referenced + # shellcheck disable=SC2034 + { + printf \ + 'Info: Determining the absolute path of the program...\n' + if ! script="$(realpath "${BASH_SOURCE[0]}")"; then + printf \ + 'Error: Unable to determine the absolute path of the program.\n' \ + 1>&2 + exit 1 + fi + script_dir="${script%/*}" + script_filename="${script##*/}" + script_name="${script_filename%%.*}" + } +fi +# Convenience variables may not need to be referenced +# shellcheck disable=SC2034 +{ + script_args=("${@}") +} + +trap_err(){ + printf -- \ + '%s: Error: The program prematurely terminated due to an unhandled error.\n' \ + "${script_name}" \ + 1>&2 + exit 99 +} +if ! trap trap_err ERR; then + printf -- \ + '%s: Error: Unable to set the ERR trap.\n' \ + "${script_name}" \ + 1>&2 + exit 1 +fi + +if test "${EUID}" -ne 0; then + printf -- \ + '%s: Error: This program is required to be run as the superuser(root).\n' \ + "${script_name}" \ + 1>&2 + exit 1 +fi + +if test "${#script_args[@]}" -ne 0; then + printf -- \ + '%s: Error: This program does not accept any command-line arguments.\n' \ + "${script_name}" \ + 1>&2 + exit 2 +fi + +if ! current_epoch_time="$(printf '%(%s)T')"; then + printf -- \ + '%s: Error: Unable to query the current epoch time.\n' \ + "${script_name}" \ + 1>&2 + exit 2 +fi + +if ! apt_list_modification_time="$(stat --format=%Y /var/lib/apt/lists)"; then + printf -- \ + '%s: Error: Unable to query the modification of the local cache of the APT software sources.\n' \ + "${script_name}" \ + 1>&2 + exit 2 +fi + +if test "$((current_epoch_time - apt_list_modification_time))" -ge 86400; then + printf -- \ + '%s: Info: Syncing the software source cache of the APT package manager...\n' \ + "${script_name}" + if ! apt-get update; then + printf -- \ + '%s: Error: Unable to sync the software source cache of the APT package manager.\n' \ + "${script_name}" \ + 1>&2 + exit 2 + fi +fi + +# Avoid debconf interactive prompts on Debian-like systems +export DEBIAN_FRONTEND=noninteractive + +dev_deps_pkgs=( + grunt + npm +) +if ! dpkg --status "${dev_deps_pkgs[@]}" &>/dev/null; then + printf -- \ + '%s: Info: Installing the project development dependencies packages...\n' \ + "${script_name}" + aptget_install_opts=( + --no-install-recommends + --yes + ) + if ! apt-get install "${aptget_install_opts[@]}" \ + "${dev_deps_pkgs[@]}"; then + printf -- \ + '%s: Error: Unable to install the project development dependencies packages.\n' \ + "${script_name}" \ + 1>&2 + exit 2 + fi +fi + +if test "${script_dir}" == /etc/profile.d; then + # We're in the development environment container + project_dir=/project +else + if ! project_dir="$(realpath "${script_dir}/..")"; then + printf -- \ + '%s: Error: Unable to determine the absolute path of the project directory.\n' \ + "${script_name}" \ + 1>&2 + exit 2 + fi +fi + +if ! cd "${project_dir}"; then + printf -- \ + '%s: Error: Unable to switch the working directory to the project directory.\n' \ + "${script_name}" \ + 1>&2 + exit 2 +fi + +printf -- \ + '%s: Info: Installing the Node.js dependencies of the project...\n' \ + "${script_name}" +if test -v SUDO_USER; then + if ! sudo -u "${SUDO_USER}" npm install; then + printf -- \ + '%s: Error: Unable to install the Node.js dependencies of the project.\n' \ + "${script_name}" \ + 1>&2 + exit 2 + fi +else + if ! npm install; then + printf -- \ + '%s: Error: Unable to install the Node.js dependencies of the project.\n' \ + "${script_name}" \ + 1>&2 + exit 2 + fi +fi + +printf -- \ + '%s: Info: Operation completed without errors.\n' \ + "${script_name}" + +if test "${#BASH_SOURCE[@]}" -gt 1; then + set_opts=( + +o errexit + +o errtrace + +o nounset + ) + if ! set "${set_opts[@]}"; then + printf -- \ + '%s: Error: Unable to reset the interpreter behaviors.\n' \ + "${script_name}" \ + 1>&2 + return 2 + fi + + if ! trap - ERR; then + printf -- \ + '%s: Error: Unable to reset the ERR trap.\n' \ + "${script_name}" \ + 1>&2 + return 2 + fi + + return 0 +else + exit 0 +fi diff --git a/dev-utils/nginx-default.conf b/dev-utils/nginx-default.conf new file mode 100644 index 00000000..d3ff9ae0 --- /dev/null +++ b/dev-utils/nginx-default.conf @@ -0,0 +1,14 @@ +# Default website virtual host configuration of the NGINX container +server { + listen 80; + server_name localhost; + location / { + root /usr/share/nginx/html; + index index.html index.htm; + } + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } + charset utf-8; +} diff --git a/docker-compose.yml b/docker-compose.yml index e31467e9..9348aca4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,17 +1,58 @@ -version: '3.4' - +# Docker Compose configuration file +# +# References: +# +# * Compose Specification | Compose file reference | Reference | Docker Docs +# https://docs.docker.com/compose/compose-file/ +# +# Copyright 2025 林博仁(Buo-ren Lin) +# SPDX-License-Identifier: MPL-2.0 +name: www-moztw-org services: - httpd: - container_name: moztw-httpd - image: httpd - restart: unless-stopped + # Environment for developing the project + dev-environment: + container_name: www-moztw-org-dev + hostname: www-moztw-org-dev + image: ubuntu:24.04 + volumes: + - type: bind + source: ./ + target: /project + - type: bind + source: ./dev-utils/deploy-dev-env.sh + target: /etc/profile.d/20-deploy-dev-env.sh + environment: + # Set this environment variable to your local timezone settings for proper operation timestamp + - TZ=CST-8 + + # Avoid debconf interactive prompts on Debian-like systems + - DEBIAN_FRONTEND=noninteractive ports: - - "8080:80" # change 8080 to whatever you want to listen to, e.g. "8888:80" will listen to port 8888 + # For serving the website + - 127.0.0.1:3000:3000 + # Browsersync + - 127.0.0.1:3001:3001 + init: true + command: sleep infinity + profiles: + - dev + + # Environment for testing the project + test-environment: + container_name: www-moztw-org-test + hostname: www-moztw-org-test + image: nginx:1.27-alpine volumes: - - .:/var/www/html - - ./docker-apache.conf:/usr/local/apache2/conf/httpd.conf - healthcheck: - test: [ "CMD", "service" ,"apache2", "status" ] - interval: 5s - timeout: 20s - retries: 10 + - type: bind + source: ./_site + target: /usr/share/nginx/html + read_only: true + - type: bind + source: ./dev-utils/nginx-default.conf + target: /etc/nginx/conf.d/default.conf + read_only: true + ports: + # For serving the website + - 8080:80 + profiles: + - test