# .gitea/workflows/promote.yml # Manual promotion workflow to copy staged artefacts to production name: Promote Feedser (Manual) on: workflow_dispatch: inputs: include_docs: description: 'Also promote the generated documentation bundle' required: false default: 'true' type: boolean tag: description: 'Optional build identifier to record in the summary' required: false default: 'latest' type: string jobs: promote: runs-on: ubuntu-22.04 environment: production steps: - name: Checkout repository uses: actions/checkout@v4 - name: Resolve staging credentials id: staging run: | missing=() host="${{ secrets.STAGING_DEPLOYMENT_HOST }}" if [ -z "$host" ]; then host="${{ vars.STAGING_DEPLOYMENT_HOST }}"; fi if [ -z "$host" ]; then host="${{ secrets.DEPLOYMENT_HOST }}"; fi if [ -z "$host" ]; then host="${{ vars.DEPLOYMENT_HOST }}"; fi if [ -z "$host" ]; then missing+=("STAGING_DEPLOYMENT_HOST"); fi user="${{ secrets.STAGING_DEPLOYMENT_USERNAME }}" if [ -z "$user" ]; then user="${{ vars.STAGING_DEPLOYMENT_USERNAME }}"; fi if [ -z "$user" ]; then user="${{ secrets.DEPLOYMENT_USERNAME }}"; fi if [ -z "$user" ]; then user="${{ vars.DEPLOYMENT_USERNAME }}"; fi if [ -z "$user" ]; then missing+=("STAGING_DEPLOYMENT_USERNAME"); fi path="${{ secrets.STAGING_DEPLOYMENT_PATH }}" if [ -z "$path" ]; then path="${{ vars.STAGING_DEPLOYMENT_PATH }}"; fi if [ -z "$path" ]; then missing+=("STAGING_DEPLOYMENT_PATH") fi docs_path="${{ secrets.STAGING_DOCS_PATH }}" if [ -z "$docs_path" ]; then docs_path="${{ vars.STAGING_DOCS_PATH }}"; fi key="${{ secrets.STAGING_DEPLOYMENT_KEY }}" if [ -z "$key" ]; then key="${{ secrets.DEPLOYMENT_KEY }}"; fi if [ -z "$key" ]; then key="${{ vars.STAGING_DEPLOYMENT_KEY }}"; fi if [ -z "$key" ]; then key="${{ vars.DEPLOYMENT_KEY }}"; fi if [ -z "$key" ]; then missing+=("STAGING_DEPLOYMENT_KEY"); fi if [ ${#missing[@]} -gt 0 ]; then echo "❌ Missing staging configuration: ${missing[*]}" exit 1 fi key_file="$RUNNER_TEMP/staging_key" printf '%s\n' "$key" > "$key_file" chmod 600 "$key_file" echo "host=$host" >> $GITHUB_OUTPUT echo "user=$user" >> $GITHUB_OUTPUT echo "path=$path" >> $GITHUB_OUTPUT echo "docs-path=$docs_path" >> $GITHUB_OUTPUT echo "key-file=$key_file" >> $GITHUB_OUTPUT - name: Resolve production credentials id: production run: | missing=() host="${{ secrets.PRODUCTION_DEPLOYMENT_HOST }}" if [ -z "$host" ]; then host="${{ vars.PRODUCTION_DEPLOYMENT_HOST }}"; fi if [ -z "$host" ]; then host="${{ secrets.DEPLOYMENT_HOST }}"; fi if [ -z "$host" ]; then host="${{ vars.DEPLOYMENT_HOST }}"; fi if [ -z "$host" ]; then missing+=("PRODUCTION_DEPLOYMENT_HOST"); fi user="${{ secrets.PRODUCTION_DEPLOYMENT_USERNAME }}" if [ -z "$user" ]; then user="${{ vars.PRODUCTION_DEPLOYMENT_USERNAME }}"; fi if [ -z "$user" ]; then user="${{ secrets.DEPLOYMENT_USERNAME }}"; fi if [ -z "$user" ]; then user="${{ vars.DEPLOYMENT_USERNAME }}"; fi if [ -z "$user" ]; then missing+=("PRODUCTION_DEPLOYMENT_USERNAME"); fi path="${{ secrets.PRODUCTION_DEPLOYMENT_PATH }}" if [ -z "$path" ]; then path="${{ vars.PRODUCTION_DEPLOYMENT_PATH }}"; fi if [ -z "$path" ]; then missing+=("PRODUCTION_DEPLOYMENT_PATH") fi docs_path="${{ secrets.PRODUCTION_DOCS_PATH }}" if [ -z "$docs_path" ]; then docs_path="${{ vars.PRODUCTION_DOCS_PATH }}"; fi key="${{ secrets.PRODUCTION_DEPLOYMENT_KEY }}" if [ -z "$key" ]; then key="${{ secrets.DEPLOYMENT_KEY }}"; fi if [ -z "$key" ]; then key="${{ vars.PRODUCTION_DEPLOYMENT_KEY }}"; fi if [ -z "$key" ]; then key="${{ vars.DEPLOYMENT_KEY }}"; fi if [ -z "$key" ]; then missing+=("PRODUCTION_DEPLOYMENT_KEY"); fi if [ ${#missing[@]} -gt 0 ]; then echo "❌ Missing production configuration: ${missing[*]}" exit 1 fi key_file="$RUNNER_TEMP/production_key" printf '%s\n' "$key" > "$key_file" chmod 600 "$key_file" echo "host=$host" >> $GITHUB_OUTPUT echo "user=$user" >> $GITHUB_OUTPUT echo "path=$path" >> $GITHUB_OUTPUT echo "docs-path=$docs_path" >> $GITHUB_OUTPUT echo "key-file=$key_file" >> $GITHUB_OUTPUT - name: Install rsync run: | if command -v rsync >/dev/null 2>&1; then exit 0 fi CACHE_DIR="${CI_CACHE_ROOT:-/tmp}/apt" mkdir -p "$CACHE_DIR" KEY="rsync-$(lsb_release -rs 2>/dev/null || echo unknown)" DEB_DIR="$CACHE_DIR/$KEY" mkdir -p "$DEB_DIR" if ls "$DEB_DIR"/rsync*.deb >/dev/null 2>&1; then apt-get update apt-get install -y --no-install-recommends "$DEB_DIR"/libpopt0*.deb "$DEB_DIR"/rsync*.deb else apt-get update apt-get download rsync libpopt0 mv rsync*.deb libpopt0*.deb "$DEB_DIR"/ dpkg -i "$DEB_DIR"/libpopt0*.deb "$DEB_DIR"/rsync*.deb || apt-get install -f -y fi - name: Fetch staging artefacts id: fetch run: | staging_root="${{ runner.temp }}/staging" mkdir -p "$staging_root/service" "$staging_root/docs" echo "đŸ“Ĩ Copying service bundle from staging" rsync -az --delete \ -e "ssh -i ${{ steps.staging.outputs['key-file'] }} -o StrictHostKeyChecking=no" \ "${{ steps.staging.outputs.user }}@${{ steps.staging.outputs.host }}:${{ steps.staging.outputs.path }}/" \ "$staging_root/service/" if [ "${{ github.event.inputs.include_docs }}" = "true" ] && [ -n "${{ steps.staging.outputs['docs-path'] }}" ]; then echo "đŸ“Ĩ Copying documentation bundle from staging" rsync -az --delete \ -e "ssh -i ${{ steps.staging.outputs['key-file'] }} -o StrictHostKeyChecking=no" \ "${{ steps.staging.outputs.user }}@${{ steps.staging.outputs.host }}:${{ steps.staging.outputs['docs-path'] }}/" \ "$staging_root/docs/" else echo "â„šī¸ Documentation promotion skipped" fi echo "service-dir=$staging_root/service" >> $GITHUB_OUTPUT echo "docs-dir=$staging_root/docs" >> $GITHUB_OUTPUT - name: Backup production service content run: | ssh -o StrictHostKeyChecking=no -i "${{ steps.production.outputs['key-file'] }}" \ "${{ steps.production.outputs.user }}@${{ steps.production.outputs.host }}" \ "set -e; TARGET='${{ steps.production.outputs.path }}'; \ if [ -d \"$TARGET\" ]; then \ parent=\$(dirname \"$TARGET\"); \ base=\$(basename \"$TARGET\"); \ backup=\"\$parent/\${base}.backup.\$(date +%Y%m%d_%H%M%S)\"; \ mkdir -p \"\$backup\"; \ rsync -a --delete \"$TARGET/\" \"\$backup/\"; \ ls -dt \"\$parent/\${base}.backup.*\" 2>/dev/null | tail -n +6 | xargs rm -rf || true; \ echo 'Backup created at ' \"\$backup\"; \ else \ echo 'Production service path missing; skipping backup'; \ fi" - name: Publish service to production run: | rsync -az --delete \ -e "ssh -i ${{ steps.production.outputs['key-file'] }} -o StrictHostKeyChecking=no" \ "${{ steps.fetch.outputs['service-dir'] }}/" \ "${{ steps.production.outputs.user }}@${{ steps.production.outputs.host }}:${{ steps.production.outputs.path }}/" - name: Promote documentation bundle if: github.event.inputs.include_docs == 'true' && steps.production.outputs['docs-path'] != '' run: | rsync -az --delete \ -e "ssh -i ${{ steps.production.outputs['key-file'] }} -o StrictHostKeyChecking=no" \ "${{ steps.fetch.outputs['docs-dir'] }}/" \ "${{ steps.production.outputs.user }}@${{ steps.production.outputs.host }}:${{ steps.production.outputs['docs-path'] }}/" - name: Promotion summary run: | echo "✅ Promotion completed" echo " Tag: ${{ github.event.inputs.tag }}" echo " Service: ${{ steps.staging.outputs.host }} → ${{ steps.production.outputs.host }}" if [ "${{ github.event.inputs.include_docs }}" = "true" ]; then echo " Docs: included" else echo " Docs: skipped" fi