aboutsummaryrefslogtreecommitdiffstats
path: root/packaging/publish.sh
blob: a697dcaea1d2b698ef10de46a05f4607b149a2cf (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
#!/usr/bin/env bash
# publish.sh — put a built release on sl0ppi.sl0p.foo.
#
#   packaging/publish.sh [--host root@sl0p.foo] [--dry-run]
#
# Publishing is deliberately dumb: rsync a rendered site plus the artifact, then
# ask the live site whether it agrees. There is no CI and no build on the box —
# the tarball must already exist (`make package`).
#
# The vhost lives in the sl0pday repo (nginx/sl0p.conf, deployed with its own
# ./deploy.sh). This script only ships content into /opt/sl0ppi/www.
set -euo pipefail

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
HOST="${SL0PPI_PUBLISH_HOST:-root@sl0p.foo}"
REMOTE_DIR="/opt/sl0ppi/www"
BASE_URL="https://sl0ppi.sl0p.foo"
dry_run=0

while [ $# -gt 0 ]; do
    case "$1" in
        --host) HOST="$2"; shift 2 ;;
        --host=*) HOST="${1#*=}"; shift ;;
        -n|--dry-run) dry_run=1; shift ;;
        -h|--help) sed -n '2,12p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
        *) echo "publish.sh: unknown argument '$1'" >&2; exit 2 ;;
    esac
done

log()  { printf '\033[36m==>\033[0m %s\n' "$*"; }
die()  { printf '\033[31merror:\033[0m %s\n' "$*" >&2; exit 1; }
run()  { if [ "$dry_run" = 1 ]; then echo "  would: $*"; else "$@"; fi; }

VERSION="$(cat "$ROOT/VERSION")"
STEM="sl0ppi-$VERSION-x86_64-linux"
ARTIFACT="$STEM.tar.gz"
OUT="$ROOT/packaging/out"

[ -f "$OUT/$ARTIFACT" ] || die "no $OUT/$ARTIFACT — run 'make package' first"
[ -f "$OUT/$ARTIFACT.sha256" ] || die "no checksum beside the artifact"

# Trust nothing: recompute rather than believing the sidecar we are about to
# publish as authoritative.
SHA="$(sha256sum "$OUT/$ARTIFACT" | cut -d' ' -f1)"
SIDECAR="$(cut -d' ' -f1 < "$OUT/$ARTIFACT.sha256")"
[ "$SHA" = "$SIDECAR" ] || die "artifact does not match its .sha256 sidecar"

# Refuse to overwrite a published version with different bytes: released URLs are
# immutable (nginx caches /releases/ for a year), so republishing a changed file
# under the same name would serve two different things to different people.
remote_sha="$(ssh -o BatchMode=yes "$HOST" \
    "sha256sum $REMOTE_DIR/releases/$ARTIFACT 2>/dev/null | cut -d' ' -f1" || true)"
if [ -n "$remote_sha" ] && [ "$remote_sha" != "$SHA" ]; then
    die "$ARTIFACT is already published with a DIFFERENT checksum
  published $remote_sha
  local     $SHA
Bump VERSION instead of rewriting a release."
fi

# --- render the site -------------------------------------------------------
STAGE="$(mktemp -d "${TMPDIR:-/tmp}/sl0ppi-publish.XXXXXX")"
trap 'rm -rf "$STAGE"' EXIT
mkdir -p "$STAGE/releases"

DATE="$(date -u +%Y-%m-%d)"
MANIFEST="$ROOT/packaging/stage/$STEM/MANIFEST"
components=""
if [ -f "$MANIFEST" ]; then
    while read -r name version; do
        [ -n "$name" ] || continue
        components+="  <tr><td>$name</td><td>$version</td><td class=\"dim\"></td></tr>"$'\n'
    done < "$MANIFEST"
else
    components="  <tr><td colspan=\"3\" class=\"dim\">see MANIFEST inside the tarball</td></tr>"
fi

python3 - "$ROOT/site/index.html.in" "$STAGE/index.html" <<PYEOF
import sys
src, dst = sys.argv[1], sys.argv[2]
html = open(src, encoding="utf-8").read()
for key, value in {
    "@VERSION@": """$VERSION""",
    "@DATE@": """$DATE""",
    "@ARTIFACT@": """$ARTIFACT""",
    "@STEM@": """$STEM""",
    "@SHA256@": """$SHA""",
    "@COMPONENTS@": """$components""",
}.items():
    html = html.replace(key, value)
leftover = [line for line in html.splitlines() if "@" in line and "@media" not in line
            and any(t in line for t in ("@VERSION@", "@DATE@", "@ARTIFACT@", "@STEM@", "@SHA256@", "@COMPONENTS@"))]
if leftover:
    raise SystemExit("unsubstituted placeholders: %r" % leftover[:3])
open(dst, "w", encoding="utf-8").write(html)
PYEOF

install -m644 "$OUT/$ARTIFACT" "$STAGE/releases/$ARTIFACT"
install -m644 "$OUT/$ARTIFACT.sha256" "$STAGE/releases/$ARTIFACT.sha256"
install -m755 "$ROOT/site/install.sh" "$STAGE/install.sh"

cat > "$STAGE/latest.json" <<EOF
{
  "version": "$VERSION",
  "artifact": "$ARTIFACT",
  "sha256": "$SHA",
  "url": "$BASE_URL/releases/$ARTIFACT",
  "published": "$DATE"
}
EOF

log "publishing sl0ppi $VERSION to $HOST:$REMOTE_DIR"

# Artifacts first, pointers second: latest.json must never name a file that is
# not downloadable yet. Same reasoning as the SPA's two-pass deploy.
run rsync -a --chmod=D755,F644 "$STAGE/releases/" "$HOST:$REMOTE_DIR/releases/"
run rsync -a --chmod=F644 "$STAGE/index.html" "$HOST:$REMOTE_DIR/index.html"
run rsync -a --chmod=F755 "$STAGE/install.sh" "$HOST:$REMOTE_DIR/install.sh"
run rsync -a --chmod=F644 "$STAGE/latest.json" "$HOST:$REMOTE_DIR/latest.json"

[ "$dry_run" = 1 ] && { log "dry run: nothing was uploaded"; exit 0; }

# --- verify against the live site ------------------------------------------
log "verifying $BASE_URL"
fail=0
check() {
    local url="$1" want="$2"
    local code
    code="$(curl -sS -o /dev/null -w '%{http_code}' "$url")"
    if [ "$code" = "$want" ]; then
        printf '   ok   %-58s %s\n' "$url" "$code"
    else
        printf '   FAIL %-58s %s (want %s)\n' "$url" "$code" "$want"; fail=1
    fi
}
check "$BASE_URL/" 200
check "$BASE_URL/install.sh" 200
check "$BASE_URL/latest.json" 200
check "$BASE_URL/releases/$ARTIFACT" 200
check "$BASE_URL/releases/$ARTIFACT.sha256" 200

published_sha="$(curl -fsSL "$BASE_URL/releases/$ARTIFACT" | sha256sum | cut -d' ' -f1)"
if [ "$published_sha" = "$SHA" ]; then
    printf '   ok   %-58s %s\n' "downloaded artifact checksum" "matches"
else
    printf '   FAIL %-58s %s\n' "downloaded artifact checksum" "MISMATCH"; fail=1
fi

[ "$fail" = 0 ] || die "the live site does not agree with what we published"
log "published: $BASE_URL"