diff --git a/.github/workflows/publish-dev.yml b/.github/workflows/publish-dev.yml index a0f8d5b7496d..ff169c30dae5 100644 --- a/.github/workflows/publish-dev.yml +++ b/.github/workflows/publish-dev.yml @@ -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 }} diff --git a/packages/actions/src/releasePackages/action.yml b/packages/actions/src/releasePackages/action.yml index 1e495ebcdb4e..f7b767700b95 100644 --- a/packages/actions/src/releasePackages/action.yml +++ b/packages/actions/src/releasePackages/action.yml @@ -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 }} diff --git a/packages/actions/src/releasePackages/generateReleaseTree.ts b/packages/actions/src/releasePackages/generateReleaseTree.ts index c0f187f97573..193f473e4051 100644 --- a/packages/actions/src/releasePackages/generateReleaseTree.ts +++ b/packages/actions/src/releasePackages/generateReleaseTree.ts @@ -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); diff --git a/packages/actions/src/releasePackages/index.ts b/packages/actions/src/releasePackages/index.ts index 7a70900e9daf..a8caa2495591 100644 --- a/packages/actions/src/releasePackages/index.ts +++ b/packages/actions/src/releasePackages/index.ts @@ -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 to use for dev releases (defaults to "dev")', getInput('tag')) .parse(); -const { exclude, dry, dev } = program.opts<{ dev: boolean; dry: boolean; exclude: 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(); } diff --git a/packages/actions/src/releasePackages/releasePackage.ts b/packages/actions/src/releasePackages/releasePackage.ts index 6b911136bf22..ee7d8a9d2a3a 100644 --- a/packages/actions/src/releasePackages/releasePackage.ts +++ b/packages/actions/src/releasePackages/releasePackage.ts @@ -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); } }