Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 4 additions & 8 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,6 @@
# TypeScript incremental build info
*.tsbuildinfo

# Early access images from docs-early-access repo
assets/images/early-access/

# Accidentally committed file that should be ignored
assets/images/help/writing/unordered-list-rendered (1).png

Expand All @@ -52,15 +49,14 @@ blc_output_internal.log
# Old broken links report
broken_links.md

# Early access content from docs-early-access repo
content/early-access/
# Directories from the docs-early-access repo. Used for symlinks in local docs-internal checkouts. Don't add trailing slashes.
content/early-access
data/early-access
assets/images/early-access

# Test coverage reports
coverage/

# Early access data from docs-early-access repo
data/early-access/

# Cloned for Elasticsearch indexing data
docs-internal-data/

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ To use a {% data variables.product.prodname_ghe_server %} instance, you must upl
There are two types of {% data variables.product.prodname_enterprise %} (GHE) licensing models, with different processes for enabling combined use of {% data variables.product.prodname_ghe_cloud %} and {% data variables.product.prodname_ghe_server %}.

* **GHE (Usage-based, also called metered)**: A cloud-first license where users must first be assigned to a {% data variables.product.prodname_ghe_cloud %} organization.
* All Cloud users automatically receive a use right for {% data variables.product.prodname_ghe_server %}.
* All Cloud users automatically receive a right to use {% data variables.product.prodname_ghe_server %}.
* Billing is based on the number of active users each month.
* Users can generate their own Server license, which covers the number of assigned Cloud seats at the time of generation and is valid for one year.
* Server-only users will be added to GHE (Metered) billing. These users are de-duplicated with email matching to avoid double billing.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,6 @@ Enterprise **owners** or **billing managers** can add or remove user licenses.

1. Navigate to your enterprise account.
{% data reusables.billing.enterprise-billing-menu %}
1. In the left sidebar, click **Licensing**.
1. In the left sidebar, click {% octicon "law" aria-hidden="true" aria-label="law" %} **Licensing**.
1. Next to "Enterprise Cloud", click **{% octicon "kebab-horizontal" aria-hidden="true" aria-label="kebab-horizontal" %}**, then click **Manage licenses**.
1. Choose your number of licenses, then click **Confirm licenses**.
2 changes: 1 addition & 1 deletion data/reusables/billing/usage-based-billing.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{% ifversion enhanced-billing-platform %}

> [!NOTE] If you currently pay for your {% data variables.product.prodname_enterprise %} licenses through a volume, subscription, or prepaid agreement, you will continue to be billed in this way until your agreement expires. At renewal, you have the option to switch to the metered billing model.
> [!NOTE] If you currently pay for your {% data variables.product.prodname_enterprise %} licenses through a volume, subscription, or prepaid agreement, you will continue to be billed in this way until your agreement expires or you are invited to transition. At renewal, you have the option to switch to the metered billing model.

{% endif %}
2 changes: 2 additions & 0 deletions src/frame/middleware/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import { MAX_REQUEST_TIMEOUT } from '@/frame/lib/constants'
import { initLoggerContext } from '@/observability/logger/lib/logger-context'
import { getAutomaticRequestLogger } from '@/observability/logger/middleware/get-automatic-request-logger'
import appRouterGateway from './app-router-gateway'
import urlDecode from './url-decode'

const { NODE_ENV } = process.env
const isTest = NODE_ENV === 'test' || process.env.GITHUB_ACTIONS === 'true'
Expand Down Expand Up @@ -199,6 +200,7 @@ export default function (app: Express) {
app.set('etag', false) // We will manage our own ETags if desired

// *** Config and context for redirects ***
app.use(urlDecode) // Must come before detectLanguage to decode @ symbols in version segments
app.use(detectLanguage) // Must come before context, breadcrumbs, find-page, handle-errors, homepages
app.use(asyncMiddleware(reloadTree)) // Must come before context
app.use(asyncMiddleware(context)) // Must come before early-access-*, handle-redirects
Expand Down
28 changes: 28 additions & 0 deletions src/frame/middleware/url-decode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { NextFunction, Response } from 'express'
import type { ExtendedRequest } from '@/types'

/**
* Middleware to decode URL-encoded @ symbols.
*
* SharePoint and other systems automatically encode @ symbols to %40,
* which breaks our versioned URLs like /en/enterprise-cloud@latest.
* This middleware decodes @ symbols anywhere in the URL.
*/
export default function urlDecode(req: ExtendedRequest, res: Response, next: NextFunction) {
const originalUrl = req.url

// Only process URLs that contain %40 (encoded @)
if (!originalUrl.includes('%40')) {
return next()
}

try {
// Decode the entire URL, replacing %40 with @
const decodedUrl = originalUrl.replace(/%40/g, '@')
req.url = decodedUrl
return next()
} catch {
// If decoding fails for any reason, continue with original URL
return next()
}
}
72 changes: 72 additions & 0 deletions src/frame/tests/url-encoding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { describe, expect, test } from 'vitest'
import { get } from '@/tests/helpers/e2etest'

describe('URL encoding for version paths', () => {
test('handles URL-encoded @ symbol in enterprise-cloud version', async () => {
// SharePoint encodes @ as %40, so /en/enterprise-cloud@latest becomes /en/enterprise-cloud%40latest
const encodedUrl = '/en/enterprise-cloud%40latest/copilot/concepts/chat'
const res = await get(encodedUrl)

// Should either:
// 1. Work directly (200) - the encoded URL should decode and work
// 2. Redirect (301/302) to the proper decoded URL
// Should NOT return 404
expect([200, 301, 302]).toContain(res.statusCode)

if (res.statusCode === 301 || res.statusCode === 302) {
// If it redirects, it should redirect to the decoded version
expect(res.headers.location).toBe('/en/enterprise-cloud@latest/copilot/concepts/chat')
}
})

test('handles URL-encoded @ symbol in enterprise-server version', async () => {
const encodedUrl =
'/en/enterprise-server%403.17/admin/managing-github-actions-for-your-enterprise'
const res = await get(encodedUrl)

expect([200, 301, 302]).toContain(res.statusCode)

if (res.statusCode === 301 || res.statusCode === 302) {
expect(res.headers.location).toBe(
'/en/[email protected]/admin/managing-github-actions-for-your-enterprise',
)
}
})

test('handles URL-encoded @ symbol in second path segment', async () => {
// When no language prefix is present
const encodedUrl = '/enterprise-cloud%40latest/copilot/concepts/chat'
const res = await get(encodedUrl)

// Should redirect to add language prefix and decode
expect([301, 302]).toContain(res.statusCode)
expect(res.headers.location).toBe('/en/enterprise-cloud@latest/copilot/concepts/chat')
})

test('normal @ symbol paths continue to work', async () => {
// Ensure we don't break existing functionality
const normalUrl = '/en/enterprise-cloud@latest/copilot/concepts/chat'
const res = await get(normalUrl)

expect(res.statusCode).toBe(200)
})

test('URL encoding in other parts of URL is preserved', async () => {
// Only @ symbols in version paths should be decoded, other encoding should be preserved
const encodedUrl = '/en/enterprise-cloud@latest/copilot/concepts/some%20page'
const res = await get(encodedUrl)

// This might 404 if the page doesn't exist, but shouldn't break due to encoding
expect(res.statusCode).not.toBe(500)
})

test('Express URL properties are correctly updated after decoding', async () => {
// Test that req.path, req.query, etc. are properly updated when req.url is modified
const encodedUrl = '/en/enterprise-cloud%40latest/copilot/concepts/chat?test=value'
const res = await get(encodedUrl)

// Should work correctly (200 or redirect) - the middleware should properly update
// req.path from '/en/enterprise-cloud%40latest/...' to '/en/enterprise-cloud@latest/...'
expect([200, 301, 302]).toContain(res.statusCode)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -962,21 +962,48 @@
"permissions": [
{
"category": "orgs",
"slug": "list-custom-property-values-for-organization-repositories",
"slug": "get-all-custom-properties-for-an-organization",
"subcategory": "custom-properties",
"verb": "get",
"requestPath": "/orgs/{org}/properties/values",
"requestPath": "/orgs/{org}/properties/schema",
"additional-permissions": false,
"access": "read"
},
{
"category": "orgs",
"slug": "create-or-update-custom-property-values-for-organization-repositories",
"slug": "create-or-update-custom-properties-for-an-organization",
"subcategory": "custom-properties",
"verb": "patch",
"requestPath": "/orgs/{org}/properties/values",
"requestPath": "/orgs/{org}/properties/schema",
"additional-permissions": false,
"access": "write"
"access": "admin"
},
{
"category": "orgs",
"slug": "get-a-custom-property-for-an-organization",
"subcategory": "custom-properties",
"verb": "get",
"requestPath": "/orgs/{org}/properties/schema/{custom_property_name}",
"additional-permissions": false,
"access": "read"
},
{
"category": "orgs",
"slug": "create-or-update-a-custom-property-for-an-organization",
"subcategory": "custom-properties",
"verb": "put",
"requestPath": "/orgs/{org}/properties/schema/{custom_property_name}",
"additional-permissions": false,
"access": "admin"
},
{
"category": "orgs",
"slug": "remove-a-custom-property-for-an-organization",
"subcategory": "custom-properties",
"verb": "delete",
"requestPath": "/orgs/{org}/properties/schema/{custom_property_name}",
"additional-permissions": false,
"access": "admin"
}
]
},
Expand Down
26 changes: 22 additions & 4 deletions src/github-apps/data/fpt-2022-11-28/fine-grained-pat.json
Original file line number Diff line number Diff line change
Expand Up @@ -3349,16 +3349,34 @@
"requestPath": "/orgs/{org}/outside_collaborators/{username}"
},
{
"slug": "list-custom-property-values-for-organization-repositories",
"slug": "get-all-custom-properties-for-an-organization",
"subcategory": "custom-properties",
"verb": "get",
"requestPath": "/orgs/{org}/properties/values"
"requestPath": "/orgs/{org}/properties/schema"
},
{
"slug": "create-or-update-custom-property-values-for-organization-repositories",
"slug": "create-or-update-custom-properties-for-an-organization",
"subcategory": "custom-properties",
"verb": "patch",
"requestPath": "/orgs/{org}/properties/values"
"requestPath": "/orgs/{org}/properties/schema"
},
{
"slug": "get-a-custom-property-for-an-organization",
"subcategory": "custom-properties",
"verb": "get",
"requestPath": "/orgs/{org}/properties/schema/{custom_property_name}"
},
{
"slug": "create-or-update-a-custom-property-for-an-organization",
"subcategory": "custom-properties",
"verb": "put",
"requestPath": "/orgs/{org}/properties/schema/{custom_property_name}"
},
{
"slug": "remove-a-custom-property-for-an-organization",
"subcategory": "custom-properties",
"verb": "delete",
"requestPath": "/orgs/{org}/properties/schema/{custom_property_name}"
},
{
"slug": "list-public-organization-members",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1168,22 +1168,55 @@
"permissions": [
{
"category": "orgs",
"slug": "list-custom-property-values-for-organization-repositories",
"slug": "get-all-custom-properties-for-an-organization",
"subcategory": "custom-properties",
"verb": "get",
"requestPath": "/orgs/{org}/properties/values",
"requestPath": "/orgs/{org}/properties/schema",
"access": "read",
"user-to-server": true,
"server-to-server": true,
"additional-permissions": false
},
{
"category": "orgs",
"slug": "create-or-update-custom-property-values-for-organization-repositories",
"slug": "create-or-update-custom-properties-for-an-organization",
"subcategory": "custom-properties",
"verb": "patch",
"requestPath": "/orgs/{org}/properties/values",
"access": "write",
"requestPath": "/orgs/{org}/properties/schema",
"access": "admin",
"user-to-server": true,
"server-to-server": true,
"additional-permissions": false
},
{
"category": "orgs",
"slug": "get-a-custom-property-for-an-organization",
"subcategory": "custom-properties",
"verb": "get",
"requestPath": "/orgs/{org}/properties/schema/{custom_property_name}",
"access": "read",
"user-to-server": true,
"server-to-server": true,
"additional-permissions": false
},
{
"category": "orgs",
"slug": "create-or-update-a-custom-property-for-an-organization",
"subcategory": "custom-properties",
"verb": "put",
"requestPath": "/orgs/{org}/properties/schema/{custom_property_name}",
"access": "admin",
"user-to-server": true,
"server-to-server": true,
"additional-permissions": false
},
{
"category": "orgs",
"slug": "remove-a-custom-property-for-an-organization",
"subcategory": "custom-properties",
"verb": "delete",
"requestPath": "/orgs/{org}/properties/schema/{custom_property_name}",
"access": "admin",
"user-to-server": true,
"server-to-server": true,
"additional-permissions": false
Expand Down
26 changes: 22 additions & 4 deletions src/github-apps/data/fpt-2022-11-28/server-to-server-rest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3303,16 +3303,34 @@
"requestPath": "/orgs/{org}/personal-access-tokens/{pat_id}/repositories"
},
{
"slug": "list-custom-property-values-for-organization-repositories",
"slug": "get-all-custom-properties-for-an-organization",
"subcategory": "custom-properties",
"verb": "get",
"requestPath": "/orgs/{org}/properties/values"
"requestPath": "/orgs/{org}/properties/schema"
},
{
"slug": "create-or-update-custom-property-values-for-organization-repositories",
"slug": "create-or-update-custom-properties-for-an-organization",
"subcategory": "custom-properties",
"verb": "patch",
"requestPath": "/orgs/{org}/properties/values"
"requestPath": "/orgs/{org}/properties/schema"
},
{
"slug": "get-a-custom-property-for-an-organization",
"subcategory": "custom-properties",
"verb": "get",
"requestPath": "/orgs/{org}/properties/schema/{custom_property_name}"
},
{
"slug": "create-or-update-a-custom-property-for-an-organization",
"subcategory": "custom-properties",
"verb": "put",
"requestPath": "/orgs/{org}/properties/schema/{custom_property_name}"
},
{
"slug": "remove-a-custom-property-for-an-organization",
"subcategory": "custom-properties",
"verb": "delete",
"requestPath": "/orgs/{org}/properties/schema/{custom_property_name}"
},
{
"slug": "list-public-organization-members",
Expand Down
Loading
Loading