@@ -9,7 +9,6 @@ versions:
|
||||
children:
|
||||
- /viewing-github-actions-metrics
|
||||
- /making-retired-namespaces-available-on-ghecom
|
||||
- /managing-custom-actions
|
||||
redirect_from:
|
||||
- /actions/administering-github-actions
|
||||
---
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
---
|
||||
title: Developing a third party CLI action
|
||||
shortTitle: CLI setup action
|
||||
title: Creating a third party CLI action
|
||||
shortTitle: Create a CLI action
|
||||
intro: 'Learn how to develop an action to set up a CLI on {% data variables.product.prodname_actions %} runners.'
|
||||
redirect_from:
|
||||
- /actions/creating-actions/developing-a-third-party-cli-action
|
||||
- /actions/sharing-automations/creating-actions/developing-a-third-party-cli-action
|
||||
- /actions/how-tos/sharing-automations/creating-actions/developing-a-third-party-cli-action
|
||||
versions:
|
||||
fpt: '*'
|
||||
ghec: '*'
|
||||
type: tutorial
|
||||
topics:
|
||||
- Actions
|
||||
---
|
||||
@@ -29,7 +29,7 @@ This article will demonstrate how to write an action that retrieves a specific v
|
||||
|
||||
## Prerequisites
|
||||
|
||||
You should have an understanding of how to write a custom action. For more information, see [AUTOTITLE](/actions/creating-actions/about-custom-actions). For a more detailed guide on how to write a custom action, see [AUTOTITLE](/actions/creating-actions/creating-a-javascript-action).
|
||||
You should have an understanding of how to write a custom action. For more information, see [AUTOTITLE](/actions/how-tos/creating-and-publishing-actions/managing-custom-actions).
|
||||
|
||||
## Example
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
---
|
||||
title: Creating actions
|
||||
shortTitle: Create actions
|
||||
title: Creating and publishing actions
|
||||
shortTitle: Create and publish actions
|
||||
intro: 'You can create your own actions, use and customize actions shared by the {% data variables.product.prodname_dotcom %} community, or write and share the actions you build.'
|
||||
versions:
|
||||
fpt: '*'
|
||||
ghes: '*'
|
||||
ghec: '*'
|
||||
children:
|
||||
- /setting-exit-codes-for-actions
|
||||
- /releasing-and-maintaining-actions
|
||||
- /publishing-actions-in-github-marketplace
|
||||
- /developing-a-third-party-cli-action
|
||||
redirect_from:
|
||||
- /actions/sharing-automations/creating-actions
|
||||
- /actions/how-tos/sharing-automations/creating-actions
|
||||
children:
|
||||
- /managing-custom-actions
|
||||
- /creating-a-third-party-cli-action
|
||||
- /setting-exit-codes-for-actions
|
||||
- /publishing-actions-in-github-marketplace
|
||||
- /releasing-and-maintaining-actions
|
||||
---
|
||||
|
||||
{% data reusables.actions.enterprise-github-hosted-runners %}
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
title: Managing custom actions
|
||||
shortTitle: Manage custom actions
|
||||
intro: 'Learn how to create and manage your own actions, and customize actions shared by the {% data variables.product.prodname_dotcom %} community.'
|
||||
versions:
|
||||
fpt: '*'
|
||||
@@ -9,6 +10,8 @@ type: overview
|
||||
topics:
|
||||
- Action development
|
||||
- Fundamentals
|
||||
redirect_from:
|
||||
- /actions/how-tos/administering-github-actions/managing-custom-actions
|
||||
---
|
||||
|
||||
## Choosing a location for your action
|
||||
@@ -7,16 +7,17 @@ redirect_from:
|
||||
- /actions/building-actions/publishing-actions-in-github-marketplace
|
||||
- /actions/creating-actions/publishing-actions-in-github-marketplace
|
||||
- /actions/sharing-automations/creating-actions/publishing-actions-in-github-marketplace
|
||||
- /actions/how-tos/sharing-automations/creating-actions/publishing-actions-in-github-marketplace
|
||||
versions:
|
||||
fpt: '*'
|
||||
ghec: '*'
|
||||
type: how_to
|
||||
shortTitle: Publish in GitHub Marketplace
|
||||
---
|
||||
|
||||
You must accept the terms of service to publish actions in {% data variables.product.prodname_marketplace %}.
|
||||
## Prerequisites
|
||||
|
||||
## About publishing actions
|
||||
>[!NOTE]
|
||||
> You must accept the terms of service to publish actions in {% data variables.product.prodname_marketplace %}.
|
||||
|
||||
Before you can publish an action, you'll need to create an action in your repository. For more information, see [AUTOTITLE](/actions/creating-actions).
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
title: Releasing and maintaining actions
|
||||
shortTitle: Release and maintain actions
|
||||
intro: You can leverage automation and open source best practices to release and maintain actions.
|
||||
type: tutorial
|
||||
topics:
|
||||
- Action development
|
||||
- Actions
|
||||
@@ -14,6 +13,7 @@ versions:
|
||||
redirect_from:
|
||||
- /actions/creating-actions/releasing-and-maintaining-actions
|
||||
- /actions/sharing-automations/creating-actions/releasing-and-maintaining-actions
|
||||
- /actions/how-tos/sharing-automations/creating-actions/releasing-and-maintaining-actions
|
||||
---
|
||||
|
||||
{% data reusables.actions.enterprise-github-hosted-runners %}
|
||||
@@ -6,11 +6,11 @@ redirect_from:
|
||||
- /actions/building-actions/setting-exit-codes-for-actions
|
||||
- /actions/creating-actions/setting-exit-codes-for-actions
|
||||
- /actions/sharing-automations/creating-actions/setting-exit-codes-for-actions
|
||||
- /actions/how-tos/sharing-automations/creating-actions/setting-exit-codes-for-actions
|
||||
versions:
|
||||
fpt: '*'
|
||||
ghes: '*'
|
||||
ghec: '*'
|
||||
type: how_to
|
||||
---
|
||||
|
||||
{% data reusables.actions.enterprise-github-hosted-runners %}
|
||||
@@ -9,6 +9,7 @@ versions:
|
||||
children:
|
||||
- /writing-workflows
|
||||
- /managing-workflow-runs-and-deployments
|
||||
- /creating-and-publishing-actions
|
||||
- /sharing-automations
|
||||
- /using-github-hosted-runners
|
||||
- /managing-self-hosted-runners
|
||||
|
||||
@@ -14,7 +14,6 @@ redirect_from:
|
||||
- /articles/creating-a-github-action
|
||||
- /actions/sharing-automations
|
||||
children:
|
||||
- /creating-actions
|
||||
- /reuse-workflows
|
||||
- /creating-workflow-templates-for-your-organization
|
||||
- /sharing-actions-and-workflows-from-your-private-repository
|
||||
|
||||
@@ -29,6 +29,9 @@ You must authorize your {% data variables.product.pat_v1 %} after creation befor
|
||||
|
||||
1. In the dropdown menu, to the right of the organization you'd like to authorize the token for, click **Authorize**.
|
||||
|
||||
> [!NOTE]
|
||||
> When authorizing a {% data variables.product.pat_v1 %} for use within an organization that belongs to an enterprise which has both an IP allow list and single sign-on enabled at the enterprise level, your IP must also be allowed at the enterprise level. See [AUTOTITLE](/admin/configuring-settings/hardening-security-for-your-enterprise/restricting-network-traffic-to-your-enterprise-with-an-ip-allow-list).
|
||||
|
||||
## Further reading
|
||||
|
||||
* [AUTOTITLE](/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token)
|
||||
|
||||
@@ -37,6 +37,9 @@ You do not need to authorize SSH certificates signed by your organization's SSH
|
||||

|
||||
1. In the dropdown menu, to the right of the organization you'd like to authorize the SSH key for, click **Authorize**.
|
||||
|
||||
> [!NOTE]
|
||||
> When authorizing an SSH key for use within an organization that belongs to an enterprise which has both an IP allow list and single sign-on enabled at the enterprise level, your IP must also be allowed at the enterprise level. See [AUTOTITLE](/admin/configuring-settings/hardening-security-for-your-enterprise/restricting-network-traffic-to-your-enterprise-with-an-ip-allow-list).
|
||||
|
||||
## Further reading
|
||||
|
||||
* [AUTOTITLE](/authentication/connecting-to-github-with-ssh/checking-for-existing-ssh-keys)
|
||||
|
||||
@@ -33,7 +33,13 @@ For specific ecosystems, you can configure {% data variables.product.prodname_de
|
||||
|
||||
## Configuring private registries
|
||||
|
||||
You configure {% data variables.product.prodname_dependabot %}'s access to private registries in the `dependabot.yml` file.
|
||||
{% ifversion org-private-registry %}
|
||||
|
||||
You can configure {% data variables.product.prodname_dependabot %}'s access to private registries at the org-level. For more information on how to configure that, see [AUTOTITLE](/code-security/securing-your-organization/enabling-security-features-in-your-organization/giving-org-access-private-registries).
|
||||
|
||||
{% endif %}
|
||||
|
||||
You can also configure {% data variables.product.prodname_dependabot %}'s access to private registries in the `dependabot.yml` file.
|
||||
The top-level `registries` key is optional and specifies authentication details.
|
||||
|
||||
{% data reusables.dependabot.dependabot-updates-registries %}
|
||||
|
||||
@@ -59,7 +59,7 @@ Any private registries used by the build must also be accessible to the workflow
|
||||
|
||||
## {% data variables.product.prodname_dependabot %} updates access to private registries
|
||||
|
||||
{% data variables.product.prodname_dependabot %} uses any private registries defined in the `dependabot.yml` file. It does not have access to the organization-level private registries used by {% data variables.product.prodname_code_scanning %} default setup.
|
||||
{% data variables.product.prodname_dependabot %} can use any of the org-level private registries, as well as uses any private registries defined in the `dependabot.yml` file in the repo.
|
||||
|
||||
{% data variables.product.prodname_dependabot %} cannot check for security or version updates for code stored in a private registry unless it can access the registry. If you do not configure access to the private registry, then {% data variables.product.prodname_dependabot %} cannot raise pull requests to update any of the dependencies stored in the registry.
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ Content exclusions also apply to {% data variables.copilot.copilot_code-review_s
|
||||
|
||||
{% data reusables.copilot.content-exclusion-limitations %}
|
||||
|
||||
Currently, content exclusions do not apply to symbolic links (symlinks).
|
||||
Currently, content exclusions do not apply to symbolic links (symlinks) and repositories located on remote filesystems.
|
||||
|
||||
### Data sent to {% data variables.product.prodname_dotcom %}
|
||||
|
||||
|
||||
@@ -49,4 +49,6 @@ Used for:
|
||||
|
||||
{% data variables.product.prodname_copilot %} uses {% data variables.copilot.copilot_gemini_flash %} and {% data variables.copilot.copilot_gemini_25_pro %} hosted on Google Cloud Platform (GCP). When using {% data variables.copilot.copilot_gemini %} models, prompts and metadata are sent to GCP, which makes the [following data commitment](https://cloud.google.com/vertex-ai/generative-ai/docs/data-governance): _{% data variables.copilot.copilot_gemini %} doesn't use your prompts, or its responses, as data to train its models._
|
||||
|
||||
To provide better service quality and reduce latency, {% data variables.product.github %} uses [prompt caching](https://cloud.google.com/vertex-ai/generative-ai/docs/data-governance#customer_data_retention_and_achieving_zero_data_retention).
|
||||
|
||||
When using {% data variables.copilot.copilot_gemini %} models, input prompts and output completions continue to run through {% data variables.product.prodname_copilot %}'s content filters for public code matching, when applied, along with those for harmful or offensive content.
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
>[!NOTE] Repository policies are currently in {% data variables.release-phases.public_preview %} and subject to change.
|
||||
>You can have up to 75 total policies and rulesets per organization, and up to 75 total policies and rulesets per enterprise.
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
> [!NOTE]
|
||||
> {% ifversion ghes > 3.16 %}Only changes made to a ruleset after you have upgraded to {% data variables.product.prodname_ghe_server %} 3.17.0, or a later version, are included in the ruleset history.{% endif %}
|
||||
> {% ifversion ghes > 3.16 %}[!NOTE]
|
||||
> Only changes made to a ruleset after you have upgraded to {% data variables.product.prodname_ghe_server %} 3.17.0, or a later version, are included in the ruleset history.{% endif %}
|
||||
|
||||
@@ -2,7 +2,8 @@ If your support request is outside of the scope of what our team can help you wi
|
||||
|
||||
* Third party integrations, such as Jira{% ifversion ghes %}
|
||||
* Hardware setup
|
||||
* Hypervisor-related issues, such as OS disk detection, network setup, boot failure, VM console access, etc{% endif %}
|
||||
* Hypervisor-related issues, such as OS disk detection, network setup, boot failure, VM console access, etc.
|
||||
* Migration assistance between specific hardware platforms or storage providers{% endif %}
|
||||
* CI/CD, such as Jenkins
|
||||
* Azure DevOps (please contact Azure Support)
|
||||
* Writing scripts
|
||||
|
||||
42
package-lock.json
generated
42
package-lock.json
generated
@@ -4783,19 +4783,44 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/request/node_modules/form-data": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz",
|
||||
"integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==",
|
||||
"version": "2.5.5",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz",
|
||||
"integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.6",
|
||||
"mime-types": "^2.1.12"
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.35",
|
||||
"safe-buffer": "^5.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.12"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/request/node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/semver": {
|
||||
"version": "7.5.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
|
||||
@@ -8957,14 +8982,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
|
||||
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -103,11 +103,25 @@ export async function syncGitHubAppsData(openApiSource, sourceSchemas, progAcces
|
||||
|
||||
const excludedActors = progActorResources[permissionName]['excluded_actors']
|
||||
|
||||
const additionalPermissions =
|
||||
progAccessData[operation.operationId].permissions.length > 1 ||
|
||||
progAccessData[operation.operationId].permissions.some(
|
||||
(permissionSet) => Object.keys(permissionSet).length > 1,
|
||||
const additionalPermissions = calculateAdditionalPermissions(
|
||||
progAccessData[operation.operationId].permissions,
|
||||
)
|
||||
|
||||
// Filter out metadata permissions when combined with other permissions
|
||||
// The metadata permission is automatically granted with any other repository permission,
|
||||
// so documenting it for operations that require additional permissions is misleading.
|
||||
// This fixes the issue where mutating operations (PUT, DELETE) incorrectly appeared
|
||||
// to only need metadata access when they actually require write permissions.
|
||||
// See: https://github.com/github/docs-engineering/issues/5212
|
||||
if (
|
||||
shouldFilterMetadataPermission(
|
||||
permissionName,
|
||||
progAccessData[operation.operationId].permissions,
|
||||
)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
// github app permissions
|
||||
if (!isActorExcluded(excludedActors, 'server_to_server', actorTypeMap)) {
|
||||
const serverToServerPermissions = githubAppsData['server-to-server-permissions']
|
||||
@@ -332,6 +346,28 @@ function sentenceCase(str) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates whether an operation has additional permissions beyond a single permission.
|
||||
*/
|
||||
export function calculateAdditionalPermissions(permissionSets) {
|
||||
return (
|
||||
permissionSets.length > 1 ||
|
||||
permissionSets.some((permissionSet) => Object.keys(permissionSet).length > 1)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a metadata permission should be filtered out when it has additional permissions.
|
||||
* Prevents misleading documentation where mutating operations appear to only need metadata access.
|
||||
*/
|
||||
export function shouldFilterMetadataPermission(permissionName, permissionSets) {
|
||||
if (permissionName !== 'metadata') {
|
||||
return false
|
||||
}
|
||||
|
||||
return calculateAdditionalPermissions(permissionSets)
|
||||
}
|
||||
|
||||
export function isActorExcluded(excludedActors, actorType, actorTypeMap = {}) {
|
||||
if (!excludedActors || !Array.isArray(excludedActors)) {
|
||||
return false
|
||||
@@ -358,7 +394,6 @@ export function isActorExcluded(excludedActors, actorType, actorTypeMap = {}) {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function addAppData(storage, category, data) {
|
||||
if (!storage[category]) {
|
||||
storage[category] = []
|
||||
|
||||
308
src/github-apps/tests/metadata-permissions.js
Normal file
308
src/github-apps/tests/metadata-permissions.js
Normal file
@@ -0,0 +1,308 @@
|
||||
import { describe, expect, test } from 'vitest'
|
||||
import { shouldFilterMetadataPermission, calculateAdditionalPermissions } from '../scripts/sync.js'
|
||||
|
||||
describe('metadata permissions filtering', () => {
|
||||
// Mock data structure representing operations with metadata permissions
|
||||
const mockOperationsWithMetadata = [
|
||||
{
|
||||
operationId: 'repos/enable-automated-security-fixes',
|
||||
permissionSets: [{ metadata: 'read', administration: 'write' }],
|
||||
},
|
||||
{
|
||||
operationId: 'repos/get-readme',
|
||||
permissionSets: [{ metadata: 'read' }],
|
||||
},
|
||||
{
|
||||
operationId: 'orgs/update-webhook',
|
||||
permissionSets: [{ metadata: 'read', organization_administration: 'write' }],
|
||||
},
|
||||
{
|
||||
operationId: 'repos/get-content',
|
||||
permissionSets: [{ contents: 'read' }],
|
||||
},
|
||||
]
|
||||
|
||||
// Mock programmatic access data
|
||||
const mockProgAccessData = {
|
||||
'repos/enable-automated-security-fixes': {
|
||||
userToServerRest: true,
|
||||
serverToServer: true,
|
||||
permissions: [{ metadata: 'read', administration: 'write' }],
|
||||
},
|
||||
'repos/get-readme': {
|
||||
userToServerRest: true,
|
||||
serverToServer: true,
|
||||
permissions: [{ metadata: 'read' }],
|
||||
},
|
||||
'orgs/update-webhook': {
|
||||
userToServerRest: true,
|
||||
serverToServer: true,
|
||||
permissions: [{ metadata: 'read', organization_administration: 'write' }],
|
||||
},
|
||||
'repos/get-content': {
|
||||
userToServerRest: true,
|
||||
serverToServer: true,
|
||||
permissions: [{ contents: 'read' }],
|
||||
},
|
||||
}
|
||||
|
||||
// Mock actor resources
|
||||
const mockProgActorResources = {
|
||||
metadata: {
|
||||
title: 'Metadata',
|
||||
visibility: 'public',
|
||||
},
|
||||
administration: {
|
||||
title: 'Administration',
|
||||
visibility: 'public',
|
||||
},
|
||||
organization_administration: {
|
||||
title: 'Organization administration',
|
||||
visibility: 'public',
|
||||
},
|
||||
contents: {
|
||||
title: 'Contents',
|
||||
visibility: 'public',
|
||||
},
|
||||
}
|
||||
|
||||
test('calculateAdditionalPermissions works correctly', () => {
|
||||
// Single permission set with multiple permissions
|
||||
expect(calculateAdditionalPermissions([{ metadata: 'read', admin: 'write' }])).toBe(true)
|
||||
|
||||
// Single permission set with single permission
|
||||
expect(calculateAdditionalPermissions([{ metadata: 'read' }])).toBe(false)
|
||||
|
||||
// Multiple permission sets
|
||||
expect(calculateAdditionalPermissions([{ metadata: 'read' }, { admin: 'write' }])).toBe(true)
|
||||
|
||||
// Empty permission sets
|
||||
expect(calculateAdditionalPermissions([])).toBe(false)
|
||||
})
|
||||
|
||||
test('identifies metadata with additional permissions correctly', () => {
|
||||
// Case 1: metadata + administration (should be filtered)
|
||||
const metadataWithAdmin = [{ metadata: 'read', administration: 'write' }]
|
||||
expect(shouldFilterMetadataPermission('metadata', metadataWithAdmin)).toBe(true)
|
||||
|
||||
// Case 2: metadata only (should NOT be filtered)
|
||||
const metadataOnly = [{ metadata: 'read' }]
|
||||
expect(shouldFilterMetadataPermission('metadata', metadataOnly)).toBe(false)
|
||||
|
||||
// Case 3: non-metadata permission (should NOT be filtered)
|
||||
const nonMetadata = [{ contents: 'read' }]
|
||||
expect(shouldFilterMetadataPermission('contents', nonMetadata)).toBe(false)
|
||||
})
|
||||
|
||||
test('filters metadata operations with additional permissions', () => {
|
||||
const filteredOperations = []
|
||||
const metadataPermissions = []
|
||||
|
||||
for (const operation of mockOperationsWithMetadata) {
|
||||
const progData = mockProgAccessData[operation.operationId]
|
||||
|
||||
for (const permissionSet of progData.permissions) {
|
||||
for (const [permissionName] of Object.entries(permissionSet)) {
|
||||
if (mockProgActorResources[permissionName]?.visibility === 'private') continue
|
||||
|
||||
const additionalPermissions = calculateAdditionalPermissions(progData.permissions)
|
||||
|
||||
// Apply metadata filtering logic
|
||||
if (shouldFilterMetadataPermission(permissionName, progData.permissions)) {
|
||||
// Skip this metadata permission as it has additional permissions
|
||||
continue
|
||||
}
|
||||
|
||||
if (permissionName === 'metadata') {
|
||||
metadataPermissions.push({
|
||||
operationId: operation.operationId,
|
||||
additionalPermissions,
|
||||
})
|
||||
} else {
|
||||
filteredOperations.push({
|
||||
operationId: operation.operationId,
|
||||
permission: permissionName,
|
||||
additionalPermissions,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Should only have metadata permission from the metadata-only operation
|
||||
expect(metadataPermissions).toHaveLength(1)
|
||||
expect(metadataPermissions[0].operationId).toBe('repos/get-readme')
|
||||
expect(metadataPermissions[0].additionalPermissions).toBe(false)
|
||||
|
||||
// Should have other permissions from operations with additional permissions
|
||||
const adminPermission = filteredOperations.find((op) => op.permission === 'administration')
|
||||
expect(adminPermission).toBeDefined()
|
||||
expect(adminPermission.operationId).toBe('repos/enable-automated-security-fixes')
|
||||
expect(adminPermission.additionalPermissions).toBe(true)
|
||||
|
||||
const orgAdminPermission = filteredOperations.find(
|
||||
(op) => op.permission === 'organization_administration',
|
||||
)
|
||||
expect(orgAdminPermission).toBeDefined()
|
||||
expect(orgAdminPermission.operationId).toBe('orgs/update-webhook')
|
||||
expect(orgAdminPermission.additionalPermissions).toBe(true)
|
||||
})
|
||||
|
||||
test('preserves non-metadata permissions regardless of additional permissions', () => {
|
||||
const nonMetadataOperations = mockOperationsWithMetadata.filter(
|
||||
(op) =>
|
||||
!mockProgAccessData[op.operationId].permissions.some((permSet) => 'metadata' in permSet),
|
||||
)
|
||||
|
||||
expect(nonMetadataOperations).toHaveLength(1)
|
||||
expect(nonMetadataOperations[0].operationId).toBe('repos/get-content')
|
||||
|
||||
// Verify contents permission would be preserved
|
||||
const contentsPermissionSet = mockProgAccessData['repos/get-content'].permissions[0]
|
||||
expect('contents' in contentsPermissionSet).toBe(true)
|
||||
expect('metadata' in contentsPermissionSet).toBe(false)
|
||||
})
|
||||
|
||||
test('handles edge cases in permission sets', () => {
|
||||
// Empty permission set
|
||||
expect(shouldFilterMetadataPermission('metadata', [])).toBe(false)
|
||||
|
||||
// Permission set with empty object (edge case)
|
||||
const edgeCase1 = [{ metadata: 'read' }, {}]
|
||||
expect(shouldFilterMetadataPermission('metadata', edgeCase1)).toBe(true)
|
||||
|
||||
// Multiple permission sets with metadata in different sets
|
||||
const edgeCase2 = [{ metadata: 'read' }, { admin: 'write' }]
|
||||
expect(shouldFilterMetadataPermission('metadata', edgeCase2)).toBe(true)
|
||||
})
|
||||
|
||||
test('filters metadata permissions that match the GitHub issue examples', () => {
|
||||
// These are examples from the GitHub issue that should be filtered out
|
||||
// PUT /orgs/{org}/actions/permissions/repositories/{repository_id}
|
||||
const putActionsPermissions = [{ metadata: 'read', organization_administration: 'write' }]
|
||||
|
||||
// DELETE /orgs/{org}/actions/permissions/repositories/{repository_id}
|
||||
const deleteActionsPermissions = [{ metadata: 'read', organization_administration: 'write' }]
|
||||
|
||||
// These should be filtered out because they have metadata + additional permissions
|
||||
expect(shouldFilterMetadataPermission('metadata', putActionsPermissions)).toBe(true)
|
||||
expect(shouldFilterMetadataPermission('metadata', deleteActionsPermissions)).toBe(true)
|
||||
|
||||
// But the organization_administration permissions should NOT be filtered
|
||||
expect(
|
||||
shouldFilterMetadataPermission('organization_administration', putActionsPermissions),
|
||||
).toBe(false)
|
||||
expect(
|
||||
shouldFilterMetadataPermission('organization_administration', deleteActionsPermissions),
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
test('preserves metadata permissions that are standalone', () => {
|
||||
// Example of a metadata-only permission that should be preserved
|
||||
const metadataOnlyPermissions = [{ metadata: 'read' }]
|
||||
|
||||
// This should NOT be filtered out
|
||||
expect(shouldFilterMetadataPermission('metadata', metadataOnlyPermissions)).toBe(false)
|
||||
})
|
||||
|
||||
test('handles complex permission structures from real data', () => {
|
||||
// Multiple permission sets (should filter metadata)
|
||||
const multiplePermissionSets = [{ metadata: 'read' }, { administration: 'write' }]
|
||||
expect(shouldFilterMetadataPermission('metadata', multiplePermissionSets)).toBe(true)
|
||||
|
||||
// Single permission set with multiple permissions (should filter metadata)
|
||||
const multiplePermissionsInSet = [
|
||||
{ metadata: 'read', contents: 'write', pull_requests: 'write' },
|
||||
]
|
||||
expect(shouldFilterMetadataPermission('metadata', multiplePermissionsInSet)).toBe(true)
|
||||
|
||||
// Multiple permission sets where metadata is not in the first set
|
||||
const metadataInSecondSet = [{ administration: 'write' }, { metadata: 'read' }]
|
||||
expect(shouldFilterMetadataPermission('metadata', metadataInSecondSet)).toBe(true)
|
||||
})
|
||||
|
||||
test('validates filtering logic against known problematic endpoints', () => {
|
||||
// Based on the issue description, these types of operations should have
|
||||
// their metadata permissions filtered out:
|
||||
|
||||
// Runner group operations
|
||||
const runnerGroupPermissions = [{ metadata: 'read', organization_administration: 'write' }]
|
||||
|
||||
// Organization secrets operations
|
||||
const orgSecretsPermissions = [{ metadata: 'read', organization_secrets: 'write' }]
|
||||
|
||||
// Repository operations with admin permissions
|
||||
const repoAdminPermissions = [{ metadata: 'read', administration: 'write' }]
|
||||
|
||||
// All of these should filter out metadata
|
||||
expect(shouldFilterMetadataPermission('metadata', runnerGroupPermissions)).toBe(true)
|
||||
expect(shouldFilterMetadataPermission('metadata', orgSecretsPermissions)).toBe(true)
|
||||
expect(shouldFilterMetadataPermission('metadata', repoAdminPermissions)).toBe(true)
|
||||
|
||||
// But should preserve the actual required permissions
|
||||
expect(
|
||||
shouldFilterMetadataPermission('organization_administration', runnerGroupPermissions),
|
||||
).toBe(false)
|
||||
expect(shouldFilterMetadataPermission('organization_secrets', orgSecretsPermissions)).toBe(
|
||||
false,
|
||||
)
|
||||
expect(shouldFilterMetadataPermission('administration', repoAdminPermissions)).toBe(false)
|
||||
})
|
||||
|
||||
test('verifies consistency with additional-permissions flag calculation', () => {
|
||||
const testCases = [
|
||||
// Single permission, single set - no additional permissions
|
||||
{ permissionSets: [{ metadata: 'read' }], expected: false },
|
||||
|
||||
// Multiple permissions, single set - has additional permissions
|
||||
{ permissionSets: [{ metadata: 'read', admin: 'write' }], expected: true },
|
||||
|
||||
// Single permission, multiple sets - has additional permissions
|
||||
{ permissionSets: [{ metadata: 'read' }, { admin: 'write' }], expected: true },
|
||||
|
||||
// Multiple permissions, multiple sets - has additional permissions
|
||||
{
|
||||
permissionSets: [{ metadata: 'read', contents: 'read' }, { admin: 'write' }],
|
||||
expected: true,
|
||||
},
|
||||
]
|
||||
|
||||
for (const testCase of testCases) {
|
||||
const additionalPermissions = calculateAdditionalPermissions(testCase.permissionSets)
|
||||
const shouldFilter = shouldFilterMetadataPermission('metadata', testCase.permissionSets)
|
||||
|
||||
// The filtering logic should match the additional permissions calculation
|
||||
expect(shouldFilter).toBe(additionalPermissions)
|
||||
expect(additionalPermissions).toBe(testCase.expected)
|
||||
}
|
||||
})
|
||||
|
||||
test('validates filtering logic matches expected behavior from issue', () => {
|
||||
// Based on the GitHub issue, these operations should be filtered out from metadata:
|
||||
// - PUT /orgs/{org}/actions/permissions/repositories/{repository_id}
|
||||
// - DELETE /orgs/{org}/actions/permissions/repositories/{repository_id}
|
||||
// Because they have metadata + organization_administration permissions
|
||||
|
||||
const mockMutatingOperation = {
|
||||
operationId: 'actions/set-selected-repositories-enabled-github-actions-organization',
|
||||
permissionSets: [{ metadata: 'read', organization_administration: 'write' }],
|
||||
}
|
||||
|
||||
const progData = {
|
||||
userToServerRest: true,
|
||||
serverToServer: true,
|
||||
permissions: [{ metadata: 'read', organization_administration: 'write' }],
|
||||
}
|
||||
|
||||
// This should be filtered out from metadata permissions
|
||||
expect(shouldFilterMetadataPermission('metadata', progData.permissions)).toBe(true)
|
||||
|
||||
// But organization_administration permission should still be included
|
||||
expect(
|
||||
shouldFilterMetadataPermission('organization_administration', progData.permissions),
|
||||
).toBe(false)
|
||||
|
||||
// Verify additional permissions flag is set correctly
|
||||
expect(calculateAdditionalPermissions(progData.permissions)).toBe(true)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user