Skip to content

Commit e299f83

Browse files
authored
Merge branch 'main' into august-20260511-restore-platform-free-trial
2 parents d08af8d + 7660f00 commit e299f83

340 files changed

Lines changed: 39829 additions & 20818 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
name: publish-postman
2+
3+
# Pushes generated OpenAPI specs to their Postman public collections via
4+
# the Postman API (PUT /collections/:id). One matrix entry per collection.
5+
#
6+
# Triggered only when a watched openapi.yaml actually changes on main.
7+
# Each matrix job double-checks via `git diff` so unrelated specs are skipped.
8+
#
9+
# Required repo configuration:
10+
# secrets.POSTMAN_API_KEY - Postman API key
11+
# vars.POSTMAN_SIGNALWIRE_REST_COLLECTION_ID - public collection UID
12+
# vars.POSTMAN_COMPATIBILITY_COLLECTION_ID - public collection UID
13+
#
14+
# The public collections must already exist in Postman; PUT replaces contents
15+
# but cannot create a new collection.
16+
17+
on:
18+
push:
19+
branches:
20+
- main
21+
paths:
22+
- 'fern/apis/signalwire-rest/openapi.yaml'
23+
- 'fern/apis/compatibility/openapi.yaml'
24+
workflow_dispatch:
25+
inputs:
26+
force_all:
27+
description: 'Push every collection regardless of change detection'
28+
type: boolean
29+
default: false
30+
31+
# Restrict the default GITHUB_TOKEN: this workflow only reads source files
32+
# (for git diff) and never writes back to the repo. Lock it down so a
33+
# compromised dependency cannot use the token to push commits.
34+
permissions:
35+
contents: read
36+
37+
jobs:
38+
publish:
39+
runs-on: ubuntu-latest
40+
strategy:
41+
fail-fast: false
42+
matrix:
43+
# collection_id values are looked up from repo Variables at matrix
44+
# expansion time (static `vars.X` references, not dynamic indexing).
45+
# UIDs are not sensitive — they appear in public Postman URLs — so
46+
# they live in Variables, not Secrets.
47+
include:
48+
- name: signalwire-rest
49+
spec: fern/apis/signalwire-rest/openapi.yaml
50+
collection_id: ${{ vars.POSTMAN_SIGNALWIRE_REST_COLLECTION_ID }}
51+
- name: compatibility
52+
spec: fern/apis/compatibility/openapi.yaml
53+
collection_id: ${{ vars.POSTMAN_COMPATIBILITY_COLLECTION_ID }}
54+
steps:
55+
- name: Checkout
56+
# Pinned to commit SHA (v4.2.2) — tags are mutable and a tag
57+
# repoint by a compromised upstream would run with our secrets.
58+
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
59+
with:
60+
fetch-depth: 2 # need HEAD^ for the per-spec diff
61+
62+
- name: Detect whether ${{ matrix.spec }} changed
63+
id: changed
64+
env:
65+
SPEC: ${{ matrix.spec }}
66+
FORCE_ALL: ${{ github.event_name == 'workflow_dispatch' && inputs.force_all || 'false' }}
67+
run: |
68+
if [[ "$FORCE_ALL" == "true" ]]; then
69+
echo "Manual dispatch with force_all=true; running."
70+
echo "changed=true" >> "$GITHUB_OUTPUT"
71+
elif git diff --name-only HEAD^ HEAD | grep -qx "$SPEC"; then
72+
echo "$SPEC changed in this push."
73+
echo "changed=true" >> "$GITHUB_OUTPUT"
74+
else
75+
echo "$SPEC did not change; skipping."
76+
echo "changed=false" >> "$GITHUB_OUTPUT"
77+
fi
78+
79+
- name: Setup Node.js
80+
if: steps.changed.outputs.changed == 'true'
81+
# Pinned to commit SHA for the same reason as actions/checkout above.
82+
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
83+
with:
84+
node-version: '22'
85+
86+
- name: Install openapi-to-postmanv2
87+
if: steps.changed.outputs.changed == 'true'
88+
# Exact version pin so an upstream npm compromise can't ship new
89+
# code under the same major. Bump deliberately; verify CLI flags
90+
# below still validate against any new major.
91+
run: npm install -g openapi-to-postmanv2@6.0.1
92+
93+
- name: Rewrite relative docs links for Postman
94+
if: steps.changed.outputs.changed == 'true'
95+
# Rewrites markdown links like [foo](/docs/...) to absolute
96+
# [foo](https://signalwire.com/docs/...) so the published collection
97+
# links to the live docs site. The OpenAPI source-of-truth stays
98+
# relative (correct for Fern); only the workflow's checkout copy is
99+
# rewritten. Absolute URLs already in the spec (externalDocs.url,
100+
# /company/contact, my.signalwire.com, etc.) are untouched because
101+
# the rewrite is anchored on `](/` — see the script for details.
102+
run: node scripts/postman/rewrite-links.js "${{ matrix.spec }}"
103+
104+
- name: Convert OpenAPI to Postman Collection
105+
if: steps.changed.outputs.changed == 'true'
106+
# Conversion options live in scripts/postman/convert-options.json so
107+
# they're identical here and in any local invocation.
108+
#
109+
# We pass them via `-c` (config file) rather than `-O key=value` because
110+
# the CLI's `-O` parser filters its accepted-option list against an
111+
# OpenAPI version that defaults to 3.0, dropping any 3.1-only option
112+
# like `includeWebhooks` with a stderr warning and continuing. The
113+
# `-c` path reads the JSON as-is and bypasses the filter, so 3.1
114+
# webhooks (3 in signalwire-rest, 5 in compatibility) come through.
115+
# Tracked as a quirk of openapi-to-postmanv2 v6.0.1.
116+
run: |
117+
openapi2postmanv2 \
118+
-s "${{ matrix.spec }}" \
119+
-o collection.json \
120+
-p \
121+
-c scripts/postman/convert-options.json
122+
123+
- name: Group APIs under a wrapper folder, strip IDs, wrap for PUT
124+
if: steps.changed.outputs.changed == 'true'
125+
# Postman API requires {"collection": {...}} and rejects payloads
126+
# carrying foreign _postman_id / id / uid fields.
127+
#
128+
# Layout produced by openapi-to-postmanv2 with `folderStrategy: "Tags"`
129+
# and `includeWebhooks: true`: top-level `.item[]` is a flat list of
130+
# tag-derived folders plus one converter-generated "Webhooks" folder
131+
# that holds the OpenAPI 3.1 `webhooks:` entries. We restructure so
132+
# webhooks stay peer-level at the root while every tag folder moves
133+
# into a single wrapper folder named "APIs", alphabetized
134+
# case-insensitively. "cXML *" then slots near "Call Flows" instead
135+
# of at the bottom under ASCII byte order.
136+
#
137+
# Partitioning is by name ("Webhooks" vs the rest) because the
138+
# converter doesn't tag the synthetic webhooks folder with any
139+
# distinguishing metadata. If a future spec adds a tag literally
140+
# named "Webhooks", that tag would land at the root with the
141+
# webhooks folder instead of inside "APIs" — revisit then.
142+
# openapi-to-postmanv2 has no built-in sort or wrap option, so we
143+
# do it here. Items inside each tag folder keep their existing
144+
# path/method order; items inside the Webhooks folder are unchanged.
145+
run: |
146+
jq '
147+
(.item | map(select(.name != "Webhooks")) | sort_by(.name | ascii_downcase)) as $apis
148+
| (.item | map(select(.name == "Webhooks"))) as $webhooks
149+
| .item = ([{name: "APIs", item: $apis}] + $webhooks)
150+
| walk(if type == "object" then del(._postman_id, .id, .uid) else . end)
151+
| {collection: .}
152+
' collection.json > payload.json
153+
154+
- name: PUT collection to Postman
155+
if: steps.changed.outputs.changed == 'true'
156+
# Both env vars are required. The matrix supplies COLLECTION_ID per
157+
# entry (static `vars.X` resolved at matrix-expansion time); the
158+
# secret supplies the API key. Both are validated below before any
159+
# network call.
160+
#
161+
# We capture the HTTP status with curl's `-w "%{http_code}"` and
162+
# branch on it explicitly rather than relying on `--fail-with-body`,
163+
# so the workflow logs report a clear ✓/✗ line for the PUT and dump
164+
# the response body on failure. The PUT response shape is
165+
# {"collection": {"id", "name", "uid"}} — name/uid live directly on
166+
# `.collection`, not under `.collection.info` (that nested path is
167+
# the *collection schema* shape, not the PUT envelope).
168+
env:
169+
POSTMAN_API_KEY: ${{ secrets.POSTMAN_API_KEY }}
170+
COLLECTION_ID: ${{ matrix.collection_id }}
171+
run: |
172+
: "${POSTMAN_API_KEY:?missing repo secret POSTMAN_API_KEY}"
173+
: "${COLLECTION_ID:?missing repo variable for ${{ matrix.name }} collection ID — check Settings → Variables}"
174+
175+
http_status=$(curl -sS -X PUT \
176+
"https://api.getpostman.com/collections/$COLLECTION_ID" \
177+
-H "X-Api-Key: $POSTMAN_API_KEY" \
178+
-H "Content-Type: application/json" \
179+
--data-binary @payload.json \
180+
-o response.json \
181+
-w "%{http_code}")
182+
183+
if [[ "$http_status" =~ ^2[0-9][0-9]$ ]]; then
184+
echo "✓ Postman PUT succeeded (HTTP $http_status) for ${{ matrix.name }}"
185+
jq -r '.collection | " name: \(.name)\n uid: \(.uid)"' response.json \
186+
|| cat response.json
187+
else
188+
echo "✗ Postman PUT failed (HTTP $http_status) for ${{ matrix.name }}"
189+
echo "Response body:"
190+
cat response.json
191+
exit 1
192+
fi

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,6 @@ node_modules
55
.serena
66
.claude
77
.vscode
8+
.env
9+
.env.*
10+
!.env.example

README.md

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -109,19 +109,30 @@ Whether you're fixing a typo, reporting missing information, or submitting new c
109109
3. Make your changes and [submit a pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request).
110110

111111
> [!IMPORTANT]
112-
> Contributions to the API docs require additional workflows.
113-
> TypeSpec source files live in `specs/` and compile to OpenAPI specs in `fern/apis/`.
112+
> Contributions to the API reference or SWML schema require additional workflows.
113+
> Spec source lives in `specs/` as [TypeSpec](https://typespec.io/docs) (`.tsp`) and compiles to OpenAPI / JSON Schema in `fern/apis/` and `specs/swml/**/tsp-output/`.
114114
> See the [TypeSpec documentation](https://typespec.io/docs) for guidance on editing spec files.
115115
116-
### REST API documentation
116+
### API specifications
117117

118-
API reference pages are generated from [TypeSpec](https://typespec.io/docs) definitions in [`specs/`](specs/), which compile into [OpenAPI](https://swagger.io/specification/) spec files consumed by Fern.
118+
The [`specs/`](specs/) workspace holds three sibling TypeSpec projects. Each one declares its own `tspconfig.yaml` and emits a different downstream artifact:
119+
120+
| Project | Source | Emits | Powers |
121+
|---|---|---|---|
122+
| **SignalWire REST API** | [`specs/signalwire-rest/`](specs/signalwire-rest/) | `fern/apis/signalwire-rest/openapi.yaml` (OpenAPI 3.1) | The [`/docs/apis`](https://signalwire.com/docs/apis) reference |
123+
| **Compatibility API** | [`specs/compatibility-api/`](specs/compatibility-api/) | `fern/apis/compatibility/openapi.yaml` (OpenAPI 3.1) | The [`/docs/compatibility-api`](https://signalwire.com/docs/compatibility-api) reference |
124+
| **SWML** | [`specs/swml/`](specs/swml/) | `specs/swml/**/tsp-output/.../SWMLObject.json` (JSON Schema) | SWML editor + runtime validator tooling |
125+
126+
Shared building blocks live in [`specs/_shared/`](specs/_shared/): repo-wide scalars (`uuid`, `jwt`), status-code aliases, and a custom `@webhook(...)` decorator that emits OpenAPI 3.1 `webhooks:` entries for outbound payloads SignalWire sends to your servers.
119127

120128
```bash
121-
yarn build:specs # Compile TypeSpec → OpenAPI
122-
yarn format:specs # Format spec files
129+
yarn build:specs # Compile every TypeSpec project → OpenAPI + JSON Schema
130+
yarn format:specs # tsp format every .tsp file in the workspace
123131
```
124132

133+
> [!NOTE]
134+
> Generated files (`fern/apis/*/openapi.yaml` and `specs/swml/**/tsp-output/`) are committed but **not** hand-edited. Always change the TypeSpec source, then rebuild — Fern and the SWML tooling read the generated artifacts directly.
135+
125136
---
126137

127138
## Usage

0 commit comments

Comments
 (0)