Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .github/actions/publish-verified-package/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Publish and verify one stable package
description: Publish one package once and wait for its verified public archive.
inputs:
directory:
description: Release package workspace directory.
required: true
plan:
description: Version and dependency manifest from the checked-out release commit.
required: true
runs:
using: composite
steps:
- name: Check registry and prior upload attempt
id: inspect
shell: bash
run: node scripts/release-publish-plan.mjs inspect
env:
GH_TOKEN: ${{ github.token }}
STABLE_RELEASE_PLAN: ${{ inputs.plan }}
RELEASE_PACKAGE_DIR: ${{ inputs.directory }}
- name: Persist upload attempt before publishing
if: steps.inspect.outputs.publish == 'true'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: ${{ steps.inspect.outputs.marker }}
path: ${{ steps.inspect.outputs.receipt }}
if-no-files-found: error
- name: Publish exact package
if: steps.inspect.outputs.publish == 'true'
shell: bash
run: pnpm --dir "$RELEASE_PACKAGE_DIR" publish --json --no-git-checks --access public --tag latest
env:
RELEASE_PACKAGE_DIR: ${{ inputs.directory }}
npm_config_registry: https://registry.npmjs.org
- name: Verify exact public archive
shell: bash
run: node scripts/verify-release-artifacts.mjs
env:
PUBLISHED_PACKAGES: ${{ steps.inspect.outputs.package }}
95 changes: 89 additions & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
queue: max

env:
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
Expand All @@ -22,7 +22,13 @@ jobs:
permissions:
contents: write
id-token: write
actions: read
pull-requests: write
outputs:
published: ${{ steps.changesets.outputs.published == 'true' || steps.stable-tags.outputs.published == 'true' }}
packages: ${{ steps.changesets.outputs.publishedPackages }}
prerelease: ${{ steps.dist-tag.outputs.prerelease }}
latest: ${{ steps.dist-tag.outputs.latest }}
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
Expand Down Expand Up @@ -72,16 +78,93 @@ jobs:
uses: changesets/action@63a615b9cd06ba9a3e6d13796c7fbcb080a60a0b # v1.8.0
with:
version: pnpm run changeset:version
publish: pnpm run changeset:publish ${{ steps.dist-tag.outputs.tag && format('--tag {0}', steps.dist-tag.outputs.tag) }}
publish: ${{ github.ref_name != 'main' && format('pnpm run changeset:publish --tag {0}', steps.dist-tag.outputs.tag) || '' }}
title: 'ci: Version Packages'
commit: 'ci: changeset release'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Plan stable publication
id: stable-plan
if: github.ref_name == 'main' && steps.changesets.outputs.hasChangesets == 'false'
run: node scripts/release-publish-plan.mjs plan
- name: Publish and verify create
if: steps.stable-plan.outputs.active == 'true'
uses: ./.github/actions/publish-verified-package
with:
directory: packages/create
plan: ${{ steps.stable-plan.outputs.packages }}
- name: Publish and verify cli
if: steps.stable-plan.outputs.active == 'true'
uses: ./.github/actions/publish-verified-package
with:
directory: packages/cli
plan: ${{ steps.stable-plan.outputs.packages }}
- name: Publish and verify create-start-app
if: steps.stable-plan.outputs.active == 'true'
uses: ./.github/actions/publish-verified-package
with:
directory: cli-aliases/create-start-app
plan: ${{ steps.stable-plan.outputs.packages }}
- name: Publish and verify create-tanstack
if: steps.stable-plan.outputs.active == 'true'
uses: ./.github/actions/publish-verified-package
with:
directory: cli-aliases/create-tanstack
plan: ${{ steps.stable-plan.outputs.packages }}
- name: Publish and verify create-tanstack-app
if: steps.stable-plan.outputs.active == 'true'
uses: ./.github/actions/publish-verified-package
with:
directory: cli-aliases/create-tanstack-app
plan: ${{ steps.stable-plan.outputs.packages }}
- name: Publish and verify create-tsrouter-app
if: steps.stable-plan.outputs.active == 'true'
uses: ./.github/actions/publish-verified-package
with:
directory: cli-aliases/create-tsrouter-app
plan: ${{ steps.stable-plan.outputs.packages }}
- name: Publish and verify ts-create-start
if: steps.stable-plan.outputs.active == 'true'
uses: ./.github/actions/publish-verified-package
with:
directory: cli-aliases/ts-create-start
plan: ${{ steps.stable-plan.outputs.packages }}
- name: Tag verified stable packages
id: stable-tags
if: steps.stable-plan.outputs.active == 'true'
run: node scripts/release-publish-plan.mjs tags
env:
STABLE_RELEASE_PLAN: ${{ steps.stable-plan.outputs.packages }}
verify-release:
name: Verify Published Release
needs: release
if: needs.release.outputs.published == 'true'
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
persist-credentials: false
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 24
- name: Verify Package Availability and Integrity
if: needs.release.outputs.latest != 'true'
run: node scripts/verify-release-artifacts.mjs
env:
PUBLISHED_PACKAGES: ${{ needs.release.outputs.packages }}
- name: Create GitHub Release
if: steps.changesets.outputs.published == 'true'
run: node scripts/create-github-release.mjs ${PRERELEASE_FLAG} ${LATEST_FLAG}
run: |
gh auth setup-git
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
node scripts/create-github-release.mjs ${PRERELEASE_FLAG} ${LATEST_FLAG}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PRERELEASE_FLAG: ${{ steps.dist-tag.outputs.prerelease == 'true' && '--prerelease' }}
LATEST_FLAG: ${{ steps.dist-tag.outputs.latest == 'true' && '--latest' }}
PRERELEASE_FLAG: ${{ needs.release.outputs.prerelease == 'true' && '--prerelease' }}
LATEST_FLAG: ${{ needs.release.outputs.latest == 'true' && '--latest' }}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"build": "nx run-many --target=build --parallel 5",
"dev": "nx run-many --target=dev --parallel 20",
"test": "pnpm test:unit && pnpm test:e2e",
"test:unit": "nx run-many --target=test",
"test:unit": "nx run-many --target=test && node --test scripts/release-*.test.mjs",
"test:e2e": "nx run @tanstack/cli:test:e2e",
"test:e2e:matrix": "nx run @tanstack/cli:test:e2e:matrix",
"check-outdated": "node scripts/check-outdated-packages.js",
Expand Down
87 changes: 86 additions & 1 deletion scripts/create-github-release.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { execFileSync, execSync } from 'node:child_process'
import fs, { globSync } from 'node:fs'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { findReleaseForCommit, latestReleaseFlag } from './release-identity.mjs'

const rootDir = path.join(import.meta.dirname, '..')
const ghToken = process.env.GH_TOKEN || process.env.GITHUB_TOKEN
Expand Down Expand Up @@ -64,6 +65,57 @@ const releaseLogs = execSync(
const currentRelease = releaseLogs[0] || 'HEAD'
const previousRelease = releaseLogs[1]

const releaseHead = execFileSync('git', ['rev-parse', 'HEAD'], {
encoding: 'utf8',
}).trim()
const releases = JSON.parse(
execFileSync(
'gh',
[
'api',
'--paginate',
'--slurp',
'repos/TanStack/cli/releases?per_page=100',
],
{ encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 },
),
).flat()
const resolveTagCommit = (tag) => {
try {
return execFileSync('git', ['rev-parse', '--verify', `${tag}^{commit}`], {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore'],
}).trim()
} catch {
return execFileSync(
'gh',
[
'api',
`repos/TanStack/cli/commits/${encodeURIComponent(tag)}`,
'--jq',
'.sha',
],
{ encoding: 'utf8' },
).trim()
}
}
const existingRelease = await findReleaseForCommit(
releases,
releaseHead,
resolveTagCommit,
)
if (existingRelease) {
if (existingRelease.draft) {
throw new Error(
`A draft release already exists for ${releaseHead}: ${existingRelease.html_url}`,
)
}
console.info(
`Release already exists for ${releaseHead}: ${existingRelease.html_url}`,
)
process.exit(0)
}

const bumpedPackages = []
for (const workspaceDir of workspaceDirs) {
const absWorkspaceDir = path.join(rootDir, workspaceDir)
Expand Down Expand Up @@ -224,6 +276,40 @@ ${changelogMd}
${bumpedPackages.map((pkg) => `- ${pkg.name}@${pkg.version}`).join('\n')}
`

let latestHead
if (
isLatest &&
releases.some((release) => !release.draft && !release.prerelease)
) {
const latestRelease = JSON.parse(
execFileSync('gh', ['api', 'repos/TanStack/cli/releases/latest'], {
encoding: 'utf8',
}),
)
latestHead = resolveTagCommit(latestRelease.tag_name)
}
const latestOption = latestReleaseFlag({
requested: process.argv.includes('--latest=false')
? 'false'
: isLatest
? 'true'
: undefined,
releaseHead,
latestHead,
isAncestor: (older, newer) => {
try {
execFileSync('git', ['merge-base', '--is-ancestor', older, newer], {
stdio: 'ignore',
})
return true
} catch (error) {
if (error.status === 1) return false
throw error
}
},
})
const latestFlag = latestOption ? ` ${latestOption}` : ''

let tagExists = false
try {
execSync(`git rev-parse ${tagName}`, { stdio: 'ignore' })
Expand All @@ -238,7 +324,6 @@ if (!tagExists) {
}

const prereleaseFlag = isPrerelease ? '--prerelease' : ''
const latestFlag = isLatest ? ' --latest' : ''
const tmpFile = path.join(tmpdir(), `release-notes-${tagName}.md`)
fs.writeFileSync(tmpFile, body)

Expand Down
Loading
Loading