ci: implement workflow to publish dev versions (#11120)

* ci: implement workflow to publish dev versions

* ci: refactor into the other dev job

* fix: use dev tag

* chore: clarify

* fix: always use actions from main

* fix: conditionally

* chore: don't ask for meaningless perm
This commit is contained in:
Denis-Adrian Cristea
2025-10-08 14:08:39 +03:00
committed by GitHub
parent fcf7f27fd7
commit 0c2975e3fd
5 changed files with 93 additions and 21 deletions

View File

@@ -4,6 +4,14 @@ on:
- cron: '0 */12 * * *'
workflow_dispatch:
inputs:
pull:
description: 'The pull number to check out'
required: false
default: 'main'
tag:
description: 'The tag to use, generally a feature name'
required: false
type: string
dry_run:
description: 'Perform a dry run that skips publishing and outputs logs indicating what would have happened'
type: boolean
@@ -19,10 +27,27 @@ jobs:
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
if: github.repository_owner == 'discordjs'
steps:
- uses: actions/create-github-app-token@v2
id: app-token
with:
app-id: ${{ vars.DISCORDJS_APP_ID }}
private-key: ${{ secrets.DISCORDJS_APP_KEY_RELEASE }}
- name: Decide ref
id: ref
run: |
if [ -n "${{ github.event.inputs.pull }}" ]; then
echo "ref=refs/pull/${{ github.event.inputs.pull }}/head" >> $GITHUB_OUTPUT
else
echo "ref=refs/heads/main" >> $GITHUB_OUTPUT
fi
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 0
token: ${{ steps.app-token.outputs.token }}
ref: ${{ steps.ref.outputs.ref }}
- name: Install Node.js v22
uses: actions/setup-node@v5
@@ -37,12 +62,42 @@ jobs:
- name: Build dependencies
run: pnpm run build
- name: Publish packages
- name: Checkout main repository (non-main ref)
if: ${{ steps.ref.outputs.ref != 'refs/heads/main' }}
uses: actions/checkout@v5
with:
path: 'main'
- name: Install action deps (non-main ref)
if: ${{ steps.ref.outputs.ref != 'refs/heads/main' }}
shell: bash
working-directory: ./main
env:
COREPACK_ENABLE_STRICT: 0
run: |
pnpm self-update 10
pnpm install --filter @discordjs/actions --frozen-lockfile --prefer-offline --loglevel error
- name: Publish packages (non-main ref)
if: ${{ steps.ref.outputs.ref != 'refs/heads/main' }}
uses: ./main/packages/actions/src/releasePackages
with:
exclude: '@discordjs/docgen'
dry: ${{ inputs.dry_run }}
dev: true
tag: ${{ inputs.tag || 'dev' }}
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Publish packages (main ref)
if: ${{ steps.ref.outputs.ref == 'refs/heads/main' }}
uses: ./packages/actions/src/releasePackages
with:
exclude: '@discordjs/docgen'
dry: ${{ inputs.dry_run }}
dev: true
tag: ${{ inputs.tag || 'dev' }}
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -11,6 +11,8 @@ inputs:
description: 'The published name of a single package to release'
exclude:
description: 'Comma separated list of packages to exclude from release (if not depended upon)'
tag:
description: 'The tag to use, generally a feature name'
runs:
using: composite
steps:
@@ -22,3 +24,4 @@ runs:
INPUT_DRY: ${{ inputs.dry }}
INPUT_PACKAGE: ${{ inputs.package }}
INPUT_EXCLUDE: ${{ inputs.exclude }}
INPUT_TAG: ${{ inputs.tag }}

View File

@@ -26,9 +26,9 @@ export interface ReleaseEntry {
version: string;
}
async function fetchDevVersion(pkg: string) {
async function fetchDevVersion(pkg: string, tag: string) {
try {
const res = await fetch(`https://registry.npmjs.org/${pkg}/dev`);
const res = await fetch(`https://registry.npmjs.org/${pkg}/${tag}`);
if (!res.ok) return null;
const packument = (await res.json()) as PackumentVersion;
return packument.version;
@@ -37,7 +37,7 @@ async function fetchDevVersion(pkg: string) {
}
}
async function getReleaseEntries(dev: boolean, dry: boolean) {
async function getReleaseEntries(dry: boolean, devTag?: string) {
const releaseEntries: ReleaseEntry[] = [];
const packageList: pnpmTree[] =
await $`pnpm list --recursive --only-projects --filter {packages/\*} --prod --json`.json();
@@ -57,8 +57,8 @@ async function getReleaseEntries(dev: boolean, dry: boolean) {
version: pkg.version,
};
if (dev) {
const devVersion = await fetchDevVersion(pkg.name);
if (devTag) {
const devVersion = await fetchDevVersion(pkg.name, devTag);
if (devVersion?.endsWith(commitHash)) {
// Write the currently released dev version so when pnpm publish runs on dependents they depend on the dev versions
if (dry) {
@@ -72,9 +72,9 @@ async function getReleaseEntries(dev: boolean, dry: boolean) {
release.version = devVersion;
} else if (dry) {
info(`[DRY] Bumping ${pkg.name} via git-cliff.`);
release.version = `${pkg.version}.DRY-dev.${timestamp}-${commitHash}`;
release.version = `${pkg.version}.DRY-${devTag}.${timestamp}-${commitHash}`;
} else {
await $`pnpm --filter=${pkg.name} run release --preid "dev.${timestamp}-${commitHash}" --skip-changelog`;
await $`pnpm --filter=${pkg.name} run release --preid "${devTag}.${timestamp}-${commitHash}" --skip-changelog`;
// Read again instead of parsing the output to be sure we're matching when checking against npm
const pkgJson = (await file(`${pkg.path}/package.json`).json()) as PackageJSON;
release.version = pkgJson.version;
@@ -129,8 +129,8 @@ async function getReleaseEntries(dev: boolean, dry: boolean) {
return releaseEntries;
}
export async function generateReleaseTree(dev: boolean, dry: boolean, packageName?: string, exclude?: string[]) {
let releaseEntries = await getReleaseEntries(dev, dry);
export async function generateReleaseTree(dry: boolean, devTag?: string, packageName?: string, exclude?: string[]) {
let releaseEntries = await getReleaseEntries(dry, devTag);
// Try to early return if the package doesn't have deps
if (packageName && packageName !== 'all') {
const releaseEntry = releaseEntries.find((entry) => entry.name === packageName);

View File

@@ -30,15 +30,29 @@ program
)
.option('--dry', 'skips actual publishing and outputs logs instead', dryInput)
.option('--dev', 'publishes development versions and skips tagging / github releases', devInput)
.option('--tag <tag>', 'tag to use for dev releases (defaults to "dev")', getInput('tag'))
.parse();
const { exclude, dry, dev } = program.opts<{ dev: boolean; dry: boolean; exclude: string[] }>();
const [packageName] = program.processedArgs as [string];
const {
exclude,
dry,
dev,
tag: inputTag,
} = program.opts<{ dev: boolean; dry: boolean; exclude: string[]; tag: string }>();
// All this because getInput('tag') will return empty string when not set :P
if (!dev && inputTag.length) {
throw new Error('The --tag option can only be used with --dev');
}
const tag = inputTag.length ? inputTag : dev ? 'dev' : undefined;
const [packageName] = program.processedArgs as [string];
const tree = await generateReleaseTree(dry, tag, packageName, exclude);
const tree = await generateReleaseTree(dev, dry, packageName, exclude);
for (const branch of tree) {
startGroup(`Releasing ${branch.map((entry) => `${entry.name}@${entry.version}`).join(', ')}`);
await Promise.all(branch.map(async (release) => releasePackage(release, dev, dry)));
await Promise.all(branch.map(async (release) => releasePackage(release, dry, tag)));
endGroup();
}

View File

@@ -41,7 +41,7 @@ async function gitTagAndRelease(release: ReleaseEntry, dry: boolean) {
}
}
export async function releasePackage(release: ReleaseEntry, dev: boolean, dry: boolean, doGitRelease = !dev) {
export async function releasePackage(release: ReleaseEntry, dry: boolean, devTag?: string, doGitRelease = !devTag) {
// Sanity check against the registry first
if (await checkRegistry(release)) {
info(`${release.name}@${release.version} already published, skipping.`);
@@ -51,11 +51,11 @@ export async function releasePackage(release: ReleaseEntry, dev: boolean, dry: b
if (dry) {
info(`[DRY] Releasing ${release.name}@${release.version}`);
} else {
await $`pnpm --filter=${release.name} publish --provenance --no-git-checks ${dev ? '--tag=dev' : ''}`;
await $`pnpm --filter=${release.name} publish --provenance --no-git-checks ${devTag ? `--tag=${devTag}` : ''}`;
}
// && !dev just to be sure
if (doGitRelease && !dev) await gitTagAndRelease(release, dry);
// && !devTag just to be sure
if (doGitRelease && !devTag) await gitTagAndRelease(release, dry);
if (dry) return;
@@ -77,9 +77,9 @@ export async function releasePackage(release: ReleaseEntry, dev: boolean, dry: b
}, 15_000);
});
if (dev) {
if (devTag) {
// Send and forget, deprecations are less important than releasing other dev versions and can be done manually
void $`pnpm exec npm-deprecate --name "*dev*" --message "This version is deprecated. Please use a newer version." --package ${release.name}`
void $`pnpm exec npm-deprecate --name "*${devTag}*" --message "This version is deprecated. Please use a newer version." --package ${release.name}`
.nothrow()
// eslint-disable-next-line promise/prefer-await-to-then
.then(() => {});
@@ -90,6 +90,6 @@ export async function releasePackage(release: ReleaseEntry, dev: boolean, dry: b
await $`pnpm --filter=create-discord-bot run rename-to-app`;
// eslint-disable-next-line require-atomic-updates
release.name = 'create-discord-app';
await releasePackage(release, dev, dry, false);
await releasePackage(release, dry, devTag, false);
}
}