From ce904d8d4fd34c1d0b0f3d9cce2c9a907d177e44 Mon Sep 17 00:00:00 2001 From: Luke Karrys Date: Wed, 8 Feb 2023 23:00:00 -0700 Subject: [PATCH] wip --- .github/actions/audit/action.yml | 18 + .github/actions/changed-workspaces/action.yml | 55 +++ .github/actions/conclude-check/action.yml | 25 ++ .github/actions/create-check/action.yml | 65 +++ .github/actions/deps/action.yml | 20 + .github/actions/lint/action.yml | 21 + .github/actions/setup/action.yml | 89 +++++ .github/actions/test/action.yml | 21 + .github/actions/upsert-comment/action.yml | 84 ++++ .github/workflows/audit.yml | 24 +- .github/workflows/ci-release.yml | 216 ---------- .github/workflows/ci-test-workspace.yml | 111 ------ .github/workflows/ci.yml | 162 +++++--- .github/workflows/codeql-analysis.yml | 10 +- .github/workflows/post-dependabot.yml | 18 +- .github/workflows/pull-request.yml | 21 +- .github/workflows/release-integration.yml | 57 +++ .github/workflows/release.yml | 369 ++++++------------ README.md | 1 - bin/changed-workspaces.js | 59 +++ lib/config.js | 3 +- lib/content/_job-matrix.yml | 29 -- lib/content/_job-release-integration.yml | 32 -- lib/content/_job.yml | 8 - lib/content/_on-ci.yml | 30 -- lib/content/_step-audit.yml | 4 - lib/content/_step-checks.yml | 54 --- lib/content/_step-deps.yml | 2 - lib/content/_step-git.yml | 12 - lib/content/_step-lint.yml | 4 - lib/content/_step-node.yml | 31 -- lib/content/_step-test.yml | 4 - lib/content/_steps-setup.yml | 6 - lib/content/actions/audit.yml | 17 + lib/content/actions/changed-workspaces.yml | 53 +++ lib/content/actions/conclude-check.yml | 23 ++ lib/content/actions/create-check.yml | 63 +++ lib/content/actions/deps.yml | 18 + lib/content/actions/lint.yml | 19 + lib/content/actions/setup.yml | 87 +++++ lib/content/actions/test.yml | 19 + lib/content/actions/upsert-comment.yml | 82 ++++ lib/content/audit.yml | 12 - lib/content/ci-release.yml | 37 -- lib/content/ci.yml | 13 - lib/content/{ => files}/CODEOWNERS | 0 lib/content/{ => files}/CODE_OF_CONDUCT.md | 0 lib/content/{ => files}/LICENSE.md | 0 lib/content/{ => files}/SECURITY.md | 0 lib/content/{ => files}/bug.yml | 0 lib/content/{ => files}/commitlintrc.js | 0 lib/content/{ => files}/config.yml | 0 lib/content/{ => files}/dependabot.yml | 0 lib/content/{ => files}/eslintrc.js | 0 lib/content/{ => files}/gitignore | 0 lib/content/{ => files}/npmrc | 0 lib/content/{ => files}/pkg.json | 4 +- .../{ => files}/release-please-config.json | 0 .../{ => files}/release-please-manifest.json | 0 lib/content/{ => files}/tap.json | 0 lib/content/index.js | 82 ++-- lib/content/partials/if-org.yml | 1 + lib/content/partials/job-defaults.yml | 4 + lib/content/release.yml | 218 ----------- lib/content/workflows/audit.yml | 24 ++ lib/content/workflows/ci.yml | 165 ++++++++ .../{ => workflows}/codeql-analysis.yml | 8 +- .../{ => workflows}/post-dependabot.yml | 17 +- lib/content/{ => workflows}/pull-request.yml | 14 +- lib/content/workflows/release-integration.yml | 53 +++ lib/content/workflows/release.yml | 273 +++++++++++++ lib/util/template.js | 45 ++- package.json | 3 +- 73 files changed, 1788 insertions(+), 1231 deletions(-) create mode 100644 .github/actions/audit/action.yml create mode 100644 .github/actions/changed-workspaces/action.yml create mode 100644 .github/actions/conclude-check/action.yml create mode 100644 .github/actions/create-check/action.yml create mode 100644 .github/actions/deps/action.yml create mode 100644 .github/actions/lint/action.yml create mode 100644 .github/actions/setup/action.yml create mode 100644 .github/actions/test/action.yml create mode 100644 .github/actions/upsert-comment/action.yml delete mode 100644 .github/workflows/ci-release.yml delete mode 100644 .github/workflows/ci-test-workspace.yml create mode 100644 .github/workflows/release-integration.yml create mode 100644 bin/changed-workspaces.js delete mode 100644 lib/content/_job-matrix.yml delete mode 100644 lib/content/_job-release-integration.yml delete mode 100644 lib/content/_job.yml delete mode 100644 lib/content/_on-ci.yml delete mode 100644 lib/content/_step-audit.yml delete mode 100644 lib/content/_step-checks.yml delete mode 100644 lib/content/_step-deps.yml delete mode 100644 lib/content/_step-git.yml delete mode 100644 lib/content/_step-lint.yml delete mode 100644 lib/content/_step-node.yml delete mode 100644 lib/content/_step-test.yml delete mode 100644 lib/content/_steps-setup.yml create mode 100644 lib/content/actions/audit.yml create mode 100644 lib/content/actions/changed-workspaces.yml create mode 100644 lib/content/actions/conclude-check.yml create mode 100644 lib/content/actions/create-check.yml create mode 100644 lib/content/actions/deps.yml create mode 100644 lib/content/actions/lint.yml create mode 100644 lib/content/actions/setup.yml create mode 100644 lib/content/actions/test.yml create mode 100644 lib/content/actions/upsert-comment.yml delete mode 100644 lib/content/audit.yml delete mode 100644 lib/content/ci-release.yml delete mode 100644 lib/content/ci.yml rename lib/content/{ => files}/CODEOWNERS (100%) rename lib/content/{ => files}/CODE_OF_CONDUCT.md (100%) rename lib/content/{ => files}/LICENSE.md (100%) rename lib/content/{ => files}/SECURITY.md (100%) rename lib/content/{ => files}/bug.yml (100%) rename lib/content/{ => files}/commitlintrc.js (100%) rename lib/content/{ => files}/config.yml (100%) rename lib/content/{ => files}/dependabot.yml (100%) rename lib/content/{ => files}/eslintrc.js (100%) rename lib/content/{ => files}/gitignore (100%) rename lib/content/{ => files}/npmrc (100%) rename lib/content/{ => files}/pkg.json (89%) rename lib/content/{ => files}/release-please-config.json (100%) rename lib/content/{ => files}/release-please-manifest.json (100%) rename lib/content/{ => files}/tap.json (100%) create mode 100644 lib/content/partials/if-org.yml create mode 100644 lib/content/partials/job-defaults.yml delete mode 100644 lib/content/release.yml create mode 100644 lib/content/workflows/audit.yml create mode 100644 lib/content/workflows/ci.yml rename lib/content/{ => workflows}/codeql-analysis.yml (84%) rename lib/content/{ => workflows}/post-dependabot.yml (92%) rename lib/content/{ => workflows}/pull-request.yml (70%) create mode 100644 lib/content/workflows/release-integration.yml create mode 100644 lib/content/workflows/release.yml diff --git a/.github/actions/audit/action.yml b/.github/actions/audit/action.yml new file mode 100644 index 00000000..eaa69bce --- /dev/null +++ b/.github/actions/audit/action.yml @@ -0,0 +1,18 @@ +# 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 Production Audit + shell: ${{ inputs.shell }} + run: npm audit --omit=dev + - name: Run Full Audit + shell: ${{ inputs.shell }} + run: npm audit --audit-level=none diff --git a/.github/actions/changed-workspaces/action.yml b/.github/actions/changed-workspaces/action.yml new file mode 100644 index 00000000..a5007add --- /dev/null +++ b/.github/actions/changed-workspaces/action.yml @@ -0,0 +1,55 @@ +# 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 + +outputs: + files: + value: ${{ steps.files.outputs.result }} + flags: + value: ${{ steps.workspaces.outputs.flags }} + +runs: + using: composite + steps: + - name: Get Changed Files + uses: actions/github-script@v6 + if: ${{ !inputs.all }} + 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) + + - name: Get Workspaces + shell: ${{ inputs.shell }} + id: workspaces + run: | + flags=$(npm exec --offline -- template-oss-changed-workspaces '${{ (inputs.all && '--all') || 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..95d2940d --- /dev/null +++ b/.github/actions/create-check/action.yml @@ -0,0 +1,65 @@ +# 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, + }) + console.log(jobs) + 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..d44e449a --- /dev/null +++ b/.github/actions/lint/action.yml @@ -0,0 +1,21 @@ +# 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 }} + + - name: Post Lint + shell: ${{ inputs.shell }} + run: 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..40bde05f --- /dev/null +++ b/.github/actions/setup/action.yml @@ -0,0 +1,89 @@ +# 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) + if npx semver@7 -r "<=10" "$NODE_VERSION"; then + echo "ten-or-lower=true" >> $GITHUB_OUTPUT + fi + if npx semver@7 -r "<=14" "$NODE_VERSION"; 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 }} diff --git a/.github/actions/test/action.yml b/.github/actions/test/action.yml new file mode 100644 index 00000000..f3097f56 --- /dev/null +++ b/.github/actions/test/action.yml @@ -0,0 +1,21 @@ +# 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: Add Problem Matcher + shell: ${{ inputs.shell }} + run: echo "::add-matcher::.github/matchers/tap.json" + + - 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..25febcc7 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -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..3fb3ad65 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,15 +4,29 @@ name: CI on: workflow_dispatch: + inputs: + ref: + required: true + 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" @@ -28,28 +42,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 +108,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..af4ea37f 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -18,7 +18,11 @@ on: 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..b0d1920f 100644 --- a/.github/workflows/post-dependabot.yml +++ b/.github/workflows/post-dependabot.yml @@ -20,20 +20,10 @@ jobs: 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: Setup + uses: ./.github/actions/setup + - name: Fetch Dependabot Metadata id: metadata uses: dependabot/fetch-metadata@v1 diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 99877daa..25050015 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,25 +23,16 @@ 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 }} + - name: Run Commitlint on PR Title if: steps.commit.outcome == 'failure' run: | 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..9d316649 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,150 +21,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 +118,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 +127,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 +174,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 +256,34 @@ 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 }} 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 } = 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:` + 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..0aff550f 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,6 @@ single devDependency. Configure the use of `@npmcli/template-oss` in your `package.json` using the `templateOSS` property. - ```js { name: 'my-package', diff --git a/bin/changed-workspaces.js b/bin/changed-workspaces.js new file mode 100644 index 00000000..09283abd --- /dev/null +++ b/bin/changed-workspaces.js @@ -0,0 +1,59 @@ +#!/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) { + console.log(ws.path, file) + 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/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-matrix.yml b/lib/content/_job-matrix.yml deleted file mode 100644 index 8d77a5c2..00000000 --- a/lib/content/_job-matrix.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: {{ jobName }} - $\{{ matrix.platform.name }} - $\{{ matrix.node-version }} -if: github.repository_owner == 'npm' -strategy: - fail-fast: false - matrix: - platform: - - name: Linux - os: ubuntu-latest - shell: bash - {{#if macCI}} - - name: macOS - os: macos-latest - shell: bash - {{/if}} - {{#if windowsCI}} - - name: Windows - os: windows-latest - shell: cmd - {{/if}} - node-version: - {{#each ciVersions}} - - {{ . }} - {{/each}} -runs-on: $\{{ matrix.platform.os }} -defaults: - run: - shell: $\{{ matrix.platform.shell }} -steps: - {{> stepsSetup jobIsMatrix=true }} 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..d9c387bf --- /dev/null +++ b/lib/content/actions/audit.yml @@ -0,0 +1,17 @@ +name: Audit + +inputs: + shell: + description: shell to run on + default: bash + +runs: + using: composite + steps: + - name: Run Production Audit + shell: $\{{ inputs.shell }} + run: {{ rootNpmPath }} audit --omit=dev + - name: Run Full Audit + shell: $\{{ inputs.shell }} + run: {{ rootNpmPath }} audit --audit-level=none + diff --git a/lib/content/actions/changed-workspaces.yml b/lib/content/actions/changed-workspaces.yml new file mode 100644 index 00000000..27ece88e --- /dev/null +++ b/lib/content/actions/changed-workspaces.yml @@ -0,0 +1,53 @@ +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 + +outputs: + files: + value: $\{{ steps.files.outputs.result }} + flags: + value: $\{{ steps.workspaces.outputs.flags }} + +runs: + using: composite + steps: + - name: Get Changed Files + uses: actions/github-script@v6 + if: $\{{ !inputs.all }} + 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) + + - name: Get Workspaces + shell: $\{{ inputs.shell }} + id: workspaces + run: | + flags=$({{ rootNpmPath }} exec --offline -- template-oss-changed-workspaces '$\{{ (inputs.all && '--all') || 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..9654ff1d --- /dev/null +++ b/lib/content/actions/create-check.yml @@ -0,0 +1,63 @@ +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, + }) + console.log(jobs) + 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..7b1d101e --- /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: bash + +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..615fb73b --- /dev/null +++ b/lib/content/actions/lint.yml @@ -0,0 +1,19 @@ +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: {{ rootNpmPath }} run lint --ignore-scripts $\{{ inputs.flags }} + + - name: Post Lint + shell: $\{{ inputs.shell }} + run: {{ 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..173b6cc8 --- /dev/null +++ b/lib/content/actions/setup.yml @@ -0,0 +1,87 @@ +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: 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) + if npx semver@7 -r "<=10" "$NODE_VERSION"; then + echo "ten-or-lower=true" >> $GITHUB_OUTPUT + fi + if npx semver@7 -r "<=14" "$NODE_VERSION"; 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 }} diff --git a/lib/content/actions/test.yml b/lib/content/actions/test.yml new file mode 100644 index 00000000..cc22867d --- /dev/null +++ b/lib/content/actions/test.yml @@ -0,0 +1,19 @@ +name: Test + +inputs: + flags: + description: flags to pass to the commands + shell: + description: shell to run on + default: bash + +runs: + using: composite + steps: + - name: Add Problem Matcher + shell: $\{{ inputs.shell }} + run: echo "::add-matcher::.github/matchers/tap.json" + + - 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 89% rename from lib/content/pkg.json rename to lib/content/files/pkg.json index 387626ef..cd05cd20 100644 --- a/lib/content/pkg.json +++ b/lib/content/files/pkg.json @@ -10,8 +10,8 @@ "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 }}}, 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..0164e946 100644 --- a/lib/content/index.js +++ b/lib/content/index.js @@ -2,41 +2,57 @@ 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-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 +62,7 @@ const sharedRootAdd = (name) => ({ id = 'directory' }, }, - '.github/workflows/post-dependabot.yml': { - file: 'post-dependabot.yml', - }, + }) const sharedRootRm = () => ({ @@ -60,12 +74,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 +94,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 +109,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 '.github/workflows/release-please-{{ pkgNameFs }}.yml': true, + '.github/workflows/ci-{{ pkgNameFs }}.yml': true, ...sharedRootRm(), }, } @@ -109,9 +122,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', @@ -153,6 +166,7 @@ module.exports = { codeowner: '@npm/cli-team', npm: 'npm', npx: 'npx', + org: 'npm', npmSpec: 'latest', dependabot: 'increase-if-necessary', unwantedPackages: [ diff --git a/lib/content/partials/if-org.yml b/lib/content/partials/if-org.yml new file mode 100644 index 00000000..960b96d0 --- /dev/null +++ b/lib/content/partials/if-org.yml @@ -0,0 +1 @@ +github.repository_owner == '{{ org }}' \ No newline at end of file diff --git a/lib/content/partials/job-defaults.yml b/lib/content/partials/job-defaults.yml new file mode 100644 index 00000000..4679be2d --- /dev/null +++ b/lib/content/partials/job-defaults.yml @@ -0,0 +1,4 @@ +runs-on: ubuntu-latest +defaults: + run: + shell: bash 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..fdfa1cbc --- /dev/null +++ b/lib/content/workflows/audit.yml @@ -0,0 +1,24 @@ +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: + name: Audit Dependencies + if: {{> partialsIfOrg }} + {{> 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..209f51a7 --- /dev/null +++ b/lib/content/workflows/ci.yml @@ -0,0 +1,165 @@ + +name: CI + +on: + workflow_dispatch: + inputs: + ref: + required: true + 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: + branches: + {{#each branches}} + - {{ . }} + {{/each}} + 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: {{> partialsIfOrg }} + {{> 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 }} + if: {{> partialsIfOrg }} + runs-on: $\{{ matrix.platform.os }} + defaults: + run: + shell: $\{{ matrix.platform.shell }} + strategy: + fail-fast: false + matrix: + platform: + - name: Linux + os: ubuntu-latest + shell: bash + {{#if macCI}} + - name: macOS + os: macos-latest + shell: bash + {{/if}} + {{#if windowsCI}} + - name: Windows + os: windows-latest + shell: cmd + {{/if}} + node-version: + {{#each ciVersions}} + - {{ . }} + {{/each}} + 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 84% rename from lib/content/codeql-analysis.yml rename to lib/content/workflows/codeql-analysis.yml index 4e4c18f4..8817b0cf 100644 --- a/lib/content/codeql-analysis.yml +++ b/lib/content/workflows/codeql-analysis.yml @@ -18,16 +18,20 @@ on: jobs: analyze: name: Analyze - runs-on: ubuntu-latest + if: {{> partialsIfOrg }} + {{> 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 92% rename from lib/content/post-dependabot.yml rename to lib/content/workflows/post-dependabot.yml index fe7526c4..013571f5 100644 --- a/lib/content/post-dependabot.yml +++ b/lib/content/workflows/post-dependabot.yml @@ -8,11 +8,18 @@ permissions: jobs: template-oss: - {{> job - jobName="template-oss" - jobIf="github.actor == 'dependabot[bot]'" - jobCheckout=(obj ref="${{ github.event.pull_request.head.ref }}") - }} + name: template-oss + if: {{> partialsIfOrg }} && github.actor == 'dependabot[bot]' + {{> partialsJobDefaults }} + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + ref: $\{{ github.event.pull_request.head.ref }} + + - name: Setup + uses: ./.github/actions/setup + - name: Fetch Dependabot Metadata id: metadata uses: dependabot/fetch-metadata@v1 diff --git a/lib/content/pull-request.yml b/lib/content/workflows/pull-request.yml similarity index 70% rename from lib/content/pull-request.yml rename to lib/content/workflows/pull-request.yml index a8f3b7e6..5ac8e693 100644 --- a/lib/content/pull-request.yml +++ b/lib/content/workflows/pull-request.yml @@ -10,12 +10,24 @@ on: jobs: commitlint: - {{> job jobName="Lint Commits" jobCheckout=(obj fetch-depth=0) }} + name: Lint Commit + if: {{> partialsIfOrg }} + {{> 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' run: | diff --git a/lib/content/workflows/release-integration.yml b/lib/content/workflows/release-integration.yml new file mode 100644 index 00000000..305cb562 --- /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 + if: {{> partialsIfOrg }} + {{> 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..14c15f24 --- /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: + {{#each branches}} + - {{ . }} + {{/each}} + - release/v* + +permissions: + contents: write + pull-requests: write + checks: write + +jobs: + release: + name: Release + if: {{> partialsIfOrg }} + {{> 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 }} + RESULT: $\{{ steps.needs-result.outputs.result }} + with: + script: | + const { RESULT } = 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:` + 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 }} \ No newline at end of file diff --git a/lib/util/template.js b/lib/util/template.js index 1eb67ed7..0197e769 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,10 +57,11 @@ 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('json', (c) => JSON.stringify(c)) Handlebars.registerHelper('del', () => JSON.stringify(DELETE)) 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\"",