Skip to content

Commit 0d907a2

Browse files
Diff sidebar (#607)
Add diff sidebar Shows changes to OpenAPI spec between two branches using the oasdiff tool. --------- Co-authored-by: Ulrik Andersen <ulrik@shape.dk>
1 parent 092f4ed commit 0d907a2

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1582
-121
lines changed

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,4 @@ GITHUB_APP_ID=123456
2424
GITHUB_PRIVATE_KEY_BASE_64=base 64 encoded version of the private key - see README.md for more info
2525
ENCRYPTION_PUBLIC_KEY_BASE_64=base 64 encoded version of the public key
2626
ENCRYPTION_PRIVATE_KEY_BASE_64=base 64 encoded version of the private key
27+
NEXT_PUBLIC_ENABLE_DIFF_SIDEBAR=true

.github/workflows/check-changes-to-env.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name: Check Changes to Env
22
permissions:
33
contents: read
4-
pull-requests: read
4+
pull-requests: write
55
issues: write
66
on:
77
pull_request:

Dockerfile

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
FROM node:24-alpine AS base
22

3+
FROM base AS oasdiff
4+
ARG OASDIFF_VERSION=2.10.0
5+
RUN apk add --no-cache curl tar ca-certificates
6+
RUN curl -fsSL https://raw.githubusercontent.com/oasdiff/oasdiff/main/install.sh \
7+
| sh -s -- -b /usr/local/bin "v${OASDIFF_VERSION}"
8+
39
# Install dependencies only when needed
410
FROM base AS deps
511
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
@@ -46,6 +52,7 @@ RUN addgroup --system --gid 1001 nodejs
4652
RUN adduser --system --uid 1001 nextjs
4753

4854
COPY --from=builder /app/public ./public
55+
COPY --from=oasdiff /usr/local/bin/oasdiff /usr/local/bin/oasdiff
4956

5057
# Set the correct permission for prerender cache
5158
RUN mkdir .next

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,17 @@ Please refer to the following articles in [the wiki](https://github.com/shapehq/
4141
- [Updating Documentation](https://github.com/shapehq/framna-docs/wiki/Updating-Documentation)
4242
- [Deploying Framna Docs](https://github.com/shapehq/framna-docs/wiki/Deploying-Framna-Docs)
4343

44+
### Install the OpenAPI diff tool locally
45+
46+
Framna Docs relies on the [`oasdiff`](https://github.com/oasdiff/oasdiff) CLI when comparing specifications.
47+
48+
On MacOS you can install with homebrew:
49+
50+
```bash
51+
brew tap oasdiff/homebrew-oasdiff
52+
brew install oasdiff
53+
```
54+
4455
## 👨‍🔧 How does it work?
4556

4657
Framna Docs uses [OpenAPI specifications](https://swagger.io) from GitHub repositories. Users log in with their GitHub account to access documentation for projects they have access to. A repository only needs an OpenAPI spec to be recognized by Framna Docs, but customization is possible with a .framna-docs.yml file. Here's an example:
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import { OasDiffCalculator } from "../../src/features/diff/data/OasDiffCalculator"
2+
import IGitHubClient from "../../src/common/github/IGitHubClient"
3+
4+
const createMockGitHubClient = (
5+
baseUrl: string,
6+
headUrl: string,
7+
mergeBaseSha = "abc123"
8+
): IGitHubClient => ({
9+
async compareCommitsWithBasehead() {
10+
return { mergeBaseSha }
11+
},
12+
async getRepositoryContent(request) {
13+
if (request.ref === mergeBaseSha) {
14+
return { downloadURL: baseUrl }
15+
}
16+
return { downloadURL: headUrl }
17+
},
18+
async graphql() {
19+
return {}
20+
},
21+
async getPullRequestFiles() {
22+
return []
23+
},
24+
async getPullRequestComments() {
25+
return []
26+
},
27+
async addCommentToPullRequest() {},
28+
async updatePullRequestComment() {}
29+
})
30+
31+
test("It rejects non-GitHub URLs for base spec", async () => {
32+
const mockGitHubClient = createMockGitHubClient(
33+
"https://malicious-site.com/file.yaml",
34+
"https://raw.githubusercontent.com/owner/repo/main/file.yaml"
35+
)
36+
const calculator = new OasDiffCalculator(mockGitHubClient)
37+
38+
await expect(
39+
calculator.calculateDiff("owner", "repo", "path.yaml", "base", "head")
40+
).rejects.toThrow("Invalid URL for base spec")
41+
})
42+
43+
test("It rejects invalid URLs", async () => {
44+
const mockGitHubClient = createMockGitHubClient(
45+
"not-a-valid-url",
46+
"https://raw.githubusercontent.com/owner/repo/main/file.yaml"
47+
)
48+
const calculator = new OasDiffCalculator(mockGitHubClient)
49+
50+
await expect(
51+
calculator.calculateDiff("owner", "repo", "path.yaml", "base", "head")
52+
).rejects.toThrow("Invalid URL for base spec")
53+
})
54+
55+
test("It accepts raw.githubusercontent.com URLs", async () => {
56+
const mockGitHubClient = createMockGitHubClient(
57+
"https://raw.githubusercontent.com/owner/repo/main/file1.yaml",
58+
"https://raw.githubusercontent.com/owner/repo/main/file2.yaml"
59+
)
60+
const calculator = new OasDiffCalculator(mockGitHubClient)
61+
62+
// This will fail when trying to execute oasdiff, but that's expected
63+
// We're only testing that URL validation passes
64+
await expect(
65+
calculator.calculateDiff("owner", "repo", "path.yaml", "base", "head")
66+
).rejects.toThrow("Failed to execute OpenAPI diff tool")
67+
})
68+
69+
test("It accepts github.com URLs", async () => {
70+
const mockGitHubClient = createMockGitHubClient(
71+
"https://github.com/owner/repo/raw/main/file1.yaml",
72+
"https://github.com/owner/repo/raw/main/file2.yaml"
73+
)
74+
const calculator = new OasDiffCalculator(mockGitHubClient)
75+
76+
// This will fail when trying to execute oasdiff, but that's expected
77+
// We're only testing that URL validation passes
78+
await expect(
79+
calculator.calculateDiff("owner", "repo", "path.yaml", "base", "head")
80+
).rejects.toThrow("Failed to execute OpenAPI diff tool")
81+
})
82+
83+
test("It accepts api.github.com URLs", async () => {
84+
const mockGitHubClient = createMockGitHubClient(
85+
"https://api.github.com/repos/owner/repo/contents/file1.yaml",
86+
"https://api.github.com/repos/owner/repo/contents/file2.yaml"
87+
)
88+
const calculator = new OasDiffCalculator(mockGitHubClient)
89+
90+
// This will fail when trying to execute oasdiff, but that's expected
91+
// We're only testing that URL validation passes
92+
await expect(
93+
calculator.calculateDiff("owner", "repo", "path.yaml", "base", "head")
94+
).rejects.toThrow("Failed to execute OpenAPI diff tool")
95+
})
96+
97+
test("It rejects URLs with GitHub-like subdomains but different domains", async () => {
98+
const mockGitHubClient = createMockGitHubClient(
99+
"https://raw.githubusercontent.com.evil.com/file.yaml",
100+
"https://raw.githubusercontent.com/owner/repo/main/file.yaml"
101+
)
102+
const calculator = new OasDiffCalculator(mockGitHubClient)
103+
104+
await expect(
105+
calculator.calculateDiff("owner", "repo", "path.yaml", "base", "head")
106+
).rejects.toThrow("Invalid URL for base spec")
107+
})
108+
109+
test("It validates both base and head URLs", async () => {
110+
const mockGitHubClient = createMockGitHubClient(
111+
"https://raw.githubusercontent.com/owner/repo/main/file1.yaml",
112+
"https://malicious-site.com/file.yaml"
113+
)
114+
const calculator = new OasDiffCalculator(mockGitHubClient)
115+
116+
await expect(
117+
calculator.calculateDiff("owner", "repo", "path.yaml", "base", "head")
118+
).rejects.toThrow("Invalid URL for head spec")
119+
})
120+
121+
test("It returns empty changes when comparing same refs", async () => {
122+
const mockGitHubClient = createMockGitHubClient(
123+
"https://raw.githubusercontent.com/owner/repo/main/file1.yaml",
124+
"https://raw.githubusercontent.com/owner/repo/main/file2.yaml",
125+
"abc123"
126+
)
127+
const calculator = new OasDiffCalculator(mockGitHubClient)
128+
129+
const result = await calculator.calculateDiff(
130+
"owner",
131+
"repo",
132+
"path.yaml",
133+
"abc123",
134+
"abc123"
135+
)
136+
137+
expect(result).toEqual({
138+
from: "abc123",
139+
to: "abc123",
140+
changes: []
141+
})
142+
})

0 commit comments

Comments
 (0)