@minnovate/shadow-publisher 0.15.2 → 0.16.0

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "git-publisher",
3
3
  "displayName": "Git Publisher",
4
- "version": "0.15.2",
4
+ "version": "0.16.0",
5
5
  "description": "Publish an app folder's Git history to the serverless Git publishing backend and keep it updated (\"publish my app\", \"update my app\", \"fetch the engineered version\").",
6
6
  "author": { "name": "git-publisher project" },
7
7
  "keywords": ["git", "publish", "deploy", "serverless"]
@@ -206,12 +206,27 @@ following, for when the person asks not to wait.
206
206
  publish from a complete copy: `publisher checkout <new folder> --repo <id>`.
207
207
  - `LIMIT_EXCEEDED`, `PACK_INVALID`: quote the message; suggest removing very large files from the
208
208
  folder if the message mentions size.
209
+ - `REPOSITORY_MOVED`: see "When the app moved" below.
209
210
  - `"unknown"` / exit code 3: the publication was sent but its outcome is not known yet. Run
210
211
  `publisher status . --json`; it waits for and resolves interrupted publications without creating
211
212
  duplicates. Repeat once if it still says `finalizing`.
212
213
  - Exit code 4: run `publisher doctor --json` and follow `next.action` (normally `reauthenticate`:
213
214
  run `next.command`). Never ask the user for a token.
214
215
 
216
+ ### When the app moved
217
+
218
+ `REPOSITORY_MOVED` means the app moved to another organisation (the person's team adopted it).
219
+ This folder can no longer publish; nothing was lost, and it can still be read. It comes as a
220
+ `"rejected"` publication, or as `error.code` when the publish was refused before it started; both
221
+ carry a `hint` and `details.movedTo` (in `publication.rejection.details` or `error.details`).
222
+ Pass the `hint` on, then:
223
+
224
+ 1. Tell the person to sign in again and choose the organisation the app moved to:
225
+ `publisher login`. Never ask them for a token.
226
+ 2. Find the app there: `publisher checkout <new folder> --repo <details.movedTo> --json` when
227
+ `details.movedTo` is set; otherwise `publisher list --json` and pick the app by its name.
228
+ 3. Make the change again in the new folder (copy over what this folder had that was not published),
229
+ and publish from there. Do not try to publish this folder again, and never use `--force`.
215
230
 
216
231
  ### When the checks find something
217
232
 
@@ -20,6 +20,16 @@ publish it follows the app through received → checked → going live → live
20
20
  result. `merged` says whether anything
21
21
  published elsewhere was brought in (`"fast-forward"` or `"merge"`); mention it when it is not null.
22
22
 
23
+ ## When the app moved
24
+
25
+ A sync that ends with `REPOSITORY_MOVED` — `publication.rejection.code`, or `error.code` when it
26
+ was refused before anything was sent — means the app moved to another organisation (the person's
27
+ team adopted it). Pass on its `hint`, and do what the `publish-app` skill's "When the app moved"
28
+ says: sign in again with `publisher login` and choose that organisation, then
29
+ `publisher checkout <new folder> --repo <details.movedTo>` (or find the app with
30
+ `publisher list --json` when `details.movedTo` is `null`), and publish from the new folder. Do not
31
+ sync this folder again.
32
+
23
33
  ## When it waits for an answer
24
34
 
25
35
  Sometimes the app's preparation asks one business question before it can go on (for example
@@ -39,7 +49,8 @@ publisher answer <question id>=<value> --json
39
49
  ```
40
50
 
41
51
  A value that is not an option is refused before anything is sent; `error.details.questions` lists
42
- the choices. Exit 4 means this sign-in may not answer (an owner or admin can); exit 3 means ask
52
+ the choices. Exit 4 means this sign-in may not answer (an owner or admin can, or the app's creator for the
53
+ first preparation after an adoption); exit 3 means ask
43
54
  again. Then `publisher follow . --publication <id> --until-change --timeout 100 --json` to see it go live, telling them each step as `publish-app` step 4 says.
44
55
 
45
56
  ## When it did not go live because of a fault on our side
@@ -116,6 +116,13 @@ var ERROR_CODES = [
116
116
  "REPOSITORY_NOT_FOUND",
117
117
  "REPOSITORY_CONFLICT",
118
118
  "REPOSITORY_ARCHIVED",
119
+ // The repository moved to another organisation (an adoption): it refuses every write and still
120
+ // admits reads. `details.movedTo` names the new repository only to a caller with a grant on it,
121
+ // and is null for everybody else. A client that does not know the code reads it as its 409.
122
+ "REPOSITORY_MOVED",
123
+ // A copy for an adoption names the organisation it moves the application from, and the source
124
+ // repository is in another one. Its own code: the portal tells it from a moved repository.
125
+ "REPOSITORY_NOT_IN_ORGANISATION",
119
126
  "ACCESS_CONFLICT",
120
127
  // publications (begin / finalize / status)
121
128
  "PUBLICATION_NOT_FOUND",
@@ -165,6 +172,8 @@ var HTTP_STATUS_FOR_CODE = {
165
172
  REPOSITORY_NOT_FOUND: 404,
166
173
  REPOSITORY_CONFLICT: 409,
167
174
  REPOSITORY_ARCHIVED: 409,
175
+ REPOSITORY_MOVED: 409,
176
+ REPOSITORY_NOT_IN_ORGANISATION: 409,
168
177
  ACCESS_CONFLICT: 412,
169
178
  PUBLICATION_NOT_FOUND: 404,
170
179
  NOTHING_CHANGED: 409,
@@ -2791,7 +2800,14 @@ var IndexRecord = object({
2791
2800
  * stays exactly where it was, which is also the only thing the bucket policy permits
2792
2801
  * (`DenyDeleteOfImmutableKeys`). Records written before this field existed read back as null.
2793
2802
  */
2794
- archived: _default(nullable(IsoDate), null)
2803
+ archived: _default(nullable(IsoDate), null),
2804
+ /**
2805
+ * The repository this one was copied into when its application moved to another organisation (an
2806
+ * adoption, ADR-0034), or null. Set once, by the same compare-and-swap as `archived`, after the copy
2807
+ * holds every object. A moved repository refuses every publication and compaction with
2808
+ * `REPOSITORY_MOVED`, and still admits every read. Records written before this field read as null.
2809
+ */
2810
+ movedTo: _default(nullable(RepoId), null)
2795
2811
  });
2796
2812
  var OwnerMarker = object({
2797
2813
  schemaVersion: SchemaVersion,
@@ -2823,7 +2839,13 @@ var MetaRecord = object({
2823
2839
  * (`packages/worker/src/publish.ts`), because a consumer that guessed would create an
2824
2840
  * application in an organisation nobody chose. Every repository created since carries one.
2825
2841
  */
2826
- org: _default(nullable(OrgId), null)
2842
+ org: _default(nullable(OrgId), null),
2843
+ /**
2844
+ * The repository this one is a copy of (ADR-0034), or null for a repository that was created
2845
+ * empty. Create-only with the rest of the meta, so a replayed copy can prove it copies the same
2846
+ * repository, and a creation can never reuse the id of a copy.
2847
+ */
2848
+ movedFrom: _default(nullable(RepoId), null)
2827
2849
  });
2828
2850
  var GrantPattern = string2().check(
2829
2851
  _minLength(1),
@@ -2995,6 +3017,12 @@ var CreateRepositoryRequest = object({
2995
3017
  });
2996
3018
  var RepositoryIdRequest = object({ creationId: CreationId });
2997
3019
  var RepositoryIdResponse = object({ repoId: RepoId });
3020
+ var CopyRepositoryRequest = object({
3021
+ creationId: CreationId,
3022
+ fromOrganisation: OrgId,
3023
+ grants: _default(array(Grant).check(_maxLength(200)), [])
3024
+ });
3025
+ var CopyRepositoryResponse = object({ repoId: RepoId, movedFrom: RepoId });
2998
3026
  var RepositoryInfo = object({
2999
3027
  repoId: RepoId,
3000
3028
  name: string2(),
@@ -3005,6 +3033,11 @@ var RepositoryInfo = object({
3005
3033
  createdAt: IsoDate,
3006
3034
  /** null unless the repository is archived; an admin must be able to SEE one to bring it back */
3007
3035
  archived: _default(nullable(IsoDate), null),
3036
+ /**
3037
+ * The repository this one moved to (ADR-0034), and only for a caller with a grant on THAT one:
3038
+ * anybody else reads null. Optional so an older server reads as null.
3039
+ */
3040
+ movedTo: _default(nullable(RepoId), null),
3008
3041
  size: object({ baseBytes: int(), incrementalBytes: int(), packCount: int() }),
3009
3042
  lastTransaction: nullable(object({ id: string2(), kind: string2(), generation: Generation, committedAt: IsoDate })),
3010
3043
  /** whether a compaction has produced a base bundle yet; before one, a fetch installs the packs */
@@ -5152,7 +5185,7 @@ var PortalSession = class {
5152
5185
  continue;
5153
5186
  }
5154
5187
  }
5155
- return answer2;
5188
+ return { ...answer2, merged: answer2.status === 403 ? mergedSay(answer2.body) : null };
5156
5189
  }
5157
5190
  }
5158
5191
  };
@@ -5177,6 +5210,15 @@ async function askPortal(fetchImpl, url2, token, body) {
5177
5210
  function isRecord(value) {
5178
5211
  return typeof value === "object" && value !== null && !Array.isArray(value);
5179
5212
  }
5213
+ function mergedSay(body) {
5214
+ const error2 = isRecord(body) ? body["error"] : void 0;
5215
+ if (!isRecord(error2) || error2["code"] !== "organisation-merged") {
5216
+ return null;
5217
+ }
5218
+ const details = error2["details"];
5219
+ const say = isRecord(details) ? details["say"] : void 0;
5220
+ return typeof say === "string" && say !== "" ? say : null;
5221
+ }
5180
5222
 
5181
5223
  // ../client/src/portal-refusal.ts
5182
5224
  function portalRefusalCode(body) {
@@ -5216,7 +5258,7 @@ async function createApplication(input, opts) {
5216
5258
  throw new PublisherError("UNAUTHORIZED", 'the portal did not accept this credential; run "publisher login" again');
5217
5259
  }
5218
5260
  if (answer2.status === 403) {
5219
- throw new PublisherError("FORBIDDEN", `the portal refused to make an application here (${code || "forbidden"})`);
5261
+ throw new PublisherError("FORBIDDEN", answer2.merged ?? `the portal refused to make an application here (${code || "forbidden"})`);
5220
5262
  }
5221
5263
  if (answer2.status === 409 && REFUSALS[code] !== void 0) {
5222
5264
  throw new PublisherError(REFUSALS[code], `the portal refused the repository (${code})`, { details: { portal: code } });
@@ -5449,7 +5491,7 @@ async function followRun(input, opts) {
5449
5491
  return { kind: "unavailable", reason: "no-route" };
5450
5492
  }
5451
5493
  if (answer2.status === 403) {
5452
- return { kind: "forbidden" };
5494
+ return { kind: "forbidden", say: answer2.merged };
5453
5495
  }
5454
5496
  if (answer2.status === 200) {
5455
5497
  const run2 = soleRun(answer2.body);
@@ -5559,7 +5601,7 @@ async function call(input, opts, body) {
5559
5601
  return { kind: "unauthorized" };
5560
5602
  }
5561
5603
  if (answer2.status === 403) {
5562
- return { kind: "forbidden" };
5604
+ return { kind: "forbidden", say: answer2.merged };
5563
5605
  }
5564
5606
  if (answer2.status === 409) {
5565
5607
  return { kind: "moved-on" };
@@ -5664,7 +5706,7 @@ async function runCreatorTool(input, opts) {
5664
5706
  return { kind: "unauthorized" };
5665
5707
  }
5666
5708
  if (answer2.status === 403) {
5667
- return { kind: "forbidden" };
5709
+ return { kind: "forbidden", say: answer2.merged };
5668
5710
  }
5669
5711
  if (answer2.status === 404) {
5670
5712
  return { kind: "unavailable", detail: "the portal does not serve this tool" };
@@ -7194,7 +7236,7 @@ function progressHint(r, portal, answerable = false) {
7194
7236
  case "unauthorized":
7195
7237
  return inPortal(portal, "Published, but the portal did not accept this sign-in (run publisher login).", "Follow it");
7196
7238
  case "forbidden":
7197
- return inPortal(portal, "Published, but the portal would not show this sign-in the run.", "Follow it");
7239
+ return inPortal(portal, r.say === null ? "Published, but the portal would not show this sign-in the run." : `Published. ${r.say}`, "Follow it");
7198
7240
  case "malformed":
7199
7241
  return inPortal(portal, `Published, but the portal's answer about the run could not be read (${r.detail}).`, "Follow it");
7200
7242
  }
@@ -7743,6 +7785,23 @@ async function linkFolder(inv, p, endpoint, dir, attach) {
7743
7785
  };
7744
7786
  }
7745
7787
 
7788
+ // src/moved.ts
7789
+ var Moved = class {
7790
+ /**
7791
+ * The sentence for a refusal or a rejection with `code`, or null when it is not a move. `details`
7792
+ * names the new repository only when the service showed it to this sign-in, and then the sentence
7793
+ * gives the command that brings it into a new folder.
7794
+ */
7795
+ static hint(code, details) {
7796
+ if (code !== "REPOSITORY_MOVED") {
7797
+ return null;
7798
+ }
7799
+ const movedTo = details?.["movedTo"];
7800
+ const next = typeof movedTo === "string" ? `then run publisher checkout <new folder> --repo ${movedTo}` : "then run publisher list to find it there";
7801
+ return `This app moved to another organisation. Sign in again with publisher login, choose that organisation, ${next}.`;
7802
+ }
7803
+ };
7804
+
7746
7805
  // src/commands/publication.ts
7747
7806
  var publish2 = {
7748
7807
  git: true,
@@ -7763,6 +7822,7 @@ var publish2 = {
7763
7822
  }
7764
7823
  };
7765
7824
  function published(inv, o) {
7825
+ const hint = o.rejection === null ? null : Moved.hint(o.rejection.code, o.rejection.details);
7766
7826
  inv.out({
7767
7827
  state: o.state,
7768
7828
  publicationId: o.publicationId,
@@ -7771,7 +7831,8 @@ function published(inv, o) {
7771
7831
  refs: o.refs,
7772
7832
  rejection: o.rejection,
7773
7833
  resumed: o.resumed,
7774
- ...o.next === null ? {} : { next: o.next }
7834
+ ...o.next === null ? {} : { next: o.next },
7835
+ ...hint === null ? {} : { hint }
7775
7836
  });
7776
7837
  return exitFor(o.state);
7777
7838
  }
@@ -7947,7 +8008,10 @@ function shown(portal, r) {
7947
8008
  case "unauthorized":
7948
8009
  throw new PublisherError("UNAUTHORIZED", "the portal did not accept this sign-in; run publisher login");
7949
8010
  case "forbidden":
7950
- throw new PublisherError("FORBIDDEN", "the portal will not let this sign-in answer for this organisation (an owner or admin can)");
8011
+ throw new PublisherError(
8012
+ "FORBIDDEN",
8013
+ r.say ?? "the portal will not let this sign-in answer for this organisation (an owner or admin can, or the app's creator for the first preparation after an adoption)"
8014
+ );
7951
8015
  case "unavailable":
7952
8016
  throw new PublisherError("UNAVAILABLE", `the portal could not be asked: ${r.detail}`, { retryable: true });
7953
8017
  case "malformed":
@@ -8030,7 +8094,7 @@ async function creatorTool(p, endpoint, tool, args, opts = {}) {
8030
8094
  case "unauthorized":
8031
8095
  throw new PublisherError("UNAUTHORIZED", "the portal did not accept this sign-in; run publisher login");
8032
8096
  case "forbidden":
8033
- throw new PublisherError("FORBIDDEN", "the portal will not let this sign-in do that in this organisation");
8097
+ throw new PublisherError("FORBIDDEN", result.say ?? "the portal will not let this sign-in do that in this organisation");
8034
8098
  case "unavailable":
8035
8099
  throw new PublisherError("UNAVAILABLE", `the portal could not be asked: ${result.detail}`, { retryable: true });
8036
8100
  case "malformed":
@@ -8205,7 +8269,7 @@ function outcome(r) {
8205
8269
  rejection: o.rejection,
8206
8270
  ...o.next === null ? {} : { next: o.next }
8207
8271
  },
8208
- hint: syncHint(r.state, r.reconciled.before.conflicts, r.marked)
8272
+ hint: (o?.rejection ? Moved.hint(o.rejection.code, o.rejection.details) : null) ?? syncHint(r.state, r.reconciled.before.conflicts, r.marked)
8209
8273
  };
8210
8274
  }
8211
8275
  function syncHint(state, conflicts, marked) {
@@ -8299,6 +8363,8 @@ var EXIT_CODE = {
8299
8363
  REPOSITORY_NOT_FOUND: 1,
8300
8364
  REPOSITORY_CONFLICT: 1,
8301
8365
  REPOSITORY_ARCHIVED: 1,
8366
+ REPOSITORY_MOVED: 1,
8367
+ REPOSITORY_NOT_IN_ORGANISATION: 1,
8302
8368
  ACCESS_CONFLICT: 1,
8303
8369
  PUBLICATION_NOT_FOUND: 1,
8304
8370
  NOTHING_CHANGED: 1,
@@ -8487,7 +8553,8 @@ Usage:
8487
8553
  publisher follow [dir] --publication <id> follow an accepted publication's run where a sync left it
8488
8554
  [--timeout <seconds>] [--until-change] --until-change (sync too): return at the next step, to tell it
8489
8555
  publisher question [dir] [--repo <id>] the question the app's preparation waits on, if any,
8490
- with each option's value (answering needs owner or admin)
8556
+ with each option's value (an owner or admin answers it, or
8557
+ the app's creator for the first one after an adoption)
8491
8558
  publisher answer <question>=<value>... [--repo <id>] answer it from this folder; a value that is not one of
8492
8559
  the question's options is refused before anything is sent
8493
8560
  publisher retry [dir] [--repo <id>] try the app's newest version again, when it did not go
@@ -8600,9 +8667,11 @@ main(process.argv.slice(2)).then(
8600
8667
  (e) => {
8601
8668
  const json = process.argv.includes("--json");
8602
8669
  if (e instanceof PublisherError) {
8603
- const payload = { error: { code: e.code, message: e.message, retryable: e.retryable, details: e.details ?? null } };
8670
+ const moved = Moved.hint(e.code, e.details);
8671
+ const payload = { error: { code: e.code, message: e.message, retryable: e.retryable, details: e.details ?? null }, ...moved === null ? {} : { hint: moved } };
8604
8672
  const hint = e.code === "NETWORK" ? `
8605
- if this folder's service moved, "publisher doctor" says where to` : "";
8673
+ if this folder's service moved, "publisher doctor" says where to` : moved === null ? "" : `
8674
+ ${moved}`;
8606
8675
  process.stdout.write((json ? JSON.stringify(payload) : `error [${e.code}]: ${e.message}${hint}`) + "\n");
8607
8676
  process.exit(exitCodeFor(e.code));
8608
8677
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minnovate/shadow-publisher",
3
- "version": "0.15.2",
3
+ "version": "0.16.0",
4
4
  "description": "publisher: publish an app's Git history to the serverless Git backend",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",