From 81246675e7fe42f585218c385ea9c9567c0f03d9 Mon Sep 17 00:00:00 2001 From: Durga Date: Wed, 22 Jul 2026 12:07:04 -0400 Subject: [PATCH] Publish shopify-dev-tools to Cloudsmith Assisted-By: devx/8aa8112b-1966-49eb-8fd0-855a849a29f9 --- .github/workflows/release.yml | 38 ++++- .shopify-build/VERSION | 1 + .../cli-shopify-dev-tools-publish-package.yml | 29 ++++ CONTRIBUTING.md | 8 ++ bin/resolve-package-publish-state.js | 133 ++++++++++++++++++ package.json | 1 + packages/shopify-dev-tools/README.md | 2 + 7 files changed, 210 insertions(+), 2 deletions(-) create mode 100644 .shopify-build/VERSION create mode 100644 .shopify-build/cli-shopify-dev-tools-publish-package.yml create mode 100644 bin/resolve-package-publish-state.js diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2addf268a34..4c702ad3d48 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,6 +26,7 @@ on: - nightly - latest - experimental + - shopify-dev-tools concurrency: group: changeset-${{ github.head_ref || github.run_id }} @@ -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 diff --git a/.shopify-build/VERSION b/.shopify-build/VERSION new file mode 100644 index 00000000000..8c1384d825d --- /dev/null +++ b/.shopify-build/VERSION @@ -0,0 +1 @@ +v2 diff --git a/.shopify-build/cli-shopify-dev-tools-publish-package.yml b/.shopify-build/cli-shopify-dev-tools-publish-package.yml new file mode 100644 index 00000000000..a416136e29a --- /dev/null +++ b/.shopify-build/cli-shopify-dev-tools-publish-package.yml @@ -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'] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2ec05fa3502..fb6062199fe 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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. diff --git a/bin/resolve-package-publish-state.js b/bin/resolve-package-publish-state.js new file mode 100644 index 00000000000..3361c3188cd --- /dev/null +++ b/bin/resolve-package-publish-state.js @@ -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 ') +} + +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 +}) diff --git a/package.json b/package.json index af66060d862..12b09f44b27 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/shopify-dev-tools/README.md b/packages/shopify-dev-tools/README.md index a076b4ff8f4..e68b5863743 100644 --- a/packages/shopify-dev-tools/README.md +++ b/packages/shopify-dev-tools/README.md @@ -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 ```