@valbuild/server 0.132.1 → 0.133.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.
@@ -4347,7 +4347,25 @@ class ValOps {
4347
4347
  };
4348
4348
  };
4349
4349
  const patchedJsonEntries = {};
4350
- const allResults = await Promise.all(Object.entries(patchesByModule).map(([path, patches]) => applySourceFilePatches(path, patches)));
4350
+ /*
4351
+ * Patching the FILE TEXT, for a store that has somewhere to put it.
4352
+ *
4353
+ * Where there is nowhere -- an http project with no repository -- the
4354
+ * patches are still all applied, because they are: `getSources(analysis)`
4355
+ * below applies them to Source, and that is what the commit records and
4356
+ * what every reader of this project sees. What is skipped is rendering
4357
+ * that data back out as `.val.ts` and `*.val.json`, which would be a
4358
+ * mirror with no repository to mirror into -- and which cannot even be
4359
+ * attempted, because producing it starts by READING the current text from
4360
+ * the repository that is not there.
4361
+ */
4362
+ const allResults = await Promise.all(Object.entries(patchesByModule).map(([path, patches]) => this.mirrorsSourceFiles ? applySourceFilePatches(path, patches) : Promise.resolve({
4363
+ path: path,
4364
+ appliedPatches: patches.map(p => p.patchId),
4365
+ result: null,
4366
+ extraFiles: {},
4367
+ jsonEntries: {}
4368
+ })));
4351
4369
  let hasErrors = false;
4352
4370
  const sourceFilePatchErrors = {};
4353
4371
  const appliedPatches = {};
@@ -4522,6 +4540,46 @@ class ValOps {
4522
4540
  });
4523
4541
  }
4524
4542
 
4543
+ /**
4544
+ * Why a publish cannot happen here, or `null` when one can.
4545
+ *
4546
+ * Named rather than thrown, and asked BEFORE the click: the Studio shows the
4547
+ * reason and disables the action, instead of letting someone write a commit
4548
+ * message and then meeting a failure from four layers down.
4549
+ *
4550
+ * `no-base` is the only code so far and means what it says: there is nowhere
4551
+ * for this publish's commit to be based. Note which way round that is --
4552
+ * a project whose content service is the store of record always HAS a base
4553
+ * (the service's own chain, which mints its own shas), so the refusal is not
4554
+ * about missing git. It is about a deployment that cannot do what its
4555
+ * project requires.
4556
+ */
4557
+ publishRefusal() {
4558
+ return null;
4559
+ }
4560
+
4561
+ /**
4562
+ * Whether a commit here produces `.val.ts` TEXT as well as data.
4563
+ *
4564
+ * True everywhere there is somewhere to put it: a working tree in `fs` mode,
4565
+ * a host holding its own source in memory mode, a git repository in `http`
4566
+ * mode. False for an `http` project whose content service is the store of
4567
+ * record and which has no repository attached -- see `git` on
4568
+ * {@link ValApiOptions}.
4569
+ *
4570
+ * WHAT IS NOT AFFECTED, and it is the part worth being sure of:
4571
+ * `moduleVersions` -- what each changed module IS after the commit, with its
4572
+ * schema -- comes from `getSources(analysis)`, which applies the ops to
4573
+ * Source in the stores. It does not go near the file text. So a commit with
4574
+ * no mirror still records everything history and a later `connect-github`
4575
+ * fold need; what it does not record is a rendering of that data as code.
4576
+ *
4577
+ * WHAT IS: the ops are no longer applied to the file text as well, so a
4578
+ * patch that would not fit the `.val.ts` is not reported here. That check
4579
+ * only ever existed for the text being produced, and there is none.
4580
+ */
4581
+ mirrorsSourceFiles = true;
4582
+
4525
4583
  /**
4526
4584
  * Take the `.val.ts` text a commit produced as the new committed source.
4527
4585
  *
@@ -4686,6 +4744,16 @@ function formatPatchSourceError(error) {
4686
4744
  return "Unknown patch source error: " + JSON.stringify(_exhaustiveCheck);
4687
4745
  }
4688
4746
  }
4747
+
4748
+ /**
4749
+ * Why a publish is refused, in a form a person can be shown.
4750
+ *
4751
+ * `code` is for the Studio to branch on and `message` is what it says. Both,
4752
+ * rather than a code and a lookup table on the client: the server knows what
4753
+ * is actually missing -- which branch, which commit -- and a client-side table
4754
+ * could only ever say the generic version.
4755
+ */
4756
+
4689
4757
  /**
4690
4758
  * The patches a json entry render should apply, out of the whole chain.
4691
4759
  *
@@ -7525,8 +7593,14 @@ const GetApplicablePatches = z.z.object({
7525
7593
  })),
7526
7594
  commits: z.z.array(z.z.object({
7527
7595
  commitSha: z.z.string(),
7528
- clientCommitSha: z.z.string(),
7529
- parentCommitSha: z.z.string(),
7596
+ /*
7597
+ * Nullable since the content service mints its own commit shas: a
7598
+ * root commit has no parent, and a publisher that did not say where
7599
+ * it was has no client sha. Parsed rather than trusted, so a service
7600
+ * that sends null gets null here instead of failing the whole poll.
7601
+ */
7602
+ clientCommitSha: z.z.string().nullable(),
7603
+ parentCommitSha: z.z.string().nullable(),
7530
7604
  commitMessage: z.z.string().nullable(),
7531
7605
  branch: z.z.string(),
7532
7606
  creator: z.z.string(),
@@ -7542,7 +7616,21 @@ const GetApplicablePatches = z.z.object({
7542
7616
  // `ValDeployment`: this is the only source for a deployment that Val
7543
7617
  // did not publish, and zod would otherwise strip it here.
7544
7618
  commitMessage: z.z.string().nullable().optional()
7545
- })).optional()
7619
+ })).optional(),
7620
+ /**
7621
+ * What the project EXPECTS of whoever publishes it.
7622
+ *
7623
+ * Optional because a content service that predates it sends nothing, and
7624
+ * absent means "not reported" -- never "managed". The difference decides
7625
+ * whether a publish is refused, so guessing either way would be worse than
7626
+ * not checking: guessing `managed` would let a deployment publish a
7627
+ * connected project it cannot mirror, and guessing `connected` would refuse
7628
+ * every publish against an older service.
7629
+ */
7630
+ project: z.z.object({
7631
+ sourceMode: z.z.union([z.z.literal("managed"), z.z.literal("connected")]),
7632
+ branch: z.z.string()
7633
+ }).optional()
7546
7634
  });
7547
7635
  const FilesResponse = z.z.object({
7548
7636
  files: z.z.array(z.z.union([z.z.object({
@@ -7615,8 +7703,10 @@ const CommitResponse = z.z.object({
7615
7703
  // commit changed nothing", which is indistinguishable from a real answer.
7616
7704
  const HistoricalCommitResponse = z.z.object({
7617
7705
  commitSha: z.z.string(),
7618
- parentCommitSha: z.z.string(),
7619
- clientCommitSha: z.z.string(),
7706
+ /** `null` for a root commit; see the note on the applicable-patches schema. */
7707
+ parentCommitSha: z.z.string().nullable(),
7708
+ /** `null` when the publisher did not say where it was. */
7709
+ clientCommitSha: z.z.string().nullable(),
7620
7710
  branch: z.z.string(),
7621
7711
  createdBranch: z.z.string().nullable(),
7622
7712
  creator: z.z.string().nullable(),
@@ -7634,8 +7724,8 @@ const CommitPatchesResponse = z.z.object({
7634
7724
  commitSha: z.z.string(),
7635
7725
  commit: z.z.object({
7636
7726
  commitSha: z.z.string(),
7637
- parentCommitSha: z.z.string(),
7638
- clientCommitSha: z.z.string(),
7727
+ parentCommitSha: z.z.string().nullable(),
7728
+ clientCommitSha: z.z.string().nullable(),
7639
7729
  branch: z.z.string(),
7640
7730
  createdBranch: z.z.string().nullable(),
7641
7731
  creator: z.z.string().nullable(),
@@ -7735,9 +7825,45 @@ class ValOpsHttp extends ValOps {
7735
7825
  patchesAreLocal = false;
7736
7826
  /** See {@link ValOps.requiresAuth}. */
7737
7827
  requiresAuth = true;
7738
- constructor(contentUrl, project, commitSha,
7739
- // TODO: CommitSha
7740
- branch,
7828
+ /**
7829
+ * A commit mirrors into `.val.ts` only when there is a repository.
7830
+ *
7831
+ * See {@link ValOps.mirrorsSourceFiles}. Set in the constructor rather than
7832
+ * as an initialiser because it depends on `git`, and a class field
7833
+ * initialiser runs before the constructor body has assigned it.
7834
+ */
7835
+ mirrorsSourceFiles;
7836
+ /**
7837
+ * What the content service last said this project expects of its publisher.
7838
+ *
7839
+ * `null` until something has asked it, which in practice is the first poll.
7840
+ * It is remembered rather than asked for on demand because the answer is
7841
+ * only wanted on the publish path, and that path already fetches the
7842
+ * patches it is publishing -- so a dedicated request would be a second round
7843
+ * trip for two fields that just arrived.
7844
+ *
7845
+ * It can be one poll out of date, and that is the right amount: the thing it
7846
+ * changes is whether this deployment can mirror commits into a repository,
7847
+ * which changes when a project CONNECTS one -- and a project that has just
7848
+ * connected is one whose builds are about to be replaced anyway.
7849
+ */
7850
+ projectExpectation = null;
7851
+ constructor(contentUrl, project,
7852
+ /**
7853
+ * The repository this project's commits are mirrored into, or `null`.
7854
+ *
7855
+ * `null` is a project whose content service is the store of record: it
7856
+ * mints its own commit shas, and it knows this project's branch from the
7857
+ * project itself. Every request below that would have carried a branch and
7858
+ * a commit omits them instead, and the service answers from the project's
7859
+ * own chain -- which is where those answers always came from.
7860
+ *
7861
+ * It is NOT a degraded mode. The one thing that genuinely needs a
7862
+ * repository is producing the `.val.ts` text a commit mirrors, and a
7863
+ * project with no repository has nothing to mirror into. See `git` on
7864
+ * {@link ValApiOptions}.
7865
+ */
7866
+ git,
7741
7867
  /**
7742
7868
  * An api key (how the app itself authenticates) or a personal access token
7743
7869
  * (how the CLI authenticates after `val login`). Same two shapes as
@@ -7747,14 +7873,49 @@ class ValOpsHttp extends ValOps {
7747
7873
  super(valModules, options);
7748
7874
  this.contentUrl = contentUrl;
7749
7875
  this.project = project;
7750
- this.commitSha = commitSha;
7751
- this.branch = branch;
7876
+ this.git = git;
7752
7877
  this.authHeaders = "pat" in auth ? {
7753
7878
  "x-val-pat": auth.pat
7754
7879
  } : {
7755
7880
  Authorization: `Bearer ${auth.apiKey}`
7756
7881
  };
7757
7882
  this.root = (options === null || options === void 0 ? void 0 : options.root) ?? "";
7883
+ this.mirrorsSourceFiles = git !== null;
7884
+ }
7885
+ /**
7886
+ * A deployment that cannot mirror a project which expects to be mirrored.
7887
+ *
7888
+ * This is the one shape of "no base" that exists, and it is not the one it
7889
+ * sounds like. A project with no repository is fine: the content service is
7890
+ * the store of record for it and mints its own commit shas, so there is
7891
+ * always somewhere for the commit to go. What is refused is the mismatch --
7892
+ * a project whose commits are mirrored into a repository, being published by
7893
+ * a build that was made before it had one and so has no commit to produce
7894
+ * that mirror against.
7895
+ *
7896
+ * It is a REAL state rather than a defensive check: it is exactly what a
7897
+ * deployment looks like between a project connecting a repository and its
7898
+ * next build going out. Left unchecked, such a publish writes the data and
7899
+ * silently fails to mirror it, and the repository quietly falls behind the
7900
+ * content nobody is told about.
7901
+ *
7902
+ * `null` when the service did not say (see `project` on the response
7903
+ * schema): an older content service is not evidence of anything, and
7904
+ * refusing every publish against one would be a worse failure than not
7905
+ * checking.
7906
+ */
7907
+ publishRefusal() {
7908
+ var _this$projectExpectat;
7909
+ if (this.git !== null) {
7910
+ return null;
7911
+ }
7912
+ if (((_this$projectExpectat = this.projectExpectation) === null || _this$projectExpectat === void 0 ? void 0 : _this$projectExpectat.sourceMode) !== "connected") {
7913
+ return null;
7914
+ }
7915
+ return {
7916
+ code: "no-base",
7917
+ message: `This project mirrors its content into a git repository (branch ` + `'${this.projectExpectation.branch}'), but this deployment was not ` + "built from one, so it does not know which commit to write that " + "mirror against. Publishing would save the content and silently " + "leave the repository behind. Deploy this project again from its " + "repository, and publishing will work from that build on."
7918
+ };
7758
7919
  }
7759
7920
  async onInit() {
7760
7921
  // TODO: unused for now. Implement or remove
@@ -7945,16 +8106,31 @@ class ValOpsHttp extends ValOps {
7945
8106
  * decided against.
7946
8107
  */
7947
8108
  headCommitSha: internal.newestCommitSha(allPatchData.commits) ?? undefined,
7948
- commitSha: this.commitSha
8109
+ /*
8110
+ * Spread: a project with no repository has no such commit, and saying
8111
+ * so by leaving the key out is different from sending an empty string
8112
+ * the Studio would try to show.
8113
+ */
8114
+ ...(this.git ? {
8115
+ commitSha: this.git.commit
8116
+ } : {})
7949
8117
  };
7950
8118
  }
7951
8119
  async getWebSocketNonce(profileId) {
7952
8120
  return fetch(`${this.contentUrl}/v1/${this.project}/websocket/nonces`, {
7953
8121
  method: "POST",
7954
8122
  body: JSON.stringify({
7955
- branch: this.branch,
7956
8123
  profileId,
7957
- commitSha: this.commitSha
8124
+ /*
8125
+ * Omitted when there is no repository, like every other request here.
8126
+ * The content service knows this project's branch -- it is a column on
8127
+ * the project -- and a nonce is scoped to the project and the person,
8128
+ * not to a position in a chain.
8129
+ */
8130
+ ...(this.git ? {
8131
+ branch: this.git.branch,
8132
+ commitSha: this.git.commit
8133
+ } : {})
7958
8134
  }),
7959
8135
  headers: {
7960
8136
  ...this.authHeaders,
@@ -8102,8 +8278,20 @@ class ValOpsHttp extends ValOps {
8102
8278
  }
8103
8279
  async fetchPatchesInternal(filters) {
8104
8280
  const params = [];
8105
- params.push(["branch", this.branch]);
8106
- params.push(["commit", this.commitSha]);
8281
+ /*
8282
+ * A position in the chain, WHEN THIS BUILD HAS ONE.
8283
+ *
8284
+ * `commit` tells the content service where this deployment sits, so it can
8285
+ * answer with the commits at or after that point. A build with no
8286
+ * repository has no such commit baked into it, and asking the service to
8287
+ * resolve the position itself is strictly better than inventing one: it
8288
+ * holds the chain, and for a project it is the only publisher of, the
8289
+ * position IS the head.
8290
+ */
8291
+ if (this.git) {
8292
+ params.push(["branch", this.git.branch]);
8293
+ params.push(["commit", this.git.commit]);
8294
+ }
8107
8295
  if (filters.patchIds) {
8108
8296
  for (const patchId of filters.patchIds) {
8109
8297
  params.push(["patch_id", patchId]);
@@ -8124,6 +8312,17 @@ class ValOpsHttp extends ValOps {
8124
8312
  if (parsed.success) {
8125
8313
  const errors = [];
8126
8314
  const data = parsed.data;
8315
+ /*
8316
+ * What the project expects of its publisher, remembered.
8317
+ *
8318
+ * Recorded here rather than returned because every caller of this
8319
+ * already has what it needs and only the publish path asks the
8320
+ * question -- and that path calls this first. See
8321
+ * {@link publishRefusal}.
8322
+ */
8323
+ if (data.project) {
8324
+ this.projectExpectation = data.project;
8325
+ }
8127
8326
  for (const patchesRes of data.patches) {
8128
8327
  var _patchesRes$applied;
8129
8328
  patches.push({
@@ -8344,7 +8543,7 @@ class ValOpsHttp extends ValOps {
8344
8543
  * `saveSourceFilePatch`): groups are per branch, so a request without one
8345
8544
  * is not merely under-specified, it is rejected.
8346
8545
  */
8347
- const params = new URLSearchParams([["branch", this.branch]]);
8546
+ const params = new URLSearchParams(this.git ? [["branch", this.git.branch]] : []);
8348
8547
  const res = await fetch(`${this.contentUrl}/v1/${this.project}/patch-groups?${params}`, {
8349
8548
  headers: this.authHeaders
8350
8549
  });
@@ -8496,8 +8695,10 @@ class ValOpsHttp extends ValOps {
8496
8695
  patchId,
8497
8696
  parentPatchId: parentRef.type === "patch" ? parentRef.patchId : null,
8498
8697
  baseSha,
8499
- commit: this.commitSha,
8500
- branch: this.branch,
8698
+ ...(this.git ? {
8699
+ commit: this.git.commit,
8700
+ branch: this.git.branch
8701
+ } : {}),
8501
8702
  coreVersion: core.Internal.VERSION.core,
8502
8703
  /*
8503
8704
  * Group membership in the SAME request as the patch.
@@ -8663,11 +8864,28 @@ class ValOpsHttp extends ValOps {
8663
8864
  });
8664
8865
  }
8665
8866
  async getSourceFile(path) {
8867
+ /*
8868
+ * There is no file to read without a repository to read it from.
8869
+ *
8870
+ * Reachable only through the CLI's debug snapshot now: the publish path
8871
+ * does not call this for such a project at all -- see
8872
+ * {@link ValOps.mirrorsSourceFiles} -- because there is nothing for it to
8873
+ * produce. It is an error rather than an empty string because an empty
8874
+ * `.val.ts` would be patched successfully and committed as a module that
8875
+ * had lost all its content.
8876
+ */
8877
+ if (!this.git) {
8878
+ return {
8879
+ error: {
8880
+ message: `Cannot read the source of ${path}: this project has no ` + "repository, so there is no `.val.ts` to read. Its content lives " + "in Val's content service, which is the store of record for it."
8881
+ }
8882
+ };
8883
+ }
8666
8884
  const filesRes = await this.getHttpFiles([{
8667
8885
  filePath: path,
8668
8886
  location: "repo",
8669
8887
  root: this.root,
8670
- commitSha: this.commitSha
8888
+ commitSha: this.git.commit
8671
8889
  }]);
8672
8890
  if (filesRes.error) {
8673
8891
  return filesRes;
@@ -8687,11 +8905,22 @@ class ValOpsHttp extends ValOps {
8687
8905
  async getBinaryFile(filePath) {
8688
8906
  // We could also just get this from public/ on the running server. Current approach feels more clean, but will be slower / puts more server load... We might want to change this
8689
8907
  const requestFiles = [];
8908
+ if (!this.git) {
8909
+ /*
8910
+ * A published binary lives in the repository, and there is none.
8911
+ *
8912
+ * `null` is this method's existing "not found", which is what a caller
8913
+ * already handles: the Studio falls back to the patch's own copy, which
8914
+ * is where a managed project's files stay. See the note on local files
8915
+ * in the content service's commit handler.
8916
+ */
8917
+ return null;
8918
+ }
8690
8919
  requestFiles.push({
8691
8920
  filePath: filePath,
8692
8921
  location: "repo",
8693
8922
  root: this.root,
8694
- commitSha: this.commitSha
8923
+ commitSha: this.git.commit
8695
8924
  });
8696
8925
  const filesRes = await this.getHttpFiles(requestFiles);
8697
8926
  if (filesRes.error) {
@@ -8870,7 +9099,6 @@ class ValOpsHttp extends ValOps {
8870
9099
  patchGroupId) {
8871
9100
  try {
8872
9101
  var _res$headers$get3;
8873
- const existingBranch = this.branch;
8874
9102
  const res = await fetch(`${this.contentUrl}/v1/${this.project}/commit`, {
8875
9103
  method: "POST",
8876
9104
  headers: {
@@ -8914,13 +9142,29 @@ class ValOpsHttp extends ValOps {
8914
9142
  * moved on.
8915
9143
  */
8916
9144
  modules: prepared.moduleVersions,
8917
- commit: this.commitSha,
9145
+ /*
9146
+ * WHERE THIS BUILD THOUGHT IT WAS -- and only for a project with a
9147
+ * repository, which is the only thing that still reads it.
9148
+ *
9149
+ * The content service mints its own commit shas, so this is no
9150
+ * longer the parent, and it never was the concurrency check: that is
9151
+ * `expectedHeadCommitSha` against `newestCommitSha(patches.commits)`,
9152
+ * which `/save` runs before it gets here. What is left is the git
9153
+ * fast-forward check, which needs to know the commit this build read
9154
+ * the branch at to tell whether the branch has moved under it.
9155
+ *
9156
+ * The branch goes with it. The project's branch is a column on the
9157
+ * project, so a build asserting one could only ever disagree with
9158
+ * the project it is publishing to.
9159
+ */
9160
+ ...(this.git ? {
9161
+ commit: this.git.commit
9162
+ } : {}),
8918
9163
  root: this.root,
8919
9164
  filesDirectory,
8920
9165
  baseSha: await this.getBaseSha(),
8921
9166
  committer,
8922
9167
  message,
8923
- existingBranch,
8924
9168
  newBranch,
8925
9169
  ...(patchGroupId !== undefined ? {
8926
9170
  patchGroupId
@@ -10669,7 +10913,7 @@ async function initHandlerOptions(route, opts, config) {
10669
10913
  valContentUrl
10670
10914
  });
10671
10915
  if (isProxyMode) {
10672
- var _opts$versions, _opts$versions2;
10916
+ var _opts$git, _opts$git2, _opts$versions, _opts$versions2;
10673
10917
  /*
10674
10918
  * Why this app is in http mode, in the message that says what is missing.
10675
10919
  *
@@ -10682,13 +10926,30 @@ async function initHandlerOptions(route, opts, config) {
10682
10926
  if (!maybeApiKey || !maybeValSecret) {
10683
10927
  throw new Error("VAL_API_KEY and VAL_SECRET env vars must both be set in proxy mode" + because);
10684
10928
  }
10685
- const maybeGitCommit = opts.gitCommit || process.env.VAL_GIT_COMMIT;
10686
- if (!maybeGitCommit) {
10687
- throw new Error("VAL_GIT_COMMIT env var must be set in proxy mode" + because);
10688
- }
10689
- const maybeGitBranch = opts.gitBranch || process.env.VAL_GIT_BRANCH;
10690
- if (!maybeGitBranch) {
10691
- throw new Error("VAL_GIT_BRANCH env var must be set in proxy mode" + because);
10929
+ /*
10930
+ * A COMMIT IS NOT WHAT PUTS AN APP IN HTTP MODE. Credentials are.
10931
+ *
10932
+ * Both of these used to be required here, and the requirement was a
10933
+ * repository disguised as a configuration check: a deployment with no
10934
+ * commit to name -- one whose content service owns its content, which is
10935
+ * now the normal case -- threw at boot, or, worse, never reached this
10936
+ * branch at all and fell through to `fs` mode, looking for a working tree
10937
+ * that was not there.
10938
+ *
10939
+ * Absent is a project with no repository to mirror commits into. The
10940
+ * content service mints its own commit shas and is the store of record for
10941
+ * content, so there is nothing missing: see `git` on {@link ValApiOptions}
10942
+ * for what a commit is still FOR where there is one.
10943
+ *
10944
+ * Taken together or not at all. A commit without a branch names a point
10945
+ * with no line of work to publish to, and a branch without a commit names
10946
+ * a line with no position in it; either alone would be a half-configured
10947
+ * repository that fails later, at a publish, rather than here.
10948
+ */
10949
+ const maybeGitCommit = ((_opts$git = opts.git) === null || _opts$git === void 0 ? void 0 : _opts$git.commit) || process.env.VAL_GIT_COMMIT;
10950
+ const maybeGitBranch = ((_opts$git2 = opts.git) === null || _opts$git2 === void 0 ? void 0 : _opts$git2.branch) || process.env.VAL_GIT_BRANCH;
10951
+ if (!!maybeGitCommit !== !!maybeGitBranch) {
10952
+ throw new Error(`Val is configured with a git ${maybeGitCommit ? "commit" : "branch"} ` + `but no ${maybeGitCommit ? "branch" : "commit"}. Set both (the ` + "`git` option, or VAL_GIT_COMMIT and VAL_GIT_BRANCH) for a project " + "whose content is mirrored into a repository, or neither for one " + "whose content service is the store of record." + because);
10692
10953
  }
10693
10954
  if (!maybeValProject) {
10694
10955
  throw new Error("Proxy mode does not work unless the 'project' option in val.config is defined or the VAL_PROJECT env var is set." + because);
@@ -10706,8 +10967,19 @@ async function initHandlerOptions(route, opts, config) {
10706
10967
  route,
10707
10968
  apiKey: maybeApiKey,
10708
10969
  valSecret: maybeValSecret,
10709
- commit: maybeGitCommit,
10710
- branch: maybeGitBranch,
10970
+ /*
10971
+ * Spread, so a project with no repository has no `git` key at all rather
10972
+ * than one holding undefined. `ValOpsHttp` asks `git === null` to decide
10973
+ * whether to send a branch and a commit with every request, and a key
10974
+ * that is present-but-undefined is one more thing for that check to get
10975
+ * wrong.
10976
+ */
10977
+ ...(maybeGitCommit && maybeGitBranch ? {
10978
+ git: {
10979
+ commit: maybeGitCommit,
10980
+ branch: maybeGitBranch
10981
+ }
10982
+ } : {}),
10711
10983
  root: opts.root,
10712
10984
  project: maybeValProject,
10713
10985
  valEnableRedirectUrl,
@@ -10770,7 +11042,7 @@ function createValOps(valModules, options) {
10770
11042
  });
10771
11043
  }
10772
11044
  if (options.mode === "http") {
10773
- return new ValOpsHttp(options.valContentUrl, options.project, options.commit, options.branch, {
11045
+ return new ValOpsHttp(options.valContentUrl, options.project, options.git ?? null, {
10774
11046
  apiKey: options.apiKey
10775
11047
  }, valModules, {
10776
11048
  formatter: options.formatter,
@@ -10976,6 +11248,7 @@ async function resolveRemoteFileAuth(options) {
10976
11248
  /** What a publish reports back, in either shape. */
10977
11249
 
10978
11250
  const ValServer = (valModules, options, callbacks) => {
11251
+ var _options$git;
10979
11252
  const AIContentBlock = z.z.union([z.z.object({
10980
11253
  type: z.z.literal("text"),
10981
11254
  text: z.z.string()
@@ -11007,7 +11280,7 @@ const ValServer = (valModules, options, callbacks) => {
11007
11280
  url.searchParams.set("state", token);
11008
11281
  return url.toString();
11009
11282
  };
11010
- const commit = options.mode === "http" ? options.commit : undefined;
11283
+ const commit = options.mode === "http" ? (_options$git = options.git) === null || _options$git === void 0 ? void 0 : _options$git.commit : undefined;
11011
11284
  const getAppErrorUrl = error => {
11012
11285
  if (!options.project) {
11013
11286
  throw new Error("Project is not set");
@@ -11604,12 +11877,24 @@ const ValServer = (valModules, options, callbacks) => {
11604
11877
  * start against a server that was working.
11605
11878
  */
11606
11879
  const mode = serverOps.patchesAreLocal ? "fs" : "http";
11880
+ /*
11881
+ * Why publishing is unavailable, so the Studio can say so.
11882
+ *
11883
+ * Spread rather than set to null, so a server that can publish sends
11884
+ * no such key at all: the Studio's "can I publish" is then the
11885
+ * presence of the field, and there is no null to mistake for a reason
11886
+ * it failed to compute.
11887
+ */
11888
+ const publishRefusal = serverOps.publishRefusal();
11607
11889
  return {
11608
11890
  status: 200,
11609
11891
  json: {
11610
11892
  ...currentStat,
11611
11893
  profileId: profileId ?? null,
11612
11894
  mode,
11895
+ ...(publishRefusal ? {
11896
+ publishRefusal
11897
+ } : {}),
11613
11898
  config: options.config
11614
11899
  }
11615
11900
  };
@@ -12798,6 +13083,29 @@ const ValServer = (valModules, options, callbacks) => {
12798
13083
  patchIds,
12799
13084
  excludePatchOps: false
12800
13085
  });
13086
+ /*
13087
+ * Can this deployment publish this project AT ALL?
13088
+ *
13089
+ * After the fetch above, deliberately: that call is what tells the
13090
+ * data layer what the project expects, and asking before it would be
13091
+ * asking a question nothing has answered yet. It is still before
13092
+ * anything is written, which is the part that matters.
13093
+ *
13094
+ * The Studio already knows -- `/stat` carries the same refusal, so the
13095
+ * action is disabled with the reason shown. This is the second half of
13096
+ * that: a stat can be stale by a poll, and nothing may reach `prepare`
13097
+ * on a project it cannot mirror.
13098
+ */
13099
+ const refusal = serverOps.publishRefusal();
13100
+ if (refusal) {
13101
+ return {
13102
+ status: 409,
13103
+ json: {
13104
+ message: refusal.message,
13105
+ publishRefusal: refusal
13106
+ }
13107
+ };
13108
+ }
12801
13109
  /*
12802
13110
  * Exactly the patches this request consumes, and the ONLY ones it may
12803
13111
  * delete afterwards.