@bongos/core 1.17.3 → 1.18.3
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/.bongos-core.json +212 -332
- package/.devcontainer/healthcheck.sh +0 -0
- package/.devcontainer/init-firewall.sh +0 -0
- package/.devcontainer/post-create.sh +0 -0
- package/GOVERNANCE.md +1 -1
- package/README.md +26 -22
- package/bin/bongos.js +0 -0
- package/config/branding.neutral.json +5 -1
- package/docker-entrypoint.sh +0 -0
- package/docs/adr/0142-project-context-is-prose-only.md +41 -0
- package/docs/adr/0143-onboarding-repo-picker-oauth-scope.md +35 -0
- package/docs/adr/README.md +6 -0
- package/docs/api/openapi.json +1630 -632
- package/docs/api-reference.md +50 -15
- package/docs/file-map.md +0 -18
- package/docs/onboarding/diagrams/05-versioning.mmd +4 -6
- package/docs/onboarding/diagrams/assertions.json +5 -9
- package/docs/recipes/packaging-the-core.md +13 -0
- package/docs/recipes/upgrading-the-core.md +5 -2
- package/migrations/039_newcomer_friendly_column.sql +5 -0
- package/migrations/102_remove_box_first_provision_approval.sql +4 -0
- package/migrations/core_192_github_oauth_tokens.sql +33 -0
- package/modules/hall-ui/public/builders.js +37 -0
- package/modules/hall-ui/public/goals-page.js +98 -176
- package/modules/hall-ui/public/goals.css +183 -0
- package/modules/hall-ui/public/goals.html +35 -261
- package/modules/hall-ui/public/goals.js +7 -2
- package/modules/hall-ui/public/hall-widgets.js +12 -4
- package/modules/hall-ui/public/my-projects.html +78 -0
- package/modules/hall-ui/public/my-projects.js +142 -0
- package/modules/hall-ui/public/scouting.html +45 -0
- package/modules/hall-ui/public/scouting.js +73 -0
- package/modules/hall-ui/public/settings.css +355 -0
- package/modules/hall-ui/public/settings.html +86 -414
- package/modules/hall-ui/public/settings.js +96 -35
- package/modules/hall-ui/public/style.css +31 -0
- package/modules/hall-ui/public/watch.css +184 -0
- package/modules/hall-ui/public/watch.html +111 -285
- package/modules/hall-ui/public/watch.js +59 -21
- package/modules/status-ui/public/explore.html +54 -0
- package/modules/status-ui/public/explore.js +67 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/create.html +292 -19
- package/public-landing/index.html +11 -76
- package/scripts/cron/style_guide_audit.sh +0 -0
- package/scripts/gds/api.js +0 -0
- package/scripts/gds/architect-audit.js +0 -0
- package/scripts/gds/audit-authorship.sh +0 -0
- package/scripts/gds/audit-replay.sh +0 -0
- package/scripts/gds/box.js +0 -0
- package/scripts/gds/capture.js +0 -0
- package/scripts/gds/cli-lib.js +11 -5
- package/scripts/gds/conductor-dispatch.js +0 -0
- package/scripts/gds/coreB-carve-proof.sh +0 -0
- package/scripts/gds/db-backup-nightly.sh +0 -0
- package/scripts/gds/db-backup-offsite.sh +0 -0
- package/scripts/gds/doctor.js +0 -0
- package/scripts/gds/grade-smoke.js +0 -0
- package/scripts/gds/hooks/pre-push.js +0 -0
- package/scripts/gds/install-git-hooks.js +0 -0
- package/scripts/gds/lint-migrations.js +102 -0
- package/scripts/gds/login.js +42 -10
- package/scripts/gds/main-audit.js +0 -0
- package/scripts/gds/mirror-redact.js +5 -0
- package/scripts/gds/oauth-secret.js +0 -0
- package/scripts/gds/onboarding-analyze.js +0 -0
- package/scripts/gds/onboarding-ship.js +0 -0
- package/scripts/gds/package-core.js +263 -4
- package/scripts/gds/paste-token.js +0 -0
- package/scripts/gds/portability-smoke-art.sh +0 -0
- package/scripts/gds/portability-smoke-memory.sh +0 -0
- package/scripts/gds/provision.js +270 -11
- package/scripts/gds/publish-manifest.js +15 -0
- package/scripts/gds/push-main.js +0 -0
- package/scripts/gds/render-ideas.js +0 -0
- package/scripts/gds/run-unit-tests.js +38 -14
- package/scripts/gds/security-baseline-check.sh +0 -0
- package/scripts/gds/session.js +0 -0
- package/scripts/gds/setup.js +41 -0
- package/scripts/gds/ship.js +126 -32
- package/scripts/gds/smoke-batch-claim.sh +0 -0
- package/scripts/gds/smoke-batch-release.sh +0 -0
- package/scripts/gds/smoke-box.sh +0 -0
- package/scripts/gds/smoke-claim-consistency.sh +0 -0
- package/scripts/gds/smoke-dependencies.sh +0 -0
- package/scripts/gds/smoke-write.sh +0 -0
- package/scripts/gds/upgrade.js +147 -5
- package/scripts/hall-preview/server.js +18 -1
- package/scripts/migrate.sh +10 -0
- package/scripts/migrate_cost_ledger.sh +0 -0
- package/scripts/render-diagrams.sh +0 -0
- package/scripts/security/dep-audit.js +0 -0
- package/scripts/setup-builder.js +0 -0
- package/scripts/setup-builder.sh +0 -0
- package/scripts/snapshots/maybe-render-on-deploy.sh +0 -0
- package/src/bongos/auth.js +362 -5
- package/src/bongos/db.js +66 -0
- package/src/bongos/github-repos.js +93 -0
- package/src/bongos/routes/auth.js +93 -11
- package/src/bongos/routes/github.js +90 -0
- package/src/bongos/routes/instance.js +15 -3
- package/src/bongos/routes.js +2 -0
- package/src/branding.js +17 -2
- package/src/instance-config.js +16 -0
- package/src/module-api.js +48 -1
- package/tests/branding.mjs +29 -23
- package/tests/github_repos_helpers.mjs +91 -0
- package/tests/github_token_store.mjs +121 -0
- package/tests/idp_device_exchange.mjs +105 -0
- package/tests/idp_federation.mjs +100 -0
- package/tests/migrations_forward_safe.mjs +86 -0
- package/tests/module_api.mjs +1 -1
- package/tests/package_core.mjs +246 -1
- package/tests/provision.mjs +226 -0
- package/tests/pruned_lockfile_integrity.mjs +79 -0
- package/tests/ship_ci_deploy.mjs +60 -0
- package/tests/upgrade.mjs +254 -5
- package/.gitignore +0 -91
- package/scripts/hall-preview/fixtures/access-requests.json +0 -1
- package/scripts/hall-preview/fixtures/achievements.json +0 -76
- package/scripts/hall-preview/fixtures/analytics__builder.json +0 -251
- package/scripts/hall-preview/fixtures/audit-log.json +0 -1
- package/scripts/hall-preview/fixtures/boxes__cost-ledger.json +0 -1
- package/scripts/hall-preview/fixtures/builders__active-claims.json +0 -1
- package/scripts/hall-preview/fixtures/builders__directory.json +0 -81
- package/scripts/hall-preview/fixtures/builders__roster.json +0 -1
- package/scripts/hall-preview/fixtures/goals.json +0 -502
- package/scripts/hall-preview/fixtures/goals__25.json +0 -96
- package/scripts/hall-preview/fixtures/goals__26.json +0 -67
- package/scripts/hall-preview/fixtures/goals__3.json +0 -103
- package/scripts/hall-preview/fixtures/goals__35.json +0 -49
- package/scripts/hall-preview/fixtures/goals__conflicts.json +0 -154
- package/scripts/hall-preview/fixtures/goals__detail.json +0 -96
- package/scripts/hall-preview/fixtures/goals__inbox.json +0 -5
- package/scripts/hall-preview/fixtures/goals__requests.json +0 -3
- package/scripts/hall-preview/fixtures/grades__by-builder.json +0 -1
- package/scripts/hall-preview/fixtures/inbox.json +0 -330
- package/scripts/hall-preview/fixtures/instance.json +0 -124
- package/scripts/hall-preview/fixtures/leaderboard.json +0 -184
- package/scripts/hall-preview/fixtures/me.json +0 -476
- package/scripts/hall-preview/fixtures/me__recent-ships.json +0 -304
- package/scripts/hall-preview/fixtures/override-requests.json +0 -1
- package/scripts/hall-preview/fixtures/provisioning__fleet.json +0 -1
- package/scripts/hall-preview/fixtures/provisioning__fleet__cost-ledger.json +0 -1
- package/scripts/hall-preview/fixtures/provisioning__instances.json +0 -1
- package/scripts/hall-preview/fixtures/public__analytics__ecosystem.json +0 -516
- package/scripts/hall-preview/fixtures/public__bounty-table.json +0 -37
- package/scripts/hall-preview/fixtures/public__cost-summary.json +0 -742
- package/scripts/hall-preview/fixtures/public__estimation-drift.json +0 -41
- package/scripts/hall-preview/fixtures/public__grades.json +0 -50
- package/scripts/hall-preview/fixtures/public__progress.json +0 -124
- package/scripts/hall-preview/fixtures/public__recent-shipped.json +0 -426
- package/scripts/hall-preview/fixtures/public__repo-health.json +0 -160
- package/scripts/hall-preview/fixtures/public__repo-info.json +0 -5
- package/scripts/hall-preview/fixtures/security__docs.json +0 -1
- package/scripts/hall-preview/fixtures/security__reports.json +0 -1
- package/scripts/hall-preview/fixtures/sessions__search.json +0 -1
- package/scripts/hall-preview/fixtures/tasks.json +0 -2420
- package/scripts/hall-preview/fixtures/tasks__claimable.json +0 -2555
- package/scripts/hall-preview/fixtures/tasks__detail.json +0 -111
- package/scripts/hall-preview/fixtures/tasks__peer-votes__feed.json +0 -330
- package/scripts/hall-preview/fixtures/versions.json +0 -89
package/scripts/gds/ship.js
CHANGED
|
@@ -84,8 +84,22 @@ const GRADER_TIMEOUT_MS = 240_000; // 4 min — Opus on a big diff with Read too
|
|
|
84
84
|
// feature worktree this script lives in. Falls back to two-levels-up if we
|
|
85
85
|
// can't ask git (e.g. fresh clone with no `main` worktree set up yet).
|
|
86
86
|
const REPO_ROOT = findMainWorktree(__dirname) || path.resolve(__dirname, '..', '..');
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
// Sound playback is COSMETIC — the ship pipeline must NEVER depend on it. Guard
|
|
88
|
+
// the require so a missing/broken player (e.g. .claude/sounds/ absent on a fresh
|
|
89
|
+
// clone or after a re-seed) degrades to a silent no-op instead of crashing ship.js
|
|
90
|
+
// at load. A cosmetic feature can't be allowed to break shipping (task 1002199).
|
|
91
|
+
const { playSound } = (() => {
|
|
92
|
+
try {
|
|
93
|
+
return require(path.resolve(__dirname, '../../.claude/sounds/play.js'));
|
|
94
|
+
} catch {
|
|
95
|
+
return { playSound: () => {} };
|
|
96
|
+
}
|
|
97
|
+
})();
|
|
98
|
+
// The laptop-mode SSH deploy target is resolved at land time from config/deploy.json
|
|
99
|
+
// ("sshTarget") or <PREFIX>_DEPLOY_SSH_TARGET — see resolveDeployTarget(). There is NO
|
|
100
|
+
// hardcoded default: the core carries no deploy target of its own. The founder
|
|
101
|
+
// instance's old game-box SSH target used to be baked in here, which post-migration
|
|
102
|
+
// meant a laptop-mode ship deployed to the WRONG server (task 1002202).
|
|
89
103
|
const BEHIND_MAIN_LIMIT = 5;
|
|
90
104
|
|
|
91
105
|
// arg() — the =-aware flag-value parser now lives in cli-lib (the canonical
|
|
@@ -752,9 +766,13 @@ function buildMergeArgv(branch, valueSummary) {
|
|
|
752
766
|
// Two ways the confirmed→shipped land can run, chosen by a SINGLE committed
|
|
753
767
|
// switch (config/deploy.json `mode`, env OTB_DEPLOY_MODE overrides):
|
|
754
768
|
//
|
|
755
|
-
// 'laptop' (default) — the operator's machine merges to main locally and
|
|
756
|
-
//
|
|
757
|
-
//
|
|
769
|
+
// 'laptop' (default) — the operator's machine merges to main locally and, IFF a
|
|
770
|
+
// deploy target is configured (config/deploy.json "sshTarget" or
|
|
771
|
+
// <PREFIX>_DEPLOY_SSH_TARGET, resolveDeployTarget()), SSHes `~/deploy.sh` on it.
|
|
772
|
+
// The core carries NO target of its own, so by default this is a merge-to-main
|
|
773
|
+
// with no SSH deploy — a change reaches a live instance via package-core +
|
|
774
|
+
// `bongos upgrade` (pin-bump), NOT an SSH deploy (ADR 0108). When a target IS
|
|
775
|
+
// set it requires that host's SSH key, so only the operator can land.
|
|
758
776
|
//
|
|
759
777
|
// 'ci' — NO builder box holds the prod key. ship pushes the branch, opens a
|
|
760
778
|
// PR, and enables GitHub auto-merge; once required checks pass, GitHub
|
|
@@ -805,12 +823,7 @@ function effectiveDeployMode(mode, { canPushLocally, envForced } = {}) {
|
|
|
805
823
|
}
|
|
806
824
|
|
|
807
825
|
function resolveDeployMode() {
|
|
808
|
-
|
|
809
|
-
try {
|
|
810
|
-
configRaw = fs.readFileSync(path.join(REPO_ROOT, 'config', 'deploy.json'), 'utf8');
|
|
811
|
-
} catch (_) {
|
|
812
|
-
/* no config file → safe default */
|
|
813
|
-
}
|
|
826
|
+
const configRaw = readDeployConfigRaw();
|
|
814
827
|
const mode = parseDeployMode(process.env.OTB_DEPLOY_MODE, configRaw);
|
|
815
828
|
const envForced = ['ci', 'laptop'].includes(
|
|
816
829
|
String(process.env.OTB_DEPLOY_MODE || '').trim().toLowerCase()
|
|
@@ -824,6 +837,63 @@ function resolveDeployMode() {
|
|
|
824
837
|
return mode;
|
|
825
838
|
}
|
|
826
839
|
|
|
840
|
+
// readDeployConfigRaw — raw contents of config/deploy.json (or null if absent /
|
|
841
|
+
// unreadable). One reader shared by resolveDeployMode (the mode switch) and
|
|
842
|
+
// resolveDeployTarget (the SSH target) so they never diverge on where the file is.
|
|
843
|
+
function readDeployConfigRaw() {
|
|
844
|
+
try {
|
|
845
|
+
return fs.readFileSync(path.join(REPO_ROOT, 'config', 'deploy.json'), 'utf8');
|
|
846
|
+
} catch (_) {
|
|
847
|
+
return null; // no config file → callers apply their safe default
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// safeBranding — the instance brand pack, or null if it can't be read. Guarded the
|
|
852
|
+
// same way ship.js guards the sound player (line ~91): a deploy must never crash
|
|
853
|
+
// because the brand file is missing on a fresh / re-seeded checkout.
|
|
854
|
+
function safeBranding() {
|
|
855
|
+
try {
|
|
856
|
+
return require('../../src/branding').branding();
|
|
857
|
+
} catch (_) {
|
|
858
|
+
return null;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// resolveDeployTarget — WHERE a laptop-mode ship SSHes `~/deploy.sh`, and which
|
|
863
|
+
// /healthz it re-probes when deploy.sh's own health check races the restart window.
|
|
864
|
+
// Pure (env + raw config + branding → target) and exported so the precedence and the
|
|
865
|
+
// safe "no target" default are unit-tested without touching fs/env/network.
|
|
866
|
+
//
|
|
867
|
+
// There is deliberately NO hardcoded default. The founder instance's old game-box
|
|
868
|
+
// SSH target was baked in here, so a laptop-mode ship from a migrated
|
|
869
|
+
// checkout SSHed the WRONG server (task 1002202). The core carries no deploy target
|
|
870
|
+
// of its own — under the standalone model a core change reaches a live instance via
|
|
871
|
+
// package-core + `bongos upgrade` (pin-bump), not an SSH deploy (ADR 0108). An
|
|
872
|
+
// instance whose OWN checkout is its live server opts back into laptop-SSH deploy by
|
|
873
|
+
// setting the target:
|
|
874
|
+
// config/deploy.json → { "sshTarget": "user@host", "healthzUrl": "https://…/healthz" }
|
|
875
|
+
// env → <PREFIX>_DEPLOY_SSH_TARGET, <PREFIX>_DEPLOY_HEALTHZ_URL
|
|
876
|
+
// healthzUrl, when unset, falls back to the instance's own publicOrigin + /healthz
|
|
877
|
+
// (never a hardcoded host); null when even that is unknown (re-probe then skipped).
|
|
878
|
+
function resolveDeployTarget({ env = process.env, configRaw = null, branding = null } = {}) {
|
|
879
|
+
const ic = require('../../src/instance-config');
|
|
880
|
+
let cfg = {};
|
|
881
|
+
if (configRaw) {
|
|
882
|
+
try { cfg = JSON.parse(configRaw) || {}; } catch (_) { cfg = {}; }
|
|
883
|
+
}
|
|
884
|
+
const str = (v) => (typeof v === 'string' && v.trim() ? v.trim() : null);
|
|
885
|
+
|
|
886
|
+
const sshTarget = ic.resolveEnv('DEPLOY_SSH_TARGET', { env }) || str(cfg.sshTarget) || null;
|
|
887
|
+
|
|
888
|
+
let healthzUrl = ic.resolveEnv('DEPLOY_HEALTHZ_URL', { env }) || str(cfg.healthzUrl) || null;
|
|
889
|
+
const publicOrigin = branding && branding.domains && branding.domains.publicOrigin;
|
|
890
|
+
if (!healthzUrl && str(publicOrigin)) {
|
|
891
|
+
try { healthzUrl = new URL('/healthz', publicOrigin).toString(); }
|
|
892
|
+
catch (_) { /* malformed origin → leave healthzUrl null */ }
|
|
893
|
+
}
|
|
894
|
+
return { sshTarget, healthzUrl };
|
|
895
|
+
}
|
|
896
|
+
|
|
827
897
|
// gh runners — mirror gitArgvOk/gitOk but for the GitHub CLI. No shell, ever:
|
|
828
898
|
// branch names + DB-sourced value_summary reach `gh` as literal argv elements
|
|
829
899
|
// (same injection discipline as #681). ghArgvOk = boolean side-effect; ghJson =
|
|
@@ -2119,26 +2189,48 @@ async function autoMerge(taskId, valueSummary, { dbOnly = false } = {}) {
|
|
|
2119
2189
|
return false;
|
|
2120
2190
|
}
|
|
2121
2191
|
|
|
2122
|
-
|
|
2123
|
-
//
|
|
2124
|
-
//
|
|
2125
|
-
//
|
|
2126
|
-
//
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
console.log('
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2192
|
+
// Deploy step. The core carries NO deploy target of its own — under the
|
|
2193
|
+
// standalone model a core change reaches a live instance via package-core +
|
|
2194
|
+
// `bongos upgrade` (pin-bump), NOT an SSH deploy (ADR 0108). So unless an
|
|
2195
|
+
// instance whose own checkout IS its live server has configured a target
|
|
2196
|
+
// (config/deploy.json "sshTarget" or <PREFIX>_DEPLOY_SSH_TARGET), the land is
|
|
2197
|
+
// complete at "merged + pushed to main" and the SSH deploy is skipped. (This
|
|
2198
|
+
// used to SSH a hardcoded target — the founder instance's old game box, i.e. the
|
|
2199
|
+
// WRONG server post-migration. task 1002202.)
|
|
2200
|
+
const { sshTarget, healthzUrl } = resolveDeployTarget({
|
|
2201
|
+
configRaw: readDeployConfigRaw(),
|
|
2202
|
+
branding: safeBranding(),
|
|
2203
|
+
});
|
|
2204
|
+
if (!sshTarget) {
|
|
2205
|
+
console.log(' merged + pushed to main. No deploy target configured, so no SSH deploy —');
|
|
2206
|
+
console.log(' expected for the core. To publish to a live instance run the standalone release');
|
|
2207
|
+
console.log(' flow: package-core → bongos upgrade (pin-bump), per ADR 0108. (Not an SSH deploy.)');
|
|
2208
|
+
} else {
|
|
2209
|
+
console.log(` triggering deploy on ${sshTarget}...`);
|
|
2210
|
+
// Quote ~/deploy.sh so the LOCAL shell doesn't tilde-expand it before ssh
|
|
2211
|
+
// transmits the command; with the quotes ssh sends the literal `~/deploy.sh`
|
|
2212
|
+
// and the REMOTE shell expands it. (Matches /merge-mode SKILL.md:81.)
|
|
2213
|
+
if (!shellOk(`ssh -o ConnectTimeout=10 ${sshTarget} "~/deploy.sh"`)) {
|
|
2214
|
+
// task 984 / idea 180: ~/deploy.sh's internal /healthz curl can race the
|
|
2215
|
+
// service-restart window (Node not yet bound) and exit non-zero even though
|
|
2216
|
+
// the deploy succeeded — which used to false-fail the ship and force a
|
|
2217
|
+
// needless /merge-mode. Before concluding failure, re-probe the CONFIGURED
|
|
2218
|
+
// /healthz with backoff (curl's own --retry; cross-platform, no sleep) —
|
|
2219
|
+
// never a hardcoded host. --retry-all-errors retries connection-refused
|
|
2220
|
+
// during the bind window; -f makes a non-200 an error so a 502/503 also
|
|
2221
|
+
// retries. If it comes up within the window the deploy really succeeded.
|
|
2222
|
+
if (healthzUrl) {
|
|
2223
|
+
console.log(' auto-merge: ~/deploy.sh returned non-zero — re-probing /healthz with backoff before concluding...');
|
|
2224
|
+
if (shellOk(`curl -fsS --max-time 10 --retry 5 --retry-delay 3 --retry-all-errors ${JSON.stringify(healthzUrl)} -o /dev/null`)) {
|
|
2225
|
+
console.log(' auto-merge: /healthz 200 on re-probe — deploy actually succeeded (deploy.sh raced the restart window). Proceeding.');
|
|
2226
|
+
} else {
|
|
2227
|
+
console.log(' auto-merge: deploy failed (/healthz never returned 200 after retries); task stays at confirmed');
|
|
2228
|
+
return false;
|
|
2229
|
+
}
|
|
2230
|
+
} else {
|
|
2231
|
+
console.log(' auto-merge: ~/deploy.sh returned non-zero and no healthzUrl configured to re-probe; task stays at confirmed');
|
|
2232
|
+
return false;
|
|
2233
|
+
}
|
|
2142
2234
|
}
|
|
2143
2235
|
}
|
|
2144
2236
|
|
|
@@ -3538,7 +3630,9 @@ module.exports = {
|
|
|
3538
3630
|
gitArgvOk, buildMergeArgv, sanitizeGitHubRefs,
|
|
3539
3631
|
// #679 / ADR 0042: CI deploy mode — pure helpers exported for the regression
|
|
3540
3632
|
// test (mode precedence/safe-default, no-shell PR argv, deploy-run match).
|
|
3541
|
-
|
|
3633
|
+
// task 1002202: resolveDeployTarget — SSH target / healthz precedence + the safe
|
|
3634
|
+
// "no target" default (the core has none), exported for the same reason.
|
|
3635
|
+
parseDeployMode, resolveDeployTarget, buildPrCreateArgv, buildPrMergeArgv, deployRunSucceeded,
|
|
3542
3636
|
// task 1049 (B): pure CI-land outcome classifier — a timed-out-but-armed PR is
|
|
3543
3637
|
// 'pending' (success), not failure. Exported so the semantics are unit-tested.
|
|
3544
3638
|
classifyLandOutcome,
|
|
File without changes
|
|
File without changes
|
package/scripts/gds/smoke-box.sh
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/scripts/gds/upgrade.js
CHANGED
|
@@ -295,6 +295,91 @@ async function pollHealth(healthUrl, deps = {}) {
|
|
|
295
295
|
}
|
|
296
296
|
}
|
|
297
297
|
|
|
298
|
+
// ---- integrity-pin verification (task 1002216 / audit H8, finding F1) --------
|
|
299
|
+
// The packager (package-core.js) computes tree_sha256 = canonicalTreeHash over the
|
|
300
|
+
// redacted core files (+ the two synthesized files) and records it, the per-file
|
|
301
|
+
// hashes, AND the exact tarball bytes' sha256 in a side-car manifest — stating a
|
|
302
|
+
// consumer "verifies an install by recomputing it from the unpacked tree." Until now
|
|
303
|
+
// nothing on the CONSUME side did: `bongos upgrade` compared only the version STRING,
|
|
304
|
+
// so a corrupted / truncated / swapped tarball whose package.json still read the target
|
|
305
|
+
// version installed silently — the R88-class failure this goal exists to prevent.
|
|
306
|
+
// The three functions below close that gap: recompute the pin over the INSTALLED tree
|
|
307
|
+
// and assert it equals the TRUSTED reference manifest.
|
|
308
|
+
|
|
309
|
+
// Read + parse a manifest JSON; null on any failure (missing / unparseable).
|
|
310
|
+
function readManifest(file, fsImpl = fs) {
|
|
311
|
+
try { return JSON.parse(fsImpl.readFileSync(file, 'utf8')); } catch { return null; }
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Resolve the trusted reference pin for this bump:
|
|
315
|
+
// • VENDORED — the committed side-car `vendor/<artifact>-<v>.manifest.json` (carries
|
|
316
|
+
// tree_sha256 + files[] + the exact tarball.sha256). It lives OUTSIDE the tarball
|
|
317
|
+
// (version-controlled in the consumer repo), so it detects a swapped/corrupted
|
|
318
|
+
// tarball. This is the strong reference.
|
|
319
|
+
// • REGISTRY / no side-car — the inner manifest `<core>/.bongos-core.json` that ships
|
|
320
|
+
// INSIDE the package (tree_sha256 + files[], no tarball.*). Self-referential (a full
|
|
321
|
+
// swap regenerates it too), so it is a self-consistency check backing npm's own
|
|
322
|
+
// registry integrity rather than an independent one — flagged `strong:false`.
|
|
323
|
+
function resolveReferenceManifest({ instanceDir, coreDir, targetVersion }, fsImpl = fs) {
|
|
324
|
+
const sidecarPath = path.join(instanceDir, 'vendor', `${ARTIFACT}-${targetVersion}.manifest.json`);
|
|
325
|
+
const sidecar = readManifest(sidecarPath, fsImpl);
|
|
326
|
+
if (sidecar && sidecar.tree_sha256) {
|
|
327
|
+
const tgz = path.join(instanceDir, 'vendor', `${ARTIFACT}-${targetVersion}.tgz`);
|
|
328
|
+
return { manifest: sidecar, tgzPath: fsImpl.existsSync(tgz) ? tgz : null, source: 'vendored side-car manifest', strong: true };
|
|
329
|
+
}
|
|
330
|
+
const inner = readManifest(path.join(coreDir, '.bongos-core.json'), fsImpl);
|
|
331
|
+
if (inner && inner.tree_sha256) return { manifest: inner, tgzPath: null, source: 'installed inner manifest', strong: false };
|
|
332
|
+
return { manifest: null, tgzPath: null, source: null, strong: false };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Recompute the integrity pin over the installed core tree and compare to `referenceManifest`.
|
|
336
|
+
// Reuses the PACKAGER's exact hash functions (canonicalTreeHash / sha256hex) so the consume-side
|
|
337
|
+
// recompute can never drift from the produce-side pin. Returns { ran, ok, checks, problems };
|
|
338
|
+
// ran=false ⇒ the manifest carried no pin to check against.
|
|
339
|
+
function verifyInstalledPin({ coreDir, referenceManifest, tgzPath }, deps = {}) {
|
|
340
|
+
const fsImpl = deps.fs || fs;
|
|
341
|
+
const pc = deps.packageCore || require('./package-core');
|
|
342
|
+
const checks = [];
|
|
343
|
+
const problems = [];
|
|
344
|
+
if (!referenceManifest || !referenceManifest.tree_sha256) return { ran: false, ok: false, checks, problems };
|
|
345
|
+
|
|
346
|
+
// (a) exact tarball bytes (vendored path only) — the strongest single check: the bytes
|
|
347
|
+
// npm actually installs, hashed and compared to the externally-held pin.
|
|
348
|
+
const tb = referenceManifest.tarball;
|
|
349
|
+
if (tgzPath && tb && tb.sha256 && fsImpl.existsSync(tgzPath)) {
|
|
350
|
+
const got = pc.sha256hex(fsImpl.readFileSync(tgzPath));
|
|
351
|
+
const ok = got === tb.sha256;
|
|
352
|
+
checks.push({ name: 'tarball.sha256', ok });
|
|
353
|
+
if (!ok) problems.push(`vendored tarball bytes sha256 ${got.slice(0, 12)}… ≠ pinned ${String(tb.sha256).slice(0, 12)}…`);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// (b) installed tree — recompute each pinned file's sha256 from the unpacked tree, rebuild
|
|
357
|
+
// the canonical tree hash (using the manifest's already-canonical modes), assert equality.
|
|
358
|
+
// Catches partial extraction, post-install corruption, or a registry package that diverged.
|
|
359
|
+
const files = Array.isArray(referenceManifest.files) ? referenceManifest.files : [];
|
|
360
|
+
if (files.length) {
|
|
361
|
+
const recomputed = [];
|
|
362
|
+
let missing = 0, changed = 0;
|
|
363
|
+
for (const f of files) {
|
|
364
|
+
const abs = path.join(coreDir, ...String(f.path).split('/')); // POSIX manifest path → native
|
|
365
|
+
let buf;
|
|
366
|
+
try { buf = fsImpl.readFileSync(abs); } catch { missing++; problems.push(`missing installed file: ${f.path}`); continue; }
|
|
367
|
+
const sha = pc.sha256hex(buf);
|
|
368
|
+
if (sha !== f.sha256) { changed++; problems.push(`content mismatch: ${f.path}`); }
|
|
369
|
+
recomputed.push({ path: f.path, mode: f.mode, sha256: sha });
|
|
370
|
+
}
|
|
371
|
+
const treeHash = pc.canonicalTreeHash(recomputed);
|
|
372
|
+
const ok = missing === 0 && changed === 0 && treeHash === referenceManifest.tree_sha256;
|
|
373
|
+
checks.push({ name: 'tree_sha256', ok, missing, changed, file_count: files.length });
|
|
374
|
+
if (!ok && missing === 0 && changed === 0) {
|
|
375
|
+
problems.push(`recomputed tree_sha256 ${treeHash.slice(0, 12)}… ≠ pinned ${String(referenceManifest.tree_sha256).slice(0, 12)}…`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const ran = checks.length > 0;
|
|
380
|
+
return { ran, ok: ran && problems.length === 0, checks, problems };
|
|
381
|
+
}
|
|
382
|
+
|
|
298
383
|
// Auto-rollback (task 2149): a bump failed AFTER the pin was moved — an install or migrate
|
|
299
384
|
// error, or a failed post-restart health check. Revert the code pin to the pre-upgrade
|
|
300
385
|
// snapshot, reinstall the previous core, restart, re-verify health, and record the reversal
|
|
@@ -390,6 +475,18 @@ async function runUpgrade(opts, deps = {}) {
|
|
|
390
475
|
log(` ✓ pre-flight: ${mods.note}`);
|
|
391
476
|
if (!mods.ok) return { ok: false, error: `module pre-check failed: ${mods.blocking.map((b) => b.key).join(', ')} incompatible with core ${targetVersion}` };
|
|
392
477
|
|
|
478
|
+
// 1b. mandatory post-restart health check (audit H9 / task 1002222, finding F5). A bump that
|
|
479
|
+
// brings the new core live via a managed restart (--service / --restart-cmd) MUST confirm it
|
|
480
|
+
// actually serves — an upgrade that can't be health-checked can't be reported as a success.
|
|
481
|
+
// Enforced here in PRE-FLIGHT, before anything is mutated, so there is nothing to roll back:
|
|
482
|
+
// if this bump will restart the service and the check isn't explicitly opted out, a
|
|
483
|
+
// --health-url is required. (No managed restart ⇒ nothing is brought live here ⇒ the check
|
|
484
|
+
// does not apply; the operator restarts + verifies separately.)
|
|
485
|
+
const willRestart = !!(opts.service || opts.restartCmd);
|
|
486
|
+
if (!dryRun && opts.healthCheck !== false && willRestart && !opts.healthUrl) {
|
|
487
|
+
return { ok: false, error: 'post-restart health check is mandatory for a bump that restarts the service, but no --health-url was given.\n Pass --health-url <url> to confirm the new core serves after restart, or --no-health-check to explicitly skip the confirmation.' };
|
|
488
|
+
}
|
|
489
|
+
|
|
393
490
|
// 2. bump the pin. Two mutually exclusive core sources (ADR 0134, task 2090):
|
|
394
491
|
// REGISTRY (--registry): pin @bongos/core@<version> + ensure the env-fed .npmrc, so
|
|
395
492
|
// the `npm install` below pulls the PRIVATE package from the registry (needs NPM_TOKEN).
|
|
@@ -429,6 +526,38 @@ async function runUpgrade(opts, deps = {}) {
|
|
|
429
526
|
}
|
|
430
527
|
log(' ✓ npm install');
|
|
431
528
|
|
|
529
|
+
// 3b. verify the integrity pin (audit H8 / task 1002216, finding F1). Recompute tree_sha256 over
|
|
530
|
+
// the freshly-installed core (+ the exact vendored tarball bytes) and assert it matches the
|
|
531
|
+
// TRUSTED reference manifest — the pin the packager produced but the consume side never
|
|
532
|
+
// re-checked. Runs BEFORE migrate so a corrupted core can never touch the DB. A MISMATCH is
|
|
533
|
+
// fatal → rollback (like an install/migrate failure). A MISSING reference (no side-car AND no
|
|
534
|
+
// inner manifest) can't be verified: a warning by default, fatal only with --require-pin.
|
|
535
|
+
if (!opts.skipPinVerify) {
|
|
536
|
+
const coreDir = path.join(instanceDir, 'node_modules', CORE_PKG);
|
|
537
|
+
const ref = (deps.resolveReferenceManifest || resolveReferenceManifest)({ instanceDir, coreDir, targetVersion }, fsImpl);
|
|
538
|
+
if (!ref.manifest) {
|
|
539
|
+
const msg = `integrity pin: no reference manifest (looked for vendor/${ARTIFACT}-${targetVersion}.manifest.json and ${CORE_PKG}/.bongos-core.json) — cannot verify this install`;
|
|
540
|
+
if (opts.requirePin) {
|
|
541
|
+
const error = `${msg} (--require-pin)`;
|
|
542
|
+
if (rollbackOnFailure && snapshot) { const rb = await doRollback('integrity pin unverifiable (--require-pin)'); return { ok: false, error, fromVersion, toVersion: targetVersion, rolledBack: true, rollback: rb }; }
|
|
543
|
+
return { ok: false, error, fromVersion };
|
|
544
|
+
}
|
|
545
|
+
err(` ! ${msg} — pass --require-pin to treat this as fatal.`);
|
|
546
|
+
} else {
|
|
547
|
+
const pin = (deps.verifyInstalledPin || verifyInstalledPin)({ coreDir, referenceManifest: ref.manifest, tgzPath: ref.tgzPath }, deps);
|
|
548
|
+
if (!pin.ok) {
|
|
549
|
+
const detail = pin.problems.slice(0, 6).join('; ') + (pin.problems.length > 6 ? `; +${pin.problems.length - 6} more` : '');
|
|
550
|
+
const error = `integrity pin MISMATCH (${ref.source}) — installed core does not match its pin: ${detail}`;
|
|
551
|
+
if (rollbackOnFailure && snapshot) {
|
|
552
|
+
const rb = await doRollback('integrity pin mismatch');
|
|
553
|
+
return { ok: false, error, fromVersion, toVersion: targetVersion, rolledBack: true, rollback: rb };
|
|
554
|
+
}
|
|
555
|
+
return { ok: false, error, fromVersion };
|
|
556
|
+
}
|
|
557
|
+
log(` ✓ integrity pin verified (${pin.checks.map((c) => c.name).join(' + ')}) against the ${ref.source}${ref.strong ? '' : ' — self-consistency check; npm registry integrity is the primary guard'}`);
|
|
558
|
+
}
|
|
559
|
+
} else log(' • integrity-pin verify skipped (--skip-pin-verify)');
|
|
560
|
+
|
|
432
561
|
// 4. migrate (additive core_* migrations).
|
|
433
562
|
if (!opts.skipMigrate) {
|
|
434
563
|
const mig = runMigrate(instanceDir, run);
|
|
@@ -472,11 +601,17 @@ async function runUpgrade(opts, deps = {}) {
|
|
|
472
601
|
log(` ${versionOk ? '✓' : '!'} installed core version: ${installed || '(unknown)'}${versionOk ? '' : ` (expected ${targetVersion})`}`);
|
|
473
602
|
|
|
474
603
|
let healthOk = null;
|
|
475
|
-
if (opts.
|
|
604
|
+
if (opts.healthCheck === false) {
|
|
605
|
+
log(' • post-restart health check skipped (--no-health-check) — success is NOT confirmed against a live endpoint.');
|
|
606
|
+
} else if (opts.healthUrl) {
|
|
476
607
|
const h = await pollHealth(opts.healthUrl, deps);
|
|
477
608
|
healthOk = h.ok;
|
|
478
609
|
if (h.ok) log(` ✓ health ${opts.healthUrl} → ${h.status}${h.attempts > 1 ? ` (ready after ${h.attempts} tries)` : ''}`);
|
|
479
610
|
else err(` ! health ${opts.healthUrl} not ready after ${h.attempts} tr${h.attempts === 1 ? 'y' : 'ies'}: ${h.error || 'status ' + h.status}`);
|
|
611
|
+
} else {
|
|
612
|
+
// Reachable only when NO managed restart happened (pre-flight requires a URL when one will):
|
|
613
|
+
// the new core is installed on disk, but nothing was brought live here to health-check.
|
|
614
|
+
log(' • no managed restart + no --health-url — new core installed but not confirmed serving; restart the instance + health-check it to complete the bump.');
|
|
480
615
|
}
|
|
481
616
|
|
|
482
617
|
// A failed post-restart health check means the new core is serving broken — auto-revert (task
|
|
@@ -514,6 +649,9 @@ function parseArgs(argv) {
|
|
|
514
649
|
skipMigrate: hasFlag('--skip-migrate', argv),
|
|
515
650
|
skipMaterialize: hasFlag('--skip-materialize', argv),
|
|
516
651
|
skipRegenDocs: hasFlag('--skip-regen-docs', argv),
|
|
652
|
+
skipPinVerify: hasFlag('--skip-pin-verify', argv), // task 1002216: opt out of the post-install integrity-pin check
|
|
653
|
+
requirePin: hasFlag('--require-pin', argv), // task 1002216: a MISSING reference manifest becomes fatal (not just a warning)
|
|
654
|
+
healthCheck: !hasFlag('--no-health-check', argv), // task 1002222: post-restart health check is ON by default (finding F5/H9)
|
|
517
655
|
rollbackOnFailure: !hasFlag('--no-rollback-on-failure', argv), // ADR 0100: auto-revert ON by default (task 2149)
|
|
518
656
|
};
|
|
519
657
|
}
|
|
@@ -533,16 +671,19 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
533
671
|
' --instance <dir> consumer instance root (default: cwd)',
|
|
534
672
|
' --service <unit> systemd unit to restart after applying',
|
|
535
673
|
' --restart-cmd <cmd> explicit restart command (overrides --service)',
|
|
536
|
-
' --health-url <url> poll after restart (~30s) until 2xx; still-failing
|
|
674
|
+
' --health-url <url> poll after restart (~30s) until 2xx; a still-failing check fails the bump (auto-rollback). REQUIRED for a bump that restarts the service, unless --no-health-check.',
|
|
675
|
+
' --no-health-check explicitly skip the post-restart health confirmation (default: the check is ON — a bump that restarts the service must confirm it serves)',
|
|
537
676
|
' --skip-migrate do not run `npm run migrate`',
|
|
538
677
|
' --skip-materialize do not refresh .claude/',
|
|
539
678
|
' --skip-regen-docs do not regenerate the OpenAPI spec + typed client from the new core',
|
|
540
|
-
' --
|
|
679
|
+
' --skip-pin-verify do not verify the integrity pin (tree_sha256 + tarball.sha256) after install',
|
|
680
|
+
' --require-pin treat a MISSING reference manifest as fatal (default: warn — a MISMATCH is always fatal)',
|
|
681
|
+
' --no-rollback-on-failure do NOT auto-revert on a failed install/pin-verify/migrate/health check (default: auto-rollback ON)',
|
|
541
682
|
' --dry-run show the plan; change nothing',
|
|
542
683
|
' --force skip the clean-tree + same-version guards',
|
|
543
684
|
'',
|
|
544
|
-
'Full bump: pre-flight → bump pin → npm install → migrate → materialize .claude → regen API docs+client → restart → verify.',
|
|
545
|
-
'On a failure after the pin moves (install/migrate/health), auto-rollback reverts the pin, reinstalls the previous core, restarts, and records a rolled_back ledger row (the DB is not rolled back — core migrations are additive).',
|
|
685
|
+
'Full bump: pre-flight → bump pin → npm install → verify integrity pin → migrate → materialize .claude → regen API docs+client → restart → verify.',
|
|
686
|
+
'On a failure after the pin moves (install/pin-verify/migrate/health), auto-rollback reverts the pin, reinstalls the previous core, restarts, and records a rolled_back ledger row (the DB is not rolled back — core migrations are additive).',
|
|
546
687
|
].join('\n') + '\n');
|
|
547
688
|
return 0;
|
|
548
689
|
}
|
|
@@ -560,6 +701,7 @@ module.exports = {
|
|
|
560
701
|
runUpgrade, parseArgs, main, rollback,
|
|
561
702
|
versionFromTgzPath, versionFromDep, readInstalledCoreVersion, readPinnedCoreVersion, writePin, writeRegistryPin, restorePin,
|
|
562
703
|
vendorTarball, gitTreeClean, preflightModules, npmInstall, runMigrate, regenerateApiArtifacts, restartService, recordLedger, pollHealth,
|
|
704
|
+
readManifest, resolveReferenceManifest, verifyInstalledPin, // task 1002216 (audit H8) — integrity-pin verification
|
|
563
705
|
CORE_PKG, ARTIFACT,
|
|
564
706
|
};
|
|
565
707
|
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
// window.__MODULES__ (verifies moduleOn() gating)
|
|
20
20
|
// --empty serve every fixture with its arrays emptied — the
|
|
21
21
|
// empty-state screenshot pass
|
|
22
|
+
// --rank archon override the /me fixture's builder.rank so the Archon-gated
|
|
23
|
+
// pages (watch, harbor, gate) render their content in preview
|
|
22
24
|
//
|
|
23
25
|
// Browse http://builders.localhost:<port>/ — Chromium resolves *.localhost,
|
|
24
26
|
// and the `builders.` host prefix exercises the real subdomain serving path.
|
|
@@ -39,6 +41,11 @@ function arg(name) {
|
|
|
39
41
|
}
|
|
40
42
|
const PORT = Number(arg('--port')) || 4600;
|
|
41
43
|
const EMPTY = process.argv.includes('--empty');
|
|
44
|
+
// --rank: view the hall as a different rank — the /me fixture's builder.rank is
|
|
45
|
+
// overridden in the response, so the Archon-gated pages (watch, harbor, gate)
|
|
46
|
+
// render their real content instead of the sealed notice. Preview-only theatre:
|
|
47
|
+
// the real pages are server-gated (requireArchonPage / ADR 0016).
|
|
48
|
+
const RANK_OVERRIDE = arg('--rank');
|
|
42
49
|
const MODULES_OFF = String(arg('--modules-off') || '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
43
50
|
// --modules-on: force-report modules as enabled — a dev box's local config
|
|
44
51
|
// often lacks prod's instance modules (provisioning, dev-box), and their
|
|
@@ -68,6 +75,10 @@ const FIXTURE_FALLBACKS = [
|
|
|
68
75
|
[/^goals\/\d+\/requests$/, 'goals__requests'],
|
|
69
76
|
[/^goals\/\d+$/, 'goals__detail'],
|
|
70
77
|
[/^tasks\/\d+$/, 'tasks__detail'],
|
|
78
|
+
// the #/idea/N + #/blocker/N deep-link overlay (task-detail.js) — one record
|
|
79
|
+
// fixture serves any id so the overlay seam assertion renders real content.
|
|
80
|
+
[/^inbox\/\d+$/, 'inbox__detail'],
|
|
81
|
+
[/^blockers\/\d+$/, 'blockers__detail'],
|
|
71
82
|
[/^analytics\/builder\/\d+$/, 'analytics__builder'],
|
|
72
83
|
[/^builders\/\d+\/active-claims$/, 'builders__active-claims'],
|
|
73
84
|
];
|
|
@@ -134,7 +145,13 @@ app.use((req, res, next) => {
|
|
|
134
145
|
console.log(` no fixture: ${apiPath} (add scripts/hall-preview/fixtures/${apiPath.replace(/\//g, '__')}.json)`);
|
|
135
146
|
return res.status(404).json({ error: { code: 'no_fixture', message: apiPath } });
|
|
136
147
|
}
|
|
137
|
-
|
|
148
|
+
let payload = EMPTY ? emptied(fix) : fix;
|
|
149
|
+
if (RANK_OVERRIDE && apiPath === 'me') {
|
|
150
|
+
payload = JSON.parse(JSON.stringify(payload));
|
|
151
|
+
if (payload.builder) payload.builder.rank = RANK_OVERRIDE;
|
|
152
|
+
else if ('rank' in payload) payload.rank = RANK_OVERRIDE;
|
|
153
|
+
}
|
|
154
|
+
res.json(payload);
|
|
138
155
|
});
|
|
139
156
|
|
|
140
157
|
// The hall pages, pre-auth-gate, through the real seams: boot-time stamped
|
package/scripts/migrate.sh
CHANGED
|
@@ -110,6 +110,16 @@ apply_migration() {
|
|
|
110
110
|
>/dev/null
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
# --- Forward-safety notice (task 1002223 / audit H10) ---------------------
|
|
114
|
+
# `bongos upgrade` auto-rollback reverts the code pin but NOT the DB — safe only if core migrations
|
|
115
|
+
# are forward-safe (the PREVIOUS code still runs on the new schema). Surface — never block — any
|
|
116
|
+
# UNACKNOWLEDGED forward-unsafe core migration so an operator knows a rollback past it would strand
|
|
117
|
+
# old code on new schema. Fail-closed enforcement is the unit gate (tests/migrations_forward_safe.mjs);
|
|
118
|
+
# here it is advisory (`|| true`) so a lint hiccup can NEVER break a deploy.
|
|
119
|
+
if [ -f "$CORE_ROOT/scripts/gds/lint-migrations.js" ]; then
|
|
120
|
+
node "$CORE_ROOT/scripts/gds/lint-migrations.js" --warn --dir "$CORE_ROOT/migrations" || true
|
|
121
|
+
fi
|
|
122
|
+
|
|
113
123
|
# Build the ordered migration list and apply it as ONE numeric sequence by
|
|
114
124
|
# filename stem, regardless of directory:
|
|
115
125
|
# $CORE_ROOT/migrations/*.sql — core: schema + platform-universal seeds (EVERY instance).
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/scripts/setup-builder.js
CHANGED
|
File without changes
|
package/scripts/setup-builder.sh
CHANGED
|
File without changes
|
|
File without changes
|