Skip to content

Commit

Permalink
feat: overhaul dotnetGHPR; misc refactors
Browse files Browse the repository at this point in the history
- expose tokenCanWritePackages
- add isTokenDefined
- add getNugetGitHubUrl
- deprecate nugetGitHubUrl
- refactor console.error
- refactor process.env
  • Loading branch information
BinToss committed Jun 4, 2024
1 parent 57d881a commit 8d0a1d6
Show file tree
Hide file tree
Showing 6 changed files with 247 additions and 62 deletions.
92 changes: 63 additions & 29 deletions src/dotnet/dotnetGHPR.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import type { NuGetRegistryInfo } from './dotnetHelpers.js';
import { env } from 'node:process'

/**
* @todo support custom base URL for private GitHub instances
* @param tokenEnvVar The name of the environment variable containing the NUGET token
* @returns `true` if the token is
* @throws
* @throws
* - TypeError: The environment variable ${tokenEnvVar} is undefined!
* - Error: The value of the token in ${tokenEnvVar} begins with 'github_pat_' which means it's a Fine-Grained token. At the time of writing, GitHub Fine-Grained tokens cannot push packages. If you believe this is statement is outdated, report the issue at https://github.com/halospv3/hce.shared/issues/new. For more information, see https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-nuget-registry.
* - Error:
* - The value of the token in ${tokenEnvVar} begins with 'github_pat_' which means it's a Fine-Grained token. At the time of writing, GitHub Fine-Grained tokens cannot push packages. If you believe this is statement is outdated, report the issue at https://github.com/halospv3/hce.shared/issues/new. For more information, see https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-nuget-registry.
* - The GitHub API response header lacked "x-oauth-scopes". This indicates the token we provided is not a workflow token nor a Personal Access Token (classic) and can never have permission to push packages.
*/
async function tokenCanWritePackages(tokenEnvVar: string) {
const tokenValue = process.env[tokenEnvVar];
export async function tokenCanWritePackages(tokenEnvVar: string) {
const tokenValue = env[tokenEnvVar];
if (tokenValue === undefined)
throw new TypeError(`The environment variable ${tokenEnvVar} is undefined!`)

Expand All @@ -30,39 +33,63 @@ async function tokenCanWritePackages(tokenEnvVar: string) {
throw new Error('GitHub API response header lacked "x-oauth-scopes". This indicates the token we provided is not a workflow token nor a Personal Access Token (classic) and can never have permission to push packages.')
}

const { GITHUB_REPOSITORY_OWNER } = process.env;
/** returns the value of {@link env.GITHUB_REPOSITORY_OWNER} */
function getOwner(): string | undefined {
return env.GITHUB_REPOSITORY_OWNER;
}

export const nugetGitHubUrlBase = 'https://nuget.pkg.github.com';
export const nugetGitHubUrl = GITHUB_REPOSITORY_OWNER
? `${nugetGitHubUrlBase}/${GITHUB_REPOSITORY_OWNER}/index.json`
: undefined;

/** @deprecated use {@link getNugetGitHubUrl()} instead. */
export const nugetGitHubUrl: string | undefined = getNugetGitHubUrl();

export function getNugetGitHubUrl() {
const owner = getOwner();
if (owner)
return `${nugetGitHubUrlBase}/${owner}/index.json`;
return undefined;
}

/**
* If tokenEnvVar is NOT 'GITHUB_TOKEN', then test if the token is defined and return the boolean.
* Else If tokenEnvVar is 'GITHUB_TOKEN' and defined, then return true.
* Else If tokenEnvVar is 'GITHUB_TOKEN' and undefined, then set tokenEnvVar to 'GH_TOKEN', test if GH_TOKEN is defined, and return the boolean.
* @param tokenEnvVar the name of the environment variable with the token's value. Defaults to 'GITHUB_TOKEN'. If environment variable 'GITHUB_TOKEN' is undefined, falls back to 'GH_TOKEN'.
* @returns `{isDefined: true}` if the token is defined. Else, if tokenEnvVar is 'GITHUB_TOKEN' (default) and token is defined, returns `true`. Else, if 'GH_TOKEN' is defined, returns `true`. Else, returns `false`
*/
export function isTokenDefined(tokenEnvVar = 'GITHUB_TOKEN'): { isDefined: boolean, fallback?: string } {
if (tokenEnvVar !== 'GITHUB_TOKEN')
return {
isDefined: (env[tokenEnvVar /* custom */] !== undefined && env[tokenEnvVar /* custom */] !== 'undefined')
};
else if (env[tokenEnvVar /* GITHUB_TOKEN */] !== undefined && env[tokenEnvVar /* GITHUB_TOKEN */] !== 'undefined')
return {
isDefined: true
};
else return {
isDefined: (env[tokenEnvVar = 'GH_TOKEN'] !== undefined && env[tokenEnvVar] !== 'undefined'),
fallback: 'GH_TOKEN'
};
}

/**
* Get a {@link NuGetRegistryInfo} for pushing to your GitHub Packages NuGet registry.
* todo: add support for private, custom GitHub instances. Token is only validated against github.com.
* @export
* @param {string} [tokenEnvVar="GITHUB_TOKEN"] The name of environment variable storing the GitHub Packages NuGet registry API key. Defaults to `"GITHUB_TOKEN"`;
* @param {string} [url=tokenEnvVar] The url of the GitHub Packages NuGet registry. Defaults to {@link nugetGitHubUrl}.
* @param {string | 'GITHUB_TOKEN' | 'GH_TOKEN'} [tokenEnvVar="GITHUB_TOKEN"] The name of environment variable storing the GitHub Packages NuGet registry API key. Defaults to `"GITHUB_TOKEN"`. If GITHUB_TOKEN is undefined, fallback to GH_TOKEN;
* @param {string} [url=tokenEnvVar] The url of the GitHub Packages NuGet registry. Defaults to return value of {@link getNugetGitHubUrl()}.
* @returns {(NuGetRegistryInfo | undefined)} a {@link NuGetRegistryInfo} object if {@link tokenEnvVar} and {@link url} are defined. Else, `undefined`.
* note: `url` defaults to job's repository owner's GitHub registry in GitHub Actions workflow. If GITHUB_REPOSITORY_OWNER is not defined, then an error will be logged and `undefined` will be returned.
*/
export async function getGithubNugetRegistryPair(
tokenEnvVar = 'GITHUB_TOKEN',
url: string | undefined = nugetGitHubUrl,
tokenEnvVar: string | 'GITHUB_TOKEN' | 'GH_TOKEN' = 'GITHUB_TOKEN',
url: string | undefined = getNugetGitHubUrl(),
): Promise<NuGetRegistryInfo | undefined> {
const errors: Error[] = [];
// yes, this is stupid. No, I won't change it.
const isTokenDefined = process.env[tokenEnvVar] !== undefined;
const isUrlDefined = url !== undefined;
let canTokenWritePackages = false;
const _isTokenDefinedInfo = isTokenDefined(tokenEnvVar);
let canTokenWritePackages = undefined;

if (!isTokenDefined)
errors.push(
new Error(
`The environment variable ${tokenEnvVar} was specified as the source of the token to push a NuGet package to GitHub, but the environment variable does not exist.`,
),
);
if (!isUrlDefined) {
if ((url ??= getNugetGitHubUrl()) === undefined) {
errors.push(
new Error(
'The url for the GitHub Packages NuGet registry was undefined.\n' +
Expand All @@ -72,7 +99,9 @@ export async function getGithubNugetRegistryPair(
);
}

if (isTokenDefined) {
if (_isTokenDefinedInfo.isDefined) {
if (_isTokenDefinedInfo.fallback)
tokenEnvVar = _isTokenDefinedInfo.fallback;
try {
canTokenWritePackages = await tokenCanWritePackages(tokenEnvVar);
}
Expand All @@ -83,8 +112,13 @@ export async function getGithubNugetRegistryPair(
errors.push(new Error(String(err)));
}
}
else {
const errMsg = `The environment variable ${tokenEnvVar} was specified as the source of the token to push a NuGet package to GitHub, but the environment variable does not exist.`
+ `${_isTokenDefinedInfo.fallback === undefined ? '' : ` The fallback environment variable ${_isTokenDefinedInfo.fallback} is also undefined.`}`;
errors.push(new Error(errMsg));
}

if (!canTokenWritePackages) {
if (canTokenWritePackages === false) {
// yes, this is a critical error that should be fixed before Semantic Release can succeed.
// yes, this is incredibly irritating to deal with in local runs.
errors.push(
Expand All @@ -95,13 +129,13 @@ export async function getGithubNugetRegistryPair(
}

// conditions checked so `url` is certainly defined
if (isTokenDefined && isUrlDefined && canTokenWritePackages)
if (_isTokenDefinedInfo.isDefined && url !== undefined && canTokenWritePackages)
return { tokenEnvVar, url };

const aggErr = new Error(`One more more errors occurred when getting GHPR url-token pair. Errors:\n${errors.map(v => v.message).join('\n')}`);
const aggErr = new Error(`One more more errors occurred when getting GHPR url-token pair. Errors:\n${errors.map(v => v.stack).join('\n')}`);

if (process.env['SKIP_TOKEN'] === 'true') {
console.error(aggErr.message)
if (env['SKIP_TOKEN'] === 'true' && aggErr.message.length > 0) {
console.error('WARN: errors were thrown, but SKIP_TOKEN is defined.\n' + aggErr.stack)
return undefined;
}
throw aggErr;
Expand Down
10 changes: 5 additions & 5 deletions src/dotnet/dotnetGLPR.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { error } from 'node:console';
import type { NuGetRegistryInfo } from './dotnetHelpers.js';
import { env } from "node:process"

const { CI_API_V4_URL, CI_PROJECT_ID } = process.env;
const { CI_API_V4_URL, CI_PROJECT_ID } = env;
const nameof = {
CI_API_V4_URL: 'CI_API_V4_URL',
CI_PROJECT_ID: 'CI_PROJECT_ID',
Expand All @@ -26,8 +26,8 @@ export function getGitlabNugetRegistryPair(
url: string | undefined = nugetGitLabUrl,
): NuGetRegistryInfo | undefined {
// yes, this is stupid. No, I won't change it.
if (!process.env[tokenEnvVar]) {
error(
if (!env[tokenEnvVar]) {
console.error(
new Error(
`The environment variable ${tokenEnvVar} was specified as the source of the token to push a NuGet package to GitLab, but the environment variable does not exist.`,
),
Expand All @@ -37,7 +37,7 @@ export function getGitlabNugetRegistryPair(
if (url) {
return { tokenEnvVar, url };
}
error(
console.error(
new Error(
`The environment variables ${nameof.CI_API_V4_URL} or ${nameof.CI_PROJECT_ID} do not exist and a custom GitLab Packages NuGet registry URL was not provided.`,
),
Expand Down
5 changes: 3 additions & 2 deletions src/dotnet/dotnetHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ok } from 'node:assert/strict';
import { getGithubNugetRegistryPair, nugetGitHubUrlBase } from './dotnetGHPR.js';
import { getGitlabNugetRegistryPair } from './dotnetGLPR.js';
import { MSBuildProject, MSBuildProjectPreDefinedProperties } from './MSBuildProject.js';
import { env } from 'node:process';


function formatDotnetPublish(projectsToPublish: string[], publishProperties: string[]): string {
Expand Down Expand Up @@ -175,8 +176,8 @@ export async function configureDotnetNugetPush(
return registries
.map(
(registry) => {
const tokenValue = process.env[registry.tokenEnvVar];
ok(process.env['SKIP_TOKEN'] === 'true' || tokenValue, `The environment variable ${registry.tokenEnvVar} is undefined!`);
const tokenValue = env[registry.tokenEnvVar];
ok(env['SKIP_TOKEN'] === 'true' || tokenValue, `The environment variable ${registry.tokenEnvVar} is undefined!`);
`dotnet nuget push ${nupkgDir} --source ${registry.url} --token ${tokenValue ?? '**placeholder**'}`
}
)
Expand Down
5 changes: 3 additions & 2 deletions src/semanticReleaseConfigDotnet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { baseConfig, defaultPlugins } from './semanticReleaseConfig.js';
import { setupGitPluginSpec } from './setupGitPluginSpec.js';
import debug from './debug.js'
import { inspect } from 'node:util';
import { env } from "node:process"

/**
* TODO: options/params for inserts/edits. NOT ready for production. Currently, this can only add Git plugin's options if undefined or one or more is missing.
Expand Down Expand Up @@ -90,15 +91,15 @@ export function getConfig(projectsToPublish: string[] = [], projectsToPackAndPus
const errors: Error[] = [];

if (projectsToPublish.length === 0) {
const _ = process.env["PROJECTS_TO_PUBLISH"];
const _ = env["PROJECTS_TO_PUBLISH"];
if (_ === undefined)
errors.push(new Error("projectsToPublish.length must be > 0 or PROJECTS_TO_PUBLISH must be defined and contain at least one path."));
else
projectsToPublish = _.split(';');
}

if (projectsToPackAndPush !== false && projectsToPackAndPush.length === 0) {
const _ = process.env["PROJECTS_TO_PACK_AND_PUSH"]
const _ = env["PROJECTS_TO_PACK_AND_PUSH"]
if (_ === undefined)
errors.push(new Error("projectsToPackAndPush.length must be > 0 or PROJECTS_TO_PACK_AND_PUSH must be defined and contain at least one path."));
else
Expand Down
5 changes: 3 additions & 2 deletions tests/configDotnet.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { describe, it } from 'node:test';
import { appendPlugins, getConfig, insertAndEditPlugins } from '@halospv3/hce.shared-config/semanticReleaseConfigDotnet';
import { notDeepStrictEqual, ok, strictEqual } from 'node:assert';
import { fileSync, setGracefulCleanup } from 'tmp'
import { fileSync, setGracefulCleanup } from 'tmp';
import { unlinkSync, writeFileSync } from 'node:fs';
import { env } from 'node:process';

await describe('configDotnet', async () => {
await describe('appendPlugins', () => {
Expand Down Expand Up @@ -37,7 +38,7 @@ await describe('configDotnet', async () => {


await it('does not throw when projectToPackAndPush contains at least one item', () => {
process.env['SKIP_TOKEN'] = 'true';
env['SKIP_TOKEN'] = 'true';
const actual = (() => {
setGracefulCleanup();
const tmpProj = fileSync({ postfix: '.csproj', discardDescriptor: true });
Expand Down
Loading

0 comments on commit 8d0a1d6

Please sign in to comment.