⬆️ Update dependencies: eslint packages (major) #936
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
name: CI | |
on: | |
push: | |
branches: | |
- master | |
tags-ignore: | |
- "**" | |
pull_request: | |
branches: | |
- "**" | |
jobs: | |
# If the "invalid" or "skip ci" labels are added to the Pull Request, do not run CI. | |
# see https://github.com/sounisi5011/npm-packages/blob/2a5ca2de696eeb8b40a38de90580441c4c6c96e0/.github/workflows/ci.yaml#L12-L52 | |
if-run-ci: | |
# see https://docs.github.com/en/actions/learn-github-actions/contexts#github-context | |
# see https://stackoverflow.com/a/67532120/4907315 | |
if: >- | |
${{ | |
! ( | |
contains(github.event.pull_request.labels.*.name, 'invalid') | |
|| contains(github.event.pull_request.labels.*.name, 'skip ci') | |
) | |
}} | |
runs-on: ubuntu-latest | |
steps: | |
- name: Check GitHub API rate limit | |
shell: bash | |
env: | |
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
# https://shogo82148.github.io/blog/2020/10/23/github-bot-using-actions/ | |
# https://stackoverflow.com/a/59352240/4907315 | |
# https://docs.github.com/ja/actions/learn-github-actions/workflow-commands-for-github-actions | |
# https://qiita.com/ryo0301/items/de8ce43fe61ede66f80a | |
# https://stedolan.github.io/jq/manual/v1.6/#keys,keys_unsorted | |
# https://qiita.com/richmikan@github/items/2aee77ae13bee2c648f4 | |
run: | | |
readonly DATE_FORMAT='+%Y/%m/%d %T %Z' | |
RATE_LIMIT_JSON="$(gh api 'rate_limit')" | |
echo "${RATE_LIMIT_JSON}" | jq -r '.resources | keys_unsorted[]' | while read -r type; do | |
echo '::group::' "${type}" | |
echo "${RATE_LIMIT_JSON}" | jq ".resources.${type}" | |
echo "::notice::Reset time is $(date -ud "@$(echo "${RATE_LIMIT_JSON}" | jq -r ".resources.${type}.reset")" "${DATE_FORMAT}")" | |
echo '::endgroup::' | |
done | |
core_remaining="$(echo "${RATE_LIMIT_JSON}" | jq -r ".resources.core.remaining")" | |
if [ "${core_remaining}" == 0 ]; then | |
core_reset="$(echo "${RATE_LIMIT_JSON}" | jq -r ".resources.core.reset")" | |
echo "::error::Rate limit reached. Please wait until $(date -ud "@${core_reset}" "${DATE_FORMAT}")" | |
exit 1 | |
elif [ "${core_remaining}" -lt 10 ]; then | |
echo "::warning::The rate limit is approaching: core.remaining=${core_remaining}" | |
fi | |
detect-supported-node: | |
needs: if-run-ci | |
runs-on: ubuntu-latest | |
outputs: | |
versions-json: ${{ steps.detector.outputs.versions-json }} | |
latest-commit-diff: ${{ steps.latest-commit-diff.outputs.result }} | |
steps: | |
- name: Checkout | |
uses: actions/checkout@v3 | |
- id: detector | |
uses: sounisi5011/npm-packages@actions/get-nodejs-versions-array-v0 | |
- name: Get the diff from the latest commit where the GitHub Actions succeeded | |
id: latest-commit-diff | |
uses: actions/github-script@v6 | |
with: | |
result-encoding: string | |
# see https://stackoverflow.com/a/74868904 | |
script: | | |
const { inspect } = require('util'); | |
async function fetchActionsSucceededSha1List({ workflowName, ...variables }) { | |
// Use the GraphQL API instead of the REST API. | |
// This allows us to save the rate limits of the REST API. | |
// Instead, this step consumes the GraphQL API rate limit, but since few jobs use the GraphQL API, this is probably not a problem. | |
const query = `query($owner: String!, $repo: String!, $pullRequest: Int!) { | |
repository(owner: $owner, name: $repo) { | |
pullRequest(number: $pullRequest) { | |
commits(last: 250) { | |
nodes { | |
url | |
commit { | |
oid | |
authoredDate | |
committedDate | |
checkSuites(first: 100) { | |
nodes { | |
conclusion | |
workflowRun { | |
workflow { name } | |
} | |
} | |
} | |
} | |
} | |
} | |
} | |
} | |
}`; | |
const result = await github.graphql(query, variables); | |
console.log(`::group::fetch result\n${JSON.stringify(result, null, 2)}\n::endgroup::`); | |
return ( | |
result.repository.pullRequest.commits.nodes | |
.filter(pullRequestCommit => | |
pullRequestCommit.commit.checkSuites.nodes.some(checkSuite => | |
checkSuite.workflowRun?.workflow.name === workflowName | |
&& checkSuite.conclusion === 'SUCCESS' | |
) | |
) | |
.map(pullRequestCommit => ({ | |
sha1: pullRequestCommit.commit.oid, | |
date: Math.max( | |
new Date(pullRequestCommit.commit.authoredDate).getTime(), | |
new Date(pullRequestCommit.commit.committedDate).getTime(), | |
), | |
})) | |
.sort((a, b) => b.date - a.date) | |
.map(({ sha1 }) => sha1) | |
); | |
} | |
try { | |
const baseSha1 = context.payload.pull_request?.base.sha; | |
console.log('::group::baseSha1\n', baseSha1, '\n::endgroup::'); | |
if (!baseSha1) return '/'; | |
// see https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables | |
const workflowNameOrFilepath = process.env.GITHUB_WORKFLOW ?? context.workflow; | |
const variables = { | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
pullRequest: context.issue.number ?? Number(/^refs\/pull\/(\d+)(?:\/|$)/.exec(context.ref)?.[1]), | |
workflowName: workflowNameOrFilepath, | |
}; | |
console.log('::group::variables\n', variables, '\n::endgroup::'); | |
const isFirstCommit = !context.payload.before; | |
const actionsSucceededSha1List = isFirstCommit || !Number.isInteger(variables.pullRequest) | |
? null | |
: await fetchActionsSucceededSha1List(variables); | |
console.log('::group::actionsSucceededSha1List\n', actionsSucceededSha1List, '\n::endgroup::'); | |
try { | |
console.log('::group::Get file differences'); | |
const latestSha1 = actionsSucceededSha1List?.[0] ?? baseSha1; | |
await exec.exec('git fetch --no-tags --prune --no-recurse-submodules --depth=1 origin', [latestSha1]); | |
const { stdout: diffResult } = await exec.getExecOutput('git diff --name-only', [latestSha1]); | |
return diffResult; | |
} finally { | |
console.log('::endgroup::'); | |
} | |
} catch (error) { | |
core.error(inspect(error)); | |
console.log(`::group::context\n${JSON.stringify({ ...context, issue: context.issue, repo: context.repo }, null, 2)}\n::endgroup::`); | |
return '/'; | |
} | |
lint: | |
needs: if-run-ci | |
runs-on: ubuntu-latest | |
steps: | |
- uses: actions/checkout@v3 | |
- name: Reconfigure git to use HTTP authentication | |
# see https://stackoverflow.com/a/69634516 | |
# see https://github.com/actions/setup-node/issues/214#issuecomment-810829250 | |
shell: bash | |
run: | | |
git config --global \ | |
'url.https://${{ secrets.GITHUB_TOKEN }}@github.com/.insteadOf' \ | |
'ssh://git@github.com/' | |
- name: Install Node.js | |
uses: actions/setup-node@v3 | |
with: | |
node-version: 14.x | |
- name: Enable Corepack (Automatically setup a package manager for Node.js) | |
run: | | |
corepack enable | |
corepack enable npm | |
- name: Show node and package manager version | |
shell: bash | |
run: | | |
echo node "$(node --version)" | |
echo pnpm "$(pnpm --version)" | |
echo "pnpm-store-path=$(pnpm store path --silent)" >> "${GITHUB_ENV}" | |
- name: Cache pnpm | |
uses: actions/cache@v3 | |
with: | |
key: node-cache-${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }} | |
path: ${{ env.pnpm-store-path }} | |
- name: Install dependencies | |
run: pnpm install | |
- name: Run linter | |
run: pnpm run test --pre-test-only | |
unit-test: | |
needs: detect-supported-node | |
strategy: | |
fail-fast: false | |
matrix: | |
os: [ubuntu-latest, windows-latest] | |
node-version: ${{ fromJson(needs.detect-supported-node.outputs.versions-json) }} | |
runs-on: ${{ matrix.os }} | |
steps: | |
- name: Determine whether to skip the test | |
shell: python | |
env: | |
LATEST_COMMIT_DIFF: ${{ needs.detect-supported-node.outputs.latest-commit-diff }} | |
IGNORE_FILES: | | |
.github/workflows/check-if-changelog-is-updated.yaml | |
.github/workflows/pr-auto-approve.yaml | |
.github/workflows/update-license-year.yaml | |
.template/readme.njk | |
.vscode/settings.json | |
.codeclimate.yml | |
.renovaterc.json5 | |
CHANGELOG.md | |
CONTRIBUTING.md | |
lefthook.yml | |
LICENSE | |
README.md | |
run: | | |
import os | |
import random | |
def str_to_set(lines: str): | |
return set(lines.strip('\n').split('\n')) | |
def export_var(name: str, value: str): | |
with open(os.environ.get('GITHUB_ENV'), 'a') as f: | |
# see https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings | |
delimiter = "" | |
while (delimiter in name) or (delimiter in value): | |
delimiter = f"ghadelimiter_{hex(random.getrandbits(255))}" | |
f.write(f"{name}<<{delimiter}\n{value}\n{delimiter}\n") | |
diff_files = str_to_set(os.environ.get('LATEST_COMMIT_DIFF')) | |
ignore_files = str_to_set(os.environ.get('IGNORE_FILES')) | |
if ('/' in diff_files) or (0 < len(diff_files - ignore_files - {''})): | |
export_var('run-test', '1') | |
- uses: actions/checkout@v3 | |
if: ${{ env.run-test }} | |
- name: Reconfigure git to use HTTP authentication | |
if: ${{ env.run-test }} | |
# see https://stackoverflow.com/a/69634516 | |
# see https://github.com/actions/setup-node/issues/214#issuecomment-810829250 | |
shell: bash | |
run: | | |
git config --global \ | |
'url.https://${{ secrets.GITHUB_TOKEN }}@github.com/.insteadOf' \ | |
'ssh://git@github.com/' | |
- name: Install Node.js | |
if: ${{ env.run-test }} | |
uses: actions/setup-node@v3 | |
with: | |
node-version: ${{ matrix.node-version }} | |
- name: Enable Corepack (Automatically setup a package manager for Node.js) | |
if: ${{ env.run-test }} | |
shell: bash | |
run: | | |
# Corepack v0.14 is the earliest version that can use the environment variable COREPACK_ENABLE_STRICT. | |
# see https://github.com/nodejs/corepack/blob/v0.14.0/CHANGELOG.md#0140-2022-09-02 | |
# In addition, this version supports Node.js 14.14.0 or later. | |
# see https://github.com/nodejs/corepack/pull/227 | |
readonly COREPACK_MIN_VERSION='0.14' | |
exec_with_debug() { | |
node - "$@" << 'EOS' | |
// These characters cannot be wrapped in double quotes: | |
// - ! | |
// - $ | |
// - \ | |
// - ` | |
const cannotInQuotChars = /!$\\`/; | |
const quotedArgs = process.argv.slice(2) | |
.map(arg => { | |
const wrapApos = `'${arg.replace(new RegExp(`'[^"${cannotInQuotChars.source}]*`, 'g'), `'"$&"'`)}'`.replace(/^''|''$/g, ``); | |
const wrapQuot = `"${arg.replace(/"[^']*/g, `"'$&'"`)}"`.replace(/^""|""$/g, ``); | |
return arg === `` || /[ !"$&'()*;<>?[\]\\`{|}~]/.test(arg) ? ( | |
new RegExp(`[${cannotInQuotChars.source}]`).test(arg) || wrapApos.length <= wrapQuot.length | |
? wrapApos | |
: wrapQuot | |
) : arg | |
}) | |
.join(' '); | |
// The "[command]" prefix may cause the display to shift, so use it with the "setTimeout" function. | |
setTimeout(() => { | |
process.stderr.write(`[command]${quotedArgs}`); | |
setTimeout(() => process.stderr.write("\n"), 1); | |
}, 1); | |
EOS | |
"$@" | |
} | |
version_lte() { | |
local op1 op2 i max_i | |
# see https://genzouw.com/entry/2019/12/17/120057/1831/ | |
# see https://www.shellcheck.net/wiki/SC2206 | |
IFS='.' read -r -a op1 <<< "$1" | |
IFS='.' read -r -a op2 <<< "$2" | |
max_i="${#op1[@]}" | |
if [[ "${#op2[@]}" -lt "${max_i}" ]]; then | |
max_i="${#op2[@]}" | |
fi | |
for ((i=0; i<"${max_i}"; i++)); do | |
if [[ "${op1[i]}" -lt "${op2[i]}" ]]; then | |
return 0 | |
elif [[ "${op2[i]}" -lt "${op1[i]}" ]]; then | |
return 1 | |
fi | |
done | |
return 0 | |
} | |
# On Windows we can't use the CLI we installed just by running the "npm install --global ..." command. | |
# This function allows use of the installed CLI. | |
npm_install_global() { | |
exec_with_debug npm install --global --force "$1" | |
# Clear the bash command cache. | |
# If the package is overwritten, the path to the command may change. | |
# To track this, remove the command cache. | |
exec_with_debug hash -r | |
if [[ '${{ runner.os }}' == 'Windows' ]]; then | |
# Windows installs global packages to a directory that has lower priority than the default node install so we also need to edit $PATH | |
# see https://github.com/vercel/turbo/pull/1632/files#diff-b92a3120126a9ffe46d7d5ec3a8496ef1eac951db09e1972fac7c78438e36c42R69 | |
local -r globalPath="$(exec_with_debug npm config get prefix)" | |
echo "${globalPath}" | |
local -r unixLikeGlobalPath="$(exec_with_debug cygpath --unix "${globalPath}")" | |
echo "${unixLikeGlobalPath}" | |
if [[ "${PATH}" != "${unixLikeGlobalPath}:"* ]]; then | |
echo "[command]echo '${globalPath}' >> \$GITHUB_PATH" | |
echo "${globalPath}" >> "${GITHUB_PATH}" | |
echo "[command]PATH='${unixLikeGlobalPath}':\$PATH" | |
PATH="${unixLikeGlobalPath}:${PATH}" | |
fi | |
fi | |
} | |
is_packageManager_enabled_by_corepack() { | |
local -r targetPackageManagerCLI="$1" | |
local -r packageManagerVersion="$2" | |
# If the package manager CLI does not exist, Corepack is not enabled and should return false | |
! type "${targetPackageManagerCLI}" >/dev/null 2>&1 && return 1 | |
# If the "npm/yarn/pnpm --version" command fails, Corepack has disabled this package manager and should return false | |
local targetPackageManagerVersion | |
targetPackageManagerVersion="$("${targetPackageManagerCLI}" --version 2>/dev/null)" || return 1 | |
[[ "${targetPackageManagerVersion}" = "${packageManagerVersion%%+*}" ]] | |
} | |
is_packageManager_disabled_by_corepack() { | |
local -r targetPackageManagerCLI="$1" | |
local -r packageManagerName="$2" | |
# If the package manager CLI does not exist, Corepack is not enabled and should return false | |
! type "${targetPackageManagerCLI}" >/dev/null 2>&1 && return 1 | |
# If the "npm/yarn/pnpm --version" command succeeds, Corepack has not disabled this package manager and should return false | |
local errorMessage | |
errorMessage="$("${targetPackageManagerCLI}" --version 2>&1)" && return 1 | |
echo "${errorMessage}" | grep -qE "\bThis project is configured to use ${packageManagerName}\b" | |
} | |
# Note: On Windows, the `npm ls --global corepack` command cannot be used to detect the builtin Corepack. | |
# So, use this complex conditional expression. | |
corepack_enabled() { | |
local -r packageManager="$(< "${GITHUB_WORKSPACE}/package.json" jq --raw-output '.packageManager')" | |
if [[ "${packageManager}" == 'npm@'* ]]; then | |
is_packageManager_enabled_by_corepack npm "${packageManager#*@}" \ | |
&& is_packageManager_disabled_by_corepack yarn "${packageManager%%@*}" \ | |
&& is_packageManager_disabled_by_corepack pnpm "${packageManager%%@*}" | |
elif [[ "${packageManager}" == 'yarn@'* ]]; then | |
is_packageManager_enabled_by_corepack yarn "${packageManager#*@}" \ | |
&& is_packageManager_disabled_by_corepack npm "${packageManager%%@*}" \ | |
&& is_packageManager_disabled_by_corepack pnpm "${packageManager%%@*}" | |
elif [[ "${packageManager}" == 'pnpm@'* ]]; then | |
is_packageManager_enabled_by_corepack pnpm "${packageManager#*@}" \ | |
&& is_packageManager_disabled_by_corepack npm "${packageManager%%@*}" \ | |
&& is_packageManager_disabled_by_corepack yarn "${packageManager%%@*}" | |
else | |
# see https://stackoverflow.com/a/23550347 | |
>&2 echo "Unsupported package manager specification: '${packageManager}'" | |
exit 1 | |
fi | |
} | |
if type corepack >/dev/null 2>&1; then | |
if version_lte "${COREPACK_MIN_VERSION}" "$(corepack --version)"; then | |
exec_with_debug corepack --version | |
echo '::group::Try enable Corepack' | |
else | |
echo "::group::Old Corepack is detected ( corepack@$(corepack --version 2>/dev/null || echo '[Execution failed; Unknown version]') ). Update this" | |
npm_install_global "corepack@${COREPACK_MIN_VERSION}" | |
echo '::endgroup::' | |
exec_with_debug corepack --version | |
echo '::group::Enable Corepack' | |
fi | |
exec_with_debug corepack enable | |
exec_with_debug corepack enable npm | |
echo '::endgroup::' | |
fi | |
# If Corepack is not available, install it manually. | |
# Note: Corepack is already installed on GitHub Actions. | |
# But it does not manage npm versions. | |
# To manage npm, Corepack must be installed via npm, which is builtin to the installed Node.js. | |
if ! corepack_enabled; then | |
echo '::warning::Failed to enable Corepack' | |
echo '::group::Install Corepack manually' | |
if type corepack >/dev/null 2>&1; then | |
# Disable the built-in Corepack in GitHub Actions. | |
# If enabled, problems will occur when the yarn command is executed. | |
exec_with_debug corepack disable | |
fi | |
npm_install_global "corepack@${COREPACK_MIN_VERSION}" | |
echo '::endgroup::' | |
exec_with_debug corepack --version | |
echo '::group::Enable Corepack' | |
exec_with_debug corepack enable | |
exec_with_debug corepack enable npm | |
echo '::endgroup::' | |
fi | |
- name: Show node and package manager version | |
if: ${{ env.run-test }} | |
shell: bash | |
run: | | |
echo node "$(node --version)" | |
echo pnpm "$(pnpm --version)" | |
echo "pnpm-store-path=$(pnpm store path --silent)" >> "${GITHUB_ENV}" | |
- name: Cache pnpm | |
if: ${{ env.run-test }} | |
uses: actions/cache@v3 | |
with: | |
key: node-cache-${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }} | |
path: ${{ env.pnpm-store-path }} | |
- name: Install dependencies | |
if: ${{ env.run-test }} | |
run: pnpm install | |
- name: Run unit test | |
if: ${{ env.run-test }} | |
run: pnpm run test --test-only | |
# Successfully complete this job when all jobs have been completed. | |
# Only by checking this job, it is possible to determine if CI is complete or not. | |
# So we can simplify our GitHub status check configuration. | |
# see https://github.com/orgs/community/discussions/26822 | |
# see https://github.com/sounisi5011/npm-packages/blob/2a5ca2de696eeb8b40a38de90580441c4c6c96e0/.github/workflows/ci.yaml#L482-L498 | |
complete: | |
name: Complete CI | |
needs: [lint, unit-test] | |
if: | |
${{ always() && github.event.pull_request }} | |
# This job is required only for Pull Requests. | |
# It does not need to be run on other branches. | |
runs-on: ubuntu-latest | |
steps: | |
- name: Check all job status | |
# see https://docs.github.com/en/actions/learn-github-actions/contexts#needs-context | |
# see https://docs.github.com/en/actions/learn-github-actions/expressions#contains | |
# see https://stackoverflow.com/a/67532120/4907315 | |
if: >- | |
${{ | |
contains(needs.*.result, 'failure') | |
|| contains(needs.*.result, 'cancelled') | |
|| contains(needs.*.result, 'skipped') | |
}} | |
run: exit 1 |