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
38 changes: 36 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ on:
- nightly
- latest
- experimental
- shopify-dev-tools

concurrency:
group: changeset-${{ github.head_ref || github.run_id }}
Expand Down Expand Up @@ -183,10 +184,43 @@ jobs:
-f make_latest=legacy >/dev/null
echo "Created release $TAG"

# Manual/Cron release job - runs on schedule or manual trigger with tag
shopify-dev-tools-release:
name: Publish @shopify/shopify-dev-tools
if: ${{ github.ref == 'refs/heads/main' && (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.tag == 'shopify-dev-tools')) }}
runs-on: ubuntu-latest
permissions:
contents: read
concurrency:
group: publish-shopify-dev-tools-to-cloudsmith
cancel-in-progress: false
env:
CLOUDSMITH_NPM_TOKEN_RO: ${{ secrets.CLOUDSMITH_NPM_TOKEN_RO }}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup deps
uses: ./.github/actions/setup-cli-deps
with:
node-version: 24.12.0
- name: Resolve @shopify/shopify-dev-tools publish state
id: publish-state
run: pnpm resolve-package-publish-state packages/shopify-dev-tools/package.json https://npm.shopify.io/node/
- name: Trigger Cloudsmith publish
if: steps.publish-state.outputs.should_publish == 'true'
uses: buildkite/trigger-pipeline-action@909fed762c73d5ae2b5d555ab910d66b3fae2670 # v2.4.1
with:
buildkite_api_access_token: ${{ secrets.BUILDKITE_API_TOKEN }}
pipeline: shopify/cli-shopify-dev-tools-publish-package
branch: main
commit: ${{ github.sha }}
message: 'Publish @shopify/shopify-dev-tools@${{ steps.publish-state.outputs.version }}'
build_env_vars: '{"SHOPIFY_DEV_TOOLS_RELEASE":"true"}'

# Manual/Cron release job - runs on schedule or manual trigger with a CLI release tag
manual-cron-release:
name: Manual & Cron Release
if: ${{ github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.event.inputs.tag != '') }}
if: ${{ github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.event.inputs.tag != 'shopify-dev-tools') }}
runs-on: ubuntu-latest
permissions:
contents: read
Expand Down
1 change: 1 addition & 0 deletions .shopify-build/VERSION
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
v2
29 changes: 29 additions & 0 deletions .shopify-build/cli-shopify-dev-tools-publish-package.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Register once this configuration is on main:
# @spy build setup cli-shopify-dev-tools-publish-package repo=Shopify/cli pr=false
# GitHub activity should remain disabled; .github/workflows/release.yml triggers releases.

containers:
default:
build:
from: ubuntu-24
node: 24.12.0
node_package_manager: pnpm
type: ci

steps:
- label: Publish @shopify/shopify-dev-tools to Cloudsmith
if: build.branch == "main" && build.env("SHOPIFY_DEV_TOOLS_RELEASE") == "true"
timeout: 10m
run:
- pnpm: ~
- pnpm --filter @shopify/shopify-dev-tools build
- mkdir -p /tmp/shopify-dev-tools-package
- pnpm --filter @shopify/shopify-dev-tools pack --pack-destination /tmp/shopify-dev-tools-package
publish:
package:
repository_name: node
package_type: npm
fail_on_duplicate: false
artifacts:
- path: /tmp/shopify-dev-tools-package
patterns: ['*.tgz']
8 changes: 8 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,11 @@ Before removing or incompatibly changing a stable interface:
2. Ship the deprecation notice in a **minor** release — this starts the clock.
3. Wait at least one minor release cycle before removing it.
4. Ship the removal in a **major** release with a migration guide in the release notes.

### Releasing `@shopify/shopify-dev-tools`

`@shopify/shopify-dev-tools` is a private package with a version independent from the fixed-version Shopify CLI packages. User-facing changes to it should include a changeset that targets only `@shopify/shopify-dev-tools`.

After the changeset is merged, the normal **Version Packages** PR bumps its version. Merging that PR to `main` makes the release workflow check Shopify's internal Cloudsmith registry and, when that version is missing, trigger the [`cli-shopify-dev-tools-publish-package`](https://buildkite.com/shopify/cli-shopify-dev-tools-publish-package) pipeline. The package remains private and is never published to the public npm registry.

To retry an interrupted publish, manually run the **Release** workflow on `main` with the `shopify-dev-tools` target. The registry check makes retries idempotent.
133 changes: 133 additions & 0 deletions bin/resolve-package-publish-state.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
#!/usr/bin/env node

import {execFileSync} from 'node:child_process'
import {appendFileSync, readFileSync} from 'node:fs'
import {relative, resolve, sep} from 'node:path'

const [packageJsonPath, registryUrl] = process.argv.slice(2)

if (!packageJsonPath || !registryUrl) {
throw new Error('Usage: resolve-package-publish-state.js <package-json-path> <registry-url>')
}

const githubOutput = process.env.GITHUB_OUTPUT

if (!githubOutput) {
throw new Error('GITHUB_OUTPUT must be set')
}

const eventName = process.env.GITHUB_EVENT_NAME ?? 'unknown'
const gitPackageJsonPath = relative(process.cwd(), resolve(packageJsonPath)).split(sep).join('/')

if (gitPackageJsonPath.startsWith('../')) {
throw new Error('The package.json must be inside the repository')
}

function readPackageJson(content) {
const packageJson = JSON.parse(content)

if (typeof packageJson.name !== 'string' || typeof packageJson.version !== 'string') {
throw new Error(`${packageJsonPath} must have string name and version fields`)
}

return packageJson
}

function readPackageJsonAt(ref) {
return readPackageJson(
execFileSync('git', ['show', `${ref}:${gitPackageJsonPath}`], {
encoding: 'utf8',
}),
)
}

function readPreviousPackageJson() {
try {
return readPackageJsonAt('HEAD~1')
} catch {
return undefined
}
}

async function versionExists(packageJson) {
const packageUrl = new URL(
encodeURIComponent(packageJson.name),
registryUrl.endsWith('/') ? registryUrl : `${registryUrl}/`,
)
const headers = {
accept: 'application/vnd.npm.install-v1+json',
}

if (packageUrl.hostname === 'npm.shopify.io') {
const cloudsmithToken = process.env.CLOUDSMITH_NPM_TOKEN_RO

if (!cloudsmithToken) {
throw new Error(`CLOUDSMITH_NPM_TOKEN_RO must be set to look up ${packageJson.name} in ${registryUrl}`)
}

headers.authorization = `Bearer ${cloudsmithToken}`
}

const response = await fetch(packageUrl, {
headers,
signal: AbortSignal.timeout(30_000),
})

if (response.status === 404) return false

if (!response.ok) {
throw new Error(
`Registry lookup failed for ${packageJson.name} at ${packageUrl}: ${response.status} ${response.statusText}`,
)
}

const metadata = await response.json()
return Boolean(metadata.versions?.[packageJson.version])
}

function setOutput(name, value) {
appendFileSync(githubOutput, `${name}=${value}\n`)
}

async function main() {
const currentPackageJson = readPackageJson(readFileSync(packageJsonPath, 'utf8'))
const previousPackageJson = readPreviousPackageJson()
const bumped = previousPackageJson ? previousPackageJson.version !== currentPackageJson.version : true
const manuallyDispatched = eventName === 'workflow_dispatch'
const shouldCheckRegistry = bumped || manuallyDispatched
const published = shouldCheckRegistry ? await versionExists(currentPackageJson) : false
const shouldPublish = shouldCheckRegistry && !published
const tag = `${currentPackageJson.name}@${currentPackageJson.version}`

if (previousPackageJson) {
console.log(
bumped
? `${currentPackageJson.name} version bumped: ${previousPackageJson.version} -> ${currentPackageJson.version}`
: `${currentPackageJson.name} version unchanged at ${currentPackageJson.version}`,
)
} else {
console.log(`${currentPackageJson.name} has no HEAD~1 package.json to compare; treating version as changed`)
}

if (!shouldCheckRegistry) {
console.log(`Skipping the registry lookup because ${tag} was not bumped by this commit.`)
} else {
console.log(
published
? `${tag} already exists in ${registryUrl}; skipping publish.`
: `${tag} is missing from ${registryUrl}; publish should run.`,
)
}

setOutput('bumped', bumped)
setOutput('published', published)
setOutput('should_publish', shouldPublish)
setOutput('name', currentPackageJson.name)
setOutput('version', currentPackageJson.version)
setOutput('tag', tag)
}

main().catch((error) => {
console.error(error)
process.exitCode = 1
})
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"refresh-manifests": "nx run-many --target=refresh-manifests --all --skip-nx-cache && bin/prettify-manifests.js && pnpm refresh-readme",
"refresh-readme": "nx run-many --target=refresh-readme --all --skip-nx-cache",
"release": "./bin/release",
"resolve-package-publish-state": "node bin/resolve-package-publish-state.js",
"post-release": "./bin/post-release",
"update-observe": "node bin/update-observe.js",
"shopify:run": "node packages/cli/bin/dev.js",
Expand Down
2 changes: 2 additions & 0 deletions packages/shopify-dev-tools/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Core GraphQL utilities for Shopify Dev MCP. This package provides the foundation

## Installation

This is a private package published to Shopify's internal Cloudsmith registry. It is not published to the public npm registry. After configuring npm to resolve the `@shopify` scope from Cloudsmith, install it with:

```bash
npm install @shopify/shopify-dev-tools
```
Expand Down
Loading