diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index f0ec6c9712627cce88d4f190669c6769544fb883..0000000000000000000000000000000000000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,77 +0,0 @@ -version: 2 - -services_steps: &services_steps - steps: - - checkout - - - run: - name: Install dependencies - command: | - npm ci - environment: - CYPRESS_INSTALL_BINARY: 0 - - - run: - name: Identify services tagged in the PR title - command: npm run test:services:pr:prepare - - - run: - name: Run tests for tagged services - environment: - mocha_reporter: mocha-junit-reporter - MOCHA_FILE: junit/services/results.xml - command: RETRY_COUNT=3 npm run test:services:pr:run - - - store_test_results: - path: junit - -jobs: - services: - docker: - - image: cimg/node:16.15 - - <<: *services_steps - - services@node-17: - docker: - - image: cimg/node:17.9 - environment: - NPM_CONFIG_ENGINE_STRICT: 'false' - - <<: *services_steps - -workflows: - version: 2 - - on-commit: - jobs: - - services: - filters: - branches: - ignore: - - master - - gh-pages - - services@node-17: - filters: - branches: - ignore: - - master - - gh-pages - # on-commit-with-cache: - # jobs: - # - npm-install: - # filters: - # branches: - # ignore: gh-pages - # - services: - # requires: - # - npm-install - # filters: - # branches: - # ignore: master - # - services@node-latest: - # requires: - # - npm-install - # filters: - # branches: - # ignore: master diff --git a/.github/actions/service-tests/action.yml b/.github/actions/service-tests/action.yml new file mode 100644 index 0000000000000000000000000000000000000000..d6aa6008f9a48b6172fa536ba4293a25bcc7243b --- /dev/null +++ b/.github/actions/service-tests/action.yml @@ -0,0 +1,86 @@ +name: 'Service tests' +description: 'Run tests for selected services' +inputs: + github-token: + description: 'The GITHUB_TOKEN secret' + required: true + librariesio-tokens: + description: 'The SERVICETESTS_LIBRARIESIO_TOKENS secret' + required: false + default: '' + obs-user: + description: 'The SERVICETESTS_OBS_USER secret' + required: false + default: '' + obs-pass: + description: 'The SERVICETESTS_OBS_PASS secret' + required: false + default: '' + sl-insight-user-uuid: + description: 'The SERVICETESTS_SL_INSIGHT_USER_UUID secret' + required: false + default: '' + sl-insight-api-token: + description: 'The SERVICETESTS_SL_INSIGHT_API_TOKEN secret' + required: false + default: '' + twitch-client-id: + description: 'The SERVICETESTS_TWITCH_CLIENT_ID secret' + required: false + default: '' + twitch-client-secret: + description: 'The SERVICETESTS_TWITCH_CLIENT_SECRET secret' + required: false + default: '' + wheelmap-token: + description: 'The SERVICETESTS_WHEELMAP_TOKEN secret' + required: false + default: '' + youtube-api-key: + description: 'The SERVICETESTS_YOUTUBE_API_KEY secret' + required: false + default: '' + +runs: + using: 'composite' + steps: + - name: Derive list of service tests to run + # Note: In this step we are using an intermediate env var instead of + # passing github.event.pull_request.title as an argument + # to prevent a shell injection attack. Further reading: + # https://securitylab.github.com/research/github-actions-untrusted-input/#exploitability-and-impact + # https://securitylab.github.com/research/github-actions-untrusted-input/#remediation + if: always() + env: + TITLE: ${{ github.event.pull_request.title }} + run: npm run test:services:pr:prepare "$TITLE" + shell: bash + + - name: Run service tests + if: always() + run: npm run test:services:pr:run -- --reporter json --reporter-option 'output=reports/service-tests.json' + shell: bash + env: + RETRY_COUNT: 3 + GH_TOKEN: '${{ inputs.github-token }}' + LIBRARIESIO_TOKENS: '${{ inputs.librariesio-tokens }}' + OBS_USER: '${{ inputs.obs-user }}' + OBS_PASS: '${{ inputs.obs-pass }}' + SL_INSIGHT_USER_UUID: '${{ inputs.sl-insight-user-uuid }}' + SL_INSIGHT_API_TOKEN: '${{ inputs.sl-insight-api-token }}' + TWITCH_CLIENT_ID: '${{ inputs.twitch-client-id }}' + TWITCH_CLIENT_SECRET: '${{ inputs.twitch-client-secret }}' + WHEELMAP_TOKEN: '${{ inputs.wheelmap-token }}' + YOUTUBE_API_KEY: '${{ inputs.youtube-api-key }}' + + - name: Write Markdown Summary + if: always() + run: | + if test -f 'reports/service-tests.json'; then + echo '# Services' >> $GITHUB_STEP_SUMMARY + sed -e 's/^/- /' pull-request-services.log >> $GITHUB_STEP_SUMMARY + node scripts/mocha2md.js Report reports/service-tests.json >> $GITHUB_STEP_SUMMARY + else + echo 'No services found. Nothing to do.' >> $GITHUB_STEP_SUMMARY + fi + shell: bash diff --git a/.github/workflows/test-services-17.yml b/.github/workflows/test-services-17.yml new file mode 100644 index 0000000000000000000000000000000000000000..0482a49a5cfdb2585f4ea199c7da81e107e71c92 --- /dev/null +++ b/.github/workflows/test-services-17.yml @@ -0,0 +1,40 @@ +name: Services@node 17 +on: + pull_request: + types: [opened, edited, reopened, synchronize] + +jobs: + test-services-17: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup + uses: ./.github/actions/setup + with: + node-version: 17 + env: + NPM_CONFIG_ENGINE_STRICT: 'false' + + - name: Service tests (triggered from local branch) + if: github.event.pull_request.head.repo.full_name == github.repository + uses: ./.github/actions/service-tests + with: + github-token: '${{ secrets.GH_PAT }}' + librariesio-tokens: '${{ secrets.SERVICETESTS_LIBRARIESIO_TOKENS }}' + obs-user: '${{ secrets.SERVICETESTS_OBS_USER }}' + obs-pass: '${{ secrets.SERVICETESTS_OBS_PASS }}' + sl-insight-user-uuid: '${{ secrets.SERVICETESTS_SL_INSIGHT_USER_UUID }}' + sl-insight-api-token: '${{ secrets.SERVICETESTS_SL_INSIGHT_API_TOKEN }}' + twitch-client-id: '${{ secrets.SERVICETESTS_TWITCH_CLIENT_ID }}' + twitch-client-secret: '${{ secrets.SERVICETESTS_TWITCH_CLIENT_SECRET }}' + wheelmap-token: '${{ secrets.SERVICETESTS_WHEELMAP_TOKEN }}' + youtube-api-key: '${{ secrets.SERVICETESTS_YOUTUBE_API_KEY }}' + + - name: Service tests (triggered from fork) + if: github.event.pull_request.head.repo.full_name != github.repository + uses: ./.github/actions/service-tests + with: + github-token: '${{ secrets.GITHUB_TOKEN }}' diff --git a/.github/workflows/test-services.yml b/.github/workflows/test-services.yml new file mode 100644 index 0000000000000000000000000000000000000000..f2a6cc8ff6e7dd6ce97a37178c6db347ae42de0e --- /dev/null +++ b/.github/workflows/test-services.yml @@ -0,0 +1,38 @@ +name: Services +on: + pull_request: + types: [opened, edited, reopened, synchronize] + +jobs: + test-services: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup + uses: ./.github/actions/setup + with: + node-version: 16 + + - name: Service tests (triggered from local branch) + if: github.event.pull_request.head.repo.full_name == github.repository + uses: ./.github/actions/service-tests + with: + github-token: '${{ secrets.GH_PAT }}' + librariesio-tokens: '${{ secrets.SERVICETESTS_LIBRARIESIO_TOKENS }}' + obs-user: '${{ secrets.SERVICETESTS_OBS_USER }}' + obs-pass: '${{ secrets.SERVICETESTS_OBS_PASS }}' + sl-insight-user-uuid: '${{ secrets.SERVICETESTS_SL_INSIGHT_USER_UUID }}' + sl-insight-api-token: '${{ secrets.SERVICETESTS_SL_INSIGHT_API_TOKEN }}' + twitch-client-id: '${{ secrets.SERVICETESTS_TWITCH_CLIENT_ID }}' + twitch-client-secret: '${{ secrets.SERVICETESTS_TWITCH_CLIENT_SECRET }}' + wheelmap-token: '${{ secrets.SERVICETESTS_WHEELMAP_TOKEN }}' + youtube-api-key: '${{ secrets.SERVICETESTS_YOUTUBE_API_KEY }}' + + - name: Service tests (triggered from fork) + if: github.event.pull_request.head.repo.full_name != github.repository + uses: ./.github/actions/service-tests + with: + github-token: '${{ secrets.GITHUB_TOKEN }}' diff --git a/core/service-test-runner/infer-pull-request.js b/core/service-test-runner/infer-pull-request.js deleted file mode 100644 index 699abb5bf5fb4bad402c6cf3f4bb27d0422fbd45..0000000000000000000000000000000000000000 --- a/core/service-test-runner/infer-pull-request.js +++ /dev/null @@ -1,102 +0,0 @@ -/** - * @module - */ - -import { URL, format as urlFormat } from 'url' - -function formatSlug(owner, repo, pullRequest) { - return `${owner}/${repo}#${pullRequest}` -} - -function parseGithubPullRequestUrl(url, options = {}) { - const { verifyBaseUrl } = options - - const parsed = new URL(url) - const components = parsed.pathname.substr(1).split('/') - if (components[2] !== 'pull' || components.length !== 4) { - throw Error(`Invalid GitHub pull request URL: ${url}`) - } - const [owner, repo, , pullRequest] = components - - parsed.pathname = '' - const baseUrl = urlFormat(parsed, { - auth: false, - fragment: false, - search: false, - }).replace(/\/$/, '') - - if (verifyBaseUrl && baseUrl !== verifyBaseUrl) { - throw Error(`Expected base URL to be ${verifyBaseUrl} but got ${baseUrl}`) - } - - return { - baseUrl, - owner, - repo, - pullRequest: +pullRequest, - slug: formatSlug(owner, repo, pullRequest), - } -} - -function parseGithubRepoSlug(slug) { - const components = slug.split('/') - if (components.length !== 2) { - throw Error(`Invalid GitHub repo slug: ${slug}`) - } - const [owner, repo] = components - return { owner, repo } -} - -function _inferPullRequestFromTravisEnv(env) { - const { owner, repo } = parseGithubRepoSlug(env.TRAVIS_REPO_SLUG) - const pullRequest = +env.TRAVIS_PULL_REQUEST - return { - owner, - repo, - pullRequest, - slug: formatSlug(owner, repo, pullRequest), - } -} - -function _inferPullRequestFromCircleEnv(env) { - return parseGithubPullRequestUrl( - env.CI_PULL_REQUEST || env.CIRCLE_PULL_REQUEST - ) -} - -/** - * When called inside a CI build, infer the details - * of a pull request from the environment variables. - * - * @param {object} [env=process.env] Environment variables - * @returns {module:core/service-test-runner/infer-pull-request~PullRequest} - * Pull Request - */ -function inferPullRequest(env = process.env) { - if (env.TRAVIS) { - return _inferPullRequestFromTravisEnv(env) - } else if (env.CIRCLECI) { - return _inferPullRequestFromCircleEnv(env) - } else if (env.CI) { - throw Error( - 'Unsupported CI system. Unable to obtain pull request information from the environment.' - ) - } else { - throw Error( - 'Unable to obtain pull request information from the environment. Is this running in CI?' - ) - } -} - -/** - * Pull Request - * - * @typedef PullRequest - * @property {string} pr.baseUrl (returned for travis CI only) - * @property {string} owner - * @property {string} repo - * @property {string} pullRequest PR/issue number - * @property {string} slug owner/repo/#pullRequest - */ - -export { parseGithubPullRequestUrl, parseGithubRepoSlug, inferPullRequest } diff --git a/core/service-test-runner/infer-pull-request.spec.js b/core/service-test-runner/infer-pull-request.spec.js deleted file mode 100644 index c2050d533e4062ddc23fe811e9b0da718fe37be7..0000000000000000000000000000000000000000 --- a/core/service-test-runner/infer-pull-request.spec.js +++ /dev/null @@ -1,48 +0,0 @@ -import { test, given, forCases } from 'sazerac' -import { - parseGithubPullRequestUrl, - inferPullRequest, -} from './infer-pull-request.js' - -describe('Pull request inference', function () { - test(parseGithubPullRequestUrl, () => { - forCases([ - given('https://github.com/badges/shields/pull/1234'), - given('https://github.com/badges/shields/pull/1234', { - verifyBaseUrl: 'https://github.com', - }), - ]).expect({ - baseUrl: 'https://github.com', - owner: 'badges', - repo: 'shields', - pullRequest: 1234, - slug: 'badges/shields#1234', - }) - - given('https://github.com/badges/shields/pull/1234', { - verifyBaseUrl: 'https://example.com', - }).expectError( - 'Expected base URL to be https://example.com but got https://github.com' - ) - }) - - test(inferPullRequest, () => { - const expected = { - owner: 'badges', - repo: 'shields', - pullRequest: 1234, - slug: 'badges/shields#1234', - } - - given({ - CIRCLECI: '1', - CI_PULL_REQUEST: 'https://github.com/badges/shields/pull/1234', - }).expect(Object.assign({ baseUrl: 'https://github.com' }, expected)) - - given({ - TRAVIS: '1', - TRAVIS_REPO_SLUG: 'badges/shields', - TRAVIS_PULL_REQUEST: '1234', - }).expect(expected) - }) -}) diff --git a/core/service-test-runner/pull-request-services-cli.js b/core/service-test-runner/pull-request-services-cli.js index 34d8d20993c784515a8dee0cc58ce515b11bd4ef..a0c307c20fd5b4b921149ac4f38482405c7068f0 100644 --- a/core/service-test-runner/pull-request-services-cli.js +++ b/core/service-test-runner/pull-request-services-cli.js @@ -1,5 +1,5 @@ -// Infer the current PR from the Travis environment, and look for bracketed, -// space-separated service names in the pull request title. +// Derive a list of service tests to run based on +// space-separated service names in the PR title. // // Output the list of services. // @@ -8,54 +8,26 @@ // Output: // travis // sonar -// -// Example: -// -// TRAVIS=1 TRAVIS_REPO_SLUG=badges/shields TRAVIS_PULL_REQUEST=1108 npm run test:services:pr:prepare -import got from 'got' -import { inferPullRequest } from './infer-pull-request.js' import servicesForTitle from './services-for-title.js' -async function getTitle(owner, repo, pullRequest) { - const { - body: { title }, - } = await got( - `https://api.github.com/repos/${owner}/${repo}/pulls/${pullRequest}`, - { - headers: { - 'User-Agent': 'badges/shields', - Authorization: `token ${process.env.GITHUB_TOKEN}`, - }, - responseType: 'json', - } - ) - return title -} - -async function main() { - const { owner, repo, pullRequest, slug } = inferPullRequest() - console.error(`PR: ${slug}`) +let title - const title = await getTitle(owner, repo, pullRequest) - - console.error(`Title: ${title}\n`) - const services = servicesForTitle(title) - if (services.length === 0) { - console.error('No services found. Nothing to do.') - } else { - console.error( - `Services: (${services.length} found) ${services.join(', ')}\n` - ) - console.log(services.join('\n')) +try { + if (process.argv.length < 3) { + throw new Error() } + title = process.argv[2] +} catch (e) { + console.error('Error processing arguments') + process.exit(1) } -;(async () => { - try { - await main() - } catch (e) { - console.error(e) - process.exit(1) - } -})() +console.error(`Title: ${title}\n`) +const services = servicesForTitle(title) +if (services.length === 0) { + console.error('No services found. Nothing to do.') +} else { + console.error(`Services: (${services.length} found) ${services.join(', ')}\n`) + console.log(services.join('\n')) +} diff --git a/scripts/mocha2md.js b/scripts/mocha2md.js index 6c9f4a009a6441347623767baa3dcf374bcc4dda..e8b2455d3a1bc233881f4655ca7536b60e3d9cff 100644 --- a/scripts/mocha2md.js +++ b/scripts/mocha2md.js @@ -22,6 +22,10 @@ if (data.stats.passes > 0) { if (data.stats.failures > 0) { process.stdout.write(`✖ ${data.stats.failures} failed\n\n`) } +if (data.stats.pending > 0) { + process.stdout.write(`● ${data.stats.pending} pending\n\n`) + process.exit(2) +} if (data.stats.failures > 0) { for (const test of data.tests) { diff --git a/services/github/gist/github-gist-last-commit-redirect.tester.js b/services/github/gist/github-gist-last-commit-redirect.tester.js index b99524249bff95ea678e870f8e1c055db3952df4..a200cf0e8ac4e02410b77b63e9cfd40c6da4b506 100644 --- a/services/github/gist/github-gist-last-commit-redirect.tester.js +++ b/services/github/gist/github-gist-last-commit-redirect.tester.js @@ -1,6 +1,7 @@ import { ServiceTester } from '../../tester.js' + export const t = new ServiceTester({ - id: 'GithubGistLastCommitRedirect', + id: 'GistLastCommitRedirect', title: 'Github Gist Last Commit Redirect', pathPrefix: '/github-gist', }) diff --git a/services/github/gist/github-gist-last-commit.service.js b/services/github/gist/github-gist-last-commit.service.js index 5e68852521b9038ebe9f9f81380de2aa6f42a0dc..cc148a8550f168b0d7036b207f86b9925dddb0e9 100644 --- a/services/github/gist/github-gist-last-commit.service.js +++ b/services/github/gist/github-gist-last-commit.service.js @@ -8,7 +8,7 @@ const schema = Joi.object({ updated_at: Joi.string().required(), }).required() -export default class GithubGistLastCommit extends GithubAuthV3Service { +export default class GistLastCommit extends GithubAuthV3Service { static category = 'activity' static route = { base: 'github/gist/last-commit', pattern: ':gistId' } static examples = [ diff --git a/services/github/gist/github-gist-stars-redirect.tester.js b/services/github/gist/github-gist-stars-redirect.tester.js index b28becd8fde242f10a502566e388bfa60b88cbea..8317855a6adf8151e12d7d6970a8df23b2fd3d33 100644 --- a/services/github/gist/github-gist-stars-redirect.tester.js +++ b/services/github/gist/github-gist-stars-redirect.tester.js @@ -1,6 +1,6 @@ import { ServiceTester } from '../../tester.js' export const t = new ServiceTester({ - id: 'GithubGistStarsRedirect', + id: 'GistStarsRedirect', title: 'Github Gist Stars Redirect', pathPrefix: '/github', }) diff --git a/services/github/gist/github-gist-stars.service.js b/services/github/gist/github-gist-stars.service.js index 44c13b2db749b690348085e7fdc9f463d1b766a4..3166fed8338eb5c05440a8505119d3bf20712496 100644 --- a/services/github/gist/github-gist-stars.service.js +++ b/services/github/gist/github-gist-stars.service.js @@ -24,7 +24,7 @@ const documentation = `${commonDocumentation} <p>This badge shows the number of stargazers for a gist. Gist id is accepted as input and 'gist not found' is returned if the gist is not found for the given gist id. </p>` -export default class GithubGistStars extends GithubAuthV4Service { +export default class GistStars extends GithubAuthV4Service { static category = 'social' static route = {