From 969e88670f5cb3dd0156e4a53bd84d729be4fe82 Mon Sep 17 00:00:00 2001 From: Citali Date: Wed, 11 Feb 2026 12:19:31 +0100 Subject: [PATCH] ci(delivery): add deploy and release workflow scaffolds --- .gitea/workflows/deploy.yml | 54 ++++++++++++ .gitea/workflows/release.yml | 82 +++++++++++++++++++ docs/product-engineering/delivery-pipeline.md | 77 +++++++++++++++++ 3 files changed, 213 insertions(+) create mode 100644 .gitea/workflows/deploy.yml create mode 100644 .gitea/workflows/release.yml create mode 100644 docs/product-engineering/delivery-pipeline.md diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..efbf8fe --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,54 @@ +name: CMS Deploy + +on: + workflow_dispatch: + inputs: + environment: + description: "Target environment" + required: true + type: choice + options: + - staging + - production + image_tag: + description: "Image tag to deploy (e.g. v0.1.0)" + required: true + rollback_tag: + description: "Optional rollback tag" + required: false + +jobs: + deploy: + name: Deploy Compose Stack + runs-on: ubuntu-latest + steps: + - name: Resolve deployment target + id: target + run: | + if [ "${{ github.event.inputs.environment }}" = "staging" ]; then + echo "host=${{ secrets.CMS_STAGING_HOST }}" >> "$GITHUB_OUTPUT" + echo "user=${{ secrets.CMS_STAGING_USER }}" >> "$GITHUB_OUTPUT" + echo "compose=docker-compose.staging.yml" >> "$GITHUB_OUTPUT" + else + echo "host=${{ secrets.CMS_PRODUCTION_HOST }}" >> "$GITHUB_OUTPUT" + echo "user=${{ secrets.CMS_PRODUCTION_USER }}" >> "$GITHUB_OUTPUT" + echo "compose=docker-compose.production.yml" >> "$GITHUB_OUTPUT" + fi + + - name: Setup SSH + run: | + mkdir -p ~/.ssh + echo "${{ secrets.CMS_DEPLOY_KEY }}" > ~/.ssh/id_rsa + chmod 600 ~/.ssh/id_rsa + ssh-keyscan -H "${{ steps.target.outputs.host }}" >> ~/.ssh/known_hosts + + - name: Deploy image tag + run: | + ssh "${{ steps.target.outputs.user }}@${{ steps.target.outputs.host }}" \ + "cd ${{ secrets.CMS_REMOTE_DEPLOY_PATH }} && CMS_IMAGE_TAG=${{ github.event.inputs.image_tag }} docker compose -f ${{ steps.target.outputs.compose }} up -d" + + - name: Optional rollback + if: github.event.inputs.rollback_tag != '' + run: | + ssh "${{ steps.target.outputs.user }}@${{ steps.target.outputs.host }}" \ + "cd ${{ secrets.CMS_REMOTE_DEPLOY_PATH }} && CMS_IMAGE_TAG=${{ github.event.inputs.rollback_tag }} docker compose -f ${{ steps.target.outputs.compose }} up -d" diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..4d497ac --- /dev/null +++ b/.gitea/workflows/release.yml @@ -0,0 +1,82 @@ +name: CMS Release + +on: + push: + tags: + - "v*" + workflow_dispatch: + inputs: + release_tag: + description: "Release tag in vX.Y.Z format" + required: true + rollback_image_tag: + description: "Optional rollback image tag" + required: false + +env: + BUN_VERSION: "1.3.5" + REGISTRY: ${{ secrets.CMS_IMAGE_REGISTRY }} + IMAGE_NAMESPACE: ${{ secrets.CMS_IMAGE_NAMESPACE }} + +jobs: + release: + name: Build Push Changelog + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: ${{ env.BUN_VERSION }} + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Resolve release tag + id: tag + run: | + if [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]; then + echo "value=${{ github.event.inputs.release_tag }}" >> "$GITHUB_OUTPUT" + else + echo "value=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT" + fi + + - name: Validate tag against package version + run: sh .gitea/scripts/validate-tag-version.sh "${{ steps.tag.outputs.value }}" + + - name: Generate changelog + run: bun run changelog:release + + - name: Login to image registry + run: | + echo "${{ secrets.CMS_IMAGE_REGISTRY_PASSWORD }}" | docker login "${{ env.REGISTRY }}" -u "${{ secrets.CMS_IMAGE_REGISTRY_USER }}" --password-stdin + + - name: Build and push web image + run: | + image="${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/cms-web:${{ steps.tag.outputs.value }}" + docker build -f apps/web/Dockerfile -t "$image" . + docker push "$image" + + - name: Build and push admin image + run: | + image="${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/cms-admin:${{ steps.tag.outputs.value }}" + docker build -f apps/admin/Dockerfile -t "$image" . + docker push "$image" + + - name: Release notes placeholder + run: | + echo "Release tag: ${{ steps.tag.outputs.value }}" + echo "TODO: publish CHANGELOG.md content to release notes in Gitea." + + rollback: + name: Rollback (Manual) + if: github.event_name == 'workflow_dispatch' && github.event.inputs.rollback_image_tag != '' + runs-on: ubuntu-latest + needs: release + steps: + - name: Rollback placeholder + run: | + echo "Rollback to image tag: ${{ github.event.inputs.rollback_image_tag }}" + echo "TODO: apply compose update with rollback image tags on production host." diff --git a/docs/product-engineering/delivery-pipeline.md b/docs/product-engineering/delivery-pipeline.md new file mode 100644 index 0000000..ed60a8d --- /dev/null +++ b/docs/product-engineering/delivery-pipeline.md @@ -0,0 +1,77 @@ +# Delivery Pipeline + +## Scope + +Operational pipeline baseline for image build/push, staging deploy, production promotion, and rollback. + +## Registry Credentials Strategy + +Use scoped Gitea secrets: + +- `CMS_IMAGE_REGISTRY` +- `CMS_IMAGE_NAMESPACE` +- `CMS_IMAGE_REGISTRY_USER` +- `CMS_IMAGE_REGISTRY_PASSWORD` + +Policy: + +- credentials only in CI secrets +- no plaintext credentials in repo +- least privilege: push/pull for target namespace only + +## Build and Push Flow + +- Workflow: `.gitea/workflows/release.yml` +- Trigger: + - tag push `vX.Y.Z` + - manual `workflow_dispatch` +- Steps: + 1. validate tag vs root `package.json` version + 2. generate changelog + 3. docker login + 4. build and push `cms-web` and `cms-admin` images + +## Staging Deployment Automation + +- Workflow: `.gitea/workflows/deploy.yml` +- Manual input: + - `environment=staging` + - `image_tag=vX.Y.Z` +- Remote deployment uses SSH + compose file: + - `docker-compose.staging.yml` + +Required secrets: + +- `CMS_STAGING_HOST` +- `CMS_STAGING_USER` +- `CMS_DEPLOY_KEY` +- `CMS_REMOTE_DEPLOY_PATH` + +## Production Promotion and Rollback + +Promotion: + +- run deploy workflow with: + - `environment=production` + - `image_tag=vX.Y.Z` + +Rollback: + +- release workflow supports rollback placeholder by image tag +- deploy workflow supports `rollback_tag` input +- recovery action: + - rerun deploy with previous known-good tag + +## Deployment Verification + +After deploy: + +1. app health checks (web/admin) +2. auth smoke flow +3. i18n smoke flow +4. critical route checks (`/`, `/login`, `/todo`) + +## Notes + +- Current workflows are production-oriented scaffolds and require secret provisioning in Gitea. +- Host hardening, network ACLs, and backup policy remain mandatory operational controls.