diff --git a/.gitignore b/.gitignore index baf1031..a76d5ea 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,8 @@ Desktop.ini *.temp *.swp *~ + +# Server deployment state +/.deploy.lock +/current +/releases/ diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index f0229a3..16f1f9f 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -9,6 +9,9 @@ Release manifests are stored in `/releases/`. Each manifest records the immutable image tag and Gitea run number. The `current` symlink is changed only after the new container is healthy. If startup fails, the deployment script restarts the previous image without rebuilding it. +After health succeeds, the Git checkout at `` is fast-forwarded to +the exact deployed commit. Deployment refuses to overwrite tracked server-side +changes or a checkout that has diverged from `origin/main`. ## Gitea Actions setup diff --git a/scripts/deploy-release.sh b/scripts/deploy-release.sh index 89a43be..ea12c31 100644 --- a/scripts/deploy-release.sh +++ b/scripts/deploy-release.sh @@ -49,6 +49,40 @@ mkdir -p -- "$releases_dir" exec 9>"$deploy_root/.deploy.lock" flock -x 9 +sync_checkout() { + local branch remote_release + + if [[ ! -d "$deploy_root/.git" ]]; then + echo "Deployment root is not a Git checkout: $deploy_root" >&2 + return 1 + fi + if ! git -C "$deploy_root" diff --quiet --ignore-submodules -- \ + || ! git -C "$deploy_root" diff --cached --quiet --ignore-submodules --; then + echo "Refusing to update a checkout with tracked changes: $deploy_root" >&2 + return 1 + fi + + branch="$(git -C "$deploy_root" symbolic-ref --quiet --short HEAD || true)" + if [[ "$branch" != "main" ]]; then + echo "Deployment checkout must be on main, not ${branch:-detached HEAD}." >&2 + return 1 + fi + + git -C "$deploy_root" fetch --quiet \ + origin "+refs/heads/main:refs/remotes/origin/main" + remote_release="$(git -C "$deploy_root" rev-parse refs/remotes/origin/main)" + if [[ "$remote_release" != "$release_id" ]]; then + echo "origin/main is $remote_release, expected deployed release $release_id." >&2 + return 1 + fi + + git -C "$deploy_root" merge --ff-only --quiet refs/remotes/origin/main + if [[ "$(git -C "$deploy_root" rev-parse HEAD)" != "$release_id" ]]; then + echo "Checkout did not advance to deployed release $release_id." >&2 + return 1 + fi +} + if ! docker image inspect "$image" >/dev/null 2>&1; then echo "Verified runtime image is missing from the production Docker daemon: $image" >&2 exit 1 @@ -63,7 +97,8 @@ if [[ -n "$previous_release" && -f "$previous_release/release.env" ]]; then current_sequence="$(sed -n 's/^DEPLOY_SEQUENCE=//p' "$previous_release/release.env")" current_release="$(basename "$previous_release")" if [[ "$current_release" == "$release_id" ]]; then - echo "Release is already active: $release_id" + sync_checkout + echo "Release and server checkout are already active: $release_id" exit 0 fi if [[ "$current_sequence" =~ ^[1-9][0-9]*$ ]] && (( sequence <= current_sequence )); then @@ -144,6 +179,10 @@ if activate "$release_dir"; then new_link="$deploy_root/.current-$release_id-$$" ln -s -- "$release_dir" "$new_link" mv -Tf -- "$new_link" "$current_link" + if ! sync_checkout; then + echo "Deployment is healthy, but the server checkout could not be updated." >&2 + exit 1 + fi echo "Deployment succeeded: $release_id ($image)" exit 0 fi