Add production rollout tooling and semantic index ops docs
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.
This commit is contained in:
Executable
+449
@@ -0,0 +1,449 @@
|
||||
#!/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 "$@"
|
||||
Reference in New Issue
Block a user