Files
shell-gen/generators/ci.py
T
Your Name 956797f75b feat(ci): add GitLab CE + Jenkins LTS one-click generators
Add 'ci' category with two service generators (gitlab, jenkins),
following project conventions (quoted heredocs, PKG_INSTALL, bash_header,
warnings + post_steps). Jenkins WAR URLs pinned in a class dict to
avoid 404s. app.py + generators/__init__.py updated to register.
2026-08-07 15:57:56 +08:00

288 lines
14 KiB
Python

"""
CI/CD platform generators (GitLab CE, Jenkins LTS).
Both install a self-contained service via systemd. The rendered scripts
follow the same conventions as the rest of the project:
- bash_header() prologue with set -euo pipefail + distro detection
- PKG_INSTALL() function (NOT \$PKG_INSTALL — set -u safe)
- heredocs are quoted (<<'TAG') to keep tool-specific placeholders
like nginx \$host / systemd %i intact
"""
from . import Generator, Field, register, bash_header, bool_str
# ========================== GitLab CE ==========================
class GitLab(Generator):
id = "gitlab"
title = "GitLab CE (代码仓库 + CI)"
category = "ci"
icon = "🦊"
tags = ["gitlab", "git", "ci", "devops"]
description = (
"通过官方 Omnibus 包安装 GitLab Community Edition,"
"自动配置 external_url、可选 SMTP。"
)
warnings = [
"GitLab 占用资源较大,推荐至少 4GB RAM / 4 CPU。",
"生产环境务必配置 SMTP,否则用户收不到邮件通知。",
]
post_steps = [
"浏览器访问 external_url,首次用 root 账号登录并立即改密码。",
"cat /etc/gitlab/initial_root_password # 查看初始 root 密码(24h 后失效)",
]
fields = [
Field("external_url", "External URL", "text",
default="http://gitlab.example.com",
placeholder="http://gitlab.your-domain.com",
required=True,
help="用户访问 GitLab 的地址;必须是 http(s):// 开头,"
"会自动写入 nginx vhost。"),
Field("version", "版本", "text", default="latest",
placeholder="latest 或 16.11.0-ce.0",
help="留空/填 latest 装最新版;"
"固定版本号形如 16.11.0-ce.0。"),
Field("data_dir", "数据目录", "text", default="/var/opt/gitlab",
help="GitLab 数据存放位置;挂独立盘请改成大空间路径。"),
Field("configure_smtp", "配置 SMTP", "checkbox", default="no",
help="勾选后会写入 SMTP 配置,需要填下面 3 个字段。"
"不勾选则保留 GitLab 默认(无邮件功能)。"),
Field("smtp_address", "SMTP 地址", "text",
default="smtp.gmail.com:587",
placeholder="smtp.example.com:587",
help="仅 configure_smtp=yes 时生效。"),
Field("smtp_user", "SMTP 用户名", "text", default="",
placeholder="[email protected]",
help="仅 configure_smtp=yes 时生效。"),
Field("smtp_password", "SMTP 密码", "text", default="",
placeholder="app password (推荐)",
help="仅 configure_smtp=yes 时生效;"
"推荐用 Gmail / QQ 的 app password 而非账号密码。"),
]
def render(self, p):
ext_url = p.get("external_url", "http://gitlab.example.com").strip()
if not ext_url.startswith(("http://", "https://")):
raise ValueError(
"external_url 必须以 http:// 或 https:// 开头"
)
version = p.get("version", "latest").strip() or "latest"
data_dir = p.get("data_dir", "/var/opt/gitlab").strip()
smtp_on = bool_str(p.get("configure_smtp"))
smtp_addr = p.get("smtp_address", "smtp.gmail.com:587").strip()
smtp_user = p.get("smtp_user", "").strip()
smtp_pass = p.get("smtp_password", "").strip()
if smtp_on and (not smtp_user or not smtp_pass):
raise ValueError(
"configure_smtp=yes 时必须填写 smtp_user 和 smtp_password"
)
out = [bash_header(self.title)]
out.append('log "Installing GitLab CE..."')
out.append('# GitLab Omnibus 自带 postfix / nginx / openssh 等依赖,'
'脚本不需单独装。')
out.append('PKG_INSTALL curl openssl ca-certificates perl')
out.append('')
# Download GitLab's official apt/yum repo setup script.
# The script picks the right package manager based on detected distro.
# Using 'curl | bash' is GitLab's officially documented install path.
out.append('log "Adding GitLab apt/yum repository..."')
if version == "latest":
pkg_args = ""
else:
pkg_args = '?version=' + version
out.append('curl -fsSL https://packages.gitlab.com/install/repositories/'
'gitlab/gitlab-ce/script.deb.sh' + pkg_args
+ ' -o /tmp/gl-repo.sh')
# The .rpm.sh variant is what GitLab's deb.sh script also supports;
# but the deb.sh handles apt only. For RHEL family we fall back to rpm.sh.
# To keep things simple and reliable, we just attempt deb.sh first;
# if the host is RHEL-like, GitLab's script will detect and abort with
# a hint pointing to the rpm.sh variant. We surface that as an error.
out.append('bash /tmp/gl-repo.sh')
out.append('rm -f /tmp/gl-repo.sh')
out.append('')
# Install the package. For specific version we use =<ver>, otherwise bare.
if version == "latest":
install_cmd = 'gitlab-ce'
else:
install_cmd = 'gitlab-ce=' + version
out.append('log "Installing GitLab CE package (this may take a few minutes)..."')
out.append('PKG_INSTALL ' + install_cmd)
out.append('')
# Generate /etc/gitlab/gitlab.rb
# NOTE: heredoc is QUOTED — keeps any literal $ / % in our config
# intact for reconfigure to interpret, and is safe under 'set -u'.
out.append('log "Writing /etc/gitlab/gitlab.rb..."')
rb_lines = [
"external_url '" + ext_url.replace("'", "'\\''") + "'",
"git_data_dir '" + data_dir + "'",
]
if smtp_on:
# GitLab rb SMTP block (escaped single quotes inside heredoc are fine
# because the heredoc delimiter is quoted; nothing will be expanded).
rb_lines += [
"gitlab_rails['smtp_enable'] = true",
"gitlab_rails['smtp_address'] = '" + smtp_addr.replace("'", "'\\''") + "'",
"gitlab_rails['smtp_port'] = 587",
"gitlab_rails['smtp_user_name'] = '" + smtp_user.replace("'", "'\\''") + "'",
"gitlab_rails['smtp_password'] = '" + smtp_pass.replace("'", "'\\''") + "'",
"gitlab_rails['smtp_domain'] = '" +
smtp_addr.split(":")[0].replace("'", "'\\''") + "'",
"gitlab_rails['smtp_authentication'] = 'login'",
"gitlab_rails['smtp_enable_starttls_auto'] = true",
"gitlab_rails['smtp_tls'] = false",
]
out.append("cat > /etc/gitlab/gitlab.rb <<'GITLAB_RB_EOF'\n"
+ "\n".join(rb_lines) + "\n"
"GITLAB_RB_EOF")
out.append('')
out.append('log "Running gitlab-ctl reconfigure (this takes 3-5 min)..."')
out.append('gitlab-ctl reconfigure')
out.append('')
# Extract bound port from external_url (best effort: default 80/443)
bound_port = "80"
if ext_url.startswith("https://"):
bound_port = "443"
out.append('log "Verifying GitLab is reachable..."')
out.append('sleep 5')
out.append('gitlab-ctl status >/dev/null && log "GitLab services are running" '
'|| warn "Some GitLab services may not be up yet — check: gitlab-ctl status"')
out.append('curl -fsS -o /dev/null -w "HTTP %{http_code} on :' + bound_port
+ '\\n" http://127.0.0.1:' + bound_port + '/')
out.append('log "Done. Open ' + ext_url + ' in your browser."')
return "\n".join(out) + "\n"
# ========================== Jenkins LTS ==========================
class Jenkins(Generator):
id = "jenkins"
title = "Jenkins LTS (持续集成)"
category = "ci"
icon = "🤖"
tags = ["jenkins", "ci", "cd", "pipeline"]
description = (
"下载 Jenkins LTS WAR 包,systemd 跑 java -jar,"
"默认 JDK 17 / 21。"
)
warnings = [
"首次启动会在 /var/lib/jenkins/secrets/initialAdminPassword 生成初始密码。",
"推荐在 Manage Jenkins → Configure Global Security 关掉 'Anyone can do anything'",
]
post_steps = [
"浏览器访问 http://<host>:<port>",
"cat /var/lib/jenkins/secrets/initialAdminPassword",
"解锁后选 'Install suggested plugins'",
]
fields = [
Field("version", "Jenkins LTS 版本", "select", default="2.541.3",
options=["2.541.3", "2.528.2", "2.568.2", "latest"],
help="2.541.3/2.528.2/2.568.2 是 LTS;"
"latest 是滚动版(可能不稳定)。"),
Field("port", "HTTP 端口", "number", default="8080", min_=1, max_=65535),
Field("java_opts", "JVM 参数", "text",
default="-Xms512m -Xmx1024m -Djenkins.install.runSetupWizard=false",
help="JAVA_OPTS。-Xms/-Xmx 控制堆内存。"),
Field("install_dir", "安装目录", "text", default="/opt/jenkins"),
Field("jenkins_home", "JENKINS_HOME", "text", default="/var/lib/jenkins",
help="jobs / plugins / 配置存放目录。挂独立盘请改这里。"),
Field("user", "运行用户", "text", default="jenkins"),
]
# Pre-validated URLs. Always pin to a real release; if 'latest' is selected
# we use the rolling URL. Listed URLs were all confirmed 200/302 at design
# time (see PR notes); Jenkins get.jenkins.io redirects 302 to the CDN.
_WAR_URLS = {
"2.541.3": "https://get.jenkins.io/war-stable/2.541.3/jenkins.war",
"2.528.2": "https://get.jenkins.io/war-stable/2.528.2/jenkins.war",
"2.568.2": "https://get.jenkins.io/war-stable/2.568.2/jenkins.war",
"latest": "https://get.jenkins.io/war/latest/jenkins.war",
}
def render(self, p):
version = p.get("version", "2.541.3")
port = str(p.get("port", "8080"))
java_opts = p.get(
"java_opts",
"-Xms512m -Xmx1024m -Djenkins.install.runSetupWizard=false",
)
install_dir = p.get("install_dir", "/opt/jenkins")
jh = p.get("jenkins_home", "/var/lib/jenkins")
user = p.get("user", "jenkins")
if version not in self._WAR_URLS:
raise ValueError(
f"未知 Jenkins 版本: {version}。可选: "
+ ", ".join(sorted(self._WAR_URLS.keys()))
)
url = self._WAR_URLS[version]
out = [bash_header(self.title)]
out.append('log "Installing JDK + Jenkins LTS..."')
out.append('command -v java >/dev/null || '
'PKG_INSTALL openjdk-17-jdk-headless || '
'PKG_INSTALL java-17-openjdk-devel || '
'PKG_INSTALL default-jdk-headless || '
'PKG_INSTALL default-jdk')
out.append('command -v java >/dev/null || die "Java not found after install — pick a different JDK"')
out.append('')
# User + dirs
out.append(f'useradd -r -s /bin/false -d {jh} {user} 2>/dev/null || true')
out.append(f'mkdir -p {install_dir} {jh} /var/log/jenkins')
out.append(f'chown -R {user}:{user} {jh} /var/log/jenkins')
out.append('')
# Download WAR
out.append(f'log "Downloading Jenkins WAR ({version})..."')
out.append(f'curl -fsSL -o {install_dir}/jenkins.war "{url}"')
out.append(f'chown {user}:{user} {install_dir}/jenkins.war')
out.append('')
# systemd unit — QUOTED heredoc so %i / $PORT placeholders stay intact
out.append('log "Writing systemd unit..."')
# Quote java_opts for systemd Environment= line — it must be a single
# value, and embedded '"' or '\' in user input would otherwise break
# the unit file. We use double-quote escaping (\\" within the value).
safe_java_opts = java_opts.replace("\\", "\\\\").replace('"', '\\"')
out.append(
f"cat > /etc/systemd/system/jenkins.service <<'UNIT_EOF'\n"
'[Unit]\n'
'Description=Jenkins LTS Continuous Integration Server\n'
'After=network.target\n\n'
'[Service]\n'
'Type=simple\n'
f'User={user}\n'
f'Group={user}\n'
f'Environment="JENKINS_HOME={jh}"\n'
f'Environment="JAVA_OPTS={safe_java_opts}"\n'
f'Environment="JENKINS_PORT={port}"\n'
f'WorkingDirectory={jh}\n'
f'ExecStart=/usr/bin/java $JAVA_OPTS -jar {install_dir}/jenkins.war '
f'--httpPort=$JENKINS_PORT\n'
'Restart=on-failure\n'
'RestartSec=10\n\n'
'[Install]\n'
'WantedBy=multi-user.target\n'
'UNIT_EOF'
)
out.append('systemctl daemon-reload')
out.append('systemctl enable --now jenkins')
out.append('log "Waiting for Jenkins to come up..."')
# Jenkins can take 30-90s on first start while extracting plugins
out.append('for i in $(seq 1 30); do')
out.append(' if curl -fsS -o /dev/null http://127.0.0.1:' + port + '/ ; then')
out.append(' log "Jenkins is responding on :' + port + '"')
out.append(' break')
out.append(' fi')
out.append(' sleep 3')
out.append('done')
out.append('if [ ! -f ' + jh + '/secrets/initialAdminPassword ]; then')
out.append(' warn "Initial admin password not found yet — Jenkins may still be starting."')
out.append(' warn "Check: systemctl status jenkins / journalctl -u jenkins -n 50"')
out.append('else')
out.append(' log "Initial admin password (copy this):"')
out.append(' cat ' + jh + '/secrets/initialAdminPassword')
out.append('fi')
return "\n".join(out) + "\n"
register(GitLab())
register(Jenkins())