@msout/microsoft-onenote-exporter 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,81 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.1.3] - 2026-10-02
10
+
11
+ A **patch**, and the first release driven by the automation added in the previous
12
+ one rather than by remembering to check.
13
+
14
+ ### Changed
15
+
16
+ - **`@msout/microsoft-onenote-list-notebooks` 0.0.6 → 0.0.7.** Pin bumped and the
17
+ lockfile regenerated, so it now resolves that version from the registry rather
18
+ than the previous tarball.
19
+
20
+ The contract this adapter depends on is unchanged — `listNotebooks(options)`
21
+ still returns `Array<{name, url, id}>`, and the module still exports
22
+ `listNotebooks` and `dismissMcasInterstitial` — so no code here changed.
23
+
24
+ What 0.0.7 brings, from its own release: real notebook URLs are resolved instead
25
+ of the MRU placeholder, and links are read from the MRU feed in canonical form.
26
+ That matters here because `list` prints `nb.url`, so the URLs this tool reports
27
+ are now the ones that can actually be opened.
28
+
29
+ 0.0.6 was still pinned when this was written, which is what the `stale-steps`
30
+ CI job reported on its first run and what Dependabot has a grouped PR open for.
31
+
32
+ ### Fixed
33
+
34
+ - **`entrypoint.sh` offered `--output-dir` to every subcommand.** `list`, `check`
35
+ and `logout` do not define that option, so all three died with
36
+ `error: unknown option '--output-dir'` and exit 1 — the container refused to list
37
+ anything, while the flag commander rejected had been added by the entrypoint
38
+ itself. `export` kept working, and it was the only subcommand anyone had run
39
+ through a container. Now gated on the subcommand actually being `export`.
40
+
41
+ Two tests, both of which fail against the previous entrypoint: one asserts the
42
+ injection sits *inside* the export gate by comparing source positions, because
43
+ the earlier assertion checked only that the injection existed and not which
44
+ subcommands it applied to — which is exactly why it passed against the broken
45
+ version. The other pins the subcommand match by name.
46
+
47
+ - Related: **issue #3** — `start-container.sh` still refuses `list`, `check` and
48
+ `logout` when given no flags, from a `$# -eq 0` guard written for `export` and
49
+ applied to all five subcommands. Tracked, not yet fixed; both it and the
50
+ entrypoint bug have to be resolved before `list` works through the wrapper.
51
+
52
+ Tests: 130 → 132.
53
+
54
+ ## [0.1.2] - 2026-10-02
55
+
56
+ A **patch**. It changes what is published, not what the package does.
57
+
58
+ ### Fixed
59
+
60
+ - **`entrypoint.sh` and `start-container.sh` are published again.** The `files`
61
+ whitelist listed `src/` and the four documents, so the tarball had eleven files
62
+ and neither container script — 0.1.0 and 0.1.1 both shipped that way. It
63
+ mattered because six of the fixes in 0.1.1 live inside those two files, and
64
+ because the README's Docker section is built on them: a consumer who installed
65
+ the package and then tried to run a container from it had no entrypoint to
66
+ build an image from and no wrapper to call.
67
+
68
+ The publish gate missed it because it only ever checked what must *not* ship —
69
+ no test suites, no auth state, no workflow files — and never what must. Two
70
+ tests now cover that direction, and one of them checks the executable bit too,
71
+ since a tarball carrying the scripts as `0644` would fail at the `ENTRYPOINT`
72
+ line with `Cannot exec: permission denied`.
73
+
74
+ Thirteen files, up from eleven.
75
+
76
+ ### Changed
77
+
78
+ - **`docker-output/` is gitignored.** A container run pointed at that directory
79
+ leaves `auth.json` beside the exported notes, and `git add -A` would have staged
80
+ a live full-account session.
81
+
82
+ Tests: 128 → 130.
83
+
9
84
  ## [0.1.1] - 2026-10-01
10
85
 
11
86
  A **patch**, and an unusual one: it fixes six bugs and changes no documented
package/entrypoint.sh ADDED
@@ -0,0 +1,127 @@
1
+ #!/bin/sh
2
+ # Container entrypoint for microsoft-onenote-exporter.
3
+ #
4
+ # One image, five commands. The old images took a session GUID and a notebook
5
+ # name as positional arguments, which was tied to one pipeline's idea of what a
6
+ # session is; this dispatches on the subcommand instead, so the same image can
7
+ # also be used to log in or to list.
8
+
9
+ set -e
10
+
11
+ if [ $# -eq 0 ]; then
12
+ echo "Usage: microsoft-onenote-exporter <command> [options]"
13
+ echo ""
14
+ echo "Commands:"
15
+ echo " login Sign in to Microsoft and save the session"
16
+ echo " check Report whether the saved session is still valid"
17
+ echo " logout Delete the saved session"
18
+ echo " list List the notebooks on the account"
19
+ echo " export Export one notebook to Markdown"
20
+ echo ""
21
+ echo "Example:"
22
+ echo " docker run -v ./out:/data/output microsoft-onenote-exporter \\"
23
+ echo " export --auth-file /data/output/auth.json --notebook 'Work' --non-interactive"
24
+ exit 0
25
+ fi
26
+
27
+ # A login cannot run headless: without an email and a password the browser has to
28
+ # be shown, and there is nobody at a terminal inside a container to type into it.
29
+ case "$1" in
30
+ login)
31
+ echo "ERROR: 'login' needs a visible browser, which a container has no way to show." >&2
32
+ echo " Log in on the host first, then mount the resulting auth file:" >&2
33
+ echo " microsoft-onenote-exporter login" >&2
34
+ echo " docker run -v \$HOME/.microsoft-webauth:/data/auth ..." >&2
35
+ exit 2
36
+ ;;
37
+ esac
38
+
39
+ # Allow a shell in the container for debugging, and node for poking at the CLI.
40
+ if [ "$1" = "/bin/sh" ] || [ "$1" = "sh" ]; then
41
+ exec "$@"
42
+ fi
43
+ # `shift` first, then exec node with what is left. Without the shift this runs
44
+ # `node node <script>`, which node reads as a module path named "node" and fails
45
+ # with MODULE_NOT_FOUND - so the documented debugging route
46
+ #
47
+ # docker run --rm -it microsoft-onenote-exporter node /app/src/index.js list
48
+ #
49
+ # never worked. Found by running the built image rather than by reading it.
50
+ if [ "$1" = "node" ]; then
51
+ shift
52
+ exec node "$@"
53
+ fi
54
+
55
+ # Where the notes go, unless the caller said otherwise.
56
+ #
57
+ # The container's working directory is /app, so the CLI's own default - ./output
58
+ # against the cwd - resolves to /app/output. That directory exists and is
59
+ # writable, so nothing fails: the export runs to completion and reports
60
+ # "Files saved in: /app/output/<notebook>". The notes are then destroyed with the
61
+ # container, because /app/output is inside the image rather than the mounted
62
+ # volume. A run that looked entirely successful and produced nothing on the host.
63
+ #
64
+ # /data/output is the volume mount, so that is the only default that survives.
65
+ # /app/output stays in the image as a fallback for anyone running the CLI with a
66
+ # working directory of their own choosing.
67
+ if [ ! -d /data/output ]; then
68
+ # No volume mounted: warn rather than silently writing into the image, since
69
+ # that is the failure this line exists to prevent.
70
+ echo "WARNING: no volume is mounted at /data/output." >&2
71
+ echo " Exported notes will be written inside the container and lost" >&2
72
+ echo " when it exits. Mount one, for example:" >&2
73
+ echo " -v \"\$PWD/output:/data/output\"" >&2
74
+ else
75
+ # Only `export` writes notes, so only `export` is offered --output-dir.
76
+ #
77
+ # Appending it for every subcommand broke the other three: `list`, `check` and
78
+ # `logout` do not define that option, so commander answered
79
+ #
80
+ # error: unknown option '--output-dir'
81
+ #
82
+ # and exited 1 - the container refused to list anything, while the flag it
83
+ # complained about had been added here, not by the caller. `export` kept
84
+ # working, which is why it went unnoticed: it was the only subcommand anyone
85
+ # had run through a container.
86
+ is_export=false
87
+ for arg in "$@"; do
88
+ if [ "$arg" = "export" ]; then
89
+ is_export=true
90
+ break
91
+ fi
92
+ done
93
+
94
+ if [ "$is_export" = true ]; then
95
+ # Appending --output-dir rather than exporting a variable, because the CLI
96
+ # has no environment variable for it and its default resolves against a
97
+ # cwd of /app. Only when the caller did not pass one, for the same reason
98
+ # as --auth-file below: an explicit choice must never be overridden.
99
+ has_output_dir=false
100
+ for arg in "$@"; do
101
+ if [ "$arg" = "--output-dir" ]; then
102
+ has_output_dir=true
103
+ break
104
+ fi
105
+ done
106
+ if [ "$has_output_dir" = false ]; then
107
+ set -- "$@" --output-dir /data/output
108
+ fi
109
+ fi
110
+ fi
111
+
112
+ # Only injected when the caller did not pass --auth-file themselves: appending it
113
+ # unconditionally would silently override an explicit choice, and the container
114
+ # would read a different session than the one asked for.
115
+ AUTH_FILE="${AUTH_FILE:-/data/output/auth.json}"
116
+ for arg in "$@"; do
117
+ if [ "$arg" = "--auth-file" ]; then
118
+ AUTH_FILE=""
119
+ break
120
+ fi
121
+ done
122
+
123
+ if [ -n "$AUTH_FILE" ]; then
124
+ exec node /app/src/index.js "$@" --auth-file "$AUTH_FILE"
125
+ fi
126
+
127
+ exec node /app/src/index.js "$@"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@msout/microsoft-onenote-exporter",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Log in, list and export Microsoft OneNote notebooks as Obsidian Markdown - one CLI over @msout/microsoft-webauth, @msout/microsoft-onenote-list-notebooks and @msout/microsoft-onenote-export-notebook.",
5
5
  "main": "src/index.js",
6
6
  "exports": {
@@ -14,6 +14,8 @@
14
14
  },
15
15
  "files": [
16
16
  "src/",
17
+ "entrypoint.sh",
18
+ "start-container.sh",
17
19
  "README.md",
18
20
  "LICENSE",
19
21
  "NOTICE.md",
@@ -51,7 +53,7 @@
51
53
  "homepage": "https://github.com/Ms-OneNote-Exporter/microsoft-onenote-exporter#readme",
52
54
  "dependencies": {
53
55
  "@msout/microsoft-onenote-export-notebook": "0.3.7",
54
- "@msout/microsoft-onenote-list-notebooks": "0.0.6",
56
+ "@msout/microsoft-onenote-list-notebooks": "0.0.7",
55
57
  "@msout/microsoft-webauth": "0.1.8",
56
58
  "chalk": "^4.1.2",
57
59
  "commander": "^14.0.3",
@@ -0,0 +1,301 @@
1
+ #!/bin/bash
2
+ # Runs one export in a container.
3
+ #
4
+ # Usage: ./start-container.sh --notebook "Work" [--output-dir ./out] [more flags...]
5
+ #
6
+ # Everything is overridable through the environment, because the previous script
7
+ # pointed at a sibling checkout that only existed on one machine and used a fixed
8
+ # image name, so it could not be used anywhere else.
9
+ #
10
+ # IMAGE image to run (default: microsoft-onenote-exporter)
11
+ # CONTAINER container name (default: ms_onenote_export)
12
+ # OUTPUT_DIR host dir for the export (default: ./output)
13
+ # AUTH_FILE session to use (default: the one `login` wrote)
14
+
15
+ set -euo pipefail
16
+
17
+ SESSION="$(basename "${PWD}")"
18
+ IMAGE="${IMAGE:-microsoft-onenote-exporter}"
19
+ CONTAINER="${CONTAINER:-ms_onenote_export_${SESSION}}"
20
+ OUTPUT_DIR="${OUTPUT_DIR:-./output}"
21
+
22
+ # The session defaults to where `microsoft-onenote-exporter login` writes it.
23
+ # Spelled out rather than read from @msout/microsoft-webauth/config at runtime,
24
+ # because this script runs before any node_modules is guaranteed to be present -
25
+ # it has to work in a fresh checkout. test/docker.test.js asserts this path
26
+ # matches what the CLI reports, so the two cannot drift apart silently.
27
+ #
28
+ # This used to default to $OUTPUT_DIR/auth.json, which meant the documented
29
+ # sequence - log in on the host, then run this script - always failed with "no
30
+ # auth file", because the copy step it demanded was never written down
31
+ # anywhere. Logging in on the host is the supported way to get a session into a
32
+ # container, since a container cannot run an interactive login, so the default
33
+ # simply follows it and no copy is needed.
34
+ # Which session to use, in order of preference:
35
+ #
36
+ # 1. $AUTH_FILE, if the caller set it
37
+ # 2. ./output/auth.json, the convention the README documents - the session sits
38
+ # beside the notes in the one mounted volume, so there is nothing else to
39
+ # mount and no copy step on every run
40
+ # 3. ~/.microsoft-webauth/auth-file.json, where `login` writes it, so the
41
+ # "log in on the host, then run this" sequence works with no preparation
42
+ #
43
+ # The default is NOT just (3): a check for (2) has to come first, because with the
44
+ # default set to (3) the script looks for a file named `auth-file.json` in the
45
+ # output directory and silently ignores the `auth.json` a user following the
46
+ # README actually placed there.
47
+ if [ -z "${AUTH_FILE:-}" ]; then
48
+ if [ -f "${OUTPUT_DIR}/auth.json" ]; then
49
+ AUTH_FILE="${OUTPUT_DIR}/auth.json"
50
+ else
51
+ AUTH_FILE="$HOME/.microsoft-webauth/auth-file.json"
52
+ fi
53
+ fi
54
+
55
+ # The subcommand, when the caller did not give one.
56
+ #
57
+ # This script forwards "$@" to the CLI verbatim, so it has to supply `export`
58
+ # itself - the CLI takes it as a subcommand, not as a flag. Without this the
59
+ # container received only `--notebook <name> --non-interactive` and answered
60
+ #
61
+ # error: unknown option '--notebook'
62
+ #
63
+ # which is commander refusing the first flag it saw because the subcommand that
64
+ # should have preceded it was missing. Fixed here rather than by rewriting the
65
+ # caller's arguments, so `./start-container.sh export --notebook X` also works.
66
+ SUBCOMMAND="${1:-}"
67
+ case "$SUBCOMMAND" in
68
+ login | check | logout | list | export)
69
+ shift
70
+ ;;
71
+ *)
72
+ SUBCOMMAND="export"
73
+ ;;
74
+ esac
75
+
76
+ if [ $# -eq 0 ] && [ "$SUBCOMMAND" = "export" ]; then
77
+ echo "Usage: $0 [--export] --notebook <name> | --notebook-link <url> [options...]" >&2
78
+ echo "" >&2
79
+ echo "Example:" >&2
80
+ echo " $0 --notebook 'Work' --output-dir ./out" >&2
81
+ echo "" >&2
82
+ echo "Or name the step explicitly:" >&2
83
+ echo " $0 list $0 check" >&2
84
+ exit 1
85
+ fi
86
+
87
+ if [ $# -eq 0 ]; then
88
+ echo "Usage: $0 <$SUBCOMMAND> [options...]" >&2
89
+ exit 1
90
+ fi
91
+
92
+ # One of these two must be present for a non-interactive export, and finding out
93
+ # here rather than inside the container is the difference between an explanation
94
+ # and `error: unknown option '--notebook-link'`.
95
+ if [ "$SUBCOMMAND" = "export" ]; then
96
+ has_notebook=false
97
+ for arg in "$@"; do
98
+ case "$arg" in
99
+ --notebook | --notebook-link)
100
+ has_notebook=true
101
+ ;;
102
+ esac
103
+ done
104
+ if [ "$has_notebook" = false ]; then
105
+ echo "ERROR: an export needs --notebook <name> or --notebook-link <url>." >&2
106
+ echo " This script always runs unattended, so there is no interactive picker" >&2
107
+ echo " to fall back on." >&2
108
+ exit 2
109
+ fi
110
+ fi
111
+
112
+ # The container name is derived from the working directory, so it is stable across
113
+ # runs on the same machine - which means the second run collides with the first.
114
+ # Docker refuses to reuse a name, and the error it gives ("Conflict ... already in
115
+ # use by container 302415178a51") names a hash rather than telling you what to do
116
+ # about it, so a script that is meant to be run repeatedly only worked once.
117
+ #
118
+ # A leftover container here is always from a previous run of this script: it exits
119
+ # with `docker run --detach`, so it is never expected to still be running. Removing
120
+ # it is safe for the current run and is what makes the next one possible. Named
121
+ # volumes are untouched; only the container is removed.
122
+ if docker container inspect "$CONTAINER" >/dev/null 2>&1; then
123
+ state="$(docker inspect -f '{{.State.Status}}' "$CONTAINER" 2>/dev/null || echo unknown)"
124
+ if [ "$state" = "running" ]; then
125
+ echo "ERROR: container '$CONTAINER' is already running." >&2
126
+ echo " Stop it first if that is expected, or set CONTAINER=<other name>." >&2
127
+ exit 1
128
+ fi
129
+ docker rm "$CONTAINER" >/dev/null
130
+ fi
131
+
132
+ if ! docker image inspect "$IMAGE" >/dev/null 2>&1; then
133
+ echo "ERROR: image '$IMAGE' is not built." >&2
134
+ echo " Build it: docker build -t $IMAGE ." >&2
135
+ # The message this replaces only said "build it", which is unhelpful when the
136
+ # image exists under another tag - and "docker build -t foo ." produces
137
+ # foo:latest, while a build tagged foo:test leaves foo:latest missing. So a
138
+ # script looking for an untagged name would report "not built" next to a
139
+ # perfectly good image, with nothing to suggest the retag.
140
+ # Match on the part of the name that survives a tag difference. The tag is
141
+ # stripped because that is the whole point - `foo` and `foo:test` are the
142
+ # same image, and the tag is what the user got wrong. Non-alphanumerics are
143
+ # left in place: stripping them turns `ms-onenote-exporter` into
144
+ # `msonenoteexporter`, which matches no image at all and made this branch
145
+ # silently dead.
146
+ similar="$(docker images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null \
147
+ | grep -i -- "$(printf '%s' "$IMAGE" | tr '[:upper:]' '[:lower:]' | cut -d: -f1)" || true)"
148
+ if [ -n "$similar" ]; then
149
+ echo "" >&2
150
+ echo " But these exist:" >&2
151
+ printf ' %s\n' $similar >&2
152
+ echo "" >&2
153
+ echo " Use one of them, or retag:" >&2
154
+ echo " docker tag ${similar%%:*}:${similar##*:} $IMAGE" >&2
155
+ fi
156
+ exit 1
157
+ fi
158
+
159
+ # The auth file has to exist before the container starts, because the mount is
160
+ # created from this path and Docker creates a directory when the source is
161
+ # missing - which produces a confusing "not a storage state" error from inside
162
+ # the container rather than an obvious one here.
163
+ if [ ! -f "$AUTH_FILE" ]; then
164
+ echo "ERROR: no auth file at $AUTH_FILE" >&2
165
+ echo " A container cannot run an interactive login, so sign in on the host first:" >&2
166
+ echo " microsoft-onenote-exporter login" >&2
167
+ echo " Then either copy the session into the output directory:" >&2
168
+ echo " cp ~/.microsoft-webauth/auth-file.json ./output/auth.json" >&2
169
+ echo " or point this at it directly:" >&2
170
+ echo " AUTH_FILE=~/.microsoft-webauth/auth-file.json \$0 --notebook 'Work'" >&2
171
+ exit 1
172
+ fi
173
+
174
+ mkdir -p "$OUTPUT_DIR"
175
+
176
+ # Resolve so the -v argument is valid even when the path has not been created yet.
177
+ OUTPUT_DIR_ABS="$(cd "$OUTPUT_DIR" && pwd)"
178
+ AUTH_FILE_ABS="$(cd "$(dirname "$AUTH_FILE")" && pwd)/$(basename "$AUTH_FILE")"
179
+
180
+ # The session is mounted separately, read-only, rather than being required to sit
181
+ # inside the output directory. It lives in ~/.microsoft-webauth and is a live
182
+ # credential: it does not belong in the directory the exported notes are
183
+ # collected into, and mounting it read-only means a bug in the container cannot
184
+ # rewrite or delete it. Mounting the file rather than its directory also means
185
+ # the container cannot see the other sessions sitting beside it.
186
+ AUTH_MOUNT=()
187
+ if [ ! -f "${OUTPUT_DIR_ABS}/$(basename "$AUTH_FILE_ABS")" ]; then
188
+ AUTH_MOUNT=(-v "${AUTH_FILE_ABS}:/data/auth/session.json:ro")
189
+ CONTAINER_AUTH_FILE="/data/auth/session.json"
190
+ else
191
+ # Already inside the output directory: mounting it twice would be redundant,
192
+ # and the export should write its logs and notes beside it as documented.
193
+ #
194
+ # This leaves AUTH_MOUNT empty, which is the documented primary path, and an
195
+ # empty array expanded as "${AUTH_MOUNT[@]}" under `set -u` is an error on some
196
+ # bash builds: the wrapper died with
197
+ #
198
+ # line 222: AUTH_MOUNT[@]: unbound variable
199
+ #
200
+ # before starting any container at all - so the documented way to run an
201
+ # export could not run, while the fallback path worked and hid the bug. The
202
+ # expansion below is guarded so an empty array contributes no arguments.
203
+ CONTAINER_AUTH_FILE="/data/output/$(basename "$AUTH_FILE_ABS")"
204
+ fi
205
+
206
+ # Logs go beside the notes, not inside the image. Without this the run's
207
+ # app.log - the only record of what an export actually did - died with the
208
+ # container, and the script's own "see logs/app.log" advice pointed at a path
209
+ # that never existed on the host.
210
+ ONENOTE_EXPORT_LOG_DIR=/data/output/logs
211
+
212
+ echo "Container : $CONTAINER"
213
+ echo "Image : $IMAGE"
214
+ echo "Auth file : $AUTH_FILE_ABS"
215
+ echo "Output : $OUTPUT_DIR_ABS"
216
+ echo ""
217
+ # Echoed to match what is really executed below. This line used to print
218
+ # "microsoft-onenote-exporter $* --non-interactive", which was missing the
219
+ # subcommand that the invocation adds - so the most reassuring line in the script
220
+ # was describing a command that was never run.
221
+ echo "Running: microsoft-onenote-exporter $SUBCOMMAND $* --auth-file $CONTAINER_AUTH_FILE"
222
+ echo ""
223
+
224
+ # Chromium needs more than Docker's default 64 MB of shared memory or it crashes
225
+ # on memory-heavy pages, hence --shm-size. --init runs a tiny PID-1 reaper so
226
+ # Chromium's child processes are cleaned up instead of accumulating as zombies
227
+ # when the export ends.
228
+ #
229
+ # The container runs detached and the exit status is polled below, so the script
230
+ # can report the result and the image can be reused for another run without
231
+ # rebuilding it.
232
+ docker run --detach \
233
+ --name "$CONTAINER" \
234
+ --init \
235
+ --shm-size=1g \
236
+ -e ONENOTE_EXPORT_LOG_DIR="$ONENOTE_EXPORT_LOG_DIR" \
237
+ -v "${OUTPUT_DIR_ABS}:/data/output" \
238
+ ${AUTH_MOUNT[@]+"${AUTH_MOUNT[@]}"} \
239
+ "$IMAGE" \
240
+ "$SUBCOMMAND" "$@" --auth-file "$CONTAINER_AUTH_FILE" >/dev/null
241
+
242
+ # Wait for the export to finish.
243
+ #
244
+ # `docker wait` is used rather than a poll loop over `docker inspect`. A loop
245
+ # written the obvious way reads State.ExitCode first and stops as soon as it is
246
+ # non-empty - and that field is 0 *while the container is still running*, so the
247
+ # loop exited on its first iteration and reported success for an export that had
248
+ # not written a single file. `docker wait` blocks until the container actually
249
+ # stops and then returns its status, which is the one question being asked here.
250
+ #
251
+ # It has no timeout, so a separate watchdog decides when to stop waiting and say
252
+ # so. The cap is generous - a large notebook over a slow connection can
253
+ # legitimately take tens of minutes - but past it the script reports rather than
254
+ # appearing to still be working. The container is left running so it can be
255
+ # inspected with `docker logs`.
256
+ WAIT_LIMIT=$((60 * 60)) # one hour
257
+ ( sleep "$WAIT_LIMIT"
258
+ if docker inspect -f '{{.State.Running}}' "$CONTAINER" 2>/dev/null | grep -q true; then
259
+ echo "WARNING: the export is still running after $((WAIT_LIMIT / 60)) minutes." >&2
260
+ echo " It has been left running; watch it with:" >&2
261
+ echo " docker logs -f $CONTAINER" >&2
262
+ # 124 is the conventional timeout status, and is reported as a failure
263
+ # rather than a success so a pipeline cannot mistake it for a clean run.
264
+ docker stop -t 30 "$CONTAINER" >/dev/null 2>&1 || true
265
+ fi
266
+ ) &
267
+ WATCHDOG_PID=$!
268
+
269
+ # Wait for it, then propagate the status. The container runs the export as PID 1
270
+ # under the reaper, so this is the container's exit status - which is the export's,
271
+ # because the CLI exits with the code the export step reported.
272
+ EXIT_CODE="$(docker wait "$CONTAINER")"
273
+
274
+ kill "$WATCHDOG_PID" 2>/dev/null || true
275
+ wait "$WATCHDOG_PID" 2>/dev/null || true
276
+
277
+ echo ""
278
+ if [ "$EXIT_CODE" -eq 0 ]; then
279
+ echo "Exported files are in: $OUTPUT_DIR_ABS"
280
+ else
281
+ case "$EXIT_CODE" in
282
+ 1)
283
+ echo "WARNING: the export failed and produced nothing usable." >&2
284
+ ;;
285
+ 2)
286
+ echo "WARNING: the arguments were wrong - check --notebook or --notebook-link." >&2
287
+ ;;
288
+ 3)
289
+ echo "NOTE: the export finished but some pages, sections or groups are missing." >&2
290
+ echo " The notes that were written are complete and name any asset they" >&2
291
+ echo " could not download. Re-run to try again." >&2
292
+ ;;
293
+ *)
294
+ echo "WARNING: the container exited with status $EXIT_CODE." >&2
295
+ ;;
296
+ esac
297
+ echo "Anything already written to $OUTPUT_DIR_ABS has been kept." >&2
298
+ echo "See logs/app.log inside the output directory for the full run." >&2
299
+ fi
300
+
301
+ exit "$EXIT_CODE"