diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 531b080..28b2cd6 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -8,7 +8,7 @@ concurrency: cancel-in-progress: false jobs: check: - runs-on: demo-build + runs-on: go-build timeout-minutes: 30 services: buildkit: diff --git a/.gitea/workflows/release.yaml b/.gitea/workflows/release.yaml index f902001..64bf0da 100644 --- a/.gitea/workflows/release.yaml +++ b/.gitea/workflows/release.yaml @@ -18,26 +18,25 @@ concurrency: jobs: build: if: ${{ inputs.operation == 'release' }} - runs-on: demo-build + runs-on: go-build timeout-minutes: 30 services: buildkit: image: mirror.gcr.io/moby/buildkit@sha256:f8a833b2de9d68e27f0815e4a737abdfaf8a2e4c615650557df11025101557b4 steps: - - name: Build and publish private immutable candidate + - name: Build and archive immutable binary 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 + runs-on: release-deploy timeout-minutes: 15 steps: - - name: Verify actual runtime image and deploy + - name: Verify binary and restart only this project in shared release env: GITEA_REPOSITORY: ${{ gitea.repository }} SOURCE_SHA: ${{ gitea.sha }} diff --git a/README.md b/README.md index 42f9caa..8d8b8c5 100644 --- a/README.md +++ b/README.md @@ -1,44 +1,44 @@ # Demo -最小 Go HTTP 服务,源码入口 `src/`,项目根是 Git 仓库边界;公开仓库为 `https://git.dever.cc/shemic/demo`。仅使用 Go 标准库,不引入业务框架。 +最小 Go HTTP 服务,源码入口 `src/`,项目根为 Git 仓库边界。公开源码仓库为 `https://git.dever.cc/shemic/demo`,仅使用 Go 标准库。 -2026-10-08(UTC)已部署到共享 dev,`https://demo-test.5dev.cn` 无需登录即可返回 `hello world`。真实 HTTP、源码同步、单服务重启、停止释放端口及 dev Pod 重建恢复均已验证;没有构建或发布业务生产镜像。 +## 开发与正式运行 -## 行为 +| 环境 | 常驻容器 | 端口 | 域名 | 运行方式 | +| --- | --- | --- | --- | --- | +| 开发 | `platform-dev/dev` | 18080 | `demo-test.5dev.cn` | Supervisor 管理 `go run .` | +| 正式 | `platform-release/release` | 28080 | `demo.5dev.cn` | Supervisor 管理已发布的二进制 | -- `GET /` 返回 `hello world` 和换行,Content-Type 为 `text/plain; charset=utf-8`。 -- 其他路径返回 404,不支持的方法返回 405;`HEAD /` 使用 HTTP 标准行为。 -- 程序读取 `PORT`,开发配置固定为 `18080`,未提供时使用 `8080`;非法端口启动失败。 -- Supervisor 程序名为 `demo`,日志是 dev 内 `/work/log/demo.log`。 +两个环境都按项目共享容器,不再为 Demo 单独保留运行 Deployment。正式端口等于开发端口加 10000;开发范围 18000–18999,正式范围 28000–28999。Service/Ingress 仍归各项目自身;正式入口配置在 `config/prod/`,选择共享 release 并转发到 28080。 -## 开发 +`GET /` 返回 `hello world` 和换行;未知路径 404,不支持的方法 405,`HEAD /` 无响应体。程序从 `PORT` 读取监听端口。入口不叠加平台登录,鉴权由项目自行实现。 + +## 日常操作 ```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 +kubectl exec -n platform-release deployment/release -c release -- supervisorctl -c /etc/supervisor/supervisord.conf status 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` 为准。 +正常发布通过 Actions,而不是手动复制开发目录中的可执行文件。需要运维重启时,将最后一条命令的 `status demo` 改为 `restart demo`,只重启该项目,不重建整个 release Pod。日志为相应容器内的 `/work/log/demo.log`。 ## 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 会清理。 +- `.gitea/workflows/ci.yaml`:推送 main 或手动触发,固定提交清洁检出,运行格式检查、`go test`、`go vet` 和编译;不发布生产。 +- `.gitea/workflows/release.yaml`:手动选择 Git ref 和 `operation=release`,构建版本化二进制,不再为项目推送运行镜像。 +- 发布配置在 `config/prod/release.json`,流水线校验项目名、开发/正式端口对应关系与当前开发配置,并拒绝与其他已发布项目冲突的正式端口。 +- 候选二进制先在临时 Job 中使用实际 release 基础镜像运行并验证 HTTP,再原子切换该项目的 `current`,仅重启其 Supervisor 进程,验证内部 HTTP 与正式 HTTPS。 +- `operation=rollback` 配合 `target_revision=<完整40位提交>`,只能回退到曾通过验证的二进制版本。复用校验过的产物,不重新编译。失败发布会尝试恢复之前的项目版本,并保留失败记录。 +- 二进制归档为 `/dm/service/platform/release/artifacts/demo//app`、`app.sha256`、`release.json`;正式运行副本为 `/dm/runtime/platform/release/programs/demo/versions//`,`current` 指向选定版本。它们不进入源码 Git。 +- 2026-10-09 切换前的旧项目镜像仍保留在 `images/`,仅用于历史恢复。新流水线的回退目标是新二进制发行,不把旧镜像记录冒充新发行。 -`config/namespace.yaml` 和 `config/prod/` 保存生产拓扑和首次验证通过的镜像基线。当前生产镜像与副本数由发布流程管理;不要直接重套基线配置覆盖正在运行的版本。更新平台拓扑前需保留当前固定镜像摘要和副本数,审查差异后再应用;日常发布和回退使用 Actions。 +项目已有的 package-write Actions Secret 不再被工作流引用;当前构建和发布不需要镜像仓库写凭据。平台基础镜像只在工具链或运行环境变化时重建,并固定 digest。 -Runner、工具镜像、缓存和角色配置归 `/dm/service/platform/ci/`。Runner 仅接受本仓库任务、并发为 1;构建 Pod 不挂 Kubernetes 令牌,部署 Pod 的生产权限仅能读取和更新本项目的 `demo` Deployment。只有可信仓库维护者可以修改工作流或触发发布,不接收外部 fork/PR 的自动任务。 +## 边界 -2026-10-09 首次自动 CI 和手动发布已通过,开发与正式域名均已实际返回 `hello world`。后续添加依赖时,将 `go.mod`、`go.sum` 一起提交;构建的下载、测试和编译步骤复用同一模块缓存,不把缓存内容提交到 Git。 +复用 `platform-ci/runner`,标签为 `go-build`、`release-deploy`。Runner 注册到可信的 `shemic` 组织,并发为 1;不为新项目常驻新的 Runner。不要给不可信贡献者工作流修改或生产发布权限,不启用外部 fork/PR 自动执行。 + +构建 Pod 不挂 Kubernetes 令牌和正式运行目录;部署 Pod 可以写共享程序目录并 exec 到共享 release,因此是可信平台发布权限,而不是项目间安全隔离。release 不挂源码、开发缓存、集群令牌或宿主机 socket,程序目录只读;整个 release Pod 重启影响其中所有项目。 + +新增项目只需自己的源码、工作流、端口/健康检查配置及 Service/Ingress,不增加项目运行 Deployment。首次新增入口仍需经过平台授权部署,发布流水线本身不修改集群入口。 diff --git a/build/Dockerfile b/build/Dockerfile index d78f037..24ae16c 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -10,12 +10,5 @@ RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/g && 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"] +FROM scratch AS artifact +COPY --from=build /out/demo /app diff --git a/config/namespace.yaml b/config/namespace.yaml deleted file mode 100644 index c308abf..0000000 --- a/config/namespace.yaml +++ /dev/null @@ -1,6 +0,0 @@ -apiVersion: v1 -kind: Namespace -metadata: - name: project-demo - labels: - app.kubernetes.io/part-of: demo diff --git a/config/prod/deployment.yaml b/config/prod/deployment.yaml deleted file mode 100644 index c520306..0000000 --- a/config/prod/deployment.yaml +++ /dev/null @@ -1,66 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: demo - namespace: project-demo -spec: - replicas: 1 - 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@sha256:25c2c028d9e241424ff91391597a8d349036764d38a66192266ad7d5cdd26e38 - 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 diff --git a/config/prod/ingress.yaml b/config/prod/ingress.yaml index 2b5b5f5..063f86c 100644 --- a/config/prod/ingress.yaml +++ b/config/prod/ingress.yaml @@ -2,9 +2,10 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: demo - namespace: project-demo + namespace: platform-release annotations: traefik.ingress.kubernetes.io/router.entrypoints: web + traefik.ingress.kubernetes.io/router.priority: '100' spec: ingressClassName: traefik rules: diff --git a/config/prod/kustomization.yaml b/config/prod/kustomization.yaml index 0be9968..ab2ea8d 100644 --- a/config/prod/kustomization.yaml +++ b/config/prod/kustomization.yaml @@ -1,8 +1,5 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - - deployment.yaml - service.yaml - ingress.yaml - - networkpolicy.yaml - - rbac.yaml diff --git a/config/prod/networkpolicy.yaml b/config/prod/networkpolicy.yaml deleted file mode 100644 index 386d5f8..0000000 --- a/config/prod/networkpolicy.yaml +++ /dev/null @@ -1,22 +0,0 @@ -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 diff --git a/config/prod/rbac.yaml b/config/prod/rbac.yaml deleted file mode 100644 index 556d771..0000000 --- a/config/prod/rbac.yaml +++ /dev/null @@ -1,24 +0,0 @@ -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 diff --git a/config/prod/release.json b/config/prod/release.json new file mode 100644 index 0000000..b5ee680 --- /dev/null +++ b/config/prod/release.json @@ -0,0 +1,7 @@ +{ + "project": "demo", + "development_port": 18080, + "production_port": 28080, + "host": "demo.5dev.cn", + "expected_body": "hello world\n" +} diff --git a/config/prod/service.yaml b/config/prod/service.yaml index 19b2641..3444ecc 100644 --- a/config/prod/service.yaml +++ b/config/prod/service.yaml @@ -2,11 +2,11 @@ apiVersion: v1 kind: Service metadata: name: demo - namespace: project-demo + namespace: platform-release spec: selector: - app.kubernetes.io/name: demo + app.kubernetes.io/name: release ports: - name: http port: 80 - targetPort: http + targetPort: 28080