@devdogsuga/backstage 0.1.6 → 0.1.8

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/README.md CHANGED
@@ -49,6 +49,7 @@ pnpm backstage export stars --from 2026-08-17 # audited under your gh login
49
49
  | `deploy plan`, `deploy migrate` | Dry-run the migrations into the job summary; apply them to `DB_URL`. |
50
50
  | `deploy smoke --tier <t> [--app]` | Public routes answer 200, the auth redirect works, this deploy's Sentry release shows. |
51
51
  | `deploy reconcile --tier <t>` | The platform's config reconcile, after the deploy (`CRON_SECRET`). |
52
+ | `deploy prune-monitors --tier <t> [--app]` | Deletes the app's Sentry Crons monitors it no longer declares (production only). |
52
53
  | `env pull\|push\|audit --target <t>` | One env file per target, synced to Bitwarden and GitHub. `audit` lists orphans. |
53
54
  | `planner status\|create\|reset-password\|drop` | The `migration_planner` role the preflight tier holds. |
54
55
  | `graphics [graphic…]` | Club images from `@devdogsuga/brand`: `brand/*`, `app/*`, `event/*`. |
@@ -77,8 +78,7 @@ DevDogsUGA, as a `smoke` field on each app's entry in `workers.json`:
77
78
  "protectedPath": "/console/permissions",
78
79
  "protectedRedirectPrefix": "/auth"
79
80
  }
80
- },
81
- "apps/sandbox"
81
+ }
82
82
  ]
83
83
  ```
84
84
 
@@ -234,7 +234,8 @@ stderr, and every confirmation needs `--yes`. Against staging or production a
234
234
  command that is not read-only asks once first (`--yes` answers it).
235
235
 
236
236
  Each command checks for the secrets it uses up front: `deploy <app>` for
237
- `CLOUDFLARE_API_TOKEN`, `deploy reconcile` for `CRON_SECRET`, `env` for the
237
+ `CLOUDFLARE_API_TOKEN`, `deploy reconcile` for `CRON_SECRET`, `deploy
238
+ prune-monitors` for `SENTRY_MONITORS_TOKEN` (skipped without it), `env` for the
238
239
  Secrets Manager token (flag, environment, then the Bitwarden vault, which `env`
239
240
  signs in to and unlocks itself, then asking).
240
241
 
@@ -1,6 +1,6 @@
1
- import { A as YES, D as DRY_RUN, O as JSON_FLAG, j as createCatalog, k as SCOPES, v as noteRan, w as findRepoRoot } from "./telemetry-Bjoz29Hl.js";
2
- import { n as isDryRun } from "./dry-run-3IxPmCtW.js";
3
- import { o as unwrap } from "./ui-CdKo8mLw.js";
1
+ import { A as YES, D as DRY_RUN, O as JSON_FLAG, j as createCatalog, k as SCOPES, v as noteRan, w as findRepoRoot } from "./telemetry-Be_lbB5c.js";
2
+ import { n as isDryRun } from "./dry-run-CFCZUpjb.js";
3
+ import { o as unwrap } from "./ui-Bb69ZfVv.js";
4
4
  import { n as positionals } from "./args-Cjr_Iqts.js";
5
5
  import { t as EXPORTS } from "./queries-DeEwfe2c.js";
6
6
  import { r as qrCatalogOptions } from "./options-DCaqm8BI.js";
@@ -1099,7 +1099,6 @@ const deployCommand = {
1099
1099
  subcommands: [
1100
1100
  app("platform"),
1101
1101
  app("schedule-builder"),
1102
- app("sandbox"),
1103
1102
  {
1104
1103
  name: "write-env",
1105
1104
  summary: "Compose .env.<DEPLOY_ENV> from the GitHub environment.",
@@ -1143,6 +1142,16 @@ const deployCommand = {
1143
1142
  summary: "Run the platform's config reconcile after a deploy.",
1144
1143
  hint: "needs CRON_SECRET",
1145
1144
  options: [DEPLOY_TIER, SMOKE_APP]
1145
+ },
1146
+ {
1147
+ name: "prune-monitors",
1148
+ summary: "Delete the Sentry Crons monitors an app no longer declares.",
1149
+ hint: "needs SENTRY_MONITORS_TOKEN; production only",
1150
+ options: [
1151
+ DEPLOY_TIER,
1152
+ SMOKE_APP,
1153
+ DRY_RUN
1154
+ ]
1146
1155
  }
1147
1156
  ]
1148
1157
  };
@@ -1,6 +1,6 @@
1
- import { C as discoverRepoRoot, p as isNonInteractive } from "./telemetry-Bjoz29Hl.js";
1
+ import { C as discoverRepoRoot, p as isNonInteractive } from "./telemetry-Be_lbB5c.js";
2
2
  import { t as nonEmpty } from "./connection-D3XsOE3S.js";
3
- import { a as explainError, i as explain, o as unwrap, t as UsageError } from "./ui-CdKo8mLw.js";
3
+ import { a as explainError, i as explain, o as unwrap, t as UsageError } from "./ui-Bb69ZfVv.js";
4
4
  import { t as EnvDocument } from "./document-DumoZXoO.js";
5
5
  import { resolve } from "node:path";
6
6
  import { confirm, log, text } from "@clack/prompts";
@@ -31,7 +31,7 @@ async function readProductionKey(key) {
31
31
  const value = nonEmpty(EnvDocument.parse(text).get(key));
32
32
  if (value) return value;
33
33
  } catch {}
34
- const { listSecrets, projectIdFor } = await import("./client-DwDGjalU.js");
34
+ const { listSecrets, projectIdFor } = await import("./client-A6pJcxSE.js");
35
35
  const secrets = await listSecrets(await projectIdFor(PRODUCTION_PROJECT));
36
36
  return nonEmpty(secrets.find((s) => s.key === key)?.value);
37
37
  }
@@ -1,13 +1,13 @@
1
- import { C as discoverRepoRoot, T as cliName, b as ownPackageDir, c as reportDevtoolsError, f as isNoEnv, l as reportDevtoolsFailure, n as captureDevtoolsError, o as initDevtoolsTelemetry, p as isNonInteractive, w as findRepoRoot, x as ownVersion } from "./telemetry-Bjoz29Hl.js";
2
- import { a as recordEnteredTier, f as helpPath, i as beginInvocation, l as dbPush, m as renderHelp, n as bareGroupStartPath, o as recordResolved, p as renderCommandList, r as runMenu, s as reproducibleCommand, t as catalog, u as dbPushDryRun } from "./catalog-Dke7bQVz.js";
3
- import { a as repoPeerUrl, n as loadEnv, r as loadEnvLoad, t as getEnvSync } from "./peers-B0KDkI6a.js";
1
+ import { C as discoverRepoRoot, T as cliName, b as ownPackageDir, c as reportDevtoolsError, f as isNoEnv, l as reportDevtoolsFailure, n as captureDevtoolsError, o as initDevtoolsTelemetry, p as isNonInteractive, w as findRepoRoot, x as ownVersion } from "./telemetry-Be_lbB5c.js";
2
+ import { a as recordEnteredTier, f as helpPath, i as beginInvocation, l as dbPush, m as renderHelp, n as bareGroupStartPath, o as recordResolved, p as renderCommandList, r as runMenu, s as reproducibleCommand, t as catalog, u as dbPushDryRun } from "./catalog-BWtUtxMF.js";
3
+ import { a as repoPeerUrl, n as loadEnv, r as loadEnvLoad, t as getEnvSync } from "./peers-u1zxe-Ub.js";
4
4
  import { t as nonEmpty } from "./connection-D3XsOE3S.js";
5
- import { a as wouldRunLine, n as isDryRun, t as dryRunKind } from "./dry-run-3IxPmCtW.js";
6
- import { a as explainError, i as explain, n as bail, o as unwrap, r as errorMessage } from "./ui-CdKo8mLw.js";
5
+ import { a as wouldRunLine, n as isDryRun, t as dryRunKind } from "./dry-run-CFCZUpjb.js";
6
+ import { a as explainError, i as explain, n as bail, o as unwrap, r as errorMessage } from "./ui-Bb69ZfVv.js";
7
7
  import { n as positionals, t as flagValue } from "./args-Cjr_Iqts.js";
8
8
  import { t as DONE } from "./dispatch-D048O65I.js";
9
9
  import { t as EnvDocument } from "./document-DumoZXoO.js";
10
- import { a as listSecrets$1, c as updateSecret, i as createSecret, o as projectIdFor, r as byKey, s as setExplicitAccessToken } from "./client-B_MY9kVI.js";
10
+ import { a as listSecrets$1, c as updateSecret, i as createSecret, o as projectIdFor, r as byKey, s as setExplicitAccessToken } from "./client-NuoKMQmV.js";
11
11
  import { register } from "node:module";
12
12
  import { pathToFileURL } from "node:url";
13
13
  import { appendFileSync, existsSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
@@ -249,16 +249,12 @@ async function importRepoTs(absolutePath) {
249
249
  * Most workspace packages declare nothing, so "no env.ts found" means
250
250
  * not-a-manifest rather than an error.
251
251
  *
252
- * ⚠️ `apps/sandbox` was once listed here as having no manifest ON PURPOSE, on
253
- * the grounds that a Worker reads bindings that arrive as a function argument
254
- * rather than `process.env`. That premise is still true and the conclusion
255
- * drawn from it was still wrong: a manifest is also the only thing that routes
256
- * a credential to a deployed environment and the only thing that tells
257
- * `env audit` a Worker secret is supposed to be there. Without one,
258
- * `SANDBOX_PROXY_TOKEN`, minted at deploy time and therefore in no Bitwarden
259
- * project by design, was reported as an orphan, i.e. as safe for the §3.6 prune
260
- * path to delete. `apps/sandbox/env.ts` exists now, and states that reasoning
261
- * at length.
252
+ * ⚠️ A Worker still needs a manifest, even though its bindings arrive as a
253
+ * function argument rather than `process.env`: a manifest is the only thing
254
+ * that routes a credential to a deployed environment and the only thing that
255
+ * tells `env audit` a Worker secret is supposed to be there. Without one, a
256
+ * minted secret (in no Bitwarden project by design) is reported as an orphan,
257
+ * i.e. as safe for the §3.6 prune path to delete.
262
258
  */
263
259
  /**
264
260
  * The single in-flight (or settled) load.
@@ -553,6 +549,116 @@ async function runDeployMigrate(env = process.env, push = (url) => dbPush(url, {
553
549
  say(["deploy migrate: migrations applied."]);
554
550
  }
555
551
  //#endregion
552
+ //#region src/deploy/monitors.ts
553
+ /**
554
+ * `deploy prune-monitors`: deletes the Sentry Crons monitors an app no longer
555
+ * declares.
556
+ *
557
+ * Sentry creates a monitor the first time a job checks in, and never removes
558
+ * one. Rename or drop a `monitorSlug` and the old monitor keeps expecting
559
+ * check-ins that will never come, opening a "missed check-in" issue every
560
+ * period until someone deletes it by hand (PLATFORM-2: the Discord role sync
561
+ * folded into the fifteen-minute cron, and its ten-minute monitor alerted
562
+ * every ten minutes after the deploy).
563
+ *
564
+ * The declared set is every `monitorSlug` on the entries of the app's
565
+ * `cloudflare/scheduled.ts` exports `CRON_ROUTES` and `WORKFLOW_CRONS`. Every
566
+ * monitor in the app's Sentry project that is not in it, and whose slug
567
+ * starts with `<app>-`, is deleted. The prefix keeps a monitor someone made
568
+ * by hand in the Sentry UI out of reach.
569
+ *
570
+ * Delete, not disable. Sentry drops every check-in sent to a disabled monitor
571
+ * and never re-enables one from a check-in, so a slug that came back would
572
+ * report nowhere. A deleted one is recreated by the next check-in that
573
+ * carries its config (each `withMonitor`/`captureCheckIn` here does), and the
574
+ * delete renames the old slug first, so the name is free at once.
575
+ *
576
+ * Production only. Monitors belong to a project, not a tier, and staging
577
+ * deploys `main`, which can be ahead of production: a monitor `main` dropped
578
+ * still has a production job checking in to it.
579
+ *
580
+ * `SENTRY_MONITORS_TOKEN`, not `SENTRY_AUTH_TOKEN`: the deploy token is an
581
+ * organization token (`org:ci`), which Sentry lets neither list nor delete
582
+ * monitors. This one needs `alerts:write` (`alerts:read` to list), and is
583
+ * held in this step's `env:` alone. Absent, the step is skipped with a
584
+ * notice, the same as the Sentry release check.
585
+ */
586
+ const SENTRY_API$1 = "https://sentry.io/api/0";
587
+ /** Only the field read here; the exports' own shapes are devtools' to check. */
588
+ const Declarations = z.record(z.string(), z.object({ monitorSlug: z.string().min(1).optional() }));
589
+ const MonitorList = z.array(z.object({ slug: z.string() }));
590
+ /**
591
+ * Every `monitorSlug` the app's `cloudflare/scheduled.ts` declares, or
592
+ * `undefined` when the app has no such file (no crons, nothing to prune).
593
+ */
594
+ async function declaredMonitorSlugs(app, root = findRepoRoot()) {
595
+ const path = join(root, "apps", app, "cloudflare", "scheduled.ts");
596
+ if (!existsSync(path)) return void 0;
597
+ const mod = await import(pathToFileURL(path).href);
598
+ const slugs = /* @__PURE__ */ new Set();
599
+ for (const name of ["CRON_ROUTES", "WORKFLOW_CRONS"]) {
600
+ const parsed = Declarations.safeParse(mod[name] ?? {});
601
+ if (!parsed.success) throw new DeployError(`${path}: ${name} is not a map of entries.`);
602
+ for (const entry of Object.values(parsed.data)) if (entry.monitorSlug) slugs.add(entry.monitorSlug);
603
+ }
604
+ return slugs;
605
+ }
606
+ /** The `rel="next"` URL of a Sentry `Link` header, when it has results. */
607
+ function nextPage(link) {
608
+ for (const part of link?.split(",") ?? []) if (part.includes("rel=\"next\"") && part.includes("results=\"true\"")) return /<([^>]+)>/.exec(part)?.[1];
609
+ }
610
+ async function listMonitorSlugs(org, project, headers, fetchImpl) {
611
+ const slugs = [];
612
+ let url = `${SENTRY_API$1}/organizations/${org}/monitors/?project=${encodeURIComponent(project)}`;
613
+ while (url) {
614
+ const response = await fetchImpl(url, { headers });
615
+ if (!response.ok) throw new DeployError(`Listing Sentry monitors: HTTP ${response.status}.`, ["SENTRY_MONITORS_TOKEN needs alerts:read and alerts:write."]);
616
+ slugs.push(...MonitorList.parse(await response.json()).map((m) => m.slug));
617
+ url = nextPage(response.headers.get("link"));
618
+ }
619
+ return slugs;
620
+ }
621
+ /** Returns the slugs it deleted (or would have, on a dry run). */
622
+ async function runPruneMonitors(options) {
623
+ const env = options.env ?? process.env;
624
+ const { app } = options;
625
+ if (options.tier !== "production") {
626
+ say([`prune-monitors: skipped on ${options.tier}; monitors are pruned against production only.`]);
627
+ return [];
628
+ }
629
+ const token = env.SENTRY_MONITORS_TOKEN;
630
+ if (!token) {
631
+ say(["::notice::Sentry monitor pruning skipped -- SENTRY_MONITORS_TOKEN is not configured."]);
632
+ return [];
633
+ }
634
+ if (!env.SENTRY_ORG) throw new DeployError("SENTRY_ORG is not set.");
635
+ const declared = options.declared ?? await declaredMonitorSlugs(app);
636
+ if (!declared) {
637
+ say([`prune-monitors: ${app} has no cloudflare/scheduled.ts; nothing to prune.`]);
638
+ return [];
639
+ }
640
+ const headers = { authorization: `Bearer ${token}` };
641
+ const fetchImpl = options.fetchImpl ?? fetch;
642
+ const orphans = (await listMonitorSlugs(env.SENTRY_ORG, app, headers, fetchImpl)).filter((slug) => slug.startsWith(`${app}-`) && !declared.has(slug));
643
+ if (orphans.length === 0) {
644
+ say([`prune-monitors: every ${app} monitor is declared.`]);
645
+ return [];
646
+ }
647
+ for (const slug of orphans) {
648
+ if (options.dryRun) {
649
+ say([`Would delete Sentry monitor ${slug}`]);
650
+ continue;
651
+ }
652
+ const response = await fetchImpl(`${SENTRY_API$1}/projects/${env.SENTRY_ORG}/${app}/monitors/${encodeURIComponent(slug)}/`, {
653
+ method: "DELETE",
654
+ headers
655
+ });
656
+ if (!response.ok && response.status !== 404) throw new DeployError(`Deleting Sentry monitor ${slug}: HTTP ${response.status}.`);
657
+ say([`Deleted Sentry monitor ${slug}`]);
658
+ }
659
+ return orphans;
660
+ }
661
+ //#endregion
556
662
  //#region src/deploy/preflight.ts
557
663
  /**
558
664
  * `pnpm devtools deploy preflight`
@@ -737,8 +843,7 @@ async function runPreflight(deps = {}) {
737
843
  * ## Minted keys are refused, not filled
738
844
  *
739
845
  * A `secrecy: "secret"` key `storableKeys()` excludes is "minted": signed at
740
- * deploy time rather than stored anywhere, which is what `SANDBOX_PROXY_TOKEN`
741
- * is declared as. This has no minter of its own — there is nothing in
846
+ * deploy time rather than stored anywhere. This has no minter of its own — there is nothing in
742
847
  * this repository that produces one — so a declared minted key is always a
743
848
  * hard failure here rather than a silent omission: a Worker that expects a
744
849
  * value substituted fails loudly at the file it never got, not at whatever
@@ -1551,13 +1656,13 @@ function renderWriteEnvReport(result) {
1551
1656
  * which leaves a Wrangler "config redirect" at `.wrangler/deploy/config.json`
1552
1657
  * pointing at the generated `dist/server/wrangler.json`; a bare `wrangler
1553
1658
  * deploy` from the app dir resolves through it with no `--config` needed.
1554
- * Sandbox (plain Worker) always used this shape, so all three apps share one
1555
- * deploy step.
1659
+ * Both apps share one deploy step.
1556
1660
  *
1557
- * Steps (`write-env`, `preflight`, `plan`, `migrate`, `smoke`, `reconcile`) are
1558
- * individually addressable for jobs that run only one. The ones that hold a
1559
- * single credential in the job's own `env:` block and compose no env file
1560
- * (`preflight`, `plan`, `migrate`, `smoke`, `reconcile`), and `write-env`,
1661
+ * Steps (`write-env`, `preflight`, `plan`, `migrate`, `smoke`, `reconcile`,
1662
+ * `prune-monitors`) are individually addressable for jobs that run only one.
1663
+ * The ones that hold a single credential in the job's own `env:` block and
1664
+ * compose no env file (`preflight`, `plan`, `migrate`, `smoke`, `reconcile`,
1665
+ * `prune-monitors`), and `write-env`,
1561
1666
  * which CREATES the file tier resolution would otherwise insist on reading,
1562
1667
  * run with `--no-env`.
1563
1668
  */
@@ -1707,6 +1812,14 @@ async function runStep(sub, rest) {
1707
1812
  });
1708
1813
  return true;
1709
1814
  }
1815
+ if (sub === "prune-monitors") {
1816
+ await runPruneMonitors({
1817
+ app: flagValue(rest, "--app") ?? "platform",
1818
+ tier: requireTier(rest),
1819
+ dryRun: rest.includes("--dry-run")
1820
+ });
1821
+ return true;
1822
+ }
1710
1823
  if (sub === "write-env") {
1711
1824
  await loadRegistry();
1712
1825
  const index = rest.indexOf("--source");
@@ -2619,9 +2732,9 @@ async function listWorkerSecrets(target) {
2619
2732
  *
2620
2733
  * The expected set is derived from the app's own manifest: every key it
2621
2734
  * declares with `secrecy: "secret"`. That deliberately includes the MINTED
2622
- * ones. `SANDBOX_PROXY_TOKEN` has no copy in Bitwarden or GitHub by design, and
2623
- * an audit that reasoned from stored copies alone would report the live proxy
2624
- * credential as safe to delete. (`env audit` had exactly that bug; see
2735
+ * ones. A minted key has no copy in Bitwarden or GitHub by design, and an
2736
+ * audit that reasoned from stored copies alone would report a live credential
2737
+ * as safe to delete. (`env audit` had exactly that bug; see
2625
2738
  * `EnvMeta.minted`.)
2626
2739
  *
2627
2740
  * ## Report always, prune almost never (security plan §3.6)
@@ -5267,12 +5380,12 @@ const HANDLERS = {
5267
5380
  },
5268
5381
  completions: handleCompletions,
5269
5382
  github: handleGithub,
5270
- graphics: async (rest) => (await import("./commands-Dv5TXjvb.js")).handleGraphics(rest),
5271
- qr: async (rest) => (await import("./commands-Sx1fDNNn.js")).handleQr(rest),
5272
- newsletter: async (rest) => (await import("./commands-D345F-Od.js")).handleNewsletter(rest),
5273
- creds: async (rest) => (await import("./commands-zPSnOGzH.js")).handleCreds(rest),
5274
- import: async (rest) => (await import("./commands-DzALVX5g.js")).handleImport(rest),
5275
- export: async (rest) => (await import("./commands-5Ou8xJw5.js")).handleExport(rest)
5383
+ graphics: async (rest) => (await import("./commands-CQ-vbNe9.js")).handleGraphics(rest),
5384
+ qr: async (rest) => (await import("./commands-DQYH_m6i.js")).handleQr(rest),
5385
+ newsletter: async (rest) => (await import("./commands-35rBJ1R0.js")).handleNewsletter(rest),
5386
+ creds: async (rest) => (await import("./commands-BF5eptEi.js")).handleCreds(rest),
5387
+ import: async (rest) => (await import("./commands-zApyD2Rk.js")).handleImport(rest),
5388
+ export: async (rest) => (await import("./commands-D4FcDQuh.js")).handleExport(rest)
5276
5389
  };
5277
5390
  /**
5278
5391
  * Names that were commands of the contributor CLI, refused with where they
@@ -0,0 +1,2 @@
1
+ import { a as listSecrets, o as projectIdFor } from "./client-NuoKMQmV.js";
2
+ export { listSecrets, projectIdFor };
@@ -1,8 +1,8 @@
1
- import { w as findRepoRoot } from "./telemetry-Bjoz29Hl.js";
2
- import { n as loadEnv } from "./peers-B0KDkI6a.js";
3
- import { o as unwrap } from "./ui-CdKo8mLw.js";
1
+ import { w as findRepoRoot } from "./telemetry-Be_lbB5c.js";
2
+ import { n as loadEnv } from "./peers-u1zxe-Ub.js";
3
+ import { o as unwrap } from "./ui-Bb69ZfVv.js";
4
4
  import { t as EnvDocument } from "./document-DumoZXoO.js";
5
- import { a as readTokenFromVault, o as saveTokenToVault, t as VAULT_ITEM_NAME } from "./vault-CsBC-1Mq.js";
5
+ import { a as readTokenFromVault, o as saveTokenToVault, t as VAULT_ITEM_NAME } from "./vault-DFV0zjbT.js";
6
6
  import { dirname, join, resolve } from "node:path";
7
7
  import { log, password, select, text } from "@clack/prompts";
8
8
  import { mkdir, readFile, writeFile } from "node:fs/promises";
@@ -1,5 +1,5 @@
1
- import { p as isNonInteractive } from "./telemetry-Bjoz29Hl.js";
2
- import { a as explainError, o as unwrap, r as errorMessage, t as UsageError } from "./ui-CdKo8mLw.js";
1
+ import { p as isNonInteractive } from "./telemetry-Be_lbB5c.js";
2
+ import { a as explainError, o as unwrap, r as errorMessage, t as UsageError } from "./ui-Bb69ZfVv.js";
3
3
  import { t as DONE } from "./dispatch-D048O65I.js";
4
4
  import { dirname, join, resolve } from "node:path";
5
5
  import { confirm, log, multiselect, spinner, text } from "@clack/prompts";
@@ -1,9 +1,9 @@
1
- import { C as discoverRepoRoot, p as isNonInteractive } from "./telemetry-Bjoz29Hl.js";
1
+ import { C as discoverRepoRoot, p as isNonInteractive } from "./telemetry-Be_lbB5c.js";
2
2
  import { t as nonEmpty } from "./connection-D3XsOE3S.js";
3
- import { a as explainError, i as explain, o as unwrap, r as errorMessage, t as UsageError } from "./ui-CdKo8mLw.js";
3
+ import { a as explainError, i as explain, o as unwrap, r as errorMessage, t as UsageError } from "./ui-Bb69ZfVv.js";
4
4
  import { t as DONE } from "./dispatch-D048O65I.js";
5
5
  import { t as EnvDocument } from "./document-DumoZXoO.js";
6
- import { i as readPasswordFromVault, n as bwArgs, r as openVault, s as bwCommand } from "./vault-CsBC-1Mq.js";
6
+ import { i as readPasswordFromVault, n as bwArgs, r as openVault, s as bwCommand } from "./vault-DFV0zjbT.js";
7
7
  import { resolve } from "node:path";
8
8
  import { confirm, log, multiselect, note, password, select, text } from "@clack/prompts";
9
9
  import { readFile } from "node:fs/promises";
@@ -632,7 +632,7 @@ async function readCheckoutDbUrl() {
632
632
  }
633
633
  }
634
634
  async function readSecretsManagerDbUrl() {
635
- const { listSecrets, projectIdFor } = await import("./client-DwDGjalU.js");
635
+ const { listSecrets, projectIdFor } = await import("./client-A6pJcxSE.js");
636
636
  const secrets = await listSecrets(await projectIdFor(PRODUCTION_PROJECT));
637
637
  return nonEmpty(secrets.find((s) => s.key === "DB_URL")?.value);
638
638
  }
@@ -1,5 +1,5 @@
1
- import { p as isNonInteractive } from "./telemetry-Bjoz29Hl.js";
2
- import { a as explainError, o as unwrap, r as errorMessage, t as UsageError } from "./ui-CdKo8mLw.js";
1
+ import { p as isNonInteractive } from "./telemetry-Be_lbB5c.js";
2
+ import { a as explainError, o as unwrap, r as errorMessage, t as UsageError } from "./ui-Bb69ZfVv.js";
3
3
  import { n as positionals } from "./args-Cjr_Iqts.js";
4
4
  import { t as DONE } from "./dispatch-D048O65I.js";
5
5
  import { dirname, relative, resolve } from "node:path";
@@ -1,9 +1,9 @@
1
- import { n as isDryRun } from "./dry-run-3IxPmCtW.js";
2
- import { o as unwrap, r as errorMessage, t as UsageError } from "./ui-CdKo8mLw.js";
1
+ import { n as isDryRun } from "./dry-run-CFCZUpjb.js";
2
+ import { o as unwrap, r as errorMessage, t as UsageError } from "./ui-Bb69ZfVv.js";
3
3
  import { t as DONE } from "./dispatch-D048O65I.js";
4
- import { a as reportFailure, c as connect, d as withConnection, i as readInputFile, n as confirmWrite, o as say, r as plural, s as ProductionError, t as canPrompt, u as requireProductionKey } from "./cli-Do2nWD7W.js";
5
- import { a as createAccounts, i as createAccountFor, n as accountFinder, r as accountsWith, t as parseCsv } from "./read-B-aJJewb.js";
6
- import { n as findMeetingsWith, r as pickMeeting, t as describeMeeting } from "./meetings-DfU867eV.js";
4
+ import { a as reportFailure, c as connect, d as withConnection, i as readInputFile, n as confirmWrite, o as say, r as plural, s as ProductionError, t as canPrompt, u as requireProductionKey } from "./cli-D6swhEN-.js";
5
+ import { a as createAccounts, i as createAccountFor, n as accountFinder, r as accountsWith, t as parseCsv } from "./read-BLyKoe4l.js";
6
+ import { n as findMeetingsWith, r as pickMeeting, t as describeMeeting } from "./meetings-CtBMT_25.js";
7
7
  import { text } from "@clack/prompts";
8
8
  import { parseArgs } from "node:util";
9
9
  //#region src/attendance/plan.ts
@@ -1,9 +1,9 @@
1
- import { n as isDryRun } from "./dry-run-3IxPmCtW.js";
2
- import { r as errorMessage, t as UsageError } from "./ui-CdKo8mLw.js";
1
+ import { n as isDryRun } from "./dry-run-CFCZUpjb.js";
2
+ import { r as errorMessage, t as UsageError } from "./ui-Bb69ZfVv.js";
3
3
  import { i as exportParams, n as EXPORT_KINDS, r as auditFilters, t as EXPORTS } from "./queries-DeEwfe2c.js";
4
4
  import { t as DONE } from "./dispatch-D048O65I.js";
5
- import { a as reportFailure, c as connect, l as dbError, o as say, r as plural, s as ProductionError, u as requireProductionKey } from "./cli-Do2nWD7W.js";
6
- import { i as parseBound, n as findMeetingsWith, r as pickMeeting, t as describeMeeting } from "./meetings-DfU867eV.js";
5
+ import { a as reportFailure, c as connect, l as dbError, o as say, r as plural, s as ProductionError, u as requireProductionKey } from "./cli-D6swhEN-.js";
6
+ import { i as parseBound, n as findMeetingsWith, r as pickMeeting, t as describeMeeting } from "./meetings-CtBMT_25.js";
7
7
  import { open, rm } from "node:fs/promises";
8
8
  import { execFile } from "node:child_process";
9
9
  import { parseArgs, promisify } from "node:util";
@@ -1,6 +1,6 @@
1
- import { p as isNonInteractive } from "./telemetry-Bjoz29Hl.js";
2
- import { n as isDryRun } from "./dry-run-3IxPmCtW.js";
3
- import { a as explainError, o as unwrap, t as UsageError } from "./ui-CdKo8mLw.js";
1
+ import { p as isNonInteractive } from "./telemetry-Be_lbB5c.js";
2
+ import { n as isDryRun } from "./dry-run-CFCZUpjb.js";
3
+ import { a as explainError, o as unwrap, t as UsageError } from "./ui-Bb69ZfVv.js";
4
4
  import { i as qrParseOptions, n as QR_FLAGS, t as QR_EXTRA_FLAGS } from "./options-DCaqm8BI.js";
5
5
  import { t as DONE } from "./dispatch-D048O65I.js";
6
6
  import { dirname, extname, relative, resolve } from "node:path";
@@ -1,8 +1,8 @@
1
- import { n as isDryRun } from "./dry-run-3IxPmCtW.js";
2
- import { r as errorMessage, t as UsageError } from "./ui-CdKo8mLw.js";
1
+ import { n as isDryRun } from "./dry-run-CFCZUpjb.js";
2
+ import { r as errorMessage, t as UsageError } from "./ui-Bb69ZfVv.js";
3
3
  import { t as DONE } from "./dispatch-D048O65I.js";
4
- import { a as reportFailure, d as withConnection, i as readInputFile, n as confirmWrite, o as say, r as plural, t as canPrompt, u as requireProductionKey } from "./cli-Do2nWD7W.js";
5
- import { a as createAccounts, i as createAccountFor, n as accountFinder, o as readAccounts, t as parseCsv } from "./read-B-aJJewb.js";
4
+ import { a as reportFailure, d as withConnection, i as readInputFile, n as confirmWrite, o as say, r as plural, t as canPrompt, u as requireProductionKey } from "./cli-D6swhEN-.js";
5
+ import { a as createAccounts, i as createAccountFor, n as accountFinder, o as readAccounts, t as parseCsv } from "./read-BLyKoe4l.js";
6
6
  import { parseArgs } from "node:util";
7
7
  //#region src/involvement/csv.ts
8
8
  /**
@@ -1,9 +1,9 @@
1
- import { i as explain } from "./ui-CdKo8mLw.js";
1
+ import { i as explain } from "./ui-Bb69ZfVv.js";
2
2
  //#region src/import/commands.ts
3
3
  const handleImport = async (rest) => {
4
4
  const [what, ...args] = rest;
5
- if (what === "involvement") return (await import("./commands-CLSxKz-l.js")).handleInvolvement(args);
6
- if (what === "attendance") return (await import("./commands-BQ2NIqub.js")).handleAttendance(args);
5
+ if (what === "involvement") return (await import("./commands-Dwr4_CSu.js")).handleInvolvement(args);
6
+ if (what === "attendance") return (await import("./commands-D-sNogNX.js")).handleAttendance(args);
7
7
  explain(what ? `Unknown import "${what}". Try involvement or attendance.` : "Name the import: involvement or attendance.", "");
8
8
  process.exitCode = 1;
9
9
  return null;
@@ -1,4 +1,4 @@
1
- import { T as cliName } from "./telemetry-Bjoz29Hl.js";
1
+ import { T as cliName } from "./telemetry-Be_lbB5c.js";
2
2
  //#region ../cli-core/src/dry-run.ts
3
3
  const DRY_RUN_FLAG = "--dry-run";
4
4
  /** Commands that forward everything after their name to another tool. */
package/dist/launch.js CHANGED
@@ -1,9 +1,9 @@
1
- import { C as discoverRepoRoot, E as setCliName, S as RepoNotFoundError, T as cliName, _ as installFailureLog, d as hasYes, g as stripTierFlag, h as stripNoEnvFlag, m as setNoEnv, n as captureDevtoolsError, o as initDevtoolsTelemetry, p as isNonInteractive, s as lastSentryEventId } from "./telemetry-Bjoz29Hl.js";
2
- import { c as setMenuEnvHook, d as formatCommand, f as helpPath, n as bareGroupStartPath, t as catalog } from "./catalog-Dke7bQVz.js";
3
- import { i as loadEnvSession, r as loadEnvLoad } from "./peers-B0KDkI6a.js";
1
+ import { C as discoverRepoRoot, E as setCliName, S as RepoNotFoundError, T as cliName, _ as installFailureLog, d as hasYes, g as stripTierFlag, h as stripNoEnvFlag, m as setNoEnv, n as captureDevtoolsError, o as initDevtoolsTelemetry, p as isNonInteractive, s as lastSentryEventId } from "./telemetry-Be_lbB5c.js";
2
+ import { c as setMenuEnvHook, d as formatCommand, f as helpPath, n as bareGroupStartPath, t as catalog } from "./catalog-BWtUtxMF.js";
3
+ import { i as loadEnvSession, r as loadEnvLoad } from "./peers-u1zxe-Ub.js";
4
4
  import { t as nonEmpty } from "./connection-D3XsOE3S.js";
5
- import { i as setDryRun, n as isDryRun, r as resolveDryRun } from "./dry-run-3IxPmCtW.js";
6
- import { o as unwrap, r as errorMessage } from "./ui-CdKo8mLw.js";
5
+ import { i as setDryRun, n as isDryRun, r as resolveDryRun } from "./dry-run-CFCZUpjb.js";
6
+ import { o as unwrap, r as errorMessage } from "./ui-Bb69ZfVv.js";
7
7
  import { confirm } from "@clack/prompts";
8
8
  //#region ../cli-core/src/pipes.ts
9
9
  /**
@@ -322,7 +322,7 @@ async function launchWith(argv, options) {
322
322
  */
323
323
  async function launch(argv) {
324
324
  await launchWith(argv, { dispatch: async (args) => {
325
- const { main } = await import("./cli-BhZ4je2D.js");
325
+ const { main } = await import("./cli-J_avqnlu.js");
326
326
  await main(args);
327
327
  } });
328
328
  }
@@ -1,5 +1,5 @@
1
- import { t as UsageError } from "./ui-CdKo8mLw.js";
2
- import "./cli-Do2nWD7W.js";
1
+ import { t as UsageError } from "./ui-Bb69ZfVv.js";
2
+ import "./cli-D6swhEN-.js";
3
3
  //#region src/production/time.ts
4
4
  /**
5
5
  * Dates as officers type them. Meetings happen in Athens, so a bare
@@ -1,4 +1,4 @@
1
- import { S as RepoNotFoundError, w as findRepoRoot } from "./telemetry-Bjoz29Hl.js";
1
+ import { S as RepoNotFoundError, w as findRepoRoot } from "./telemetry-Be_lbB5c.js";
2
2
  import { createRequire } from "node:module";
3
3
  import { pathToFileURL } from "node:url";
4
4
  import { existsSync, readFileSync, readdirSync } from "node:fs";
@@ -1,5 +1,5 @@
1
- import { r as errorMessage } from "./ui-CdKo8mLw.js";
2
- import { d as withConnection, s as ProductionError } from "./cli-Do2nWD7W.js";
1
+ import { r as errorMessage } from "./ui-Bb69ZfVv.js";
2
+ import { d as withConnection, s as ProductionError } from "./cli-D6swhEN-.js";
3
3
  //#region src/production/accounts.ts
4
4
  /**
5
5
  * Production accounts: reading them, finding one by email, and creating the
@@ -438,8 +438,8 @@ function isNoEnv() {
438
438
  //#region ../cli-core/src/telemetry.ts
439
439
  /**
440
440
  * devtools' own Sentry wiring — the CLI is a consumer of
441
- * `@devdogsuga/telemetry` like `apps/platform`, `apps/schedule-builder`, and
442
- * `apps/sandbox`, but on `@sentry/node` rather than a framework SDK: a CLI
441
+ * `@devdogsuga/telemetry` like `apps/platform` and `apps/schedule-builder`,
442
+ * but on `@sentry/node` rather than a framework SDK: a CLI
443
443
  * process starts, runs one command, and exits, with none of a server's
444
444
  * request lifecycle for a framework integration to hook into.
445
445
  *
package/dist/telemetry.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as hasRealCaller, c as reportDevtoolsError, i as devtoolsTelemetryEnabled, l as reportDevtoolsFailure, n as captureDevtoolsError, o as initDevtoolsTelemetry, r as devtoolsEnvironment, s as lastSentryEventId, t as captureDevtoolsDeprecation, u as resolveDevtoolsDsn } from "./telemetry-Bjoz29Hl.js";
1
+ import { a as hasRealCaller, c as reportDevtoolsError, i as devtoolsTelemetryEnabled, l as reportDevtoolsFailure, n as captureDevtoolsError, o as initDevtoolsTelemetry, r as devtoolsEnvironment, s as lastSentryEventId, t as captureDevtoolsDeprecation, u as resolveDevtoolsDsn } from "./telemetry-Be_lbB5c.js";
2
2
  export { captureDevtoolsDeprecation, captureDevtoolsError, devtoolsEnvironment, devtoolsTelemetryEnabled, hasRealCaller, initDevtoolsTelemetry, lastSentryEventId, reportDevtoolsError, reportDevtoolsFailure, resolveDevtoolsDsn };
@@ -1,4 +1,4 @@
1
- import { c as reportDevtoolsError, p as isNonInteractive, y as suppressFailureLog } from "./telemetry-Bjoz29Hl.js";
1
+ import { c as reportDevtoolsError, p as isNonInteractive, y as suppressFailureLog } from "./telemetry-Be_lbB5c.js";
2
2
  import { cancel, isCancel, log, note } from "@clack/prompts";
3
3
  //#region ../cli-core/src/ui.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { o as unwrap } from "./ui-CdKo8mLw.js";
1
+ import { o as unwrap } from "./ui-Bb69ZfVv.js";
2
2
  import { createRequire } from "node:module";
3
3
  import { dirname, join } from "node:path";
4
4
  import { confirm, log, spinner } from "@clack/prompts";
package/env.ts CHANGED
@@ -96,7 +96,22 @@ declare({
96
96
  doc:
97
97
  "A Sentry organization auth token. Deploys use it to create each " +
98
98
  "release and upload its source maps, and the smoke test uses it to " +
99
- "read cron check-ins. One token serves every environment.",
99
+ "confirm the release landed. One token serves every environment.",
100
+ scope: "environment",
101
+ secrecy: "secret",
102
+ commented: true,
103
+ }),
104
+ // Read only by `deploy prune-monitors`, in its own step's `env:`: an
105
+ // organization token (above) can neither list nor delete Crons monitors,
106
+ // and one that can delete monitors and alert rules org-wide should not
107
+ // sit in every step of the deploy job. Without it, pruning is skipped
108
+ // with a notice.
109
+ SENTRY_MONITORS_TOKEN: define(z.string().min(1).optional(), {
110
+ doc:
111
+ "A Sentry internal integration token with Alerts: Read & Write and " +
112
+ "nothing else. The production deploy uses it to delete the Crons " +
113
+ "monitors an app no longer declares, which would otherwise alert " +
114
+ "on missed check-ins forever.",
100
115
  scope: "environment",
101
116
  secrecy: "secret",
102
117
  commented: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devdogsuga/backstage",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Officer and production CLI for DevDogsUGA: deploys, env sync with Bitwarden and GitHub, the migration planner role, club graphics and QR codes, GitHub rulesets and settings, and the newsletter. Ships built JS; run it anywhere with `pnpm dlx @devdogsuga/backstage` (no checkout needed to start; commands that read a checkout say so).",
@@ -40,10 +40,10 @@
40
40
  "tsx": "^4.23.12",
41
41
  "typescript": "^6.0.3",
42
42
  "zod": "4.4.3",
43
- "@devdogsuga/brand": "0.1.5",
43
+ "@devdogsuga/events": "0.1.5",
44
44
  "@devdogsuga/newsletter": "0.1.10",
45
- "@devdogsuga/telemetry": "0.1.3",
46
- "@devdogsuga/events": "0.1.5"
45
+ "@devdogsuga/brand": "0.1.5",
46
+ "@devdogsuga/telemetry": "0.1.4"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "@devdogsuga/db": "*",
@@ -66,8 +66,8 @@
66
66
  "vitest": "^4.1.11",
67
67
  "@devdogsuga/config": "0.1.2",
68
68
  "@devdogsuga/db": "0.1.3",
69
- "@devdogsuga/env": "0.1.5",
70
- "@devdogsuga/cli-core": "0.0.0"
69
+ "@devdogsuga/cli-core": "0.0.0",
70
+ "@devdogsuga/env": "0.1.5"
71
71
  },
72
72
  "scripts": {
73
73
  "build": "tsdown && node scripts/write-build-info.mjs && node ../../scripts/check-bundle-imports.mjs",
@@ -1,2 +0,0 @@
1
- import { a as listSecrets, o as projectIdFor } from "./client-B_MY9kVI.js";
2
- export { listSecrets, projectIdFor };