@norskvideo/ctl-dev-kit 0.2.26 → 0.2.28
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/conventions/bot-prs.yml +34 -11
- package/conventions/upgrade-latest.yml +86 -66
- package/package.json +1 -1
package/conventions/bot-prs.yml
CHANGED
|
@@ -38,8 +38,9 @@ on:
|
|
|
38
38
|
workflow_dispatch:
|
|
39
39
|
|
|
40
40
|
permissions:
|
|
41
|
-
contents:
|
|
42
|
-
|
|
41
|
+
contents: read
|
|
42
|
+
checks: read
|
|
43
|
+
pull-requests: read
|
|
43
44
|
|
|
44
45
|
concurrency:
|
|
45
46
|
# One at a time per repo, and never cancelled: a merge half-done by a run that
|
|
@@ -54,18 +55,36 @@ jobs:
|
|
|
54
55
|
- name: Merge green bot PRs, report red ones
|
|
55
56
|
id: triage
|
|
56
57
|
env:
|
|
57
|
-
|
|
58
|
+
# Reads go through the job's own token, which the permissions above
|
|
59
|
+
# scope to exactly what is read; CI_DISPATCH_TOKEN (a PAT) has no
|
|
60
|
+
# Checks access, and the check-runs call returned an error object.
|
|
61
|
+
# Merges and branch deletes go through the PAT, because a push made
|
|
62
|
+
# with GITHUB_TOKEN does not trigger main's own workflows.
|
|
63
|
+
READ_TOKEN: ${{ github.token }}
|
|
64
|
+
WRITE_TOKEN: ${{ secrets.CI_DISPATCH_TOKEN }}
|
|
58
65
|
REPO: ${{ github.repository }}
|
|
59
66
|
run: |
|
|
60
67
|
set -euo pipefail
|
|
61
68
|
api="https://api.github.com/repos/$REPO"
|
|
62
|
-
|
|
69
|
+
curl_as() { local tok="$1"; shift; curl -sS --retry 3 --retry-all-errors -H "Authorization: Bearer $tok" -H "Accept: application/vnd.github+json" "$@"; }
|
|
70
|
+
gh_api() { curl_as "$READ_TOKEN" "$@"; }
|
|
71
|
+
gh_write() { curl_as "$WRITE_TOKEN" "$@"; }
|
|
72
|
+
# An API error is a JSON object with a message; say what it was rather
|
|
73
|
+
# than letting jq fail later on a missing field.
|
|
74
|
+
expect() {
|
|
75
|
+
local what="$1" filter="$2" body="$3"
|
|
76
|
+
jq -e "$filter" >/dev/null 2>&1 <<<"$body" && return 0
|
|
77
|
+
echo "::error::$what: $(jq -r '.message // .' <<<"$body" 2>/dev/null || echo "$body")"
|
|
78
|
+
exit 1
|
|
79
|
+
}
|
|
63
80
|
|
|
64
81
|
# The bots own exactly these branches (see each workflow's PR step).
|
|
65
|
-
|
|
82
|
+
pulls="$(gh_api "$api/pulls?state=open&per_page=100")"
|
|
83
|
+
expect "listing open PRs" 'type == "array"' "$pulls"
|
|
84
|
+
prs="$(jq -c --arg repo "$REPO" '
|
|
66
85
|
[.[] | select(.head.repo.full_name == $repo)
|
|
67
86
|
| select(.head.ref | test("^(upgrade-latest/|sync-dev-kit/|chore/sync-ctl-packages$)"))
|
|
68
|
-
| {number, ref: .head.ref, sha: .head.sha, draft, url: .html_url}]')"
|
|
87
|
+
| {number, ref: .head.ref, sha: .head.sha, draft, url: .html_url}]' <<<"$pulls")"
|
|
69
88
|
|
|
70
89
|
red=()
|
|
71
90
|
merged=()
|
|
@@ -73,6 +92,7 @@ jobs:
|
|
|
73
92
|
[ -n "$pr" ] || continue
|
|
74
93
|
n="$(jq -r .number <<<"$pr")"; ref="$(jq -r .ref <<<"$pr")"; sha="$(jq -r .sha <<<"$pr")"
|
|
75
94
|
runs="$(gh_api "$api/commits/$sha/check-runs?per_page=100")"
|
|
95
|
+
expect "reading checks for #$n" '.check_runs | type == "array"' "$runs"
|
|
76
96
|
failed="$(jq -r '[.check_runs[] | select(.conclusion | IN("failure","cancelled","timed_out","action_required","startup_failure")) | .name] | unique | join(", ")' <<<"$runs")"
|
|
77
97
|
pending="$(jq -r '[.check_runs[] | select(.status != "completed")] | length' <<<"$runs")"
|
|
78
98
|
passed="$(jq -r '[.check_runs[] | select(.conclusion == "success")] | length' <<<"$runs")"
|
|
@@ -89,12 +109,12 @@ jobs:
|
|
|
89
109
|
if [ "$mergeable" != "true" ]; then red+=("#$n $ref: does not merge cleanly with main"); continue; fi
|
|
90
110
|
# sha pins the merge to what was checked: a bot force-pushing the
|
|
91
111
|
# branch meanwhile makes this refuse rather than land unchecked code.
|
|
92
|
-
res="$(
|
|
112
|
+
res="$(gh_write -X PUT "$api/pulls/$n/merge" -d "$(jq -nc --arg sha "$sha" '{merge_method:"squash", sha:$sha}')")"
|
|
93
113
|
if [ "$(jq -r '.merged // false' <<<"$res")" = "true" ]; then
|
|
94
114
|
merged+=("#$n")
|
|
95
115
|
echo "merged #$n ($ref)"
|
|
96
116
|
# The bots recreate their branch on the next bump.
|
|
97
|
-
|
|
117
|
+
gh_write -X DELETE "$api/git/refs/heads/$ref" -o /dev/null || true
|
|
98
118
|
else
|
|
99
119
|
red+=("#$n $ref: merge refused: $(jq -r '.message // "unknown"' <<<"$res")")
|
|
100
120
|
fi
|
|
@@ -112,10 +132,13 @@ jobs:
|
|
|
112
132
|
echo "detail=${detail%; }" >> "$GITHUB_OUTPUT"
|
|
113
133
|
|
|
114
134
|
- name: Report to the product dashboard
|
|
135
|
+
# Always: a triage that broke is exactly what nobody would otherwise see.
|
|
136
|
+
if: always()
|
|
115
137
|
env:
|
|
116
138
|
TOKEN: ${{ secrets.CI_DISPATCH_TOKEN }}
|
|
117
|
-
|
|
118
|
-
|
|
139
|
+
READ_TOKEN: ${{ github.token }}
|
|
140
|
+
STATUS: ${{ steps.triage.outputs.state || 'failure' }}
|
|
141
|
+
DETAIL: ${{ steps.triage.outputs.detail || 'the bot-PR triage itself failed; see the run' }}
|
|
119
142
|
SHA: ${{ github.sha }}
|
|
120
143
|
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
|
121
144
|
REPO: ${{ github.repository }}
|
|
@@ -123,7 +146,7 @@ jobs:
|
|
|
123
146
|
set -euo pipefail
|
|
124
147
|
# The dashboard key is the one upgrade-latest.yml reports under, read
|
|
125
148
|
# from it so this file can stay byte-identical across products.
|
|
126
|
-
product="$(curl -sS -H "Authorization: Bearer $
|
|
149
|
+
product="$(curl -sS -H "Authorization: Bearer $READ_TOKEN" -H "Accept: application/vnd.github.raw" \
|
|
127
150
|
"https://api.github.com/repos/$REPO/contents/.github/workflows/upgrade-latest.yml" \
|
|
128
151
|
| sed -nE 's/^[[:space:]]*product:[[:space:]]*([^[:space:]]+).*/\1/p' | head -1)"
|
|
129
152
|
if [ -z "$product" ] || [ "$product" = "__PRODUCT__" ]; then
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# Keep the product fresh against the Norsk "nightly" channel (RFC 0001 —
|
|
2
|
-
# product version freshness).
|
|
2
|
+
# product version freshness). The {engine, studio image} pair comes from the
|
|
3
|
+
# nightly entry of norsk-studio's channel manifest -- the pair studio TESTED
|
|
4
|
+
# together -- never from the two channels separately. Version discovery of the
|
|
5
|
+
# product's current pins anchors on manifest.seed.json
|
|
3
6
|
# (the machine-writable source of truth for the launched {media, studio} image
|
|
4
7
|
# pair — every product repo has one). Any npm pins on the same version string
|
|
5
8
|
# (media build libraries, the @norskvideo/norsk-studio* family in whichever
|
|
@@ -119,22 +122,41 @@ jobs:
|
|
|
119
122
|
# product, even when nothing changed — pure dashboard noise.
|
|
120
123
|
parts=()
|
|
121
124
|
|
|
125
|
+
# ONE TESTED PAIR, NOT TWO CHANNELS. The engine and the studio image are
|
|
126
|
+
# taken together from the nightly entry of norsk-studio's channel
|
|
127
|
+
# manifest: the pair studio's publish built and tested against each
|
|
128
|
+
# other (scripts/write-channel-manifest.sh in norsk-studio). Resolving
|
|
129
|
+
# them apart -- the engine off norsk-sdk's nightly dist-tag, the image
|
|
130
|
+
# off Docker Hub's nightly tag -- produced pairs nobody had tested
|
|
131
|
+
# whenever one channel moved without the other. On 2026-10-04 studio's
|
|
132
|
+
# nightly had been held by its test gate since 09-28 while the engine
|
|
133
|
+
# walked on to 10-02, a breaking SDK change landed in between, and
|
|
134
|
+
# commentary's audio mix produced nothing on the old-studio /
|
|
135
|
+
# new-engine pair it was pinned to (norsk task 112).
|
|
136
|
+
#
|
|
137
|
+
# Consequence, deliberately: when studio's nightly stalls, the engine
|
|
138
|
+
# stalls with it (or steps BACK to the pair), rather than running ahead
|
|
139
|
+
# untested. Unblocking studio's nightly is how products move forward.
|
|
140
|
+
#
|
|
141
|
+
# Everything is resolved and checked BEFORE anything is rewritten, so a
|
|
142
|
+
# refusal never leaves the engine moved and studio not.
|
|
143
|
+
manifest_url="https://s3.eu-west-1.amazonaws.com/norsk.video/channel-manifest.json"
|
|
144
|
+
pair="$(curl -fsSL --retry 3 --retry-all-errors --retry-delay 2 "$manifest_url" | jq -c '.channels.nightly // empty')"
|
|
145
|
+
[ -n "$pair" ] || { echo "::error::no nightly pair in $manifest_url"; exit 1; }
|
|
146
|
+
tag_re='^[0-9]+\.[0-9]+\.[0-9]+-[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9a-f]+$'
|
|
147
|
+
new_media="$(jq -r '(.norsk // "") | split(":") | .[1] // ""' <<<"$pair")"
|
|
148
|
+
new_studio_img="$(jq -r '(.studio // "") | split(":") | .[1] // ""' <<<"$pair")"
|
|
149
|
+
printf '%s' "$new_media" | grep -qE "$tag_re" || { echo "::error::the manifest's nightly norsk image is not a dated nightly tag: $pair"; exit 1; }
|
|
150
|
+
printf '%s' "$new_studio_img" | grep -qE "$tag_re" || { echo "::error::the manifest's nightly studio image is not a dated nightly tag: $pair"; exit 1; }
|
|
151
|
+
echo "nightly pair: norsk $new_media + studio $new_studio_img"
|
|
152
|
+
|
|
122
153
|
# MEDIA: the npm packages and the launched image are built from the SAME
|
|
123
|
-
# commit (their hash suffixes match), so
|
|
124
|
-
# whole family
|
|
125
|
-
#
|
|
126
|
-
#
|
|
127
|
-
# version-pin test) EXCEPT the lockfile, which bun install regenerates.
|
|
154
|
+
# commit (their hash suffixes match), so the pair's engine version drives
|
|
155
|
+
# the whole family: overrides, sub-package deps, the manifest image ref,
|
|
156
|
+
# the version-pin test -- every occurrence EXCEPT the lockfile, which bun
|
|
157
|
+
# install regenerates.
|
|
128
158
|
old_media="$(jq -r '.latest.media | split(":")[1]' manifest.seed.json)"
|
|
129
|
-
|
|
130
|
-
[ -n "$new_media" ] && [ "$new_media" != "null" ] || { echo "could not resolve norsk-sdk nightly"; exit 1; }
|
|
131
|
-
echo "media: $old_media -> $new_media"
|
|
132
|
-
if [ "$old_media" != "$new_media" ]; then
|
|
133
|
-
git grep -lF "$old_media" -- . ':!bun.lock' | xargs -r sed -i "s|$old_media|$new_media|g"
|
|
134
|
-
changed=1
|
|
135
|
-
parts+=("media \`$new_media\`")
|
|
136
|
-
fi
|
|
137
|
-
# The rewrite above is a LITERAL replace of the value found in
|
|
159
|
+
# The rewrite below is a LITERAL replace of the value found in
|
|
138
160
|
# manifest.seed.json, so it only ever touches pins that already equal the
|
|
139
161
|
# seed. A pin that has drifted off it -- a hand edit, a merge, a
|
|
140
162
|
# half-finished run -- is invisible to this bump and to every later one,
|
|
@@ -148,9 +170,10 @@ jobs:
|
|
|
148
170
|
# Not auto-rewritten: a pin held back on purpose is someone's decision,
|
|
149
171
|
# and silently moving it would be the same class of surprise in the
|
|
150
172
|
# other direction. Fail, name them, and let a human fix the pin or the
|
|
151
|
-
# intent.
|
|
173
|
+
# intent. (Checked against the SEED, the family's current value: after
|
|
174
|
+
# the rewrite every seed-equal pin reads the pair's engine.)
|
|
152
175
|
stranded="$(git grep -hoE '"@norskvideo/norsk-(api|sdk|auto-manager|manager-sdk)"[[:space:]]*:[[:space:]]*"[0-9]+\.[0-9]+\.[0-9]+-[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9a-f]+"' -- '*package.json' \
|
|
153
|
-
| sed -E 's/.*"([^"]+)"$/\1/' | sort -u | grep -vxF "$new_media" || true)"
|
|
176
|
+
| sed -E 's/.*"([^"]+)"$/\1/' | sort -u | grep -vxF "$old_media" | grep -vxF "$new_media" || true)"
|
|
154
177
|
if [ -n "$stranded" ]; then
|
|
155
178
|
echo "::error::npm norsk pins are stranded off the nightly channel: $(printf '%s' "$stranded" | tr '\n' ' ')"
|
|
156
179
|
echo "The media family (api, sdk, auto-manager, manager-sdk) and the launched image are built from the same"
|
|
@@ -159,76 +182,73 @@ jobs:
|
|
|
159
182
|
exit 1
|
|
160
183
|
fi
|
|
161
184
|
|
|
162
|
-
# STUDIO:
|
|
163
|
-
#
|
|
164
|
-
#
|
|
165
|
-
#
|
|
166
|
-
#
|
|
167
|
-
#
|
|
168
|
-
#
|
|
169
|
-
#
|
|
170
|
-
#
|
|
171
|
-
#
|
|
172
|
-
# (no dependence on which files hold each — some products have no
|
|
173
|
-
# version-pin test).
|
|
185
|
+
# STUDIO: the studio npm packages and the studio image are built from
|
|
186
|
+
# DIFFERENT commits -- their hash suffixes never match -- so the image
|
|
187
|
+
# comes from the pair and the build libraries from npm's nightly. The
|
|
188
|
+
# libraries must belong to the same pair: their norsk-sdk dependency has
|
|
189
|
+
# to name the pair's engine, and their version line the image's. A
|
|
190
|
+
# mismatch means the channels are inconsistent (studio published npm
|
|
191
|
+
# without updating the manifest, or the reverse); refuse rather than pin
|
|
192
|
+
# a combination nobody tested. (Studio's publish installs the SDK it
|
|
193
|
+
# builds against as a DEV dependency -- `npm install --save-dev
|
|
194
|
+
# @norskvideo/norsk-sdk@<channel>` -- so that is where the pairing lives.)
|
|
174
195
|
#
|
|
175
196
|
# The old npm pin is discovered from the repo's REAL studio deps: any
|
|
176
197
|
# exact nightly on a @norskvideo/norsk-studio* package in a tracked
|
|
177
|
-
# package.json (deps, devDeps, or overrides
|
|
178
|
-
# repo with none skips the lib
|
|
179
|
-
#
|
|
180
|
-
# pins disagreeing across files abort rather than half-rewrite.
|
|
198
|
+
# package.json (deps, devDeps, or overrides -- wherever it lives). A
|
|
199
|
+
# repo with none skips the lib entirely. Distinct pins disagreeing
|
|
200
|
+
# across files abort rather than half-rewrite.
|
|
181
201
|
old_studio_lib="$(git grep -hoE '"@norskvideo/norsk-studio[^"]*"[[:space:]]*:[[:space:]]*"[0-9]+\.[0-9]+\.[0-9]+-[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9a-f]+"' -- '*package.json' \
|
|
182
202
|
| sed -E 's/.*"([^"]+)"$/\1/' | sort -u || true)"
|
|
183
203
|
if [ "$(printf '%s' "$old_studio_lib" | grep -c .)" -gt 1 ]; then
|
|
184
204
|
echo "studio npm pins disagree across package.json files:"; echo "$old_studio_lib"; exit 1
|
|
185
205
|
fi
|
|
186
|
-
new_studio_lib="$(curl -fsSL --retry 3 --retry-all-errors --retry-delay 2 "$reg/@norskvideo%2Fnorsk-studio" | jq -r '."dist-tags".nightly')"
|
|
187
|
-
[ -n "$new_studio_lib" ] && [ "$new_studio_lib" != "null" ] || { echo "could not resolve norsk-studio npm nightly"; exit 1; }
|
|
188
|
-
studio_line="${new_studio_lib%%-*}" # e.g. 1.28.0 — keep the image on this line
|
|
189
206
|
old_studio_img="$(jq -r '.latest.studio | split(":")[1]' manifest.seed.json)"
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
echo "Either the npm and docker version lines have diverged, or nightly has moved to a"
|
|
212
|
-
echo "different line than the npm nightly ($new_studio_lib). Not guessing — fix the channel."
|
|
213
|
-
exit 1
|
|
214
|
-
}
|
|
215
|
-
echo "studio lib: ${old_studio_lib:-<no npm pins>} -> $new_studio_lib"
|
|
207
|
+
new_studio_lib=""
|
|
208
|
+
if [ -n "$old_studio_lib" ]; then
|
|
209
|
+
studio_doc="$(curl -fsSL --retry 3 --retry-all-errors --retry-delay 2 "$reg/@norskvideo%2Fnorsk-studio")"
|
|
210
|
+
new_studio_lib="$(jq -r '."dist-tags".nightly // empty' <<<"$studio_doc")"
|
|
211
|
+
[ -n "$new_studio_lib" ] || { echo "could not resolve norsk-studio npm nightly"; exit 1; }
|
|
212
|
+
lib_sdk="$(jq -r --arg v "$new_studio_lib" \
|
|
213
|
+
'.versions[$v] | ((.devDependencies // {}) + (.peerDependencies // {}) + (.dependencies // {}))["@norskvideo/norsk-sdk"] // empty' <<<"$studio_doc")"
|
|
214
|
+
lib_sdk="${lib_sdk#^}"; lib_sdk="${lib_sdk#~}"
|
|
215
|
+
if [ "$lib_sdk" != "$new_media" ]; then
|
|
216
|
+
echo "::error::norsk-studio npm nightly $new_studio_lib was built against norsk-sdk ${lib_sdk:-<none declared>}, not the tested pair's engine $new_media."
|
|
217
|
+
echo "The npm channel and the channel manifest disagree; not pinning a combination nobody tested."
|
|
218
|
+
exit 1
|
|
219
|
+
fi
|
|
220
|
+
if [ "${new_studio_lib%%-*}" != "${new_studio_img%%-*}" ]; then
|
|
221
|
+
echo "::error::norsk-studio npm nightly $new_studio_lib is on a different version line from the pair's image $new_studio_img."
|
|
222
|
+
exit 1
|
|
223
|
+
fi
|
|
224
|
+
fi
|
|
225
|
+
|
|
226
|
+
echo "media: $old_media -> $new_media"
|
|
227
|
+
echo "studio lib: ${old_studio_lib:-<no npm pins>} -> ${new_studio_lib:-<unchanged>}"
|
|
216
228
|
echo "studio image: $old_studio_img -> $new_studio_img"
|
|
229
|
+
if [ "$old_media" != "$new_media" ]; then
|
|
230
|
+
git grep -lF "$old_media" -- . ':!bun.lock' | xargs -r sed -i "s|$old_media|$new_media|g"
|
|
231
|
+
changed=1
|
|
232
|
+
parts+=("media \`$new_media\`")
|
|
233
|
+
fi
|
|
234
|
+
# Rewrite the image ref FIRST, then the bare library version -- so even
|
|
235
|
+
# when the two coincide, the image ref has already moved and the
|
|
236
|
+
# bare-version rewrite only touches the npm pins.
|
|
217
237
|
if [ "$old_studio_img" != "$new_studio_img" ]; then
|
|
218
238
|
git grep -lF "norskvideo/norsk-studio:$old_studio_img" -- . ':!bun.lock' \
|
|
219
239
|
| xargs -r sed -i "s|norskvideo/norsk-studio:$old_studio_img|norskvideo/norsk-studio:$new_studio_img|g"
|
|
220
240
|
changed=1
|
|
221
241
|
fi
|
|
222
|
-
if [ -n "$
|
|
242
|
+
if [ -n "$new_studio_lib" ] && [ "$old_studio_lib" != "$new_studio_lib" ]; then
|
|
223
243
|
git grep -lF "$old_studio_lib" -- . ':!bun.lock' | xargs -r sed -i "s|$old_studio_lib|$new_studio_lib|g"
|
|
224
244
|
changed=1
|
|
225
245
|
fi
|
|
226
|
-
# One studio entry, not two
|
|
246
|
+
# One studio entry, not two -- the launched image tag is the runtime-
|
|
227
247
|
# relevant one; only fall back to the lib tag if the image held but the
|
|
228
|
-
# library advanced
|
|
248
|
+
# library advanced.
|
|
229
249
|
if [ "$old_studio_img" != "$new_studio_img" ]; then
|
|
230
250
|
parts+=("studio \`$new_studio_img\`")
|
|
231
|
-
elif [ -n "$
|
|
251
|
+
elif [ -n "$new_studio_lib" ] && [ "$old_studio_lib" != "$new_studio_lib" ]; then
|
|
232
252
|
parts+=("studio-lib \`$new_studio_lib\`")
|
|
233
253
|
fi
|
|
234
254
|
|