diff --git a/.github/actions/audit/action.yml b/.github/actions/audit/action.yml new file mode 100644 index 00000000..c66c5ea4 --- /dev/null +++ b/.github/actions/audit/action.yml @@ -0,0 +1,17 @@ +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Audit + +inputs: + shell: + description: shell to run on + default: bash + +runs: + using: composite + steps: + - name: Run Audit + shell: ${{ inputs.shell }} + run: | + npm audit --omit=dev + npm audit --audit-level=none diff --git a/.github/actions/changed-files/action.yml b/.github/actions/changed-files/action.yml new file mode 100644 index 00000000..721ad58e --- /dev/null +++ b/.github/actions/changed-files/action.yml @@ -0,0 +1,39 @@ +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Get Changed Files + +inputs: + token: + description: GitHub token to use + required: true + +outputs: + files: + value: ${{ steps.files.outputs.result }} + +runs: + using: composite + steps: + - name: Get Changed Files + uses: actions/github-script@v6 + id: files + with: + github-token: ${{ inputs.token }} + script: | + const { repo: { owner, repo }, eventName, payload, sha } = context + let files + if (eventName === 'pull_request' || eventName === 'pull_request_target') { + files = await github.paginate(github.rest.pulls.listFiles, { + owner, + repo, + pull_number: payload.pull_request.number, + }) + } else { + const { data: commit } = await github.rest.repos.getCommit({ + owner, + repo, + ref: sha, + }) + files = commit.files + } + return files.map(f => f.filename) diff --git a/.github/actions/changed-workspaces/action.yml b/.github/actions/changed-workspaces/action.yml new file mode 100644 index 00000000..dced0657 --- /dev/null +++ b/.github/actions/changed-workspaces/action.yml @@ -0,0 +1,37 @@ +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Get Changed Workspaces + +inputs: + token: + description: GitHub token to use + shell: + description: shell to run on + default: bash + all: + default: false + type: boolean + files: + description: json stringified array of file names + type: string + +outputs: + flags: + value: ${{ steps.workspaces.outputs.flags }} + +runs: + using: composite + steps: + - name: Get Changed Files + uses: ./.github/actions/changed-files + if: ${{ !inputs.all && !inputs.files }} + id: files + with: + token: ${{ inputs.token }} + + - name: Get Workspaces + shell: ${{ inputs.shell }} + id: workspaces + run: | + flags=$(npm exec --offline -- template-oss-changed-workspaces '${{ (inputs.all && '--all') || (inputs.files || steps.files.outputs.result) }}') + echo "flags=${flags}" >> $GITHUB_OUTPUT diff --git a/.github/actions/conclude-check/action.yml b/.github/actions/conclude-check/action.yml new file mode 100644 index 00000000..8503fa8f --- /dev/null +++ b/.github/actions/conclude-check/action.yml @@ -0,0 +1,25 @@ +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Conclude Check +description: Conclude a check + +inputs: + token: + description: GitHub token to use + required: true + conclusion: + description: conclusion of check + require: true + check-id: + description: id of check to conclude + required: true + +runs: + using: composite + steps: + - name: Conclude Check + uses: LouisBrunner/checks-action@v1.5.0 + with: + token: ${{ inputs.token }} + conclusion: ${{ inputs.conclusion }} + check_id: ${{ inputs.check-id }} diff --git a/.github/actions/create-check/action.yml b/.github/actions/create-check/action.yml new file mode 100644 index 00000000..98ed6e02 --- /dev/null +++ b/.github/actions/create-check/action.yml @@ -0,0 +1,64 @@ +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Create Check +description: Create a check and associate it with a sha + +inputs: + token: + description: GitHub token to use + required: true + sha: + description: sha to attach the check to + required: true + job-name: + description: Name of the job to find + required: true + job-status: + description: Status of the check being created + default: in_progress + +outputs: + check-id: + description: The ID of the check that was created + value: ${{ steps.check.outputs.check_id }} + +runs: + using: composite + steps: + - name: Get Workflow Job + uses: actions/github-script@v6 + id: workflow-job + env: + JOB_NAME: ${{ inputs.job-name }} + with: + github-token: ${{ inputs.token }} + script: | + const { JOB_NAME } = process.env + const { repo: { owner, repo }, runId, serverUrl } = context + + const jobs = await github.paginate(github.rest.actions.listJobsForWorkflowRun, { + owner, + repo, + run_id: runId, + }) + const job = jobs.find(j => j.name.endsWith(JOB_NAME)) + + const shaUrl = `${serverUrl}/${owner}/${repo}/commit/${{ inputs.sha }}` + const summary = `This check is assosciated with ${shaUrl}\n\n` + const message = job?.html_url + ? `For run logs, click here: ${job.html_url}` + : `Run logs could not be found for a job with name: "${JOB_NAME}"` + + // Return a json object with properties that LouisBrunner/checks-actions + // expects as the output of the check + return { summary: summary + message } + + - name: Create Check + uses: LouisBrunner/checks-action@v1.5.0 + id: check + with: + token: ${{ inputs.token }} + status: ${{ inputs.job-status }} + name: ${{ inputs.job-name }} + sha: ${{ inputs.sha }} + output: ${{ steps.workflow-job.outputs.result }} diff --git a/.github/actions/deps/action.yml b/.github/actions/deps/action.yml new file mode 100644 index 00000000..d0234587 --- /dev/null +++ b/.github/actions/deps/action.yml @@ -0,0 +1,20 @@ +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Dependencies + +inputs: + command: + description: command to run for the dependencies step + default: install --ignore-scripts --no-audit --no-fund + flags: + description: extra flags to pass to the dependencies step + shell: + description: shell to run on + default: bash + +runs: + using: composite + steps: + - name: Install Dependencies + shell: ${{ inputs.shell }} + run: npm ${{ inputs.command }} ${{ inputs.flags }} diff --git a/.github/actions/lint/action.yml b/.github/actions/lint/action.yml new file mode 100644 index 00000000..2c335673 --- /dev/null +++ b/.github/actions/lint/action.yml @@ -0,0 +1,19 @@ +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Lint + +inputs: + flags: + description: flags to pass to the commands + shell: + description: shell to run on + default: bash + +runs: + using: composite + steps: + - name: Lint + shell: ${{ inputs.shell }} + run: | + npm run lint --ignore-scripts ${{ inputs.flags }} + npm run postlint --ignore-scripts ${{ inputs.flags }} diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml new file mode 100644 index 00000000..2d35f859 --- /dev/null +++ b/.github/actions/setup/action.yml @@ -0,0 +1,95 @@ +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Setup Repo +description: Setup a repo with standard tools + +inputs: + node-version: + description: node version to use + default: 18.x + npm-version: + description: npm version to use + default: latest + cache: + description: whether to cache npm install or not + type: boolean + default: false + shell: + description: shell to run on + default: bash + deps: + description: whether to run the deps step + type: boolean + default: true + deps-command: + description: command to run for the dependencies step + default: install --ignore-scripts --no-audit --no-fund + deps-flags: + description: extra flags to pass to the dependencies step + +runs: + using: composite + steps: + - name: Setup Git User + shell: ${{ inputs.shell }} + run: | + git config --global user.email "npm-cli+bot@github.com" + git config --global user.name "npm CLI robot" + + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: ${{ inputs.node-version }} + cache: ${{ (inputs.cache && 'npm') || null }} + + - name: Check Node Version + if: inputs.npm-version + id: node-version + shell: ${{ inputs.shell }} + run: | + NODE_VERSION=$(node --version) + echo $NODE_VERSION + if npx semver@7 -r "<=10" "$NODE_VERSION" --yes; then + echo "ten-or-lower=true" >> $GITHUB_OUTPUT + fi + if npx semver@7 -r "<=14" "$NODE_VERSION" --yes; then + echo "fourteen-or-lower=true" >> $GITHUB_OUTPUT + fi + + - name: Update Windows npm + # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows + if: inputs.npm-version && runner.os == 'Windows' && steps.node-version.outputs.fourteen-or-lower + shell: ${{ inputs.shell }} + run: | + curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz + tar xf npm-7.5.4.tgz + cd package + node lib/npm.js install --no-fund --no-audit -g ..\npm-7.5.4.tgz + cd .. + rmdir /s /q package + + - name: Install npm@7 + if: inputs.npm-version && steps.node-version.outputs.ten-or-lower + shell: ${{ inputs.shell }} + run: npm i --prefer-online --no-fund --no-audit -g npm@7 + + - name: Install npm@${{ inputs.npm-version }} + if: inputs.npm-version && !steps.node-version.outputs.ten-or-lower + shell: ${{ inputs.shell }} + run: npm i --prefer-online --no-fund --no-audit -g npm@${{ inputs.npm-version }} + + - name: npm Version + shell: ${{ inputs.shell }} + run: npm -v + + - name: Setup Dependencies + if: inputs.deps + uses: ./.github/actions/deps + with: + command: ${{ inputs.deps-command }} + flags: ${{ inputs.deps-flags }} + + - name: Add Problem Matcher + shell: ${{ inputs.shell }} + run: | + [[ -f ./.github/matchers/tap.json ]] && echo "::add-matcher::.github/matchers/tap.json" diff --git a/.github/actions/test/action.yml b/.github/actions/test/action.yml new file mode 100644 index 00000000..fe736f6d --- /dev/null +++ b/.github/actions/test/action.yml @@ -0,0 +1,17 @@ +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Test + +inputs: + flags: + description: flags to pass to the commands + shell: + description: shell to run on + default: bash + +runs: + using: composite + steps: + - name: Test + shell: ${{ inputs.shell }} + run: npm test --ignore-scripts ${{ inputs.flags }} diff --git a/.github/actions/upsert-comment/action.yml b/.github/actions/upsert-comment/action.yml new file mode 100644 index 00000000..38ceef98 --- /dev/null +++ b/.github/actions/upsert-comment/action.yml @@ -0,0 +1,84 @@ +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Upsert Comment +description: Update or create a comment + +inputs: + token: + description: GitHub token to use + required: true + number: + description: Number of the issue or pull request + required: true + login: + description: Login name of user to look for comments from + default: github-actions[bot] + body: + description: Body of the comment, the first line will be used to match to an existing comment + find: + description: string to find in body + replace: + description: string to replace in body + append: + description: string to append to the body + includes: + description: A string that the comment needs to include + +outputs: + comment-id: + description: The ID of the comment + value: ${{ steps.comment.outputs.result }} + +runs: + using: composite + steps: + - name: Create or Update Comment + uses: actions/github-script@v6 + id: comment + env: + NUMBER: ${{ inputs.number }} + BODY: ${{ inputs.body }} + FIND: ${{ inputs.find }} + REPLACE: ${{ inputs.replace }} + APPEND: ${{ inputs.append }} + LOGIN: ${{ inputs.login }} + INCLUDES: ${{ inputs.includes }} + with: + github-token: ${{ inputs.token }} + script: | + const { BODY, FIND, REPLACE, APPEND, LOGIN, NUMBER: issue_number, INCLUDES } = process.env + const { repo: { owner, repo } } = context + const TITLE = BODY.split('\n')[0].trim() + '\n' + const bodyIncludes = (c) => INCLUDES ? c.body.includes(INCLUDES) : true + + const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number }) + .then(comments => comments.map(c => ({ id: c.id, login: c.user.login, body: c.body }))) + + console.log(`Found comments: ${JSON.stringify(comments, null, 2)}`) + console.log(`Looking for comment with: ${JSON.stringify({ LOGIN, TITLE, INCLUDES }, null, 2)}`) + + const comment = comments.find(c => + c.login === LOGIN && + c.body.startsWith(TITLE) && + bodyIncludes(c) + ) + + if (comment) { + console.log(`Found comment: ${JSON.stringify(comment, null, 2)}`) + let newBody = FIND && REPLACE ? comment.body.replace(new RegExp(FIND, 'g'), REPLACE) : BODY + if (APPEND) { + newBody += APPEND + } + await github.rest.issues.updateComment({ owner, repo, comment_id: comment.id, body: newBody }) + return comment.id + } + + if (FIND || REPLACE || APPEND) { + console.log('Could not find a comment to use find/replace or append to') + return + } + + console.log('Creating new comment') + + const res = await github.rest.issues.createComment({ owner, repo, issue_number, body: BODY }) + return res.data.id diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index 62892f99..e986f34a 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -5,8 +5,8 @@ name: Audit on: workflow_dispatch: schedule: - # "At 08:00 UTC (01:00 PT) on Monday" https://crontab.guru/#0_8_*_*_1 - - cron: "0 8 * * 1" + # "At 09:00 UTC (01:00 PT) on Monday" https://crontab.guru/#0_9_*_*_1 + - cron: "0 9 * * 1" jobs: audit: @@ -19,21 +19,11 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 - - name: Setup Git User - run: | - git config --global user.email "npm-cli+bot@github.com" - git config --global user.name "npm CLI robot" - - name: Setup Node - uses: actions/setup-node@v3 + + - name: Setup + uses: ./.github/actions/setup with: - node-version: 18.x - - name: Install npm@latest - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - name: npm Version - run: npm -v - - name: Install Dependencies - run: npm i --ignore-scripts --no-audit --no-fund --package-lock - - name: Run Production Audit - run: npm audit --omit=dev - - name: Run Full Audit - run: npm audit --audit-level=none + deps-flags: "--package-lock" + + - name: Audit + uses: ./.github/actions/audit diff --git a/.github/workflows/ci-release.yml b/.github/workflows/ci-release.yml deleted file mode 100644 index 73209f32..00000000 --- a/.github/workflows/ci-release.yml +++ /dev/null @@ -1,216 +0,0 @@ -# This file is automatically added by @npmcli/template-oss. Do not edit. - -name: CI - Release - -on: - workflow_dispatch: - inputs: - ref: - required: true - type: string - default: main - workflow_call: - inputs: - ref: - required: true - type: string - check-sha: - required: true - type: string - -jobs: - lint-all: - name: Lint All - if: github.repository_owner == 'npm' - runs-on: ubuntu-latest - defaults: - run: - shell: bash - steps: - - name: Get Workflow Job - uses: actions/github-script@v6 - if: inputs.check-sha - id: check-output - env: - JOB_NAME: "Lint All" - MATRIX_NAME: "" - with: - script: | - const { owner, repo } = context.repo - - const { data } = await github.rest.actions.listJobsForWorkflowRun({ - owner, - repo, - run_id: context.runId, - per_page: 100 - }) - - const jobName = process.env.JOB_NAME + process.env.MATRIX_NAME - const job = data.jobs.find(j => j.name.endsWith(jobName)) - const jobUrl = job?.html_url - - const shaUrl = `${context.serverUrl}/${owner}/${repo}/commit/${{ inputs.check-sha }}` - - let summary = `This check is assosciated with ${shaUrl}\n\n` - - if (jobUrl) { - summary += `For run logs, click here: ${jobUrl}` - } else { - summary += `Run logs could not be found for a job with name: "${jobName}"` - } - - return { summary } - - name: Create Check - uses: LouisBrunner/checks-action@v1.3.1 - id: check - if: inputs.check-sha - with: - token: ${{ secrets.GITHUB_TOKEN }} - status: in_progress - name: Lint All - sha: ${{ inputs.check-sha }} - output: ${{ steps.check-output.outputs.result }} - - name: Checkout - uses: actions/checkout@v3 - with: - ref: ${{ inputs.ref }} - - name: Setup Git User - run: | - git config --global user.email "npm-cli+bot@github.com" - git config --global user.name "npm CLI robot" - - name: Setup Node - uses: actions/setup-node@v3 - with: - node-version: 18.x - - name: Install npm@latest - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - name: npm Version - run: npm -v - - name: Install Dependencies - run: npm i --ignore-scripts --no-audit --no-fund - - name: Lint - run: npm run lint --ignore-scripts -ws -iwr --if-present - - name: Post Lint - run: npm run postlint --ignore-scripts -ws -iwr --if-present - - name: Conclude Check - uses: LouisBrunner/checks-action@v1.3.1 - if: steps.check.outputs.check_id && always() - with: - token: ${{ secrets.GITHUB_TOKEN }} - conclusion: ${{ job.status }} - check_id: ${{ steps.check.outputs.check_id }} - - test-all: - name: Test All - ${{ matrix.platform.name }} - ${{ matrix.node-version }} - if: github.repository_owner == 'npm' - strategy: - fail-fast: false - matrix: - platform: - - name: Linux - os: ubuntu-latest - shell: bash - - name: macOS - os: macos-latest - shell: bash - - name: Windows - os: windows-latest - shell: cmd - node-version: - - 14.17.0 - - 14.x - - 16.13.0 - - 16.x - - 18.0.0 - - 18.x - runs-on: ${{ matrix.platform.os }} - defaults: - run: - shell: ${{ matrix.platform.shell }} - steps: - - name: Get Workflow Job - uses: actions/github-script@v6 - if: inputs.check-sha - id: check-output - env: - JOB_NAME: "Test All" - MATRIX_NAME: " - ${{ matrix.platform.name }} - ${{ matrix.node-version }}" - with: - script: | - const { owner, repo } = context.repo - - const { data } = await github.rest.actions.listJobsForWorkflowRun({ - owner, - repo, - run_id: context.runId, - per_page: 100 - }) - - const jobName = process.env.JOB_NAME + process.env.MATRIX_NAME - const job = data.jobs.find(j => j.name.endsWith(jobName)) - const jobUrl = job?.html_url - - const shaUrl = `${context.serverUrl}/${owner}/${repo}/commit/${{ inputs.check-sha }}` - - let summary = `This check is assosciated with ${shaUrl}\n\n` - - if (jobUrl) { - summary += `For run logs, click here: ${jobUrl}` - } else { - summary += `Run logs could not be found for a job with name: "${jobName}"` - } - - return { summary } - - name: Create Check - uses: LouisBrunner/checks-action@v1.3.1 - id: check - if: inputs.check-sha - with: - token: ${{ secrets.GITHUB_TOKEN }} - status: in_progress - name: Test All - ${{ matrix.platform.name }} - ${{ matrix.node-version }} - sha: ${{ inputs.check-sha }} - output: ${{ steps.check-output.outputs.result }} - - name: Checkout - uses: actions/checkout@v3 - with: - ref: ${{ inputs.ref }} - - name: Setup Git User - run: | - git config --global user.email "npm-cli+bot@github.com" - git config --global user.name "npm CLI robot" - - name: Setup Node - uses: actions/setup-node@v3 - with: - node-version: ${{ matrix.node-version }} - - name: Update Windows npm - # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows - if: matrix.platform.os == 'windows-latest' && (startsWith(matrix.node-version, '12.') || startsWith(matrix.node-version, '14.')) - run: | - curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz - tar xf npm-7.5.4.tgz - cd package - node lib/npm.js install --no-fund --no-audit -g ..\npm-7.5.4.tgz - cd .. - rmdir /s /q package - - name: Install npm@7 - if: startsWith(matrix.node-version, '10.') - run: npm i --prefer-online --no-fund --no-audit -g npm@7 - - name: Install npm@latest - if: ${{ !startsWith(matrix.node-version, '10.') }} - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - name: npm Version - run: npm -v - - name: Install Dependencies - run: npm i --ignore-scripts --no-audit --no-fund - - name: Add Problem Matcher - run: echo "::add-matcher::.github/matchers/tap.json" - - name: Test - run: npm test --ignore-scripts -ws -iwr --if-present - - name: Conclude Check - uses: LouisBrunner/checks-action@v1.3.1 - if: steps.check.outputs.check_id && always() - with: - token: ${{ secrets.GITHUB_TOKEN }} - conclusion: ${{ job.status }} - check_id: ${{ steps.check.outputs.check_id }} diff --git a/.github/workflows/ci-test-workspace.yml b/.github/workflows/ci-test-workspace.yml deleted file mode 100644 index 797092e2..00000000 --- a/.github/workflows/ci-test-workspace.yml +++ /dev/null @@ -1,111 +0,0 @@ -# This file is automatically added by @npmcli/template-oss. Do not edit. - -name: CI - test-workspace - -on: - workflow_dispatch: - pull_request: - paths: - - workspace/test-workspace/** - push: - branches: - - main - - latest - paths: - - workspace/test-workspace/** - schedule: - # "At 09:00 UTC (02:00 PT) on Monday" https://crontab.guru/#0_9_*_*_1 - - cron: "0 9 * * 1" - -jobs: - lint: - name: Lint - if: github.repository_owner == 'npm' - runs-on: ubuntu-latest - defaults: - run: - shell: bash - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Setup Git User - run: | - git config --global user.email "npm-cli+bot@github.com" - git config --global user.name "npm CLI robot" - - name: Setup Node - uses: actions/setup-node@v3 - with: - node-version: 18.x - - name: Install npm@latest - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - name: npm Version - run: npm -v - - name: Install Dependencies - run: npm i --ignore-scripts --no-audit --no-fund - - name: Lint - run: npm run lint --ignore-scripts -w test-workspace - - name: Post Lint - run: npm run postlint --ignore-scripts -w test-workspace - - test: - name: Test - ${{ matrix.platform.name }} - ${{ matrix.node-version }} - if: github.repository_owner == 'npm' - strategy: - fail-fast: false - matrix: - platform: - - name: Linux - os: ubuntu-latest - shell: bash - - name: macOS - os: macos-latest - shell: bash - - name: Windows - os: windows-latest - shell: cmd - node-version: - - 14.17.0 - - 14.x - - 16.13.0 - - 16.x - - 18.0.0 - - 18.x - runs-on: ${{ matrix.platform.os }} - defaults: - run: - shell: ${{ matrix.platform.shell }} - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Setup Git User - run: | - git config --global user.email "npm-cli+bot@github.com" - git config --global user.name "npm CLI robot" - - name: Setup Node - uses: actions/setup-node@v3 - with: - node-version: ${{ matrix.node-version }} - - name: Update Windows npm - # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows - if: matrix.platform.os == 'windows-latest' && (startsWith(matrix.node-version, '12.') || startsWith(matrix.node-version, '14.')) - run: | - curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz - tar xf npm-7.5.4.tgz - cd package - node lib/npm.js install --no-fund --no-audit -g ..\npm-7.5.4.tgz - cd .. - rmdir /s /q package - - name: Install npm@7 - if: startsWith(matrix.node-version, '10.') - run: npm i --prefer-online --no-fund --no-audit -g npm@7 - - name: Install npm@latest - if: ${{ !startsWith(matrix.node-version, '10.') }} - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - name: npm Version - run: npm -v - - name: Install Dependencies - run: npm i --ignore-scripts --no-audit --no-fund - - name: Add Problem Matcher - run: echo "::add-matcher::.github/matchers/tap.json" - - name: Test - run: npm test --ignore-scripts -w test-workspace diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fede7162..07af9fea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,18 +4,34 @@ name: CI on: workflow_dispatch: + inputs: + ref: + required: true + type: string + check-sha: + type: string + all: + default: true + type: boolean + workflow_call: + inputs: + ref: + required: true + type: string + check-sha: + required: true + type: string + all: + default: true + type: boolean pull_request: - paths-ignore: - - workspace/test-workspace/** push: branches: - main - latest - paths-ignore: - - workspace/test-workspace/** schedule: - # "At 09:00 UTC (02:00 PT) on Monday" https://crontab.guru/#0_9_*_*_1 - - cron: "0 9 * * 1" + # "At 10:00 UTC (02:00 PT) on Monday" https://crontab.guru/#0_10_*_*_1 + - cron: "0 10 * * 1" jobs: lint: @@ -28,28 +44,52 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 - - name: Setup Git User - run: | - git config --global user.email "npm-cli+bot@github.com" - git config --global user.name "npm CLI robot" - - name: Setup Node - uses: actions/setup-node@v3 with: - node-version: 18.x - - name: Install npm@latest - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - name: npm Version - run: npm -v - - name: Install Dependencies - run: npm i --ignore-scripts --no-audit --no-fund + ref: ${{ inputs.ref }} + + - name: Create Check + uses: ./.github/actions/create-check + if: inputs.check-sha + id: check + with: + sha: ${{ inputs.check-sha }} + token: ${{ secrets.GITHUB_TOKEN }} + job-name: Lint + + - name: Setup + id: setup + continue-on-error: ${{ !!steps.check.outputs.check-id }} + uses: ./.github/actions/setup + + - name: Get Changed Workspaces + id: workspaces + continue-on-error: ${{ !!steps.check.outputs.check-id }} + uses: ./.github/actions/changed-workspaces + with: + token: ${{ secrets.GITHUB_TOKEN }} + all: ${{ inputs.all }} + - name: Lint - run: npm run lint --ignore-scripts - - name: Post Lint - run: npm run postlint --ignore-scripts + uses: ./.github/actions/lint + continue-on-error: ${{ !!steps.check.outputs.check-id }} + with: + flags: ${{ steps.workspaces.outputs.flags }} + + - name: Conclude Check + uses: ./.github/actions/conclude-check + if: steps.check.outputs.check-id && (success() || failure()) + with: + token: ${{ secrets.GITHUB_TOKEN }} + conclusion: ${{ job.status }} + check-id: ${{ steps.check.outputs.check-id }} test: name: Test - ${{ matrix.platform.name }} - ${{ matrix.node-version }} if: github.repository_owner == 'npm' + runs-on: ${{ matrix.platform.os }} + defaults: + run: + shell: ${{ matrix.platform.shell }} strategy: fail-fast: false matrix: @@ -70,42 +110,60 @@ jobs: - 16.x - 18.0.0 - 18.x - runs-on: ${{ matrix.platform.os }} - defaults: - run: - shell: ${{ matrix.platform.shell }} steps: + - name: Continue Matrix Run + id: continue-matrix + run: | + if [[ "${{ matrix.node-version }}" == "14.17.0" || "${{ inputs.all }}" == "true" ]]; then + echo "result=true" >> $GITHUB_OUTPUT + fi + - name: Checkout + if: steps.continue-matrix.outputs.result uses: actions/checkout@v3 - - name: Setup Git User - run: | - git config --global user.email "npm-cli+bot@github.com" - git config --global user.name "npm CLI robot" - - name: Setup Node - uses: actions/setup-node@v3 + with: + ref: ${{ inputs.ref }} + + - name: Create Check + if: steps.continue-matrix.outputs.result && inputs.check-sha + uses: ./.github/actions/create-check + id: check + with: + sha: ${{ inputs.check-sha }} + token: ${{ secrets.GITHUB_TOKEN }} + job-name: "Test - ${{ matrix.platform.name }} - ${{ matrix.node-version }}" + + - name: Setup + if: steps.continue-matrix.outputs.result + uses: ./.github/actions/setup + id: setup + continue-on-error: ${{ !!steps.check.outputs.check-id }} with: node-version: ${{ matrix.node-version }} - - name: Update Windows npm - # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows - if: matrix.platform.os == 'windows-latest' && (startsWith(matrix.node-version, '12.') || startsWith(matrix.node-version, '14.')) - run: | - curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz - tar xf npm-7.5.4.tgz - cd package - node lib/npm.js install --no-fund --no-audit -g ..\npm-7.5.4.tgz - cd .. - rmdir /s /q package - - name: Install npm@7 - if: startsWith(matrix.node-version, '10.') - run: npm i --prefer-online --no-fund --no-audit -g npm@7 - - name: Install npm@latest - if: ${{ !startsWith(matrix.node-version, '10.') }} - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - name: npm Version - run: npm -v - - name: Install Dependencies - run: npm i --ignore-scripts --no-audit --no-fund - - name: Add Problem Matcher - run: echo "::add-matcher::.github/matchers/tap.json" + shell: ${{ matrix.platform.shell }} + + - name: Get Changed Workspaces + if: steps.continue-matrix.outputs.result + id: workspaces + continue-on-error: ${{ !!steps.check.outputs.check-id }} + uses: ./.github/actions/changed-workspaces + with: + shell: ${{ matrix.platform.shell }} + token: ${{ secrets.GITHUB_TOKEN }} + all: ${{ inputs.all }} + - name: Test - run: npm test --ignore-scripts + if: steps.continue-matrix.outputs.result + uses: ./.github/actions/test + continue-on-error: ${{ !!steps.check.outputs.check-id }} + with: + flags: ${{ steps.workspaces.outputs.flags }} + shell: ${{ matrix.platform.shell }} + + - name: Conclude Check + uses: ./.github/actions/conclude-check + if: steps.continue-matrix.outputs.result && steps.check.outputs.check-id && (success() || failure()) + with: + token: ${{ secrets.GITHUB_TOKEN }} + conclusion: ${{ job.status }} + check-id: ${{ steps.check.outputs.check-id }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 66b9498a..54ab9f4b 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -12,13 +12,17 @@ on: - main - latest schedule: - # "At 10:00 UTC (03:00 PT) on Monday" https://crontab.guru/#0_10_*_*_1 - - cron: "0 10 * * 1" + # "At 11:00 UTC (03:00 PT) on Monday" https://crontab.guru/#0_11_*_*_1 + - cron: "0 11 * * 1" jobs: analyze: name: Analyze + if: github.repository_owner == 'npm' runs-on: ubuntu-latest + defaults: + run: + shell: bash permissions: actions: read contents: read @@ -26,13 +30,11 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 - - name: Setup Git User - run: | - git config --global user.email "npm-cli+bot@github.com" - git config --global user.name "npm CLI robot" + - name: Initialize CodeQL uses: github/codeql-action/init@v2 with: languages: javascript + - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/post-dependabot.yml b/.github/workflows/post-dependabot.yml index ce383405..381a2055 100644 --- a/.github/workflows/post-dependabot.yml +++ b/.github/workflows/post-dependabot.yml @@ -4,62 +4,51 @@ name: Post Dependabot on: pull_request -permissions: - contents: write - jobs: template-oss: name: template-oss + permissions: + contents: write if: github.repository_owner == 'npm' && github.actor == 'dependabot[bot]' runs-on: ubuntu-latest defaults: run: shell: bash steps: - - name: Checkout - uses: actions/checkout@v3 - with: - ref: ${{ github.event.pull_request.head.ref }} - - name: Setup Git User - run: | - git config --global user.email "npm-cli+bot@github.com" - git config --global user.name "npm CLI robot" - - name: Setup Node - uses: actions/setup-node@v3 - with: - node-version: 18.x - - name: Install npm@latest - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - name: npm Version - run: npm -v - - name: Install Dependencies - run: npm i --ignore-scripts --no-audit --no-fund - name: Fetch Dependabot Metadata id: metadata uses: dependabot/fetch-metadata@v1 with: github-token: ${{ secrets.GITHUB_TOKEN }} - # Dependabot can update multiple directories so we output which directory - # it is acting on so we can run the command for the correct root or workspace - - name: Get Dependabot Directory + - name: Is Dependency if: contains(steps.metadata.outputs.dependency-names, '@npmcli/template-oss') - id: flags - run: | - dependabot_dir="${{ steps.metadata.outputs.directory }}" - if [[ "$dependabot_dir" == "/" ]]; then - echo "workspace=-iwr" >> $GITHUB_OUTPUT - else - # strip leading slash from directory so it works as a - # a path to the workspace flag - echo "workspace=-w ${dependabot_dir#/}" >> $GITHUB_OUTPUT - fi + id: dependency + run: echo "continue=true" >> $GITHUB_OUTPUT + + - name: Checkout + if: steps.dependency.outputs.continue + uses: actions/checkout@v3 + with: + ref: ${{ github.event.pull_request.head.ref }} + + - name: Setup + if: steps.dependency.outputs.continue + uses: ./.github/actions/setup + + - name: Get Workspaces + if: steps.dependency.outputs.continue + uses: ./.github/actions/changed-workspaces + id: workspaces + with: + token: ${{ secrets.GITHUB_TOKEN }} + files: '["${{ steps.metadata.outputs.directory }}"]' - name: Apply Changes - if: steps.flags.outputs.workspace + if: steps.workspaces.outputs.flags id: apply run: | - npm run template-oss-apply ${{ steps.flags.outputs.workspace }} + npm run template-oss-apply ${{ steps.workspaces.outputs.flags }} if [[ `git status --porcelain` ]]; then echo "changes=true" >> $GITHUB_OUTPUT fi @@ -111,7 +100,7 @@ jobs: - name: Check Changes if: steps.apply.outputs.changes run: | - npm exec --offline ${{ steps.flags.outputs.workspace }} -- template-oss-check + npm exec --offline ${{ steps.workspaces.outputs.flags }} -- template-oss-check - name: Fail on Breaking Change if: steps.apply.outputs.changes && startsWith(steps.apply.outputs.message, 'feat!') diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 99877daa..f8e5fc9a 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -12,7 +12,7 @@ on: jobs: commitlint: - name: Lint Commits + name: Lint Commit if: github.repository_owner == 'npm' runs-on: ubuntu-latest defaults: @@ -23,26 +23,19 @@ jobs: uses: actions/checkout@v3 with: fetch-depth: 0 - - name: Setup Git User - run: | - git config --global user.email "npm-cli+bot@github.com" - git config --global user.name "npm CLI robot" - - name: Setup Node - uses: actions/setup-node@v3 - with: - node-version: 18.x - - name: Install npm@latest - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - name: npm Version - run: npm -v - - name: Install Dependencies - run: npm i --ignore-scripts --no-audit --no-fund + + - name: Setup + uses: ./.github/actions/setup + - name: Run Commitlint on Commits id: commit continue-on-error: true run: | - npx --offline commitlint -V --from 'origin/${{ github.base_ref }}' --to ${{ github.event.pull_request.head.sha }} + npx --offline commitlint -V --from 'origin/${{ github.base_ref }}' --to '${{ github.event.pull_request.head.sha }}' + - name: Run Commitlint on PR Title if: steps.commit.outcome == 'failure' + env: + PR_TITLE: ${{ github.event.pull_request.title }} run: | - echo '${{ github.event.pull_request.title }}' | npx --offline commitlint -V + echo "$PR_TITLE" | npx --offline commitlint -V diff --git a/.github/workflows/release-integration.yml b/.github/workflows/release-integration.yml new file mode 100644 index 00000000..964a00bd --- /dev/null +++ b/.github/workflows/release-integration.yml @@ -0,0 +1,57 @@ +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Release Integration + +on: + workflow_call: + inputs: + release: + required: true + type: string + releases: + required: true + type: string + +jobs: + check-registry: + name: Check Registry + if: github.repository_owner == 'npm' + runs-on: ubuntu-latest + defaults: + run: + shell: bash + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup + uses: ./.github/actions/setup + with: + deps: false + + - name: View in Registry + run: | + EXIT_CODE=0 + + function is_published { + if npm view "$@" --loglevel=error > /dev/null; then + echo 0 + else + echo 1 + fi + } + + for release in $(echo '${{ needs.release.outputs.releases }}' | jq -r '.[] | @base64'); do + name=$(echo "$release" | base64 --decode | jq -r .pkgName) + version=$(echo "$release" | base64 --decode | jq -r .version) + spec="$name@$version" + status=$(is_published "$spec") + if [[ "$status" -eq 1 ]]; then + echo "$spec ERROR" + EXIT_CODE=$status + else + echo "$spec OK" + fi + done + + exit $EXIT_CODE diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 84fd7d17..377f92e5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,9 +10,10 @@ on: type: string push: branches: - - main - - latest - - release/v* + branches: + - main + - latest + - release/v* permissions: contents: write @@ -21,150 +22,95 @@ permissions: jobs: release: - outputs: - pr: ${{ steps.release.outputs.pr }} - release: ${{ steps.release.outputs.release }} - releases: ${{ steps.release.outputs.releases }} - branch: ${{ steps.release.outputs.pr-branch }} - pr-number: ${{ steps.release.outputs.pr-number }} - comment-id: ${{ steps.pr-comment.outputs.result }} - check-id: ${{ steps.check.outputs.check_id }} name: Release if: github.repository_owner == 'npm' runs-on: ubuntu-latest defaults: run: shell: bash + outputs: + pr: ${{ steps.release.outputs.pr }} + release: ${{ steps.release.outputs.release }} + releases: ${{ steps.release.outputs.releases }} + pr-branch: ${{ steps.release.outputs.pr-branch }} + pr-number: ${{ steps.release.outputs.pr-number }} + comment-id: ${{ steps.pr-comment.outputs.comment-id }} + check-id: ${{ steps.check.outputs.check-id }} steps: - name: Checkout uses: actions/checkout@v3 - - name: Setup Git User - run: | - git config --global user.email "npm-cli+bot@github.com" - git config --global user.name "npm CLI robot" - - name: Setup Node - uses: actions/setup-node@v3 - with: - node-version: 18.x - - name: Install npm@latest - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - name: npm Version - run: npm -v - - name: Install Dependencies - run: npm i --ignore-scripts --no-audit --no-fund + + - name: Setup + uses: ./.github/actions/setup + - name: Release Please id: release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | npx --offline template-oss-release-please "${{ github.ref_name }}" "${{ inputs.release-pr }}" - - name: Post Pull Request Comment - if: steps.release.outputs.pr-number + + # If we have opened a release PR, then immediately create an "in_progress" + # check for it so the GitHub UI doesn't report that its mergeable. + # This check will be swapped out for real CI checks once those are started. + - name: Create Check + uses: ./.github/actions/create-check + if: steps.release.outputs.pr-sha + id: check + with: + sha: ${{ steps.release.outputs.pr-sha }} + token: ${{ secrets.GITHUB_TOKEN }} + job-name: Release + + - name: Comment Text uses: actions/github-script@v6 - id: pr-comment + if: steps.release.outputs.pr-number + id: comment-text env: PR_NUMBER: ${{ steps.release.outputs.pr-number }} REF_NAME: ${{ github.ref_name }} with: + result-encoding: string script: | - const { REF_NAME, PR_NUMBER: issue_number } = process.env const { runId, repo: { owner, repo } } = context - const { data: workflow } = await github.rest.actions.getWorkflowRun({ owner, repo, run_id: runId }) - let body = '## Release Manager\n\n' - - const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number }) - let commentId = comments.find(c => c.user.login === 'github-actions[bot]' && c.body.startsWith(body))?.id - body += `Release workflow run: ${workflow.html_url}\n\n#### Force CI to Update This Release\n\n` body += `This PR will be updated and CI will run for every non-\`chore:\` commit that is pushed to \`main\`. ` body += `To force CI to update this PR, run this command:\n\n` - body += `\`\`\`\ngh workflow run release.yml -r ${REF_NAME} -R ${owner}/${repo} -f release-pr=${issue_number}\n\`\`\`` - - if (commentId) { - await github.rest.issues.updateComment({ owner, repo, comment_id: commentId, body }) - } else { - const { data: comment } = await github.rest.issues.createComment({ owner, repo, issue_number, body }) - commentId = comment?.id - } - - return commentId - - name: Get Workflow Job - uses: actions/github-script@v6 - if: steps.release.outputs.pr-sha - id: check-output - env: - JOB_NAME: "Release" - MATRIX_NAME: "" - with: - script: | - const { owner, repo } = context.repo - - const { data } = await github.rest.actions.listJobsForWorkflowRun({ - owner, - repo, - run_id: context.runId, - per_page: 100 - }) - - const jobName = process.env.JOB_NAME + process.env.MATRIX_NAME - const job = data.jobs.find(j => j.name.endsWith(jobName)) - const jobUrl = job?.html_url - - const shaUrl = `${context.serverUrl}/${owner}/${repo}/commit/${{ steps.release.outputs.pr-sha }}` - - let summary = `This check is assosciated with ${shaUrl}\n\n` - - if (jobUrl) { - summary += `For run logs, click here: ${jobUrl}` - } else { - summary += `Run logs could not be found for a job with name: "${jobName}"` - } + body += `\`\`\`\ngh workflow run release.yml -r ${process.env.REF_NAME} -R ${owner}/${repo} -f release-pr=${process.env.PR_NUMBER}\n\`\`\`` + return body - return { summary } - - name: Create Check - uses: LouisBrunner/checks-action@v1.3.1 - id: check - if: steps.release.outputs.pr-sha + - name: Post Pull Request Comment + if: steps.comment-text.outputs.result + uses: ./.github/actions/upsert-comment + id: pr-comment with: token: ${{ secrets.GITHUB_TOKEN }} - status: in_progress - name: Release - sha: ${{ steps.release.outputs.pr-sha }} - output: ${{ steps.check-output.outputs.result }} + body: ${{ steps.comment-text.outputs.result }} + number: ${{ steps.release.outputs.pr-number }} update: - needs: release - outputs: - sha: ${{ steps.commit.outputs.sha }} - check-id: ${{ steps.check.outputs.check_id }} - name: Update - Release - if: github.repository_owner == 'npm' && needs.release.outputs.pr + name: Release PR - Update runs-on: ubuntu-latest defaults: run: shell: bash + if: needs.release.outputs.pr + needs: release + outputs: + sha: ${{ steps.commit.outputs.sha }} + check-id: ${{ steps.check.outputs.check-id }} steps: - name: Checkout uses: actions/checkout@v3 with: + ref: ${{ needs.release.outputs.pr-branch }} fetch-depth: 0 - ref: ${{ needs.release.outputs.branch }} - - name: Setup Git User - run: | - git config --global user.email "npm-cli+bot@github.com" - git config --global user.name "npm CLI robot" - - name: Setup Node - uses: actions/setup-node@v3 - with: - node-version: 18.x - - name: Install npm@latest - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - name: npm Version - run: npm -v - - name: Install Dependencies - run: npm i --ignore-scripts --no-audit --no-fund + + - name: Setup + uses: ./.github/actions/setup + - name: Run Post Pull Request Actions env: RELEASE_PR_NUMBER: ${{ needs.release.outputs.pr-number }} @@ -173,6 +119,7 @@ jobs: run: | npm exec --offline -- template-oss-release-manager --lockfile=false npm run rp-pull-request --ignore-scripts -ws -iwr --if-present + - name: Commit id: commit env: @@ -181,79 +128,45 @@ jobs: git commit --all --amend --no-edit || true git push --force-with-lease echo "sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT - - name: Get Workflow Job - uses: actions/github-script@v6 - if: steps.commit.outputs.sha - id: check-output - env: - JOB_NAME: "Update - Release" - MATRIX_NAME: "" - with: - script: | - const { owner, repo } = context.repo - - const { data } = await github.rest.actions.listJobsForWorkflowRun({ - owner, - repo, - run_id: context.runId, - per_page: 100 - }) - - const jobName = process.env.JOB_NAME + process.env.MATRIX_NAME - const job = data.jobs.find(j => j.name.endsWith(jobName)) - const jobUrl = job?.html_url - - const shaUrl = `${context.serverUrl}/${owner}/${repo}/commit/${{ steps.commit.outputs.sha }}` - - let summary = `This check is assosciated with ${shaUrl}\n\n` - - if (jobUrl) { - summary += `For run logs, click here: ${jobUrl}` - } else { - summary += `Run logs could not be found for a job with name: "${jobName}"` - } - return { summary } - name: Create Check - uses: LouisBrunner/checks-action@v1.3.1 - id: check + uses: ./.github/actions/create-check if: steps.commit.outputs.sha + id: check with: + sha: ${{ steps.vommit.outputs.sha }} token: ${{ secrets.GITHUB_TOKEN }} - status: in_progress - name: Release - sha: ${{ steps.commit.outputs.sha }} - output: ${{ steps.check-output.outputs.result }} + job-name: Release + - name: Conclude Check - uses: LouisBrunner/checks-action@v1.3.1 - if: needs.release.outputs.check-id && always() + uses: ./.github/actions/conclude-check + if: needs.release.outputs.check-id && (success() || failure()) with: token: ${{ secrets.GITHUB_TOKEN }} conclusion: ${{ job.status }} - check_id: ${{ needs.release.outputs.check-id }} + check-id: ${{ needs.release.outputs.check-id }} ci: - name: CI - Release + name: Release PR - CI needs: [ release, update ] if: needs.release.outputs.pr - uses: ./.github/workflows/ci-release.yml + uses: ./.github/workflows/ci.yml with: - ref: ${{ needs.release.outputs.branch }} + ref: ${{ needs.release.outputs.pr-branch }} check-sha: ${{ needs.update.outputs.sha }} post-ci: - needs: [ release, update, ci ] - name: Post CI - Release - if: github.repository_owner == 'npm' && needs.release.outputs.pr && always() + name: Relase PR - Post CI runs-on: ubuntu-latest defaults: run: shell: bash + needs: [ release, update, ci ] + if: needs.release.outputs.pr && (success() || failure()) steps: - name: Get Needs Result id: needs-result run: | - result="" if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then result="failure" elif [[ "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then @@ -262,108 +175,76 @@ jobs: result="success" fi echo "result=$result" >> $GITHUB_OUTPUT + - name: Conclude Check - uses: LouisBrunner/checks-action@v1.3.1 - if: needs.update.outputs.check-id && always() + uses: ./.github/actions/conclude-check + if: needs.update.outputs.check-id && (success() || failure()) with: token: ${{ secrets.GITHUB_TOKEN }} conclusion: ${{ steps.needs-result.outputs.result }} - check_id: ${{ needs.update.outputs.check-id }} + check-id: ${{ needs.update.outputs.check-id }} post-release: - needs: release - name: Post Release - Release - if: github.repository_owner == 'npm' && needs.release.outputs.releases + name: Post Release runs-on: ubuntu-latest defaults: run: shell: bash + needs: release + if: needs.release.outputs.releases steps: - - name: Create Release PR Comment + - name: Comment Text uses: actions/github-script@v6 + id: comment-text env: RELEASES: ${{ needs.release.outputs.releases }} with: + result-encoding: string script: | const releases = JSON.parse(process.env.RELEASES) const { runId, repo: { owner, repo } } = context const issue_number = releases[0].prNumber + const releasePleaseComments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number }) + .then((comments) => comments.filter(c => c.login === 'github-actions[bot]' && c.body.includes('Release is at'))) + + for (const comment of releasePleaseComments) { + await github.rest.issues.deleteComment({ owner, repo, comment_id: comment.id }) + } + let body = '## Release Workflow\n\n' for (const { pkgName, version, url } of releases) { body += `- \`${pkgName}@${version}\` ${url}\n` } + body += `- Workflow run: :arrows_counterclockwise: https://github.com/${owner}/${repo}/actions/runs/${runId}` + return body - const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number }) - .then(cs => cs.map(c => ({ id: c.id, login: c.user.login, body: c.body }))) - console.log(`Found comments: ${JSON.stringify(comments, null, 2)}`) - const releaseComments = comments.filter(c => c.login === 'github-actions[bot]' && c.body.includes('Release is at')) - - for (const comment of releaseComments) { - console.log(`Release comment: ${JSON.stringify(comment, null, 2)}`) - await github.rest.issues.deleteComment({ owner, repo, comment_id: comment.id }) - } - - const runUrl = `https://github.com/${owner}/${repo}/actions/runs/${runId}` - await github.rest.issues.createComment({ - owner, - repo, - issue_number, - body: `${body}- Workflow run: :arrows_counterclockwise: ${runUrl}`, - }) + - name: Create Release PR Comment + if: steps.comment-text.outputs.result + uses: ./.github/actions/upsert-comment + with: + token: ${{ secrets.GITHUB_TOKEN }} + body: ${{ steps.comment-text.outputs.result }} + number: ${{ fromJson(needs.release.outputs.release).prNumber }} + includes: ${{ github.run_id }} release-integration: + name: Post Release - Integration needs: release - name: Release Integration if: needs.release.outputs.release - runs-on: ubuntu-latest - defaults: - run: - shell: bash - steps: - - name: Setup Node - uses: actions/setup-node@v3 - with: - node-version: 18.x - - name: Install npm@latest - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - name: npm Version - run: npm -v - - name: View in Registry - run: | - EXIT_CODE=0 - - function is_published { - if npm view "$@" --loglevel=error > /dev/null; then - echo 0 - else - echo 1 - fi - } - - for release in $(echo '${{ needs.release.outputs.releases }}' | jq -r '.[] | @base64'); do - name=$(echo "$release" | base64 --decode | jq -r .pkgName) - version=$(echo "$release" | base64 --decode | jq -r .version) - spec="$name@$version" - status=$(is_published "$spec") - if [[ "$status" -eq 1 ]]; then - echo "$spec ERROR" - EXIT_CODE=$status - else - echo "$spec OK" - fi - done - - exit $EXIT_CODE + uses: ./.github/workflows/release-integration.yml + with: + release: needs.release.outputs.release + releases: needs.release.outputs.releases post-release-integration: - needs: [ release, release-integration ] - name: Post Release Integration - Release - if: github.repository_owner == 'npm' && needs.release.outputs.release && always() + name: Post Release - Post Integration runs-on: ubuntu-latest defaults: run: shell: bash + needs: [ release, release-integration ] + if: needs.release.outputs.release && (success() || failure()) steps: - name: Get Needs Result id: needs-result @@ -376,39 +257,37 @@ jobs: result="white_check_mark" fi echo "result=$result" >> $GITHUB_OUTPUT - - name: Update Release PR Comment + + - name: Comment Text uses: actions/github-script@v6 + id: comment-text env: PR_NUMBER: ${{ fromJSON(needs.release.outputs.release).prNumber }} + REF_NAME: ${{ github.ref_name }} RESULT: ${{ steps.needs-result.outputs.result }} with: script: | - const { PR_NUMBER: issue_number, RESULT } = process.env - const { runId, repo: { owner, repo } } = context - - const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number }) - const updateComment = comments.find(c => - c.user.login === 'github-actions[bot]' && - c.body.startsWith('## Release Workflow\n\n') && - c.body.includes(runId) - ) - - if (updateComment) { - console.log('Found comment to update:', JSON.stringify(updateComment, null, 2)) - let body = updateComment.body.replace(/Workflow run: :[a-z_]+:/, `Workflow run: :${RESULT}:`) - const tagCodeowner = RESULT !== 'white_check_mark' - if (tagCodeowner) { - body += `\n\n:rotating_light:` - body += ` @npm/cli-team: The post-release workflow failed for this release.` - body += ` Manual steps may need to be taken after examining the workflow output` - body += ` from the above workflow run. :rotating_light:` - } - await github.rest.issues.updateComment({ - owner, - repo, - body, - comment_id: updateComment.id, - }) - } else { - console.log('No matching comments found:', JSON.stringify(comments, null, 2)) + const { RESULT, PR_NUMBER, REF_NAME } = process.env + const tagCodeowner = RESULT !== 'white_check_mark' + if (tagCodeowner) { + let body = '' + body += `\n\n:rotating_light:` + body += ` @npm/cli-team: The post-release workflow failed for this release.` + body += ` Manual steps may need to be taken after examining the workflow output` + body += ` from the above workflow run. :rotating_light:` + body += `\n\nTo rerun the workflow run the following command:\n\n` + body += `\`\`\`\ngh workflow run release.yml -r ${REF_NAME} -R ${owner}/${repo} -f release-pr=${PR_NUMBER}\n\`\`\`` + return body } + + - name: Update Release PR Comment + if: steps.comment-text.outputs.result + uses: ./.github/actions/upsert-comment + with: + token: ${{ secrets.GITHUB_TOKEN }} + body: "## Release Workflow" + find: "Workflow run: :[a-z_]+:" + replace: "Workflow run :${{ steps.needs-result.outputs.result }}:" + append: ${{ steps.comment-text.outputs.result }} + number: ${{ fromJson(needs.release.outputs.release).prNumber }} + includes: ${{ github.run_id }} diff --git a/README.md b/README.md index 1a423510..a06b23c5 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,12 @@ ## @npmcli/template-oss -This module bundles the npm CLI team's basics for package development into a -single devDependency. +This module bundles the npm CLI team's basics for package development into a single devDependency. **CAUTION: THESE CHANGES WILL OVERWRITE ANY LOCAL FILES AND SETTINGS** ### Configuration -Configure the use of `@npmcli/template-oss` in your `package.json` using the -`templateOSS` property. - +Configure the use of `@npmcli/template-oss` in your `package.json` using the `templateOSS` property. ```js { @@ -39,9 +36,7 @@ Configure the use of `@npmcli/template-oss` in your `package.json` using the #### Workspaces -Individual workspaces can also supply their own config, if they are included by -the root package's `templateOSS.workspaces` array. These settings will override -any of the same settings in the root. +Individual workspaces can also supply their own config, if they are included by the root package's `templateOSS.workspaces` array. These settings will override any of the same settings in the root. ```js { @@ -59,20 +54,15 @@ any of the same settings in the root. ### Content -All the templated content for this repo lives in -[`lib/content/`](./lib/content/). The `index.js`[./lib/content/index.js] file -controls how and where this content is written. +All the templated content for this repo lives in [`lib/content/`](./lib/content/). The `index.js`[./lib/content/index.js] file controls how and where this content is written. -Content files can be overwritten or merged with the existing target file. -Currently mergining is only supported for `package.json` files. +Content files can be overwritten or merged with the existing target file. Currently mergining is only supported for `package.json` files. Each content file goes through the following pipeline: 1. It is read from its source location -1. It is are templated using Handlebars with the variables from each packages's - config (with some derived values generated in [`config.js`](./lib/config.js) -1. It is parsed based on its file extension in - [`parser.js`](./lib/util/parser.js) +1. It is are templated using Handlebars with the variables from each packages's config (with some derived values generated in [`config.js`](./lib/config.js) +1. It is parsed based on its file extension in [`parser.js`](./lib/util/parser.js) 1. Additional logic is applied by the parser 1. It is written to its target location @@ -82,15 +72,11 @@ This package provides two bin scripts: #### `template-oss-check` -This will check if any of the applied files different from the target content, -or if any of the other associated checks fail. The diffs of each file or check -will be reported with instructions on how to fix it. +This will check if any of the applied files different from the target content, or if any of the other associated checks fail. The diffs of each file or check will be reported with instructions on how to fix it. #### `template-oss-apply [--force]` -This will write all source files to their target locations in the cwd. It will -do nothing if `package.json#templateOSS.version` is the same as the version -being run. This can be overridden by `--force`. +This will write all source files to their target locations in the cwd. It will do nothing if `package.json#templateOSS.version` is the same as the version being run. This can be overridden by `--force`. This is the script that is run on `postinsall`. @@ -98,24 +84,14 @@ This is the script that is run on `postinsall`. #### `lib/apply` -This directory is where all the logic for applying files lives. It should be -possible to add new files without modifying anything in this directory. To add a -file, add the templated file to `lib/content/$FILENAME` and add entry for it in -`lib/content/index.js` depending on where and when it should be written (root vs -workspace, repo vs module, add vs remove, etc). +This directory is where all the logic for applying files lives. It should be possible to add new files without modifying anything in this directory. To add a file, add the templated file to `lib/content/$FILENAME` and add entry for it in `lib/content/index.js` depending on where and when it should be written (root vs workspace, repo vs module, add vs remove, etc). #### `lib/check` -All checks live in this directory and have the same signature. A check must be -added to `lib/check/index.js` for it to be run. +All checks live in this directory and have the same signature. A check must be added to `lib/check/index.js` for it to be run. #### Generic vs specific extensions -This repo is designed so that all (fine, most) of the logic in `lib/` is generic -and could be applied across projects of many different types. +This repo is designed so that all (fine, most) of the logic in `lib/` is generic and could be applied across projects of many different types. -The files in `lib/content` are extremely specific to the npm CLI. It would be -trivial to swap out this content directory for a different one as it is only -referenced in a single place in `lib/config.js`. However, it's not currently -possible to change this value at runtime, but that might become possible in -future versions of this package. +The files in `lib/content` are extremely specific to the npm CLI. It would be trivial to swap out this content directory for a different one as it is only referenced in a single place in `lib/config.js`. However, it's not currently possible to change this value at runtime, but that might become possible in future versions of this package. diff --git a/bin/changed-workspaces.js b/bin/changed-workspaces.js new file mode 100755 index 00000000..c36d75db --- /dev/null +++ b/bin/changed-workspaces.js @@ -0,0 +1,58 @@ +#!/usr/bin/env node + +const { join, relative } = require('path') +const mapWorkspaces = require('@npmcli/map-workspaces') + +const wsFlags = ({ root, workspaces }) => { + return [root ? '-iwr' : '', ...workspaces.map(ws => `-w="${ws.name}"`)].join(' ').trim() +} + +const main = async ({ cwd, files, all }) => { + const wsMap = await mapWorkspaces({ + cwd, + pkg: require(join(cwd, 'package.json')), + }) + + const workspaces = [...wsMap.entries()].map(([name, path]) => ({ + name, + path: relative(cwd, path), + })) + + if (all) { + return wsFlags({ root: true, workspaces }) + } + + let changedRoot = false + const changedWorkspaces = new Set() + + for (const file of files) { + let foundWs = false + for (const ws of workspaces) { + foundWs = file.startsWith(ws.path) + if (foundWs) { + changedWorkspaces.add(ws) + break + } + } + if (!foundWs) { + changedRoot = true + } + } + + return wsFlags({ root: changedRoot, workspaces: [...changedWorkspaces.values()] }) +} + +const arg = process.argv[2] +const all = arg === '--all' +const files = all ? [] : JSON.parse(arg ?? '[]') + +module.exports = main({ + cwd: process.cwd(), + files, + all, +}) + .then((r) => process.stdout.write(r)) + .catch((err) => { + console.error(err.stack) + process.exitCode = 1 + }) diff --git a/bin/config.js b/bin/config.js new file mode 100755 index 00000000..942e0c3b --- /dev/null +++ b/bin/config.js @@ -0,0 +1,26 @@ +#!/usr/bin/env node + +const run = require('../lib/index.js') + +const main = async () => { + const { + npm_config_global: globalMode, + npm_config_local_prefix: root, + } = process.env + + // do nothing in global mode or when the local prefix isn't set + if (globalMode === 'true' || !root) { + return + } + + await run(root, [{ + run: (options) => console.log(JSON.stringify(options, null, 2)), + when: () => true, + name: 'get-config', + }]) +} + +module.exports = main().catch((err) => { + console.error(err.stack) + process.exitCode = 1 +}) diff --git a/lib/config.js b/lib/config.js index f2da45d9..ced12de6 100644 --- a/lib/config.js +++ b/lib/config.js @@ -186,7 +186,6 @@ const getFullConfig = async ({ pkgDir: posixDir(pkgPath), pkgGlob: posixGlob(pkgPath), pkgFlags: isWorkspace ? `-w ${pkg.pkgJson.name}` : '', - allFlags: isMono ? '-ws -iwr --if-present' : '', workspacePaths, workspaceGlobs: workspacePaths.map(posixGlob), // booleans to control application of updates @@ -201,7 +200,7 @@ const getFullConfig = async ({ rootNpxPath: npxPath.root, // lockfiles are only present at the root, so this only should be set for // all workspaces based on the root - lockfile: rootPkgConfig.lockfile, + lockfile: !!rootPkgConfig.lockfile, // gitignore ignorePaths: [ ...gitignore.sort([ diff --git a/lib/content/_job-release-integration.yml b/lib/content/_job-release-integration.yml deleted file mode 100644 index 098d2236..00000000 --- a/lib/content/_job-release-integration.yml +++ /dev/null @@ -1,32 +0,0 @@ -runs-on: ubuntu-latest -defaults: - run: - shell: bash -steps: - {{> stepNode lockfile=false }} - - name: View in Registry - run: | - EXIT_CODE=0 - - function is_published { - if npm view "$@" --loglevel=error > /dev/null; then - echo 0 - else - echo 1 - fi - } - - for release in $(echo '$\{{ needs.release.outputs.releases }}' | jq -r '.[] | @base64'); do - name=$(echo "$release" | base64 --decode | jq -r .pkgName) - version=$(echo "$release" | base64 --decode | jq -r .version) - spec="$name@$version" - status=$(is_published "$spec") - if [[ "$status" -eq 1 ]]; then - echo "$spec ERROR" - EXIT_CODE=$status - else - echo "$spec OK" - fi - done - - exit $EXIT_CODE diff --git a/lib/content/_job.yml b/lib/content/_job.yml deleted file mode 100644 index 48c6100a..00000000 --- a/lib/content/_job.yml +++ /dev/null @@ -1,8 +0,0 @@ -name: {{ jobName }} -if: github.repository_owner == 'npm' {{~#if jobIf}} && {{{ jobIf }}}{{/if}} -runs-on: ubuntu-latest -defaults: - run: - shell: bash -steps: - {{> stepsSetup }} diff --git a/lib/content/_on-ci.yml b/lib/content/_on-ci.yml deleted file mode 100644 index 151b31ba..00000000 --- a/lib/content/_on-ci.yml +++ /dev/null @@ -1,30 +0,0 @@ -workflow_dispatch: -pull_request: - {{#if isWorkspace}} - paths: - - {{ pkgGlob }} - {{/if}} - {{#if isRootMono}} - paths-ignore: - {{#each workspaceGlobs}} - - {{ . }} - {{/each}} - {{/if}} -push: - branches: - {{#each branches}} - - {{ . }} - {{/each}} - {{#if isWorkspace}} - paths: - - {{ pkgGlob }} - {{/if}} - {{#if isRootMono}} - paths-ignore: - {{#each workspaceGlobs}} - - {{ . }} - {{/each}} - {{/if}} -schedule: - # "At 09:00 UTC (02:00 PT) on Monday" https://crontab.guru/#0_9_*_*_1 - - cron: "0 9 * * 1" diff --git a/lib/content/_step-audit.yml b/lib/content/_step-audit.yml deleted file mode 100644 index 95003c65..00000000 --- a/lib/content/_step-audit.yml +++ /dev/null @@ -1,4 +0,0 @@ -- name: Run Production Audit - run: {{ rootNpmPath }} audit --omit=dev -- name: Run Full Audit - run: {{ rootNpmPath }} audit --audit-level=none diff --git a/lib/content/_step-checks.yml b/lib/content/_step-checks.yml deleted file mode 100644 index 3eb8cc56..00000000 --- a/lib/content/_step-checks.yml +++ /dev/null @@ -1,54 +0,0 @@ -{{#if jobCheck.sha}} -- name: Get Workflow Job - uses: actions/github-script@v6 - if: {{ jobCheck.sha }} - id: check-output - env: - JOB_NAME: "{{#if jobName}}{{ jobName }}{{else}}{{ jobCheck.name }}{{/if}}" - MATRIX_NAME: "{{#if jobIsMatrix}} - $\{{ matrix.platform.name }} - $\{{ matrix.node-version }}{{/if}}" - with: - script: | - const { owner, repo } = context.repo - - const { data } = await github.rest.actions.listJobsForWorkflowRun({ - owner, - repo, - run_id: context.runId, - per_page: 100 - }) - - const jobName = process.env.JOB_NAME + process.env.MATRIX_NAME - const job = data.jobs.find(j => j.name.endsWith(jobName)) - const jobUrl = job?.html_url - - const shaUrl = `${context.serverUrl}/${owner}/${repo}/commit/$\{{ {{ jobCheck.sha }} }}` - - let summary = `This check is assosciated with ${shaUrl}\n\n` - - if (jobUrl) { - summary += `For run logs, click here: ${jobUrl}` - } else { - summary += `Run logs could not be found for a job with name: "${jobName}"` - } - - return { summary } -{{/if}} -- name: {{#if jobCheck.sha}}Create{{else}}Conclude{{/if}} Check - uses: LouisBrunner/checks-action@v1.3.1 - {{#if jobCheck.sha}} - id: check - if: {{ jobCheck.sha }} - {{else}} - if: {{#if jobCheck.id}}{{ jobCheck.id }}{{else}}steps.check.outputs.check_id{{/if}} && always() - {{/if}} - with: - token: $\{{ secrets.GITHUB_TOKEN }} - {{#if jobCheck.sha}} - status: {{#if jobCheck.status}}{{ jobCheck.status }}{{else}}in_progress{{/if}} - name: {{#if jobCheck.name}}{{ jobCheck.name }}{{else}}{{ jobName }}{{/if}}{{#if jobIsMatrix}} - $\{{ matrix.platform.name }} - $\{{ matrix.node-version }}{{/if}} - sha: $\{{ {{ jobCheck.sha }} }} - output: $\{{ steps.check-output.outputs.result }} - {{else}} - conclusion: $\{{ {{#if jobCheck.status}}{{ jobCheck.status }}{{else}}job.status{{/if}} }} - check_id: $\{{ {{#if jobCheck.id}}{{ jobCheck.id }}{{else}}steps.check.outputs.check_id{{/if}} }} - {{/if}} diff --git a/lib/content/_step-deps.yml b/lib/content/_step-deps.yml deleted file mode 100644 index de65db92..00000000 --- a/lib/content/_step-deps.yml +++ /dev/null @@ -1,2 +0,0 @@ -- name: Install Dependencies - run: {{ rootNpmPath }} i --ignore-scripts --no-audit --no-fund {{~#if jobDepFlags}} {{ jobDepFlags }}{{/if}} diff --git a/lib/content/_step-git.yml b/lib/content/_step-git.yml deleted file mode 100644 index 2211d118..00000000 --- a/lib/content/_step-git.yml +++ /dev/null @@ -1,12 +0,0 @@ -- name: Checkout - uses: actions/checkout@v3 - {{#if jobCheckout}} - with: - {{#each jobCheckout}} - {{ @key }}: {{ this }} - {{/each}} - {{/if}} -- name: Setup Git User - run: | - git config --global user.email "npm-cli+bot@github.com" - git config --global user.name "npm CLI robot" diff --git a/lib/content/_step-lint.yml b/lib/content/_step-lint.yml deleted file mode 100644 index 8c8ff7d6..00000000 --- a/lib/content/_step-lint.yml +++ /dev/null @@ -1,4 +0,0 @@ -- name: Lint - run: {{ rootNpmPath }} run lint --ignore-scripts {{~#if jobRunFlags}} {{ jobRunFlags }}{{/if}} -- name: Post Lint - run: {{ rootNpmPath }} run postlint --ignore-scripts {{~#if jobRunFlags}} {{ jobRunFlags }}{{/if}} diff --git a/lib/content/_step-node.yml b/lib/content/_step-node.yml deleted file mode 100644 index 4d54a3ab..00000000 --- a/lib/content/_step-node.yml +++ /dev/null @@ -1,31 +0,0 @@ -- name: Setup Node - uses: actions/setup-node@v3 - with: - node-version: {{#if jobIsMatrix}}$\{{ matrix.node-version }}{{else}}{{ last ciVersions }}{{/if}} - {{#if lockfile}} - cache: npm - {{/if}} -{{#if updateNpm}} -{{#if jobIsMatrix}} -- name: Update Windows npm - # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows - if: matrix.platform.os == 'windows-latest' && (startsWith(matrix.node-version, '12.') || startsWith(matrix.node-version, '14.')) - run: | - curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz - tar xf npm-7.5.4.tgz - cd package - node lib/npm.js install --no-fund --no-audit -g ..\npm-7.5.4.tgz - cd .. - rmdir /s /q package -- name: Install npm@7 - if: startsWith(matrix.node-version, '10.') - run: npm i --prefer-online --no-fund --no-audit -g npm@7 -- name: Install npm@{{ npmSpec }} - if: $\{{ !startsWith(matrix.node-version, '10.') }} -{{else}} -- name: Install npm@{{ npmSpec }} -{{/if}} - run: npm i --prefer-online --no-fund --no-audit -g npm@{{ npmSpec }} -- name: npm Version - run: npm -v -{{/if}} diff --git a/lib/content/_step-test.yml b/lib/content/_step-test.yml deleted file mode 100644 index 2a869cd9..00000000 --- a/lib/content/_step-test.yml +++ /dev/null @@ -1,4 +0,0 @@ -- name: Add Problem Matcher - run: echo "::add-matcher::.github/matchers/tap.json" -- name: Test - run: {{ rootNpmPath }} test --ignore-scripts {{~#if jobRunFlags}} {{ jobRunFlags }}{{/if}} diff --git a/lib/content/_steps-setup.yml b/lib/content/_steps-setup.yml deleted file mode 100644 index e17d5f3d..00000000 --- a/lib/content/_steps-setup.yml +++ /dev/null @@ -1,6 +0,0 @@ -{{~#if jobCheck}}{{> stepChecks }}{{/if}} -{{~#unless jobSkipSetup}} -{{> stepGit }} -{{> stepNode }} -{{> stepDeps }} -{{/unless}} diff --git a/lib/content/actions/audit.yml b/lib/content/actions/audit.yml new file mode 100644 index 00000000..14c4f25d --- /dev/null +++ b/lib/content/actions/audit.yml @@ -0,0 +1,15 @@ +name: Audit + +inputs: + shell: + description: shell to run on + default: {{ shell }} + +runs: + using: composite + steps: + - name: Run Audit + shell: $\{{ inputs.shell }} + run: | + {{ rootNpmPath }} audit --omit=dev + {{ rootNpmPath }} audit --audit-level=none diff --git a/lib/content/actions/changed-files.yml b/lib/content/actions/changed-files.yml new file mode 100644 index 00000000..fc326245 --- /dev/null +++ b/lib/content/actions/changed-files.yml @@ -0,0 +1,37 @@ +name: Get Changed Files + +inputs: + token: + description: GitHub token to use + required: true + +outputs: + files: + value: $\{{ steps.files.outputs.result }} + +runs: + using: composite + steps: + - name: Get Changed Files + uses: actions/github-script@v6 + id: files + with: + github-token: $\{{ inputs.token }} + script: | + const { repo: { owner, repo }, eventName, payload, sha } = context + let files + if (eventName === 'pull_request' || eventName === 'pull_request_target') { + files = await github.paginate(github.rest.pulls.listFiles, { + owner, + repo, + pull_number: payload.pull_request.number, + }) + } else { + const { data: commit } = await github.rest.repos.getCommit({ + owner, + repo, + ref: sha, + }) + files = commit.files + } + return files.map(f => f.filename) diff --git a/lib/content/actions/changed-workspaces.yml b/lib/content/actions/changed-workspaces.yml new file mode 100644 index 00000000..40dcd75b --- /dev/null +++ b/lib/content/actions/changed-workspaces.yml @@ -0,0 +1,35 @@ +name: Get Changed Workspaces + +inputs: + token: + description: GitHub token to use + shell: + description: shell to run on + default: {{ shell }} + all: + default: false + type: boolean + files: + description: json stringified array of file names + type: string + +outputs: + flags: + value: $\{{ steps.workspaces.outputs.flags }} + +runs: + using: composite + steps: + - name: Get Changed Files + uses: ./.github/actions/changed-files + if: $\{{ !inputs.all && !inputs.files }} + id: files + with: + token: $\{{ inputs.token }} + + - name: Get Workspaces + shell: $\{{ inputs.shell }} + id: workspaces + run: | + flags=$({{ rootNpmPath }} exec --offline -- template-oss-changed-workspaces '$\{{ (inputs.all && '--all') || (inputs.files || steps.files.outputs.result) }}') + echo "flags=${flags}" >> $GITHUB_OUTPUT diff --git a/lib/content/actions/conclude-check.yml b/lib/content/actions/conclude-check.yml new file mode 100644 index 00000000..781d0070 --- /dev/null +++ b/lib/content/actions/conclude-check.yml @@ -0,0 +1,23 @@ +name: Conclude Check +description: Conclude a check + +inputs: + token: + description: GitHub token to use + required: true + conclusion: + description: conclusion of check + require: true + check-id: + description: id of check to conclude + required: true + +runs: + using: composite + steps: + - name: Conclude Check + uses: LouisBrunner/checks-action@v1.5.0 + with: + token: $\{{ inputs.token }} + conclusion: $\{{ inputs.conclusion }} + check_id: $\{{ inputs.check-id }} diff --git a/lib/content/actions/create-check.yml b/lib/content/actions/create-check.yml new file mode 100644 index 00000000..0ca5ac56 --- /dev/null +++ b/lib/content/actions/create-check.yml @@ -0,0 +1,62 @@ +name: Create Check +description: Create a check and associate it with a sha + +inputs: + token: + description: GitHub token to use + required: true + sha: + description: sha to attach the check to + required: true + job-name: + description: Name of the job to find + required: true + job-status: + description: Status of the check being created + default: in_progress + +outputs: + check-id: + description: The ID of the check that was created + value: $\{{ steps.check.outputs.check_id }} + +runs: + using: composite + steps: + - name: Get Workflow Job + uses: actions/github-script@v6 + id: workflow-job + env: + JOB_NAME: $\{{ inputs.job-name }} + with: + github-token: $\{{ inputs.token }} + script: | + const { JOB_NAME } = process.env + const { repo: { owner, repo }, runId, serverUrl } = context + + const jobs = await github.paginate(github.rest.actions.listJobsForWorkflowRun, { + owner, + repo, + run_id: runId, + }) + const job = jobs.find(j => j.name.endsWith(JOB_NAME)) + + const shaUrl = `${serverUrl}/${owner}/${repo}/commit/$\{{ inputs.sha }}` + const summary = `This check is assosciated with ${shaUrl}\n\n` + const message = job?.html_url + ? `For run logs, click here: ${job.html_url}` + : `Run logs could not be found for a job with name: "${JOB_NAME}"` + + // Return a json object with properties that LouisBrunner/checks-actions + // expects as the output of the check + return { summary: summary + message } + + - name: Create Check + uses: LouisBrunner/checks-action@v1.5.0 + id: check + with: + token: $\{{ inputs.token }} + status: $\{{ inputs.job-status }} + name: $\{{ inputs.job-name }} + sha: $\{{ inputs.sha }} + output: $\{{ steps.workflow-job.outputs.result }} diff --git a/lib/content/actions/deps.yml b/lib/content/actions/deps.yml new file mode 100644 index 00000000..a0f1f2ad --- /dev/null +++ b/lib/content/actions/deps.yml @@ -0,0 +1,18 @@ +name: Dependencies + +inputs: + command: + description: command to run for the dependencies step + default: install --ignore-scripts --no-audit --no-fund + flags: + description: extra flags to pass to the dependencies step + shell: + description: shell to run on + default: {{ shell }} + +runs: + using: composite + steps: + - name: Install Dependencies + shell: $\{{ inputs.shell }} + run: {{ rootNpmPath }} $\{{ inputs.command }} $\{{ inputs.flags }} diff --git a/lib/content/actions/lint.yml b/lib/content/actions/lint.yml new file mode 100644 index 00000000..ecac7587 --- /dev/null +++ b/lib/content/actions/lint.yml @@ -0,0 +1,17 @@ +name: Lint + +inputs: + flags: + description: flags to pass to the commands + shell: + description: shell to run on + default: {{ shell }} + +runs: + using: composite + steps: + - name: Lint + shell: $\{{ inputs.shell }} + run: | + {{ rootNpmPath }} run lint --ignore-scripts $\{{ inputs.flags }} + {{ rootNpmPath }} run postlint --ignore-scripts $\{{ inputs.flags }} diff --git a/lib/content/actions/setup.yml b/lib/content/actions/setup.yml new file mode 100644 index 00000000..ef135e1e --- /dev/null +++ b/lib/content/actions/setup.yml @@ -0,0 +1,94 @@ +name: Setup Repo +description: Setup a repo with standard tools + +inputs: + node-version: + description: node version to use + default: {{ last ciVersions }} + npm-version: + description: npm version to use + default: {{#if updateNpm}}{{ npmSpec }}{{/if}} + cache: + description: whether to cache npm install or not + type: boolean + default: {{ lockfile }} + shell: + description: shell to run on + default: {{ shell }} + deps: + description: whether to run the deps step + type: boolean + default: true + deps-command: + description: command to run for the dependencies step + default: install --ignore-scripts --no-audit --no-fund + deps-flags: + description: extra flags to pass to the dependencies step + +runs: + using: composite + steps: + - name: Setup Git User + shell: $\{{ inputs.shell }} + run: | + git config --global user.email "npm-cli+bot@github.com" + git config --global user.name "npm CLI robot" + + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: $\{{ inputs.node-version }} + cache: $\{{ (inputs.cache && 'npm') || null }} + + - name: Check Node Version + if: inputs.npm-version + id: node-version + shell: $\{{ inputs.shell }} + run: | + NODE_VERSION=$(node --version) + echo $NODE_VERSION + if npx semver@7 -r "<=10" "$NODE_VERSION" --yes; then + echo "ten-or-lower=true" >> $GITHUB_OUTPUT + fi + if npx semver@7 -r "<=14" "$NODE_VERSION" --yes; then + echo "fourteen-or-lower=true" >> $GITHUB_OUTPUT + fi + + - name: Update Windows npm + # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows + if: inputs.npm-version && runner.os == 'Windows' && steps.node-version.outputs.fourteen-or-lower + shell: $\{{ inputs.shell }} + run: | + curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz + tar xf npm-7.5.4.tgz + cd package + node lib/npm.js install --no-fund --no-audit -g ..\npm-7.5.4.tgz + cd .. + rmdir /s /q package + + - name: Install npm@7 + if: inputs.npm-version && steps.node-version.outputs.ten-or-lower + shell: $\{{ inputs.shell }} + run: npm i --prefer-online --no-fund --no-audit -g npm@7 + + - name: Install npm@$\{{ inputs.npm-version }} + if: inputs.npm-version && !steps.node-version.outputs.ten-or-lower + shell: $\{{ inputs.shell }} + run: npm i --prefer-online --no-fund --no-audit -g npm@$\{{ inputs.npm-version }} + + - name: npm Version + shell: $\{{ inputs.shell }} + run: {{ rootNpmPath }} -v + + - name: Setup Dependencies + if: inputs.deps + uses: ./.github/actions/deps + with: + command: $\{{ inputs.deps-command }} + flags: $\{{ inputs.deps-flags }} + + - name: Add Problem Matcher + shell: $\{{ inputs.shell }} + run: | + [[ -f ./.github/matchers/tap.json ]] && echo "::add-matcher::.github/matchers/tap.json" + diff --git a/lib/content/actions/test.yml b/lib/content/actions/test.yml new file mode 100644 index 00000000..6f86397f --- /dev/null +++ b/lib/content/actions/test.yml @@ -0,0 +1,15 @@ +name: Test + +inputs: + flags: + description: flags to pass to the commands + shell: + description: shell to run on + default: {{ shell }} + +runs: + using: composite + steps: + - name: Test + shell: $\{{ inputs.shell }} + run: {{ rootNpmPath }} test --ignore-scripts $\{{ inputs.flags }} diff --git a/lib/content/actions/upsert-comment.yml b/lib/content/actions/upsert-comment.yml new file mode 100644 index 00000000..c0c18454 --- /dev/null +++ b/lib/content/actions/upsert-comment.yml @@ -0,0 +1,82 @@ +name: Upsert Comment +description: Update or create a comment + +inputs: + token: + description: GitHub token to use + required: true + number: + description: Number of the issue or pull request + required: true + login: + description: Login name of user to look for comments from + default: github-actions[bot] + body: + description: Body of the comment, the first line will be used to match to an existing comment + find: + description: string to find in body + replace: + description: string to replace in body + append: + description: string to append to the body + includes: + description: A string that the comment needs to include + +outputs: + comment-id: + description: The ID of the comment + value: $\{{ steps.comment.outputs.result }} + +runs: + using: composite + steps: + - name: Create or Update Comment + uses: actions/github-script@v6 + id: comment + env: + NUMBER: $\{{ inputs.number }} + BODY: $\{{ inputs.body }} + FIND: $\{{ inputs.find }} + REPLACE: $\{{ inputs.replace }} + APPEND: $\{{ inputs.append }} + LOGIN: $\{{ inputs.login }} + INCLUDES: $\{{ inputs.includes }} + with: + github-token: $\{{ inputs.token }} + script: | + const { BODY, FIND, REPLACE, APPEND, LOGIN, NUMBER: issue_number, INCLUDES } = process.env + const { repo: { owner, repo } } = context + const TITLE = BODY.split('\n')[0].trim() + '\n' + const bodyIncludes = (c) => INCLUDES ? c.body.includes(INCLUDES) : true + + const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number }) + .then(comments => comments.map(c => ({ id: c.id, login: c.user.login, body: c.body }))) + + console.log(`Found comments: ${JSON.stringify(comments, null, 2)}`) + console.log(`Looking for comment with: ${JSON.stringify({ LOGIN, TITLE, INCLUDES }, null, 2)}`) + + const comment = comments.find(c => + c.login === LOGIN && + c.body.startsWith(TITLE) && + bodyIncludes(c) + ) + + if (comment) { + console.log(`Found comment: ${JSON.stringify(comment, null, 2)}`) + let newBody = FIND && REPLACE ? comment.body.replace(new RegExp(FIND, 'g'), REPLACE) : BODY + if (APPEND) { + newBody += APPEND + } + await github.rest.issues.updateComment({ owner, repo, comment_id: comment.id, body: newBody }) + return comment.id + } + + if (FIND || REPLACE || APPEND) { + console.log('Could not find a comment to use find/replace or append to') + return + } + + console.log('Creating new comment') + + const res = await github.rest.issues.createComment({ owner, repo, issue_number, body: BODY }) + return res.data.id diff --git a/lib/content/audit.yml b/lib/content/audit.yml deleted file mode 100644 index 77ef4b89..00000000 --- a/lib/content/audit.yml +++ /dev/null @@ -1,12 +0,0 @@ -name: Audit - -on: - workflow_dispatch: - schedule: - # "At 08:00 UTC (01:00 PT) on Monday" https://crontab.guru/#0_8_*_*_1 - - cron: "0 8 * * 1" - -jobs: - audit: - {{> job jobName="Audit Dependencies" jobDepFlags="--package-lock" }} - {{> stepAudit }} diff --git a/lib/content/ci-release.yml b/lib/content/ci-release.yml deleted file mode 100644 index 81582af3..00000000 --- a/lib/content/ci-release.yml +++ /dev/null @@ -1,37 +0,0 @@ - -name: CI - Release - -on: - workflow_dispatch: - inputs: - ref: - required: true - type: string - default: {{ defaultBranch }} - workflow_call: - inputs: - ref: - required: true - type: string - check-sha: - required: true - type: string - -jobs: - lint-all: - {{> job - jobName="Lint All" - jobCheck=(obj sha="inputs.check-sha") - jobCheckout=(obj ref="${{ inputs.ref }}") - }} - {{> stepLint jobRunFlags=allFlags }} - {{> stepChecks jobCheck=true }} - - test-all: - {{> jobMatrix - jobName="Test All" - jobCheck=(obj sha="inputs.check-sha") - jobCheckout=(obj ref="${{ inputs.ref }}") - }} - {{> stepTest jobRunFlags=allFlags }} - {{> stepChecks jobCheck=true }} diff --git a/lib/content/ci.yml b/lib/content/ci.yml deleted file mode 100644 index 0226d0c3..00000000 --- a/lib/content/ci.yml +++ /dev/null @@ -1,13 +0,0 @@ -name: CI {{~#if isWorkspace}} - {{ pkgName }}{{/if}} - -on: - {{> onCi }} - -jobs: - lint: - {{> job jobName="Lint" }} - {{> stepLint jobRunFlags=pkgFlags }} - - test: - {{> jobMatrix jobName="Test" }} - {{> stepTest jobRunFlags=pkgFlags }} diff --git a/lib/content/CODEOWNERS b/lib/content/files/CODEOWNERS similarity index 100% rename from lib/content/CODEOWNERS rename to lib/content/files/CODEOWNERS diff --git a/lib/content/CODE_OF_CONDUCT.md b/lib/content/files/CODE_OF_CONDUCT.md similarity index 100% rename from lib/content/CODE_OF_CONDUCT.md rename to lib/content/files/CODE_OF_CONDUCT.md diff --git a/lib/content/LICENSE.md b/lib/content/files/LICENSE.md similarity index 100% rename from lib/content/LICENSE.md rename to lib/content/files/LICENSE.md diff --git a/lib/content/SECURITY.md b/lib/content/files/SECURITY.md similarity index 100% rename from lib/content/SECURITY.md rename to lib/content/files/SECURITY.md diff --git a/lib/content/bug.yml b/lib/content/files/bug.yml similarity index 100% rename from lib/content/bug.yml rename to lib/content/files/bug.yml diff --git a/lib/content/commitlintrc.js b/lib/content/files/commitlintrc.js similarity index 100% rename from lib/content/commitlintrc.js rename to lib/content/files/commitlintrc.js diff --git a/lib/content/config.yml b/lib/content/files/config.yml similarity index 100% rename from lib/content/config.yml rename to lib/content/files/config.yml diff --git a/lib/content/dependabot.yml b/lib/content/files/dependabot.yml similarity index 100% rename from lib/content/dependabot.yml rename to lib/content/files/dependabot.yml diff --git a/lib/content/eslintrc.js b/lib/content/files/eslintrc.js similarity index 100% rename from lib/content/eslintrc.js rename to lib/content/files/eslintrc.js diff --git a/lib/content/gitignore b/lib/content/files/gitignore similarity index 100% rename from lib/content/gitignore rename to lib/content/files/gitignore diff --git a/lib/content/npmrc b/lib/content/files/npmrc similarity index 100% rename from lib/content/npmrc rename to lib/content/files/npmrc diff --git a/lib/content/pkg.json b/lib/content/files/pkg.json similarity index 78% rename from lib/content/pkg.json rename to lib/content/files/pkg.json index 387626ef..c1454222 100644 --- a/lib/content/pkg.json +++ b/lib/content/files/pkg.json @@ -10,15 +10,20 @@ "test": "tap", "posttest": "{{ localNpmPath }} run lint", {{#if isRootMono}} - "test-all": "{{ localNpmPath }} run test {{ allFlags }}", - "lint-all": "{{ localNpmPath }} run lint {{ allFlags }}", + "test-all": "{{ localNpmPath }} run test -ws -iwr --if-present", + "lint-all": "{{ localNpmPath }} run lint -ws -iwr --if-present", {{/if}} "template-copy": {{{ del }}}, "lint:fix": {{{ del }}}, "preversion": {{{ del }}}, "postversion": {{{ del }}}, "prepublishOnly": {{{ del }}}, - "postpublish": {{{ del }}} + "postpublish": {{{ del }}}, + "eslint": {{{ del }}}, + "npmclilint": {{{ del }}}, + "prerelease": {{{ del }}}, + "postrelease": {{{ del }}}, + "postsnap": {{{ del }}} }, "repository": {{#if repository}}{{{ json repository }}}{{else}}{{{ del }}}{{/if}}, "engines": { diff --git a/lib/content/release-please-config.json b/lib/content/files/release-please-config.json similarity index 100% rename from lib/content/release-please-config.json rename to lib/content/files/release-please-config.json diff --git a/lib/content/release-please-manifest.json b/lib/content/files/release-please-manifest.json similarity index 100% rename from lib/content/release-please-manifest.json rename to lib/content/files/release-please-manifest.json diff --git a/lib/content/tap.json b/lib/content/files/tap.json similarity index 100% rename from lib/content/tap.json rename to lib/content/files/tap.json diff --git a/lib/content/index.js b/lib/content/index.js index 903366ae..71cef04d 100644 --- a/lib/content/index.js +++ b/lib/content/index.js @@ -2,41 +2,58 @@ const { name: NAME, version: LATEST_VERSION } = require('../../package.json') const isPublic = (p) => p.config.isPublic -const sharedRootAdd = (name) => ({ - // release +const sharedRootAdd = () => ({ + // composite actions + '.github/actions/audit/action.yml': 'actions/audit.yml', + '.github/actions/changed-files/action.yml': 'actions/changed-files.yml', + '.github/actions/changed-workspaces/action.yml': 'actions/changed-workspaces.yml', + '.github/actions/conclude-check/action.yml': 'actions/conclude-check.yml', + '.github/actions/create-check/action.yml': 'actions/create-check.yml', + '.github/actions/deps/action.yml': 'actions/deps.yml', + '.github/actions/lint/action.yml': 'actions/lint.yml', + '.github/actions/setup/action.yml': 'actions/setup.yml', + '.github/actions/test/action.yml': 'actions/test.yml', + '.github/actions/upsert-comment/action.yml': 'actions/upsert-comment.yml', + // workflows + '.github/workflows/audit.yml': 'workflows/audit.yml', + '.github/workflows/ci.yml': 'workflows/ci.yml', + '.github/workflows/codeql-analysis.yml': 'workflows/codeql-analysis.yml', + '.github/workflows/post-dependabot.yml': { + file: 'workflows/post-dependabot.yml', + }, + // this lint commits which is only necessary for releases + '.github/workflows/pull-request.yml': { + file: 'workflows/pull-request.yml', + filter: isPublic, + }, '.github/workflows/release.yml': { - file: 'release.yml', + file: 'workflows/release.yml', filter: isPublic, }, - '.github/workflows/ci-release.yml': { - file: 'ci-release.yml', + '.github/workflows/release-integration.yml': { + file: 'workflows/release-integration.yml', filter: isPublic, }, + // release please config '.release-please-manifest.json': { - file: 'release-please-manifest.json', + file: 'files/release-please-manifest.json', filter: isPublic, parser: (p) => class extends p.JsonMerge { comment = null }, }, 'release-please-config.json': { - file: 'release-please-config.json', + file: 'files/release-please-config.json', filter: isPublic, parser: (p) => class extends p.JsonMerge { comment = null }, }, - // this lint commits which is only necessary for releases - '.github/workflows/pull-request.yml': { - file: 'pull-request.yml', - filter: isPublic, - }, // ci - '.github/matchers/tap.json': 'tap.json', - [`.github/workflows/ci${name ? `-${name}` : ''}.yml`]: 'ci.yml', + '.github/matchers/tap.json': 'files/tap.json', // dependabot '.github/dependabot.yml': { - file: 'dependabot.yml', + file: 'files/dependabot.yml', clean: (p) => p.config.isRoot, // dependabot takes a single top level config file. this parser // will run for all configured packages and each one will have @@ -46,9 +63,7 @@ const sharedRootAdd = (name) => ({ id = 'directory' }, }, - '.github/workflows/post-dependabot.yml': { - file: 'post-dependabot.yml', - }, + }) const sharedRootRm = () => ({ @@ -60,12 +75,10 @@ const sharedRootRm = () => ({ // Changes applied to the root of the repo const rootRepo = { add: { - '.commitlintrc.js': 'commitlintrc.js', - '.github/ISSUE_TEMPLATE/bug.yml': 'bug.yml', - '.github/ISSUE_TEMPLATE/config.yml': 'config.yml', - '.github/CODEOWNERS': 'CODEOWNERS', - '.github/workflows/audit.yml': 'audit.yml', - '.github/workflows/codeql-analysis.yml': 'codeql-analysis.yml', + '.commitlintrc.js': 'files/commitlintrc.js', + '.github/ISSUE_TEMPLATE/bug.yml': 'files/bug.yml', + '.github/ISSUE_TEMPLATE/config.yml': 'files/config.yml', + '.github/CODEOWNERS': 'files/CODEOWNERS', ...sharedRootAdd(), }, rm: { @@ -82,12 +95,12 @@ const rootRepo = { // dir. so we might want to combine these const rootModule = { add: { - '.eslintrc.js': 'eslintrc.js', - '.gitignore': 'gitignore', - '.npmrc': 'npmrc', - 'SECURITY.md': 'SECURITY.md', - 'CODE_OF_CONDUCT.md': 'CODE_OF_CONDUCT.md', - 'package.json': 'pkg.json', + '.eslintrc.js': 'files/eslintrc.js', + '.gitignore': 'files/gitignore', + '.npmrc': 'files/npmrc', + 'SECURITY.md': 'files/SECURITY.md', + 'CODE_OF_CONDUCT.md': 'files/CODE_OF_CONDUCT.md', + 'package.json': 'files/pkg.json', }, rm: [ '.eslintrc.!(js|local.*)', @@ -97,11 +110,12 @@ const rootModule = { // Changes for each workspace but applied to the root of the repo const workspaceRepo = { add: { - ...sharedRootAdd('{{ pkgNameFs }}'), + ...sharedRootAdd(), }, rm: { - // These are the old release please files that should be removed now + // These are the old release please and ci files that should be removed now '.github/workflows/release-please-{{ pkgNameFs }}.yml': true, + '.github/workflows/ci-{{ pkgNameFs }}.yml': true, ...sharedRootRm(), }, } @@ -109,9 +123,9 @@ const workspaceRepo = { // Changes for each workspace but applied to the relative workspace dir const workspaceModule = { add: { - '.eslintrc.js': 'eslintrc.js', - '.gitignore': 'gitignore', - 'package.json': 'pkg.json', + '.eslintrc.js': 'files/eslintrc.js', + '.gitignore': 'files/gitignore', + 'package.json': 'files/pkg.json', }, rm: [ '.npmrc', @@ -129,6 +143,7 @@ module.exports = { macCI: true, branches: ['main', 'latest'], defaultBranch: 'main', + releaseBranches: ['release/v*'], distPaths: [ 'bin/', 'lib/', @@ -150,10 +165,13 @@ module.exports = { ignorePaths: [], ciVersions: ['14.17.0', '14.x', '16.13.0', '16.x', '18.0.0', '18.x'], lockfile: false, + org: 'npm', codeowner: '@npm/cli-team', npm: 'npm', npx: 'npx', npmSpec: 'latest', + shell: 'bash', + runsOn: 'ubuntu-latest', dependabot: 'increase-if-necessary', unwantedPackages: [ 'eslint', diff --git a/lib/content/partials/branches.yml b/lib/content/partials/branches.yml new file mode 100644 index 00000000..18def00d --- /dev/null +++ b/lib/content/partials/branches.yml @@ -0,0 +1,9 @@ +branches: + {{#each branches}} + - {{ . }} + {{/each}} + {{~#if release}} + {{#each releaseBranches}} + - {{ . }} + {{/each}} + {{/if~}} diff --git a/lib/content/partials/cron.yml b/lib/content/partials/cron.yml new file mode 100644 index 00000000..85f281c9 --- /dev/null +++ b/lib/content/partials/cron.yml @@ -0,0 +1,3 @@ +{{! All our times are early AM pacific time, so add 8 to get utc }} +# "At {{ padHour (add hour 8) }}:00 UTC ({{ padHour hour }}:00 PT) on Monday" https://crontab.guru/#0_{{ add hour 8 }}_*_*_1 +- cron: "0 {{ add hour 8 }} * * 1" diff --git a/lib/content/partials/job-defaults.yml b/lib/content/partials/job-defaults.yml new file mode 100644 index 00000000..12743c36 --- /dev/null +++ b/lib/content/partials/job-defaults.yml @@ -0,0 +1,4 @@ +runs-on: {{ runsOn }} +defaults: + run: + shell: {{ shell }} diff --git a/lib/content/partials/job-if.yml b/lib/content/partials/job-if.yml new file mode 100644 index 00000000..556c53f4 --- /dev/null +++ b/lib/content/partials/job-if.yml @@ -0,0 +1 @@ +if: github.repository_owner == '{{ org }}'{{#if @partial-block }} {{> @partial-block }}{{else}}{{ newline }}{{/if~}} \ No newline at end of file diff --git a/lib/content/_job-matrix.yml b/lib/content/partials/matrix-strategy.yml similarity index 61% rename from lib/content/_job-matrix.yml rename to lib/content/partials/matrix-strategy.yml index 8d77a5c2..4d2c2469 100644 --- a/lib/content/_job-matrix.yml +++ b/lib/content/partials/matrix-strategy.yml @@ -1,5 +1,7 @@ -name: {{ jobName }} - $\{{ matrix.platform.name }} - $\{{ matrix.node-version }} -if: github.repository_owner == 'npm' +{{> partialsJobDefaults + shell="${{ matrix.platform.shell }}" + runsOn="${{ matrix.platform.os }}" +}} strategy: fail-fast: false matrix: @@ -21,9 +23,3 @@ strategy: {{#each ciVersions}} - {{ . }} {{/each}} -runs-on: $\{{ matrix.platform.os }} -defaults: - run: - shell: $\{{ matrix.platform.shell }} -steps: - {{> stepsSetup jobIsMatrix=true }} diff --git a/lib/content/pull-request.yml b/lib/content/pull-request.yml deleted file mode 100644 index a8f3b7e6..00000000 --- a/lib/content/pull-request.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Pull Request - -on: - pull_request: - types: - - opened - - reopened - - edited - - synchronize - -jobs: - commitlint: - {{> job jobName="Lint Commits" jobCheckout=(obj fetch-depth=0) }} - - name: Run Commitlint on Commits - id: commit - continue-on-error: true - run: | - {{ rootNpxPath }} --offline commitlint -V --from 'origin/$\{{ github.base_ref }}' --to $\{{ github.event.pull_request.head.sha }} - - name: Run Commitlint on PR Title - if: steps.commit.outcome == 'failure' - run: | - echo '$\{{ github.event.pull_request.title }}' | {{ rootNpxPath }} --offline commitlint -V diff --git a/lib/content/release.yml b/lib/content/release.yml deleted file mode 100644 index 9bb7a94b..00000000 --- a/lib/content/release.yml +++ /dev/null @@ -1,218 +0,0 @@ -name: Release - -on: - workflow_dispatch: - inputs: - release-pr: - description: a release PR number to rerun release jobs on - type: string - push: - branches: - {{#each branches}} - - {{ . }} - {{/each}} - - release/v* - -permissions: - contents: write - pull-requests: write - checks: write - -jobs: - release: - outputs: - pr: $\{{ steps.release.outputs.pr }} - release: $\{{ steps.release.outputs.release }} - releases: $\{{ steps.release.outputs.releases }} - branch: $\{{ steps.release.outputs.pr-branch }} - pr-number: $\{{ steps.release.outputs.pr-number }} - comment-id: $\{{ steps.pr-comment.outputs.result }} - check-id: $\{{ steps.check.outputs.check_id }} - {{> job jobName="Release" }} - - name: Release Please - id: release - env: - GITHUB_TOKEN: $\{{ secrets.GITHUB_TOKEN }} - run: | - {{ rootNpxPath }} --offline template-oss-release-please "$\{{ github.ref_name }}" "$\{{ inputs.release-pr }}" - - name: Post Pull Request Comment - if: steps.release.outputs.pr-number - uses: actions/github-script@v6 - id: pr-comment - env: - PR_NUMBER: $\{{ steps.release.outputs.pr-number }} - REF_NAME: $\{{ github.ref_name }} - with: - script: | - const { REF_NAME, PR_NUMBER: issue_number } = process.env - const { runId, repo: { owner, repo } } = context - - const { data: workflow } = await github.rest.actions.getWorkflowRun({ owner, repo, run_id: runId }) - - let body = '## Release Manager\n\n' - - const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number }) - let commentId = comments.find(c => c.user.login === 'github-actions[bot]' && c.body.startsWith(body))?.id - - body += `Release workflow run: ${workflow.html_url}\n\n#### Force CI to Update This Release\n\n` - body += `This PR will be updated and CI will run for every non-\`chore:\` commit that is pushed to \`{{ defaultBranch }}\`. ` - body += `To force CI to update this PR, run this command:\n\n` - body += `\`\`\`\ngh workflow run release.yml -r ${REF_NAME} -R ${owner}/${repo} -f release-pr=${issue_number}\n\`\`\`` - - if (commentId) { - await github.rest.issues.updateComment({ owner, repo, comment_id: commentId, body }) - } else { - const { data: comment } = await github.rest.issues.createComment({ owner, repo, issue_number, body }) - commentId = comment?.id - } - - return commentId - {{> stepChecks jobCheck=(obj name="Release" sha="steps.release.outputs.pr-sha") }} - - update: - needs: release - outputs: - sha: $\{{ steps.commit.outputs.sha }} - check-id: $\{{ steps.check.outputs.check_id }} - {{> job - jobName="Update - Release" - jobIf="needs.release.outputs.pr" - jobCheckout=(obj ref="${{ needs.release.outputs.branch }}" fetch-depth=0) - }} - - name: Run Post Pull Request Actions - env: - RELEASE_PR_NUMBER: $\{{ needs.release.outputs.pr-number }} - RELEASE_COMMENT_ID: $\{{ needs.release.outputs.comment-id }} - GITHUB_TOKEN: $\{{ secrets.GITHUB_TOKEN }} - run: | - {{ rootNpmPath }} exec --offline -- template-oss-release-manager --lockfile={{ lockfile }} - {{ rootNpmPath }} run rp-pull-request --ignore-scripts {{~#if allFlags}} {{ allFlags }}{{else}} --if-present{{/if}} - - name: Commit - id: commit - env: - GITHUB_TOKEN: $\{{ secrets.GITHUB_TOKEN }} - run: | - git commit --all --amend --no-edit || true - git push --force-with-lease - echo "sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT - {{> stepChecks jobName="Update - Release" jobCheck=(obj sha="steps.commit.outputs.sha" name="Release" )}} - {{> stepChecks jobCheck=(obj id="needs.release.outputs.check-id" )}} - - ci: - name: CI - Release - needs: [release, update] - if: needs.release.outputs.pr - uses: ./.github/workflows/ci-release.yml - with: - ref: $\{{ needs.release.outputs.branch }} - check-sha: $\{{ needs.update.outputs.sha }} - - post-ci: - needs: [release, update, ci] - {{> job jobName="Post CI - Release" jobIf="needs.release.outputs.pr && always()" jobSkipSetup=true }} - - name: Get Needs Result - id: needs-result - run: | - result="" - if [[ "$\{{ contains(needs.*.result, 'failure') }}" == "true" ]]; then - result="failure" - elif [[ "$\{{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then - result="cancelled" - else - result="success" - fi - echo "result=$result" >> $GITHUB_OUTPUT - {{> stepChecks jobCheck=(obj id="needs.update.outputs.check-id" status="steps.needs-result.outputs.result") }} - - post-release: - needs: release - {{> job jobName="Post Release - Release" jobIf="needs.release.outputs.releases" jobSkipSetup=true }} - - name: Create Release PR Comment - uses: actions/github-script@v6 - env: - RELEASES: $\{{ needs.release.outputs.releases }} - with: - script: | - const releases = JSON.parse(process.env.RELEASES) - const { runId, repo: { owner, repo } } = context - const issue_number = releases[0].prNumber - - let body = '## Release Workflow\n\n' - for (const { pkgName, version, url } of releases) { - body += `- \`${pkgName}@${version}\` ${url}\n` - } - - const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number }) - .then(cs => cs.map(c => ({ id: c.id, login: c.user.login, body: c.body }))) - console.log(`Found comments: ${JSON.stringify(comments, null, 2)}`) - const releaseComments = comments.filter(c => c.login === 'github-actions[bot]' && c.body.includes('Release is at')) - - for (const comment of releaseComments) { - console.log(`Release comment: ${JSON.stringify(comment, null, 2)}`) - await github.rest.issues.deleteComment({ owner, repo, comment_id: comment.id }) - } - - const runUrl = `https://github.com/${owner}/${repo}/actions/runs/${runId}` - await github.rest.issues.createComment({ - owner, - repo, - issue_number, - body: `${body}- Workflow run: :arrows_counterclockwise: ${runUrl}`, - }) - - release-integration: - needs: release - name: Release Integration - if: needs.release.outputs.release - {{> jobReleaseIntegration }} - - post-release-integration: - needs: [release, release-integration] - {{> job jobName="Post Release Integration - Release" jobIf="needs.release.outputs.release && always()" jobSkipSetup=true }} - - name: Get Needs Result - id: needs-result - run: | - if [[ "$\{{ contains(needs.*.result, 'failure') }}" == "true" ]]; then - result="x" - elif [[ "$\{{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then - result="heavy_multiplication_x" - else - result="white_check_mark" - fi - echo "result=$result" >> $GITHUB_OUTPUT - - name: Update Release PR Comment - uses: actions/github-script@v6 - env: - PR_NUMBER: $\{{ fromJSON(needs.release.outputs.release).prNumber }} - RESULT: $\{{ steps.needs-result.outputs.result }} - with: - script: | - const { PR_NUMBER: issue_number, RESULT } = process.env - const { runId, repo: { owner, repo } } = context - - const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number }) - const updateComment = comments.find(c => - c.user.login === 'github-actions[bot]' && - c.body.startsWith('## Release Workflow\n\n') && - c.body.includes(runId) - ) - - if (updateComment) { - console.log('Found comment to update:', JSON.stringify(updateComment, null, 2)) - let body = updateComment.body.replace(/Workflow run: :[a-z_]+:/, `Workflow run: :${RESULT}:`) - const tagCodeowner = RESULT !== 'white_check_mark' - if (tagCodeowner) { - body += `\n\n:rotating_light:` - body += ` {{ codeowner }}: The post-release workflow failed for this release.` - body += ` Manual steps may need to be taken after examining the workflow output` - body += ` from the above workflow run. :rotating_light:` - } - await github.rest.issues.updateComment({ - owner, - repo, - body, - comment_id: updateComment.id, - }) - } else { - console.log('No matching comments found:', JSON.stringify(comments, null, 2)) - } diff --git a/lib/content/workflows/audit.yml b/lib/content/workflows/audit.yml new file mode 100644 index 00000000..2e58d46d --- /dev/null +++ b/lib/content/workflows/audit.yml @@ -0,0 +1,23 @@ +name: Audit + +on: + workflow_dispatch: + schedule: + {{> partialsCron hour=1 }} + +jobs: + audit: + name: Audit Dependencies + {{> partialsJobIf }} + {{> partialsJobDefaults }} + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup + uses: ./.github/actions/setup + with: + deps-flags: "--package-lock" + + - name: Audit + uses: ./.github/actions/audit diff --git a/lib/content/workflows/ci.yml b/lib/content/workflows/ci.yml new file mode 100644 index 00000000..f92fda17 --- /dev/null +++ b/lib/content/workflows/ci.yml @@ -0,0 +1,139 @@ + +name: CI + +on: + workflow_dispatch: + inputs: + ref: + required: true + type: string + check-sha: + type: string + all: + default: true + type: boolean + workflow_call: + inputs: + ref: + required: true + type: string + check-sha: + required: true + type: string + all: + default: true + type: boolean + pull_request: + push: + {{> partialsBranches }} + schedule: + {{> partialsCron hour=2 }} + +jobs: + lint: + name: Lint + {{> partialsJobIf }} + {{> partialsJobDefaults }} + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + ref: $\{{ inputs.ref }} + + - name: Create Check + uses: ./.github/actions/create-check + if: inputs.check-sha + id: check + with: + sha: $\{{ inputs.check-sha }} + token: $\{{ secrets.GITHUB_TOKEN }} + job-name: Lint + + - name: Setup + id: setup + continue-on-error: $\{{ !!steps.check.outputs.check-id }} + uses: ./.github/actions/setup + + - name: Get Changed Workspaces + id: workspaces + continue-on-error: $\{{ !!steps.check.outputs.check-id }} + uses: ./.github/actions/changed-workspaces + with: + token: $\{{ secrets.GITHUB_TOKEN }} + all: $\{{ inputs.all }} + + - name: Lint + uses: ./.github/actions/lint + continue-on-error: $\{{ !!steps.check.outputs.check-id }} + with: + flags: $\{{ steps.workspaces.outputs.flags }} + + - name: Conclude Check + uses: ./.github/actions/conclude-check + if: steps.check.outputs.check-id && (success() || failure()) + with: + token: $\{{ secrets.GITHUB_TOKEN }} + conclusion: $\{{ job.status }} + check-id: $\{{ steps.check.outputs.check-id }} + + test: + name: Test - $\{{ matrix.platform.name }} - $\{{ matrix.node-version }} + {{> partialsJobIf }} + {{> partialsMatrixStrategy }} + steps: + - name: Continue Matrix Run + id: continue-matrix + run: | + if [[ "$\{{ matrix.node-version }}" == "{{ first ciVersions }}" || "$\{{ inputs.all }}" == "true" ]]; then + echo "result=true" >> $GITHUB_OUTPUT + fi + + - name: Checkout + if: steps.continue-matrix.outputs.result + uses: actions/checkout@v3 + with: + ref: $\{{ inputs.ref }} + + - name: Create Check + if: steps.continue-matrix.outputs.result && inputs.check-sha + uses: ./.github/actions/create-check + id: check + with: + sha: $\{{ inputs.check-sha }} + token: $\{{ secrets.GITHUB_TOKEN }} + job-name: "Test - $\{{ matrix.platform.name }} - $\{{ matrix.node-version }}" + + - name: Setup + if: steps.continue-matrix.outputs.result + uses: ./.github/actions/setup + id: setup + continue-on-error: $\{{ !!steps.check.outputs.check-id }} + with: + node-version: $\{{ matrix.node-version }} + shell: $\{{ matrix.platform.shell }} + + - name: Get Changed Workspaces + if: steps.continue-matrix.outputs.result + id: workspaces + continue-on-error: $\{{ !!steps.check.outputs.check-id }} + uses: ./.github/actions/changed-workspaces + with: + shell: $\{{ matrix.platform.shell }} + token: $\{{ secrets.GITHUB_TOKEN }} + all: $\{{ inputs.all }} + + - name: Test + if: steps.continue-matrix.outputs.result + uses: ./.github/actions/test + continue-on-error: $\{{ !!steps.check.outputs.check-id }} + with: + flags: $\{{ steps.workspaces.outputs.flags }} + shell: $\{{ matrix.platform.shell }} + + - name: Conclude Check + uses: ./.github/actions/conclude-check + if: steps.continue-matrix.outputs.result && steps.check.outputs.check-id && (success() || failure()) + with: + token: $\{{ secrets.GITHUB_TOKEN }} + conclusion: $\{{ job.status }} + check-id: $\{{ steps.check.outputs.check-id }} diff --git a/lib/content/codeql-analysis.yml b/lib/content/workflows/codeql-analysis.yml similarity index 57% rename from lib/content/codeql-analysis.yml rename to lib/content/workflows/codeql-analysis.yml index 4e4c18f4..d09d0eee 100644 --- a/lib/content/codeql-analysis.yml +++ b/lib/content/workflows/codeql-analysis.yml @@ -2,32 +2,29 @@ name: CodeQL on: push: - branches: - {{#each branches}} - - {{ . }} - {{/each}} + {{> partialsBranches }} pull_request: - branches: - {{#each branches}} - - {{ . }} - {{/each}} + {{> partialsBranches }} schedule: - # "At 10:00 UTC (03:00 PT) on Monday" https://crontab.guru/#0_10_*_*_1 - - cron: "0 10 * * 1" + {{> partialsCron hour=3 }} jobs: analyze: name: Analyze - runs-on: ubuntu-latest + {{> partialsJobIf }} + {{> partialsJobDefaults }} permissions: actions: read contents: read security-events: write steps: - {{> stepGit }} + - name: Checkout + uses: actions/checkout@v3 + - name: Initialize CodeQL uses: github/codeql-action/init@v2 with: languages: javascript + - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 diff --git a/lib/content/post-dependabot.yml b/lib/content/workflows/post-dependabot.yml similarity index 75% rename from lib/content/post-dependabot.yml rename to lib/content/workflows/post-dependabot.yml index fe7526c4..5a0f311b 100644 --- a/lib/content/post-dependabot.yml +++ b/lib/content/workflows/post-dependabot.yml @@ -3,42 +3,50 @@ name: Post Dependabot on: pull_request -permissions: - contents: write + jobs: template-oss: - {{> job - jobName="template-oss" - jobIf="github.actor == 'dependabot[bot]'" - jobCheckout=(obj ref="${{ github.event.pull_request.head.ref }}") - }} + name: template-oss + permissions: + contents: write + {{#> partialsJobIf}}&& github.actor == 'dependabot[bot]'{{/ partialsJobIf}} + {{> partialsJobDefaults }} + steps: - name: Fetch Dependabot Metadata id: metadata uses: dependabot/fetch-metadata@v1 with: github-token: $\{{ secrets.GITHUB_TOKEN }} - # Dependabot can update multiple directories so we output which directory - # it is acting on so we can run the command for the correct root or workspace - - name: Get Dependabot Directory + - name: Is Dependency if: contains(steps.metadata.outputs.dependency-names, '{{ __NAME__ }}') - id: flags - run: | - dependabot_dir="$\{{ steps.metadata.outputs.directory }}" - if [[ "$dependabot_dir" == "/" ]]; then - echo "workspace=-iwr" >> $GITHUB_OUTPUT - else - # strip leading slash from directory so it works as a - # a path to the workspace flag - echo "workspace=-w ${dependabot_dir#/}" >> $GITHUB_OUTPUT - fi + id: dependency + run: echo "continue=true" >> $GITHUB_OUTPUT + + - name: Checkout + if: steps.dependency.outputs.continue + uses: actions/checkout@v3 + with: + ref: $\{{ github.event.pull_request.head.ref }} + + - name: Setup + if: steps.dependency.outputs.continue + uses: ./.github/actions/setup + + - name: Get Workspaces + if: steps.dependency.outputs.continue + uses: ./.github/actions/changed-workspaces + id: workspaces + with: + token: $\{{ secrets.GITHUB_TOKEN }} + files: '["$\{{ steps.metadata.outputs.directory }}"]' - name: Apply Changes - if: steps.flags.outputs.workspace + if: steps.workspaces.outputs.flags id: apply run: | - {{ rootNpmPath }} run template-oss-apply $\{{ steps.flags.outputs.workspace }} + {{ rootNpmPath }} run template-oss-apply $\{{ steps.workspaces.outputs.flags }} if [[ `git status --porcelain` ]]; then echo "changes=true" >> $GITHUB_OUTPUT fi @@ -90,7 +98,7 @@ jobs: - name: Check Changes if: steps.apply.outputs.changes run: | - {{ rootNpmPath }} exec --offline $\{{ steps.flags.outputs.workspace }} -- template-oss-check + {{ rootNpmPath }} exec --offline $\{{ steps.workspaces.outputs.flags }} -- template-oss-check - name: Fail on Breaking Change if: steps.apply.outputs.changes && startsWith(steps.apply.outputs.message, 'feat!') @@ -98,4 +106,3 @@ jobs: echo "This PR has a breaking change. Run 'npx -p @npmcli/stafftools gh template-oss-fix'" echo "for more information on how to fix this with a BREAKING CHANGE footer." exit 1 - diff --git a/lib/content/workflows/pull-request.yml b/lib/content/workflows/pull-request.yml new file mode 100644 index 00000000..cda41fe4 --- /dev/null +++ b/lib/content/workflows/pull-request.yml @@ -0,0 +1,36 @@ +name: Pull Request + +on: + pull_request: + types: + - opened + - reopened + - edited + - synchronize + +jobs: + commitlint: + name: Lint Commit + {{> partialsJobIf }} + {{> partialsJobDefaults }} + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Setup + uses: ./.github/actions/setup + + - name: Run Commitlint on Commits + id: commit + continue-on-error: true + run: | + {{ rootNpxPath }} --offline commitlint -V --from 'origin/$\{{ github.base_ref }}' --to '$\{{ github.event.pull_request.head.sha }}' + + - name: Run Commitlint on PR Title + if: steps.commit.outcome == 'failure' + env: + PR_TITLE: $\{{ github.event.pull_request.title }} + run: | + echo "$PR_TITLE" | {{ rootNpxPath }} --offline commitlint -V diff --git a/lib/content/workflows/release-integration.yml b/lib/content/workflows/release-integration.yml new file mode 100644 index 00000000..289e1d12 --- /dev/null +++ b/lib/content/workflows/release-integration.yml @@ -0,0 +1,53 @@ + +name: Release Integration + +on: + workflow_call: + inputs: + release: + required: true + type: string + releases: + required: true + type: string + +jobs: + check-registry: + name: Check Registry + {{> partialsJobIf }} + {{> partialsJobDefaults }} + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup + uses: ./.github/actions/setup + with: + deps: false + + - name: View in Registry + run: | + EXIT_CODE=0 + + function is_published { + if {{ rootNpmPath }} view "$@" --loglevel=error > /dev/null; then + echo 0 + else + echo 1 + fi + } + + for release in $(echo '$\{{ needs.release.outputs.releases }}' | jq -r '.[] | @base64'); do + name=$(echo "$release" | base64 --decode | jq -r .pkgName) + version=$(echo "$release" | base64 --decode | jq -r .version) + spec="$name@$version" + status=$(is_published "$spec") + if [[ "$status" -eq 1 ]]; then + echo "$spec ERROR" + EXIT_CODE=$status + else + echo "$spec OK" + fi + done + + exit $EXIT_CODE diff --git a/lib/content/workflows/release.yml b/lib/content/workflows/release.yml new file mode 100644 index 00000000..57a010ae --- /dev/null +++ b/lib/content/workflows/release.yml @@ -0,0 +1,273 @@ +name: Release + +on: + workflow_dispatch: + inputs: + release-pr: + description: a release PR number to rerun release jobs on + type: string + push: + branches: + {{> partialsBranches release=true }} + +permissions: + contents: write + pull-requests: write + checks: write + +jobs: + release: + name: Release + {{> partialsJobIf }} + {{> partialsJobDefaults }} + outputs: + pr: $\{{ steps.release.outputs.pr }} + release: $\{{ steps.release.outputs.release }} + releases: $\{{ steps.release.outputs.releases }} + pr-branch: $\{{ steps.release.outputs.pr-branch }} + pr-number: $\{{ steps.release.outputs.pr-number }} + comment-id: $\{{ steps.pr-comment.outputs.comment-id }} + check-id: $\{{ steps.check.outputs.check-id }} + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup + uses: ./.github/actions/setup + + - name: Release Please + id: release + env: + GITHUB_TOKEN: $\{{ secrets.GITHUB_TOKEN }} + run: | + {{ rootNpxPath }} --offline template-oss-release-please "$\{{ github.ref_name }}" "$\{{ inputs.release-pr }}" + + # If we have opened a release PR, then immediately create an "in_progress" + # check for it so the GitHub UI doesn't report that its mergeable. + # This check will be swapped out for real CI checks once those are started. + - name: Create Check + uses: ./.github/actions/create-check + if: steps.release.outputs.pr-sha + id: check + with: + sha: $\{{ steps.release.outputs.pr-sha }} + token: $\{{ secrets.GITHUB_TOKEN }} + job-name: Release + + - name: Comment Text + uses: actions/github-script@v6 + if: steps.release.outputs.pr-number + id: comment-text + env: + PR_NUMBER: $\{{ steps.release.outputs.pr-number }} + REF_NAME: $\{{ github.ref_name }} + with: + result-encoding: string + script: | + const { runId, repo: { owner, repo } } = context + const { data: workflow } = await github.rest.actions.getWorkflowRun({ owner, repo, run_id: runId }) + let body = '## Release Manager\n\n' + body += `Release workflow run: ${workflow.html_url}\n\n#### Force CI to Update This Release\n\n` + body += `This PR will be updated and CI will run for every non-\`chore:\` commit that is pushed to \`{{ defaultBranch }}\`. ` + body += `To force CI to update this PR, run this command:\n\n` + body += `\`\`\`\ngh workflow run release.yml -r ${process.env.REF_NAME} -R ${owner}/${repo} -f release-pr=${process.env.PR_NUMBER}\n\`\`\`` + return body + + - name: Post Pull Request Comment + if: steps.comment-text.outputs.result + uses: ./.github/actions/upsert-comment + id: pr-comment + with: + token: $\{{ secrets.GITHUB_TOKEN }} + body: $\{{ steps.comment-text.outputs.result }} + number: $\{{ steps.release.outputs.pr-number }} + + update: + name: Release PR - Update + {{> partialsJobDefaults }} + if: needs.release.outputs.pr + needs: release + outputs: + sha: $\{{ steps.commit.outputs.sha }} + check-id: $\{{ steps.check.outputs.check-id }} + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + ref: $\{{ needs.release.outputs.pr-branch }} + fetch-depth: 0 + + - name: Setup + uses: ./.github/actions/setup + + - name: Run Post Pull Request Actions + env: + RELEASE_PR_NUMBER: $\{{ needs.release.outputs.pr-number }} + RELEASE_COMMENT_ID: $\{{ needs.release.outputs.comment-id }} + GITHUB_TOKEN: $\{{ secrets.GITHUB_TOKEN }} + run: | + {{ rootNpmPath }} exec --offline -- template-oss-release-manager --lockfile={{ lockfile }} + {{ rootNpmPath }} run rp-pull-request --ignore-scripts -ws -iwr --if-present + + - name: Commit + id: commit + env: + GITHUB_TOKEN: $\{{ secrets.GITHUB_TOKEN }} + run: | + git commit --all --amend --no-edit || true + git push --force-with-lease + echo "sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT + + - name: Create Check + uses: ./.github/actions/create-check + if: steps.commit.outputs.sha + id: check + with: + sha: $\{{ steps.vommit.outputs.sha }} + token: $\{{ secrets.GITHUB_TOKEN }} + job-name: Release + + - name: Conclude Check + uses: ./.github/actions/conclude-check + if: needs.release.outputs.check-id && (success() || failure()) + with: + token: $\{{ secrets.GITHUB_TOKEN }} + conclusion: $\{{ job.status }} + check-id: $\{{ needs.release.outputs.check-id }} + + ci: + name: Release PR - CI + needs: [release, update] + if: needs.release.outputs.pr + uses: ./.github/workflows/ci.yml + with: + ref: $\{{ needs.release.outputs.pr-branch }} + check-sha: $\{{ needs.update.outputs.sha }} + + post-ci: + name: Relase PR - Post CI + {{> partialsJobDefaults }} + needs: [release, update, ci] + if: needs.release.outputs.pr && (success() || failure()) + steps: + - name: Get Needs Result + id: needs-result + run: | + if [[ "$\{{ contains(needs.*.result, 'failure') }}" == "true" ]]; then + result="failure" + elif [[ "$\{{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then + result="cancelled" + else + result="success" + fi + echo "result=$result" >> $GITHUB_OUTPUT + + - name: Conclude Check + uses: ./.github/actions/conclude-check + if: needs.update.outputs.check-id && (success() || failure()) + with: + token: $\{{ secrets.GITHUB_TOKEN }} + conclusion: $\{{ steps.needs-result.outputs.result }} + check-id: $\{{ needs.update.outputs.check-id }} + + post-release: + name: Post Release + {{> partialsJobDefaults }} + needs: release + if: needs.release.outputs.releases + steps: + - name: Comment Text + uses: actions/github-script@v6 + id: comment-text + env: + RELEASES: $\{{ needs.release.outputs.releases }} + with: + result-encoding: string + script: | + const releases = JSON.parse(process.env.RELEASES) + const { runId, repo: { owner, repo } } = context + const issue_number = releases[0].prNumber + + const releasePleaseComments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number }) + .then((comments) => comments.filter(c => c.login === 'github-actions[bot]' && c.body.includes('Release is at'))) + + for (const comment of releasePleaseComments) { + await github.rest.issues.deleteComment({ owner, repo, comment_id: comment.id }) + } + + let body = '## Release Workflow\n\n' + for (const { pkgName, version, url } of releases) { + body += `- \`${pkgName}@${version}\` ${url}\n` + } + body += `- Workflow run: :arrows_counterclockwise: https://github.com/${owner}/${repo}/actions/runs/${runId}` + return body + + - name: Create Release PR Comment + if: steps.comment-text.outputs.result + uses: ./.github/actions/upsert-comment + with: + token: $\{{ secrets.GITHUB_TOKEN }} + body: $\{{ steps.comment-text.outputs.result }} + number: $\{{ fromJson(needs.release.outputs.release).prNumber }} + includes: $\{{ github.run_id }} + + release-integration: + name: Post Release - Integration + needs: release + if: needs.release.outputs.release + uses: ./.github/workflows/release-integration.yml + with: + release: needs.release.outputs.release + releases: needs.release.outputs.releases + + post-release-integration: + name: Post Release - Post Integration + {{> partialsJobDefaults }} + needs: [release, release-integration] + if: needs.release.outputs.release && (success() || failure()) + steps: + - name: Get Needs Result + id: needs-result + run: | + if [[ "$\{{ contains(needs.*.result, 'failure') }}" == "true" ]]; then + result="x" + elif [[ "$\{{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then + result="heavy_multiplication_x" + else + result="white_check_mark" + fi + echo "result=$result" >> $GITHUB_OUTPUT + + - name: Comment Text + uses: actions/github-script@v6 + id: comment-text + env: + PR_NUMBER: $\{{ fromJSON(needs.release.outputs.release).prNumber }} + REF_NAME: $\{{ github.ref_name }} + RESULT: $\{{ steps.needs-result.outputs.result }} + with: + script: | + const { RESULT, PR_NUMBER, REF_NAME } = process.env + const tagCodeowner = RESULT !== 'white_check_mark' + if (tagCodeowner) { + let body = '' + body += `\n\n:rotating_light:` + body += ` {{ codeowner }}: The post-release workflow failed for this release.` + body += ` Manual steps may need to be taken after examining the workflow output` + body += ` from the above workflow run. :rotating_light:` + body += `\n\nTo rerun the workflow run the following command:\n\n` + body += `\`\`\`\ngh workflow run release.yml -r ${REF_NAME} -R ${owner}/${repo} -f release-pr=${PR_NUMBER}\n\`\`\`` + return body + } + + - name: Update Release PR Comment + if: steps.comment-text.outputs.result + uses: ./.github/actions/upsert-comment + with: + token: $\{{ secrets.GITHUB_TOKEN }} + body: "## Release Workflow" + find: "Workflow run: :[a-z_]+:" + replace: "Workflow run :$\{{ steps.needs-result.outputs.result }}:" + append: $\{{ steps.comment-text.outputs.result }} + number: $\{{ fromJson(needs.release.outputs.release).prNumber }} + includes: $\{{ github.run_id }} diff --git a/lib/util/parser.js b/lib/util/parser.js index e209dfe7..659f06b7 100644 --- a/lib/util/parser.js +++ b/lib/util/parser.js @@ -1,5 +1,5 @@ const fs = require('fs/promises') -const { basename, extname, dirname } = require('path') +const { basename, extname, dirname, relative, join } = require('path') const yaml = require('yaml') const NpmPackageJson = require('@npmcli/package-json') const jsonParse = require('json-parse-even-better-errors') @@ -60,7 +60,21 @@ class Base { return null } - read (s) { + async read (s) { + if (s === this.source) { + // allow shadowing files in configured content directories + // without needing to add the file to the config explicitly + const dirs = this.options.config.__PARTIAL_DIRS__ + const relDir = dirs.find(d => s.startsWith(d)) + const relFile = relative(relDir, s) + const lookupDirs = dirs.slice(0).reverse() + for (const dir of lookupDirs) { + const file = await fs.readFile(join(dir, relFile), { encoding: 'utf-8' }).catch(fsOk()) + if (file !== null) { + return file + } + } + } return fs.readFile(s, { encoding: 'utf-8' }) } diff --git a/lib/util/template.js b/lib/util/template.js index 1eb67ed7..df3962d7 100644 --- a/lib/util/template.js +++ b/lib/util/template.js @@ -1,27 +1,51 @@ const Handlebars = require('handlebars') -const { basename, extname, join } = require('path') +const { basename, dirname, sep, extname, join, relative } = require('path') const fs = require('fs') const DELETE = '__DELETE__' const safeValues = (obj) => Object.entries(obj).map(([key, value]) => [key, new Handlebars.SafeString(value)]) -const partialName = (s) => basename(s, extname(s)) // remove extension - .replace(/^_/, '') // remove leading underscore - .replace(/-([a-z])/g, (_, g) => g.toUpperCase()) // camelcase +const partialName = (s) => { + const dir = dirname(s) + return join(dir === '.' ? '' : dir, basename(s, extname(s)))// remove extension + // camelcase with -, /, and \\ as separators + .replace(/(?:[-/]|\\)([a-z])/g, (_, g) => g.toUpperCase()) +} + +const walkDir = (dir, res = [], root = dir) => { + const contents = fs.readdirSync(dir) + for (const c of contents) { + if (c.startsWith('.')) { + continue + } + const itemPath = join(dir, c) + if (fs.statSync(itemPath).isDirectory()) { + walkDir(itemPath, res, root) + } else { + res.push({ + name: relative(root, itemPath), + path: itemPath, + }) + } + } + return res +} const makePartials = (dir, isBase) => { - const partials = fs.readdirSync(dir).reduce((acc, f) => { - const partial = fs.readFileSync(join(dir, f)).toString() - const name = partialName(f) + const contents = walkDir(dir) + + const partials = contents.reduce((acc, f) => { + const partial = fs.readFileSync(f.path).toString() + const name = partialName(f.name) if (isBase) { // in the default dir, everything is a partial // and also gets set with a default prefix for extending acc[name] = partial acc[partialName(`default-${name}`)] = partial - } else if (f.startsWith('_')) { - // otherwise only _ files get set as partials + } else if (f.name.startsWith('partials' + sep)) { + // otherwise only files in partials dir get set as partials acc[name] = partial } @@ -33,12 +57,16 @@ const makePartials = (dir, isBase) => { const setupHandlebars = (baseDir, ...otherDirs) => { Handlebars.registerHelper('obj', ({ hash }) => Object.fromEntries(safeValues(hash))) - Handlebars.registerHelper('join', (arr, sep) => arr.join(typeof sep === 'string' ? sep : ', ')) + Handlebars.registerHelper('join', (arr, s) => arr.join(typeof s === 'string' ? s : ', ')) Handlebars.registerHelper('pluck', (arr, key) => arr.map(a => a[key])) Handlebars.registerHelper('quote', (arr) => arr.map(a => `'${a}'`)) Handlebars.registerHelper('last', (arr) => arr[arr.length - 1]) + Handlebars.registerHelper('first', (arr) => arr[0]) + Handlebars.registerHelper('add', (num, addend) => +num + +addend) + Handlebars.registerHelper('padHour', (num, addend) => num.toString().padStart(2, '0')) Handlebars.registerHelper('json', (c) => JSON.stringify(c)) Handlebars.registerHelper('del', () => JSON.stringify(DELETE)) + Handlebars.registerHelper('newline', () => '\n') makePartials(baseDir, true) for (const dir of otherDirs) { diff --git a/package.json b/package.json index 4fc56507..bf1b1eb0 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "template-oss-apply": "bin/apply.js", "template-oss-check": "bin/check.js", "template-oss-release-please": "bin/release-please.js", - "template-oss-release-manager": "bin/release-manager.js" + "template-oss-release-manager": "bin/release-manager.js", + "template-oss-changed-workspaces": "bin/changed-workspaces.js" }, "scripts": { "lint": "eslint \"**/*.js\"", diff --git a/tap-snapshots/test/apply/files-snapshots.js.test.cjs b/tap-snapshots/test/apply/files-snapshots.js.test.cjs index 382a5258..3fe18c9e 100644 --- a/tap-snapshots/test/apply/files-snapshots.js.test.cjs +++ b/tap-snapshots/test/apply/files-snapshots.js.test.cjs @@ -8,19 +8,27 @@ exports[`test/apply/files-snapshots.js TAP private workspace > expect resolving Promise 1`] = ` .commitlintrc.js .eslintrc.js +.github/actions/audit/action.yml +.github/actions/changed-files/action.yml +.github/actions/changed-workspaces/action.yml +.github/actions/conclude-check/action.yml +.github/actions/create-check/action.yml +.github/actions/deps/action.yml +.github/actions/lint/action.yml +.github/actions/setup/action.yml +.github/actions/test/action.yml +.github/actions/upsert-comment/action.yml .github/CODEOWNERS .github/dependabot.yml .github/ISSUE_TEMPLATE/bug.yml .github/ISSUE_TEMPLATE/config.yml .github/matchers/tap.json .github/workflows/audit.yml -.github/workflows/ci-a.yml -.github/workflows/ci-b.yml -.github/workflows/ci-release.yml .github/workflows/ci.yml .github/workflows/codeql-analysis.yml .github/workflows/post-dependabot.yml .github/workflows/pull-request.yml +.github/workflows/release-integration.yml .github/workflows/release.yml .gitignore .npmrc @@ -39,17 +47,27 @@ workspaces/b/package.json exports[`test/apply/files-snapshots.js TAP turn off add/rm types > expect resolving Promise 1`] = ` .commitlintrc.js +.github/actions/audit/action.yml +.github/actions/changed-files/action.yml +.github/actions/changed-workspaces/action.yml +.github/actions/conclude-check/action.yml +.github/actions/create-check/action.yml +.github/actions/deps/action.yml +.github/actions/lint/action.yml +.github/actions/setup/action.yml +.github/actions/test/action.yml +.github/actions/upsert-comment/action.yml .github/CODEOWNERS .github/dependabot.yml .github/ISSUE_TEMPLATE/bug.yml .github/ISSUE_TEMPLATE/config.yml .github/matchers/tap.json .github/workflows/audit.yml -.github/workflows/ci-release.yml .github/workflows/ci.yml .github/workflows/codeql-analysis.yml .github/workflows/post-dependabot.yml .github/workflows/pull-request.yml +.github/workflows/release-integration.yml .github/workflows/release.yml .release-please-manifest.json package.json @@ -58,17 +76,27 @@ release-please-config.json exports[`test/apply/files-snapshots.js TAP turn off module > expect resolving Promise 1`] = ` .commitlintrc.js +.github/actions/audit/action.yml +.github/actions/changed-files/action.yml +.github/actions/changed-workspaces/action.yml +.github/actions/conclude-check/action.yml +.github/actions/create-check/action.yml +.github/actions/deps/action.yml +.github/actions/lint/action.yml +.github/actions/setup/action.yml +.github/actions/test/action.yml +.github/actions/upsert-comment/action.yml .github/CODEOWNERS .github/dependabot.yml .github/ISSUE_TEMPLATE/bug.yml .github/ISSUE_TEMPLATE/config.yml .github/matchers/tap.json .github/workflows/audit.yml -.github/workflows/ci-release.yml .github/workflows/ci.yml .github/workflows/codeql-analysis.yml .github/workflows/post-dependabot.yml .github/workflows/pull-request.yml +.github/workflows/release-integration.yml .github/workflows/release.yml .release-please-manifest.json package.json @@ -90,17 +118,27 @@ package.json exports[`test/apply/files-snapshots.js TAP turn off specific files > expect resolving Promise 1`] = ` .eslintrc.yml +.github/actions/audit/action.yml +.github/actions/changed-files/action.yml +.github/actions/changed-workspaces/action.yml +.github/actions/conclude-check/action.yml +.github/actions/create-check/action.yml +.github/actions/deps/action.yml +.github/actions/lint/action.yml +.github/actions/setup/action.yml +.github/actions/test/action.yml +.github/actions/upsert-comment/action.yml .github/CODEOWNERS .github/dependabot.yml .github/ISSUE_TEMPLATE/bug.yml .github/ISSUE_TEMPLATE/config.yml .github/matchers/tap.json .github/workflows/audit.yml -.github/workflows/ci-release.yml .github/workflows/ci.yml .github/workflows/codeql-analysis.yml .github/workflows/post-dependabot.yml .github/workflows/pull-request.yml +.github/workflows/release-integration.yml .github/workflows/release-test.yml .github/workflows/release.yml .gitignore @@ -113,12 +151,24 @@ SECURITY.md ` exports[`test/apply/files-snapshots.js TAP workspaces > expect resolving Promise 1`] = ` +.github/actions/audit/action.yml +.github/actions/changed-files/action.yml +.github/actions/changed-workspaces/action.yml +.github/actions/conclude-check/action.yml +.github/actions/create-check/action.yml +.github/actions/deps/action.yml +.github/actions/lint/action.yml +.github/actions/setup/action.yml +.github/actions/test/action.yml +.github/actions/upsert-comment/action.yml .github/dependabot.yml .github/matchers/tap.json -.github/workflows/ci-d.yml -.github/workflows/ci-release.yml +.github/workflows/audit.yml +.github/workflows/ci.yml +.github/workflows/codeql-analysis.yml .github/workflows/post-dependabot.yml .github/workflows/pull-request.yml +.github/workflows/release-integration.yml .github/workflows/release.yml .release-please-manifest.json package.json @@ -136,13 +186,24 @@ workspaces/d/package.json ` exports[`test/apply/files-snapshots.js TAP workspaces only (like npm/cli) > expect resolving Promise 1`] = ` +.github/actions/audit/action.yml +.github/actions/changed-files/action.yml +.github/actions/changed-workspaces/action.yml +.github/actions/conclude-check/action.yml +.github/actions/create-check/action.yml +.github/actions/deps/action.yml +.github/actions/lint/action.yml +.github/actions/setup/action.yml +.github/actions/test/action.yml +.github/actions/upsert-comment/action.yml .github/dependabot.yml .github/matchers/tap.json -.github/workflows/ci-a.yml -.github/workflows/ci-b.yml -.github/workflows/ci-release.yml +.github/workflows/audit.yml +.github/workflows/ci.yml +.github/workflows/codeql-analysis.yml .github/workflows/post-dependabot.yml .github/workflows/pull-request.yml +.github/workflows/release-integration.yml .github/workflows/release.yml .release-please-manifest.json package.json diff --git a/tap-snapshots/test/apply/source-snapshots.js.test.cjs b/tap-snapshots/test/apply/source-snapshots.js.test.cjs index 958a5e4d..7a9641c6 100644 --- a/tap-snapshots/test/apply/source-snapshots.js.test.cjs +++ b/tap-snapshots/test/apply/source-snapshots.js.test.cjs @@ -39,6 +39,452 @@ module.exports = { ], } +.github/actions/audit/action.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Audit + +inputs: + shell: + description: shell to run on + default: bash + +runs: + using: composite + steps: + - name: Run Audit + shell: \${{ inputs.shell }} + run: | + npm audit --omit=dev + npm audit --audit-level=none + +.github/actions/changed-files/action.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Get Changed Files + +inputs: + token: + description: GitHub token to use + required: true + +outputs: + files: + value: \${{ steps.files.outputs.result }} + +runs: + using: composite + steps: + - name: Get Changed Files + uses: actions/github-script@v6 + id: files + with: + github-token: \${{ inputs.token }} + script: | + const { repo: { owner, repo }, eventName, payload, sha } = context + let files + if (eventName === 'pull_request' || eventName === 'pull_request_target') { + files = await github.paginate(github.rest.pulls.listFiles, { + owner, + repo, + pull_number: payload.pull_request.number, + }) + } else { + const { data: commit } = await github.rest.repos.getCommit({ + owner, + repo, + ref: sha, + }) + files = commit.files + } + return files.map(f => f.filename) + +.github/actions/changed-workspaces/action.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Get Changed Workspaces + +inputs: + token: + description: GitHub token to use + required: true + shell: + description: shell to run on + default: bash + all: + default: false + type: boolean + files: + description: json stringified array of file names + type: string + +outputs: + flags: + value: \${{ steps.workspaces.outputs.flags }} + +runs: + using: composite + steps: + - name: Get Changed Files + uses: ./.github/actions/changed-files + if: \${{ !inputs.all && !inputs.files }} + id: files + + - name: Get Workspaces + shell: \${{ inputs.shell }} + id: workspaces + run: | + flags=$(npm exec --offline -- template-oss-changed-workspaces '\${{ (inputs.all && '--all') || (inputs.files || steps.files.outputs.result) }}') + echo "flags=\${flags}" >> $GITHUB_OUTPUT + +.github/actions/conclude-check/action.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Conclude Check +description: Conclude a check + +inputs: + token: + description: GitHub token to use + required: true + conclusion: + description: conclusion of check + require: true + check-id: + description: id of check to conclude + required: true + +runs: + using: composite + steps: + - name: Conclude Check + uses: LouisBrunner/checks-action@v1.5.0 + with: + token: \${{ inputs.token }} + conclusion: \${{ inputs.conclusion }} + check_id: \${{ inputs.check-id }} + +.github/actions/create-check/action.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Create Check +description: Create a check and associate it with a sha + +inputs: + token: + description: GitHub token to use + required: true + sha: + description: sha to attach the check to + required: true + job-name: + description: Name of the job to find + required: true + job-status: + description: Status of the check being created + default: in_progress + +outputs: + check-id: + description: The ID of the check that was created + value: \${{ steps.check.outputs.check_id }} + +runs: + using: composite + steps: + - name: Get Workflow Job + uses: actions/github-script@v6 + id: workflow-job + env: + JOB_NAME: \${{ inputs.job-name }} + with: + github-token: \${{ inputs.token }} + script: | + const { JOB_NAME } = process.env + const { repo: { owner, repo }, runId, serverUrl } = context + + const jobs = await github.paginate(github.rest.actions.listJobsForWorkflowRun, { + owner, + repo, + run_id: runId, + }) + const job = jobs.find(j => j.name.endsWith(JOB_NAME)) + + const shaUrl = \`\${serverUrl}/\${owner}/\${repo}/commit/\${{ inputs.sha }}\` + const summary = \`This check is assosciated with \${shaUrl}/n/n\` + const message = job?.html_url + ? \`For run logs, click here: \${job.html_url}\` + : \`Run logs could not be found for a job with name: "\${JOB_NAME}"\` + + // Return a json object with properties that LouisBrunner/checks-actions + // expects as the output of the check + return { summary: summary + message } + + - name: Create Check + uses: LouisBrunner/checks-action@v1.5.0 + id: check + with: + token: \${{ inputs.token }} + status: \${{ inputs.job-status }} + name: \${{ inputs.job-name }} + sha: \${{ inputs.sha }} + output: \${{ steps.workflow-job.outputs.result }} + +.github/actions/deps/action.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Dependencies + +inputs: + command: + description: command to run for the dependencies step + default: install --ignore-scripts --no-audit --no-fund + flags: + description: extra flags to pass to the dependencies step + shell: + description: shell to run on + default: bash + +runs: + using: composite + steps: + - name: Install Dependencies + shell: \${{ inputs.shell }} + run: npm \${{ inputs.command }} \${{ inputs.flags }} + +.github/actions/lint/action.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Lint + +inputs: + flags: + description: flags to pass to the commands + shell: + description: shell to run on + default: bash + +runs: + using: composite + steps: + - name: Lint + shell: \${{ inputs.shell }} + run: | + npm run lint --ignore-scripts \${{ inputs.flags }} + npm run postlint --ignore-scripts \${{ inputs.flags }} + +.github/actions/setup/action.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Setup Repo +description: Setup a repo with standard tools + +inputs: + node-version: + description: node version to use + default: 18.x + npm-version: + description: npm version to use + default: latest + cache: + description: whether to cache npm install or not + type: boolean + default: false + shell: + description: shell to run on + default: bash + deps: + description: whether to run the deps step + type: boolean + default: true + deps-command: + description: command to run for the dependencies step + default: install --ignore-scripts --no-audit --no-fund + deps-flags: + description: extra flags to pass to the dependencies step + +runs: + using: composite + steps: + - name: Setup Git User + shell: \${{ inputs.shell }} + run: | + git config --global user.email "npm-cli+bot@github.com" + git config --global user.name "npm CLI robot" + + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: \${{ inputs.node-version }} + cache: \${{ (inputs.cache && 'npm') || null }} + + - name: Check Node Version + if: inputs.npm-version + id: node-version + shell: \${{ inputs.shell }} + run: | + NODE_VERSION=$(node --version) + echo $NODE_VERSION + if npx semver@7 -r "<=10" "$NODE_VERSION" --yes; then + echo "ten-or-lower=true" >> $GITHUB_OUTPUT + fi + if npx semver@7 -r "<=14" "$NODE_VERSION" --yes; then + echo "fourteen-or-lower=true" >> $GITHUB_OUTPUT + fi + + - name: Update Windows npm + # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows + if: inputs.npm-version && runner.os == 'Windows' && steps.node-version.outputs.fourteen-or-lower + shell: \${{ inputs.shell }} + run: | + curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz + tar xf npm-7.5.4.tgz + cd package + node lib/npm.js install --no-fund --no-audit -g ../npm-7.5.4.tgz + cd .. + rmdir /s /q package + + - name: Install npm@7 + if: inputs.npm-version && steps.node-version.outputs.ten-or-lower + shell: \${{ inputs.shell }} + run: npm i --prefer-online --no-fund --no-audit -g npm@7 + + - name: Install npm@\${{ inputs.npm-version }} + if: inputs.npm-version && !steps.node-version.outputs.ten-or-lower + shell: \${{ inputs.shell }} + run: npm i --prefer-online --no-fund --no-audit -g npm@\${{ inputs.npm-version }} + + - name: npm Version + shell: \${{ inputs.shell }} + run: npm -v + + - name: Setup Dependencies + if: inputs.deps + uses: ./.github/actions/deps + with: + command: \${{ inputs.deps-command }} + flags: \${{ inputs.deps-flags }} + + - name: Add Problem Matcher + shell: \${{ inputs.shell }} + run: | + [[ -f ./.github/matchers/tap.json ]] && echo "::add-matcher::.github/matchers/tap.json" + +.github/actions/test/action.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Test + +inputs: + flags: + description: flags to pass to the commands + shell: + description: shell to run on + default: bash + +runs: + using: composite + steps: + - name: Test + shell: \${{ inputs.shell }} + run: npm test --ignore-scripts \${{ inputs.flags }} + +.github/actions/upsert-comment/action.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Upsert Comment +description: Update or create a comment + +inputs: + token: + description: GitHub token to use + required: true + number: + description: Number of the issue or pull request + required: true + login: + description: Login name of user to look for comments from + default: github-actions[bot] + body: + description: Body of the comment, the first line will be used to match to an existing comment + find: + description: string to find in body + replace: + description: string to replace in body + append: + description: string to append to the body + includes: + description: A string that the comment needs to include + +outputs: + comment-id: + description: The ID of the comment + value: \${{ steps.comment.outputs.result }} + +runs: + using: composite + steps: + - name: Create or Update Comment + uses: actions/github-script@v6 + id: comment + env: + NUMBER: \${{ inputs.number }} + BODY: \${{ inputs.body }} + FIND: \${{ inputs.find }} + REPLACE: \${{ inputs.replace }} + APPEND: \${{ inputs.append }} + LOGIN: \${{ inputs.login }} + INCLUDES: \${{ inputs.includes }} + with: + github-token: \${{ inputs.token }} + script: | + const { BODY, FIND, REPLACE, APPEND, LOGIN, NUMBER: issue_number, INCLUDES } = process.env + const { repo: { owner, repo } } = context + const TITLE = BODY.split('/n')[0].trim() + '/n' + const bodyIncludes = (c) => INCLUDES ? c.body.includes(INCLUDES) : true + + const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number }) + .then(comments => comments.map(c => ({ id: c.id, login: c.user.login, body: c.body }))) + + console.log(\`Found comments: \${JSON.stringify(comments, null, 2)}\`) + console.log(\`Looking for comment with: \${JSON.stringify({ LOGIN, TITLE, INCLUDES }, null, 2)}\`) + + const comment = comments.find(c => + c.login === LOGIN && + c.body.startsWith(TITLE) && + bodyIncludes(c) + ) + + if (comment) { + console.log(\`Found comment: \${JSON.stringify(comment, null, 2)}\`) + let newBody = FIND && REPLACE ? comment.body.replace(new RegExp(FIND, 'g'), REPLACE) : BODY + if (APPEND) { + newBody += APPEND + } + await github.rest.issues.updateComment({ owner, repo, comment_id: comment.id, body: newBody }) + return comment.id + } + + if (FIND || REPLACE || APPEND) { + console.log('Could not find a comment to use find/replace or append to') + return + } + + console.log('Creating new comment') + + const res = await github.rest.issues.createComment({ owner, repo, issue_number, body: BODY }) + return res.data.id + .github/CODEOWNERS ======================================== # This file is automatically added by @npmcli/template-oss. Do not edit. @@ -172,8 +618,8 @@ name: Audit on: workflow_dispatch: schedule: - # "At 08:00 UTC (01:00 PT) on Monday" https://crontab.guru/#0_8_*_*_1 - - cron: "0 8 * * 1" + # "At 09:00 UTC (01:00 PT) on Monday" https://crontab.guru/#0_9_*_*_1 + - cron: "0 9 * * 1" jobs: audit: @@ -186,30 +632,20 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 - - name: Setup Git User - run: | - git config --global user.email "npm-cli+bot@github.com" - git config --global user.name "npm CLI robot" - - name: Setup Node - uses: actions/setup-node@v3 + + - name: Setup + uses: ./.github/actions/setup with: - node-version: 18.x - - name: Install npm@latest - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - name: npm Version - run: npm -v - - name: Install Dependencies - run: npm i --ignore-scripts --no-audit --no-fund --package-lock - - name: Run Production Audit - run: npm audit --omit=dev - - name: Run Full Audit - run: npm audit --audit-level=none - -.github/workflows/ci-release.yml + deps-flags: "--package-lock" + + - name: Audit + uses: ./.github/actions/audit + +.github/workflows/ci.yml ======================================== # This file is automatically added by @npmcli/template-oss. Do not edit. -name: CI - Release +name: CI on: workflow_dispatch: @@ -217,7 +653,11 @@ on: ref: required: true type: string - default: main + check-sha: + type: string + all: + default: true + type: boolean workflow_call: inputs: ref: @@ -226,92 +666,75 @@ on: check-sha: required: true type: string + all: + default: true + type: boolean + pull_request: + push: + branches: + - main + - latest + schedule: + # "At 10:00 UTC (02:00 PT) on Monday" https://crontab.guru/#0_10_*_*_1 + - cron: "0 10 * * 1" jobs: - lint-all: - name: Lint All + lint: + name: Lint if: github.repository_owner == 'npm' runs-on: ubuntu-latest defaults: run: shell: bash steps: - - name: Get Workflow Job - uses: actions/github-script@v6 - if: inputs.check-sha - id: check-output - env: - JOB_NAME: "Lint All" - MATRIX_NAME: "" + - name: Checkout + uses: actions/checkout@v3 with: - script: | - const { owner, repo } = context.repo - - const { data } = await github.rest.actions.listJobsForWorkflowRun({ - owner, - repo, - run_id: context.runId, - per_page: 100 - }) - - const jobName = process.env.JOB_NAME + process.env.MATRIX_NAME - const job = data.jobs.find(j => j.name.endsWith(jobName)) - const jobUrl = job?.html_url - - const shaUrl = \`\${context.serverUrl}/\${owner}/\${repo}/commit/\${{ inputs.check-sha }}\` - - let summary = \`This check is assosciated with \${shaUrl}/n/n\` - - if (jobUrl) { - summary += \`For run logs, click here: \${jobUrl}\` - } else { - summary += \`Run logs could not be found for a job with name: "\${jobName}"\` - } + ref: \${{ inputs.ref }} - return { summary } - name: Create Check - uses: LouisBrunner/checks-action@v1.3.1 - id: check + uses: ./.github/actions/create-check if: inputs.check-sha + id: check with: - token: \${{ secrets.GITHUB_TOKEN }} - status: in_progress - name: Lint All sha: \${{ inputs.check-sha }} - output: \${{ steps.check-output.outputs.result }} - - name: Checkout - uses: actions/checkout@v3 - with: - ref: \${{ inputs.ref }} - - name: Setup Git User - run: | - git config --global user.email "npm-cli+bot@github.com" - git config --global user.name "npm CLI robot" - - name: Setup Node - uses: actions/setup-node@v3 + token: \${{ secrets.GITHUB_TOKEN }} + job-name: Lint + + - name: Setup + id: setup + continue-on-error: \${{ !!steps.check.outputs.check-id }} + uses: ./.github/actions/setup + + - name: Get Changed Workspaces + id: workspaces + continue-on-error: \${{ !!steps.check.outputs.check-id }} + uses: ./.github/actions/changed-workspaces with: - node-version: 18.x - - name: Install npm@latest - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - name: npm Version - run: npm -v - - name: Install Dependencies - run: npm i --ignore-scripts --no-audit --no-fund + token: \${{ secrets.GITHUB_TOKEN }} + all: \${{ inputs.all }} + - name: Lint - run: npm run lint --ignore-scripts - - name: Post Lint - run: npm run postlint --ignore-scripts + uses: ./.github/actions/lint + continue-on-error: \${{ !!steps.check.outputs.check-id }} + with: + flags: \${{ steps.workspaces.outputs.flags }} + - name: Conclude Check - uses: LouisBrunner/checks-action@v1.3.1 - if: steps.check.outputs.check_id && always() + uses: ./.github/actions/conclude-check + if: steps.check.outputs.check-id && (success() || failure()) with: token: \${{ secrets.GITHUB_TOKEN }} conclusion: \${{ job.status }} - check_id: \${{ steps.check.outputs.check_id }} + check-id: \${{ steps.check.outputs.check-id }} - test-all: - name: Test All - \${{ matrix.platform.name }} - \${{ matrix.node-version }} + test: + name: Test - \${{ matrix.platform.name }} - \${{ matrix.node-version }} if: github.repository_owner == 'npm' + runs-on: \${{ matrix.platform.os }} + defaults: + run: + shell: \${{ matrix.platform.shell }} strategy: fail-fast: false matrix: @@ -332,207 +755,63 @@ jobs: - 16.x - 18.0.0 - 18.x - runs-on: \${{ matrix.platform.os }} - defaults: - run: - shell: \${{ matrix.platform.shell }} steps: - - name: Get Workflow Job - uses: actions/github-script@v6 - if: inputs.check-sha - id: check-output - env: - JOB_NAME: "Test All" - MATRIX_NAME: " - \${{ matrix.platform.name }} - \${{ matrix.node-version }}" - with: - script: | - const { owner, repo } = context.repo - - const { data } = await github.rest.actions.listJobsForWorkflowRun({ - owner, - repo, - run_id: context.runId, - per_page: 100 - }) - - const jobName = process.env.JOB_NAME + process.env.MATRIX_NAME - const job = data.jobs.find(j => j.name.endsWith(jobName)) - const jobUrl = job?.html_url - - const shaUrl = \`\${context.serverUrl}/\${owner}/\${repo}/commit/\${{ inputs.check-sha }}\` - - let summary = \`This check is assosciated with \${shaUrl}/n/n\` + - name: Continue Matrix Run + id: continue-matrix + run: | + if [[ "\${{ matrix.node-version }}" == "14.17.0" || "\${{ inputs.all }}" == "true" ]]; then + echo "result=true" >> $GITHUB_OUTPUT + fi - if (jobUrl) { - summary += \`For run logs, click here: \${jobUrl}\` - } else { - summary += \`Run logs could not be found for a job with name: "\${jobName}"\` - } + - name: Checkout + if: steps.continue-matrix.outputs.result + uses: actions/checkout@v3 + with: + ref: \${{ inputs.ref }} - return { summary } - name: Create Check - uses: LouisBrunner/checks-action@v1.3.1 + if: steps.continue-matrix.outputs.result && inputs.check-sha + uses: ./.github/actions/create-check id: check - if: inputs.check-sha with: - token: \${{ secrets.GITHUB_TOKEN }} - status: in_progress - name: Test All - \${{ matrix.platform.name }} - \${{ matrix.node-version }} sha: \${{ inputs.check-sha }} - output: \${{ steps.check-output.outputs.result }} - - name: Checkout - uses: actions/checkout@v3 - with: - ref: \${{ inputs.ref }} - - name: Setup Git User - run: | - git config --global user.email "npm-cli+bot@github.com" - git config --global user.name "npm CLI robot" - - name: Setup Node - uses: actions/setup-node@v3 + token: \${{ secrets.GITHUB_TOKEN }} + job-name: "Test - \${{ matrix.platform.name }} - \${{ matrix.node-version }}" + + - name: Setup + if: steps.continue-matrix.outputs.result + uses: ./.github/actions/setup + id: setup + continue-on-error: \${{ !!steps.check.outputs.check-id }} with: node-version: \${{ matrix.node-version }} - - name: Update Windows npm - # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows - if: matrix.platform.os == 'windows-latest' && (startsWith(matrix.node-version, '12.') || startsWith(matrix.node-version, '14.')) - run: | - curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz - tar xf npm-7.5.4.tgz - cd package - node lib/npm.js install --no-fund --no-audit -g ../npm-7.5.4.tgz - cd .. - rmdir /s /q package - - name: Install npm@7 - if: startsWith(matrix.node-version, '10.') - run: npm i --prefer-online --no-fund --no-audit -g npm@7 - - name: Install npm@latest - if: \${{ !startsWith(matrix.node-version, '10.') }} - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - name: npm Version - run: npm -v - - name: Install Dependencies - run: npm i --ignore-scripts --no-audit --no-fund - - name: Add Problem Matcher - run: echo "::add-matcher::.github/matchers/tap.json" - - name: Test - run: npm test --ignore-scripts - - name: Conclude Check - uses: LouisBrunner/checks-action@v1.3.1 - if: steps.check.outputs.check_id && always() + shell: \${{ matrix.platform.shell }} + + - name: Get Changed Workspaces + if: steps.continue-matrix.outputs.result + id: workspaces + continue-on-error: \${{ !!steps.check.outputs.check-id }} + uses: ./.github/actions/changed-workspaces with: + shell: \${{ matrix.platform.shell }} token: \${{ secrets.GITHUB_TOKEN }} - conclusion: \${{ job.status }} - check_id: \${{ steps.check.outputs.check_id }} - -.github/workflows/ci.yml -======================================== -# This file is automatically added by @npmcli/template-oss. Do not edit. - -name: CI - -on: - workflow_dispatch: - pull_request: - push: - branches: - - main - - latest - schedule: - # "At 09:00 UTC (02:00 PT) on Monday" https://crontab.guru/#0_9_*_*_1 - - cron: "0 9 * * 1" + all: \${{ inputs.all }} -jobs: - lint: - name: Lint - if: github.repository_owner == 'npm' - runs-on: ubuntu-latest - defaults: - run: - shell: bash - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Setup Git User - run: | - git config --global user.email "npm-cli+bot@github.com" - git config --global user.name "npm CLI robot" - - name: Setup Node - uses: actions/setup-node@v3 + - name: Test + if: steps.continue-matrix.outputs.result + uses: ./.github/actions/test + continue-on-error: \${{ !!steps.check.outputs.check-id }} with: - node-version: 18.x - - name: Install npm@latest - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - name: npm Version - run: npm -v - - name: Install Dependencies - run: npm i --ignore-scripts --no-audit --no-fund - - name: Lint - run: npm run lint --ignore-scripts - - name: Post Lint - run: npm run postlint --ignore-scripts + flags: \${{ steps.workspaces.outputs.flags }} + shell: \${{ matrix.platform.shell }} - test: - name: Test - \${{ matrix.platform.name }} - \${{ matrix.node-version }} - if: github.repository_owner == 'npm' - strategy: - fail-fast: false - matrix: - platform: - - name: Linux - os: ubuntu-latest - shell: bash - - name: macOS - os: macos-latest - shell: bash - - name: Windows - os: windows-latest - shell: cmd - node-version: - - 14.17.0 - - 14.x - - 16.13.0 - - 16.x - - 18.0.0 - - 18.x - runs-on: \${{ matrix.platform.os }} - defaults: - run: - shell: \${{ matrix.platform.shell }} - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Setup Git User - run: | - git config --global user.email "npm-cli+bot@github.com" - git config --global user.name "npm CLI robot" - - name: Setup Node - uses: actions/setup-node@v3 + - name: Conclude Check + uses: ./.github/actions/conclude-check + if: steps.continue-matrix.outputs.result && steps.check.outputs.check-id && (success() || failure()) with: - node-version: \${{ matrix.node-version }} - - name: Update Windows npm - # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows - if: matrix.platform.os == 'windows-latest' && (startsWith(matrix.node-version, '12.') || startsWith(matrix.node-version, '14.')) - run: | - curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz - tar xf npm-7.5.4.tgz - cd package - node lib/npm.js install --no-fund --no-audit -g ../npm-7.5.4.tgz - cd .. - rmdir /s /q package - - name: Install npm@7 - if: startsWith(matrix.node-version, '10.') - run: npm i --prefer-online --no-fund --no-audit -g npm@7 - - name: Install npm@latest - if: \${{ !startsWith(matrix.node-version, '10.') }} - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - name: npm Version - run: npm -v - - name: Install Dependencies - run: npm i --ignore-scripts --no-audit --no-fund - - name: Add Problem Matcher - run: echo "::add-matcher::.github/matchers/tap.json" - - name: Test - run: npm test --ignore-scripts + token: \${{ secrets.GITHUB_TOKEN }} + conclusion: \${{ job.status }} + check-id: \${{ steps.check.outputs.check-id }} .github/workflows/codeql-analysis.yml ======================================== @@ -550,13 +829,17 @@ on: - main - latest schedule: - # "At 10:00 UTC (03:00 PT) on Monday" https://crontab.guru/#0_10_*_*_1 - - cron: "0 10 * * 1" + # "At 11:00 UTC (03:00 PT) on Monday" https://crontab.guru/#0_11_*_*_1 + - cron: "0 11 * * 1" jobs: analyze: name: Analyze + if: github.repository_owner == 'npm' runs-on: ubuntu-latest + defaults: + run: + shell: bash permissions: actions: read contents: read @@ -564,14 +847,12 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 - - name: Setup Git User - run: | - git config --global user.email "npm-cli+bot@github.com" - git config --global user.name "npm CLI robot" + - name: Initialize CodeQL uses: github/codeql-action/init@v2 with: languages: javascript + - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 @@ -583,62 +864,51 @@ name: Post Dependabot on: pull_request -permissions: - contents: write - jobs: template-oss: name: template-oss + permissions: + contents: write if: github.repository_owner == 'npm' && github.actor == 'dependabot[bot]' runs-on: ubuntu-latest defaults: run: shell: bash steps: - - name: Checkout - uses: actions/checkout@v3 - with: - ref: \${{ github.event.pull_request.head.ref }} - - name: Setup Git User - run: | - git config --global user.email "npm-cli+bot@github.com" - git config --global user.name "npm CLI robot" - - name: Setup Node - uses: actions/setup-node@v3 - with: - node-version: 18.x - - name: Install npm@latest - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - name: npm Version - run: npm -v - - name: Install Dependencies - run: npm i --ignore-scripts --no-audit --no-fund - name: Fetch Dependabot Metadata id: metadata uses: dependabot/fetch-metadata@v1 with: github-token: \${{ secrets.GITHUB_TOKEN }} - # Dependabot can update multiple directories so we output which directory - # it is acting on so we can run the command for the correct root or workspace - - name: Get Dependabot Directory + - name: Is Dependency if: contains(steps.metadata.outputs.dependency-names, '@npmcli/template-oss') - id: flags - run: | - dependabot_dir="\${{ steps.metadata.outputs.directory }}" - if [[ "$dependabot_dir" == "/" ]]; then - echo "workspace=-iwr" >> $GITHUB_OUTPUT - else - # strip leading slash from directory so it works as a - # a path to the workspace flag - echo "workspace=-w \${dependabot_dir#/}" >> $GITHUB_OUTPUT - fi + id: dependency + run: echo "continue=true" >> $GITHUB_OUTPUT + + - name: Checkout + if: steps.dependency.outputs.continue + uses: actions/checkout@v3 + with: + ref: \${{ github.event.pull_request.head.ref }} + + - name: Setup + if: steps.dependency.outputs.continue + uses: ./.github/actions/setup + + - name: Get Workspaces + if: steps.dependency.outputs.continue + uses: ./.github/actions/changed-workspaces + id: workspaces + with: + token: \${{ secrets.GITHUB_TOKEN }} + files: '["\${{ steps.metadata.outputs.directory }}"]' - name: Apply Changes - if: steps.flags.outputs.workspace + if: steps.workspaces.outputs.flags id: apply run: | - npm run template-oss-apply \${{ steps.flags.outputs.workspace }} + npm run template-oss-apply \${{ steps.workspaces.outputs.flags }} if [[ \`git status --porcelain\` ]]; then echo "changes=true" >> $GITHUB_OUTPUT fi @@ -690,7 +960,7 @@ jobs: - name: Check Changes if: steps.apply.outputs.changes run: | - npm exec --offline \${{ steps.flags.outputs.workspace }} -- template-oss-check + npm exec --offline \${{ steps.workspaces.outputs.flags }} -- template-oss-check - name: Fail on Breaking Change if: steps.apply.outputs.changes && startsWith(steps.apply.outputs.message, 'feat!') @@ -715,7 +985,7 @@ on: jobs: commitlint: - name: Lint Commits + name: Lint Commit if: github.repository_owner == 'npm' runs-on: ubuntu-latest defaults: @@ -726,47 +996,101 @@ jobs: uses: actions/checkout@v3 with: fetch-depth: 0 - - name: Setup Git User - run: | - git config --global user.email "npm-cli+bot@github.com" - git config --global user.name "npm CLI robot" - - name: Setup Node - uses: actions/setup-node@v3 - with: - node-version: 18.x - - name: Install npm@latest - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - name: npm Version - run: npm -v - - name: Install Dependencies - run: npm i --ignore-scripts --no-audit --no-fund + + - name: Setup + uses: ./.github/actions/setup + - name: Run Commitlint on Commits id: commit continue-on-error: true run: | - npx --offline commitlint -V --from 'origin/\${{ github.base_ref }}' --to \${{ github.event.pull_request.head.sha }} + npx --offline commitlint -V --from 'origin/\${{ github.base_ref }}' --to '\${{ github.event.pull_request.head.sha }}' + - name: Run Commitlint on PR Title if: steps.commit.outcome == 'failure' + env: + PR_TITLE: \${{ github.event.pull_request.title }} run: | - echo '\${{ github.event.pull_request.title }}' | npx --offline commitlint -V + echo "$PR_TITLE" | npx --offline commitlint -V -.github/workflows/release.yml +.github/workflows/release-integration.yml ======================================== # This file is automatically added by @npmcli/template-oss. Do not edit. -name: Release +name: Release Integration on: - workflow_dispatch: + workflow_call: inputs: - release-pr: - description: a release PR number to rerun release jobs on + release: + required: true type: string - push: - branches: - - main - - latest - - release/v* + releases: + required: true + type: string + +jobs: + check-registry: + name: Check Registry + if: github.repository_owner == 'npm' + runs-on: ubuntu-latest + defaults: + run: + shell: bash + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup + uses: ./.github/actions/setup + with: + deps: false + + - name: View in Registry + run: | + EXIT_CODE=0 + + function is_published { + if npm view "$@" --loglevel=error > /dev/null; then + echo 0 + else + echo 1 + fi + } + + for release in $(echo '\${{ needs.release.outputs.releases }}' | jq -r '.[] | @base64'); do + name=$(echo "$release" | base64 --decode | jq -r .pkgName) + version=$(echo "$release" | base64 --decode | jq -r .version) + spec="$name@$version" + status=$(is_published "$spec") + if [[ "$status" -eq 1 ]]; then + echo "$spec ERROR" + EXIT_CODE=$status + else + echo "$spec OK" + fi + done + + exit $EXIT_CODE + +.github/workflows/release.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Release + +on: + workflow_dispatch: + inputs: + release-pr: + description: a release PR number to rerun release jobs on + type: string + push: + branches: + branches: + - main + - latest + - release/v* permissions: contents: write @@ -775,150 +1099,95 @@ permissions: jobs: release: - outputs: - pr: \${{ steps.release.outputs.pr }} - release: \${{ steps.release.outputs.release }} - releases: \${{ steps.release.outputs.releases }} - branch: \${{ steps.release.outputs.pr-branch }} - pr-number: \${{ steps.release.outputs.pr-number }} - comment-id: \${{ steps.pr-comment.outputs.result }} - check-id: \${{ steps.check.outputs.check_id }} name: Release if: github.repository_owner == 'npm' runs-on: ubuntu-latest defaults: run: shell: bash + outputs: + pr: \${{ steps.release.outputs.pr }} + release: \${{ steps.release.outputs.release }} + releases: \${{ steps.release.outputs.releases }} + pr-branch: \${{ steps.release.outputs.pr-branch }} + pr-number: \${{ steps.release.outputs.pr-number }} + comment-id: \${{ steps.pr-comment.outputs.comment-id }} + check-id: \${{ steps.check.outputs.check-id }} steps: - name: Checkout uses: actions/checkout@v3 - - name: Setup Git User - run: | - git config --global user.email "npm-cli+bot@github.com" - git config --global user.name "npm CLI robot" - - name: Setup Node - uses: actions/setup-node@v3 - with: - node-version: 18.x - - name: Install npm@latest - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - name: npm Version - run: npm -v - - name: Install Dependencies - run: npm i --ignore-scripts --no-audit --no-fund + + - name: Setup + uses: ./.github/actions/setup + - name: Release Please id: release env: GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }} run: | npx --offline template-oss-release-please "\${{ github.ref_name }}" "\${{ inputs.release-pr }}" - - name: Post Pull Request Comment - if: steps.release.outputs.pr-number + + # If we have opened a release PR, then immediately create an "in_progress" + # check for it so the GitHub UI doesn't report that its mergeable. + # This check will be swapped out for real CI checks once those are started. + - name: Create Check + uses: ./.github/actions/create-check + if: steps.release.outputs.pr-sha + id: check + with: + sha: \${{ steps.release.outputs.pr-sha }} + token: \${{ secrets.GITHUB_TOKEN }} + job-name: Release + + - name: Comment Text uses: actions/github-script@v6 - id: pr-comment + if: steps.release.outputs.pr-number + id: comment-text env: PR_NUMBER: \${{ steps.release.outputs.pr-number }} REF_NAME: \${{ github.ref_name }} with: + result-encoding: string script: | - const { REF_NAME, PR_NUMBER: issue_number } = process.env const { runId, repo: { owner, repo } } = context - const { data: workflow } = await github.rest.actions.getWorkflowRun({ owner, repo, run_id: runId }) - let body = '## Release Manager/n/n' - - const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number }) - let commentId = comments.find(c => c.user.login === 'github-actions[bot]' && c.body.startsWith(body))?.id - body += \`Release workflow run: \${workflow.html_url}/n/n#### Force CI to Update This Release/n/n\` body += \`This PR will be updated and CI will run for every non-/\`chore:/\` commit that is pushed to /\`main/\`. \` body += \`To force CI to update this PR, run this command:/n/n\` - body += \`/\`/\`/\`/ngh workflow run release.yml -r \${REF_NAME} -R \${owner}/\${repo} -f release-pr=\${issue_number}/n/\`/\`/\`\` - - if (commentId) { - await github.rest.issues.updateComment({ owner, repo, comment_id: commentId, body }) - } else { - const { data: comment } = await github.rest.issues.createComment({ owner, repo, issue_number, body }) - commentId = comment?.id - } - - return commentId - - name: Get Workflow Job - uses: actions/github-script@v6 - if: steps.release.outputs.pr-sha - id: check-output - env: - JOB_NAME: "Release" - MATRIX_NAME: "" - with: - script: | - const { owner, repo } = context.repo - - const { data } = await github.rest.actions.listJobsForWorkflowRun({ - owner, - repo, - run_id: context.runId, - per_page: 100 - }) - - const jobName = process.env.JOB_NAME + process.env.MATRIX_NAME - const job = data.jobs.find(j => j.name.endsWith(jobName)) - const jobUrl = job?.html_url - - const shaUrl = \`\${context.serverUrl}/\${owner}/\${repo}/commit/\${{ steps.release.outputs.pr-sha }}\` + body += \`/\`/\`/\`/ngh workflow run release.yml -r \${process.env.REF_NAME} -R \${owner}/\${repo} -f release-pr=\${process.env.PR_NUMBER}/n/\`/\`/\`\` + return body - let summary = \`This check is assosciated with \${shaUrl}/n/n\` - - if (jobUrl) { - summary += \`For run logs, click here: \${jobUrl}\` - } else { - summary += \`Run logs could not be found for a job with name: "\${jobName}"\` - } - - return { summary } - - name: Create Check - uses: LouisBrunner/checks-action@v1.3.1 - id: check - if: steps.release.outputs.pr-sha + - name: Post Pull Request Comment + if: steps.comment-text.outputs.result + uses: ./.github/actions/upsert-comment + id: pr-comment with: token: \${{ secrets.GITHUB_TOKEN }} - status: in_progress - name: Release - sha: \${{ steps.release.outputs.pr-sha }} - output: \${{ steps.check-output.outputs.result }} + body: \${{ steps.comment-text.outputs.result }} + number: \${{ steps.release.outputs.pr-number }} update: - needs: release - outputs: - sha: \${{ steps.commit.outputs.sha }} - check-id: \${{ steps.check.outputs.check_id }} - name: Update - Release - if: github.repository_owner == 'npm' && needs.release.outputs.pr + name: Release PR - Update runs-on: ubuntu-latest defaults: run: shell: bash + if: needs.release.outputs.pr + needs: release + outputs: + sha: \${{ steps.commit.outputs.sha }} + check-id: \${{ steps.check.outputs.check-id }} steps: - name: Checkout uses: actions/checkout@v3 with: + ref: \${{ needs.release.outputs.pr-branch }} fetch-depth: 0 - ref: \${{ needs.release.outputs.branch }} - - name: Setup Git User - run: | - git config --global user.email "npm-cli+bot@github.com" - git config --global user.name "npm CLI robot" - - name: Setup Node - uses: actions/setup-node@v3 - with: - node-version: 18.x - - name: Install npm@latest - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - name: npm Version - run: npm -v - - name: Install Dependencies - run: npm i --ignore-scripts --no-audit --no-fund + + - name: Setup + uses: ./.github/actions/setup + - name: Run Post Pull Request Actions env: RELEASE_PR_NUMBER: \${{ needs.release.outputs.pr-number }} @@ -926,7 +1195,8 @@ jobs: GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }} run: | npm exec --offline -- template-oss-release-manager --lockfile=false - npm run rp-pull-request --ignore-scripts --if-present + npm run rp-pull-request --ignore-scripts -ws -iwr --if-present + - name: Commit id: commit env: @@ -935,79 +1205,45 @@ jobs: git commit --all --amend --no-edit || true git push --force-with-lease echo "sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT - - name: Get Workflow Job - uses: actions/github-script@v6 - if: steps.commit.outputs.sha - id: check-output - env: - JOB_NAME: "Update - Release" - MATRIX_NAME: "" - with: - script: | - const { owner, repo } = context.repo - - const { data } = await github.rest.actions.listJobsForWorkflowRun({ - owner, - repo, - run_id: context.runId, - per_page: 100 - }) - - const jobName = process.env.JOB_NAME + process.env.MATRIX_NAME - const job = data.jobs.find(j => j.name.endsWith(jobName)) - const jobUrl = job?.html_url - - const shaUrl = \`\${context.serverUrl}/\${owner}/\${repo}/commit/\${{ steps.commit.outputs.sha }}\` - - let summary = \`This check is assosciated with \${shaUrl}/n/n\` - - if (jobUrl) { - summary += \`For run logs, click here: \${jobUrl}\` - } else { - summary += \`Run logs could not be found for a job with name: "\${jobName}"\` - } - return { summary } - name: Create Check - uses: LouisBrunner/checks-action@v1.3.1 - id: check + uses: ./.github/actions/create-check if: steps.commit.outputs.sha + id: check with: + sha: \${{ steps.vommit.outputs.sha }} token: \${{ secrets.GITHUB_TOKEN }} - status: in_progress - name: Release - sha: \${{ steps.commit.outputs.sha }} - output: \${{ steps.check-output.outputs.result }} + job-name: Release + - name: Conclude Check - uses: LouisBrunner/checks-action@v1.3.1 - if: needs.release.outputs.check-id && always() + uses: ./.github/actions/conclude-check + if: needs.release.outputs.check-id && (success() || failure()) with: token: \${{ secrets.GITHUB_TOKEN }} conclusion: \${{ job.status }} - check_id: \${{ needs.release.outputs.check-id }} + check-id: \${{ needs.release.outputs.check-id }} ci: - name: CI - Release + name: Release PR - CI needs: [ release, update ] if: needs.release.outputs.pr - uses: ./.github/workflows/ci-release.yml + uses: ./.github/workflows/ci.yml with: - ref: \${{ needs.release.outputs.branch }} + ref: \${{ needs.release.outputs.pr-branch }} check-sha: \${{ needs.update.outputs.sha }} post-ci: - needs: [ release, update, ci ] - name: Post CI - Release - if: github.repository_owner == 'npm' && needs.release.outputs.pr && always() + name: Relase PR - Post CI runs-on: ubuntu-latest defaults: run: shell: bash + needs: [ release, update, ci ] + if: needs.release.outputs.pr && (success() || failure()) steps: - name: Get Needs Result id: needs-result run: | - result="" if [[ "\${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then result="failure" elif [[ "\${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then @@ -1016,108 +1252,76 @@ jobs: result="success" fi echo "result=$result" >> $GITHUB_OUTPUT + - name: Conclude Check - uses: LouisBrunner/checks-action@v1.3.1 - if: needs.update.outputs.check-id && always() + uses: ./.github/actions/conclude-check + if: needs.update.outputs.check-id && (success() || failure()) with: token: \${{ secrets.GITHUB_TOKEN }} conclusion: \${{ steps.needs-result.outputs.result }} - check_id: \${{ needs.update.outputs.check-id }} + check-id: \${{ needs.update.outputs.check-id }} post-release: - needs: release - name: Post Release - Release - if: github.repository_owner == 'npm' && needs.release.outputs.releases + name: Post Release runs-on: ubuntu-latest defaults: run: shell: bash + needs: release + if: needs.release.outputs.releases steps: - - name: Create Release PR Comment + - name: Comment Text uses: actions/github-script@v6 + id: comment-text env: RELEASES: \${{ needs.release.outputs.releases }} with: + result-encoding: string script: | const releases = JSON.parse(process.env.RELEASES) const { runId, repo: { owner, repo } } = context const issue_number = releases[0].prNumber + const releasePleaseComments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number }) + .then((comments) => comments.filter(c => c.login === 'github-actions[bot]' && c.body.includes('Release is at'))) + + for (const comment of releasePleaseComments) { + await github.rest.issues.deleteComment({ owner, repo, comment_id: comment.id }) + } + let body = '## Release Workflow/n/n' for (const { pkgName, version, url } of releases) { body += \`- /\`\${pkgName}@\${version}/\` \${url}/n\` } + body += \`- Workflow run: :arrows_counterclockwise: https://github.com/\${owner}/\${repo}/actions/runs/\${runId}\` + return body - const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number }) - .then(cs => cs.map(c => ({ id: c.id, login: c.user.login, body: c.body }))) - console.log(\`Found comments: \${JSON.stringify(comments, null, 2)}\`) - const releaseComments = comments.filter(c => c.login === 'github-actions[bot]' && c.body.includes('Release is at')) - - for (const comment of releaseComments) { - console.log(\`Release comment: \${JSON.stringify(comment, null, 2)}\`) - await github.rest.issues.deleteComment({ owner, repo, comment_id: comment.id }) - } - - const runUrl = \`https://github.com/\${owner}/\${repo}/actions/runs/\${runId}\` - await github.rest.issues.createComment({ - owner, - repo, - issue_number, - body: \`\${body}- Workflow run: :arrows_counterclockwise: \${runUrl}\`, - }) + - name: Create Release PR Comment + if: steps.comment-text.outputs.result + uses: ./.github/actions/upsert-comment + with: + token: \${{ secrets.GITHUB_TOKEN }} + body: \${{ steps.comment-text.outputs.result }} + number: \${{ fromJson(needs.release.outputs.release).prNumber }} + includes: \${{ github.run_id }} release-integration: + name: Post Release - Integration needs: release - name: Release Integration if: needs.release.outputs.release - runs-on: ubuntu-latest - defaults: - run: - shell: bash - steps: - - name: Setup Node - uses: actions/setup-node@v3 - with: - node-version: 18.x - - name: Install npm@latest - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - name: npm Version - run: npm -v - - name: View in Registry - run: | - EXIT_CODE=0 - - function is_published { - if npm view "$@" --loglevel=error > /dev/null; then - echo 0 - else - echo 1 - fi - } - - for release in $(echo '\${{ needs.release.outputs.releases }}' | jq -r '.[] | @base64'); do - name=$(echo "$release" | base64 --decode | jq -r .pkgName) - version=$(echo "$release" | base64 --decode | jq -r .version) - spec="$name@$version" - status=$(is_published "$spec") - if [[ "$status" -eq 1 ]]; then - echo "$spec ERROR" - EXIT_CODE=$status - else - echo "$spec OK" - fi - done - - exit $EXIT_CODE + uses: ./.github/workflows/release-integration.yml + with: + release: needs.release.outputs.release + releases: needs.release.outputs.releases post-release-integration: - needs: [ release, release-integration ] - name: Post Release Integration - Release - if: github.repository_owner == 'npm' && needs.release.outputs.release && always() + name: Post Release - Post Integration runs-on: ubuntu-latest defaults: run: shell: bash + needs: [ release, release-integration ] + if: needs.release.outputs.release && (success() || failure()) steps: - name: Get Needs Result id: needs-result @@ -1130,43 +1334,41 @@ jobs: result="white_check_mark" fi echo "result=$result" >> $GITHUB_OUTPUT - - name: Update Release PR Comment + + - name: Comment Text uses: actions/github-script@v6 + id: comment-text env: PR_NUMBER: \${{ fromJSON(needs.release.outputs.release).prNumber }} + REF_NAME: \${{ github.ref_name }} RESULT: \${{ steps.needs-result.outputs.result }} with: script: | - const { PR_NUMBER: issue_number, RESULT } = process.env - const { runId, repo: { owner, repo } } = context - - const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number }) - const updateComment = comments.find(c => - c.user.login === 'github-actions[bot]' && - c.body.startsWith('## Release Workflow/n/n') && - c.body.includes(runId) - ) - - if (updateComment) { - console.log('Found comment to update:', JSON.stringify(updateComment, null, 2)) - let body = updateComment.body.replace(/Workflow run: :[a-z_]+:/, \`Workflow run: :\${RESULT}:\`) - const tagCodeowner = RESULT !== 'white_check_mark' - if (tagCodeowner) { - body += \`/n/n:rotating_light:\` - body += \` @npm/cli-team: The post-release workflow failed for this release.\` - body += \` Manual steps may need to be taken after examining the workflow output\` - body += \` from the above workflow run. :rotating_light:\` - } - await github.rest.issues.updateComment({ - owner, - repo, - body, - comment_id: updateComment.id, - }) - } else { - console.log('No matching comments found:', JSON.stringify(comments, null, 2)) + const { RESULT, PR_NUMBER, REF_NAME } = process.env + const tagCodeowner = RESULT !== 'white_check_mark' + if (tagCodeowner) { + let body = '' + body += \`/n/n:rotating_light:\` + body += \` @npm/cli-team: The post-release workflow failed for this release.\` + body += \` Manual steps may need to be taken after examining the workflow output\` + body += \` from the above workflow run. :rotating_light:\` + body += \`/n/nTo rerun the workflow run the following command:/n/n\` + body += \`/\`/\`/\`/ngh workflow run release.yml -r \${REF_NAME} -R \${owner}/\${repo} -f release-pr=\${PR_NUMBER}/n/\`/\`/\`\` + return body } + - name: Update Release PR Comment + if: steps.comment-text.outputs.result + uses: ./.github/actions/upsert-comment + with: + token: \${{ secrets.GITHUB_TOKEN }} + body: "## Release Workflow" + find: "Workflow run: :[a-z_]+:" + replace: "Workflow run :\${{ steps.needs-result.outputs.result }}:" + append: \${{ steps.comment-text.outputs.result }} + number: \${{ fromJson(needs.release.outputs.release).prNumber }} + includes: \${{ github.run_id }} + .gitignore ======================================== # This file is automatically added by @npmcli/template-oss. Do not edit. @@ -1366,6 +1568,452 @@ module.exports = { ], } +.github/actions/audit/action.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Audit + +inputs: + shell: + description: shell to run on + default: bash + +runs: + using: composite + steps: + - name: Run Audit + shell: \${{ inputs.shell }} + run: | + npm audit --omit=dev + npm audit --audit-level=none + +.github/actions/changed-files/action.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Get Changed Files + +inputs: + token: + description: GitHub token to use + required: true + +outputs: + files: + value: \${{ steps.files.outputs.result }} + +runs: + using: composite + steps: + - name: Get Changed Files + uses: actions/github-script@v6 + id: files + with: + github-token: \${{ inputs.token }} + script: | + const { repo: { owner, repo }, eventName, payload, sha } = context + let files + if (eventName === 'pull_request' || eventName === 'pull_request_target') { + files = await github.paginate(github.rest.pulls.listFiles, { + owner, + repo, + pull_number: payload.pull_request.number, + }) + } else { + const { data: commit } = await github.rest.repos.getCommit({ + owner, + repo, + ref: sha, + }) + files = commit.files + } + return files.map(f => f.filename) + +.github/actions/changed-workspaces/action.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Get Changed Workspaces + +inputs: + token: + description: GitHub token to use + required: true + shell: + description: shell to run on + default: bash + all: + default: false + type: boolean + files: + description: json stringified array of file names + type: string + +outputs: + flags: + value: \${{ steps.workspaces.outputs.flags }} + +runs: + using: composite + steps: + - name: Get Changed Files + uses: ./.github/actions/changed-files + if: \${{ !inputs.all && !inputs.files }} + id: files + + - name: Get Workspaces + shell: \${{ inputs.shell }} + id: workspaces + run: | + flags=$(npm exec --offline -- template-oss-changed-workspaces '\${{ (inputs.all && '--all') || (inputs.files || steps.files.outputs.result) }}') + echo "flags=\${flags}" >> $GITHUB_OUTPUT + +.github/actions/conclude-check/action.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Conclude Check +description: Conclude a check + +inputs: + token: + description: GitHub token to use + required: true + conclusion: + description: conclusion of check + require: true + check-id: + description: id of check to conclude + required: true + +runs: + using: composite + steps: + - name: Conclude Check + uses: LouisBrunner/checks-action@v1.5.0 + with: + token: \${{ inputs.token }} + conclusion: \${{ inputs.conclusion }} + check_id: \${{ inputs.check-id }} + +.github/actions/create-check/action.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Create Check +description: Create a check and associate it with a sha + +inputs: + token: + description: GitHub token to use + required: true + sha: + description: sha to attach the check to + required: true + job-name: + description: Name of the job to find + required: true + job-status: + description: Status of the check being created + default: in_progress + +outputs: + check-id: + description: The ID of the check that was created + value: \${{ steps.check.outputs.check_id }} + +runs: + using: composite + steps: + - name: Get Workflow Job + uses: actions/github-script@v6 + id: workflow-job + env: + JOB_NAME: \${{ inputs.job-name }} + with: + github-token: \${{ inputs.token }} + script: | + const { JOB_NAME } = process.env + const { repo: { owner, repo }, runId, serverUrl } = context + + const jobs = await github.paginate(github.rest.actions.listJobsForWorkflowRun, { + owner, + repo, + run_id: runId, + }) + const job = jobs.find(j => j.name.endsWith(JOB_NAME)) + + const shaUrl = \`\${serverUrl}/\${owner}/\${repo}/commit/\${{ inputs.sha }}\` + const summary = \`This check is assosciated with \${shaUrl}/n/n\` + const message = job?.html_url + ? \`For run logs, click here: \${job.html_url}\` + : \`Run logs could not be found for a job with name: "\${JOB_NAME}"\` + + // Return a json object with properties that LouisBrunner/checks-actions + // expects as the output of the check + return { summary: summary + message } + + - name: Create Check + uses: LouisBrunner/checks-action@v1.5.0 + id: check + with: + token: \${{ inputs.token }} + status: \${{ inputs.job-status }} + name: \${{ inputs.job-name }} + sha: \${{ inputs.sha }} + output: \${{ steps.workflow-job.outputs.result }} + +.github/actions/deps/action.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Dependencies + +inputs: + command: + description: command to run for the dependencies step + default: install --ignore-scripts --no-audit --no-fund + flags: + description: extra flags to pass to the dependencies step + shell: + description: shell to run on + default: bash + +runs: + using: composite + steps: + - name: Install Dependencies + shell: \${{ inputs.shell }} + run: npm \${{ inputs.command }} \${{ inputs.flags }} + +.github/actions/lint/action.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Lint + +inputs: + flags: + description: flags to pass to the commands + shell: + description: shell to run on + default: bash + +runs: + using: composite + steps: + - name: Lint + shell: \${{ inputs.shell }} + run: | + npm run lint --ignore-scripts \${{ inputs.flags }} + npm run postlint --ignore-scripts \${{ inputs.flags }} + +.github/actions/setup/action.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Setup Repo +description: Setup a repo with standard tools + +inputs: + node-version: + description: node version to use + default: 18.x + npm-version: + description: npm version to use + default: latest + cache: + description: whether to cache npm install or not + type: boolean + default: false + shell: + description: shell to run on + default: bash + deps: + description: whether to run the deps step + type: boolean + default: true + deps-command: + description: command to run for the dependencies step + default: install --ignore-scripts --no-audit --no-fund + deps-flags: + description: extra flags to pass to the dependencies step + +runs: + using: composite + steps: + - name: Setup Git User + shell: \${{ inputs.shell }} + run: | + git config --global user.email "npm-cli+bot@github.com" + git config --global user.name "npm CLI robot" + + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: \${{ inputs.node-version }} + cache: \${{ (inputs.cache && 'npm') || null }} + + - name: Check Node Version + if: inputs.npm-version + id: node-version + shell: \${{ inputs.shell }} + run: | + NODE_VERSION=$(node --version) + echo $NODE_VERSION + if npx semver@7 -r "<=10" "$NODE_VERSION" --yes; then + echo "ten-or-lower=true" >> $GITHUB_OUTPUT + fi + if npx semver@7 -r "<=14" "$NODE_VERSION" --yes; then + echo "fourteen-or-lower=true" >> $GITHUB_OUTPUT + fi + + - name: Update Windows npm + # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows + if: inputs.npm-version && runner.os == 'Windows' && steps.node-version.outputs.fourteen-or-lower + shell: \${{ inputs.shell }} + run: | + curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz + tar xf npm-7.5.4.tgz + cd package + node lib/npm.js install --no-fund --no-audit -g ../npm-7.5.4.tgz + cd .. + rmdir /s /q package + + - name: Install npm@7 + if: inputs.npm-version && steps.node-version.outputs.ten-or-lower + shell: \${{ inputs.shell }} + run: npm i --prefer-online --no-fund --no-audit -g npm@7 + + - name: Install npm@\${{ inputs.npm-version }} + if: inputs.npm-version && !steps.node-version.outputs.ten-or-lower + shell: \${{ inputs.shell }} + run: npm i --prefer-online --no-fund --no-audit -g npm@\${{ inputs.npm-version }} + + - name: npm Version + shell: \${{ inputs.shell }} + run: npm -v + + - name: Setup Dependencies + if: inputs.deps + uses: ./.github/actions/deps + with: + command: \${{ inputs.deps-command }} + flags: \${{ inputs.deps-flags }} + + - name: Add Problem Matcher + shell: \${{ inputs.shell }} + run: | + [[ -f ./.github/matchers/tap.json ]] && echo "::add-matcher::.github/matchers/tap.json" + +.github/actions/test/action.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Test + +inputs: + flags: + description: flags to pass to the commands + shell: + description: shell to run on + default: bash + +runs: + using: composite + steps: + - name: Test + shell: \${{ inputs.shell }} + run: npm test --ignore-scripts \${{ inputs.flags }} + +.github/actions/upsert-comment/action.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Upsert Comment +description: Update or create a comment + +inputs: + token: + description: GitHub token to use + required: true + number: + description: Number of the issue or pull request + required: true + login: + description: Login name of user to look for comments from + default: github-actions[bot] + body: + description: Body of the comment, the first line will be used to match to an existing comment + find: + description: string to find in body + replace: + description: string to replace in body + append: + description: string to append to the body + includes: + description: A string that the comment needs to include + +outputs: + comment-id: + description: The ID of the comment + value: \${{ steps.comment.outputs.result }} + +runs: + using: composite + steps: + - name: Create or Update Comment + uses: actions/github-script@v6 + id: comment + env: + NUMBER: \${{ inputs.number }} + BODY: \${{ inputs.body }} + FIND: \${{ inputs.find }} + REPLACE: \${{ inputs.replace }} + APPEND: \${{ inputs.append }} + LOGIN: \${{ inputs.login }} + INCLUDES: \${{ inputs.includes }} + with: + github-token: \${{ inputs.token }} + script: | + const { BODY, FIND, REPLACE, APPEND, LOGIN, NUMBER: issue_number, INCLUDES } = process.env + const { repo: { owner, repo } } = context + const TITLE = BODY.split('/n')[0].trim() + '/n' + const bodyIncludes = (c) => INCLUDES ? c.body.includes(INCLUDES) : true + + const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number }) + .then(comments => comments.map(c => ({ id: c.id, login: c.user.login, body: c.body }))) + + console.log(\`Found comments: \${JSON.stringify(comments, null, 2)}\`) + console.log(\`Looking for comment with: \${JSON.stringify({ LOGIN, TITLE, INCLUDES }, null, 2)}\`) + + const comment = comments.find(c => + c.login === LOGIN && + c.body.startsWith(TITLE) && + bodyIncludes(c) + ) + + if (comment) { + console.log(\`Found comment: \${JSON.stringify(comment, null, 2)}\`) + let newBody = FIND && REPLACE ? comment.body.replace(new RegExp(FIND, 'g'), REPLACE) : BODY + if (APPEND) { + newBody += APPEND + } + await github.rest.issues.updateComment({ owner, repo, comment_id: comment.id, body: newBody }) + return comment.id + } + + if (FIND || REPLACE || APPEND) { + console.log('Could not find a comment to use find/replace or append to') + return + } + + console.log('Creating new comment') + + const res = await github.rest.issues.createComment({ owner, repo, issue_number, body: BODY }) + return res.data.id + .github/CODEOWNERS ======================================== # This file is automatically added by @npmcli/template-oss. Do not edit. @@ -1523,8 +2171,8 @@ name: Audit on: workflow_dispatch: schedule: - # "At 08:00 UTC (01:00 PT) on Monday" https://crontab.guru/#0_8_*_*_1 - - cron: "0 8 * * 1" + # "At 09:00 UTC (01:00 PT) on Monday" https://crontab.guru/#0_9_*_*_1 + - cron: "0 9 * * 1" jobs: audit: @@ -1537,258 +2185,20 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 - - name: Setup Git User - run: | - git config --global user.email "npm-cli+bot@github.com" - git config --global user.name "npm CLI robot" - - name: Setup Node - uses: actions/setup-node@v3 + + - name: Setup + uses: ./.github/actions/setup with: - node-version: 18.x - - name: Install npm@latest - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - name: npm Version - run: npm -v - - name: Install Dependencies - run: npm i --ignore-scripts --no-audit --no-fund --package-lock - - name: Run Production Audit - run: npm audit --omit=dev - - name: Run Full Audit - run: npm audit --audit-level=none - -.github/workflows/ci-a.yml -======================================== -# This file is automatically added by @npmcli/template-oss. Do not edit. - -name: CI - a - -on: - workflow_dispatch: - pull_request: - paths: - - workspaces/a/** - push: - branches: - - main - - latest - paths: - - workspaces/a/** - schedule: - # "At 09:00 UTC (02:00 PT) on Monday" https://crontab.guru/#0_9_*_*_1 - - cron: "0 9 * * 1" - -jobs: - lint: - name: Lint - if: github.repository_owner == 'npm' - runs-on: ubuntu-latest - defaults: - run: - shell: bash - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Setup Git User - run: | - git config --global user.email "npm-cli+bot@github.com" - git config --global user.name "npm CLI robot" - - name: Setup Node - uses: actions/setup-node@v3 - with: - node-version: 18.x - - name: Install npm@latest - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - name: npm Version - run: npm -v - - name: Install Dependencies - run: npm i --ignore-scripts --no-audit --no-fund - - name: Lint - run: npm run lint --ignore-scripts -w a - - name: Post Lint - run: npm run postlint --ignore-scripts -w a - - test: - name: Test - \${{ matrix.platform.name }} - \${{ matrix.node-version }} - if: github.repository_owner == 'npm' - strategy: - fail-fast: false - matrix: - platform: - - name: Linux - os: ubuntu-latest - shell: bash - - name: macOS - os: macos-latest - shell: bash - - name: Windows - os: windows-latest - shell: cmd - node-version: - - 14.17.0 - - 14.x - - 16.13.0 - - 16.x - - 18.0.0 - - 18.x - runs-on: \${{ matrix.platform.os }} - defaults: - run: - shell: \${{ matrix.platform.shell }} - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Setup Git User - run: | - git config --global user.email "npm-cli+bot@github.com" - git config --global user.name "npm CLI robot" - - name: Setup Node - uses: actions/setup-node@v3 - with: - node-version: \${{ matrix.node-version }} - - name: Update Windows npm - # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows - if: matrix.platform.os == 'windows-latest' && (startsWith(matrix.node-version, '12.') || startsWith(matrix.node-version, '14.')) - run: | - curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz - tar xf npm-7.5.4.tgz - cd package - node lib/npm.js install --no-fund --no-audit -g ../npm-7.5.4.tgz - cd .. - rmdir /s /q package - - name: Install npm@7 - if: startsWith(matrix.node-version, '10.') - run: npm i --prefer-online --no-fund --no-audit -g npm@7 - - name: Install npm@latest - if: \${{ !startsWith(matrix.node-version, '10.') }} - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - name: npm Version - run: npm -v - - name: Install Dependencies - run: npm i --ignore-scripts --no-audit --no-fund - - name: Add Problem Matcher - run: echo "::add-matcher::.github/matchers/tap.json" - - name: Test - run: npm test --ignore-scripts -w a + deps-flags: "--package-lock" -.github/workflows/ci-b.yml -======================================== -# This file is automatically added by @npmcli/template-oss. Do not edit. - -name: CI - b + - name: Audit + uses: ./.github/actions/audit -on: - workflow_dispatch: - pull_request: - paths: - - workspaces/b/** - push: - branches: - - main - - latest - paths: - - workspaces/b/** - schedule: - # "At 09:00 UTC (02:00 PT) on Monday" https://crontab.guru/#0_9_*_*_1 - - cron: "0 9 * * 1" - -jobs: - lint: - name: Lint - if: github.repository_owner == 'npm' - runs-on: ubuntu-latest - defaults: - run: - shell: bash - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Setup Git User - run: | - git config --global user.email "npm-cli+bot@github.com" - git config --global user.name "npm CLI robot" - - name: Setup Node - uses: actions/setup-node@v3 - with: - node-version: 18.x - - name: Install npm@latest - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - name: npm Version - run: npm -v - - name: Install Dependencies - run: npm i --ignore-scripts --no-audit --no-fund - - name: Lint - run: npm run lint --ignore-scripts -w b - - name: Post Lint - run: npm run postlint --ignore-scripts -w b - - test: - name: Test - \${{ matrix.platform.name }} - \${{ matrix.node-version }} - if: github.repository_owner == 'npm' - strategy: - fail-fast: false - matrix: - platform: - - name: Linux - os: ubuntu-latest - shell: bash - - name: macOS - os: macos-latest - shell: bash - - name: Windows - os: windows-latest - shell: cmd - node-version: - - 14.17.0 - - 14.x - - 16.13.0 - - 16.x - - 18.0.0 - - 18.x - runs-on: \${{ matrix.platform.os }} - defaults: - run: - shell: \${{ matrix.platform.shell }} - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Setup Git User - run: | - git config --global user.email "npm-cli+bot@github.com" - git config --global user.name "npm CLI robot" - - name: Setup Node - uses: actions/setup-node@v3 - with: - node-version: \${{ matrix.node-version }} - - name: Update Windows npm - # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows - if: matrix.platform.os == 'windows-latest' && (startsWith(matrix.node-version, '12.') || startsWith(matrix.node-version, '14.')) - run: | - curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz - tar xf npm-7.5.4.tgz - cd package - node lib/npm.js install --no-fund --no-audit -g ../npm-7.5.4.tgz - cd .. - rmdir /s /q package - - name: Install npm@7 - if: startsWith(matrix.node-version, '10.') - run: npm i --prefer-online --no-fund --no-audit -g npm@7 - - name: Install npm@latest - if: \${{ !startsWith(matrix.node-version, '10.') }} - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - name: npm Version - run: npm -v - - name: Install Dependencies - run: npm i --ignore-scripts --no-audit --no-fund - - name: Add Problem Matcher - run: echo "::add-matcher::.github/matchers/tap.json" - - name: Test - run: npm test --ignore-scripts -w b - -.github/workflows/ci-release.yml +.github/workflows/ci.yml ======================================== # This file is automatically added by @npmcli/template-oss. Do not edit. -name: CI - Release +name: CI on: workflow_dispatch: @@ -1796,7 +2206,11 @@ on: ref: required: true type: string - default: main + check-sha: + type: string + all: + default: true + type: boolean workflow_call: inputs: ref: @@ -1805,92 +2219,75 @@ on: check-sha: required: true type: string + all: + default: true + type: boolean + pull_request: + push: + branches: + - main + - latest + schedule: + # "At 10:00 UTC (02:00 PT) on Monday" https://crontab.guru/#0_10_*_*_1 + - cron: "0 10 * * 1" jobs: - lint-all: - name: Lint All + lint: + name: Lint if: github.repository_owner == 'npm' runs-on: ubuntu-latest defaults: run: shell: bash steps: - - name: Get Workflow Job - uses: actions/github-script@v6 - if: inputs.check-sha - id: check-output - env: - JOB_NAME: "Lint All" - MATRIX_NAME: "" + - name: Checkout + uses: actions/checkout@v3 with: - script: | - const { owner, repo } = context.repo - - const { data } = await github.rest.actions.listJobsForWorkflowRun({ - owner, - repo, - run_id: context.runId, - per_page: 100 - }) - - const jobName = process.env.JOB_NAME + process.env.MATRIX_NAME - const job = data.jobs.find(j => j.name.endsWith(jobName)) - const jobUrl = job?.html_url - - const shaUrl = \`\${context.serverUrl}/\${owner}/\${repo}/commit/\${{ inputs.check-sha }}\` - - let summary = \`This check is assosciated with \${shaUrl}/n/n\` - - if (jobUrl) { - summary += \`For run logs, click here: \${jobUrl}\` - } else { - summary += \`Run logs could not be found for a job with name: "\${jobName}"\` - } + ref: \${{ inputs.ref }} - return { summary } - name: Create Check - uses: LouisBrunner/checks-action@v1.3.1 - id: check + uses: ./.github/actions/create-check if: inputs.check-sha + id: check with: - token: \${{ secrets.GITHUB_TOKEN }} - status: in_progress - name: Lint All sha: \${{ inputs.check-sha }} - output: \${{ steps.check-output.outputs.result }} - - name: Checkout - uses: actions/checkout@v3 - with: - ref: \${{ inputs.ref }} - - name: Setup Git User - run: | - git config --global user.email "npm-cli+bot@github.com" - git config --global user.name "npm CLI robot" - - name: Setup Node - uses: actions/setup-node@v3 + token: \${{ secrets.GITHUB_TOKEN }} + job-name: Lint + + - name: Setup + id: setup + continue-on-error: \${{ !!steps.check.outputs.check-id }} + uses: ./.github/actions/setup + + - name: Get Changed Workspaces + id: workspaces + continue-on-error: \${{ !!steps.check.outputs.check-id }} + uses: ./.github/actions/changed-workspaces with: - node-version: 18.x - - name: Install npm@latest - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - name: npm Version - run: npm -v - - name: Install Dependencies - run: npm i --ignore-scripts --no-audit --no-fund + token: \${{ secrets.GITHUB_TOKEN }} + all: \${{ inputs.all }} + - name: Lint - run: npm run lint --ignore-scripts -ws -iwr --if-present - - name: Post Lint - run: npm run postlint --ignore-scripts -ws -iwr --if-present + uses: ./.github/actions/lint + continue-on-error: \${{ !!steps.check.outputs.check-id }} + with: + flags: \${{ steps.workspaces.outputs.flags }} + - name: Conclude Check - uses: LouisBrunner/checks-action@v1.3.1 - if: steps.check.outputs.check_id && always() + uses: ./.github/actions/conclude-check + if: steps.check.outputs.check-id && (success() || failure()) with: token: \${{ secrets.GITHUB_TOKEN }} conclusion: \${{ job.status }} - check_id: \${{ steps.check.outputs.check_id }} + check-id: \${{ steps.check.outputs.check-id }} - test-all: - name: Test All - \${{ matrix.platform.name }} - \${{ matrix.node-version }} + test: + name: Test - \${{ matrix.platform.name }} - \${{ matrix.node-version }} if: github.repository_owner == 'npm' + runs-on: \${{ matrix.platform.os }} + defaults: + run: + shell: \${{ matrix.platform.shell }} strategy: fail-fast: false matrix: @@ -1911,213 +2308,63 @@ jobs: - 16.x - 18.0.0 - 18.x - runs-on: \${{ matrix.platform.os }} - defaults: - run: - shell: \${{ matrix.platform.shell }} steps: - - name: Get Workflow Job - uses: actions/github-script@v6 - if: inputs.check-sha - id: check-output - env: - JOB_NAME: "Test All" - MATRIX_NAME: " - \${{ matrix.platform.name }} - \${{ matrix.node-version }}" - with: - script: | - const { owner, repo } = context.repo - - const { data } = await github.rest.actions.listJobsForWorkflowRun({ - owner, - repo, - run_id: context.runId, - per_page: 100 - }) - - const jobName = process.env.JOB_NAME + process.env.MATRIX_NAME - const job = data.jobs.find(j => j.name.endsWith(jobName)) - const jobUrl = job?.html_url - - const shaUrl = \`\${context.serverUrl}/\${owner}/\${repo}/commit/\${{ inputs.check-sha }}\` - - let summary = \`This check is assosciated with \${shaUrl}/n/n\` + - name: Continue Matrix Run + id: continue-matrix + run: | + if [[ "\${{ matrix.node-version }}" == "14.17.0" || "\${{ inputs.all }}" == "true" ]]; then + echo "result=true" >> $GITHUB_OUTPUT + fi - if (jobUrl) { - summary += \`For run logs, click here: \${jobUrl}\` - } else { - summary += \`Run logs could not be found for a job with name: "\${jobName}"\` - } + - name: Checkout + if: steps.continue-matrix.outputs.result + uses: actions/checkout@v3 + with: + ref: \${{ inputs.ref }} - return { summary } - name: Create Check - uses: LouisBrunner/checks-action@v1.3.1 + if: steps.continue-matrix.outputs.result && inputs.check-sha + uses: ./.github/actions/create-check id: check - if: inputs.check-sha with: - token: \${{ secrets.GITHUB_TOKEN }} - status: in_progress - name: Test All - \${{ matrix.platform.name }} - \${{ matrix.node-version }} sha: \${{ inputs.check-sha }} - output: \${{ steps.check-output.outputs.result }} - - name: Checkout - uses: actions/checkout@v3 - with: - ref: \${{ inputs.ref }} - - name: Setup Git User - run: | - git config --global user.email "npm-cli+bot@github.com" - git config --global user.name "npm CLI robot" - - name: Setup Node - uses: actions/setup-node@v3 + token: \${{ secrets.GITHUB_TOKEN }} + job-name: "Test - \${{ matrix.platform.name }} - \${{ matrix.node-version }}" + + - name: Setup + if: steps.continue-matrix.outputs.result + uses: ./.github/actions/setup + id: setup + continue-on-error: \${{ !!steps.check.outputs.check-id }} with: node-version: \${{ matrix.node-version }} - - name: Update Windows npm - # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows - if: matrix.platform.os == 'windows-latest' && (startsWith(matrix.node-version, '12.') || startsWith(matrix.node-version, '14.')) - run: | - curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz - tar xf npm-7.5.4.tgz - cd package - node lib/npm.js install --no-fund --no-audit -g ../npm-7.5.4.tgz - cd .. - rmdir /s /q package - - name: Install npm@7 - if: startsWith(matrix.node-version, '10.') - run: npm i --prefer-online --no-fund --no-audit -g npm@7 - - name: Install npm@latest - if: \${{ !startsWith(matrix.node-version, '10.') }} - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - name: npm Version - run: npm -v - - name: Install Dependencies - run: npm i --ignore-scripts --no-audit --no-fund - - name: Add Problem Matcher - run: echo "::add-matcher::.github/matchers/tap.json" - - name: Test - run: npm test --ignore-scripts -ws -iwr --if-present - - name: Conclude Check - uses: LouisBrunner/checks-action@v1.3.1 - if: steps.check.outputs.check_id && always() + shell: \${{ matrix.platform.shell }} + + - name: Get Changed Workspaces + if: steps.continue-matrix.outputs.result + id: workspaces + continue-on-error: \${{ !!steps.check.outputs.check-id }} + uses: ./.github/actions/changed-workspaces with: + shell: \${{ matrix.platform.shell }} token: \${{ secrets.GITHUB_TOKEN }} - conclusion: \${{ job.status }} - check_id: \${{ steps.check.outputs.check_id }} - -.github/workflows/ci.yml -======================================== -# This file is automatically added by @npmcli/template-oss. Do not edit. - -name: CI - -on: - workflow_dispatch: - pull_request: - paths-ignore: - - workspaces/a/** - - workspaces/b/** - push: - branches: - - main - - latest - paths-ignore: - - workspaces/a/** - - workspaces/b/** - schedule: - # "At 09:00 UTC (02:00 PT) on Monday" https://crontab.guru/#0_9_*_*_1 - - cron: "0 9 * * 1" + all: \${{ inputs.all }} -jobs: - lint: - name: Lint - if: github.repository_owner == 'npm' - runs-on: ubuntu-latest - defaults: - run: - shell: bash - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Setup Git User - run: | - git config --global user.email "npm-cli+bot@github.com" - git config --global user.name "npm CLI robot" - - name: Setup Node - uses: actions/setup-node@v3 + - name: Test + if: steps.continue-matrix.outputs.result + uses: ./.github/actions/test + continue-on-error: \${{ !!steps.check.outputs.check-id }} with: - node-version: 18.x - - name: Install npm@latest - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - name: npm Version - run: npm -v - - name: Install Dependencies - run: npm i --ignore-scripts --no-audit --no-fund - - name: Lint - run: npm run lint --ignore-scripts - - name: Post Lint - run: npm run postlint --ignore-scripts + flags: \${{ steps.workspaces.outputs.flags }} + shell: \${{ matrix.platform.shell }} - test: - name: Test - \${{ matrix.platform.name }} - \${{ matrix.node-version }} - if: github.repository_owner == 'npm' - strategy: - fail-fast: false - matrix: - platform: - - name: Linux - os: ubuntu-latest - shell: bash - - name: macOS - os: macos-latest - shell: bash - - name: Windows - os: windows-latest - shell: cmd - node-version: - - 14.17.0 - - 14.x - - 16.13.0 - - 16.x - - 18.0.0 - - 18.x - runs-on: \${{ matrix.platform.os }} - defaults: - run: - shell: \${{ matrix.platform.shell }} - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Setup Git User - run: | - git config --global user.email "npm-cli+bot@github.com" - git config --global user.name "npm CLI robot" - - name: Setup Node - uses: actions/setup-node@v3 + - name: Conclude Check + uses: ./.github/actions/conclude-check + if: steps.continue-matrix.outputs.result && steps.check.outputs.check-id && (success() || failure()) with: - node-version: \${{ matrix.node-version }} - - name: Update Windows npm - # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows - if: matrix.platform.os == 'windows-latest' && (startsWith(matrix.node-version, '12.') || startsWith(matrix.node-version, '14.')) - run: | - curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz - tar xf npm-7.5.4.tgz - cd package - node lib/npm.js install --no-fund --no-audit -g ../npm-7.5.4.tgz - cd .. - rmdir /s /q package - - name: Install npm@7 - if: startsWith(matrix.node-version, '10.') - run: npm i --prefer-online --no-fund --no-audit -g npm@7 - - name: Install npm@latest - if: \${{ !startsWith(matrix.node-version, '10.') }} - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - name: npm Version - run: npm -v - - name: Install Dependencies - run: npm i --ignore-scripts --no-audit --no-fund - - name: Add Problem Matcher - run: echo "::add-matcher::.github/matchers/tap.json" - - name: Test - run: npm test --ignore-scripts + token: \${{ secrets.GITHUB_TOKEN }} + conclusion: \${{ job.status }} + check-id: \${{ steps.check.outputs.check-id }} .github/workflows/codeql-analysis.yml ======================================== @@ -2135,13 +2382,17 @@ on: - main - latest schedule: - # "At 10:00 UTC (03:00 PT) on Monday" https://crontab.guru/#0_10_*_*_1 - - cron: "0 10 * * 1" + # "At 11:00 UTC (03:00 PT) on Monday" https://crontab.guru/#0_11_*_*_1 + - cron: "0 11 * * 1" jobs: analyze: name: Analyze + if: github.repository_owner == 'npm' runs-on: ubuntu-latest + defaults: + run: + shell: bash permissions: actions: read contents: read @@ -2149,14 +2400,12 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 - - name: Setup Git User - run: | - git config --global user.email "npm-cli+bot@github.com" - git config --global user.name "npm CLI robot" + - name: Initialize CodeQL uses: github/codeql-action/init@v2 with: languages: javascript + - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 @@ -2168,62 +2417,51 @@ name: Post Dependabot on: pull_request -permissions: - contents: write - jobs: template-oss: name: template-oss + permissions: + contents: write if: github.repository_owner == 'npm' && github.actor == 'dependabot[bot]' runs-on: ubuntu-latest defaults: run: shell: bash steps: - - name: Checkout - uses: actions/checkout@v3 - with: - ref: \${{ github.event.pull_request.head.ref }} - - name: Setup Git User - run: | - git config --global user.email "npm-cli+bot@github.com" - git config --global user.name "npm CLI robot" - - name: Setup Node - uses: actions/setup-node@v3 - with: - node-version: 18.x - - name: Install npm@latest - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - name: npm Version - run: npm -v - - name: Install Dependencies - run: npm i --ignore-scripts --no-audit --no-fund - name: Fetch Dependabot Metadata id: metadata uses: dependabot/fetch-metadata@v1 with: github-token: \${{ secrets.GITHUB_TOKEN }} - # Dependabot can update multiple directories so we output which directory - # it is acting on so we can run the command for the correct root or workspace - - name: Get Dependabot Directory + - name: Is Dependency if: contains(steps.metadata.outputs.dependency-names, '@npmcli/template-oss') - id: flags - run: | - dependabot_dir="\${{ steps.metadata.outputs.directory }}" - if [[ "$dependabot_dir" == "/" ]]; then - echo "workspace=-iwr" >> $GITHUB_OUTPUT - else - # strip leading slash from directory so it works as a - # a path to the workspace flag - echo "workspace=-w \${dependabot_dir#/}" >> $GITHUB_OUTPUT - fi + id: dependency + run: echo "continue=true" >> $GITHUB_OUTPUT + + - name: Checkout + if: steps.dependency.outputs.continue + uses: actions/checkout@v3 + with: + ref: \${{ github.event.pull_request.head.ref }} + + - name: Setup + if: steps.dependency.outputs.continue + uses: ./.github/actions/setup + + - name: Get Workspaces + if: steps.dependency.outputs.continue + uses: ./.github/actions/changed-workspaces + id: workspaces + with: + token: \${{ secrets.GITHUB_TOKEN }} + files: '["\${{ steps.metadata.outputs.directory }}"]' - name: Apply Changes - if: steps.flags.outputs.workspace + if: steps.workspaces.outputs.flags id: apply run: | - npm run template-oss-apply \${{ steps.flags.outputs.workspace }} + npm run template-oss-apply \${{ steps.workspaces.outputs.flags }} if [[ \`git status --porcelain\` ]]; then echo "changes=true" >> $GITHUB_OUTPUT fi @@ -2275,7 +2513,7 @@ jobs: - name: Check Changes if: steps.apply.outputs.changes run: | - npm exec --offline \${{ steps.flags.outputs.workspace }} -- template-oss-check + npm exec --offline \${{ steps.workspaces.outputs.flags }} -- template-oss-check - name: Fail on Breaking Change if: steps.apply.outputs.changes && startsWith(steps.apply.outputs.message, 'feat!') @@ -2300,7 +2538,7 @@ on: jobs: commitlint: - name: Lint Commits + name: Lint Commit if: github.repository_owner == 'npm' runs-on: ubuntu-latest defaults: @@ -2311,29 +2549,82 @@ jobs: uses: actions/checkout@v3 with: fetch-depth: 0 - - name: Setup Git User - run: | - git config --global user.email "npm-cli+bot@github.com" - git config --global user.name "npm CLI robot" - - name: Setup Node - uses: actions/setup-node@v3 - with: - node-version: 18.x - - name: Install npm@latest - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - name: npm Version - run: npm -v - - name: Install Dependencies - run: npm i --ignore-scripts --no-audit --no-fund + + - name: Setup + uses: ./.github/actions/setup + - name: Run Commitlint on Commits id: commit continue-on-error: true run: | - npx --offline commitlint -V --from 'origin/\${{ github.base_ref }}' --to \${{ github.event.pull_request.head.sha }} + npx --offline commitlint -V --from 'origin/\${{ github.base_ref }}' --to '\${{ github.event.pull_request.head.sha }}' + - name: Run Commitlint on PR Title if: steps.commit.outcome == 'failure' + env: + PR_TITLE: \${{ github.event.pull_request.title }} + run: | + echo "$PR_TITLE" | npx --offline commitlint -V + +.github/workflows/release-integration.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Release Integration + +on: + workflow_call: + inputs: + release: + required: true + type: string + releases: + required: true + type: string + +jobs: + check-registry: + name: Check Registry + if: github.repository_owner == 'npm' + runs-on: ubuntu-latest + defaults: + run: + shell: bash + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup + uses: ./.github/actions/setup + with: + deps: false + + - name: View in Registry run: | - echo '\${{ github.event.pull_request.title }}' | npx --offline commitlint -V + EXIT_CODE=0 + + function is_published { + if npm view "$@" --loglevel=error > /dev/null; then + echo 0 + else + echo 1 + fi + } + + for release in $(echo '\${{ needs.release.outputs.releases }}' | jq -r '.[] | @base64'); do + name=$(echo "$release" | base64 --decode | jq -r .pkgName) + version=$(echo "$release" | base64 --decode | jq -r .version) + spec="$name@$version" + status=$(is_published "$spec") + if [[ "$status" -eq 1 ]]; then + echo "$spec ERROR" + EXIT_CODE=$status + else + echo "$spec OK" + fi + done + + exit $EXIT_CODE .github/workflows/release.yml ======================================== @@ -2349,9 +2640,10 @@ on: type: string push: branches: - - main - - latest - - release/v* + branches: + - main + - latest + - release/v* permissions: contents: write @@ -2360,150 +2652,95 @@ permissions: jobs: release: - outputs: - pr: \${{ steps.release.outputs.pr }} - release: \${{ steps.release.outputs.release }} - releases: \${{ steps.release.outputs.releases }} - branch: \${{ steps.release.outputs.pr-branch }} - pr-number: \${{ steps.release.outputs.pr-number }} - comment-id: \${{ steps.pr-comment.outputs.result }} - check-id: \${{ steps.check.outputs.check_id }} name: Release if: github.repository_owner == 'npm' runs-on: ubuntu-latest defaults: run: shell: bash + outputs: + pr: \${{ steps.release.outputs.pr }} + release: \${{ steps.release.outputs.release }} + releases: \${{ steps.release.outputs.releases }} + pr-branch: \${{ steps.release.outputs.pr-branch }} + pr-number: \${{ steps.release.outputs.pr-number }} + comment-id: \${{ steps.pr-comment.outputs.comment-id }} + check-id: \${{ steps.check.outputs.check-id }} steps: - name: Checkout uses: actions/checkout@v3 - - name: Setup Git User - run: | - git config --global user.email "npm-cli+bot@github.com" - git config --global user.name "npm CLI robot" - - name: Setup Node - uses: actions/setup-node@v3 - with: - node-version: 18.x - - name: Install npm@latest - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - name: npm Version - run: npm -v - - name: Install Dependencies - run: npm i --ignore-scripts --no-audit --no-fund + + - name: Setup + uses: ./.github/actions/setup + - name: Release Please id: release env: GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }} run: | npx --offline template-oss-release-please "\${{ github.ref_name }}" "\${{ inputs.release-pr }}" - - name: Post Pull Request Comment - if: steps.release.outputs.pr-number + + # If we have opened a release PR, then immediately create an "in_progress" + # check for it so the GitHub UI doesn't report that its mergeable. + # This check will be swapped out for real CI checks once those are started. + - name: Create Check + uses: ./.github/actions/create-check + if: steps.release.outputs.pr-sha + id: check + with: + sha: \${{ steps.release.outputs.pr-sha }} + token: \${{ secrets.GITHUB_TOKEN }} + job-name: Release + + - name: Comment Text uses: actions/github-script@v6 - id: pr-comment + if: steps.release.outputs.pr-number + id: comment-text env: PR_NUMBER: \${{ steps.release.outputs.pr-number }} REF_NAME: \${{ github.ref_name }} with: + result-encoding: string script: | - const { REF_NAME, PR_NUMBER: issue_number } = process.env const { runId, repo: { owner, repo } } = context - const { data: workflow } = await github.rest.actions.getWorkflowRun({ owner, repo, run_id: runId }) - let body = '## Release Manager/n/n' - - const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number }) - let commentId = comments.find(c => c.user.login === 'github-actions[bot]' && c.body.startsWith(body))?.id - body += \`Release workflow run: \${workflow.html_url}/n/n#### Force CI to Update This Release/n/n\` body += \`This PR will be updated and CI will run for every non-/\`chore:/\` commit that is pushed to /\`main/\`. \` body += \`To force CI to update this PR, run this command:/n/n\` - body += \`/\`/\`/\`/ngh workflow run release.yml -r \${REF_NAME} -R \${owner}/\${repo} -f release-pr=\${issue_number}/n/\`/\`/\`\` - - if (commentId) { - await github.rest.issues.updateComment({ owner, repo, comment_id: commentId, body }) - } else { - const { data: comment } = await github.rest.issues.createComment({ owner, repo, issue_number, body }) - commentId = comment?.id - } - - return commentId - - name: Get Workflow Job - uses: actions/github-script@v6 - if: steps.release.outputs.pr-sha - id: check-output - env: - JOB_NAME: "Release" - MATRIX_NAME: "" - with: - script: | - const { owner, repo } = context.repo - - const { data } = await github.rest.actions.listJobsForWorkflowRun({ - owner, - repo, - run_id: context.runId, - per_page: 100 - }) - - const jobName = process.env.JOB_NAME + process.env.MATRIX_NAME - const job = data.jobs.find(j => j.name.endsWith(jobName)) - const jobUrl = job?.html_url + body += \`/\`/\`/\`/ngh workflow run release.yml -r \${process.env.REF_NAME} -R \${owner}/\${repo} -f release-pr=\${process.env.PR_NUMBER}/n/\`/\`/\`\` + return body - const shaUrl = \`\${context.serverUrl}/\${owner}/\${repo}/commit/\${{ steps.release.outputs.pr-sha }}\` - - let summary = \`This check is assosciated with \${shaUrl}/n/n\` - - if (jobUrl) { - summary += \`For run logs, click here: \${jobUrl}\` - } else { - summary += \`Run logs could not be found for a job with name: "\${jobName}"\` - } - - return { summary } - - name: Create Check - uses: LouisBrunner/checks-action@v1.3.1 - id: check - if: steps.release.outputs.pr-sha + - name: Post Pull Request Comment + if: steps.comment-text.outputs.result + uses: ./.github/actions/upsert-comment + id: pr-comment with: token: \${{ secrets.GITHUB_TOKEN }} - status: in_progress - name: Release - sha: \${{ steps.release.outputs.pr-sha }} - output: \${{ steps.check-output.outputs.result }} + body: \${{ steps.comment-text.outputs.result }} + number: \${{ steps.release.outputs.pr-number }} update: - needs: release - outputs: - sha: \${{ steps.commit.outputs.sha }} - check-id: \${{ steps.check.outputs.check_id }} - name: Update - Release - if: github.repository_owner == 'npm' && needs.release.outputs.pr + name: Release PR - Update runs-on: ubuntu-latest defaults: run: shell: bash + if: needs.release.outputs.pr + needs: release + outputs: + sha: \${{ steps.commit.outputs.sha }} + check-id: \${{ steps.check.outputs.check-id }} steps: - name: Checkout uses: actions/checkout@v3 with: + ref: \${{ needs.release.outputs.pr-branch }} fetch-depth: 0 - ref: \${{ needs.release.outputs.branch }} - - name: Setup Git User - run: | - git config --global user.email "npm-cli+bot@github.com" - git config --global user.name "npm CLI robot" - - name: Setup Node - uses: actions/setup-node@v3 - with: - node-version: 18.x - - name: Install npm@latest - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - name: npm Version - run: npm -v - - name: Install Dependencies - run: npm i --ignore-scripts --no-audit --no-fund + + - name: Setup + uses: ./.github/actions/setup + - name: Run Post Pull Request Actions env: RELEASE_PR_NUMBER: \${{ needs.release.outputs.pr-number }} @@ -2512,6 +2749,7 @@ jobs: run: | npm exec --offline -- template-oss-release-manager --lockfile=false npm run rp-pull-request --ignore-scripts -ws -iwr --if-present + - name: Commit id: commit env: @@ -2520,79 +2758,45 @@ jobs: git commit --all --amend --no-edit || true git push --force-with-lease echo "sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT - - name: Get Workflow Job - uses: actions/github-script@v6 - if: steps.commit.outputs.sha - id: check-output - env: - JOB_NAME: "Update - Release" - MATRIX_NAME: "" - with: - script: | - const { owner, repo } = context.repo - - const { data } = await github.rest.actions.listJobsForWorkflowRun({ - owner, - repo, - run_id: context.runId, - per_page: 100 - }) - - const jobName = process.env.JOB_NAME + process.env.MATRIX_NAME - const job = data.jobs.find(j => j.name.endsWith(jobName)) - const jobUrl = job?.html_url - const shaUrl = \`\${context.serverUrl}/\${owner}/\${repo}/commit/\${{ steps.commit.outputs.sha }}\` - - let summary = \`This check is assosciated with \${shaUrl}/n/n\` - - if (jobUrl) { - summary += \`For run logs, click here: \${jobUrl}\` - } else { - summary += \`Run logs could not be found for a job with name: "\${jobName}"\` - } - - return { summary } - name: Create Check - uses: LouisBrunner/checks-action@v1.3.1 - id: check + uses: ./.github/actions/create-check if: steps.commit.outputs.sha + id: check with: + sha: \${{ steps.vommit.outputs.sha }} token: \${{ secrets.GITHUB_TOKEN }} - status: in_progress - name: Release - sha: \${{ steps.commit.outputs.sha }} - output: \${{ steps.check-output.outputs.result }} + job-name: Release + - name: Conclude Check - uses: LouisBrunner/checks-action@v1.3.1 - if: needs.release.outputs.check-id && always() + uses: ./.github/actions/conclude-check + if: needs.release.outputs.check-id && (success() || failure()) with: token: \${{ secrets.GITHUB_TOKEN }} conclusion: \${{ job.status }} - check_id: \${{ needs.release.outputs.check-id }} + check-id: \${{ needs.release.outputs.check-id }} ci: - name: CI - Release + name: Release PR - CI needs: [ release, update ] if: needs.release.outputs.pr - uses: ./.github/workflows/ci-release.yml + uses: ./.github/workflows/ci.yml with: - ref: \${{ needs.release.outputs.branch }} + ref: \${{ needs.release.outputs.pr-branch }} check-sha: \${{ needs.update.outputs.sha }} post-ci: - needs: [ release, update, ci ] - name: Post CI - Release - if: github.repository_owner == 'npm' && needs.release.outputs.pr && always() + name: Relase PR - Post CI runs-on: ubuntu-latest defaults: run: shell: bash + needs: [ release, update, ci ] + if: needs.release.outputs.pr && (success() || failure()) steps: - name: Get Needs Result id: needs-result run: | - result="" if [[ "\${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then result="failure" elif [[ "\${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then @@ -2601,108 +2805,76 @@ jobs: result="success" fi echo "result=$result" >> $GITHUB_OUTPUT + - name: Conclude Check - uses: LouisBrunner/checks-action@v1.3.1 - if: needs.update.outputs.check-id && always() + uses: ./.github/actions/conclude-check + if: needs.update.outputs.check-id && (success() || failure()) with: token: \${{ secrets.GITHUB_TOKEN }} conclusion: \${{ steps.needs-result.outputs.result }} - check_id: \${{ needs.update.outputs.check-id }} + check-id: \${{ needs.update.outputs.check-id }} post-release: - needs: release - name: Post Release - Release - if: github.repository_owner == 'npm' && needs.release.outputs.releases + name: Post Release runs-on: ubuntu-latest defaults: run: shell: bash + needs: release + if: needs.release.outputs.releases steps: - - name: Create Release PR Comment + - name: Comment Text uses: actions/github-script@v6 + id: comment-text env: RELEASES: \${{ needs.release.outputs.releases }} with: + result-encoding: string script: | const releases = JSON.parse(process.env.RELEASES) const { runId, repo: { owner, repo } } = context const issue_number = releases[0].prNumber + const releasePleaseComments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number }) + .then((comments) => comments.filter(c => c.login === 'github-actions[bot]' && c.body.includes('Release is at'))) + + for (const comment of releasePleaseComments) { + await github.rest.issues.deleteComment({ owner, repo, comment_id: comment.id }) + } + let body = '## Release Workflow/n/n' for (const { pkgName, version, url } of releases) { body += \`- /\`\${pkgName}@\${version}/\` \${url}/n\` } + body += \`- Workflow run: :arrows_counterclockwise: https://github.com/\${owner}/\${repo}/actions/runs/\${runId}\` + return body - const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number }) - .then(cs => cs.map(c => ({ id: c.id, login: c.user.login, body: c.body }))) - console.log(\`Found comments: \${JSON.stringify(comments, null, 2)}\`) - const releaseComments = comments.filter(c => c.login === 'github-actions[bot]' && c.body.includes('Release is at')) - - for (const comment of releaseComments) { - console.log(\`Release comment: \${JSON.stringify(comment, null, 2)}\`) - await github.rest.issues.deleteComment({ owner, repo, comment_id: comment.id }) - } - - const runUrl = \`https://github.com/\${owner}/\${repo}/actions/runs/\${runId}\` - await github.rest.issues.createComment({ - owner, - repo, - issue_number, - body: \`\${body}- Workflow run: :arrows_counterclockwise: \${runUrl}\`, - }) + - name: Create Release PR Comment + if: steps.comment-text.outputs.result + uses: ./.github/actions/upsert-comment + with: + token: \${{ secrets.GITHUB_TOKEN }} + body: \${{ steps.comment-text.outputs.result }} + number: \${{ fromJson(needs.release.outputs.release).prNumber }} + includes: \${{ github.run_id }} release-integration: + name: Post Release - Integration needs: release - name: Release Integration if: needs.release.outputs.release - runs-on: ubuntu-latest - defaults: - run: - shell: bash - steps: - - name: Setup Node - uses: actions/setup-node@v3 - with: - node-version: 18.x - - name: Install npm@latest - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - name: npm Version - run: npm -v - - name: View in Registry - run: | - EXIT_CODE=0 - - function is_published { - if npm view "$@" --loglevel=error > /dev/null; then - echo 0 - else - echo 1 - fi - } - - for release in $(echo '\${{ needs.release.outputs.releases }}' | jq -r '.[] | @base64'); do - name=$(echo "$release" | base64 --decode | jq -r .pkgName) - version=$(echo "$release" | base64 --decode | jq -r .version) - spec="$name@$version" - status=$(is_published "$spec") - if [[ "$status" -eq 1 ]]; then - echo "$spec ERROR" - EXIT_CODE=$status - else - echo "$spec OK" - fi - done - - exit $EXIT_CODE + uses: ./.github/workflows/release-integration.yml + with: + release: needs.release.outputs.release + releases: needs.release.outputs.releases post-release-integration: - needs: [ release, release-integration ] - name: Post Release Integration - Release - if: github.repository_owner == 'npm' && needs.release.outputs.release && always() + name: Post Release - Post Integration runs-on: ubuntu-latest defaults: run: shell: bash + needs: [ release, release-integration ] + if: needs.release.outputs.release && (success() || failure()) steps: - name: Get Needs Result id: needs-result @@ -2715,43 +2887,41 @@ jobs: result="white_check_mark" fi echo "result=$result" >> $GITHUB_OUTPUT - - name: Update Release PR Comment + + - name: Comment Text uses: actions/github-script@v6 + id: comment-text env: PR_NUMBER: \${{ fromJSON(needs.release.outputs.release).prNumber }} + REF_NAME: \${{ github.ref_name }} RESULT: \${{ steps.needs-result.outputs.result }} with: script: | - const { PR_NUMBER: issue_number, RESULT } = process.env - const { runId, repo: { owner, repo } } = context - - const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number }) - const updateComment = comments.find(c => - c.user.login === 'github-actions[bot]' && - c.body.startsWith('## Release Workflow/n/n') && - c.body.includes(runId) - ) - - if (updateComment) { - console.log('Found comment to update:', JSON.stringify(updateComment, null, 2)) - let body = updateComment.body.replace(/Workflow run: :[a-z_]+:/, \`Workflow run: :\${RESULT}:\`) - const tagCodeowner = RESULT !== 'white_check_mark' - if (tagCodeowner) { - body += \`/n/n:rotating_light:\` - body += \` @npm/cli-team: The post-release workflow failed for this release.\` - body += \` Manual steps may need to be taken after examining the workflow output\` - body += \` from the above workflow run. :rotating_light:\` - } - await github.rest.issues.updateComment({ - owner, - repo, - body, - comment_id: updateComment.id, - }) - } else { - console.log('No matching comments found:', JSON.stringify(comments, null, 2)) + const { RESULT, PR_NUMBER, REF_NAME } = process.env + const tagCodeowner = RESULT !== 'white_check_mark' + if (tagCodeowner) { + let body = '' + body += \`/n/n:rotating_light:\` + body += \` @npm/cli-team: The post-release workflow failed for this release.\` + body += \` Manual steps may need to be taken after examining the workflow output\` + body += \` from the above workflow run. :rotating_light:\` + body += \`/n/nTo rerun the workflow run the following command:/n/n\` + body += \`/\`/\`/\`/ngh workflow run release.yml -r \${REF_NAME} -R \${owner}/\${repo} -f release-pr=\${PR_NUMBER}/n/\`/\`/\`\` + return body } + - name: Update Release PR Comment + if: steps.comment-text.outputs.result + uses: ./.github/actions/upsert-comment + with: + token: \${{ secrets.GITHUB_TOKEN }} + body: "## Release Workflow" + find: "Workflow run: :[a-z_]+:" + replace: "Workflow run :\${{ steps.needs-result.outputs.result }}:" + append: \${{ steps.comment-text.outputs.result }} + number: \${{ fromJson(needs.release.outputs.release).prNumber }} + includes: \${{ github.run_id }} + .gitignore ======================================== # This file is automatically added by @npmcli/template-oss. Do not edit. @@ -3038,42 +3208,488 @@ workspaces/b/.gitignore !/tap-snapshots/ !/test/ -workspaces/b/package.json -======================================== -{ - "name": "b", - "version": "1.0.0", - "scripts": { - "lint": "eslint /"**/*.js/"", - "postlint": "template-oss-check", - "template-oss-apply": "template-oss-apply --force", - "lintfix": "npm run lint -- --fix", - "snap": "tap", - "test": "tap", - "posttest": "npm run lint" - }, - "author": "GitHub Inc.", - "files": [ - "bin/", - "lib/" - ], - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - }, - "templateOSS": { - "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", - "version": "{{VERSION}}" - }, - "tap": { - "nyc-arg": [ - "--exclude", - "tap-snapshots/**" - ] - } -} -` +workspaces/b/package.json +======================================== +{ + "name": "b", + "version": "1.0.0", + "scripts": { + "lint": "eslint /"**/*.js/"", + "postlint": "template-oss-check", + "template-oss-apply": "template-oss-apply --force", + "lintfix": "npm run lint -- --fix", + "snap": "tap", + "test": "tap", + "posttest": "npm run lint" + }, + "author": "GitHub Inc.", + "files": [ + "bin/", + "lib/" + ], + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + }, + "templateOSS": { + "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", + "version": "{{VERSION}}" + }, + "tap": { + "nyc-arg": [ + "--exclude", + "tap-snapshots/**" + ] + } +} +` + +exports[`test/apply/source-snapshots.js TAP workspaces only > expect resolving Promise 1`] = ` +.github/actions/audit/action.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Audit + +inputs: + shell: + description: shell to run on + default: bash + +runs: + using: composite + steps: + - name: Run Audit + shell: \${{ inputs.shell }} + run: | + npm audit --omit=dev + npm audit --audit-level=none + +.github/actions/changed-files/action.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Get Changed Files + +inputs: + token: + description: GitHub token to use + required: true + +outputs: + files: + value: \${{ steps.files.outputs.result }} + +runs: + using: composite + steps: + - name: Get Changed Files + uses: actions/github-script@v6 + id: files + with: + github-token: \${{ inputs.token }} + script: | + const { repo: { owner, repo }, eventName, payload, sha } = context + let files + if (eventName === 'pull_request' || eventName === 'pull_request_target') { + files = await github.paginate(github.rest.pulls.listFiles, { + owner, + repo, + pull_number: payload.pull_request.number, + }) + } else { + const { data: commit } = await github.rest.repos.getCommit({ + owner, + repo, + ref: sha, + }) + files = commit.files + } + return files.map(f => f.filename) + +.github/actions/changed-workspaces/action.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Get Changed Workspaces + +inputs: + token: + description: GitHub token to use + required: true + shell: + description: shell to run on + default: bash + all: + default: false + type: boolean + files: + description: json stringified array of file names + type: string + +outputs: + flags: + value: \${{ steps.workspaces.outputs.flags }} + +runs: + using: composite + steps: + - name: Get Changed Files + uses: ./.github/actions/changed-files + if: \${{ !inputs.all && !inputs.files }} + id: files + + - name: Get Workspaces + shell: \${{ inputs.shell }} + id: workspaces + run: | + flags=$(npm exec --offline -- template-oss-changed-workspaces '\${{ (inputs.all && '--all') || (inputs.files || steps.files.outputs.result) }}') + echo "flags=\${flags}" >> $GITHUB_OUTPUT + +.github/actions/conclude-check/action.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Conclude Check +description: Conclude a check + +inputs: + token: + description: GitHub token to use + required: true + conclusion: + description: conclusion of check + require: true + check-id: + description: id of check to conclude + required: true + +runs: + using: composite + steps: + - name: Conclude Check + uses: LouisBrunner/checks-action@v1.5.0 + with: + token: \${{ inputs.token }} + conclusion: \${{ inputs.conclusion }} + check_id: \${{ inputs.check-id }} + +.github/actions/create-check/action.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Create Check +description: Create a check and associate it with a sha + +inputs: + token: + description: GitHub token to use + required: true + sha: + description: sha to attach the check to + required: true + job-name: + description: Name of the job to find + required: true + job-status: + description: Status of the check being created + default: in_progress + +outputs: + check-id: + description: The ID of the check that was created + value: \${{ steps.check.outputs.check_id }} + +runs: + using: composite + steps: + - name: Get Workflow Job + uses: actions/github-script@v6 + id: workflow-job + env: + JOB_NAME: \${{ inputs.job-name }} + with: + github-token: \${{ inputs.token }} + script: | + const { JOB_NAME } = process.env + const { repo: { owner, repo }, runId, serverUrl } = context + + const jobs = await github.paginate(github.rest.actions.listJobsForWorkflowRun, { + owner, + repo, + run_id: runId, + }) + const job = jobs.find(j => j.name.endsWith(JOB_NAME)) + + const shaUrl = \`\${serverUrl}/\${owner}/\${repo}/commit/\${{ inputs.sha }}\` + const summary = \`This check is assosciated with \${shaUrl}/n/n\` + const message = job?.html_url + ? \`For run logs, click here: \${job.html_url}\` + : \`Run logs could not be found for a job with name: "\${JOB_NAME}"\` + + // Return a json object with properties that LouisBrunner/checks-actions + // expects as the output of the check + return { summary: summary + message } + + - name: Create Check + uses: LouisBrunner/checks-action@v1.5.0 + id: check + with: + token: \${{ inputs.token }} + status: \${{ inputs.job-status }} + name: \${{ inputs.job-name }} + sha: \${{ inputs.sha }} + output: \${{ steps.workflow-job.outputs.result }} + +.github/actions/deps/action.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Dependencies + +inputs: + command: + description: command to run for the dependencies step + default: install --ignore-scripts --no-audit --no-fund + flags: + description: extra flags to pass to the dependencies step + shell: + description: shell to run on + default: bash + +runs: + using: composite + steps: + - name: Install Dependencies + shell: \${{ inputs.shell }} + run: npm \${{ inputs.command }} \${{ inputs.flags }} + +.github/actions/lint/action.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Lint + +inputs: + flags: + description: flags to pass to the commands + shell: + description: shell to run on + default: bash + +runs: + using: composite + steps: + - name: Lint + shell: \${{ inputs.shell }} + run: | + npm run lint --ignore-scripts \${{ inputs.flags }} + npm run postlint --ignore-scripts \${{ inputs.flags }} + +.github/actions/setup/action.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Setup Repo +description: Setup a repo with standard tools + +inputs: + node-version: + description: node version to use + default: 18.x + npm-version: + description: npm version to use + default: latest + cache: + description: whether to cache npm install or not + type: boolean + default: false + shell: + description: shell to run on + default: bash + deps: + description: whether to run the deps step + type: boolean + default: true + deps-command: + description: command to run for the dependencies step + default: install --ignore-scripts --no-audit --no-fund + deps-flags: + description: extra flags to pass to the dependencies step + +runs: + using: composite + steps: + - name: Setup Git User + shell: \${{ inputs.shell }} + run: | + git config --global user.email "npm-cli+bot@github.com" + git config --global user.name "npm CLI robot" + + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: \${{ inputs.node-version }} + cache: \${{ (inputs.cache && 'npm') || null }} + + - name: Check Node Version + if: inputs.npm-version + id: node-version + shell: \${{ inputs.shell }} + run: | + NODE_VERSION=$(node --version) + echo $NODE_VERSION + if npx semver@7 -r "<=10" "$NODE_VERSION" --yes; then + echo "ten-or-lower=true" >> $GITHUB_OUTPUT + fi + if npx semver@7 -r "<=14" "$NODE_VERSION" --yes; then + echo "fourteen-or-lower=true" >> $GITHUB_OUTPUT + fi + + - name: Update Windows npm + # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows + if: inputs.npm-version && runner.os == 'Windows' && steps.node-version.outputs.fourteen-or-lower + shell: \${{ inputs.shell }} + run: | + curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz + tar xf npm-7.5.4.tgz + cd package + node lib/npm.js install --no-fund --no-audit -g ../npm-7.5.4.tgz + cd .. + rmdir /s /q package + + - name: Install npm@7 + if: inputs.npm-version && steps.node-version.outputs.ten-or-lower + shell: \${{ inputs.shell }} + run: npm i --prefer-online --no-fund --no-audit -g npm@7 + + - name: Install npm@\${{ inputs.npm-version }} + if: inputs.npm-version && !steps.node-version.outputs.ten-or-lower + shell: \${{ inputs.shell }} + run: npm i --prefer-online --no-fund --no-audit -g npm@\${{ inputs.npm-version }} + + - name: npm Version + shell: \${{ inputs.shell }} + run: npm -v + + - name: Setup Dependencies + if: inputs.deps + uses: ./.github/actions/deps + with: + command: \${{ inputs.deps-command }} + flags: \${{ inputs.deps-flags }} + + - name: Add Problem Matcher + shell: \${{ inputs.shell }} + run: | + [[ -f ./.github/matchers/tap.json ]] && echo "::add-matcher::.github/matchers/tap.json" + +.github/actions/test/action.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Test + +inputs: + flags: + description: flags to pass to the commands + shell: + description: shell to run on + default: bash + +runs: + using: composite + steps: + - name: Test + shell: \${{ inputs.shell }} + run: npm test --ignore-scripts \${{ inputs.flags }} + +.github/actions/upsert-comment/action.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Upsert Comment +description: Update or create a comment + +inputs: + token: + description: GitHub token to use + required: true + number: + description: Number of the issue or pull request + required: true + login: + description: Login name of user to look for comments from + default: github-actions[bot] + body: + description: Body of the comment, the first line will be used to match to an existing comment + find: + description: string to find in body + replace: + description: string to replace in body + append: + description: string to append to the body + includes: + description: A string that the comment needs to include + +outputs: + comment-id: + description: The ID of the comment + value: \${{ steps.comment.outputs.result }} + +runs: + using: composite + steps: + - name: Create or Update Comment + uses: actions/github-script@v6 + id: comment + env: + NUMBER: \${{ inputs.number }} + BODY: \${{ inputs.body }} + FIND: \${{ inputs.find }} + REPLACE: \${{ inputs.replace }} + APPEND: \${{ inputs.append }} + LOGIN: \${{ inputs.login }} + INCLUDES: \${{ inputs.includes }} + with: + github-token: \${{ inputs.token }} + script: | + const { BODY, FIND, REPLACE, APPEND, LOGIN, NUMBER: issue_number, INCLUDES } = process.env + const { repo: { owner, repo } } = context + const TITLE = BODY.split('/n')[0].trim() + '/n' + const bodyIncludes = (c) => INCLUDES ? c.body.includes(INCLUDES) : true + + const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number }) + .then(comments => comments.map(c => ({ id: c.id, login: c.user.login, body: c.body }))) + + console.log(\`Found comments: \${JSON.stringify(comments, null, 2)}\`) + console.log(\`Looking for comment with: \${JSON.stringify({ LOGIN, TITLE, INCLUDES }, null, 2)}\`) + + const comment = comments.find(c => + c.login === LOGIN && + c.body.startsWith(TITLE) && + bodyIncludes(c) + ) + + if (comment) { + console.log(\`Found comment: \${JSON.stringify(comment, null, 2)}\`) + let newBody = FIND && REPLACE ? comment.body.replace(new RegExp(FIND, 'g'), REPLACE) : BODY + if (APPEND) { + newBody += APPEND + } + await github.rest.issues.updateComment({ owner, repo, comment_id: comment.id, body: newBody }) + return comment.id + } + + if (FIND || REPLACE || APPEND) { + console.log('Could not find a comment to use find/replace or append to') + return + } + + console.log('Creating new comment') + + const res = await github.rest.issues.createComment({ owner, repo, issue_number, body: BODY }) + return res.data.id -exports[`test/apply/source-snapshots.js TAP workspaces only > expect resolving Promise 1`] = ` .github/dependabot.yml ======================================== # This file is automatically added by @npmcli/template-oss. Do not edit. @@ -3141,30 +3757,21 @@ updates: ] } -.github/workflows/ci-a.yml +.github/workflows/audit.yml ======================================== # This file is automatically added by @npmcli/template-oss. Do not edit. -name: CI - a +name: Audit on: workflow_dispatch: - pull_request: - paths: - - workspaces/a/** - push: - branches: - - main - - latest - paths: - - workspaces/a/** schedule: - # "At 09:00 UTC (02:00 PT) on Monday" https://crontab.guru/#0_9_*_*_1 + # "At 09:00 UTC (01:00 PT) on Monday" https://crontab.guru/#0_9_*_*_1 - cron: "0 9 * * 1" jobs: - lint: - name: Lint + audit: + name: Audit Dependencies if: github.repository_owner == 'npm' runs-on: ubuntu-latest defaults: @@ -3173,207 +3780,20 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 - - name: Setup Git User - run: | - git config --global user.email "npm-cli+bot@github.com" - git config --global user.name "npm CLI robot" - - name: Setup Node - uses: actions/setup-node@v3 - with: - node-version: 18.x - - name: Install npm@latest - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - name: npm Version - run: npm -v - - name: Install Dependencies - run: npm i --ignore-scripts --no-audit --no-fund - - name: Lint - run: npm run lint --ignore-scripts -w a - - name: Post Lint - run: npm run postlint --ignore-scripts -w a - - test: - name: Test - \${{ matrix.platform.name }} - \${{ matrix.node-version }} - if: github.repository_owner == 'npm' - strategy: - fail-fast: false - matrix: - platform: - - name: Linux - os: ubuntu-latest - shell: bash - - name: macOS - os: macos-latest - shell: bash - - name: Windows - os: windows-latest - shell: cmd - node-version: - - 14.17.0 - - 14.x - - 16.13.0 - - 16.x - - 18.0.0 - - 18.x - runs-on: \${{ matrix.platform.os }} - defaults: - run: - shell: \${{ matrix.platform.shell }} - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Setup Git User - run: | - git config --global user.email "npm-cli+bot@github.com" - git config --global user.name "npm CLI robot" - - name: Setup Node - uses: actions/setup-node@v3 - with: - node-version: \${{ matrix.node-version }} - - name: Update Windows npm - # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows - if: matrix.platform.os == 'windows-latest' && (startsWith(matrix.node-version, '12.') || startsWith(matrix.node-version, '14.')) - run: | - curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz - tar xf npm-7.5.4.tgz - cd package - node lib/npm.js install --no-fund --no-audit -g ../npm-7.5.4.tgz - cd .. - rmdir /s /q package - - name: Install npm@7 - if: startsWith(matrix.node-version, '10.') - run: npm i --prefer-online --no-fund --no-audit -g npm@7 - - name: Install npm@latest - if: \${{ !startsWith(matrix.node-version, '10.') }} - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - name: npm Version - run: npm -v - - name: Install Dependencies - run: npm i --ignore-scripts --no-audit --no-fund - - name: Add Problem Matcher - run: echo "::add-matcher::.github/matchers/tap.json" - - name: Test - run: npm test --ignore-scripts -w a - -.github/workflows/ci-b.yml -======================================== -# This file is automatically added by @npmcli/template-oss. Do not edit. - -name: CI - b - -on: - workflow_dispatch: - pull_request: - paths: - - workspaces/b/** - push: - branches: - - main - - latest - paths: - - workspaces/b/** - schedule: - # "At 09:00 UTC (02:00 PT) on Monday" https://crontab.guru/#0_9_*_*_1 - - cron: "0 9 * * 1" -jobs: - lint: - name: Lint - if: github.repository_owner == 'npm' - runs-on: ubuntu-latest - defaults: - run: - shell: bash - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Setup Git User - run: | - git config --global user.email "npm-cli+bot@github.com" - git config --global user.name "npm CLI robot" - - name: Setup Node - uses: actions/setup-node@v3 + - name: Setup + uses: ./.github/actions/setup with: - node-version: 18.x - - name: Install npm@latest - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - name: npm Version - run: npm -v - - name: Install Dependencies - run: npm i --ignore-scripts --no-audit --no-fund - - name: Lint - run: npm run lint --ignore-scripts -w b - - name: Post Lint - run: npm run postlint --ignore-scripts -w b + deps-flags: "--package-lock" - test: - name: Test - \${{ matrix.platform.name }} - \${{ matrix.node-version }} - if: github.repository_owner == 'npm' - strategy: - fail-fast: false - matrix: - platform: - - name: Linux - os: ubuntu-latest - shell: bash - - name: macOS - os: macos-latest - shell: bash - - name: Windows - os: windows-latest - shell: cmd - node-version: - - 14.17.0 - - 14.x - - 16.13.0 - - 16.x - - 18.0.0 - - 18.x - runs-on: \${{ matrix.platform.os }} - defaults: - run: - shell: \${{ matrix.platform.shell }} - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Setup Git User - run: | - git config --global user.email "npm-cli+bot@github.com" - git config --global user.name "npm CLI robot" - - name: Setup Node - uses: actions/setup-node@v3 - with: - node-version: \${{ matrix.node-version }} - - name: Update Windows npm - # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows - if: matrix.platform.os == 'windows-latest' && (startsWith(matrix.node-version, '12.') || startsWith(matrix.node-version, '14.')) - run: | - curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz - tar xf npm-7.5.4.tgz - cd package - node lib/npm.js install --no-fund --no-audit -g ../npm-7.5.4.tgz - cd .. - rmdir /s /q package - - name: Install npm@7 - if: startsWith(matrix.node-version, '10.') - run: npm i --prefer-online --no-fund --no-audit -g npm@7 - - name: Install npm@latest - if: \${{ !startsWith(matrix.node-version, '10.') }} - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - name: npm Version - run: npm -v - - name: Install Dependencies - run: npm i --ignore-scripts --no-audit --no-fund - - name: Add Problem Matcher - run: echo "::add-matcher::.github/matchers/tap.json" - - name: Test - run: npm test --ignore-scripts -w b + - name: Audit + uses: ./.github/actions/audit -.github/workflows/ci-release.yml +.github/workflows/ci.yml ======================================== # This file is automatically added by @npmcli/template-oss. Do not edit. -name: CI - Release +name: CI on: workflow_dispatch: @@ -3381,7 +3801,11 @@ on: ref: required: true type: string - default: main + check-sha: + type: string + all: + default: true + type: boolean workflow_call: inputs: ref: @@ -3390,92 +3814,75 @@ on: check-sha: required: true type: string + all: + default: true + type: boolean + pull_request: + push: + branches: + - main + - latest + schedule: + # "At 10:00 UTC (02:00 PT) on Monday" https://crontab.guru/#0_10_*_*_1 + - cron: "0 10 * * 1" jobs: - lint-all: - name: Lint All + lint: + name: Lint if: github.repository_owner == 'npm' runs-on: ubuntu-latest defaults: run: shell: bash steps: - - name: Get Workflow Job - uses: actions/github-script@v6 - if: inputs.check-sha - id: check-output - env: - JOB_NAME: "Lint All" - MATRIX_NAME: "" + - name: Checkout + uses: actions/checkout@v3 with: - script: | - const { owner, repo } = context.repo - - const { data } = await github.rest.actions.listJobsForWorkflowRun({ - owner, - repo, - run_id: context.runId, - per_page: 100 - }) - - const jobName = process.env.JOB_NAME + process.env.MATRIX_NAME - const job = data.jobs.find(j => j.name.endsWith(jobName)) - const jobUrl = job?.html_url - - const shaUrl = \`\${context.serverUrl}/\${owner}/\${repo}/commit/\${{ inputs.check-sha }}\` - - let summary = \`This check is assosciated with \${shaUrl}/n/n\` - - if (jobUrl) { - summary += \`For run logs, click here: \${jobUrl}\` - } else { - summary += \`Run logs could not be found for a job with name: "\${jobName}"\` - } + ref: \${{ inputs.ref }} - return { summary } - name: Create Check - uses: LouisBrunner/checks-action@v1.3.1 - id: check + uses: ./.github/actions/create-check if: inputs.check-sha + id: check with: - token: \${{ secrets.GITHUB_TOKEN }} - status: in_progress - name: Lint All sha: \${{ inputs.check-sha }} - output: \${{ steps.check-output.outputs.result }} - - name: Checkout - uses: actions/checkout@v3 - with: - ref: \${{ inputs.ref }} - - name: Setup Git User - run: | - git config --global user.email "npm-cli+bot@github.com" - git config --global user.name "npm CLI robot" - - name: Setup Node - uses: actions/setup-node@v3 + token: \${{ secrets.GITHUB_TOKEN }} + job-name: Lint + + - name: Setup + id: setup + continue-on-error: \${{ !!steps.check.outputs.check-id }} + uses: ./.github/actions/setup + + - name: Get Changed Workspaces + id: workspaces + continue-on-error: \${{ !!steps.check.outputs.check-id }} + uses: ./.github/actions/changed-workspaces with: - node-version: 18.x - - name: Install npm@latest - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - name: npm Version - run: npm -v - - name: Install Dependencies - run: npm i --ignore-scripts --no-audit --no-fund + token: \${{ secrets.GITHUB_TOKEN }} + all: \${{ inputs.all }} + - name: Lint - run: npm run lint --ignore-scripts -ws -iwr --if-present - - name: Post Lint - run: npm run postlint --ignore-scripts -ws -iwr --if-present + uses: ./.github/actions/lint + continue-on-error: \${{ !!steps.check.outputs.check-id }} + with: + flags: \${{ steps.workspaces.outputs.flags }} + - name: Conclude Check - uses: LouisBrunner/checks-action@v1.3.1 - if: steps.check.outputs.check_id && always() + uses: ./.github/actions/conclude-check + if: steps.check.outputs.check-id && (success() || failure()) with: token: \${{ secrets.GITHUB_TOKEN }} conclusion: \${{ job.status }} - check_id: \${{ steps.check.outputs.check_id }} + check-id: \${{ steps.check.outputs.check-id }} - test-all: - name: Test All - \${{ matrix.platform.name }} - \${{ matrix.node-version }} + test: + name: Test - \${{ matrix.platform.name }} - \${{ matrix.node-version }} if: github.repository_owner == 'npm' + runs-on: \${{ matrix.platform.os }} + defaults: + run: + shell: \${{ matrix.platform.shell }} strategy: fail-fast: false matrix: @@ -3496,97 +3903,106 @@ jobs: - 16.x - 18.0.0 - 18.x - runs-on: \${{ matrix.platform.os }} - defaults: - run: - shell: \${{ matrix.platform.shell }} steps: - - name: Get Workflow Job - uses: actions/github-script@v6 - if: inputs.check-sha - id: check-output - env: - JOB_NAME: "Test All" - MATRIX_NAME: " - \${{ matrix.platform.name }} - \${{ matrix.node-version }}" - with: - script: | - const { owner, repo } = context.repo + - name: Continue Matrix Run + id: continue-matrix + run: | + if [[ "\${{ matrix.node-version }}" == "14.17.0" || "\${{ inputs.all }}" == "true" ]]; then + echo "result=true" >> $GITHUB_OUTPUT + fi - const { data } = await github.rest.actions.listJobsForWorkflowRun({ - owner, - repo, - run_id: context.runId, - per_page: 100 - }) + - name: Checkout + if: steps.continue-matrix.outputs.result + uses: actions/checkout@v3 + with: + ref: \${{ inputs.ref }} - const jobName = process.env.JOB_NAME + process.env.MATRIX_NAME - const job = data.jobs.find(j => j.name.endsWith(jobName)) - const jobUrl = job?.html_url + - name: Create Check + if: steps.continue-matrix.outputs.result && inputs.check-sha + uses: ./.github/actions/create-check + id: check + with: + sha: \${{ inputs.check-sha }} + token: \${{ secrets.GITHUB_TOKEN }} + job-name: "Test - \${{ matrix.platform.name }} - \${{ matrix.node-version }}" - const shaUrl = \`\${context.serverUrl}/\${owner}/\${repo}/commit/\${{ inputs.check-sha }}\` + - name: Setup + if: steps.continue-matrix.outputs.result + uses: ./.github/actions/setup + id: setup + continue-on-error: \${{ !!steps.check.outputs.check-id }} + with: + node-version: \${{ matrix.node-version }} + shell: \${{ matrix.platform.shell }} - let summary = \`This check is assosciated with \${shaUrl}/n/n\` + - name: Get Changed Workspaces + if: steps.continue-matrix.outputs.result + id: workspaces + continue-on-error: \${{ !!steps.check.outputs.check-id }} + uses: ./.github/actions/changed-workspaces + with: + shell: \${{ matrix.platform.shell }} + token: \${{ secrets.GITHUB_TOKEN }} + all: \${{ inputs.all }} - if (jobUrl) { - summary += \`For run logs, click here: \${jobUrl}\` - } else { - summary += \`Run logs could not be found for a job with name: "\${jobName}"\` - } + - name: Test + if: steps.continue-matrix.outputs.result + uses: ./.github/actions/test + continue-on-error: \${{ !!steps.check.outputs.check-id }} + with: + flags: \${{ steps.workspaces.outputs.flags }} + shell: \${{ matrix.platform.shell }} - return { summary } - - name: Create Check - uses: LouisBrunner/checks-action@v1.3.1 - id: check - if: inputs.check-sha + - name: Conclude Check + uses: ./.github/actions/conclude-check + if: steps.continue-matrix.outputs.result && steps.check.outputs.check-id && (success() || failure()) with: token: \${{ secrets.GITHUB_TOKEN }} - status: in_progress - name: Test All - \${{ matrix.platform.name }} - \${{ matrix.node-version }} - sha: \${{ inputs.check-sha }} - output: \${{ steps.check-output.outputs.result }} + conclusion: \${{ job.status }} + check-id: \${{ steps.check.outputs.check-id }} + +.github/workflows/codeql-analysis.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: CodeQL + +on: + push: + branches: + - main + - latest + pull_request: + branches: + - main + - latest + schedule: + # "At 11:00 UTC (03:00 PT) on Monday" https://crontab.guru/#0_11_*_*_1 + - cron: "0 11 * * 1" + +jobs: + analyze: + name: Analyze + if: github.repository_owner == 'npm' + runs-on: ubuntu-latest + defaults: + run: + shell: bash + permissions: + actions: read + contents: read + security-events: write + steps: - name: Checkout uses: actions/checkout@v3 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 with: - ref: \${{ inputs.ref }} - - name: Setup Git User - run: | - git config --global user.email "npm-cli+bot@github.com" - git config --global user.name "npm CLI robot" - - name: Setup Node - uses: actions/setup-node@v3 - with: - node-version: \${{ matrix.node-version }} - - name: Update Windows npm - # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows - if: matrix.platform.os == 'windows-latest' && (startsWith(matrix.node-version, '12.') || startsWith(matrix.node-version, '14.')) - run: | - curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz - tar xf npm-7.5.4.tgz - cd package - node lib/npm.js install --no-fund --no-audit -g ../npm-7.5.4.tgz - cd .. - rmdir /s /q package - - name: Install npm@7 - if: startsWith(matrix.node-version, '10.') - run: npm i --prefer-online --no-fund --no-audit -g npm@7 - - name: Install npm@latest - if: \${{ !startsWith(matrix.node-version, '10.') }} - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - name: npm Version - run: npm -v - - name: Install Dependencies - run: npm i --ignore-scripts --no-audit --no-fund - - name: Add Problem Matcher - run: echo "::add-matcher::.github/matchers/tap.json" - - name: Test - run: npm test --ignore-scripts -ws -iwr --if-present - - name: Conclude Check - uses: LouisBrunner/checks-action@v1.3.1 - if: steps.check.outputs.check_id && always() - with: - token: \${{ secrets.GITHUB_TOKEN }} - conclusion: \${{ job.status }} - check_id: \${{ steps.check.outputs.check_id }} + languages: javascript + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 .github/workflows/post-dependabot.yml ======================================== @@ -3596,62 +4012,51 @@ name: Post Dependabot on: pull_request -permissions: - contents: write - jobs: template-oss: name: template-oss + permissions: + contents: write if: github.repository_owner == 'npm' && github.actor == 'dependabot[bot]' runs-on: ubuntu-latest defaults: run: shell: bash steps: - - name: Checkout - uses: actions/checkout@v3 - with: - ref: \${{ github.event.pull_request.head.ref }} - - name: Setup Git User - run: | - git config --global user.email "npm-cli+bot@github.com" - git config --global user.name "npm CLI robot" - - name: Setup Node - uses: actions/setup-node@v3 - with: - node-version: 18.x - - name: Install npm@latest - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - name: npm Version - run: npm -v - - name: Install Dependencies - run: npm i --ignore-scripts --no-audit --no-fund - name: Fetch Dependabot Metadata id: metadata uses: dependabot/fetch-metadata@v1 with: github-token: \${{ secrets.GITHUB_TOKEN }} - # Dependabot can update multiple directories so we output which directory - # it is acting on so we can run the command for the correct root or workspace - - name: Get Dependabot Directory + - name: Is Dependency if: contains(steps.metadata.outputs.dependency-names, '@npmcli/template-oss') - id: flags - run: | - dependabot_dir="\${{ steps.metadata.outputs.directory }}" - if [[ "$dependabot_dir" == "/" ]]; then - echo "workspace=-iwr" >> $GITHUB_OUTPUT - else - # strip leading slash from directory so it works as a - # a path to the workspace flag - echo "workspace=-w \${dependabot_dir#/}" >> $GITHUB_OUTPUT - fi + id: dependency + run: echo "continue=true" >> $GITHUB_OUTPUT + + - name: Checkout + if: steps.dependency.outputs.continue + uses: actions/checkout@v3 + with: + ref: \${{ github.event.pull_request.head.ref }} + + - name: Setup + if: steps.dependency.outputs.continue + uses: ./.github/actions/setup + + - name: Get Workspaces + if: steps.dependency.outputs.continue + uses: ./.github/actions/changed-workspaces + id: workspaces + with: + token: \${{ secrets.GITHUB_TOKEN }} + files: '["\${{ steps.metadata.outputs.directory }}"]' - name: Apply Changes - if: steps.flags.outputs.workspace + if: steps.workspaces.outputs.flags id: apply run: | - npm run template-oss-apply \${{ steps.flags.outputs.workspace }} + npm run template-oss-apply \${{ steps.workspaces.outputs.flags }} if [[ \`git status --porcelain\` ]]; then echo "changes=true" >> $GITHUB_OUTPUT fi @@ -3703,7 +4108,7 @@ jobs: - name: Check Changes if: steps.apply.outputs.changes run: | - npm exec --offline \${{ steps.flags.outputs.workspace }} -- template-oss-check + npm exec --offline \${{ steps.workspaces.outputs.flags }} -- template-oss-check - name: Fail on Breaking Change if: steps.apply.outputs.changes && startsWith(steps.apply.outputs.message, 'feat!') @@ -3728,7 +4133,7 @@ on: jobs: commitlint: - name: Lint Commits + name: Lint Commit if: github.repository_owner == 'npm' runs-on: ubuntu-latest defaults: @@ -3739,29 +4144,82 @@ jobs: uses: actions/checkout@v3 with: fetch-depth: 0 - - name: Setup Git User - run: | - git config --global user.email "npm-cli+bot@github.com" - git config --global user.name "npm CLI robot" - - name: Setup Node - uses: actions/setup-node@v3 - with: - node-version: 18.x - - name: Install npm@latest - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - name: npm Version - run: npm -v - - name: Install Dependencies - run: npm i --ignore-scripts --no-audit --no-fund + + - name: Setup + uses: ./.github/actions/setup + - name: Run Commitlint on Commits id: commit continue-on-error: true run: | - npx --offline commitlint -V --from 'origin/\${{ github.base_ref }}' --to \${{ github.event.pull_request.head.sha }} + npx --offline commitlint -V --from 'origin/\${{ github.base_ref }}' --to '\${{ github.event.pull_request.head.sha }}' + - name: Run Commitlint on PR Title if: steps.commit.outcome == 'failure' + env: + PR_TITLE: \${{ github.event.pull_request.title }} + run: | + echo "$PR_TITLE" | npx --offline commitlint -V + +.github/workflows/release-integration.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Release Integration + +on: + workflow_call: + inputs: + release: + required: true + type: string + releases: + required: true + type: string + +jobs: + check-registry: + name: Check Registry + if: github.repository_owner == 'npm' + runs-on: ubuntu-latest + defaults: + run: + shell: bash + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup + uses: ./.github/actions/setup + with: + deps: false + + - name: View in Registry run: | - echo '\${{ github.event.pull_request.title }}' | npx --offline commitlint -V + EXIT_CODE=0 + + function is_published { + if npm view "$@" --loglevel=error > /dev/null; then + echo 0 + else + echo 1 + fi + } + + for release in $(echo '\${{ needs.release.outputs.releases }}' | jq -r '.[] | @base64'); do + name=$(echo "$release" | base64 --decode | jq -r .pkgName) + version=$(echo "$release" | base64 --decode | jq -r .version) + spec="$name@$version" + status=$(is_published "$spec") + if [[ "$status" -eq 1 ]]; then + echo "$spec ERROR" + EXIT_CODE=$status + else + echo "$spec OK" + fi + done + + exit $EXIT_CODE .github/workflows/release.yml ======================================== @@ -3777,9 +4235,10 @@ on: type: string push: branches: - - main - - latest - - release/v* + branches: + - main + - latest + - release/v* permissions: contents: write @@ -3788,150 +4247,95 @@ permissions: jobs: release: - outputs: - pr: \${{ steps.release.outputs.pr }} - release: \${{ steps.release.outputs.release }} - releases: \${{ steps.release.outputs.releases }} - branch: \${{ steps.release.outputs.pr-branch }} - pr-number: \${{ steps.release.outputs.pr-number }} - comment-id: \${{ steps.pr-comment.outputs.result }} - check-id: \${{ steps.check.outputs.check_id }} name: Release if: github.repository_owner == 'npm' runs-on: ubuntu-latest defaults: run: shell: bash + outputs: + pr: \${{ steps.release.outputs.pr }} + release: \${{ steps.release.outputs.release }} + releases: \${{ steps.release.outputs.releases }} + pr-branch: \${{ steps.release.outputs.pr-branch }} + pr-number: \${{ steps.release.outputs.pr-number }} + comment-id: \${{ steps.pr-comment.outputs.comment-id }} + check-id: \${{ steps.check.outputs.check-id }} steps: - name: Checkout uses: actions/checkout@v3 - - name: Setup Git User - run: | - git config --global user.email "npm-cli+bot@github.com" - git config --global user.name "npm CLI robot" - - name: Setup Node - uses: actions/setup-node@v3 - with: - node-version: 18.x - - name: Install npm@latest - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - name: npm Version - run: npm -v - - name: Install Dependencies - run: npm i --ignore-scripts --no-audit --no-fund + + - name: Setup + uses: ./.github/actions/setup + - name: Release Please id: release env: GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }} run: | npx --offline template-oss-release-please "\${{ github.ref_name }}" "\${{ inputs.release-pr }}" - - name: Post Pull Request Comment - if: steps.release.outputs.pr-number + + # If we have opened a release PR, then immediately create an "in_progress" + # check for it so the GitHub UI doesn't report that its mergeable. + # This check will be swapped out for real CI checks once those are started. + - name: Create Check + uses: ./.github/actions/create-check + if: steps.release.outputs.pr-sha + id: check + with: + sha: \${{ steps.release.outputs.pr-sha }} + token: \${{ secrets.GITHUB_TOKEN }} + job-name: Release + + - name: Comment Text uses: actions/github-script@v6 - id: pr-comment + if: steps.release.outputs.pr-number + id: comment-text env: PR_NUMBER: \${{ steps.release.outputs.pr-number }} REF_NAME: \${{ github.ref_name }} with: + result-encoding: string script: | - const { REF_NAME, PR_NUMBER: issue_number } = process.env const { runId, repo: { owner, repo } } = context - const { data: workflow } = await github.rest.actions.getWorkflowRun({ owner, repo, run_id: runId }) - let body = '## Release Manager/n/n' - - const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number }) - let commentId = comments.find(c => c.user.login === 'github-actions[bot]' && c.body.startsWith(body))?.id - body += \`Release workflow run: \${workflow.html_url}/n/n#### Force CI to Update This Release/n/n\` body += \`This PR will be updated and CI will run for every non-/\`chore:/\` commit that is pushed to /\`main/\`. \` body += \`To force CI to update this PR, run this command:/n/n\` - body += \`/\`/\`/\`/ngh workflow run release.yml -r \${REF_NAME} -R \${owner}/\${repo} -f release-pr=\${issue_number}/n/\`/\`/\`\` - - if (commentId) { - await github.rest.issues.updateComment({ owner, repo, comment_id: commentId, body }) - } else { - const { data: comment } = await github.rest.issues.createComment({ owner, repo, issue_number, body }) - commentId = comment?.id - } - - return commentId - - name: Get Workflow Job - uses: actions/github-script@v6 - if: steps.release.outputs.pr-sha - id: check-output - env: - JOB_NAME: "Release" - MATRIX_NAME: "" - with: - script: | - const { owner, repo } = context.repo - - const { data } = await github.rest.actions.listJobsForWorkflowRun({ - owner, - repo, - run_id: context.runId, - per_page: 100 - }) + body += \`/\`/\`/\`/ngh workflow run release.yml -r \${process.env.REF_NAME} -R \${owner}/\${repo} -f release-pr=\${process.env.PR_NUMBER}/n/\`/\`/\`\` + return body - const jobName = process.env.JOB_NAME + process.env.MATRIX_NAME - const job = data.jobs.find(j => j.name.endsWith(jobName)) - const jobUrl = job?.html_url - - const shaUrl = \`\${context.serverUrl}/\${owner}/\${repo}/commit/\${{ steps.release.outputs.pr-sha }}\` - - let summary = \`This check is assosciated with \${shaUrl}/n/n\` - - if (jobUrl) { - summary += \`For run logs, click here: \${jobUrl}\` - } else { - summary += \`Run logs could not be found for a job with name: "\${jobName}"\` - } - - return { summary } - - name: Create Check - uses: LouisBrunner/checks-action@v1.3.1 - id: check - if: steps.release.outputs.pr-sha + - name: Post Pull Request Comment + if: steps.comment-text.outputs.result + uses: ./.github/actions/upsert-comment + id: pr-comment with: token: \${{ secrets.GITHUB_TOKEN }} - status: in_progress - name: Release - sha: \${{ steps.release.outputs.pr-sha }} - output: \${{ steps.check-output.outputs.result }} + body: \${{ steps.comment-text.outputs.result }} + number: \${{ steps.release.outputs.pr-number }} update: - needs: release - outputs: - sha: \${{ steps.commit.outputs.sha }} - check-id: \${{ steps.check.outputs.check_id }} - name: Update - Release - if: github.repository_owner == 'npm' && needs.release.outputs.pr + name: Release PR - Update runs-on: ubuntu-latest defaults: run: shell: bash + if: needs.release.outputs.pr + needs: release + outputs: + sha: \${{ steps.commit.outputs.sha }} + check-id: \${{ steps.check.outputs.check-id }} steps: - name: Checkout uses: actions/checkout@v3 with: + ref: \${{ needs.release.outputs.pr-branch }} fetch-depth: 0 - ref: \${{ needs.release.outputs.branch }} - - name: Setup Git User - run: | - git config --global user.email "npm-cli+bot@github.com" - git config --global user.name "npm CLI robot" - - name: Setup Node - uses: actions/setup-node@v3 - with: - node-version: 18.x - - name: Install npm@latest - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - name: npm Version - run: npm -v - - name: Install Dependencies - run: npm i --ignore-scripts --no-audit --no-fund + + - name: Setup + uses: ./.github/actions/setup + - name: Run Post Pull Request Actions env: RELEASE_PR_NUMBER: \${{ needs.release.outputs.pr-number }} @@ -3940,6 +4344,7 @@ jobs: run: | npm exec --offline -- template-oss-release-manager --lockfile=false npm run rp-pull-request --ignore-scripts -ws -iwr --if-present + - name: Commit id: commit env: @@ -3948,79 +4353,45 @@ jobs: git commit --all --amend --no-edit || true git push --force-with-lease echo "sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT - - name: Get Workflow Job - uses: actions/github-script@v6 - if: steps.commit.outputs.sha - id: check-output - env: - JOB_NAME: "Update - Release" - MATRIX_NAME: "" - with: - script: | - const { owner, repo } = context.repo - - const { data } = await github.rest.actions.listJobsForWorkflowRun({ - owner, - repo, - run_id: context.runId, - per_page: 100 - }) - const jobName = process.env.JOB_NAME + process.env.MATRIX_NAME - const job = data.jobs.find(j => j.name.endsWith(jobName)) - const jobUrl = job?.html_url - - const shaUrl = \`\${context.serverUrl}/\${owner}/\${repo}/commit/\${{ steps.commit.outputs.sha }}\` - - let summary = \`This check is assosciated with \${shaUrl}/n/n\` - - if (jobUrl) { - summary += \`For run logs, click here: \${jobUrl}\` - } else { - summary += \`Run logs could not be found for a job with name: "\${jobName}"\` - } - - return { summary } - name: Create Check - uses: LouisBrunner/checks-action@v1.3.1 - id: check + uses: ./.github/actions/create-check if: steps.commit.outputs.sha + id: check with: + sha: \${{ steps.vommit.outputs.sha }} token: \${{ secrets.GITHUB_TOKEN }} - status: in_progress - name: Release - sha: \${{ steps.commit.outputs.sha }} - output: \${{ steps.check-output.outputs.result }} + job-name: Release + - name: Conclude Check - uses: LouisBrunner/checks-action@v1.3.1 - if: needs.release.outputs.check-id && always() + uses: ./.github/actions/conclude-check + if: needs.release.outputs.check-id && (success() || failure()) with: token: \${{ secrets.GITHUB_TOKEN }} conclusion: \${{ job.status }} - check_id: \${{ needs.release.outputs.check-id }} + check-id: \${{ needs.release.outputs.check-id }} ci: - name: CI - Release + name: Release PR - CI needs: [ release, update ] if: needs.release.outputs.pr - uses: ./.github/workflows/ci-release.yml + uses: ./.github/workflows/ci.yml with: - ref: \${{ needs.release.outputs.branch }} + ref: \${{ needs.release.outputs.pr-branch }} check-sha: \${{ needs.update.outputs.sha }} post-ci: - needs: [ release, update, ci ] - name: Post CI - Release - if: github.repository_owner == 'npm' && needs.release.outputs.pr && always() + name: Relase PR - Post CI runs-on: ubuntu-latest defaults: run: shell: bash + needs: [ release, update, ci ] + if: needs.release.outputs.pr && (success() || failure()) steps: - name: Get Needs Result id: needs-result run: | - result="" if [[ "\${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then result="failure" elif [[ "\${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then @@ -4029,108 +4400,76 @@ jobs: result="success" fi echo "result=$result" >> $GITHUB_OUTPUT + - name: Conclude Check - uses: LouisBrunner/checks-action@v1.3.1 - if: needs.update.outputs.check-id && always() + uses: ./.github/actions/conclude-check + if: needs.update.outputs.check-id && (success() || failure()) with: token: \${{ secrets.GITHUB_TOKEN }} conclusion: \${{ steps.needs-result.outputs.result }} - check_id: \${{ needs.update.outputs.check-id }} + check-id: \${{ needs.update.outputs.check-id }} post-release: - needs: release - name: Post Release - Release - if: github.repository_owner == 'npm' && needs.release.outputs.releases + name: Post Release runs-on: ubuntu-latest defaults: run: shell: bash + needs: release + if: needs.release.outputs.releases steps: - - name: Create Release PR Comment + - name: Comment Text uses: actions/github-script@v6 + id: comment-text env: RELEASES: \${{ needs.release.outputs.releases }} with: + result-encoding: string script: | const releases = JSON.parse(process.env.RELEASES) const { runId, repo: { owner, repo } } = context const issue_number = releases[0].prNumber + const releasePleaseComments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number }) + .then((comments) => comments.filter(c => c.login === 'github-actions[bot]' && c.body.includes('Release is at'))) + + for (const comment of releasePleaseComments) { + await github.rest.issues.deleteComment({ owner, repo, comment_id: comment.id }) + } + let body = '## Release Workflow/n/n' for (const { pkgName, version, url } of releases) { body += \`- /\`\${pkgName}@\${version}/\` \${url}/n\` } + body += \`- Workflow run: :arrows_counterclockwise: https://github.com/\${owner}/\${repo}/actions/runs/\${runId}\` + return body - const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number }) - .then(cs => cs.map(c => ({ id: c.id, login: c.user.login, body: c.body }))) - console.log(\`Found comments: \${JSON.stringify(comments, null, 2)}\`) - const releaseComments = comments.filter(c => c.login === 'github-actions[bot]' && c.body.includes('Release is at')) - - for (const comment of releaseComments) { - console.log(\`Release comment: \${JSON.stringify(comment, null, 2)}\`) - await github.rest.issues.deleteComment({ owner, repo, comment_id: comment.id }) - } - - const runUrl = \`https://github.com/\${owner}/\${repo}/actions/runs/\${runId}\` - await github.rest.issues.createComment({ - owner, - repo, - issue_number, - body: \`\${body}- Workflow run: :arrows_counterclockwise: \${runUrl}\`, - }) + - name: Create Release PR Comment + if: steps.comment-text.outputs.result + uses: ./.github/actions/upsert-comment + with: + token: \${{ secrets.GITHUB_TOKEN }} + body: \${{ steps.comment-text.outputs.result }} + number: \${{ fromJson(needs.release.outputs.release).prNumber }} + includes: \${{ github.run_id }} release-integration: + name: Post Release - Integration needs: release - name: Release Integration if: needs.release.outputs.release - runs-on: ubuntu-latest - defaults: - run: - shell: bash - steps: - - name: Setup Node - uses: actions/setup-node@v3 - with: - node-version: 18.x - - name: Install npm@latest - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - name: npm Version - run: npm -v - - name: View in Registry - run: | - EXIT_CODE=0 - - function is_published { - if npm view "$@" --loglevel=error > /dev/null; then - echo 0 - else - echo 1 - fi - } - - for release in $(echo '\${{ needs.release.outputs.releases }}' | jq -r '.[] | @base64'); do - name=$(echo "$release" | base64 --decode | jq -r .pkgName) - version=$(echo "$release" | base64 --decode | jq -r .version) - spec="$name@$version" - status=$(is_published "$spec") - if [[ "$status" -eq 1 ]]; then - echo "$spec ERROR" - EXIT_CODE=$status - else - echo "$spec OK" - fi - done - - exit $EXIT_CODE + uses: ./.github/workflows/release-integration.yml + with: + release: needs.release.outputs.release + releases: needs.release.outputs.releases post-release-integration: - needs: [ release, release-integration ] - name: Post Release Integration - Release - if: github.repository_owner == 'npm' && needs.release.outputs.release && always() + name: Post Release - Post Integration runs-on: ubuntu-latest defaults: run: shell: bash + needs: [ release, release-integration ] + if: needs.release.outputs.release && (success() || failure()) steps: - name: Get Needs Result id: needs-result @@ -4143,43 +4482,41 @@ jobs: result="white_check_mark" fi echo "result=$result" >> $GITHUB_OUTPUT - - name: Update Release PR Comment + + - name: Comment Text uses: actions/github-script@v6 + id: comment-text env: PR_NUMBER: \${{ fromJSON(needs.release.outputs.release).prNumber }} + REF_NAME: \${{ github.ref_name }} RESULT: \${{ steps.needs-result.outputs.result }} with: script: | - const { PR_NUMBER: issue_number, RESULT } = process.env - const { runId, repo: { owner, repo } } = context - - const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number }) - const updateComment = comments.find(c => - c.user.login === 'github-actions[bot]' && - c.body.startsWith('## Release Workflow/n/n') && - c.body.includes(runId) - ) - - if (updateComment) { - console.log('Found comment to update:', JSON.stringify(updateComment, null, 2)) - let body = updateComment.body.replace(/Workflow run: :[a-z_]+:/, \`Workflow run: :\${RESULT}:\`) - const tagCodeowner = RESULT !== 'white_check_mark' - if (tagCodeowner) { - body += \`/n/n:rotating_light:\` - body += \` @npm/cli-team: The post-release workflow failed for this release.\` - body += \` Manual steps may need to be taken after examining the workflow output\` - body += \` from the above workflow run. :rotating_light:\` - } - await github.rest.issues.updateComment({ - owner, - repo, - body, - comment_id: updateComment.id, - }) - } else { - console.log('No matching comments found:', JSON.stringify(comments, null, 2)) + const { RESULT, PR_NUMBER, REF_NAME } = process.env + const tagCodeowner = RESULT !== 'white_check_mark' + if (tagCodeowner) { + let body = '' + body += \`/n/n:rotating_light:\` + body += \` @npm/cli-team: The post-release workflow failed for this release.\` + body += \` Manual steps may need to be taken after examining the workflow output\` + body += \` from the above workflow run. :rotating_light:\` + body += \`/n/nTo rerun the workflow run the following command:/n/n\` + body += \`/\`/\`/\`/ngh workflow run release.yml -r \${REF_NAME} -R \${owner}/\${repo} -f release-pr=\${PR_NUMBER}/n/\`/\`/\`\` + return body } + - name: Update Release PR Comment + if: steps.comment-text.outputs.result + uses: ./.github/actions/upsert-comment + with: + token: \${{ secrets.GITHUB_TOKEN }} + body: "## Release Workflow" + find: "Workflow run: :[a-z_]+:" + replace: "Workflow run :\${{ steps.needs-result.outputs.result }}:" + append: \${{ steps.comment-text.outputs.result }} + number: \${{ fromJson(needs.release.outputs.release).prNumber }} + includes: \${{ github.run_id }} + .release-please-manifest.json ======================================== { diff --git a/tap-snapshots/test/check/diff-snapshots.js.test.cjs b/tap-snapshots/test/check/diff-snapshots.js.test.cjs index 70d415dc..9608bb2c 100644 --- a/tap-snapshots/test/check/diff-snapshots.js.test.cjs +++ b/tap-snapshots/test/check/diff-snapshots.js.test.cjs @@ -99,25 +99,25 @@ The repo file audit.yml needs to be updated: [@npmcli/template-oss ERROR] There was an erroring getting the target file [@npmcli/template-oss ERROR] Error: {{ROOT}}/test/check/tap-testdir-diff-snapshots-update-and-remove-errors/.github/workflows/audit.yml - YAMLParseError: Implicit keys need to be on a single line at line 40, column 1: + YAMLParseError: Implicit keys need to be on a single line at line 30, column 1: - run: npm audit --audit-level=none + uses: ./.github/actions/audit >>>>I HOPE THIS IS NOT VALID YAML<<<<<<<<<<< ^ - YAMLParseError: Block scalar header includes extra characters: >>>>I at line 40, column 2: + YAMLParseError: Block scalar header includes extra characters: >>>>I at line 30, column 2: >>>>I HOPE THIS IS NOT VALID YAML<<<<<<<<<<< ^ - YAMLParseError: Not a YAML token: HOPE THIS IS NOT VALID YAML<<<<<<<<<<< at line 40, column 7: + YAMLParseError: Not a YAML token: HOPE THIS IS NOT VALID YAML<<<<<<<<<<< at line 30, column 7: >>>>I HOPE THIS IS NOT VALID YAML<<<<<<<<<<< ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - YAMLParseError: Implicit map keys need to be followed by map values at line 40, column 1: + YAMLParseError: Implicit map keys need to be followed by map values at line 30, column 1: - run: npm audit --audit-level=none + uses: ./.github/actions/audit >>>>I HOPE THIS IS NOT VALID YAML<<<<<<<<<<< ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -131,8 +131,8 @@ The repo file audit.yml needs to be updated: on: workflow_dispatch: schedule: - # "At 08:00 UTC (01:00 PT) on Monday" https://crontab.guru/#0_8_*_*_1 - - cron: "0 8 * * 1" + # "At 09:00 UTC (01:00 PT) on Monday" https://crontab.guru/#0_9_*_*_1 + - cron: "0 9 * * 1" jobs: audit: @@ -145,24 +145,14 @@ The repo file audit.yml needs to be updated: steps: - name: Checkout uses: actions/checkout@v3 - - name: Setup Git User - run: | - git config --global user.email "npm-cli+bot@github.com" - git config --global user.name "npm CLI robot" - - name: Setup Node - uses: actions/setup-node@v3 + + - name: Setup + uses: ./.github/actions/setup with: - node-version: 18.x - - name: Install npm@latest - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - name: npm Version - run: npm -v - - name: Install Dependencies - run: npm i --ignore-scripts --no-audit --no-fund --package-lock - - name: Run Production Audit - run: npm audit --omit=dev - - name: Run Full Audit - run: npm audit --audit-level=none + deps-flags: "--package-lock" + + - name: Audit + uses: ./.github/actions/audit To correct it: npx template-oss-apply --force @@ -173,33 +163,31 @@ The repo file ci.yml needs to be updated: .github/workflows/ci.yml ======================================== - @@ -83,5 +83,25 @@ - node-version: \${{ matrix.node-version }} - - name: Update Windows npm - # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows - if: matrix.platform.os == 'windows-latest' && (startsWith(matrix.node-version, '12.') || startsWith(matrix.node-version, '14.')) - - run: "" - + run: | - + curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz - + tar xf npm-7.5.4.tgz - + cd package - + node lib/npm.js install --no-fund --no-audit -g ../npm-7.5.4.tgz - + cd .. - + rmdir /s /q package - + - name: Install npm@7 - + if: startsWith(matrix.node-version, '10.') - + run: npm i --prefer-online --no-fund --no-audit -g npm@7 - + - name: Install npm@latest - + if: \${{ !startsWith(matrix.node-version, '10.') }} - + run: npm i --prefer-online --no-fund --no-audit -g npm@latest - + - name: npm Version - + run: npm -v - + - name: Install Dependencies - + run: npm i --ignore-scripts --no-audit --no-fund - + - name: Add Problem Matcher - + run: echo "::add-matcher::.github/matchers/tap.json" + @@ -146,4 +146,24 @@ + if: steps.continue-matrix.outputs.result + id: workspaces + continue-on-error: \${{ !!steps.check.outputs.check-id }} + uses: ./.github/actions/changed-workspaces + + with: + + shell: \${{ matrix.platform.shell }} + + token: \${{ secrets.GITHUB_TOKEN }} + + all: \${{ inputs.all }} + + + - name: Test - + run: npm test --ignore-scripts + + if: steps.continue-matrix.outputs.result + + uses: ./.github/actions/test + + continue-on-error: \${{ !!steps.check.outputs.check-id }} + + with: + + flags: \${{ steps.workspaces.outputs.flags }} + + shell: \${{ matrix.platform.shell }} + + + + - name: Conclude Check + + uses: ./.github/actions/conclude-check + + if: steps.continue-matrix.outputs.result && steps.check.outputs.check-id && (success() || failure()) + + with: + + token: \${{ secrets.GITHUB_TOKEN }} + + conclusion: \${{ job.status }} + + check-id: \${{ steps.check.outputs.check-id }} To correct it: npx template-oss-apply --force diff --git a/tap-snapshots/test/check/snapshots.js.test.cjs b/tap-snapshots/test/check/snapshots.js.test.cjs index 0e317bad..130438ff 100644 --- a/tap-snapshots/test/check/snapshots.js.test.cjs +++ b/tap-snapshots/test/check/snapshots.js.test.cjs @@ -30,17 +30,27 @@ Some problems were detected: The following repo files need to be added: .commitlintrc.js + .github/actions/audit/action.yml + .github/actions/changed-files/action.yml + .github/actions/changed-workspaces/action.yml + .github/actions/conclude-check/action.yml + .github/actions/create-check/action.yml + .github/actions/deps/action.yml + .github/actions/lint/action.yml + .github/actions/setup/action.yml + .github/actions/test/action.yml + .github/actions/upsert-comment/action.yml .github/CODEOWNERS .github/dependabot.yml .github/ISSUE_TEMPLATE/bug.yml .github/ISSUE_TEMPLATE/config.yml .github/matchers/tap.json .github/workflows/audit.yml - .github/workflows/ci-release.yml .github/workflows/ci.yml .github/workflows/codeql-analysis.yml .github/workflows/post-dependabot.yml .github/workflows/pull-request.yml + .github/workflows/release-integration.yml .github/workflows/release.yml .release-please-manifest.json release-please-config.json @@ -277,17 +287,27 @@ Some problems were detected: The following repo files need to be added: .commitlintrc.js + .github/actions/audit/action.yml + .github/actions/changed-files/action.yml + .github/actions/changed-workspaces/action.yml + .github/actions/conclude-check/action.yml + .github/actions/create-check/action.yml + .github/actions/deps/action.yml + .github/actions/lint/action.yml + .github/actions/setup/action.yml + .github/actions/test/action.yml + .github/actions/upsert-comment/action.yml .github/CODEOWNERS .github/dependabot.yml .github/ISSUE_TEMPLATE/bug.yml .github/ISSUE_TEMPLATE/config.yml .github/matchers/tap.json .github/workflows/audit.yml - .github/workflows/ci-release.yml .github/workflows/ci.yml .github/workflows/codeql-analysis.yml .github/workflows/post-dependabot.yml .github/workflows/pull-request.yml + .github/workflows/release-integration.yml .github/workflows/release.yml .release-please-manifest.json release-please-config.json @@ -363,12 +383,24 @@ To correct it: npm rm @npmcli/template-oss @npmcli/eslint-config tap && npm i @n The following repo files need to be added: + .github/actions/audit/action.yml + .github/actions/changed-files/action.yml + .github/actions/changed-workspaces/action.yml + .github/actions/conclude-check/action.yml + .github/actions/create-check/action.yml + .github/actions/deps/action.yml + .github/actions/lint/action.yml + .github/actions/setup/action.yml + .github/actions/test/action.yml + .github/actions/upsert-comment/action.yml .github/dependabot.yml .github/matchers/tap.json - .github/workflows/ci-name-aaaa.yml - .github/workflows/ci-release.yml + .github/workflows/audit.yml + .github/workflows/ci.yml + .github/workflows/codeql-analysis.yml .github/workflows/post-dependabot.yml .github/workflows/pull-request.yml + .github/workflows/release-integration.yml .github/workflows/release.yml .release-please-manifest.json release-please-config.json @@ -434,12 +466,24 @@ To correct it: npm rm @npmcli/template-oss @npmcli/eslint-config tap && npm i @n The following repo files need to be added: + .github/actions/audit/action.yml + .github/actions/changed-files/action.yml + .github/actions/changed-workspaces/action.yml + .github/actions/conclude-check/action.yml + .github/actions/create-check/action.yml + .github/actions/deps/action.yml + .github/actions/lint/action.yml + .github/actions/setup/action.yml + .github/actions/test/action.yml + .github/actions/upsert-comment/action.yml .github/dependabot.yml .github/matchers/tap.json - .github/workflows/ci-bbb.yml - .github/workflows/ci-release.yml + .github/workflows/audit.yml + .github/workflows/ci.yml + .github/workflows/codeql-analysis.yml .github/workflows/post-dependabot.yml .github/workflows/pull-request.yml + .github/workflows/release-integration.yml .github/workflows/release.yml .release-please-manifest.json release-please-config.json diff --git a/test/apply/index.js b/test/apply/index.js index a26ea1bf..2db8b507 100644 --- a/test/apply/index.js +++ b/test/apply/index.js @@ -161,7 +161,7 @@ t.test('workspace config can override root', async (t) => { a: { templateOSS: { workspaceModule: { - add: { '.eslintrc.js': 'eslintrc.js' }, + add: { '.eslintrc.js': 'files/eslintrc.js' }, rm: { '.npmrc': true }, }, }, @@ -224,19 +224,14 @@ t.test('content can override partials', async (t) => { add:{'.github/workflows/ci-release.yml': 'ci-release.yml'} } }`, - 'ci-release.yml': '{{> ciRelease }}\n job: 1', - '_step-deps.yml': '- run: INSTALL\n', - '_step-test.yml': '- run: TEST\n{{> defaultStepTest }}\n', + 'ci-release.yml': '{{> workflowsCi }}\n job: 1', }, }, }) await s.apply() const ci = await s.readFile(join('.github', 'workflows', 'ci.yml')) const release = await s.readFile(join('.github', 'workflows', 'ci-release.yml')) - t.ok(ci.includes('- run: INSTALL')) - t.ok(ci.includes('- run: TEST')) - t.notOk(ci.includes('npm i --ignore-scripts --no-audit --no-fund')) - t.ok(ci.includes('npm test --ignore-scripts')) + t.notOk(ci.includes('job: 1')) t.ok(release.includes('job: 1')) }) @@ -251,7 +246,7 @@ t.test('content can extend files', async (t) => { content_dir: { // eslint-disable-next-line max-len 'index.js': 'module.exports={rootRepo:{add:{".github/workflows/release.yml": "release.yml"}}}', - 'release.yml': '{{> ciRelease}}\n smoke-publish:\n runs-on: ubuntu-latest', + 'release.yml': '{{> workflowsRelease }}\n smoke-publish:\n runs-on: ubuntu-latest', }, }, }) diff --git a/test/apply/release.js b/test/apply/release.js deleted file mode 100644 index 80a267b1..00000000 --- a/test/apply/release.js +++ /dev/null @@ -1,27 +0,0 @@ -const t = require('tap') -const { join } = require('path') -const setup = require('../setup.js') - -t.test('no workspace flags in commands', async (t) => { - const s = await setup(t) - await s.apply() - - const release = await s.readFile(join('.github', 'workflows', 'ci-release.yml')) - - t.match(release, '--ignore-scripts\n') - t.notMatch(release, '--ignore-scripts -ws -iwr --if-present\n') -}) - -t.test('uses workspace flags in commands', async (t) => { - const s = await setup(t, { - workspaces: { - a: 'a', - }, - }) - await s.apply() - - const release = await s.readFile(join('.github', 'workflows', 'ci-release.yml')) - - t.notMatch(release, '--ignore-scripts\n') - t.match(release, '--ignore-scripts -ws -iwr --if-present\n') -})