Skip to content

Commit 5272a7e

Browse files
authored
feat(openapi/upload): docs + DX enhancements for slugs vs. legacy IDs (#1309)
## 🧰 Changes folks have been a little confused with the slug vs. legacy id parameter paradigm, so this PR enhances the DX and docs for `rdme openapi upload` in a few ways: - [x] expanded upon the whole legacy ID ➡️ slug change in the migration guide - [x] added a little bit of smarter DX for when a user passes in a `--slug <some slug that looks eerily like a mongo object id>` (which is a no-no since object IDs should be using `--legacy-id`): - if a user passes an object ID and it matches one of the legacy IDs in the API definitions list, it'll prompt the user before updating that API definition, and emit a warning telling them what the correct slug should be. if this is run in CI, it'll throw an error that tells them the correct file name. <img width="2906" height="219" alt="CleanShot 2025-07-17 at 17 28 43@2x" src="https://github.com/user-attachments/assets/2bf7f7b2-7b27-4605-8bab-aa25f79d24cf" /> - if the object ID doesn't match anything, it'll proceed as normal but emit a warning telling them to check out our migration guide, just in case. <img width="2512" height="196" alt="CleanShot 2025-07-17 at 17 31 55@2x" src="https://github.com/user-attachments/assets/d505d174-1f65-43fb-a5ff-197db1c03ddd" /> - [x] if a user passes the `--legacy-id` flag and there aren't any matches but there are legacy API definitions in the project, the error message will suggest those - [x] if a user passes the `--legacy-id` flag and there is a match, the warning is now a bit more custom tailored and will inform them of what the slug is and encourage them to use that instead ## 🧬 QA & Testing wrote a bunch of tests for every permutation of this logic, and confirmed it works against my lil clone of our internal handbook.
1 parent 9eae363 commit 5272a7e

File tree

5 files changed

+288
-16
lines changed

5 files changed

+288
-16
lines changed

__tests__/commands/openapi/__snapshots__/upload.test.ts.snap

Lines changed: 81 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,28 @@ See more help with --help],
2020
}
2121
`;
2222

23-
exports[`rdme openapi upload > given that the "--legacy-id" flag is passed > should error if no matching API definition found 1`] = `
23+
exports[`rdme openapi upload > given that the "--legacy-id" flag is passed > should error if no matching API definition found (and there are no existing definitions) 1`] = `
2424
{
25-
"error": [Error: No API definition found with legacy ID 1234567890.],
26-
"stderr": " › Warning: The "legacy-id" flag has been deprecated.
27-
- Validating the API definition located at __tests__/__fixtures__/petstore-simple-weird-version.json...
25+
"error": [Error: No API definition found with legacy ID 1234567890. No existing definitions were found. Did you mean to use the \`--slug\` flag instead?],
26+
"stderr": "- Validating the API definition located at __tests__/__fixtures__/petstore-simple-weird-version.json...
27+
",
28+
"stdout": "",
29+
}
30+
`;
31+
32+
exports[`rdme openapi upload > given that the "--legacy-id" flag is passed > should error if no matching API definition found (and there are several existing definitions) 1`] = `
33+
{
34+
"error": [Error: No API definition found with legacy ID 1234567890. 3 file(s) were foundif you meant to update one of those, pass in one of these slugs via the \`--slug\` flag: \`file1.json\`, \`file2.json\`, \`file3.json\`.],
35+
"stderr": "- Validating the API definition located at __tests__/__fixtures__/petstore-simple-weird-version.json...
36+
",
37+
"stdout": "",
38+
}
39+
`;
40+
41+
exports[`rdme openapi upload > given that the "--legacy-id" flag is passed > should error if no matching API definition found (and there is a single existing definition) 1`] = `
42+
{
43+
"error": [Error: No API definition found with legacy ID 1234567890. 1 file was founddid you mean to update that? If so, try replacing \`--legacy-id 1234567890\` with \`--slug file1.json\`.],
44+
"stderr": "- Validating the API definition located at __tests__/__fixtures__/petstore-simple-weird-version.json...
2845
",
2946
"stdout": "",
3047
}
@@ -36,8 +53,10 @@ exports[`rdme openapi upload > given that the "--legacy-id" flag is passed > sho
3653
"status": "done",
3754
"uri": "/branches/1.0.0/apis/legacy-spec.json",
3855
},
39-
"stderr": " › Warning: The "legacy-id" flag has been deprecated.
40-
- Validating the API definition located at __tests__/__fixtures__/petstore-simple-weird-version.json...
56+
"stderr": "- Validating the API definition located at __tests__/__fixtures__/petstore-simple-weird-version.json...
57+
Warning: The \`--legacy-id\` flag will be removed in a future version. We
58+
› recommend passing \`--slug legacy-spec.json\` for maximum compatibility and
59+
› readability.
4160
- Updating your API definition to ReadMe...
4261
✔ Updating your API definition to ReadMe... done!
4362
",
@@ -101,6 +120,34 @@ exports[`rdme openapi upload > given that the API definition is a URL > should u
101120
}
102121
`;
103122
123+
exports[`rdme openapi upload > given that the API definition is a local file > and the \`--slug\` flag is passed > should emit a warning if an object ID is passed and no legacy match is found 1`] = `
124+
{
125+
"result": {
126+
"status": "done",
127+
"uri": "/branches/1.0.0/apis/687855c3600c6e14c79a94cb.json",
128+
},
129+
"stderr": "- Validating the API definition located at __tests__/__fixtures__/petstore-simple-weird-version.json...
130+
Warning: The slug you provided looks like a legacy API definition ID, and
131+
these IDs are now deprecated in ReadMe. More info here:
132+
https://github.com/readmeio/rdme/blob/v10/documentation/migration-guide.md
133+
› #v10-openapi-upload-command-updates
134+
- Creating your API definition to ReadMe...
135+
Creating your API definition to ReadMe... done!
136+
",
137+
"stdout": "🚀 Your API definition (687855c3600c6e14c79a94cb.json) was successfully created in ReadMe!
138+
",
139+
}
140+
`;
141+
142+
exports[`rdme openapi upload > given that the API definition is a local file > and the \`--slug\` flag is passed > should exit if an object ID is passed and a legacy match is found but the user declines the prompt 1`] = `
143+
{
144+
"error": [Error: Aborting, no changes were made.],
145+
"stderr": "- Validating the API definition located at __tests__/__fixtures__/petstore-simple-weird-version.json...
146+
",
147+
"stdout": "",
148+
}
149+
`;
150+
104151
exports[`rdme openapi upload > given that the API definition is a local file > and the \`--slug\` flag is passed > should handle a slug with a valid but mismatching file extension 1`] = `
105152
{
106153
"error": [Error: Please provide a valid file extension that matches the extension on the file you provided. Must be \`.json\`, \`.yaml\`, or \`.yml\`.],
@@ -119,6 +166,25 @@ exports[`rdme openapi upload > given that the API definition is a local file > a
119166
}
120167
`;
121168
169+
exports[`rdme openapi upload > given that the API definition is a local file > and the \`--slug\` flag is passed > should prompt the user before updating if an object ID is passed and a legacy match is found 1`] = `
170+
{
171+
"result": {
172+
"status": "done",
173+
"uri": "/branches/1.0.0/apis/legacy-spec.json",
174+
},
175+
"stderr": "- Validating the API definition located at __tests__/__fixtures__/petstore-simple-weird-version.json...
176+
Warning: The slug you provided matches a legacy API definition ID and
177+
these IDs are now deprecated in ReadMe. To ensure maximum compatibility
178+
going forward, we recommend updating your \`--slug\` flag to
179+
\`legacy-spec.json\`.
180+
- Updating your API definition to ReadMe...
181+
✔ Updating your API definition to ReadMe... done!
182+
",
183+
"stdout": "🚀 Your API definition (legacy-spec.json) was successfully updated in ReadMe!
184+
",
185+
}
186+
`;
187+
122188
exports[`rdme openapi upload > given that the API definition is a local file > and the \`--slug\` flag is passed > should use the provided slug (includes file extension) as the filename 1`] = `
123189
{
124190
"result": {
@@ -149,6 +215,15 @@ exports[`rdme openapi upload > given that the API definition is a local file > a
149215
}
150216
`;
151217
218+
exports[`rdme openapi upload > given that the API definition is a local file > and the command is being run in a CI environment > should error out if an object ID is passed and there is a legacy match found 1`] = `
219+
{
220+
"error": [Error: The slug you provided matches a legacy API definition ID, which have been deprecated in ReadMe. To fix this, update your \`--slug\` flag to \`legacy-spec.json\`.],
221+
"stderr": "- Validating the API definition located at __tests__/__fixtures__/petstore-simple-weird-version.json...
222+
",
223+
"stdout": "",
224+
}
225+
`;
226+
152227
exports[`rdme openapi upload > given that the API definition is a local file > and the command is being run in a CI environment > should overwrite an existing API definition without asking for confirmation 1`] = `
153228
{
154229
"result": {

__tests__/commands/openapi/upload.test.ts

Lines changed: 125 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,20 +192,94 @@ describe('rdme openapi upload', () => {
192192
mock.done();
193193
});
194194

195+
it('should emit a warning if an object ID is passed and no legacy match is found', async () => {
196+
const customSlug = '687855c3600c6e14c79a94cb';
197+
const customSlugWithExtension = `${customSlug}.json`;
198+
const mock = getAPIv2Mock({ authorization: `Bearer ${key}` })
199+
.get(`/branches/${branch}/apis`)
200+
.reply(200, { data: [] })
201+
.post(`/branches/${branch}/apis`, body =>
202+
body.match(`form-data; name="schema"; filename="${customSlugWithExtension}"`),
203+
)
204+
.reply(200, {
205+
data: {
206+
upload: { status: 'done' },
207+
uri: `/branches/${branch}/apis/${customSlugWithExtension}`,
208+
},
209+
});
210+
211+
const result = await run(['--branch', branch, filename, '--key', key, '--slug', customSlug]);
212+
213+
expect(result).toMatchSnapshot();
214+
215+
mock.done();
216+
});
217+
218+
it('should prompt the user before updating if an object ID is passed and a legacy match is found', async () => {
219+
prompts.inject([true]);
220+
const customSlug = '687855c3600c6e14c79a94cb';
221+
const existingFilename = 'legacy-spec.json';
222+
const mock = getAPIv2Mock({ authorization: `Bearer ${key}` })
223+
.get(`/branches/${branch}/apis`)
224+
.reply(200, { data: [{ legacy_id: customSlug, filename: existingFilename }] })
225+
.put(`/branches/${branch}/apis/${existingFilename}`, body =>
226+
body.match(`form-data; name="schema"; filename="${existingFilename}"`),
227+
)
228+
.reply(200, {
229+
data: {
230+
upload: { status: 'done' },
231+
uri: `/branches/${branch}/apis/${existingFilename}`,
232+
},
233+
});
234+
235+
const result = await run(['--branch', branch, filename, '--key', key, '--slug', customSlug]);
236+
237+
expect(result).toMatchSnapshot();
238+
239+
mock.done();
240+
});
241+
242+
it('should exit if an object ID is passed and a legacy match is found but the user declines the prompt', async () => {
243+
prompts.inject([false]);
244+
const customSlug = '687855c3600c6e14c79a94cb';
245+
const existingFilename = 'legacy-spec.json';
246+
const mock = getAPIv2Mock({ authorization: `Bearer ${key}` })
247+
.get(`/branches/${branch}/apis`)
248+
.reply(200, { data: [{ legacy_id: customSlug, filename: existingFilename }] });
249+
250+
const result = await run(['--branch', branch, filename, '--key', key, '--slug', customSlug]);
251+
252+
expect(result).toMatchSnapshot();
253+
254+
mock.done();
255+
});
256+
195257
it('should handle a slug with an invalid file extension', async () => {
196258
const customSlug = 'custom-slug.yikes';
197259

260+
const mock = getAPIv2Mock({ authorization: `Bearer ${key}` })
261+
.get(`/branches/${branch}/apis`)
262+
.reply(200, { data: [] });
263+
198264
const result = await run(['--branch', branch, filename, '--key', key, '--slug', customSlug]);
199265

200266
expect(result).toMatchSnapshot();
267+
268+
mock.done();
201269
});
202270

203271
it('should handle a slug with a valid but mismatching file extension', async () => {
204272
const customSlug = 'custom-slug.yml';
205273

274+
const mock = getAPIv2Mock({ authorization: `Bearer ${key}` })
275+
.get(`/branches/${branch}/apis`)
276+
.reply(200, { data: [] });
277+
206278
const result = await run(['--branch', branch, filename, '--key', key, '--slug', customSlug]);
207279

208280
expect(result).toMatchSnapshot();
281+
282+
mock.done();
209283
});
210284
});
211285

@@ -390,6 +464,20 @@ describe('rdme openapi upload', () => {
390464

391465
mock.done();
392466
});
467+
468+
it('should error out if an object ID is passed and there is a legacy match found', async () => {
469+
const customSlug = '687855c3600c6e14c79a94cb';
470+
const existingFilename = 'legacy-spec.json';
471+
const mock = getAPIv2MockForGHA({ authorization: `Bearer ${key}` })
472+
.get(`/branches/${branch}/apis`)
473+
.reply(200, { data: [{ legacy_id: customSlug, filename: existingFilename }] });
474+
475+
const result = await run(['--branch', branch, filename, '--key', key, '--slug', customSlug]);
476+
477+
expect(result).toMatchSnapshot();
478+
479+
mock.done();
480+
});
393481
});
394482

395483
describe('given that the `--branch` flag is not set', () => {
@@ -578,7 +666,7 @@ describe('rdme openapi upload', () => {
578666
putMock.done();
579667
});
580668

581-
it('should error if no matching API definition found', async () => {
669+
it('should error if no matching API definition found (and there are no existing definitions)', async () => {
582670
const legacyId = '1234567890';
583671
prompts.inject([true]);
584672
const getMock = getAPIv2Mock({ authorization: `Bearer ${key}` })
@@ -591,5 +679,41 @@ describe('rdme openapi upload', () => {
591679

592680
getMock.done();
593681
});
682+
683+
it('should error if no matching API definition found (and there is a single existing definition)', async () => {
684+
const legacyId = '1234567890';
685+
prompts.inject([true]);
686+
const getMock = getAPIv2Mock({ authorization: `Bearer ${key}` })
687+
.get(`/branches/${branch}/apis`)
688+
.reply(200, {
689+
data: [{ legacy_id: 'some-mismatching-id1', filename: 'file1.json' }],
690+
});
691+
692+
const result = await run(['--branch', branch, filename, '--key', key, '--legacy-id', legacyId]);
693+
694+
expect(result).toMatchSnapshot();
695+
696+
getMock.done();
697+
});
698+
699+
it('should error if no matching API definition found (and there are several existing definitions)', async () => {
700+
const legacyId = '1234567890';
701+
prompts.inject([true]);
702+
const getMock = getAPIv2Mock({ authorization: `Bearer ${key}` })
703+
.get(`/branches/${branch}/apis`)
704+
.reply(200, {
705+
data: [
706+
{ legacy_id: 'some-mismatching-id1', filename: 'file1.json' },
707+
{ legacy_id: 'some-mismatching-id2', filename: 'file2.json' },
708+
{ legacy_id: 'some-mismatching-id3', filename: 'file3.json' },
709+
],
710+
});
711+
712+
const result = await run(['--branch', branch, filename, '--key', key, '--legacy-id', legacyId]);
713+
714+
expect(result).toMatchSnapshot();
715+
716+
getMock.done();
717+
});
594718
});
595719
});

documentation/commands/openapi.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,9 @@ DESCRIPTION
212212
If the spec is a URL, the inferred slug is the base file name from the URL (e.g., the slug for
213213
`https://example.com/docs/petstore.json` will be `petstore.json`).
214214
215+
For the best and most explicit results, we recommend using the `--slug` flag to set a slug for your API definition,
216+
especially if you're managing many API definitions at scale.
217+
215218
EXAMPLES
216219
You can pass in a file name like so:
217220

documentation/migration-guide.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,11 @@ If you're using the `rdme` GitHub Action, update your GitHub Actions workflow fi
120120
If you previously uploaded API definitions to ReadMe via `rdme openapi`, the command is now `rdme openapi upload`. There are now two main updates:
121121
- Like the Markdown uploading commands above, there is no prompt to select your ReadMe project version if you omit the `--version` flag. It now defaults to `stable` (i.e., your main ReadMe project version). This flag has also been renamed to `--branch`.
122122

123-
- The flag paradigms have been simplified based on community feedback. Previously with `openapi`, the `--id` flag was a hexadecimal object ID that required an initial upload to ReadMe, which made it difficult to upsert API definitions and manage many at scale (and workarounds were added after the fact in the form of the `--create` and `--update` flags). With `openapi upload`, the `--id` flag has been renamed to `--slug` and is now optional. The slug (i.e., the unique identifier for your API definition resource in ReadMe) is inferred from the file path or URL to your API definition.
123+
- The flag paradigms have been simplified based on community feedback. With the previous command, the `--id` flag was a hexadecimal object ID (e.g., `687855c3600c6e14c79a94cb`). These IDs lacked readability and required an initial upload to ReadMe to obtain, which made it difficult to upsert API definitions and manage many at scale. With `openapi upload` and ReadMe Refactored, **the `--id` flag has been removed in favor of `--slug`**. Slugs are now the unique identifier for an API definition resource in ReadMe.
124+
125+
Unlike the object ID paradigm that was previously used with `--id`, the `--slug` flag enables you to define a slug during initial upload. While we encourage you to use this flag and especially encourage slugs that are human-readable and descriptive, `--slug` is technically optional. When omitted, your API definition's slug is inferred from the file path or URL to your API definition.
126+
127+
If you've migrated your project from our legacy platform and you'd like to continue syncing any previously uploaded API definitions via `rdme`, we recommend either 1) looking at the post-migration file name in ReadMe and using the `--slug` flag to refer to that (recommended!) or 2) passing the legacy object ID reference via the `--legacy-id` flag (this is a hidden flag and should not be used in new workflows).
124128

125129
Read more in [the `rdme openapi upload` command docs](https://github.com/readmeio/rdme/blob/v10/documentation/commands/openapi.md#rdme-openapi-upload-spec) and in [the ReadMe API migration guide](https://docs.readme.com/main/reference/api-migration-guide).
126130

0 commit comments

Comments
 (0)