@shortlink-org/portolan 0.1.0 → 0.2.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.
- package/README.md +69 -205
- package/cli/init.mjs +285 -0
- package/cli/init.test.mjs +158 -0
- package/cli/portolan.mjs +37 -73
- package/index.html +19 -0
- package/package.json +5 -10
- package/plugins/README.md +66 -27
- package/plugins/portolan-go.wasm +0 -0
- package/portolan.json +52 -73
- package/public/landing/cats/bug-hunter.webp +0 -0
- package/public/landing/cats/compass-nap.webp +0 -0
- package/public/landing/cats/diagrammer.webp +0 -0
- package/public/landing/cats/docs-reader.webp +0 -0
- package/public/landing/cats/laptop.webp +0 -0
- package/public/landing/cats/map-inspector.webp +0 -0
- package/public/landing/cats/server-break.webp +0 -0
- package/public/landing/cats/star-mapper.webp +0 -0
- package/public/landing/cats/system-builder.webp +0 -0
- package/public/landing/cats/thread-tangle.webp +0 -0
- package/public/og.png +0 -0
- package/public/readme-header.webp +0 -0
- package/schema/portolan.schema.json +180 -0
- package/scripts/builtin-plugins.mjs +14 -0
- package/scripts/delivery-presets.mjs +353 -0
- package/scripts/gen.mjs +37 -4
- package/scripts/history.mjs +126 -0
- package/scripts/history.test.mjs +110 -0
- package/scripts/host-plugins/fetch-bsr.mjs +351 -0
- package/{plugins/fetch-bsr/options.schema.json → scripts/host-plugins/fetch-bsr.options.json} +4 -0
- package/scripts/host-plugins/fetch-bsr.test.mjs +212 -0
- package/scripts/host-plugins/fetch-csr.mjs +386 -0
- package/scripts/host-plugins/fetch-csr.test.mjs +178 -0
- package/scripts/host-plugins/fetch-git.mjs +379 -0
- package/scripts/host-plugins/fetch-git.test.mjs +210 -0
- package/scripts/local-api.mjs +22 -4
- package/scripts/local-api.test.mjs +84 -2
- package/scripts/manifest.mjs +3 -2
- package/scripts/package-smoke.mjs +6 -2
- package/scripts/plugin-host.mjs +77 -18
- package/scripts/plugin-host.test.mjs +50 -1
- package/scripts/plugin-wasm-worker.mjs +6 -3
- package/scripts/run-builtin.mjs +6 -26
- package/scripts/schema.mjs +6 -1
- package/src/app/App.tsx +24 -429
- package/src/app/Breadcrumbs.test.ts +7 -0
- package/src/app/Breadcrumbs.tsx +15 -2
- package/src/app/CatalogApp.tsx +436 -0
- package/src/app/Sidebar.tsx +0 -1
- package/src/chat/ChatPanel.tsx +98 -41
- package/src/chat/Composer.tsx +4 -2
- package/src/chat/Conversation.tsx +79 -15
- package/src/chat/Header.tsx +43 -9
- package/src/chat/Starter.tsx +64 -3
- package/src/chat/Steps.tsx +1 -1
- package/src/chat/flags.test.ts +51 -14
- package/src/chat/flags.ts +8 -2
- package/src/chat/page-context.test.ts +51 -0
- package/src/chat/page-context.ts +128 -0
- package/src/chat/prompt.test.ts +49 -1
- package/src/chat/prompt.ts +42 -1
- package/src/chat/tools.ts +7 -2
- package/src/chat/transport.ts +16 -4
- package/src/components/ProblemRow.tsx +182 -0
- package/src/data.ts +20 -5
- package/src/er/ErCanvas.tsx +1 -1
- package/src/graph/DependencyGraph.tsx +10 -1
- package/src/graph/FocusedEventGraph.tsx +1 -1
- package/src/graph/GraphToolbar.tsx +2 -4
- package/src/index.css +220 -2
- package/src/landing/DraggableReveal.tsx +168 -0
- package/src/landing/EstateGraph.tsx +28 -0
- package/src/landing/FlowPlayback.tsx +282 -0
- package/src/landing/HeroMap.tsx +128 -0
- package/src/landing/LandingPage.tsx +757 -0
- package/src/landing/ProductFrame.tsx +68 -0
- package/src/landing/ProductTour.tsx +371 -0
- package/src/landing/catalog.ts +25 -0
- package/src/landing/motion.tsx +52 -0
- package/src/lib/all-problems.ts +28 -0
- package/src/lib/local-api.ts +35 -1
- package/src/lib/setup-info.test.ts +8 -0
- package/src/lib/setup-info.ts +3 -2
- package/src/lib/source-code.ts +2 -1
- package/src/likec4/C4View.tsx +5 -0
- package/src/likec4/InteractiveView.tsx +5 -1
- package/src/map/ContextMapGraph.tsx +47 -20
- package/src/pages/ContextMap.tsx +5 -1
- package/src/pages/FlowDetail.tsx +24 -7
- package/src/pages/Overview.tsx +68 -12
- package/src/pages/Problems.tsx +6 -193
- package/src/pages/Settings.tsx +133 -108
- package/src/pages/settings/DeliverySettings.tsx +229 -0
- package/src/pages/settings/PreferencesSettings.tsx +101 -0
- package/src/routes.test.ts +20 -0
- package/src/routes.ts +13 -1
- package/vite.config.ts +12 -8
- package/catalog/enum_test.go +0 -46
- package/catalog/model.go +0 -932
- package/catalog/roundtrip_test.go +0 -185
- package/catalog/via_test.go +0 -38
- package/go.mod +0 -14
- package/go.sum +0 -14
- package/internal/commands/cargo.go +0 -154
- package/internal/commands/commands.go +0 -143
- package/internal/commands/commands_test.go +0 -323
- package/internal/commands/gradle.go +0 -77
- package/internal/commands/justfile.go +0 -82
- package/internal/commands/makefile.go +0 -106
- package/internal/commands/maven.go +0 -93
- package/internal/commands/packagejson.go +0 -159
- package/internal/commands/pyproject.go +0 -169
- package/internal/commands/taskfile.go +0 -101
- package/internal/commands/testdata/estate/.cargo/config.toml +0 -7
- package/internal/commands/testdata/estate/Makefile +0 -33
- package/internal/commands/testdata/estate/Taskfile.yml +0 -21
- package/internal/commands/testdata/estate/build.gradle.kts +0 -21
- package/internal/commands/testdata/estate/justfile +0 -20
- package/internal/commands/testdata/estate/package.json +0 -13
- package/internal/commands/testdata/estate/pom.xml +0 -29
- package/internal/commands/testdata/estate/pyproject.toml +0 -27
- package/internal/commands/testdata/estate/xtask/src/main.rs +0 -27
- package/internal/commands/testdata/golden/commands.json +0 -305
- package/internal/gohttp/analyze.go +0 -2500
- package/internal/gohttp/endpoints.go +0 -1067
- package/internal/gohttp/roots.go +0 -320
- package/internal/gohttp/typed.go +0 -143
- package/internal/goscan/constants.go +0 -85
- package/internal/goscan/goscan_test.go +0 -227
- package/internal/goscan/names.go +0 -52
- package/internal/goscan/parse_test.go +0 -11
- package/internal/goscan/source.go +0 -36
- package/internal/goscan/tree.go +0 -155
- package/internal/goscan/types.go +0 -99
- package/internal/wsdl/ids.go +0 -127
- package/internal/wsdl/ids_test.go +0 -21
- package/internal/wsdl/model.go +0 -62
- package/internal/wsdl/parse.go +0 -920
- package/internal/wsdl/parse_test.go +0 -133
- package/plugin/describe.go +0 -107
- package/plugin/describe_test.go +0 -114
- package/plugin/protocol.go +0 -117
- package/plugin/schematest/schematest.go +0 -126
- package/plugins/extract-adr/describe.go +0 -19
- package/plugins/extract-adr/describe_test.go +0 -11
- package/plugins/extract-adr/extract.go +0 -153
- package/plugins/extract-adr/extract_test.go +0 -350
- package/plugins/extract-adr/history.go +0 -99
- package/plugins/extract-adr/main.go +0 -65
- package/plugins/extract-adr/parse.go +0 -642
- package/plugins/extract-adr/parse_test.go +0 -405
- package/plugins/extract-asyncapi/describe.go +0 -19
- package/plugins/extract-asyncapi/describe_test.go +0 -11
- package/plugins/extract-asyncapi/extract.go +0 -315
- package/plugins/extract-asyncapi/extract_test.go +0 -197
- package/plugins/extract-asyncapi/main.go +0 -48
- package/plugins/extract-asyncapi/spec.go +0 -151
- package/plugins/extract-commands/describe.go +0 -19
- package/plugins/extract-commands/describe_test.go +0 -11
- package/plugins/extract-commands/extract.go +0 -79
- package/plugins/extract-commands/extract_test.go +0 -92
- package/plugins/extract-commands/main.go +0 -55
- package/plugins/extract-csr/avro.go +0 -258
- package/plugins/extract-csr/describe.go +0 -19
- package/plugins/extract-csr/describe_test.go +0 -11
- package/plugins/extract-csr/extract.go +0 -338
- package/plugins/extract-csr/extract_test.go +0 -374
- package/plugins/extract-csr/jsonschema.go +0 -343
- package/plugins/extract-csr/lock.go +0 -25
- package/plugins/extract-csr/main.go +0 -84
- package/plugins/extract-csr/subject.go +0 -102
- package/plugins/extract-flows/describe.go +0 -19
- package/plugins/extract-flows/describe_test.go +0 -11
- package/plugins/extract-flows/extract.go +0 -91
- package/plugins/extract-flows/main.go +0 -45
- package/plugins/extract-flows/parse.go +0 -593
- package/plugins/extract-flows/parse_test.go +0 -204
- package/plugins/extract-glossary/describe.go +0 -19
- package/plugins/extract-glossary/describe_test.go +0 -11
- package/plugins/extract-glossary/extract.go +0 -115
- package/plugins/extract-glossary/extract_test.go +0 -220
- package/plugins/extract-glossary/main.go +0 -59
- package/plugins/extract-glossary/parse.go +0 -214
- package/plugins/extract-glossary/parse_test.go +0 -203
- package/plugins/extract-go/aggregate.go +0 -214
- package/plugins/extract-go/client.go +0 -409
- package/plugins/extract-go/client_test.go +0 -266
- package/plugins/extract-go/describe.go +0 -19
- package/plugins/extract-go/describe_test.go +0 -11
- package/plugins/extract-go/enum.go +0 -195
- package/plugins/extract-go/enum_test.go +0 -82
- package/plugins/extract-go/event.go +0 -99
- package/plugins/extract-go/extract.go +0 -191
- package/plugins/extract-go/extract_test.go +0 -261
- package/plugins/extract-go/flow.go +0 -1441
- package/plugins/extract-go/flow_test.go +0 -609
- package/plugins/extract-go/httpclient.go +0 -174
- package/plugins/extract-go/httpclient_test.go +0 -296
- package/plugins/extract-go/ids.go +0 -92
- package/plugins/extract-go/layout.go +0 -225
- package/plugins/extract-go/layout_test.go +0 -125
- package/plugins/extract-go/lifecycle.go +0 -324
- package/plugins/extract-go/lifecycle_test.go +0 -108
- package/plugins/extract-go/main.go +0 -79
- package/plugins/extract-go/operation.go +0 -157
- package/plugins/extract-go/source.go +0 -316
- package/plugins/extract-go/transport.go +0 -321
- package/plugins/extract-go/transport_test.go +0 -145
- package/plugins/extract-go/wiring.go +0 -434
- package/plugins/extract-go-nats/describe.go +0 -19
- package/plugins/extract-go-nats/describe_test.go +0 -11
- package/plugins/extract-go-nats/extract.go +0 -177
- package/plugins/extract-go-nats/extract_test.go +0 -389
- package/plugins/extract-go-nats/index.go +0 -379
- package/plugins/extract-go-nats/main.go +0 -42
- package/plugins/extract-go-nats/resolve.go +0 -161
- package/plugins/extract-go-nats/sites.go +0 -224
- package/plugins/extract-graphql/describe.go +0 -19
- package/plugins/extract-graphql/describe_test.go +0 -11
- package/plugins/extract-graphql/extract.go +0 -433
- package/plugins/extract-graphql/extract_test.go +0 -256
- package/plugins/extract-graphql/ids.go +0 -49
- package/plugins/extract-graphql/lex.go +0 -237
- package/plugins/extract-graphql/main.go +0 -51
- package/plugins/extract-graphql/parse.go +0 -621
- package/plugins/extract-graphql/parse_test.go +0 -122
- package/plugins/extract-http-clients/describe.go +0 -19
- package/plugins/extract-http-clients/describe_test.go +0 -11
- package/plugins/extract-http-clients/extract.go +0 -705
- package/plugins/extract-http-clients/extract_test.go +0 -1263
- package/plugins/extract-http-clients/main.go +0 -42
- package/plugins/extract-openapi/describe.go +0 -19
- package/plugins/extract-openapi/describe_test.go +0 -11
- package/plugins/extract-openapi/discover.go +0 -243
- package/plugins/extract-openapi/extract.go +0 -526
- package/plugins/extract-openapi/extract_test.go +0 -545
- package/plugins/extract-openapi/main.go +0 -75
- package/plugins/extract-openapi/spec.go +0 -350
- package/plugins/extract-project/describe.go +0 -19
- package/plugins/extract-project/describe_test.go +0 -11
- package/plugins/extract-project/extract.go +0 -221
- package/plugins/extract-project/extract_test.go +0 -109
- package/plugins/extract-project/main.go +0 -41
- package/plugins/extract-proto/ast.go +0 -125
- package/plugins/extract-proto/consumes.go +0 -77
- package/plugins/extract-proto/describe.go +0 -19
- package/plugins/extract-proto/extract.go +0 -293
- package/plugins/extract-proto/extract_test.go +0 -459
- package/plugins/extract-proto/ids.go +0 -89
- package/plugins/extract-proto/ids_test.go +0 -57
- package/plugins/extract-proto/lex.go +0 -285
- package/plugins/extract-proto/main.go +0 -100
- package/plugins/extract-proto/module.go +0 -120
- package/plugins/extract-proto/parse.go +0 -720
- package/plugins/extract-proto/parse_test.go +0 -307
- package/plugins/extract-proto/provides.go +0 -236
- package/plugins/extract-proto/resolve.go +0 -222
- package/plugins/extract-proto/resolve_test.go +0 -119
- package/plugins/extract-redis/describe.go +0 -19
- package/plugins/extract-redis/describe_test.go +0 -11
- package/plugins/extract-redis/extract.go +0 -183
- package/plugins/extract-redis/extract_test.go +0 -168
- package/plugins/extract-redis/keyspaces.go +0 -469
- package/plugins/extract-redis/main.go +0 -44
- package/plugins/extract-river/describe.go +0 -19
- package/plugins/extract-river/describe_test.go +0 -11
- package/plugins/extract-river/extract.go +0 -507
- package/plugins/extract-river/extract_test.go +0 -132
- package/plugins/extract-river/main.go +0 -42
- package/plugins/extract-sql/ddl.go +0 -893
- package/plugins/extract-sql/ddl_test.go +0 -401
- package/plugins/extract-sql/describe.go +0 -19
- package/plugins/extract-sql/describe_test.go +0 -11
- package/plugins/extract-sql/layout.go +0 -221
- package/plugins/extract-sql/layout_test.go +0 -55
- package/plugins/extract-sql/lineage.go +0 -107
- package/plugins/extract-sql/main.go +0 -141
- package/plugins/extract-sql/maps.go +0 -564
- package/plugins/extract-sql/maps_java.go +0 -117
- package/plugins/extract-sql/maps_rust.go +0 -333
- package/plugins/extract-sql/maps_rust_test.go +0 -70
- package/plugins/extract-sql/maps_test.go +0 -204
- package/plugins/extract-sql/maps_ts.go +0 -398
- package/plugins/extract-sql/maps_ts_test.go +0 -136
- package/plugins/extract-sql/projection.go +0 -70
- package/plugins/extract-sql/projection_test.go +0 -50
- package/plugins/extract-sql/store.go +0 -420
- package/plugins/extract-sql/store_test.go +0 -233
- package/plugins/extract-sql/view.go +0 -295
- package/plugins/extract-watermill/describe.go +0 -19
- package/plugins/extract-watermill/describe_test.go +0 -11
- package/plugins/extract-watermill/extract.go +0 -1228
- package/plugins/extract-watermill/extract_test.go +0 -234
- package/plugins/extract-watermill/main.go +0 -41
- package/plugins/extract-wsdl/describe.go +0 -19
- package/plugins/extract-wsdl/describe_test.go +0 -11
- package/plugins/extract-wsdl/extract.go +0 -148
- package/plugins/extract-wsdl/extract_test.go +0 -45
- package/plugins/extract-wsdl/main.go +0 -53
- package/plugins/fetch-bsr/auth.go +0 -114
- package/plugins/fetch-bsr/bsr.go +0 -240
- package/plugins/fetch-bsr/cache.go +0 -66
- package/plugins/fetch-bsr/describe.go +0 -19
- package/plugins/fetch-bsr/fetch.go +0 -192
- package/plugins/fetch-bsr/fetch_test.go +0 -465
- package/plugins/fetch-bsr/lock.go +0 -71
- package/plugins/fetch-bsr/main.go +0 -117
- package/plugins/fetch-csr/auth.go +0 -95
- package/plugins/fetch-csr/cache.go +0 -69
- package/plugins/fetch-csr/describe.go +0 -19
- package/plugins/fetch-csr/describe_test.go +0 -11
- package/plugins/fetch-csr/fetch.go +0 -254
- package/plugins/fetch-csr/fetch_test.go +0 -494
- package/plugins/fetch-csr/lock.go +0 -95
- package/plugins/fetch-csr/main.go +0 -122
- package/plugins/fetch-csr/registry.go +0 -209
- package/plugins/fetch-git/cache.go +0 -65
- package/plugins/fetch-git/describe.go +0 -19
- package/plugins/fetch-git/describe_test.go +0 -11
- package/plugins/fetch-git/fetch.go +0 -181
- package/plugins/fetch-git/fetch_test.go +0 -332
- package/plugins/fetch-git/git.go +0 -169
- package/plugins/fetch-git/lock.go +0 -72
- package/plugins/fetch-git/main.go +0 -127
- package/plugins/fetch-git/offline.go +0 -39
- package/plugins/fetch-git/pin.go +0 -90
- package/plugins/fetch-git/pin_test.go +0 -74
- package/plugins/gen-backstage/describe.go +0 -17
- package/plugins/gen-backstage/main.go +0 -22
- package/plugins/gen-backstage/plugin.go +0 -473
- package/plugins/gen-backstage/plugin_test.go +0 -145
- package/plugins/gen-backstage.wasm +0 -0
- package/plugins/gen-markdown/adr.go +0 -162
- package/plugins/gen-markdown/aggregate.go +0 -362
- package/plugins/gen-markdown/canonical.go +0 -93
- package/plugins/gen-markdown/context.go +0 -90
- package/plugins/gen-markdown/coverage_test.go +0 -89
- package/plugins/gen-markdown/describe.go +0 -19
- package/plugins/gen-markdown/describe_test.go +0 -11
- package/plugins/gen-markdown/external.go +0 -71
- package/plugins/gen-markdown/flow.go +0 -277
- package/plugins/gen-markdown/glossary.go +0 -76
- package/plugins/gen-markdown/glossary_test.go +0 -148
- package/plugins/gen-markdown/llms.go +0 -302
- package/plugins/gen-markdown/main.go +0 -22
- package/plugins/gen-markdown/markdown.go +0 -254
- package/plugins/gen-markdown/markdown_test.go +0 -100
- package/plugins/gen-markdown/module.go +0 -107
- package/plugins/gen-markdown/plugin.go +0 -39
- package/plugins/gen-markdown/quality_test.go +0 -176
- package/plugins/gen-markdown/redis_test.go +0 -38
- package/plugins/gen-markdown/render.go +0 -344
- package/plugins/gen-markdown/render_test.go +0 -186
- package/plugins/gen-markdown/service.go +0 -456
- package/plugins/gen-markdown/source.go +0 -177
- package/plugins/gen-markdown/store.go +0 -201
- package/plugins/gen-markdown.wasm +0 -0
- package/plugins/gen-mermaid/describe.go +0 -17
- package/plugins/gen-mermaid/main.go +0 -22
- package/plugins/gen-mermaid/plugin.go +0 -104
- package/plugins/gen-mermaid/plugin_test.go +0 -33
- package/plugins/gen-mermaid.wasm +0 -0
- package/plugins/openapi/ids.go +0 -261
- package/plugins/openapi/ids_test.go +0 -98
- package/plugins/verify-codeowners/describe.go +0 -19
- package/plugins/verify-codeowners/describe_test.go +0 -11
- package/plugins/verify-codeowners/main.go +0 -75
- package/plugins/verify-codeowners/match.go +0 -85
- package/plugins/verify-codeowners/match_test.go +0 -47
- package/plugins/verify-codeowners/owners.go +0 -164
- package/plugins/verify-codeowners/owners_test.go +0 -225
- package/plugins/verify-codeowners/parse.go +0 -90
- package/plugins/verify-codeowners/parse_test.go +0 -62
- package/plugins/verify-otel/describe.go +0 -19
- package/plugins/verify-otel/describe_test.go +0 -11
- package/plugins/verify-otel/main.go +0 -53
- package/plugins/verify-otel/match.go +0 -336
- package/plugins/verify-otel/otlp.go +0 -200
- package/plugins/verify-otel/verify.go +0 -734
- package/plugins/verify-otel/verify_test.go +0 -460
- /package/{plugins/fetch-csr/options.schema.json → scripts/host-plugins/fetch-csr.options.json} +0 -0
- /package/{plugins/fetch-git/options.schema.json → scripts/host-plugins/fetch-git.options.json} +0 -0
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
// fetch-bsr, run by the host: a schema registry on one side, .proto files
|
|
2
|
+
// committed to this repository on the other.
|
|
3
|
+
//
|
|
4
|
+
// It is split off from extract-proto because it opens a socket. Extraction
|
|
5
|
+
// stays a pure function of the tree; fetching is the step that can fail, need
|
|
6
|
+
// a credential, or come back with something different than it did yesterday.
|
|
7
|
+
// Keeping them apart is what lets CI verify an estate whose schemas live in a
|
|
8
|
+
// registry it never talks to.
|
|
9
|
+
//
|
|
10
|
+
// It runs in the host (portolan.0008) because a socket and a secret are the
|
|
11
|
+
// host's to hold. The contract is the plugin's: the fetched protos are named,
|
|
12
|
+
// never written, so the host writes them, they get a manifest entry, are
|
|
13
|
+
// compared by `gen:check` like any other generated file, and are removed
|
|
14
|
+
// when the step stops naming them. Refreshing a pin then produces ONE pull
|
|
15
|
+
// request holding the pin bump, the proto diff, the lock diff and the
|
|
16
|
+
// fragment diff - which is the review worth having.
|
|
17
|
+
//
|
|
18
|
+
// In CI, or with PORTOLAN_OFFLINE=1, the step replays the committed copies,
|
|
19
|
+
// checks them against their locks and emits an identical file list, so a
|
|
20
|
+
// fork's pull request needs no secret and the registry being down cannot
|
|
21
|
+
// turn the tree red.
|
|
22
|
+
//
|
|
23
|
+
// No token in the manifest, ever: portolan.json is committed. The credential
|
|
24
|
+
// comes from BUF_TOKEN or the netrc `buf registry login` writes, and it
|
|
25
|
+
// changes whether the fetch succeeds, never what the fetch says.
|
|
26
|
+
|
|
27
|
+
import { createHash } from "node:crypto";
|
|
28
|
+
import { readFileSync } from "node:fs";
|
|
29
|
+
import { homedir } from "node:os";
|
|
30
|
+
import { join, posix } from "node:path";
|
|
31
|
+
|
|
32
|
+
import optionsSchema from "./fetch-bsr.options.json" with { type: "json" };
|
|
33
|
+
|
|
34
|
+
export const LOCK_NAME = "bsr.lock.json";
|
|
35
|
+
export const TOKEN_ENV = "BUF_TOKEN";
|
|
36
|
+
export const OFFLINE_ENV = "PORTOLAN_OFFLINE";
|
|
37
|
+
|
|
38
|
+
// The BSR's public API is Connect, and a Connect unary call is an ordinary
|
|
39
|
+
// HTTP POST carrying the request message as JSON. The message shapes are
|
|
40
|
+
// buf/registry/module/v1 with protojson's names: lowerCamelCase, bytes as
|
|
41
|
+
// base64. Everything the wire knows is in this file on purpose.
|
|
42
|
+
export const DOWNLOAD_METHOD = "/buf.registry.module.v1.DownloadService/Download";
|
|
43
|
+
export const COMMITS_METHOD = "/buf.registry.module.v1.CommitService/GetCommits";
|
|
44
|
+
|
|
45
|
+
export function describe() {
|
|
46
|
+
return {
|
|
47
|
+
name: "fetch-bsr",
|
|
48
|
+
summary: "Fetches pinned modules from a Buf Schema Registry into the tree, with a lock beside each, so the proto extractor can read a published contract.",
|
|
49
|
+
phases: ["extract"],
|
|
50
|
+
options: optionsSchema,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The step. Reads nothing out of the tree being described; its `cache` is
|
|
56
|
+
* repo-relative and everything else is in the options.
|
|
57
|
+
*
|
|
58
|
+
* The rules below are the design, not error handling. The one that matters
|
|
59
|
+
* most is the third: a failed fetch with no cache is an ERROR, never a short
|
|
60
|
+
* file list, because the host deletes files a step stops naming.
|
|
61
|
+
*
|
|
62
|
+
* @param {{options?: object}} request
|
|
63
|
+
* @param {{env?: NodeJS.ProcessEnv, fetch?: typeof fetch}} [io]
|
|
64
|
+
*/
|
|
65
|
+
export async function run(request, { env = process.env, fetch: fetchFn = globalThis.fetch } = {}) {
|
|
66
|
+
const options = request.options ?? {};
|
|
67
|
+
const modules = Array.isArray(options.modules) ? options.modules : [];
|
|
68
|
+
if (modules.length === 0) throw new Error("no modules to fetch: name at least one in the manifest");
|
|
69
|
+
if (!options.cache) {
|
|
70
|
+
throw new Error("no cache directory: set `cache` to the same path as the step's `out`, so an offline run can replay what the last online one wrote");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const out = new Builder();
|
|
74
|
+
const wanted = [...modules].sort((a, b) => String(a.module).localeCompare(String(b.module)));
|
|
75
|
+
const skip = offline(env);
|
|
76
|
+
|
|
77
|
+
for (const want of wanted) {
|
|
78
|
+
const parts = splitModule(want.module);
|
|
79
|
+
const registry = options.registry || parts.registry;
|
|
80
|
+
// Every module lives in its own directory with its own lock, so a
|
|
81
|
+
// vendored module is self-describing and extract-proto finds the lock
|
|
82
|
+
// by looking beside the files it is already reading.
|
|
83
|
+
const dir = `${parts.owner}/${parts.module}`;
|
|
84
|
+
const at = join(options.cache, parts.owner, parts.module);
|
|
85
|
+
|
|
86
|
+
if (skip) {
|
|
87
|
+
emitCached(out, dir, at, want, "offline");
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let fetched;
|
|
92
|
+
try {
|
|
93
|
+
fetched = await live(registry, parts, want, out, env, fetchFn);
|
|
94
|
+
} catch (cause) {
|
|
95
|
+
// Rule 2: the tree still holds a good copy, so the output is unchanged
|
|
96
|
+
// and `--check` stays clean.
|
|
97
|
+
try {
|
|
98
|
+
emitCached(out, dir, at, want, cause.message);
|
|
99
|
+
} catch (cacheCause) {
|
|
100
|
+
// Rule 3: nothing to fall back to.
|
|
101
|
+
throw new Error(`${want.module} could not be fetched (${cause.message}) and there is no usable copy in the tree (${cacheCause.message})`);
|
|
102
|
+
}
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
emitFetched(out, dir, want.module, fetched.commit, fetched.files);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return out.response();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function offline(env = process.env) {
|
|
112
|
+
if (String(env[OFFLINE_ENV] ?? "").trim()) return true;
|
|
113
|
+
return !["", "0", "false"].includes(String(env.CI ?? "").trim().toLowerCase());
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** `buf.build/acme/shop` in its three parts. */
|
|
117
|
+
export function splitModule(name) {
|
|
118
|
+
const parts = String(name ?? "").trim().split("/");
|
|
119
|
+
if (parts.length !== 3 || parts.some((part) => !part)) {
|
|
120
|
+
throw new Error(`"${String(name ?? "").trim()}" is not a module name; expected <registry>/<owner>/<module>`);
|
|
121
|
+
}
|
|
122
|
+
return { registry: parts[0], owner: parts[1], module: parts[2] };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* A credential for a registry, or "". None is a normal outcome: public
|
|
127
|
+
* modules download anonymously. The environment wins over the netrc
|
|
128
|
+
* `buf registry login` wrote.
|
|
129
|
+
*/
|
|
130
|
+
export function token(registry, env = process.env) {
|
|
131
|
+
const fromEnv = String(env[TOKEN_ENV] ?? "").trim();
|
|
132
|
+
if (fromEnv) return fromEnv;
|
|
133
|
+
const home = env.HOME || homedir();
|
|
134
|
+
for (const name of [".netrc", "_netrc"]) {
|
|
135
|
+
let text;
|
|
136
|
+
try {
|
|
137
|
+
text = readFileSync(join(home, name), "utf8");
|
|
138
|
+
} catch {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const found = netrcPassword(text, registry);
|
|
142
|
+
if (found) return found;
|
|
143
|
+
}
|
|
144
|
+
return "";
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** The password belonging to one machine in a netrc: a stream of words. */
|
|
148
|
+
export function netrcPassword(text, machine) {
|
|
149
|
+
const words = text.split(/\s+/).filter(Boolean);
|
|
150
|
+
let current = "";
|
|
151
|
+
for (let index = 0; index < words.length; index += 1) {
|
|
152
|
+
switch (words[index]) {
|
|
153
|
+
case "machine":
|
|
154
|
+
index += 1;
|
|
155
|
+
if (index >= words.length) return "";
|
|
156
|
+
current = words[index];
|
|
157
|
+
break;
|
|
158
|
+
case "default":
|
|
159
|
+
current = machine;
|
|
160
|
+
break;
|
|
161
|
+
case "password":
|
|
162
|
+
index += 1;
|
|
163
|
+
if (index >= words.length) return "";
|
|
164
|
+
if (current === machine) return words[index];
|
|
165
|
+
break;
|
|
166
|
+
case "login":
|
|
167
|
+
case "account":
|
|
168
|
+
index += 1;
|
|
169
|
+
break;
|
|
170
|
+
default:
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return "";
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** A digest the way buf writes one: `b5:<hex>`. */
|
|
178
|
+
export function displayDigest(digest) {
|
|
179
|
+
const value = digest?.value ? Buffer.from(String(digest.value), "base64") : Buffer.alloc(0);
|
|
180
|
+
if (value.length === 0) return "";
|
|
181
|
+
let kind = String(digest.type ?? "").replace(/^DIGEST_TYPE_/, "").toLowerCase();
|
|
182
|
+
if (!kind || kind === "unspecified") kind = "b5";
|
|
183
|
+
return `${kind}:${value.toString("hex")}`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** The lock, written the way every generated file here is written. */
|
|
187
|
+
export function encodeLock(entry) {
|
|
188
|
+
const record = { module: entry.module, commit: entry.commit };
|
|
189
|
+
if (entry.digest) record.digest = entry.digest;
|
|
190
|
+
if (entry.deps?.length) record.deps = [...entry.deps].sort();
|
|
191
|
+
record.files = [...entry.files]
|
|
192
|
+
.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0))
|
|
193
|
+
.map((file) => ({ path: file.path, sha256: file.sha256, size: file.size }));
|
|
194
|
+
return `${JSON.stringify({ modules: [record] }, null, 2)}\n`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function digestOf(contents) {
|
|
198
|
+
return createHash("sha256").update(contents).digest("hex");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function live(registry, parts, want, out, env, fetchFn) {
|
|
202
|
+
const client = new Client(registry, token(registry, env), fetchFn);
|
|
203
|
+
let ref = String(want.commit ?? "");
|
|
204
|
+
if (!ref) {
|
|
205
|
+
// Pin, or every run is a lottery. Resolving a label still works, but it
|
|
206
|
+
// is said out loud.
|
|
207
|
+
const label = want.ref || "main";
|
|
208
|
+
const resolved = await client.resolve(parts.owner, parts.module, label);
|
|
209
|
+
out.warn(want.module, `is not pinned; "${label}" resolved to ${resolved.id}. Pin it in portolan.json or every run is a lottery.`);
|
|
210
|
+
ref = resolved.id;
|
|
211
|
+
}
|
|
212
|
+
const { commit, files } = await client.download(parts.owner, parts.module, ref, want.paths ?? []);
|
|
213
|
+
if (files.size === 0) throw new Error(`${want.module} at ${ref} holds no .proto files`);
|
|
214
|
+
return { commit, files };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function emitFetched(out, dir, name, commit, files) {
|
|
218
|
+
const entry = { module: name, commit: commit.id, digest: displayDigest(commit.digest), files: [] };
|
|
219
|
+
for (const path of [...files.keys()].sort()) {
|
|
220
|
+
const contents = files.get(path);
|
|
221
|
+
out.file(posix.join(dir, path), contents.toString("utf8"));
|
|
222
|
+
entry.files.push({ path, sha256: digestOf(contents), size: contents.length });
|
|
223
|
+
}
|
|
224
|
+
out.file(posix.join(dir, LOCK_NAME), encodeLock(entry));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function emitCached(out, dir, at, want, why) {
|
|
228
|
+
if (!want.commit) throw new Error(`${want.module} is not pinned to a commit, so there is nothing to replay`);
|
|
229
|
+
const held = replay(at);
|
|
230
|
+
if (held.lock.commit !== want.commit) {
|
|
231
|
+
throw new Error(`${want.module} holds commit ${held.lock.commit} but the manifest pins ${want.commit}; fetch it`);
|
|
232
|
+
}
|
|
233
|
+
for (const file of held.lock.files) out.file(posix.join(dir, file.path), held.files.get(file.path).toString("utf8"));
|
|
234
|
+
out.file(posix.join(dir, LOCK_NAME), encodeLock(held.lock));
|
|
235
|
+
out.warn(want.module, `not fetched (${why}); the copy committed in this repository is used unchanged`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function replay(dir) {
|
|
239
|
+
const lockPath = join(dir, LOCK_NAME);
|
|
240
|
+
let raw;
|
|
241
|
+
try {
|
|
242
|
+
raw = readFileSync(lockPath, "utf8");
|
|
243
|
+
} catch (cause) {
|
|
244
|
+
if (cause.code === "ENOENT") throw new Error(`no ${LOCK_NAME} in ${dir.split("\\").join("/")}`);
|
|
245
|
+
throw cause;
|
|
246
|
+
}
|
|
247
|
+
let lock;
|
|
248
|
+
try {
|
|
249
|
+
lock = JSON.parse(raw);
|
|
250
|
+
} catch (cause) {
|
|
251
|
+
throw new Error(`${lockPath}: ${cause.message}`);
|
|
252
|
+
}
|
|
253
|
+
const modules = Array.isArray(lock.modules) ? lock.modules : [];
|
|
254
|
+
if (modules.length !== 1) throw new Error(`${lockPath} names ${modules.length} modules; expected exactly one`);
|
|
255
|
+
const entry = modules[0];
|
|
256
|
+
const files = new Map();
|
|
257
|
+
for (const want of entry.files ?? []) {
|
|
258
|
+
let contents;
|
|
259
|
+
try {
|
|
260
|
+
contents = readFileSync(join(dir, ...String(want.path).split("/")));
|
|
261
|
+
} catch (cause) {
|
|
262
|
+
if (cause.code === "ENOENT") throw new Error(`${want.path} is in the lock but not on disk`);
|
|
263
|
+
throw cause;
|
|
264
|
+
}
|
|
265
|
+
if (digestOf(contents) !== want.sha256) throw new Error(`${want.path} does not match its digest; the vendored copy was edited by hand`);
|
|
266
|
+
files.set(want.path, contents);
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
lock: { module: entry.module, commit: entry.commit, digest: entry.digest ?? "", deps: entry.deps ?? [], files: entry.files ?? [] },
|
|
270
|
+
files,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** One registry, spoken to over Connect. */
|
|
275
|
+
class Client {
|
|
276
|
+
constructor(registry, credential, fetchFn) {
|
|
277
|
+
// buf.build is https and needs no help; a self-hosted BSR behind a plain
|
|
278
|
+
// http address, or a test server, says so with a scheme.
|
|
279
|
+
this.base = /^https?:\/\//.test(registry) ? registry.replace(/\/$/, "") : `https://${registry}`;
|
|
280
|
+
this.credential = credential;
|
|
281
|
+
this.fetch = fetchFn;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async download(owner, module, ref, paths) {
|
|
285
|
+
const body = {
|
|
286
|
+
values: [{
|
|
287
|
+
resourceRef: { name: { owner, module, ref } },
|
|
288
|
+
// Only .proto: the docs and licence a module also carries are not
|
|
289
|
+
// something portolan reads.
|
|
290
|
+
fileTypes: ["FILE_TYPE_PROTO"],
|
|
291
|
+
...(paths.length ? { paths, pathsAllowNotExist: true } : {}),
|
|
292
|
+
}],
|
|
293
|
+
};
|
|
294
|
+
const answer = await this.call(DOWNLOAD_METHOD, body);
|
|
295
|
+
const content = answer.contents?.[0];
|
|
296
|
+
if (!content) throw new Error(`${owner}/${module}: the registry returned no content`);
|
|
297
|
+
const files = new Map();
|
|
298
|
+
for (const file of content.files ?? []) files.set(file.path, Buffer.from(String(file.content ?? ""), "base64"));
|
|
299
|
+
return { commit: content.commit ?? {}, files };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async resolve(owner, module, ref) {
|
|
303
|
+
const answer = await this.call(COMMITS_METHOD, { resourceRefs: [{ name: { owner, module, ref } }] });
|
|
304
|
+
const commit = answer.commits?.[0];
|
|
305
|
+
if (!commit) throw new Error(`${owner}/${module}: no commit for "${ref}"`);
|
|
306
|
+
return commit;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async call(method, body) {
|
|
310
|
+
const headers = { "Content-Type": "application/json", "Connect-Protocol-Version": "1" };
|
|
311
|
+
if (this.credential) headers.Authorization = `Bearer ${this.credential}`;
|
|
312
|
+
let response;
|
|
313
|
+
try {
|
|
314
|
+
// A timeout rather than none: a step that hangs holds up the whole run.
|
|
315
|
+
response = await this.fetch(`${this.base}${method}`, { method: "POST", headers, body: JSON.stringify(body), signal: AbortSignal.timeout(60_000) });
|
|
316
|
+
} catch (cause) {
|
|
317
|
+
throw new Error(`${method}: ${cause.cause?.message ?? cause.message}`);
|
|
318
|
+
}
|
|
319
|
+
const raw = await response.text();
|
|
320
|
+
if (!response.ok) throw new Error(`${method}: ${connectError(response.status, raw)}`);
|
|
321
|
+
return JSON.parse(raw);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** The error body Connect sends, or the status when the body is not one. */
|
|
326
|
+
function connectError(status, raw) {
|
|
327
|
+
try {
|
|
328
|
+
const wire = JSON.parse(raw);
|
|
329
|
+
if (wire?.message) return wire.code ? `${wire.code}: ${wire.message}` : wire.message;
|
|
330
|
+
} catch {
|
|
331
|
+
// Not a Connect error body.
|
|
332
|
+
}
|
|
333
|
+
return `http ${status}`;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
class Builder {
|
|
337
|
+
files = [];
|
|
338
|
+
warnings = [];
|
|
339
|
+
|
|
340
|
+
file(name, contents) {
|
|
341
|
+
this.files.push({ name, contents });
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
warn(ref, message) {
|
|
345
|
+
this.warnings.push({ severity: "warning", message, ref });
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
response() {
|
|
349
|
+
return { files: this.files, warnings: this.warnings };
|
|
350
|
+
}
|
|
351
|
+
}
|
package/{plugins/fetch-bsr/options.schema.json → scripts/host-plugins/fetch-bsr.options.json}
RENAMED
|
@@ -34,6 +34,10 @@
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
},
|
|
37
|
+
"registry": {
|
|
38
|
+
"type": "string",
|
|
39
|
+
"description": "Overrides the registry in every module name, for a self-hosted BSR: a host, or a URL when it is not https."
|
|
40
|
+
},
|
|
37
41
|
"cache": {
|
|
38
42
|
"type": "string",
|
|
39
43
|
"description": "Where the last run's files were written, repo-relative: the same path as the step's out. An offline run replays what is there and checks it against the lock beside it."
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// The fetcher, against a recorded registry. NEVER against a live one: a test
|
|
2
|
+
// that reaches buf.build would fail on a train, and the whole point of this
|
|
3
|
+
// plugin's design is that the build does not depend on a registry being up.
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { createServer } from "node:http";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { dirname, join } from "node:path";
|
|
9
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
10
|
+
|
|
11
|
+
import { runPlugin } from "../plugin-host.mjs";
|
|
12
|
+
import { COMMITS_METHOD, DOWNLOAD_METHOD, OFFLINE_ENV, TOKEN_ENV, displayDigest, encodeLock, netrcPassword, run, splitModule, token } from "./fetch-bsr.mjs";
|
|
13
|
+
|
|
14
|
+
const MODULE = "buf.build/acme/shop";
|
|
15
|
+
const PINNED = "c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6";
|
|
16
|
+
const PROTO = 'syntax = "proto3";\n\npackage shop.v1;\n\nservice Orders {\n rpc PlaceOrder(PlaceOrderRequest) returns (PlaceOrderResponse);\n}\n\nmessage PlaceOrderRequest { string customer_id = 1; }\nmessage PlaceOrderResponse { string order_id = 1; }\n';
|
|
17
|
+
const DIGEST = { type: "DIGEST_TYPE_B5", value: Buffer.from([0xde, 0xad, 0xbe, 0xef]).toString("base64") };
|
|
18
|
+
|
|
19
|
+
const cleanups = [];
|
|
20
|
+
afterEach(async () => {
|
|
21
|
+
for (const cleanup of cleanups.splice(0)) await cleanup();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
/** Serves the two calls the plugin makes, from recorded shapes. */
|
|
25
|
+
async function registry(handle = null) {
|
|
26
|
+
const seen = [];
|
|
27
|
+
const requests = [];
|
|
28
|
+
const server = createServer((request, response) => {
|
|
29
|
+
let body = "";
|
|
30
|
+
request.on("data", (chunk) => { body += chunk; });
|
|
31
|
+
request.on("end", () => {
|
|
32
|
+
seen.push(request.headers.authorization ?? "");
|
|
33
|
+
if (handle) return handle(request, response, body);
|
|
34
|
+
response.setHeader("Content-Type", "application/json");
|
|
35
|
+
if (request.url === DOWNLOAD_METHOD) {
|
|
36
|
+
requests.push(JSON.parse(body));
|
|
37
|
+
response.end(JSON.stringify({ contents: [{ commit: { id: PINNED, digest: DIGEST }, files: [{ path: "shop/v1/orders.proto", content: Buffer.from(PROTO).toString("base64") }] }] }));
|
|
38
|
+
} else if (request.url === COMMITS_METHOD) {
|
|
39
|
+
response.end(JSON.stringify({ commits: [{ id: PINNED, digest: DIGEST }] }));
|
|
40
|
+
} else {
|
|
41
|
+
response.statusCode = 404;
|
|
42
|
+
response.end(JSON.stringify({ code: "not_found", message: "no such method" }));
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
47
|
+
cleanups.push(() => new Promise((resolve) => server.close(resolve)));
|
|
48
|
+
return { url: `http://127.0.0.1:${server.address().port}`, seen, requests };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function cache() {
|
|
52
|
+
const dir = mkdtempSync(join(tmpdir(), "portolan-fetch-bsr-"));
|
|
53
|
+
cleanups.push(() => rmSync(dir, { recursive: true, force: true }));
|
|
54
|
+
return dir;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// A test must never inherit a real credential or a real offline setting.
|
|
58
|
+
const isolated = { [TOKEN_ENV]: "", [OFFLINE_ENV]: "", CI: "", HOME: "/nonexistent" };
|
|
59
|
+
const options = (base, cacheDir, extra = {}) => ({ registry: base, cache: cacheDir, modules: [{ module: MODULE, commit: PINNED, ...extra }] });
|
|
60
|
+
const fetch = (opts, env = {}) => run({ options: opts }, { env: { ...isolated, ...env } });
|
|
61
|
+
const names = (response) => response.files.map((file) => file.name).sort();
|
|
62
|
+
const contentsOf = (response, name) => response.files.find((file) => file.name === name)?.contents ?? "";
|
|
63
|
+
const write = (dir, response) => {
|
|
64
|
+
for (const file of response.files) {
|
|
65
|
+
mkdirSync(dirname(join(dir, file.name)), { recursive: true });
|
|
66
|
+
writeFileSync(join(dir, file.name), file.contents);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
describe("fetch-bsr", () => {
|
|
71
|
+
it("writes the protos and a lock beside them", async () => {
|
|
72
|
+
const server = await registry();
|
|
73
|
+
const response = await fetch(options(server.url, cache()));
|
|
74
|
+
// Each module in its own directory with its own lock, so extract-proto
|
|
75
|
+
// finds the lock beside the files it is already reading.
|
|
76
|
+
expect(names(response)).toEqual(["acme/shop/bsr.lock.json", "acme/shop/shop/v1/orders.proto"]);
|
|
77
|
+
expect(contentsOf(response, "acme/shop/shop/v1/orders.proto")).toBe(PROTO);
|
|
78
|
+
const lock = JSON.parse(contentsOf(response, "acme/shop/bsr.lock.json"));
|
|
79
|
+
expect(lock.modules).toHaveLength(1);
|
|
80
|
+
expect(lock.modules[0]).toEqual({
|
|
81
|
+
module: MODULE,
|
|
82
|
+
commit: PINNED,
|
|
83
|
+
digest: "b5:deadbeef",
|
|
84
|
+
files: [{ path: "shop/v1/orders.proto", sha256: createHash("sha256").update(PROTO).digest("hex"), size: PROTO.length }],
|
|
85
|
+
});
|
|
86
|
+
expect(response.warnings).toEqual([]);
|
|
87
|
+
// The wire shape, asserted rather than assumed.
|
|
88
|
+
expect(server.requests[0].values[0]).toEqual({ resourceRef: { name: { owner: "acme", module: "shop", ref: PINNED } }, fileTypes: ["FILE_TYPE_PROTO"] });
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("replays the committed copy offline, byte for byte, and in CI", async () => {
|
|
92
|
+
const server = await registry();
|
|
93
|
+
const cacheDir = cache();
|
|
94
|
+
const online = await fetch(options(server.url, cacheDir));
|
|
95
|
+
write(cacheDir, online);
|
|
96
|
+
|
|
97
|
+
const replayed = await fetch(options(server.url, cacheDir), { [OFFLINE_ENV]: "1" });
|
|
98
|
+
expect(names(replayed)).toEqual(names(online));
|
|
99
|
+
for (const file of online.files) expect(contentsOf(replayed, file.name)).toBe(file.contents);
|
|
100
|
+
expect(replayed.warnings).toHaveLength(1);
|
|
101
|
+
expect(replayed.warnings[0].message).toContain("not fetched");
|
|
102
|
+
|
|
103
|
+
const inCi = await fetch(options(server.url, cacheDir), { CI: "true" });
|
|
104
|
+
expect(names(inCi)).toEqual(names(online));
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("names an edited vendored file, and refuses a copy at another commit", async () => {
|
|
108
|
+
const server = await registry();
|
|
109
|
+
const cacheDir = cache();
|
|
110
|
+
write(cacheDir, await fetch(options(server.url, cacheDir)));
|
|
111
|
+
|
|
112
|
+
await expect(fetch(options(server.url, cacheDir, { commit: "0000000000000000000000000000000f" }), { [OFFLINE_ENV]: "1" })).rejects.toThrow(/holds commit .* but the manifest pins/);
|
|
113
|
+
|
|
114
|
+
writeFileSync(join(cacheDir, "acme/shop/shop/v1/orders.proto"), "// edited by hand\n");
|
|
115
|
+
await expect(fetch(options(server.url, cacheDir), { [OFFLINE_ENV]: "1" })).rejects.toThrow(/shop\/v1\/orders\.proto.*edited by hand/);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("fails rather than emitting nothing when there is no copy to fall back to", async () => {
|
|
119
|
+
await expect(fetch(options("http://127.0.0.1:1", cache()), { [OFFLINE_ENV]: "1" })).rejects.toThrow(/no bsr\.lock\.json/);
|
|
120
|
+
|
|
121
|
+
const broken = await registry((request, response) => {
|
|
122
|
+
response.statusCode = 500;
|
|
123
|
+
response.end(JSON.stringify({ code: "internal", message: "the registry is having a day" }));
|
|
124
|
+
});
|
|
125
|
+
await expect(fetch(options(broken.url, cache()))).rejects.toThrow(/the registry is having a day.*no usable copy/);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("falls back to the committed copy when the registry fails", async () => {
|
|
129
|
+
const server = await registry();
|
|
130
|
+
const cacheDir = cache();
|
|
131
|
+
const online = await fetch(options(server.url, cacheDir));
|
|
132
|
+
write(cacheDir, online);
|
|
133
|
+
|
|
134
|
+
const broken = await registry((request, response) => {
|
|
135
|
+
response.statusCode = 502;
|
|
136
|
+
response.end();
|
|
137
|
+
});
|
|
138
|
+
const fallback = await fetch(options(broken.url, cacheDir));
|
|
139
|
+
expect(names(fallback)).toEqual(names(online));
|
|
140
|
+
expect(fallback.warnings[0].message).toMatch(/not fetched \(.*http 502\)/);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("resolves an unpinned module online with a warning, and refuses it offline", async () => {
|
|
144
|
+
const server = await registry();
|
|
145
|
+
const response = await fetch(options(server.url, cache(), { commit: "" }));
|
|
146
|
+
expect(response.warnings).toHaveLength(1);
|
|
147
|
+
expect(response.warnings[0].message).toContain("not pinned");
|
|
148
|
+
expect(response.warnings[0].message).toContain(PINNED);
|
|
149
|
+
|
|
150
|
+
await expect(fetch(options("http://127.0.0.1:1", cache(), { commit: "" }), { [OFFLINE_ENV]: "1" })).rejects.toThrow(MODULE);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("sends the token and never lets it reach the output", async () => {
|
|
154
|
+
const server = await registry();
|
|
155
|
+
const anonymous = await fetch(options(server.url, cache()));
|
|
156
|
+
const authorised = await fetch(options(server.url, cache()), { [TOKEN_ENV]: "a-real-looking-secret" });
|
|
157
|
+
for (const file of anonymous.files) {
|
|
158
|
+
expect(contentsOf(authorised, file.name)).toBe(file.contents);
|
|
159
|
+
expect(contentsOf(authorised, file.name)).not.toContain("a-real-looking-secret");
|
|
160
|
+
}
|
|
161
|
+
expect(server.seen[0]).toBe("");
|
|
162
|
+
expect(server.seen.at(-1)).toBe("Bearer a-real-looking-secret");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("refuses a missing cache and a module name without three parts", async () => {
|
|
166
|
+
await expect(fetch(options("http://127.0.0.1:1", ""))).rejects.toThrow(/cache/);
|
|
167
|
+
await expect(fetch(options("http://127.0.0.1:1", cache(), { module: "acme/shop" }))).rejects.toThrow(/module name/);
|
|
168
|
+
expect(splitModule(MODULE)).toEqual({ registry: "buf.build", owner: "acme", module: "shop" });
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("runs through the host like any plugin", async () => {
|
|
172
|
+
const server = await registry();
|
|
173
|
+
const cacheDir = cache();
|
|
174
|
+
write(cacheDir, await fetch(options(server.url, cacheDir)));
|
|
175
|
+
const saved = { CI: process.env.CI, [OFFLINE_ENV]: process.env[OFFLINE_ENV] };
|
|
176
|
+
process.env.CI = "";
|
|
177
|
+
process.env[OFFLINE_ENV] = "1";
|
|
178
|
+
try {
|
|
179
|
+
const result = await runPlugin({ name: "bsr", host: "fetch-bsr" }, { portolanVersion: "0.1.0", options: options(server.url, cacheDir) });
|
|
180
|
+
expect(result.files).toHaveLength(2);
|
|
181
|
+
expect(result.warnings).toEqual([`${MODULE}: not fetched (offline); the copy committed in this repository is used unchanged`]);
|
|
182
|
+
const described = await runPlugin({ name: "bsr", host: "fetch-bsr" }, { portolanVersion: "0.1.0", kind: "describe" });
|
|
183
|
+
expect(described.describe).toMatchObject({ name: "fetch-bsr", phases: ["extract"] });
|
|
184
|
+
} finally {
|
|
185
|
+
for (const [key, value] of Object.entries(saved)) {
|
|
186
|
+
if (value === undefined) delete process.env[key];
|
|
187
|
+
else process.env[key] = value;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe("credentials and locks", () => {
|
|
194
|
+
it("takes the environment over the netrc, and the netrc over nothing", () => {
|
|
195
|
+
const home = cache();
|
|
196
|
+
writeFileSync(join(home, ".netrc"), "machine buf.build login someone password from-netrc\nmachine other.example login x password other\n");
|
|
197
|
+
expect(token("buf.build", { HOME: home })).toBe("from-netrc");
|
|
198
|
+
expect(token("elsewhere.example", { HOME: home })).toBe("");
|
|
199
|
+
expect(token("buf.build", { HOME: home, [TOKEN_ENV]: "from-env" })).toBe("from-env");
|
|
200
|
+
expect(netrcPassword("default login a password d\n", "any.host")).toBe("d");
|
|
201
|
+
expect(netrcPassword("machine buf.build login", "buf.build")).toBe("");
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("writes a digest the way buf does and a lock the way every generated file is written", () => {
|
|
205
|
+
expect(displayDigest(DIGEST)).toBe("b5:deadbeef");
|
|
206
|
+
expect(displayDigest({ type: "DIGEST_TYPE_UNSPECIFIED", value: DIGEST.value })).toBe("b5:deadbeef");
|
|
207
|
+
expect(displayDigest({})).toBe("");
|
|
208
|
+
const lock = encodeLock({ module: MODULE, commit: PINNED, digest: "", deps: ["b", "a"], files: [{ path: "z", sha256: "2", size: 2 }, { path: "a", sha256: "1", size: 1 }] });
|
|
209
|
+
expect(JSON.parse(lock)).toEqual({ modules: [{ module: MODULE, commit: PINNED, deps: ["a", "b"], files: [{ path: "a", sha256: "1", size: 1 }, { path: "z", sha256: "2", size: 2 }] }] });
|
|
210
|
+
expect(lock.endsWith("\n")).toBe(true);
|
|
211
|
+
});
|
|
212
|
+
});
|