Skip to content

Commit

Permalink
[Content Linter Rule] Third Party Action References Must Use SHA (#52…
Browse files Browse the repository at this point in the history
…282)

Co-authored-by: Rachael Sewell <[email protected]>
Co-authored-by: Ben Ahmady <[email protected]>
Co-authored-by: Felicity Chapman <[email protected]>
  • Loading branch information
4 people authored Sep 27, 2024
1 parent 72faf23 commit 7c49068
Show file tree
Hide file tree
Showing 18 changed files with 231 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ jobs:
- name: Git clone the repository
uses: {% data reusables.actions.action-checkout %}
- name: configure aws credentials
uses: aws-actions/configure-aws-credentials@v3
uses: aws-actions/configure-aws-credentials@d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3
with:
role-to-assume: ROLE-TO-ASSUME
role-session-name: samplerolesession
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 'Az CLI login'
uses: azure/login@v1
uses: azure/login@a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ jobs:
steps:
- id: 'auth'
name: 'Authenticate to GCP'
uses: 'google-github-actions/auth@v0.3.1'
uses: 'google-github-actions/auth@f1e2d3c4b5a6f7e8d9c0b1a2c3d4e5f6a7b8c9d0'
with:
create_credentials_file: 'true'
workload_identity_provider: 'WORKLOAD-IDENTITY-PROVIDER'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ jobs:
contents: read
steps:
- name: Retrieve secret from Vault
uses: hashicorp/vault-action@v2.4.0
uses: hashicorp/vault-action@9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0b
with:
method: jwt
url: VAULT-URL
Expand Down Expand Up @@ -176,7 +176,7 @@ jobs:
contents: read
steps:
- name: Retrieve secret from Vault
uses: hashicorp/vault-action@v2.4.0
uses: hashicorp/vault-action@9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0b
with:
exportToken: true
method: jwt
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,5 +94,5 @@ jobs:
path: dist/

- name: Publish release distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
uses: pypa/gh-action-pypi-publish@3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f
```
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ Before you begin, you'll create a repository on {% data variables.product.github
The following workflow code uses the completed hello world action that you made in "[AUTOTITLE](/actions/creating-actions/creating-a-composite-action#creating-an-action-metadata-file)".
Copy the workflow code into a `.github/workflows/main.yml` file in another repository, replacing `OWNER` and `TAG` with the repository owner and the tag you created, respectively. You can also replace the `who-to-greet` input with your name.
Copy the workflow code into a `.github/workflows/main.yml` file in another repository, replacing `actions` and `SHA` with the repository owner and the SHA of the commit you want to use, respectively. You can also replace the `who-to-greet` input with your name.
```yaml copy
on: [push]
Expand All @@ -167,7 +167,7 @@ jobs:
steps:
- uses: {% data reusables.actions.action-checkout %}
- id: foo
uses: OWNER/hello-world-composite-action@TAG
uses: OWNER/hello-world-composite-action@SHA
with:
who-to-greet: 'Mona the Octocat'
- run: echo random-number "$RANDOM_NUMBER"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ Public actions can be used by workflows in any repository. When an action is in
This example demonstrates how your new public action can be run from within an external repository.
Copy the following YAML into a new file at `.github/workflows/main.yml`, and update the `uses: octocat/hello-world-javascript-action@v1.1` line with your username and the name of the public repository you created above. You can also replace the `who-to-greet` input with your name.
Copy the following YAML into a new file at `.github/workflows/main.yml`, and update the `uses: octocat/hello-world-javascript-action@1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b` line with your username and the name of the public repository you created above. You can also replace the `who-to-greet` input with your name.
{% raw %}
Expand All @@ -237,7 +237,7 @@ jobs:
steps:
- name: Hello world action step
id: hello
uses: octocat/hello-world-javascript-action@v1.1
uses: octocat/hello-world-javascript-action@1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b
with:
who-to-greet: 'Mona the Octocat'
# Use the output from the `hello` step
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,7 @@ jobs:
path: dist/
- name: Publish release distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
uses: pypa/gh-action-pypi-publish@6f7e8d9c0b1a2c3d4e5f6a7b8c9d0e1f2a3b4c5d
```

{% ifversion not ghes %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,10 @@ jobs:
- uses: {% data reusables.actions.action-checkout %}

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b

- name: Log in to GitHub container registry
uses: docker/login-action@v2
uses: docker/login-action@8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d
with:
registry: ghcr.io
username: {% raw %}${{ github.actor }}{% endraw %}
Expand All @@ -114,7 +114,7 @@ jobs:
run: echo "REPO=${GITHUB_REPOSITORY,,}" >>${GITHUB_ENV}

- name: Build and push container image to registry
uses: docker/build-push-action@v4
uses: docker/build-push-action@9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f
with:
push: true
tags: ghcr.io/{% raw %}${{ env.REPO }}{% endraw %}:{% raw %}${{ github.sha }}{% endraw %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,13 @@ jobs:
- uses: {% data reusables.actions.action-checkout %}

- name: Setup PHP
uses: shivammathur/setup-php@v2
uses: shivammathur/setup-php@1f2e3d4c5b6a7f8e9d0c1b2a3e4f5d6c7b8a9e0f
with:
php-version: {% raw %}${{ env.PHP_VERSION }}{% endraw %}

- name: Check if composer.json exists
id: check_files
uses: andstor/file-existence-action@v2
uses: andstor/file-existence-action@2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b
with:
files: 'composer.json'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,7 @@ jobs:
outputs:
handle: {% raw %}${{ steps.generate-secret.outputs.handle }}{% endraw %}
steps:
- uses: some/secret-store@v1
- uses: some/secret-store@27b31702a0e7fc50959f5ad993c78deac1bdfc29
with:{% raw %}
credentials: ${{ secrets.SECRET_STORE_CREDENTIALS }}
instance: ${{ secrets.SECRET_STORE_INSTANCE }}{% endraw %}
Expand All @@ -428,7 +428,7 @@ jobs:
runs-on: macos-latest
needs: secret-generator
steps:
- uses: some/secret-store@v1
- uses: some/secret-store@27b31702a0e7fc50959f5ad993c78deac1bdfc29
with:{% raw %}
credentials: ${{ secrets.SECRET_STORE_CREDENTIALS }}
instance: ${{ secrets.SECRET_STORE_INSTANCE }}{% endraw %}
Expand All @@ -452,7 +452,7 @@ jobs:
secret-generator:
runs-on: ubuntu-latest
steps:
- uses: some/secret-store@v1
- uses: some/secret-store@27b31702a0e7fc50959f5ad993c78deac1bdfc29
with:{% raw %}
credentials: ${{ secrets.SECRET_STORE_CREDENTIALS }}
instance: ${{ secrets.SECRET_STORE_INSTANCE }}{% endraw %}
Expand All @@ -467,7 +467,7 @@ jobs:
runs-on: macos-latest
needs: secret-generator
steps:
- uses: some/secret-store@v1
- uses: some/secret-store@27b31702a0e7fc50959f5ad993c78deac1bdfc29
with:{% raw %}
credentials: ${{ secrets.SECRET_STORE_CREDENTIALS }}
instance: ${{ secrets.SECRET_STORE_INSTANCE }}{% endraw %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ jobs:
uses: {% data reusables.actions.action-checkout %}
- name: Login to private container registry for dependencies
uses: docker/login-action@v2
uses: docker/login-action@3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c
with:
registry: https://1234567890.dkr.ecr.us-east-1.amazonaws.com
username: {% raw %}${{ secrets.READONLY_AWS_ACCESS_KEY_ID }}{% endraw %}
Expand Down Expand Up @@ -136,7 +136,7 @@ jobs:
steps:
- name: Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@v2
uses: dependabot/fetch-metadata@4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
# The following properties are now available:
Expand Down Expand Up @@ -173,7 +173,7 @@ jobs:
steps:
- name: Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@v2
uses: dependabot/fetch-metadata@4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
- name: Add a label for all production dependencies
Expand Down Expand Up @@ -205,7 +205,7 @@ jobs:
steps:
- name: Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@v2
uses: dependabot/fetch-metadata@4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
- name: Approve a PR
Expand Down Expand Up @@ -248,7 +248,7 @@ jobs:
steps:
- name: Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@v2
uses: dependabot/fetch-metadata@4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
- name: Enable auto-merge for Dependabot PRs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ jobs:
name: sbom
path: _manifest/spdx_2.2
- name: SBOM upload
uses: advanced-security/spdx-dependency-submission-action@v0.0.1
uses: advanced-security/spdx-dependency-submission-action@5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e
with:
filePath: "_manifest/spdx_2.2/"
```
3 changes: 2 additions & 1 deletion data/reusables/contributing/content-linter-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,5 @@
| GHD036 | image-no-gif | Image must not be a gif, styleguide reference: contributing/style-guide-and-content-model/style-guide.md#images | error | images |
| GHD038 | expired-content | Expired content must be remediated. | error | expired |
| GHD039 | expiring-soon | Content that expires soon should be proactively addressed. | warning | expired |
| [GHD040](https://github.com/github/docs/blob/main/src/content-linter/README.md) | table-liquid-versioning | Tables must use the correct liquid versioning format | error | tables |
| [GHD040](https://github.com/github/docs/blob/main/src/content-linter/README.md) | table-liquid-versioning | Tables must use the correct liquid versioning format | error | tables |
| GHD041 | third-party-action-pinning | Code examples that use third-party actions must always pin to a full length commit SHA | error | feature, actions |
2 changes: 2 additions & 0 deletions src/content-linter/lib/linting-rules/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { raiReusableUsage } from './rai-reusable-usage.js'
import { imageNoGif } from './image-no-gif.js'
import { expiredContent, expiringSoon } from './expired-content.js'
import { tableLiquidVersioning } from './table-liquid-versioning.js'
import { thirdPartyActionPinning } from './third-party-action-pinning.js'

const noDefaultAltText = markdownlintGitHub.find((elem) =>
elem.names.includes('no-default-alt-text'),
Expand Down Expand Up @@ -75,5 +76,6 @@ export const gitHubDocsMarkdownlint = {
expiredContent,
expiringSoon,
tableLiquidVersioning,
thirdPartyActionPinning,
],
}
80 changes: 80 additions & 0 deletions src/content-linter/lib/linting-rules/third-party-action-pinning.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import yaml from 'js-yaml'
import { addError, filterTokens } from 'markdownlint-rule-helpers'

import { liquid } from '#src/content-render/index.js'
import { allVersions } from '#src/versions/lib/all-versions.js'

// Detects third-party actions in the format `owner/repo@ref`
const actionRegex = /[\w-]+\/[\w-]+@[\w-]+/
// Detects a full-length commit SHA (40 hexadecimal characters)
const shaRegex = /[\w-]+\/[\w-]+@[0-9a-fA-F]{40}/
// Detects first-party actions
const firstPartyPrefixes = ['actions/', 'github/', 'octo-org/', 'OWNER/']

export const thirdPartyActionPinning = {
names: ['GHD041', 'third-party-action-pinning'],
description:
'Code examples that use third-party actions must always pin to a full length commit SHA',
tags: ['feature', 'actions'],
parser: 'markdownit',
asynchronous: true,
function: (params, onError) => {
filterTokens(params, 'fence', async (token) => {
const lang = token.info.trim().split(/\s+/u).shift().toLowerCase()
if (lang !== 'yaml' && lang !== 'yml') return
if (!token.content.includes('steps:')) return
if (!token.content.includes('uses:')) return

const context = {
currentLanguage: 'en',
currentVersionObj: allVersions['free-pro-team@latest'],
}
// If we don't parse the Liquid first, yaml loading chokes on {% raw %} tags
const renderedYaml = await liquid.parseAndRender(token.content, context)
try {
const yamlObj = yaml.load(renderedYaml)
const steps = getWorkflowSteps(yamlObj)
if (!steps.some((step) => step.uses)) return

steps.forEach((step) => {
if (step.uses) {
const actionMatch = step.uses.match(actionRegex)
if (actionMatch) {
const isFirstParty = firstPartyPrefixes.some((prefix) => step.uses.startsWith(prefix))
if (!isFirstParty && !shaRegex.test(step.uses)) {
addError(
onError,
getLineNumber(token.content, step.uses) + token.lineNumber,
'Code examples that use third-party actions must always pin to a full length commit SHA',
step.uses,
)
}
}
}
})
} catch (e) {
if (e instanceof yaml.YAMLException) {
console.log('YAML Exception file:', params.name)
console.error('YAML Exception:', e.message)
} else {
console.error('Error parsing YAML:', e)
}
}
})
},
}

function getWorkflowSteps(yamlObj) {
if (yamlObj?.jobs) {
const jobs = Object.values(yamlObj.jobs)
return jobs.flatMap((job) => job.steps || [])
} else if (yamlObj?.steps) {
return yamlObj.steps
}
return []
}

function getLineNumber(tokenContent, step) {
const contentLines = tokenContent.split('\n')
return contentLines.findIndex((line) => line.includes(step)) + 1
}
6 changes: 6 additions & 0 deletions src/content-linter/style/github-docs.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,12 @@ const githubDocsConfig = {
severity: 'error',
'partial-markdown-files': true,
},
'third-party-action-pinning': {
// GH041
severity: 'error',
'partial-markdown-files': true,
'yml-files': true,
},
}

export const githubDocsFrontmatterConfig = {
Expand Down
Loading

0 comments on commit 7c49068

Please sign in to comment.