Add Go demo with isolated CI and manual production release
Demo CI / check (push) Successful in 2m10s
Demo CI / check (push) Successful in 2m10s
This commit is contained in:
commit
c65b5470e0
19 files changed
+398
No files matched your search
@@ -0,0 +1,8 @@
|
|||||||
|
**
|
||||||
|
!src/
|
||||||
|
!src/**
|
||||||
|
!build/
|
||||||
|
!build/Dockerfile
|
||||||
|
src/.env
|
||||||
|
src/.env.*
|
||||||
|
src/demo
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
name: Demo CI
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
concurrency:
|
||||||
|
group: demo-cicd
|
||||||
|
cancel-in-progress: false
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
runs-on: demo-build
|
||||||
|
timeout-minutes: 30
|
||||||
|
services:
|
||||||
|
buildkit:
|
||||||
|
image: mirror.gcr.io/moby/buildkit@sha256:f8a833b2de9d68e27f0815e4a737abdfaf8a2e4c615650557df11025101557b4
|
||||||
|
steps:
|
||||||
|
- name: Clean checkout and isolated Go checks
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ gitea.token }}
|
||||||
|
GITEA_REPOSITORY: ${{ gitea.repository }}
|
||||||
|
SOURCE_SHA: ${{ gitea.sha }}
|
||||||
|
run: python3 /opt/ci/pipeline.py check
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
name: Demo Release
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
operation:
|
||||||
|
description: Release selected Git ref, or roll back to a verified revision
|
||||||
|
type: choice
|
||||||
|
options: [release, rollback]
|
||||||
|
default: release
|
||||||
|
required: true
|
||||||
|
target_revision:
|
||||||
|
description: Full 40-character previously released commit, required only for rollback
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
concurrency:
|
||||||
|
group: demo-cicd
|
||||||
|
cancel-in-progress: false
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
if: ${{ inputs.operation == 'release' }}
|
||||||
|
runs-on: demo-build
|
||||||
|
timeout-minutes: 30
|
||||||
|
services:
|
||||||
|
buildkit:
|
||||||
|
image: mirror.gcr.io/moby/buildkit@sha256:f8a833b2de9d68e27f0815e4a737abdfaf8a2e4c615650557df11025101557b4
|
||||||
|
steps:
|
||||||
|
- name: Build and publish private immutable candidate
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ gitea.token }}
|
||||||
|
GITEA_REPOSITORY: ${{ gitea.repository }}
|
||||||
|
SOURCE_SHA: ${{ gitea.sha }}
|
||||||
|
REGISTRY_TOKEN: ${{ secrets.DEMO_REGISTRY_PUSH_TOKEN }}
|
||||||
|
run: python3 /opt/ci/pipeline.py build
|
||||||
|
deploy:
|
||||||
|
needs: [build]
|
||||||
|
if: ${{ always() && ((inputs.operation == 'release' && needs.build.result == 'success') || inputs.operation == 'rollback') }}
|
||||||
|
runs-on: demo-deploy
|
||||||
|
timeout-minutes: 15
|
||||||
|
steps:
|
||||||
|
- name: Verify actual runtime image and deploy
|
||||||
|
env:
|
||||||
|
GITEA_REPOSITORY: ${{ gitea.repository }}
|
||||||
|
SOURCE_SHA: ${{ gitea.sha }}
|
||||||
|
TARGET_REVISION: ${{ inputs.target_revision }}
|
||||||
|
OPERATION: ${{ inputs.operation }}
|
||||||
|
run: |
|
||||||
|
case "$OPERATION" in
|
||||||
|
release) python3 /opt/ci/pipeline.py deploy ;;
|
||||||
|
rollback) python3 /opt/ci/pipeline.py rollback ;;
|
||||||
|
*) exit 1 ;;
|
||||||
|
esac
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
/images/
|
||||||
|
/src/demo
|
||||||
|
/.env
|
||||||
|
/.env.*
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# Demo
|
||||||
|
|
||||||
|
最小 Go HTTP 服务,源码入口 `src/`,项目根是 Git 仓库边界;私有仓库为 `https://git.dever.cc/rabin/demo`。仅使用 Go 标准库,不引入业务框架。
|
||||||
|
|
||||||
|
2026-10-08(UTC)已部署到共享 dev,`https://demo-test.5dev.cn` 无需登录即可返回 `hello world`。真实 HTTP、源码同步、单服务重启、停止释放端口及 dev Pod 重建恢复均已验证;没有构建或发布业务生产镜像。
|
||||||
|
|
||||||
|
## 行为
|
||||||
|
|
||||||
|
- `GET /` 返回 `hello world` 和换行,Content-Type 为 `text/plain; charset=utf-8`。
|
||||||
|
- 其他路径返回 404,不支持的方法返回 405;`HEAD /` 使用 HTTP 标准行为。
|
||||||
|
- 程序读取 `PORT`,开发配置固定为 `18080`,未提供时使用 `8080`;非法端口启动失败。
|
||||||
|
- Supervisor 程序名为 `demo`,日志是 dev 内 `/work/log/demo.log`。
|
||||||
|
|
||||||
|
## 开发
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl exec -n platform-dev deployment/dev -c dev -- sh -c 'cd /dm/service/project/demo/src && go fmt ./... && go test ./... && go vet ./...'
|
||||||
|
kubectl exec -n platform-dev deployment/dev -c dev -- supervisorctl -c /etc/supervisor/supervisord.conf restart demo
|
||||||
|
```
|
||||||
|
|
||||||
|
当前没有额外测试框架,`go test` 的包检查不能替代真实 HTTP 请求验证。源码修改不构建镜像;普通依赖在 dev 内按兼容 Go 1.25 的固定版本添加,检查 `go.mod`、`go.sum` 的变化。
|
||||||
|
|
||||||
|
## 开发入口
|
||||||
|
|
||||||
|
`demo-test.5dev.cn` 复用现有 Caddy、Traefik 和 `*.5dev.cn` 证书。`config/dev/` 管理 Service、Ingress;Service `demo-dev:80` 指向共享 dev 的 `18080` 端口。
|
||||||
|
|
||||||
|
平台入口不叠加统一登录,登录和鉴权由项目自行实现;后续 project 项目也遵循此约定。Demo 没有应用认证,因此可公开访问。初次部署使用的 BasicAuth Middleware、专用 Secret 和生成的凭据文件已按用户要求移除。
|
||||||
|
|
||||||
|
开发 Service 不用于生产。正式环境使用独立的 `project-demo/demo` Deployment,通过 `demo.5dev.cn` 访问;实际发布结果以 Actions 和 `images/<完整提交>.json` 为准。
|
||||||
|
|
||||||
|
## CI/CD
|
||||||
|
|
||||||
|
- `.gitea/workflows/ci.yaml`:推送到 `main` 后自动检查格式、执行 `go test`、`go vet` 并编译;不推镜像、不发布生产。也可手动执行检查。
|
||||||
|
- `.gitea/workflows/release.yaml`:只接受手动触发。选择明确 Git 分支或标签,`operation=release`,流水线检出该次事件的完整 commit 后测试、构建、推送私有镜像、验证真实运行镜像,再发布生产并检查公网 HTTPS。
|
||||||
|
- `operation=rollback` 时,`target_revision` 必须填写曾经成功发布的完整 40 位 commit。回退复用已有镜像摘要和校验过的归档,不重新编译;仍执行运行镜像与正式 HTTPS 检查。
|
||||||
|
- 镜像为 `git.dever.cc/demo-ci/demo:<完整提交>`,实际 Deployment 固定 `@sha256`,不用 `latest`。二进制在隔离 BuildKit 容器里用 Go 1.25.14 编译,运行层是非 root 的 scratch 镜像,不含编译器、Supervisor 或项目源码。
|
||||||
|
- 发布阶段失败会尝试恢复之前的 Demo 镜像和副本数,并在 Actions 中报失败,不把自动回退当作发布成功。首次发布失败恢复未启动状态。
|
||||||
|
- `images/<完整提交>.tar`、`.tar.sha256`、`.json` 是正式发行归档,Git 忽略整个 `images/`。源码、Git、配置与归档保留;临时 CI 工作目录和验证 Job 会清理。
|
||||||
|
|
||||||
|
`config/namespace.yaml` 和 `config/prod/` 是首次建立生产资源的配置。生产镜像与副本数之后由发布流程管理;不要直接重套初始化配置覆盖正在运行的版本。更新平台拓扑前需保留当前固定镜像摘要和副本数,审查差异后再应用;日常发布和回退使用 Actions。
|
||||||
|
|
||||||
|
Runner、工具镜像、缓存和角色配置归 `/dm/service/platform/ci/`。Runner 仅接受本仓库任务、并发为 1;构建 Pod 不挂 Kubernetes 令牌,部署 Pod 的生产权限仅能读取和更新本项目的 `demo` Deployment。只有可信仓库维护者可以修改工作流或触发发布,不接收外部 fork/PR 的自动任务。
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
FROM docker.io/library/golang:1.25.14-bookworm@sha256:c268a04d59aea0b180ed9946a658cfab9e7b3391dc90eed6e4969ccff98c851f AS build
|
||||||
|
ENV GOTOOLCHAIN=local CGO_ENABLED=0
|
||||||
|
WORKDIR /src
|
||||||
|
COPY src/go.mod ./
|
||||||
|
RUN --mount=type=cache,target=/go/pkg/mod go mod download
|
||||||
|
COPY src/ ./
|
||||||
|
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||||
|
test -z "$(gofmt -l .)" \
|
||||||
|
&& go test ./... \
|
||||||
|
&& go vet ./... \
|
||||||
|
&& go build -trimpath -ldflags='-s -w' -o /out/demo .
|
||||||
|
|
||||||
|
FROM scratch AS runtime
|
||||||
|
ARG SOURCE_REVISION
|
||||||
|
LABEL org.opencontainers.image.source="https://git.dever.cc/rabin/demo" \
|
||||||
|
org.opencontainers.image.revision=$SOURCE_REVISION
|
||||||
|
COPY --from=build /out/demo /demo
|
||||||
|
USER 65532:65532
|
||||||
|
ENV PORT=8080
|
||||||
|
EXPOSE 8080
|
||||||
|
ENTRYPOINT ["/demo"]
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: demo-dev
|
||||||
|
namespace: platform-dev
|
||||||
|
annotations:
|
||||||
|
traefik.ingress.kubernetes.io/router.entrypoints: web
|
||||||
|
spec:
|
||||||
|
ingressClassName: traefik
|
||||||
|
rules:
|
||||||
|
- host: demo-test.5dev.cn
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: demo-dev
|
||||||
|
port:
|
||||||
|
name: http
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
resources:
|
||||||
|
- service.yaml
|
||||||
|
- ingress.yaml
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: demo-dev
|
||||||
|
namespace: platform-dev
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
selector:
|
||||||
|
app.kubernetes.io/name: dev
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 80
|
||||||
|
targetPort: 18080
|
||||||
|
protocol: TCP
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
[program:demo]
|
||||||
|
directory=/dm/service/project/demo/src
|
||||||
|
command=/usr/local/go/bin/go run .
|
||||||
|
environment=PORT="18080"
|
||||||
|
autostart=true
|
||||||
|
autorestart=unexpected
|
||||||
|
stopasgroup=true
|
||||||
|
killasgroup=true
|
||||||
|
redirect_stderr=true
|
||||||
|
stdout_logfile=/work/log/demo.log
|
||||||
|
stdout_logfile_maxbytes=10MB
|
||||||
|
stdout_logfile_backups=3
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: project-demo
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/part-of: demo
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: demo
|
||||||
|
namespace: project-demo
|
||||||
|
spec:
|
||||||
|
replicas: 0
|
||||||
|
revisionHistoryLimit: 5
|
||||||
|
strategy:
|
||||||
|
type: RollingUpdate
|
||||||
|
rollingUpdate:
|
||||||
|
maxSurge: 1
|
||||||
|
maxUnavailable: 0
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: demo
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: demo
|
||||||
|
spec:
|
||||||
|
automountServiceAccountToken: false
|
||||||
|
nodeSelector:
|
||||||
|
kubernetes.io/hostname: shemic
|
||||||
|
imagePullSecrets:
|
||||||
|
- name: demo-registry-pull
|
||||||
|
terminationGracePeriodSeconds: 15
|
||||||
|
securityContext:
|
||||||
|
runAsNonRoot: true
|
||||||
|
runAsUser: 65532
|
||||||
|
runAsGroup: 65532
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
|
containers:
|
||||||
|
- name: demo
|
||||||
|
image: git.dever.cc/demo-ci/demo:bootstrap
|
||||||
|
imagePullPolicy: Always
|
||||||
|
env:
|
||||||
|
- name: PORT
|
||||||
|
value: '8080'
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: 8080
|
||||||
|
securityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
readOnlyRootFilesystem: true
|
||||||
|
capabilities:
|
||||||
|
drop: [ALL]
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 10m
|
||||||
|
memory: 16Mi
|
||||||
|
limits:
|
||||||
|
cpu: 250m
|
||||||
|
memory: 64Mi
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /
|
||||||
|
port: http
|
||||||
|
periodSeconds: 3
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /
|
||||||
|
port: http
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 10
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: demo
|
||||||
|
namespace: project-demo
|
||||||
|
annotations:
|
||||||
|
traefik.ingress.kubernetes.io/router.entrypoints: web
|
||||||
|
spec:
|
||||||
|
ingressClassName: traefik
|
||||||
|
rules:
|
||||||
|
- host: demo.5dev.cn
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: demo
|
||||||
|
port:
|
||||||
|
name: http
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
resources:
|
||||||
|
- deployment.yaml
|
||||||
|
- service.yaml
|
||||||
|
- ingress.yaml
|
||||||
|
- networkpolicy.yaml
|
||||||
|
- rbac.yaml
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata:
|
||||||
|
name: demo
|
||||||
|
namespace: project-demo
|
||||||
|
spec:
|
||||||
|
podSelector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: demo
|
||||||
|
policyTypes: [Ingress, Egress]
|
||||||
|
egress: []
|
||||||
|
ingress:
|
||||||
|
- from:
|
||||||
|
- namespaceSelector:
|
||||||
|
matchLabels:
|
||||||
|
kubernetes.io/metadata.name: kube-system
|
||||||
|
podSelector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: traefik
|
||||||
|
ports:
|
||||||
|
- protocol: TCP
|
||||||
|
port: 8080
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: Role
|
||||||
|
metadata:
|
||||||
|
name: demo-release
|
||||||
|
namespace: project-demo
|
||||||
|
rules:
|
||||||
|
- apiGroups: [apps]
|
||||||
|
resources: [deployments]
|
||||||
|
resourceNames: [demo]
|
||||||
|
verbs: [get, watch, patch]
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: RoleBinding
|
||||||
|
metadata:
|
||||||
|
name: demo-release
|
||||||
|
namespace: project-demo
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: demo-deployer
|
||||||
|
namespace: platform-ci
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: Role
|
||||||
|
name: demo-release
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: demo
|
||||||
|
namespace: project-demo
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app.kubernetes.io/name: demo
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 80
|
||||||
|
targetPort: http
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
module example.com/demo
|
||||||
|
|
||||||
|
go 1.25.0
|
||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
port := os.Getenv("PORT")
|
||||||
|
if port == "" {
|
||||||
|
port = "8080"
|
||||||
|
}
|
||||||
|
portNumber, err := strconv.Atoi(port)
|
||||||
|
if err != nil || portNumber < 1 || portNumber > 65535 {
|
||||||
|
log.Fatal("PORT must be an integer between 1 and 65535")
|
||||||
|
}
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("GET /{$}", func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||||
|
if _, err := writer.Write([]byte("hello world\n")); err != nil {
|
||||||
|
log.Printf("write response: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
server := &http.Server{
|
||||||
|
Addr: ":" + port,
|
||||||
|
Handler: mux,
|
||||||
|
ReadHeaderTimeout: 5 * time.Second,
|
||||||
|
WriteTimeout: 10 * time.Second,
|
||||||
|
IdleTimeout: 60 * time.Second,
|
||||||
|
}
|
||||||
|
log.Printf("listening on %s", server.Addr)
|
||||||
|
log.Fatal(server.ListenAndServe())
|
||||||
|
}
|
||||||
Reference in new issue
Block a user