From 2c2db37ecfa0f14341354e15a7f594f54b8dec7c Mon Sep 17 00:00:00 2001 From: Li-Lun Lin <70696274+alan910127@users.noreply.github.com> Date: Sat, 18 Nov 2023 01:12:15 +0800 Subject: [PATCH] refactor: improve internal usage of environment variables with prepared env (#142) --- package.json | 3 +++ src/cli.ts | 8 -------- src/comment.ts | 28 +++++++++++++--------------- src/env.ts | 27 +++++++++++++++++++++++++++ src/gitUtils.ts | 10 +++------- src/index.ts | 8 +++++--- src/main.ts | 20 +++++++++----------- src/types.ts | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ src/utils.ts | 4 +++- 9 files changed, 111 insertions(+), 45 deletions(-) diff --git a/package.json b/package.json index dda67642..eb775edf 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,9 @@ "useESM": true } ] + }, + "moduleNameMapper": { + "^(\\.{1,2}/.*)\\.js$": "$1" } }, "typeCoverage": { diff --git a/src/cli.ts b/src/cli.ts index 0b08d56b..4dca3193 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -4,7 +4,6 @@ import './env.js' import _ from 'node:module' -import { setFailed } from '@actions/core' import { program } from 'commander' import { comment } from './comment.js' @@ -15,13 +14,6 @@ const cjsRequire = typeof require === 'undefined' ? _.createRequire(import.meta.url) : require const run = async () => { - const { GITLAB_TOKEN } = process.env - - if (!GITLAB_TOKEN) { - setFailed('Please add the `GITLAB_TOKEN` to the changesets action') - return - } - program.version( (cjsRequire('../package.json') as { version: string }).version, ) diff --git a/src/comment.ts b/src/comment.ts index 26742f63..52df9a10 100644 --- a/src/comment.ts +++ b/src/comment.ts @@ -10,6 +10,7 @@ import { humanId } from 'human-id' import { markdownTable } from 'markdown-table' import * as context from './context.js' +import { env } from './env.js' import { getChangedPackages } from './get-changed-packages.js' import { getUsername } from './utils.js' @@ -145,20 +146,20 @@ const hasChangesetBeenAdded = async ( } export const comment = async () => { - const { - CI_MERGE_REQUEST_IID, - CI_MERGE_REQUEST_PROJECT_URL, - CI_MERGE_REQUEST_SOURCE_BRANCH_NAME: mrBranch, - CI_MERGE_REQUEST_SOURCE_BRANCH_SHA, - CI_MERGE_REQUEST_TITLE, - GITLAB_COMMENT_TYPE = 'discussion', - } = process.env - + const mrBranch = env.CI_MERGE_REQUEST_SOURCE_BRANCH_NAME if (!mrBranch) { console.warn('[changesets-gitlab:comment] It should only be used on MR') return } + const { + CI_MERGE_REQUEST_IID: mrIid, + CI_MERGE_REQUEST_PROJECT_URL, + CI_MERGE_REQUEST_SOURCE_BRANCH_SHA, + CI_MERGE_REQUEST_TITLE, + GITLAB_COMMENT_TYPE, + } = env + if (mrBranch.startsWith('changeset-release')) { return } @@ -166,11 +167,8 @@ export const comment = async () => { const api = createApi() let errFromFetchingChangedFiles = '' - - const mrIid = +CI_MERGE_REQUEST_IID! - try { - const latestCommitSha = CI_MERGE_REQUEST_SOURCE_BRANCH_SHA! + const latestCommitSha = CI_MERGE_REQUEST_SOURCE_BRANCH_SHA const changedFilesPromise = api.MergeRequests.showChanges( context.projectId, mrIid, @@ -199,14 +197,14 @@ export const comment = async () => { }), ] as const) - const addChangesetUrl = `${CI_MERGE_REQUEST_PROJECT_URL!}/-/new/${mrBranch}?file_name=.changeset/${humanId( + const addChangesetUrl = `${CI_MERGE_REQUEST_PROJECT_URL}/-/new/${mrBranch}?file_name=.changeset/${humanId( { separator: '-', capitalize: false, }, )}.md&file=${getNewChangesetTemplate( changedPackages, - CI_MERGE_REQUEST_TITLE!, + CI_MERGE_REQUEST_TITLE, )}` const prComment = diff --git a/src/env.ts b/src/env.ts index 5a8f1f0d..2f2c4600 100644 --- a/src/env.ts +++ b/src/env.ts @@ -1,3 +1,30 @@ +import { setFailed } from '@actions/core' import dotenv from 'dotenv' +import type { Env } from './types' + dotenv.config() + +let isGitlabTokenValidated = false + +export const env = { + ...process.env, + + CI_MERGE_REQUEST_IID: +process.env.CI_MERGE_REQUEST_IID!, + GITLAB_CI_USER_EMAIL: + process.env.GITLAB_CI_USER_EMAIL || 'gitlab[bot]@users.noreply.gitlab.com', + GITLAB_COMMENT_TYPE: process.env.GITLAB_COMMENT_TYPE ?? 'discussion', + DEBUG_GITLAB_CREDENTIAL: process.env.DEBUG_GITLAB_CREDENTIAL ?? 'false', + + // only check for the token if we are explicitly using it + // eslint-disable-next-line sonar/function-name + get GITLAB_TOKEN() { + if (!isGitlabTokenValidated) { + isGitlabTokenValidated = true + if (!process.env.GITLAB_TOKEN) { + setFailed('Please add the `GITLAB_TOKEN` to the changesets action') + } + } + return process.env.GITLAB_TOKEN as string + }, +} as Env diff --git a/src/gitUtils.ts b/src/gitUtils.ts index 5f8cf468..82b7bfbf 100644 --- a/src/gitUtils.ts +++ b/src/gitUtils.ts @@ -1,19 +1,15 @@ import { exec } from '@actions/exec' +import { env } from './env.js' import { execWithOutput, identify } from './utils.js' export const setupUser = async () => { await exec('git', [ 'config', 'user.name', - process.env.GITLAB_CI_USER_NAME || process.env.GITLAB_USER_NAME!, - ]) - await exec('git', [ - 'config', - 'user.email', - process.env.GITLAB_CI_USER_EMAIL || - '"gitlab[bot]@users.noreply.gitlab.com"', + env.GITLAB_CI_USER_NAME || env.GITLAB_USER_NAME, ]) + await exec('git', ['config', 'user.email', env.GITLAB_CI_USER_EMAIL]) } export const pullBranch = async (branch: string) => { diff --git a/src/index.ts b/src/index.ts index 8d14d471..2c740e55 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,8 @@ import { Gitlab } from '@gitbeaker/rest' import type { ProxyAgentConfigurationType } from 'global-agent' import { bootstrap } from 'global-agent' +import { env } from './env.js' + const PROXY_PROPS = ['http_proxy', 'https_proxy', 'no_proxy'] as const declare global { @@ -23,12 +25,12 @@ export const createApi = (gitlabToken?: string) => { } } - const token = gitlabToken || process.env.GITLAB_TOKEN! - const host = process.env.GITLAB_HOST ?? process.env.CI_SERVER_URL + const token = gitlabToken || env.GITLAB_TOKEN + const host = env.GITLAB_HOST // we cannot use { [tokenType]: token } now // because it will break the type of the Gitlab constructor - switch (process.env.GITLAB_TOKEN_TYPE) { + switch (env.GITLAB_TOKEN_TYPE) { case 'job': { return new Gitlab({ host, diff --git a/src/main.ts b/src/main.ts index ee74275b..0f2625b4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,6 +4,7 @@ import { getInput, setFailed, setOutput, exportVariable } from '@actions/core' import { exec } from '@actions/exec' import fs from 'fs-extra' +import { env } from './env.js' import { setupUser } from './gitUtils.js' import readChangesetState from './readChangesetState.js' import { runPublish, runVersion } from './run.js' @@ -15,18 +16,15 @@ import { createApi } from './index.js' export const main = async ({ published, onlyChangesets, -}: // eslint-disable-next-line sonarjs/cognitive-complexity -MainCommandOptions = {}) => { +}: MainCommandOptions = {}) => { const { CI, - CI_PROJECT_PATH, - CI_SERVER_URL, GITLAB_HOST, GITLAB_TOKEN, HOME, NPM_TOKEN, DEBUG_GITLAB_CREDENTIAL = 'false', - } = process.env + } = env setOutput('published', false) setOutput('publishedPackages', []) @@ -35,7 +33,7 @@ MainCommandOptions = {}) => { console.log('setting git user') await setupUser() - const url = new URL(GITLAB_HOST ?? CI_SERVER_URL ?? 'https://gitlab.com') + const url = new URL(GITLAB_HOST) console.log('setting GitLab credentials') const username = await getUsername(createApi()) @@ -46,9 +44,9 @@ MainCommandOptions = {}) => { 'remote', 'set-url', 'origin', - `${url.protocol}//${username}:${GITLAB_TOKEN!}@${ + `${url.protocol}//${username}:${GITLAB_TOKEN}@${ url.host - }${url.pathname.replace(/\/$/, '')}/${CI_PROJECT_PATH!}.git`, + }${url.pathname.replace(/\/$/, '')}/${env.CI_PROJECT_PATH}.git`, // eslint-disable-line unicorn/consistent-destructuring ], { silent: !['true', '1'].includes(DEBUG_GITLAB_CREDENTIAL) }, ) @@ -70,7 +68,7 @@ MainCommandOptions = {}) => { 'No changesets found, attempting to publish any unpublished packages to npm', ) - const npmrcPath = `${HOME!}/.npmrc` + const npmrcPath = `${HOME}/.npmrc` if (fs.existsSync(npmrcPath)) { console.log('Found existing .npmrc file') } else if (NPM_TOKEN) { @@ -88,7 +86,7 @@ MainCommandOptions = {}) => { const result = await runPublish({ script: publishScript, - gitlabToken: GITLAB_TOKEN!, + gitlabToken: GITLAB_TOKEN, createGitlabReleases: getInput('create_gitlab_releases') !== 'false', }) @@ -106,7 +104,7 @@ MainCommandOptions = {}) => { case hasChangesets: { await runVersion({ script: getOptionalInput('version'), - gitlabToken: GITLAB_TOKEN!, + gitlabToken: GITLAB_TOKEN, mrTitle: getOptionalInput('title'), mrTargetBranch: getOptionalInput('target_branch'), commitMessage: getOptionalInput('commit'), diff --git a/src/types.ts b/src/types.ts index 4a171ee2..d8377383 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,3 +2,51 @@ export interface MainCommandOptions { published?: string onlyChangesets?: string } + +// This type represents a couple of possible literal values for a string, but also +// allows any other string value. It's useful for environment variables that can +// have a default value, but can also be overridden by the user. +// The weird `string & {}` is to make sure that the type is not narrowed to `string` +// See: https://twitter.com/mattpocockuk/status/1671908303918473217 +// eslint-disable-next-line @typescript-eslint/ban-types, sonar/no-useless-intersection +export type LooseString = T | (string & {}) + +export type Env = GitLabCIPredefinedVariables & + MergeRequestVariables & { + GITLAB_HOST: string + + GITLAB_TOKEN: string + GITLAB_TOKEN_TYPE: LooseString<'job' | 'oauth'> + GITLAB_CI_USER_NAME?: string + GITLAB_CI_USER_EMAIL: string + GITLAB_COMMENT_TYPE: LooseString<'discussion' | 'note'> + DEBUG_GITLAB_CREDENTIAL: LooseString<'1' | 'true'> + + HOME: string + NPM_TOKEN?: string + } + +type MergeRequestVariables = + | { + // this is used to be checked if the current job is a merge request job + CI_MERGE_REQUEST_SOURCE_BRANCH_NAME: undefined + } + | { + CI_MERGE_REQUEST_IID: number + CI_MERGE_REQUEST_PROJECT_URL: string + CI_MERGE_REQUEST_SOURCE_BRANCH_NAME: string + CI_MERGE_REQUEST_SOURCE_BRANCH_SHA: string + CI_MERGE_REQUEST_TITLE: string + } + +type GitLabCIPredefinedVariables = { GITLAB_USER_NAME: string } & ( + | { + // this is used to be checked if the current job is in a CI environment + CI: undefined + } + | { + CI: 'true' + CI_PROJECT_PATH: string + CI_SERVER_URL: string + } +) diff --git a/src/utils.ts b/src/utils.ts index f0a7067a..960f7684 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -12,6 +12,8 @@ import remarkParse from 'remark-parse' import remarkStringify from 'remark-stringify' import { unified } from 'unified' +import { env } from './env.js' + export const BumpLevels = { dep: 0, patch: 1, @@ -156,7 +158,7 @@ export const getOptionalInput = (name: string) => getInput(name) || undefined export const getUsername = (api: Gitlab) => { return ( - process.env.GITLAB_CI_USER_NAME ?? + env.GITLAB_CI_USER_NAME ?? api.Users.showCurrentUser().then(currentUser => currentUser.username) ) }