Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/publish-with-admin-app.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@stripe/stripe-cli-plugin-bootstrap': minor
---

Add `stripe-cli-publish-plugin-with-admin-app` script for publishing plugins via the Stripe Admin API instead of a manifest file
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@
"stripe-cli-get-target-manifest-file": "./scripts/release/dist/getTargetManifestFile.js",
"stripe-cli-download-manifest": "./scripts/release/dist/downloadManifest.js",
"stripe-cli-update-manifest": "./scripts/release/dist/updateManifest.js",
"stripe-cli-publish-plugin": "./scripts/release/dist/publishPlugin.js"
"stripe-cli-publish-plugin": "./scripts/release/dist/publishPlugin.js",
"stripe-cli-publish-plugin-with-admin-app": "./scripts/release/dist/publishPluginWithAdminApp.js"
},
"scripts": {
"api:extract": "api-extractor run --local",
Expand Down
74 changes: 70 additions & 4 deletions scripts/release/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,20 +91,21 @@ Downloads `plugins-<plugin-name>.toml` from Artifactory to `dist/` directory.

### publishPlugin.ts

Uploads plugin binaries and manifest to Artifactory.
Uploads plugin binaries and a manifest file to Artifactory.

**Usage:**

```bash
export ARTIFACTORY_HOST="your-host"
export ARTIFACTORY_SECRET="your-secret"
export ARTIFACTORY_REPO="stripe-cli-plugins"
npm run publish-plugin <version>
npm run publish-plugin <version> <manifest-filename>
```

**Arguments:**

- `version` - Plugin version (e.g., 0.0.2 or v0.0.2)
- `manifest-filename` - Manifest filename to upload from `dist/`

**Environment Variables:**

Expand All @@ -121,7 +122,7 @@ npm run publish-plugin <version>
- All binaries are uploaded with the same name (`stripe-cli-<plugin-name>`)
- Differentiated by their path (OS/arch)
- Each binary is self-contained (assets are embedded at build time)
4. Uploads manifest file to `plugins-<plugin-name>.toml`
4. Uploads the manifest file to the Artifactory root using the provided manifest filename

**Example upload paths for version 0.0.2:**

Expand Down Expand Up @@ -164,11 +165,76 @@ In `.github/workflows/release.yml`:
working-directory: ./scripts/release

- name: Publish plugin
run: npm run publish-plugin ${{ steps.bump-version.outputs.new_version }}
run: npm run publish-plugin ${{ steps.bump-version.outputs.new_version }} plugins-generate.toml
working-directory: ./scripts/release
env:
ARTIFACTORY_HOST: ${{ secrets.ARTIFACTORY_HOST }}
ARTIFACTORY_SECRET: ${{ secrets.ARTIFACTORY_SECRET }}
ARTIFACTORY_REPO: stripe-cli-plugins
DRYRUN_PUBLISH: ${{ vars.DRYRUN_PUBLISH }}
```

### publishPluginWithAdminApp.ts

Uploads plugin binaries to Artifactory and updates Stripe CLI plugin metadata via
the authenticated Stripe API.

**Usage:**

```bash
export ARTIFACTORY_HOST="your-host"
export ARTIFACTORY_SECRET="your-secret"
export ARTIFACTORY_REPO="stripe-cli-plugins"
export STRIPE_API_KEY="sk_live_..."
npm run publish-plugin-with-admin-app <version>
```

**Arguments:**

- `version` - Plugin version (e.g., 0.0.2 or v0.0.2)

**Environment Variables:**

- `ARTIFACTORY_HOST` - Artifactory hostname (required)
- `ARTIFACTORY_SECRET` - Bearer token for Artifactory authentication
- `ARTIFACTORY_REPO` - Artifactory repository name
- `STRIPE_API_KEY` - Stripe secret key with `stripecli_plugin_write` permission
- `PLUGIN_AVAILABILITY` - Optional override for release availability (`public` or `conditional`); defaults to `conditional` for `docs`, `generate`, `health`, and `spec`, otherwise `public`
- `DRYRUN_PUBLISH` - Set to any value to enable dry-run mode (no actual uploads)

**What it does:**

1. Reads plugin name from `.plugin` file
2. Finds all built binaries in `bin/` directory
3. Computes SHA256 checksums for each binary
4. Uploads binaries to `<plugin-name>/<version>/<os>/<arch>/stripe-cli-<plugin-name>`
- All binaries are uploaded with the same name (`stripe-cli-<plugin-name>`)
- Differentiated by their path (OS/arch)
- Each binary is self-contained (assets are embedded at build time)
5. Calls `POST /v1/stripecli/update-plugin-metadata` for each uploaded platform
to register the version, OS, arch, checksum, and availability in Stripe

### CI Usage

In `.github/workflows/release.yml`:

```yaml
- name: Build binaries
run: npm run build:package

- name: Install release script dependencies
run: npm install
working-directory: ./scripts/release

- name: Publish plugin with Admin App
run: npm run publish-plugin-with-admin-app ${{ steps.bump-version.outputs.new_version }}
working-directory: ./scripts/release
env:
ARTIFACTORY_HOST: ${{ secrets.ARTIFACTORY_HOST }}
ARTIFACTORY_SECRET: ${{ secrets.ARTIFACTORY_SECRET }}
ARTIFACTORY_REPO: stripe-cli-plugins
STRIPE_API_KEY: ${{ secrets.STRIPE_API_KEY }}
# Optional for feature-gated plugins; omit for public releases.
PLUGIN_AVAILABILITY: conditional
DRYRUN_PUBLISH: ${{ vars.DRYRUN_PUBLISH }}
```
1 change: 1 addition & 0 deletions scripts/release/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion scripts/release/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"get-target-manifest-file": "dist/getTargetManifestFile.js",
"download-manifest": "dist/downloadManifest.js",
"update-manifest": "dist/updateManifest.js",
"publish-plugin": "dist/publishPlugin.js"
"publish-plugin": "dist/publishPlugin.js",
"publish-plugin-with-admin-app": "dist/publishPluginWithAdminApp.js"
},
"scripts": {
"build": "pnpm compile:ts",
Expand Down
159 changes: 159 additions & 0 deletions scripts/release/src/publishPluginWithAdminApp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
#!/usr/bin/env node
import path from 'path'
import * as fs from 'fs'
import * as crypto from 'crypto'
import {
uploadOrThrow,
updatePluginMetadata,
type PluginAvailability,
} from './util/requests'
import * as core from '@actions/core'
import { readPluginConfig } from './util/config'
import { removeVPrefix } from './util/version'

const BIN_DIR = path.join(process.cwd(), 'bin')
const CONDITIONAL_PLUGINS = new Set(['docs', 'generate', 'health', 'spec'])

/**
* Platform and architecture mapping for binary paths
*/
function getPlatforms(pluginName: string) {
return [
{
os: 'darwin',
arch: 'amd64',
filename: `stripe-cli-${pluginName}-macos-x64`,
},
{
os: 'darwin',
arch: 'arm64',
filename: `stripe-cli-${pluginName}-macos-arm64`,
},
{
os: 'linux',
arch: 'amd64',
filename: `stripe-cli-${pluginName}-linux-x64`,
},
{
os: 'linux',
arch: 'arm64',
filename: `stripe-cli-${pluginName}-linux-arm64`,
},
{
os: 'windows',
arch: 'amd64',
filename: `stripe-cli-${pluginName}-win-x64.exe`,
},
]
}

function computeChecksum(filePath: string): string {
const fileBuffer = fs.readFileSync(filePath)
const hashSum = crypto.createHash('sha256')
hashSum.update(fileBuffer)
return hashSum.digest('hex')
}

function getPluginAvailability(pluginName: string): PluginAvailability {
const explicitAvailability = process.env.PLUGIN_AVAILABILITY?.trim()

if (explicitAvailability) {
if (explicitAvailability === 'conditional' || explicitAvailability === 'public') {
return explicitAvailability
}

throw new Error(
`Invalid PLUGIN_AVAILABILITY: ${explicitAvailability}. Must be "public" or "conditional".`,
)
}

if (CONDITIONAL_PLUGINS.has(pluginName)) {
return 'conditional'
}

return 'public'
}

async function main() {
const rawVersion = process.argv[2]

if (!rawVersion) {
return core.setFailed('No version passed.')
}

const version = removeVPrefix(rawVersion)
const pluginConfig = readPluginConfig()
const pluginName = pluginConfig.name
const availability = getPluginAvailability(pluginName)

console.log(`Publishing plugin with Admin App metadata: ${pluginName}`)
console.log(`Version: ${version}`)
console.log(`Availability: ${availability}`)
console.log('')

const filesToUpload: Array<{
localPath: string
remotePath: string
os: string
arch: string
checksum: string
}> = []

const binaryName = `stripe-cli-${pluginName}`
const platforms = getPlatforms(pluginName)

for (const platform of platforms) {
const localPath = path.join(BIN_DIR, platform.filename)

if (!fs.existsSync(localPath)) {
console.warn(`Warning: Binary not found: ${localPath}`)
console.warn(`Skipping ${platform.os}/${platform.arch}`)
continue
}

const platformPrefix = `${pluginName}/${version}/${platform.os}/${platform.arch}`
const remotePath = `${platformPrefix}/${binaryName}`
const checksum = computeChecksum(localPath)

filesToUpload.push({
localPath,
remotePath,
os: platform.os,
arch: platform.arch,
checksum,
})
}

if (filesToUpload.length === 0) {
return core.setFailed('No files to upload. Make sure binaries are built.')
}

console.log(`Uploading ${filesToUpload.length} files:`)
filesToUpload.forEach(({ localPath, remotePath, checksum }) => {
console.log(` ${path.basename(localPath)} -> ${remotePath} (sha256: ${checksum})`)
})
console.log('')

for (const { localPath, remotePath, os, arch, checksum } of filesToUpload) {
await uploadOrThrow({
localPath,
remotePath,
})

await updatePluginMetadata({
pluginName,
version,
os,
arch,
checksum,
availability,
})
}

console.log('')
console.log('🎉 Plugin published successfully!')
}

main().catch(error => {
core.setFailed(error instanceof Error ? error.message : String(error))
})
Loading
Loading