@norskvideo/ctl-dev-kit 0.1.71 → 0.1.73

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.
@@ -78,16 +78,27 @@ jobs:
78
78
  bun run docs:check
79
79
  '
80
80
 
81
- # Does this repo's licence template still describe this repo? A template
82
- # naming the wrong product, or an imageRef the build never produces, mints
83
- # a licence that entitles nothing -- and the first sign of that is a
84
- # 60-second readiness timeout at launch, indistinguishable from a product
85
- # that failed to start. Cheap, no secrets, every PR.
81
+ # The two halves of one licence guarantee, run together because they only
82
+ # work as a pair: check-template asserts this repo RECORDS the entitlement
83
+ # it needs, and check-product-name asserts it ASKS for the one it records.
84
+ #
85
+ # Either alone leaves them free to drift. funke-pegasus recorded
86
+ # funke-pegasus and asked for "norsk-studio" -- Studio's baked default,
87
+ # used whenever NORSK_PRODUCT_NAME is unset -- for its entire life. The
88
+ # template check was green throughout, because the template was never the
89
+ # wrong half. What that cost: the handshake was refused, so every media
90
+ # node was refused, so no SRT listener bound, while the instance reported
91
+ # healthy and Studio served its API. It reads as a broken workflow, and it
92
+ # took most of a day to find.
93
+ #
94
+ # PRODUCT_NAME is read ONCE and handed to both: two readings could
95
+ # disagree, and a lint that passes while naming the wrong product is worse
96
+ # than no lint. Cheap, no secrets, every PR.
86
97
  #
87
98
  # The image name is CI's own rule: PUBLISH_IMAGE when set, else the build
88
99
  # script's IMAGE_TAG default, tag stripped -- a licence matches the repo
89
100
  # part alone, never the tag.
90
- - name: Check the licence template matches this repo
101
+ - name: Check this repo records, and asks for, its own licence
91
102
  run: |
92
103
  set -euo pipefail
93
104
  product="$(sed -nE 's/.*PRODUCT_NAME[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p' shared/src/version.ts | head -1)"
@@ -102,6 +113,13 @@ jobs:
102
113
  nix develop .#build --command bun \
103
114
  node_modules/@norskvideo/ctl-dev-kit/licence/check-template.ts \
104
115
  licenseTemplate.json "$product" "$image"
116
+ # The compose the product renders is where the declaration has to be;
117
+ # the check exits 1 on an unreadable file rather than passing quietly,
118
+ # so a repo that moves this module fails here instead of silently
119
+ # dropping the gate.
120
+ nix develop .#build --command bun \
121
+ node_modules/@norskvideo/ctl-dev-kit/licence/check-product-name.ts \
122
+ shared/src/product-template.ts "$product"
105
123
 
106
124
  # Report this pipeline's result to the aggregated product CI dashboard
107
125
  # (id3as/ci-workflows) instead of posting its own pony — the dashboard renders
@@ -114,8 +114,11 @@ jobs:
114
114
  - name: Open (or update) the pin-freshness PR
115
115
  if: steps.bump.outputs.changed == '1'
116
116
  env:
117
- GH_TOKEN: ${{ secrets.CI_DISPATCH_TOKEN }}
117
+ API_TOKEN: ${{ secrets.CI_DISPATCH_TOKEN }}
118
118
  GATE: ${{ steps.gate.outcome }}
119
+ # Through the env, never interpolated into the script below: see the
120
+ # heredoc comment for what happens when it is.
121
+ MOVED: ${{ steps.bump.outputs.moved }}
119
122
  run: |
120
123
  set -euo pipefail
121
124
  branch="chore/sync-ctl-packages"
@@ -130,18 +133,24 @@ jobs:
130
133
 
131
134
  if [ "$GATE" = "success" ]; then
132
135
  verdict="Gate green: check:drift, lint, typecheck and unit tests all pass on the new pins."
133
- draft=""
136
+ draft=false
134
137
  else
135
138
  verdict="**Gate red** — the new pins do not fit this product as-is. Adopting them is real engineering, not a merge."
136
- draft="--draft"
139
+ draft=true
137
140
  fi
138
141
 
142
+ # $MOVED, not the expression that produced it. This heredoc is unquoted
143
+ # so $verdict expands, which means its literal text is scanned for
144
+ # command substitution too -- and the moved-pin list names every package
145
+ # in backticks. Pasted in directly, each name RAN (exit 127, no PR,
146
+ # every morning since this workflow shipped). Bash does not rescan an
147
+ # expansion's result, so through the env the same backticks are inert.
139
148
  body="$(cat <<EOF
140
149
  Automated by \`sync-ctl-packages\`.
141
150
 
142
151
  ## Pins moved
143
152
 
144
- ${{ steps.bump.outputs.moved }}
153
+ $MOVED
145
154
 
146
155
  ## What the gate proved
147
156
 
@@ -153,10 +162,24 @@ jobs:
153
162
  EOF
154
163
  )"
155
164
 
156
- if [ -z "$(gh pr list --head "$branch" --state open --json number --jq '.[].number')" ]; then
157
- gh pr create $draft --head "$branch" --base main \
158
- --title "chore: sync the ctl-* pins to their newest published versions" \
159
- --body "$body"
165
+ # gh is not on the self-hosted runner PATH -- it lives only inside
166
+ # `nix develop`, and this step runs outside it -- so create/update the
167
+ # PR straight against the REST API with the same PAT that pushed the
168
+ # branch. This is the identical fix sync-dev-kit already carries and
169
+ # documents; it was applied to one sibling convention and not its twin,
170
+ # so this step failed with exit 127 in every product repo. curl and jq
171
+ # ARE on the host PATH (the pin-resolve step above uses them).
172
+ title="chore: sync the ctl-* pins to their newest published versions"
173
+ repo="${{ github.repository }}"; owner="${repo%%/*}"
174
+ api="https://api.github.com/repos/$repo/pulls"
175
+ curl_api=(--fail-with-body -sS -H "Authorization: Bearer $API_TOKEN" -H "Accept: application/vnd.github+json")
176
+ existing="$(curl "${curl_api[@]}" "$api?head=$owner:$branch&state=open" | jq -r '.[0].number // empty')"
177
+ if [ -n "$existing" ]; then
178
+ # draft is not settable by PATCH; the standing PR keeps whichever
179
+ # state it opened in and the refreshed body carries the verdict.
180
+ curl "${curl_api[@]}" -X PATCH "$api/$existing" \
181
+ --data "$(jq -nc --arg t "$title" --arg b "$body" '{title:$t, body:$b}')" >/dev/null
160
182
  else
161
- gh pr edit "$branch" --body "$body"
183
+ curl "${curl_api[@]}" -X POST "$api" \
184
+ --data "$(jq -nc --arg t "$title" --arg b "$body" --arg h "$branch" --argjson d "$draft" '{title:$t, body:$b, head:$h, base:"main", draft:$d}')" >/dev/null
162
185
  fi
@@ -0,0 +1,27 @@
1
+ // Convention workflows carry real shell scripts inside YAML, and the two live in
2
+ // different worlds: the runner's bare PATH outside `nix develop`, a single-quoted
3
+ // word inside `bash -c '...'`. The guards over those hazards all need the same
4
+ // question answered first — is this line inside a quoted block or not — so it is
5
+ // asked in one place.
6
+
7
+ /** 1-indexed line numbers inside a multi-line `bash -c '` block, which ends at a
8
+ * line that is just `'`. A one-liner closing its own quote (`bash -c 'a; b'`)
9
+ * opens no block — it is already balanced. */
10
+ export function bashBlockLines(yaml: string): Set<number> {
11
+ const OPEN = "bash -c '";
12
+ const inside = new Set<number>();
13
+ let open = false;
14
+ yaml.split("\n").forEach((text, i) => {
15
+ if (!open) {
16
+ const at = text.indexOf(OPEN);
17
+ if (at >= 0 && !text.slice(at + OPEN.length).includes("'")) open = true;
18
+ return;
19
+ }
20
+ if (text.trim() === "'") {
21
+ open = false;
22
+ return;
23
+ }
24
+ inside.add(i + 1);
25
+ });
26
+ return inside;
27
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@norskvideo/ctl-dev-kit",
3
- "version": "0.1.71",
3
+ "version": "0.1.73",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./create-product": "./create-product/create-product.ts",