bd26c8894f
Capture the production plugin rollout workflow and Qdrant validation steps so operations stay repeatable. Also harden redMCP stdio/schema compatibility to keep diverse MCP clients and validators working.
450 lines
12 KiB
Bash
Executable File
450 lines
12 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
|
REPO_ROOT="$SCRIPT_DIR"
|
|
REDMINE_ROOT="/usr/share/redmine"
|
|
BACKUP_ROOT="/root/redmine-plugin-backups"
|
|
|
|
REDMINE_URL="${REDMINE_URL:-}"
|
|
REDMINE_API_KEY="${REDMINE_API_KEY:-${REDMNINE_API_KEY:-}}"
|
|
HELPDESK_ISSUE_ID="${HELPDESK_ISSUE_ID:-}"
|
|
NON_HELPDESK_ISSUE_ID="${NON_HELPDESK_ISSUE_ID:-}"
|
|
|
|
SKIP_CONTACTS=0
|
|
SKIP_HELPDESK=0
|
|
SKIP_OUTBOX=0
|
|
|
|
ACTION="deploy"
|
|
ROLLBACK_DIR=""
|
|
APPLY=0
|
|
APPLY_SET=0
|
|
PRINT_CHANGE_MAP=0
|
|
|
|
PLUGINS=(redmine_contacts redmine_contacts_helpdesk redmine_event_outbox)
|
|
SELECTED_PLUGINS=()
|
|
BACKUP_DIR=""
|
|
ROLLBACK_RUNNING=0
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage:
|
|
./deploy_redmine_prod_patches.sh [options]
|
|
|
|
Defaults to dry-run deploy mode.
|
|
|
|
Modes:
|
|
--apply Execute actions (default is dry-run)
|
|
--dry-run Print actions only
|
|
--rollback <backup_dir> Restore plugins from a prior backup directory
|
|
|
|
Core options:
|
|
--repo-root <path> Local repo root (default: script directory)
|
|
--redmine-root <path> Production Redmine root (default: /usr/share/redmine)
|
|
--backup-root <path> Backup root for deploy mode (default: /root/redmine-plugin-backups)
|
|
|
|
Verification options:
|
|
--redmine-url <url> Redmine base URL for API checks
|
|
--api-key <key> Redmine API key for API checks
|
|
--helpdesk-issue-id <id> Known Helpdesk issue id for include=helpdesk verification
|
|
--non-helpdesk-issue-id <id> Known non-Helpdesk issue id for include=helpdesk verification
|
|
|
|
Selection options:
|
|
--skip-contacts
|
|
--skip-helpdesk
|
|
--skip-outbox
|
|
|
|
Informational:
|
|
--print-change-map Show patch groups and key files, then exit
|
|
-h, --help Show this help
|
|
|
|
Examples:
|
|
./deploy_redmine_prod_patches.sh --dry-run
|
|
./deploy_redmine_prod_patches.sh --apply --redmine-url https://redmine.example.com --api-key ... --helpdesk-issue-id 39779 --non-helpdesk-issue-id 12345
|
|
./deploy_redmine_prod_patches.sh --rollback /root/redmine-plugin-backups/prod-plugin-rollout-20260506T120000Z
|
|
EOF
|
|
}
|
|
|
|
log() {
|
|
printf '[%s] %s\n' "$(date -u +%H:%M:%S)" "$*"
|
|
}
|
|
|
|
die() {
|
|
printf 'error: %s\n' "$*" >&2
|
|
exit 1
|
|
}
|
|
|
|
print_change_map() {
|
|
cat <<'EOF'
|
|
Patch Groups -> Key Files
|
|
|
|
1) Helpdesk API include patch (semantic-index dependency)
|
|
- plugins/redmine_contacts_helpdesk/app/views/issues/show.api.rsb
|
|
- plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/issue_api_serializer.rb
|
|
- plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk.rb
|
|
|
|
2) Helpdesk search routes/controller in local helpdesk fork
|
|
- plugins/redmine_contacts_helpdesk/app/controllers/helpdesk_search_controller.rb
|
|
- plugins/redmine_contacts_helpdesk/config/routes.rb
|
|
- plugins/redmine_contacts_helpdesk/init.rb
|
|
|
|
3) POP3 compatibility fix in contacts fork
|
|
- plugins/redmine_contacts/lib/redmine_contacts/utils/check_mail.rb
|
|
|
|
4) Event outbox + Helpdesk-related hooks
|
|
- plugins/redmine_event_outbox/db/migrate/001_create_event_outbox_events.rb
|
|
- plugins/redmine_event_outbox/lib/redmine_event_outbox.rb
|
|
- plugins/redmine_event_outbox/lib/redmine_event_outbox/patches/helpdesk_ticket_patch.rb
|
|
- plugins/redmine_event_outbox/lib/redmine_event_outbox/patches/journal_message_patch.rb
|
|
|
|
Operational note:
|
|
This script deploys full plugin directories to match the post-import workflow:
|
|
- plugins/redmine_contacts/
|
|
- plugins/redmine_contacts_helpdesk/
|
|
- plugins/redmine_event_outbox/
|
|
EOF
|
|
}
|
|
|
|
cmd_string() {
|
|
local out=""
|
|
local arg
|
|
for arg in "$@"; do
|
|
out+=" $(printf '%q' "$arg")"
|
|
done
|
|
printf '%s' "${out# }"
|
|
}
|
|
|
|
run_cmd() {
|
|
log "+ $(cmd_string "$@")"
|
|
if [[ "$APPLY" -eq 1 ]]; then
|
|
"$@"
|
|
fi
|
|
}
|
|
|
|
require_path() {
|
|
local path="$1"
|
|
[[ -e "$path" ]] || die "missing required path: $path"
|
|
}
|
|
|
|
require_dir() {
|
|
local path="$1"
|
|
[[ -d "$path" ]] || die "missing required directory: $path"
|
|
}
|
|
|
|
build_selected_plugins() {
|
|
SELECTED_PLUGINS=()
|
|
if [[ "$SKIP_CONTACTS" -eq 0 ]]; then
|
|
SELECTED_PLUGINS+=("redmine_contacts")
|
|
fi
|
|
if [[ "$SKIP_HELPDESK" -eq 0 ]]; then
|
|
SELECTED_PLUGINS+=("redmine_contacts_helpdesk")
|
|
fi
|
|
if [[ "$SKIP_OUTBOX" -eq 0 ]]; then
|
|
SELECTED_PLUGINS+=("redmine_event_outbox")
|
|
fi
|
|
|
|
if [[ "${#SELECTED_PLUGINS[@]}" -eq 0 ]]; then
|
|
die "nothing selected; remove skip flags or pick at least one plugin"
|
|
fi
|
|
}
|
|
|
|
backup_plugin() {
|
|
local plugin="$1"
|
|
local src="$REDMINE_ROOT/plugins/$plugin"
|
|
local dst="$BACKUP_DIR/$plugin"
|
|
local absent_marker="$BACKUP_DIR/.${plugin}.absent"
|
|
|
|
if [[ -d "$src" ]]; then
|
|
run_cmd mkdir -p "$dst"
|
|
run_cmd rsync -a "$src/" "$dst/"
|
|
else
|
|
run_cmd mkdir -p "$BACKUP_DIR"
|
|
run_cmd touch "$absent_marker"
|
|
fi
|
|
}
|
|
|
|
restore_plugin() {
|
|
local plugin="$1"
|
|
local src="$ROLLBACK_DIR/$plugin"
|
|
local dst="$REDMINE_ROOT/plugins/$plugin"
|
|
local absent_marker="$ROLLBACK_DIR/.${plugin}.absent"
|
|
|
|
if [[ -f "$absent_marker" ]]; then
|
|
run_cmd rm -rf "$dst"
|
|
return
|
|
fi
|
|
|
|
require_dir "$src"
|
|
run_cmd mkdir -p "$dst"
|
|
run_cmd rsync -a --delete "$src/" "$dst/"
|
|
}
|
|
|
|
deploy_plugin() {
|
|
local plugin="$1"
|
|
local src="$REPO_ROOT/plugins/$plugin"
|
|
local dst="$REDMINE_ROOT/plugins/$plugin"
|
|
|
|
require_dir "$src"
|
|
run_cmd mkdir -p "$dst"
|
|
run_cmd rsync -a --delete "$src/" "$dst/"
|
|
}
|
|
|
|
verify_contacts_syntax() {
|
|
run_cmd ruby -c "$REDMINE_ROOT/plugins/redmine_contacts/lib/redmine_contacts/utils/check_mail.rb"
|
|
}
|
|
|
|
verify_helpdesk_syntax() {
|
|
run_cmd ruby -c "$REDMINE_ROOT/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk.rb"
|
|
run_cmd ruby -c "$REDMINE_ROOT/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/issue_api_serializer.rb"
|
|
run_cmd ruby -c "$REDMINE_ROOT/plugins/redmine_contacts_helpdesk/app/views/issues/show.api.rsb"
|
|
run_cmd ruby -c "$REDMINE_ROOT/plugins/redmine_contacts_helpdesk/app/models/helpdesk_mailer.rb"
|
|
}
|
|
|
|
verify_outbox_syntax() {
|
|
run_cmd ruby -c "$REDMINE_ROOT/plugins/redmine_event_outbox/lib/redmine_event_outbox.rb"
|
|
}
|
|
|
|
migrate_plugins() {
|
|
run_cmd bash -lc "cd $(printf '%q' "$REDMINE_ROOT") && RAILS_ENV=production bundle exec rake redmine:plugins:migrate"
|
|
}
|
|
|
|
restart_passenger() {
|
|
run_cmd touch "$REDMINE_ROOT/tmp/restart.txt"
|
|
}
|
|
|
|
verify_outbox_table() {
|
|
if [[ "$SKIP_OUTBOX" -eq 1 ]]; then
|
|
return
|
|
fi
|
|
run_cmd bash -lc "cd $(printf '%q' "$REDMINE_ROOT") && RAILS_ENV=production bundle exec ruby -e \"require './config/environment'; abort('missing event_outbox_events') unless ActiveRecord::Base.connection.table_exists?(:event_outbox_events); puts 'OK event_outbox_events'\""
|
|
}
|
|
|
|
verify_helpdesk_api() {
|
|
if [[ "$SKIP_HELPDESK" -eq 1 ]]; then
|
|
return
|
|
fi
|
|
|
|
if [[ -z "$REDMINE_URL" || -z "$REDMINE_API_KEY" ]]; then
|
|
log "Skipping API verification (set --redmine-url and --api-key to enable)"
|
|
return
|
|
fi
|
|
|
|
local tmp1 tmp2
|
|
tmp1="$(mktemp)"
|
|
tmp2="$(mktemp)"
|
|
|
|
if [[ -n "$HELPDESK_ISSUE_ID" ]]; then
|
|
run_cmd curl -fsS -H "X-Redmine-API-Key: $REDMINE_API_KEY" "${REDMINE_URL%/}/issues/${HELPDESK_ISSUE_ID}.json?include=journals,helpdesk" -o "$tmp1"
|
|
run_cmd grep -q '"helpdesk_ticket"' "$tmp1"
|
|
else
|
|
log "Skipping Helpdesk issue include check (set --helpdesk-issue-id)"
|
|
fi
|
|
|
|
if [[ -n "$NON_HELPDESK_ISSUE_ID" ]]; then
|
|
run_cmd curl -fsS -H "X-Redmine-API-Key: $REDMINE_API_KEY" "${REDMINE_URL%/}/issues/${NON_HELPDESK_ISSUE_ID}.json?include=helpdesk" -o "$tmp2"
|
|
run_cmd grep -q '"issue"' "$tmp2"
|
|
else
|
|
log "Skipping non-Helpdesk include check (set --non-helpdesk-issue-id)"
|
|
fi
|
|
|
|
if [[ "$APPLY" -eq 1 ]]; then
|
|
rm -f "$tmp1" "$tmp2"
|
|
else
|
|
log "Temporary API response files (dry-run placeholders): $tmp1 $tmp2"
|
|
fi
|
|
}
|
|
|
|
rollback_selected_plugins() {
|
|
local plugin
|
|
for plugin in "${SELECTED_PLUGINS[@]}"; do
|
|
log "Restoring plugin: $plugin"
|
|
restore_plugin "$plugin"
|
|
done
|
|
restart_passenger
|
|
}
|
|
|
|
on_error() {
|
|
local line="$1"
|
|
local rc="$2"
|
|
if [[ "$ROLLBACK_RUNNING" -eq 1 ]]; then
|
|
exit "$rc"
|
|
fi
|
|
log "Failure at line ${line} (exit ${rc})"
|
|
if [[ "$ACTION" == "deploy" && "$APPLY" -eq 1 && -n "$BACKUP_DIR" ]]; then
|
|
ROLLBACK_RUNNING=1
|
|
ROLLBACK_DIR="$BACKUP_DIR"
|
|
log "Attempting automatic rollback from: $ROLLBACK_DIR"
|
|
rollback_selected_plugins || true
|
|
log "Automatic rollback finished"
|
|
fi
|
|
exit "$rc"
|
|
}
|
|
|
|
parse_args() {
|
|
while [[ "$#" -gt 0 ]]; do
|
|
case "$1" in
|
|
--apply)
|
|
APPLY=1
|
|
APPLY_SET=1
|
|
;;
|
|
--dry-run)
|
|
APPLY=0
|
|
APPLY_SET=1
|
|
;;
|
|
--rollback)
|
|
ACTION="rollback"
|
|
shift
|
|
[[ "$#" -gt 0 ]] || die "--rollback requires a backup directory"
|
|
ROLLBACK_DIR="$1"
|
|
;;
|
|
--repo-root)
|
|
shift
|
|
[[ "$#" -gt 0 ]] || die "--repo-root requires a path"
|
|
REPO_ROOT="$1"
|
|
;;
|
|
--redmine-root)
|
|
shift
|
|
[[ "$#" -gt 0 ]] || die "--redmine-root requires a path"
|
|
REDMINE_ROOT="$1"
|
|
;;
|
|
--backup-root)
|
|
shift
|
|
[[ "$#" -gt 0 ]] || die "--backup-root requires a path"
|
|
BACKUP_ROOT="$1"
|
|
;;
|
|
--redmine-url)
|
|
shift
|
|
[[ "$#" -gt 0 ]] || die "--redmine-url requires a value"
|
|
REDMINE_URL="$1"
|
|
;;
|
|
--api-key)
|
|
shift
|
|
[[ "$#" -gt 0 ]] || die "--api-key requires a value"
|
|
REDMINE_API_KEY="$1"
|
|
;;
|
|
--helpdesk-issue-id)
|
|
shift
|
|
[[ "$#" -gt 0 ]] || die "--helpdesk-issue-id requires a value"
|
|
HELPDESK_ISSUE_ID="$1"
|
|
;;
|
|
--non-helpdesk-issue-id)
|
|
shift
|
|
[[ "$#" -gt 0 ]] || die "--non-helpdesk-issue-id requires a value"
|
|
NON_HELPDESK_ISSUE_ID="$1"
|
|
;;
|
|
--skip-contacts)
|
|
SKIP_CONTACTS=1
|
|
;;
|
|
--skip-helpdesk)
|
|
SKIP_HELPDESK=1
|
|
;;
|
|
--skip-outbox)
|
|
SKIP_OUTBOX=1
|
|
;;
|
|
--print-change-map)
|
|
PRINT_CHANGE_MAP=1
|
|
;;
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
die "unknown argument: $1"
|
|
;;
|
|
esac
|
|
shift
|
|
done
|
|
}
|
|
|
|
main() {
|
|
parse_args "$@"
|
|
|
|
if [[ "$PRINT_CHANGE_MAP" -eq 1 ]]; then
|
|
print_change_map
|
|
exit 0
|
|
fi
|
|
|
|
if [[ "$ACTION" == "rollback" && "$APPLY_SET" -eq 0 ]]; then
|
|
APPLY=1
|
|
fi
|
|
|
|
if [[ "$APPLY" -eq 1 && "$EUID" -ne 0 ]]; then
|
|
die "--apply and --rollback apply mode require root"
|
|
fi
|
|
|
|
require_dir "$REDMINE_ROOT"
|
|
require_dir "$REDMINE_ROOT/plugins"
|
|
require_path "$REDMINE_ROOT/tmp"
|
|
|
|
build_selected_plugins
|
|
trap 'on_error ${LINENO} $?' ERR
|
|
|
|
if [[ "$ACTION" == "rollback" ]]; then
|
|
require_dir "$ROLLBACK_DIR"
|
|
log "Mode: rollback ($([[ "$APPLY" -eq 1 ]] && echo apply || echo dry-run))"
|
|
log "Redmine root: $REDMINE_ROOT"
|
|
log "Backup source: $ROLLBACK_DIR"
|
|
rollback_selected_plugins
|
|
trap - ERR
|
|
log "Rollback completed"
|
|
exit 0
|
|
fi
|
|
|
|
require_dir "$REPO_ROOT"
|
|
require_dir "$REPO_ROOT/plugins"
|
|
|
|
BACKUP_DIR="$BACKUP_ROOT/prod-plugin-rollout-$(date -u +%Y%m%dT%H%M%SZ)"
|
|
|
|
log "Mode: deploy ($([[ "$APPLY" -eq 1 ]] && echo apply || echo dry-run))"
|
|
log "Repo root: $REPO_ROOT"
|
|
log "Redmine root: $REDMINE_ROOT"
|
|
log "Backup dir: $BACKUP_DIR"
|
|
log "Selected plugins: ${SELECTED_PLUGINS[*]}"
|
|
|
|
run_cmd mkdir -p "$BACKUP_DIR"
|
|
|
|
local plugin
|
|
for plugin in "${SELECTED_PLUGINS[@]}"; do
|
|
log "Backing up plugin: $plugin"
|
|
backup_plugin "$plugin"
|
|
done
|
|
|
|
if [[ "$SKIP_CONTACTS" -eq 0 ]]; then
|
|
log "Deploying plugin: redmine_contacts"
|
|
deploy_plugin "redmine_contacts"
|
|
log "Verifying syntax: redmine_contacts"
|
|
verify_contacts_syntax
|
|
fi
|
|
|
|
if [[ "$SKIP_HELPDESK" -eq 0 ]]; then
|
|
log "Deploying plugin: redmine_contacts_helpdesk"
|
|
deploy_plugin "redmine_contacts_helpdesk"
|
|
log "Verifying syntax: redmine_contacts_helpdesk"
|
|
verify_helpdesk_syntax
|
|
fi
|
|
|
|
if [[ "$SKIP_OUTBOX" -eq 0 ]]; then
|
|
log "Deploying plugin: redmine_event_outbox"
|
|
deploy_plugin "redmine_event_outbox"
|
|
log "Verifying syntax: redmine_event_outbox"
|
|
verify_outbox_syntax
|
|
fi
|
|
|
|
log "Running plugin migrations"
|
|
migrate_plugins
|
|
|
|
log "Restarting Passenger"
|
|
restart_passenger
|
|
|
|
log "Running runtime verifications"
|
|
verify_outbox_table
|
|
verify_helpdesk_api
|
|
|
|
trap - ERR
|
|
log "Deploy completed successfully"
|
|
log "Rollback command: $(cmd_string "$0" --rollback "$BACKUP_DIR")"
|
|
}
|
|
|
|
main "$@"
|