Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
0d907a2
Diff sidebar (#607)
oscarlund121 Dec 8, 2025
fa9046b
Add scroll-to-change navigation from diff sidebar (#634)
ulrikandersen Dec 11, 2025
fbf2e22
Add has-changes indicator for specs in PR branches (#633)
ulrikandersen Dec 11, 2025
0a11200
style: improve diff sidebar layout and colors (#635)
ulrikandersen Dec 11, 2025
1f3f873
fix: pass hasChanges to spec selector for change indicator (#636)
ulrikandersen Dec 11, 2025
5524d29
fix: scroll to change works without operationId for SwaggerUI and Red…
ulrikandersen Dec 11, 2025
04a7219
Bump next from 16.0.7 to 16.0.10 (#638)
dependabot[bot] Dec 12, 2025
dbbef79
fix: refresh projects when window regains focus (#639)
ulrikandersen Dec 12, 2025
5e00ecc
feat: add loading indicator when refreshing projects (#640)
ulrikandersen Dec 12, 2025
dc61995
fix: preserve scroll position when projects refresh
ulrikandersen Dec 19, 2025
f897e1f
Bump the react group with 2 updates
dependabot[bot] Jan 1, 2026
0f61d0c
Bump the everything-else group with 17 updates
dependabot[bot] Jan 1, 2026
a1bbbc7
Merge pull request #643 from shapehq/dependabot/npm_and_yarn/react-60…
simonbs Jan 5, 2026
d37070e
Merge branch 'develop' into dependabot/npm_and_yarn/everything-else-9…
simonbs Jan 5, 2026
816579e
Merge pull request #644 from shapehq/dependabot/npm_and_yarn/everythi…
simonbs Jan 5, 2026
f99db18
Merge pull request #645 from shapehq/hotfix/preserve-scroll-on-projec…
ulrikandersen Jan 5, 2026
dfb844c
Rename project configuration filename in .env.example
simonbs Jan 5, 2026
af9cdcf
Fixes loading screen shown below documentation
simonbs Jan 5, 2026
c23a828
Checks for version and specification
simonbs Jan 5, 2026
6e06dce
Merge pull request #649 from shapehq/bugfix/loading-screen-shown-belo…
simonbs Jan 5, 2026
fc9a9da
Merge branch 'develop' into update-env-example
ulrikandersen Jan 5, 2026
b7296dc
Merge pull request #647 from shapehq/update-env-example
ulrikandersen Jan 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ FRAMNA_DOCS_BASE_URL=http://localhost:3000
FRAMNA_DOCS_TITLE=Framna Docs
FRAMNA_DOCS_DESCRIPTION=Documentation for Framna's APIs
FRAMNA_DOCS_HELP_URL=https://github.com/shapehq/framna-docs/wiki
FRAMNA_DOCS_PROJECT_CONFIGURATION_FILENAME=.shape-docs.yml
FRAMNA_DOCS_PROJECT_CONFIGURATION_FILENAME=.framna-docs.yml
NEXTAUTH_URL=https://docs.example.com
NEXTAUTH_SECRET=use [openssl rand -base64 32] to generate a 32 bytes value
REDIS_URL=localhost
Expand All @@ -24,3 +24,4 @@ GITHUB_APP_ID=123456
GITHUB_PRIVATE_KEY_BASE_64=base 64 encoded version of the private key - see README.md for more info
ENCRYPTION_PUBLIC_KEY_BASE_64=base 64 encoded version of the public key
ENCRYPTION_PRIVATE_KEY_BASE_64=base 64 encoded version of the private key
NEXT_PUBLIC_ENABLE_DIFF_SIDEBAR=true
2 changes: 1 addition & 1 deletion .github/workflows/check-changes-to-env.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: Check Changes to Env
permissions:
contents: read
pull-requests: read
pull-requests: write
issues: write
on:
pull_request:
Expand Down
7 changes: 7 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
FROM node:24-alpine AS base

FROM base AS oasdiff
ARG OASDIFF_VERSION=2.10.0
RUN apk add --no-cache curl tar ca-certificates
RUN curl -fsSL https://raw.githubusercontent.com/oasdiff/oasdiff/main/install.sh \
| sh -s -- -b /usr/local/bin "v${OASDIFF_VERSION}"

# Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
Expand Down Expand Up @@ -46,6 +52,7 @@ RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

COPY --from=builder /app/public ./public
COPY --from=oasdiff /usr/local/bin/oasdiff /usr/local/bin/oasdiff

# Set the correct permission for prerender cache
RUN mkdir .next
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,17 @@ Please refer to the following articles in [the wiki](https://github.com/shapehq/
- [Updating Documentation](https://github.com/shapehq/framna-docs/wiki/Updating-Documentation)
- [Deploying Framna Docs](https://github.com/shapehq/framna-docs/wiki/Deploying-Framna-Docs)

### Install the OpenAPI diff tool locally

Framna Docs relies on the [`oasdiff`](https://github.com/oasdiff/oasdiff) CLI when comparing specifications.

On MacOS you can install with homebrew:

```bash
brew tap oasdiff/homebrew-oasdiff
brew install oasdiff
```

## 👨‍🔧 How does it work?

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:
Expand Down
142 changes: 142 additions & 0 deletions __test__/diff/OasDiffCalculator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { OasDiffCalculator } from "../../src/features/diff/data/OasDiffCalculator"
import IGitHubClient from "../../src/common/github/IGitHubClient"

const createMockGitHubClient = (
baseUrl: string,
headUrl: string,
mergeBaseSha = "abc123"
): IGitHubClient => ({
async compareCommitsWithBasehead() {
return { mergeBaseSha }
},
async getRepositoryContent(request) {
if (request.ref === mergeBaseSha) {
return { downloadURL: baseUrl }
}
return { downloadURL: headUrl }
},
async graphql() {
return {}
},
async getPullRequestFiles() {
return []
},
async getPullRequestComments() {
return []
},
async addCommentToPullRequest() {},
async updatePullRequestComment() {}
})

test("It rejects non-GitHub URLs for base spec", async () => {
const mockGitHubClient = createMockGitHubClient(
"https://malicious-site.com/file.yaml",
"https://raw.githubusercontent.com/owner/repo/main/file.yaml"
)
const calculator = new OasDiffCalculator(mockGitHubClient)

await expect(
calculator.calculateDiff("owner", "repo", "path.yaml", "base", "head")
).rejects.toThrow("Invalid URL for base spec")
})

test("It rejects invalid URLs", async () => {
const mockGitHubClient = createMockGitHubClient(
"not-a-valid-url",
"https://raw.githubusercontent.com/owner/repo/main/file.yaml"
)
const calculator = new OasDiffCalculator(mockGitHubClient)

await expect(
calculator.calculateDiff("owner", "repo", "path.yaml", "base", "head")
).rejects.toThrow("Invalid URL for base spec")
})

test("It accepts raw.githubusercontent.com URLs", async () => {
const mockGitHubClient = createMockGitHubClient(
"https://raw.githubusercontent.com/owner/repo/main/file1.yaml",
"https://raw.githubusercontent.com/owner/repo/main/file2.yaml"
)
const calculator = new OasDiffCalculator(mockGitHubClient)

// This will fail when trying to execute oasdiff, but that's expected
// We're only testing that URL validation passes
await expect(
calculator.calculateDiff("owner", "repo", "path.yaml", "base", "head")
).rejects.toThrow("Failed to execute OpenAPI diff tool")
})

test("It accepts github.com URLs", async () => {
const mockGitHubClient = createMockGitHubClient(
"https://github.com/owner/repo/raw/main/file1.yaml",
"https://github.com/owner/repo/raw/main/file2.yaml"
)
const calculator = new OasDiffCalculator(mockGitHubClient)

// This will fail when trying to execute oasdiff, but that's expected
// We're only testing that URL validation passes
await expect(
calculator.calculateDiff("owner", "repo", "path.yaml", "base", "head")
).rejects.toThrow("Failed to execute OpenAPI diff tool")
})

test("It accepts api.github.com URLs", async () => {
const mockGitHubClient = createMockGitHubClient(
"https://api.github.com/repos/owner/repo/contents/file1.yaml",
"https://api.github.com/repos/owner/repo/contents/file2.yaml"
)
const calculator = new OasDiffCalculator(mockGitHubClient)

// This will fail when trying to execute oasdiff, but that's expected
// We're only testing that URL validation passes
await expect(
calculator.calculateDiff("owner", "repo", "path.yaml", "base", "head")
).rejects.toThrow("Failed to execute OpenAPI diff tool")
})

test("It rejects URLs with GitHub-like subdomains but different domains", async () => {
const mockGitHubClient = createMockGitHubClient(
"https://raw.githubusercontent.com.evil.com/file.yaml",
"https://raw.githubusercontent.com/owner/repo/main/file.yaml"
)
const calculator = new OasDiffCalculator(mockGitHubClient)

await expect(
calculator.calculateDiff("owner", "repo", "path.yaml", "base", "head")
).rejects.toThrow("Invalid URL for base spec")
})

test("It validates both base and head URLs", async () => {
const mockGitHubClient = createMockGitHubClient(
"https://raw.githubusercontent.com/owner/repo/main/file1.yaml",
"https://malicious-site.com/file.yaml"
)
const calculator = new OasDiffCalculator(mockGitHubClient)

await expect(
calculator.calculateDiff("owner", "repo", "path.yaml", "base", "head")
).rejects.toThrow("Invalid URL for head spec")
})

test("It returns empty changes when comparing same refs", async () => {
const mockGitHubClient = createMockGitHubClient(
"https://raw.githubusercontent.com/owner/repo/main/file1.yaml",
"https://raw.githubusercontent.com/owner/repo/main/file2.yaml",
"abc123"
)
const calculator = new OasDiffCalculator(mockGitHubClient)

const result = await calculator.calculateDiff(
"owner",
"repo",
"path.yaml",
"abc123",
"abc123"
)

expect(result).toEqual({
from: "abc123",
to: "abc123",
changes: []
})
})
7 changes: 7 additions & 0 deletions __test__/projects/GitHubProjectDataSource.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -845,11 +845,13 @@ test("It adds remote versions from the project configuration", async () => {
id: "huey",
name: "Huey",
url: `/api/remotes/${base64RemoteConfigEncoder.encode({ url: "https://example.com/huey.yml" })}`,
urlHash: "89ba381286214eec",
isDefault: false
}, {
id: "dewey",
name: "Dewey",
url: `/api/remotes/${base64RemoteConfigEncoder.encode({ url: "https://example.com/dewey.yml" })}`,
urlHash: "8f810fff152505f6",
isDefault: false
}]
}, {
Expand All @@ -860,6 +862,7 @@ test("It adds remote versions from the project configuration", async () => {
id: "louie",
name: "Louie",
url: `/api/remotes/${base64RemoteConfigEncoder.encode({ url: "https://example.com/louie.yml" })}`,
urlHash: "b83ebf43ceede6bc",
isDefault: false
}]
}])
Expand Down Expand Up @@ -925,6 +928,7 @@ test("It modifies ID of remote version if the ID already exists", async () => {
id: "baz",
name: "Baz",
url: `/api/remotes/${base64RemoteConfigEncoder.encode({ url: "https://example.com/baz.yml" })}`,
urlHash: "25cb42ff63570cb5",
isDefault: false
}]
}, {
Expand All @@ -935,6 +939,7 @@ test("It modifies ID of remote version if the ID already exists", async () => {
id: "hello",
name: "Hello",
url: `/api/remotes/${base64RemoteConfigEncoder.encode({ url: "https://example.com/hello.yml" })}`,
urlHash: "d078bd689699d1f0",
isDefault: false
}]
}])
Expand Down Expand Up @@ -979,6 +984,7 @@ test("It lets users specify the ID of a remote version", async () => {
id: "baz",
name: "Baz",
url: `/api/remotes/${base64RemoteConfigEncoder.encode({ url: "https://example.com/baz.yml" })}`,
urlHash: "25cb42ff63570cb5",
isDefault: false
}]
}])
Expand Down Expand Up @@ -1023,6 +1029,7 @@ test("It lets users specify the ID of a remote specification", async () => {
id: "some-spec",
name: "Baz",
url: `/api/remotes/${base64RemoteConfigEncoder.encode({ url: "https://example.com/baz.yml" })}`,
urlHash: "25cb42ff63570cb5",
isDefault: false
}]
}])
Expand Down
Loading