@norskvideo/ctl-dev-kit 0.2.60 → 0.2.62

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.
@@ -8,14 +8,20 @@
8
8
  #
9
9
  # It bumps the @norskvideo/ctl-dev-kit pin to the newest published version,
10
10
  # reinstalls, re-syncs this product's forced copies from the new canonical
11
- # (sync-drift), then runs the gate (check:drift + lint + typecheck + unit tests):
11
+ # (sync-drift), runs the gate (check:drift + lint + typecheck + unit tests), and
12
+ # ALWAYS opens (or updates) a PR on sync-dev-kit/<version> -- never a push to
13
+ # main. bot-prs merges it once checks, smoke, integration and the guides gate
14
+ # are green on its head. The gate's verdict heads the PR body: red means the new
15
+ # dev-kit genuinely does not fit this product (a convention that fails
16
+ # lint/typecheck/tests, or a copy sync-drift could not converge), and adopting
17
+ # it is real engineering on the branch, not a copy-paste.
12
18
  #
13
- # - gate green -> the bump plus any mechanical re-sync is correct; commit it all
14
- # straight to main, hands-free. This is the common case.
15
- # - gate red -> the new dev-kit genuinely does not fit this product: a
16
- # convention that fails lint/typecheck/tests, or a copy sync-drift could not
17
- # mechanically converge. Push a branch and open a PR for a human -- real
18
- # engineering to adopt the convention, not a copy-paste.
19
+ # Why a PR even when the gate is green: a new dev-kit changes the copied
20
+ # workflows themselves, and those are exactly what the gate does not run. On
21
+ # 2026-10-09 a newer copied build-image.yml, never run against oracle-reasoning's
22
+ # set-up, broke it on main after a green gate landed it (ci-workflows
23
+ # docs/dashboard/ci-fragility.md, proposal B). On the PR, build-image's build
24
+ # and guides run against the new copy before main has it.
19
25
  #
20
26
  # sync-drift re-derives each copy from the canonical (never a second opinion that
21
27
  # can disagree), preserving the per-repo bits the gate masks -- the product: keys,
@@ -185,100 +191,94 @@ jobs:
185
191
  bun run test:unit
186
192
  '
187
193
 
188
- - name: Land the freshness bump on main (gate green)
189
- if: steps.sync.outputs.changed == '1' && steps.gate.outcome == 'success'
194
+ # The bump's only way to main is this PR. bot-prs merges it once every tier
195
+ # that runs on a bot PR -- checks, smoke, integration and build-image's
196
+ # guides gate, wherever this repo has them -- is green on its head
197
+ # (ci-workflows docs/dashboard/ci-fragility.md, proposal B). Landing on a
198
+ # green gate kept lint, typecheck and unit failures off main, but the tiers
199
+ # that break on an upstream change ran only after landing, so a bad
200
+ # upstream release still turned main red in every repo that took it. The
201
+ # gate above still runs: its verdict heads the PR body, and a red one
202
+ # shows on the PR's own checks too. Never a draft: bot-prs reads red from
203
+ # the tiers, and a draft could never become ready again by PATCH.
204
+ - name: Open (or update) the bump PR
205
+ if: steps.sync.outputs.changed == '1'
190
206
  env:
191
207
  API_TOKEN: ${{ secrets.CI_DISPATCH_TOKEN }}
208
+ GATE: ${{ steps.gate.outcome }}
192
209
  run: |
193
210
  set -euo pipefail
194
- # This bump is about to land, which makes any sync-dev-kit/* PR an earlier
195
- # red run opened obsolete. Left open they piled up -- nine were closed
196
- # by hand on 2026-10-03 -- so close them once the push has landed.
197
- # Best-effort: a failure here must not fail the landing.
211
+ branch="sync-dev-kit/${{ steps.sync.outputs.new }}"
212
+ prefix="sync-dev-kit/"
213
+ title="Sync dev-kit ${{ steps.sync.outputs.old }} -> ${{ steps.sync.outputs.new }}"
214
+ repo="${{ github.repository }}"; owner="${repo%%/*}"
215
+ api="https://api.github.com/repos/$repo/pulls"
216
+ # An older PR of this bot's family is obsolete once this one is open:
217
+ # left open they piled up (nine were closed by hand on 2026-10-03).
218
+ # Best-effort: a failure here must not fail the bump.
198
219
  close_superseded() {
199
- api="https://api.github.com/repos/$GITHUB_REPOSITORY"
220
+ local keep="$1" n
200
221
  auth=(-H "Authorization: Bearer $API_TOKEN" -H "Accept: application/vnd.github+json")
201
- for n in $(curl -sS "${auth[@]}" "$api/pulls?state=open&per_page=100" \
202
- | jq -r --arg repo "$GITHUB_REPOSITORY" '.[] | select(.head.repo.full_name == $repo and (.head.ref | startswith("sync-dev-kit/"))) | .number'); do
203
- curl -sS -o /dev/null "${auth[@]}" -X POST "$api/issues/$n/comments" \
204
- -d '{"body":"Superseded: a later sync-dev-kit run landed its bump on main."}'
205
- curl -sS -o /dev/null "${auth[@]}" -X PATCH "$api/pulls/$n" -d '{"state":"closed"}'
222
+ for n in $(curl -sS "${auth[@]}" "$api?state=open&per_page=100" \
223
+ | jq -r --arg repo "$repo" --arg prefix "$prefix" --argjson keep "$keep" \
224
+ '.[] | select(.head.repo.full_name == $repo and (.head.ref | startswith($prefix)) and .number != $keep) | .number'); do
225
+ curl -sS -o /dev/null "${auth[@]}" -X POST "https://api.github.com/repos/$repo/issues/$n/comments" \
226
+ -d "$(jq -nc --arg b "Superseded by #$keep, a later run of this bot." '{body:$b}')"
227
+ curl -sS -o /dev/null "${auth[@]}" -X PATCH "$api/$n" -d '{"state":"closed"}'
206
228
  echo "closed superseded PR #$n"
207
229
  done
208
230
  }
209
231
  git config user.name "github-actions[bot]"
210
232
  git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
233
+ git checkout -B "$branch"
211
234
  git add -A
212
235
  git commit -m "chore: sync dev-kit ${{ steps.sync.outputs.old }} -> ${{ steps.sync.outputs.new }}
213
236
 
214
237
  @norskvideo/ctl-dev-kit pin bumped to the newest published version and the
215
- forced copies re-synced from the new canonical (sync-drift); check:drift +
216
- lint + typecheck + unit tests green against the new bytes. Lockfile
238
+ forced copies re-synced from the new canonical (sync-drift). Lockfile
217
239
  regenerated."
218
- # This pushes with CI_DISPATCH_TOKEN (a PAT), so unlike a GITHUB_TOKEN
219
- # push it DOES retrigger downstream checks -- intended: the landed sync
220
- # should exercise the product's build workflows once (sync-dev-kit itself
221
- # is schedule/dispatch-only, so no loop). Rebase-and-retry so a commit
222
- # landing mid-run does not red the sync; a real conflict aborts.
223
- for attempt in 1 2 3 4 5; do
224
- if git push origin HEAD:main; then close_superseded || true; exit 0; fi
225
- echo "push rejected (attempt $attempt) - rebasing onto latest origin/main"
226
- # --unshallow, not a plain fetch: these runners keep their .git
227
- # between jobs, and a commit an earlier job fetched at depth 1 stays
228
- # recorded as parentless. A plain fetch does not re-fetch an object it
229
- # already holds, so main's tip can look like an unrelated root and the
230
- # rebase replays our base commit over it -- add/add conflicts on every
231
- # file it touches (playout + studio, 2026-10-03). The fallback covers
232
- # a repo that is already complete, where --unshallow is refused.
233
- git fetch --unshallow origin main 2>/dev/null || git fetch origin main
234
- git rebase origin/main || { git rebase --abort; echo "rebase hit a real conflict; aborting"; exit 1; }
235
- done
236
- echo "push still failing after 5 rebase attempts"; exit 1
237
-
238
- - name: Open a reconciliation PR (gate red)
239
- if: steps.sync.outputs.changed == '1' && steps.gate.outcome == 'failure'
240
- env:
241
- API_TOKEN: ${{ secrets.CI_DISPATCH_TOKEN }}
242
- run: |
243
- set -euo pipefail
244
- branch="sync-dev-kit/${{ steps.sync.outputs.new }}"
245
- git config user.name "github-actions[bot]"
246
- git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
247
- git checkout -b "$branch"
248
- git add -A
249
- git commit -m "chore: sync dev-kit ${{ steps.sync.outputs.old }} -> ${{ steps.sync.outputs.new }} (needs reconciliation)"
250
- git push -f origin "$branch"
251
- title="Sync dev-kit ${{ steps.sync.outputs.old }} -> ${{ steps.sync.outputs.new }}"
240
+ # One branch per bump, force-pushed from today's main -- but only when
241
+ # its tree changed. A red bump PR left standing is rebuilt by every run
242
+ # of this bot, and pushing the same tree again would re-run every slow
243
+ # tier on it for nothing.
244
+ if git fetch -q origin "$branch" 2>/dev/null && [ "$(git rev-parse 'HEAD^{tree}')" = "$(git rev-parse 'FETCH_HEAD^{tree}')" ]; then
245
+ echo "$branch already holds this bump; not re-pushing it"
246
+ else
247
+ git push -f origin "$branch"
248
+ fi
249
+ if [ "$GATE" = "success" ]; then
250
+ verdict="Gate green: check:drift, lint, typecheck and unit tests pass against the new dev-kit."
251
+ else
252
+ verdict="**Gate red**: the new dev-kit does not fit this product yet. Run the gate to see which part: \`bun install\`, then \`bun run check:drift\` (a copy sync-drift could not converge) and \`bun run lint && bun run typecheck && bun run test:unit\` (a convention that breaks real code). Fix it on this branch until green."
253
+ fi
252
254
  body="$(printf '%s\n' \
253
- "Automated dev-kit sync: the @norskvideo/ctl-dev-kit pin moved to the newest" \
254
- "published version and sync-drift re-synced the forced copies from the new" \
255
- "canonical -- but the gate is **red**, so this bump needs a human." \
256
- "" \
257
- "The mechanical re-sync is already on this branch. A red gate here means the" \
258
- "new dev-kit genuinely does not fit the product yet -- run the gate to see which:" \
255
+ "Automated dev-kit sync: the @norskvideo/ctl-dev-kit pin moved to the newest published version and sync-drift re-synced the forced copies from the new canonical." \
259
256
  "" \
260
- " bun install" \
261
- " bun run check:drift # a copy sync-drift could not mechanically converge" \
262
- " bun run lint && bun run typecheck && bun run test:unit # a convention that breaks real code" \
257
+ "$verdict" \
263
258
  "" \
264
- "Resolve whichever is failing, pushing to this branch until green -- then merge.")"
259
+ "bot-prs merges this once checks, smoke, integration and the guides gate" \
260
+ "(build-image) have all passed on its head, wherever this repo runs them." \
261
+ "Until then main does not move; a red tier keeps it open and reports it on" \
262
+ "the product dashboard.")"
265
263
  # gh is not on the self-hosted runner PATH (it lives only inside
266
264
  # `nix develop`, and this step runs outside it), so create/update the PR
267
265
  # straight against the REST API with the same PAT that pushed the branch.
268
- # Using the PAT (not GITHUB_TOKEN) lets the PR's own checks run so a human
269
- # sees the red/green. curl+jq are on the host PATH (the pin-resolve step
270
- # above already uses them).
271
- repo="${{ github.repository }}"; owner="${repo%%/*}"
272
- api="https://api.github.com/repos/$repo/pulls"
266
+ # The PAT (not GITHUB_TOKEN) is what lets the PR's own workflows run,
267
+ # which is the whole point: the bump is judged before it lands.
273
268
  curl_api=(--fail-with-body -sS -H "Authorization: Bearer $API_TOKEN" -H "Accept: application/vnd.github+json")
274
269
  existing="$(curl "${curl_api[@]}" "$api?head=$owner:$branch&state=open" | jq -r '.[0].number // empty')"
275
270
  if [ -n "$existing" ]; then
276
271
  curl "${curl_api[@]}" -X PATCH "$api/$existing" \
277
- --data "$(jq -nc --arg t "$title" --arg b "$body" '{title:$t, body:$b}')" >/dev/null
272
+ --data "$(jq -nc --arg t "$title" --arg b "$body" '{title:$t, body:$b}')" > /dev/null
273
+ number="$existing"
274
+ echo "updated PR #$number"
278
275
  else
279
- curl "${curl_api[@]}" -X POST "$api" \
280
- --data "$(jq -nc --arg t "$title" --arg b "$body" --arg h "$branch" '{title:$t, body:$b, head:$h, base:"main"}')" >/dev/null
276
+ number="$(curl "${curl_api[@]}" -X POST "$api" \
277
+ --data "$(jq -nc --arg t "$title" --arg b "$body" --arg h "$branch" \
278
+ '{title:$t, body:$b, head:$h, base:"main"}')" | jq -r .number)"
279
+ echo "opened PR #$number"
281
280
  fi
281
+ close_superseded "$number" || true
282
282
 
283
283
  - name: Report "already fresh"
284
284
  if: steps.sync.outputs.changed != '1'
@@ -15,11 +15,14 @@
15
15
  # template launches, which an sdk-app pins in its own source.
16
16
  #
17
17
  # Nightly cron + on demand. The bump runs the product's own gate (lint,
18
- # typecheck, unit tests) BEFORE it lands, exactly as upgrade-latest.yml and
19
- # sync-dev-kit do: green lands on main, red opens a reconciliation PR and main
20
- # stays green. The push is made with CI_DISPATCH_TOKEN (a PAT), so the push to
21
- # main fires the product's own workflows -- a GITHUB_TOKEN push would fire
22
- # nothing. No norsk-ctl pin to refresh: the sdk-app flake carries none.
18
+ # typecheck, unit tests), then ALWAYS opens (or updates) a PR on
19
+ # upgrade-latest/nightly, exactly as upgrade-latest.yml does, for bot-prs to
20
+ # merge once the PR's tiers are green; it never pushes to main (see that file
21
+ # for why). The branch is pushed with CI_DISPATCH_TOKEN (a PAT), so the PR's
22
+ # workflows fire -- a GITHUB_TOKEN push would fire nothing. It also moves
23
+ # flake.nix's ctlVersion, the norsk-ctl the product's CI tiers run, to the
24
+ # `latest` channel: an sdk-app's flake fetches no daemon, so it carries the
25
+ # version alone (ctl-pinned-in-repo.test.ts).
23
26
  #
24
27
  # Single-sourced in @norskvideo/ctl-dev-kit (conventions/upgrade-latest.sdk-app.yml)
25
28
  # and copied verbatim into each sdk-app product repo as
@@ -113,7 +116,19 @@ jobs:
113
116
  hub="https://hub.docker.com/v2/repositories/norskvideo/norsk/tags"
114
117
  tag_re='^[0-9]+\.[0-9]+\.[0-9]+-[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9a-f]+$'
115
118
  changed=0
116
- summary=""
119
+ parts=()
120
+
121
+ # NORSK-CTL: the version flake.nix's ctlVersion names is the daemon every
122
+ # CI tier runs (ctl-pinned-in-repo.test.ts) as well as the one the dev
123
+ # shell launches, so it moves here like the engine does and the bump
124
+ # PR's tiers are what test it (ci-workflows docs/dashboard/
125
+ # ci-fragility.md, proposal A). Resolved up front, with everything
126
+ # else, so a refusal rewrites nothing.
127
+ ctl_base="https://s3.eu-west-1.amazonaws.com/norsk.video/norsk-ctl"
128
+ old_ctl="$( { grep -oE 'ctlVersion = "[^"]+"' flake.nix 2>/dev/null || true; } | head -1 | sed -E 's/.*"(.*)"/\1/')"
129
+ [ -n "$old_ctl" ] || { echo "::error::flake.nix pins no ctlVersion. CI runs the norsk-ctl it names, so add ctlVersion = \"<version>\"; to flake.nix."; exit 1; }
130
+ new_ctl="$(curl -fsSL --retry 3 --retry-all-errors --retry-delay 2 "$ctl_base/latest")"
131
+ [ -n "$new_ctl" ] || { echo "::error::could not resolve norsk-ctl's latest channel"; exit 1; }
117
132
 
118
133
  # CURRENT: the product's exact norsk-sdk pin, wherever a tracked
119
134
  # package.json carries it (deps, devDeps or overrides). It is the one
@@ -157,7 +172,34 @@ jobs:
157
172
  if [ "$old" != "$new" ]; then
158
173
  git grep -lF "$old" -- . ':!bun.lock' | xargs -r sed -i "s|$old|$new|g"
159
174
  changed=1
160
- summary="media \`$new\`"
175
+ parts+=("media \`$new\`")
176
+ fi
177
+
178
+ # --- norsk-ctl pin (flake.nix) ------------------------------------
179
+ # ctlVersion, plus the per-platform hash of each platform the flake
180
+ # fetches the daemon for. A Studio product's flake fetches all four; an
181
+ # sdk-app's fetches none and carries the version alone. Mirrors
182
+ # packages/dev-kit/build/refresh-ctl-pin.sh in the ctl monorepo.
183
+ if [ "$old_ctl" != "$new_ctl" ]; then
184
+ declare -A ctlplat=([x86_64-linux]=linux-x64 [aarch64-linux]=linux-arm64 [aarch64-darwin]=darwin-arm64 [x86_64-darwin]=darwin-x64)
185
+ for sys in "${!ctlplat[@]}"; do
186
+ p="${ctlplat[$sys]}"
187
+ grep -qF "plat = \"$p\";" flake.nix || continue
188
+ hex="$(curl -fsSL --retry 3 --retry-all-errors --retry-delay 2 "$ctl_base/$new_ctl/norsk-ctl-$new_ctl-$p.sha256" | awk '{print $1}')"
189
+ [ -n "$hex" ] || { echo "no sha256 sidecar for $p"; exit 1; }
190
+ sri="$(nix hash convert --hash-algo sha256 --to sri "$hex")"
191
+ perl -0pi -e "s{(plat = \"$p\";\\s*hash = \")[^\"]*(\")}{\${1}$sri\${2}}" flake.nix
192
+ done
193
+ perl -0pi -e "s{(ctlVersion = \")[^\"]*(\")}{\${1}$new_ctl\${2}}" flake.nix
194
+ echo "ctl: $old_ctl -> $new_ctl"
195
+ changed=1
196
+ parts+=("norsk-ctl \`$new_ctl\`")
197
+ fi
198
+
199
+ summary=""
200
+ if [ ${#parts[@]} -gt 0 ]; then
201
+ summary="$(printf '%s, ' "${parts[@]}")"
202
+ summary="${summary%, }"
161
203
  fi
162
204
  {
163
205
  echo "changed=$changed"
@@ -192,111 +234,103 @@ jobs:
192
234
  bun run test:unit
193
235
  '
194
236
 
195
- - name: Land the bump on main (gate green)
196
- if: steps.bump.outputs.changed == '1' && steps.gate.outcome == 'success'
237
+ # The bump's only way to main is this PR. bot-prs merges it once every tier
238
+ # that runs on a bot PR -- checks, smoke, integration and build-image's
239
+ # guides gate, wherever this repo has them -- is green on its head
240
+ # (ci-workflows docs/dashboard/ci-fragility.md, proposal B). Landing on a
241
+ # green gate kept lint, typecheck and unit failures off main, but the tiers
242
+ # that break on an upstream change ran only after landing, so a bad
243
+ # upstream release still turned main red in every repo that took it. The
244
+ # gate above still runs: its verdict heads the PR body, and a red one
245
+ # shows on the PR's own checks too. Never a draft: bot-prs reads red from
246
+ # the tiers, and a draft could never become ready again by PATCH.
247
+ - name: Open (or update) the bump PR
248
+ if: steps.bump.outputs.changed == '1'
197
249
  env:
198
250
  API_TOKEN: ${{ secrets.CI_DISPATCH_TOKEN }}
199
- # Through env, never pasted into the script (see the PR step below).
251
+ GATE: ${{ steps.gate.outcome }}
252
+ # Through env, never pasted into the script: the summary wraps each
253
+ # version in backticks, and inside a double-quoted shell string those
254
+ # ran as commands ("1.3.1-...: command not found"), dropping every
255
+ # version from the commit message and PR body.
200
256
  SUMMARY: ${{ steps.bump.outputs.summary }}
201
257
  run: |
202
258
  set -euo pipefail
203
- # This bump is about to land, which makes any upgrade-latest/* PR an earlier
204
- # red run opened obsolete. Left open they piled up -- nine were closed
205
- # by hand on 2026-10-03 -- so close them once the push has landed.
206
- # Best-effort: a failure here must not fail the landing.
259
+ branch="upgrade-latest/nightly"
260
+ prefix="upgrade-latest/"
261
+ title="Bump the Norsk family to the SDK nightly"
262
+ repo="${{ github.repository }}"; owner="${repo%%/*}"
263
+ api="https://api.github.com/repos/$repo/pulls"
264
+ # An older PR of this bot's family is obsolete once this one is open:
265
+ # left open they piled up (nine were closed by hand on 2026-10-03).
266
+ # Best-effort: a failure here must not fail the bump.
207
267
  close_superseded() {
208
- api="https://api.github.com/repos/$GITHUB_REPOSITORY"
268
+ local keep="$1" n
209
269
  auth=(-H "Authorization: Bearer $API_TOKEN" -H "Accept: application/vnd.github+json")
210
- for n in $(curl -sS "${auth[@]}" "$api/pulls?state=open&per_page=100" \
211
- | jq -r --arg repo "$GITHUB_REPOSITORY" '.[] | select(.head.repo.full_name == $repo and (.head.ref | startswith("upgrade-latest/"))) | .number'); do
212
- curl -sS -o /dev/null "${auth[@]}" -X POST "$api/issues/$n/comments" \
213
- -d '{"body":"Superseded: a later upgrade-latest run landed its bump on main."}'
214
- curl -sS -o /dev/null "${auth[@]}" -X PATCH "$api/pulls/$n" -d '{"state":"closed"}'
270
+ for n in $(curl -sS "${auth[@]}" "$api?state=open&per_page=100" \
271
+ | jq -r --arg repo "$repo" --arg prefix "$prefix" --argjson keep "$keep" \
272
+ '.[] | select(.head.repo.full_name == $repo and (.head.ref | startswith($prefix)) and .number != $keep) | .number'); do
273
+ curl -sS -o /dev/null "${auth[@]}" -X POST "https://api.github.com/repos/$repo/issues/$n/comments" \
274
+ -d "$(jq -nc --arg b "Superseded by #$keep, a later run of this bot." '{body:$b}')"
275
+ curl -sS -o /dev/null "${auth[@]}" -X PATCH "$api/$n" -d '{"state":"closed"}'
215
276
  echo "closed superseded PR #$n"
216
277
  done
217
278
  }
218
279
  git config user.name "github-actions[bot]"
219
280
  git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
281
+ git checkout -B "$branch"
220
282
  git add -A
221
283
  git commit -m "chore: bump the Norsk family to the SDK nightly
222
284
 
223
285
  ${SUMMARY}
224
286
 
225
287
  The norsk-sdk / norsk-api pins and the engine image ref moved together
226
- via a global version-string replace; lockfile regenerated."
227
- # main can advance under a long bump (lockfile regen + registry
228
- # lookups run for minutes); a bare push then loses a non-fast-forward
229
- # race and reds the nightly. Rebase-and-retry so an unrelated commit
230
- # landing mid-run does not fail the bump; a genuine content conflict
231
- # still aborts loudly rather than force-pushing over other work.
232
- for attempt in 1 2 3 4 5; do
233
- if git push origin HEAD:main; then close_superseded || true; exit 0; fi
234
- echo "push rejected (attempt $attempt) - rebasing onto latest origin/main"
235
- # --unshallow, not a plain fetch: these runners keep their .git
236
- # between jobs, and a commit an earlier job fetched at depth 1 stays
237
- # recorded as parentless. A plain fetch does not re-fetch an object it
238
- # already holds, so main's tip can look like an unrelated root and the
239
- # rebase replays our base commit over it -- add/add conflicts on every
240
- # file it touches (playout + studio, 2026-10-03). The fallback covers
241
- # a repo that is already complete, where --unshallow is refused.
242
- git fetch --unshallow origin main 2>/dev/null || git fetch origin main
243
- git rebase origin/main || { git rebase --abort; echo "rebase hit a real conflict; aborting"; exit 1; }
244
- done
245
- echo "push still failing after 5 rebase attempts"; exit 1
246
-
247
- - name: Open a reconciliation PR (gate red)
248
- if: steps.bump.outputs.changed == '1' && steps.gate.outcome == 'failure'
249
- env:
250
- API_TOKEN: ${{ secrets.CI_DISPATCH_TOKEN }}
251
- # Through env, never pasted into the script: the summary wraps each
252
- # version in backticks, and inside a double-quoted shell string those
253
- # ran as commands ("1.3.1-...: command not found"), dropping every
254
- # version from the commit message and PR body.
255
- SUMMARY: ${{ steps.bump.outputs.summary }}
256
- run: |
257
- set -euo pipefail
258
- branch="upgrade-latest/nightly"
259
- git config user.name "github-actions[bot]"
260
- git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
261
- git checkout -b "$branch"
262
- git add -A
263
- git commit -m "chore: bump the Norsk family to the SDK nightly (needs reconciliation)
264
-
265
- ${SUMMARY}"
266
- git push -f origin "$branch"
267
- title="Bump the Norsk family to the SDK nightly"
288
+ via a global version-string replace; lockfile regenerated. flake.nix
289
+ ctlVersion, the norsk-ctl CI runs, moved to the latest channel."
290
+ # One branch per bump, force-pushed from today's main -- but only when
291
+ # its tree changed. A red bump PR left standing is rebuilt by every run
292
+ # of this bot, and pushing the same tree again would re-run every slow
293
+ # tier on it for nothing.
294
+ if git fetch -q origin "$branch" 2>/dev/null && [ "$(git rev-parse 'HEAD^{tree}')" = "$(git rev-parse 'FETCH_HEAD^{tree}')" ]; then
295
+ echo "$branch already holds this bump; not re-pushing it"
296
+ else
297
+ git push -f origin "$branch"
298
+ fi
299
+ if [ "$GATE" = "success" ]; then
300
+ verdict="Gate green: lint, typecheck and unit tests pass on the bumped pins."
301
+ else
302
+ verdict="**Gate red**: lint, typecheck or unit tests fail on the bumped pins. That usually means the new SDK nightly is bad, or genuinely changed something the product depends on. Fix it on this branch, or close this PR to skip the bump -- the next nightly run reopens it against whatever the channel holds then."
303
+ fi
268
304
  body="$(printf '%s\n' \
269
- "Automated nightly pin bump: the Norsk nightly channel moved and the coupled" \
270
- "pins came with it -- but the product gate is **red**, so this bump needs a" \
271
- "human before it reaches main." \
305
+ "Automated nightly pin bump: the Norsk SDK nightly and norsk-ctl latest moved, and the pins came with them." \
272
306
  "" \
273
307
  " ${SUMMARY}" \
274
308
  "" \
275
- "A red gate here usually means the new Norsk nightly is bad, or genuinely" \
276
- "changed something the product depends on. Run it to see which:" \
277
- "" \
278
- " bun run lint && bun run typecheck && bun run test:unit" \
309
+ "$verdict" \
279
310
  "" \
280
- "Fix it on this branch and merge, or close this PR to skip the bump -- the" \
281
- "next nightly run reopens it against whatever the channel holds then.")"
311
+ "bot-prs merges this once checks, smoke, integration and the guides gate" \
312
+ "(build-image) have all passed on its head, wherever this repo runs them." \
313
+ "Until then main does not move; a red tier keeps it open and reports it on" \
314
+ "the product dashboard.")"
282
315
  # gh is not on the self-hosted runner PATH (it lives only inside
283
316
  # `nix develop`, and this step runs outside it), so create/update the PR
284
317
  # straight against the REST API with the same PAT that pushed the branch.
285
- # The PAT (not GITHUB_TOKEN) is what lets the PR's own checks run, which
286
- # is the whole point: the bump is judged before it lands, not after.
287
- repo="${{ github.repository }}"; owner="${repo%%/*}"
288
- api="https://api.github.com/repos/$repo/pulls"
318
+ # The PAT (not GITHUB_TOKEN) is what lets the PR's own workflows run,
319
+ # which is the whole point: the bump is judged before it lands.
289
320
  curl_api=(--fail-with-body -sS -H "Authorization: Bearer $API_TOKEN" -H "Accept: application/vnd.github+json")
290
321
  existing="$(curl "${curl_api[@]}" "$api?head=$owner:$branch&state=open" | jq -r '.[0].number // empty')"
291
322
  if [ -n "$existing" ]; then
292
323
  curl "${curl_api[@]}" -X PATCH "$api/$existing" \
293
324
  --data "$(jq -nc --arg t "$title" --arg b "$body" '{title:$t, body:$b}')" > /dev/null
294
- echo "updated PR #$existing"
325
+ number="$existing"
326
+ echo "updated PR #$number"
295
327
  else
296
- curl "${curl_api[@]}" -X POST "$api" \
328
+ number="$(curl "${curl_api[@]}" -X POST "$api" \
297
329
  --data "$(jq -nc --arg t "$title" --arg b "$body" --arg h "$branch" \
298
- '{title:$t, body:$b, head:$h, base:"main"}')" | jq -r '"opened PR #\(.number)"'
330
+ '{title:$t, body:$b, head:$h, base:"main"}')" | jq -r .number)"
331
+ echo "opened PR #$number"
299
332
  fi
333
+ close_superseded "$number" || true
300
334
 
301
335
  - name: Report "already fresh"
302
336
  if: steps.bump.outputs.changed != '1'