diff --git a/.github/scripts/test.mjs b/.github/scripts/test.mjs index 50385b4..5a1cc09 100644 --- a/.github/scripts/test.mjs +++ b/.github/scripts/test.mjs @@ -4,6 +4,7 @@ import fs from 'fs'; const { default: initialize } = await import('./initialize-request.mjs'); const { default: complete } = await import('./approve-or-deny-request.mjs'); +const { default: validateRequest } = await import('./validate-request.mjs'); import nock from 'nock'; nock.disableNetConnect(); @@ -65,6 +66,59 @@ test.after.each(() => { // nothing to do here }); +test("Validate GitHub owner and repository names", function () { + const validRequests = [ + { owner: "a", repo: "a" }, + { owner: "a-b", repo: ".github" }, + { owner: "a".repeat(39), repo: "a".repeat(100) }, + { owner: "GitHub", repo: "repo_name-1.0" } + ]; + + for (const request of validRequests) { + assert.equal(validateRequest(JSON.stringify(request)), request); + } +}); + +test("Reject invalid GitHub owner names", function () { + const invalidOwners = [ + "", + "-owner", + "owner-", + "owner--name", + "owner_name", + "owner/name", + "owner name", + "a".repeat(40) + ]; + + for (const owner of invalidOwners) { + assert.throws(() => validateRequest({ owner, repo: "repo" }), /Owner must/); + } +}); + +test("Reject invalid GitHub repository names", function () { + const invalidRepositories = [ + "", + "repo/name", + "repo name", + "repo$name", + "répo", + "a".repeat(101) + ]; + + for (const repo of invalidRepositories) { + assert.throws(() => validateRequest({ owner: "owner", repo }), /Repository must/); + } +}); + +test("Reject malformed request payloads", function () { + assert.throws(() => validateRequest("not json"), /valid JSON/); + assert.throws(() => validateRequest(null), /JSON object/); + assert.throws(() => validateRequest([]), /JSON object/); + assert.throws(() => validateRequest({ owner: "owner" }), /Repository must/); + assert.throws(() => validateRequest({ repo: "repo" }), /Owner must/); +}); + // Fail the workflow because the repo already exists test("Fail the workflow because the repo already exists", async function () { let mock = nock("https://github.robandpdx.demo-stack.com/api/v3"); diff --git a/.github/scripts/validate-request.mjs b/.github/scripts/validate-request.mjs new file mode 100644 index 0000000..db9a238 --- /dev/null +++ b/.github/scripts/validate-request.mjs @@ -0,0 +1,33 @@ +const OWNER_PATTERN = /^[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*$/; +const REPOSITORY_PATTERN = /^[A-Za-z0-9._-]+$/; + +export default function validateRequest(payload) { + let request; + + try { + request = typeof payload === 'string' ? JSON.parse(payload) : payload; + } catch { + throw new Error('Request payload must be valid JSON'); + } + + if (!request || typeof request !== 'object' || Array.isArray(request)) { + throw new Error('Request payload must be a JSON object'); + } + + validateOwner(request.owner); + validateRepository(request.repo); + + return request; +} + +function validateOwner(owner) { + if (typeof owner !== 'string' || owner.length < 1 || owner.length > 39 || !OWNER_PATTERN.test(owner)) { + throw new Error('Owner must be 1-39 ASCII letters, digits, or single hyphens, and cannot begin or end with a hyphen'); + } +} + +function validateRepository(repo) { + if (typeof repo !== 'string' || repo.length < 1 || repo.length > 100 || !REPOSITORY_PATTERN.test(repo)) { + throw new Error('Repository must be 1-100 ASCII letters, digits, periods, hyphens, or underscores'); + } +} \ No newline at end of file diff --git a/.github/workflows/approve-or-deny-request.yml b/.github/workflows/approve-or-deny-request.yml index b1d78cb..c9ed806 100644 --- a/.github/workflows/approve-or-deny-request.yml +++ b/.github/workflows/approve-or-deny-request.yml @@ -15,13 +15,25 @@ jobs: payload: ${{ steps.issue_body_parser_request.outputs.payload }} steps: - name: Get JSON Data out of Issue Request - uses: peter-murray/issue-body-parser-action@v3 + uses: peter-murray/issue-body-parser-action@6f0770bae810c272cbdccf6f817b0759a9d82647 # v3 id: issue_body_parser_request with: github_token: ${{ secrets.GITHUB_TOKEN }} issue_id: ${{ github.event.issue.number }} payload_marker: request fail_on_missing: false + - name: Check out request validator + if: steps.issue_body_parser_request.outputs.payload != 'NOT_FOUND' + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - name: Validate requested repository + if: steps.issue_body_parser_request.outputs.payload != 'NOT_FOUND' + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 + env: + REQUEST_PAYLOAD: ${{ steps.issue_body_parser_request.outputs.payload }} + with: + script: | + const { default: validateRequest } = await import('${{ github.workspace }}/.github/scripts/validate-request.mjs'); + validateRequest(process.env.REQUEST_PAYLOAD); approve-or-deny-request: runs-on: self-hosted needs: parse-issue @@ -34,18 +46,18 @@ jobs: REPO: ${{ fromJson(needs.parse-issue.outputs.payload).repo }} REQUEST_VERSION: ${{ fromJson(needs.parse-issue.outputs.payload).version }} run: | - if [ $REQUEST_VERSION == 'latest' ]; then + if [ "$REQUEST_VERSION" = 'latest' ]; then echo "Finding latest release of $OWNER/$REPO..." - export VERSION=`curl https://api.github.com/repos/$OWNER/$REPO/releases/latest | jq -r .name` + VERSION=$(curl "https://api.github.com/repos/$OWNER/$REPO/releases/latest" | jq -r .name) else - export VERSION=$REQUEST_VERSION + VERSION="$REQUEST_VERSION" fi echo "VERSION: $VERSION" - echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "version=$VERSION" >> "$GITHUB_OUTPUT" - name: Check out scripts - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - name: Setup Node - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: '20' check-latest: true @@ -54,7 +66,7 @@ jobs: cd .github/scripts npm install - name: Approve or deny request - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 env: VERSION: ${{ steps.get_version.outputs.version }} REPO: ${{ fromJson(needs.parse-issue.outputs.payload).repo }} diff --git a/.github/workflows/initialize-request.yml b/.github/workflows/initialize-request.yml index b8e648b..65f2748 100644 --- a/.github/workflows/initialize-request.yml +++ b/.github/workflows/initialize-request.yml @@ -15,13 +15,25 @@ jobs: payload: ${{ steps.issue_body_parser_request.outputs.payload }} steps: - name: Get JSON Data out of Issue Request - uses: peter-murray/issue-body-parser-action@v3 + uses: peter-murray/issue-body-parser-action@6f0770bae810c272cbdccf6f817b0759a9d82647 # v3 id: issue_body_parser_request with: github_token: ${{ secrets.GITHUB_TOKEN }} issue_id: ${{ github.event.issue.number }} payload_marker: request fail_on_missing: false + - name: Check out request validator + if: steps.issue_body_parser_request.outputs.payload != 'NOT_FOUND' + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - name: Validate requested repository + if: steps.issue_body_parser_request.outputs.payload != 'NOT_FOUND' + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 + env: + REQUEST_PAYLOAD: ${{ steps.issue_body_parser_request.outputs.payload }} + with: + script: | + const { default: validateRequest } = await import('${{ github.workspace }}/.github/scripts/validate-request.mjs'); + validateRequest(process.env.REQUEST_PAYLOAD); initialize-request: runs-on: self-hosted needs: parse-issue @@ -34,22 +46,22 @@ jobs: REPO: ${{ fromJson(needs.parse-issue.outputs.payload).repo }} REQUEST_VERSION: ${{ fromJson(needs.parse-issue.outputs.payload).version }} run: | - if [ $REQUEST_VERSION == 'latest' ]; then + if [ "$REQUEST_VERSION" = 'latest' ]; then echo "Finding latest release of $OWNER/$REPO..." - export VERSION=`curl https://api.github.com/repos/$OWNER/$REPO/releases/latest | jq -r .name` + VERSION=$(curl "https://api.github.com/repos/$OWNER/$REPO/releases/latest" | jq -r .name) if [ "$VERSION" == "null" ]; then echo "No latest version found for $OWNER/$REPO" exit 1 fi else - export VERSION=$REQUEST_VERSION + VERSION="$REQUEST_VERSION" fi echo "VERSION: $VERSION" - echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "version=$VERSION" >> "$GITHUB_OUTPUT" - name: Check out scripts - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - name: Setup Node - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: '20' check-latest: true @@ -58,7 +70,7 @@ jobs: cd .github/scripts npm install - name: Create the repo ${{ fromJson(needs.parse-issue.outputs.payload).repo }}_${{ steps.get_version.outputs.version }} on GitHub Enterprise Server - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 env: VERSION: ${{ steps.get_version.outputs.version }} OWNER: ${{ fromJson(needs.parse-issue.outputs.payload).owner }} @@ -81,7 +93,7 @@ jobs: OWNER: ${{ fromJson(needs.parse-issue.outputs.payload).owner }} REPO: ${{ fromJson(needs.parse-issue.outputs.payload).repo }} run: | - git clone https://github.com/$OWNER/$REPO requested-action + git clone "https://github.com/$OWNER/$REPO" requested-action - name: Push requested action to private repo in $ACTIONS_APPROVED_ORG org on GitHub Enterprise Server env: REPO: ${{ fromJson(needs.parse-issue.outputs.payload).repo }} diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index a8bd6f8..65f1c4a 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -17,8 +17,8 @@ jobs: working-directory: ./.github/scripts steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 #v7.0.0 with: node-version: '20' check-latest: true @@ -28,8 +28,3 @@ jobs: working-directory: ${{ env.working-directory }} - run: npm test working-directory: ${{ env.working-directory }} - - uses: tintef/nyc-reporter-action@0.2.5 - with: - GITHUB_TOKEN: ${{ github.token }} - SKIP_COVERAGE_FOLDER: true - WORKING_DIRECTORY: ${{ env.working-directory }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c0e0d3e..0e30941 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,11 +12,12 @@ jobs: env: working-directory: ./.github/scripts steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 #v7.0.0 with: - node-version: 20 - cache: npm + node-version: '20' + check-latest: true + cache: 'npm' cache-dependency-path: ${{ env.working-directory }}/package-lock.json - run: npm ci working-directory: ${{ env.working-directory }}