From 569b3fd563d63682b5d5cf6e6b9c37236b5a3929 Mon Sep 17 00:00:00 2001 From: Ahmed Mahmud Date: Fri, 11 Sep 2026 11:38:03 +0100 Subject: [PATCH 1/9] release: mark CLI package publish-ready --- package.json | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index d82cf10..995eccb 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@stellarforge/cli", "version": "0.0.0", - "private": true, + "private": false, "description": "Command-line interface for building production-ready Stellar applications.", "type": "module", "packageManager": "npm@10.9.2", @@ -22,7 +22,9 @@ "changeset": "changeset", "changeset:status": "changeset status", "changeset:version": "changeset version", - "test:e2e": "node scripts/e2e-smoke.mjs" + "test:e2e": "node scripts/e2e-smoke.mjs", + "release:dry-run": "npm pack --dry-run --json", + "release:publish": "changeset publish" }, "dependencies": { "commander": "15.0.0" @@ -36,5 +38,24 @@ "typescript": "6.0.3", "typescript-eslint": "8.69.0", "vitest": "5.0.0" + }, + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/DigiNodes/stellarforge-cli.git" + }, + "homepage": "https://github.com/DigiNodes/stellarforge-cli#readme", + "bugs": { + "url": "https://github.com/DigiNodes/stellarforge-cli/issues" + }, + "files": [ + "dist", + "templates", + "README.md", + "CHANGELOG.md", + "LICENSE" + ], + "publishConfig": { + "access": "public" } } From bef9cba6011a584015c4b7f56fa9ebbfab3203df Mon Sep 17 00:00:00 2001 From: Ahmed Mahmud Date: Fri, 11 Sep 2026 11:38:06 +0100 Subject: [PATCH 2/9] release: align lockfile package metadata --- package-lock.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index e8a8ff2..7dc46f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,8 @@ "engines": { "node": ">=22.13.0 <25", "npm": ">=10.9.0" - } + }, + "license": "MIT" }, "node_modules/@changesets/apply-release-plan": { "version": "8.1.0", From e9f8f15b32b8a6e616507bd51fada4284352bcf9 Mon Sep 17 00:00:00 2001 From: Ahmed Mahmud Date: Fri, 11 Sep 2026 11:38:44 +0100 Subject: [PATCH 3/9] ci: add protected Changesets release workflow --- .github/workflows/release.yml | 215 ++++++++++++++++++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..b744ab8 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,215 @@ +name: Release + +on: + push: + branches: + - main + workflow_dispatch: + +permissions: {} + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +jobs: + select-mode: + name: Select release mode + if: github.event_name == 'push' + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + outputs: + mode: ${{ steps.mode.outputs.mode }} + publish-plan-artifact-id: ${{ steps.mode.outputs.publish-plan-artifact-id }} + + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: '24.8.0' + package-manager-cache: false + + - name: Install from committed lockfile + run: npm ci --ignore-scripts --no-audit --no-fund + + - name: Build release candidate + run: npm run build + + - name: Select Changesets mode + id: mode + uses: changesets/action/select-mode@0138f456ec3d73906fcd11169ce59502d8d241c1 + + version: + name: Create or update release PR + if: needs.select-mode.outputs.mode == 'version' + needs: select-mode + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: write + pull-requests: write + + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: '24.8.0' + package-manager-cache: false + + - name: Install from committed lockfile + run: npm ci --ignore-scripts --no-audit --no-fund + + - name: Create or update release PR + uses: changesets/action/version@0138f456ec3d73906fcd11169ce59502d8d241c1 + with: + github-token: ${{ github.token }} + pr-title: 'release: version packages' + commit-message: 'release: version packages' + + pack: + name: Validate and pack release + if: needs.select-mode.outputs.mode == 'publish' + needs: select-mode + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + outputs: + pack-dir-artifact-id: ${{ steps.pack.outputs.pack-dir-artifact-id }} + + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: '24.8.0' + package-manager-cache: false + + - name: Install from committed lockfile + run: npm ci --ignore-scripts --no-audit --no-fund + + - name: Run release quality checks + run: | + npm run typecheck + npm run lint + npm run format:check + npm test + npm run build + + - name: Inspect npm package without publishing + run: npm run release:dry-run + + - name: Pack exact Changesets publish artifacts + id: pack + uses: changesets/action/pack@0138f456ec3d73906fcd11169ce59502d8d241c1 + with: + publish-plan-artifact-id: ${{ needs.select-mode.outputs.publish-plan-artifact-id }} + + publish: + name: Publish npm package and GitHub release + if: needs.select-mode.outputs.mode == 'publish' && vars.NPM_PUBLISH_ENABLED == 'true' + needs: + - select-mode + - pack + runs-on: ubuntu-latest + timeout-minutes: 15 + environment: npm-release + permissions: + contents: write + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Setup Node.js for npm Trusted Publishing + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: '24.8.0' + registry-url: 'https://registry.npmjs.org' + package-manager-cache: false + + - name: Pin Trusted Publishing compatible npm + run: npm install --global npm@11.5.1 --ignore-scripts --no-audit --no-fund + + - name: Verify release runtime + run: | + node --version + npm --version + + - name: Publish packed artifacts + id: publish + uses: changesets/action/publish@0138f456ec3d73906fcd11169ce59502d8d241c1 + with: + github-token: ${{ github.token }} + pack-dir-artifact-id: ${{ needs.pack.outputs.pack-dir-artifact-id }} + create-github-releases: true + push-git-tags: true + + publish-disabled: + name: Publication disabled + if: needs.select-mode.outputs.mode == 'publish' && vars.NPM_PUBLISH_ENABLED != 'true' + needs: + - select-mode + - pack + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: {} + + steps: + - name: Explain protected publication gate + run: | + echo "Release artifacts validated and packed, but npm publication is disabled." + echo "Configure npm Trusted Publishing, protect the npm-release environment," + echo "then set repository variable NPM_PUBLISH_ENABLED=true." + + readiness: + name: Manual release readiness dry-run + if: github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: '24.8.0' + package-manager-cache: false + + - name: Install from committed lockfile + run: npm ci --ignore-scripts --no-audit --no-fund + + - name: Run release readiness checks + run: | + npm run typecheck + npm run lint + npm run format:check + npm test + npm run build + npm run release:dry-run + npm run changeset:status From 541e52e61b4c7554997ee52bd002ea3b5b232b35 Mon Sep 17 00:00:00 2001 From: Ahmed Mahmud Date: Fri, 11 Sep 2026 11:39:28 +0100 Subject: [PATCH 4/9] docs: define protected release operations --- docs/contributing/release-process.md | 98 +++++++++++++++++++++++----- 1 file changed, 83 insertions(+), 15 deletions(-) diff --git a/docs/contributing/release-process.md b/docs/contributing/release-process.md index fe513a3..26b60ea 100644 --- a/docs/contributing/release-process.md +++ b/docs/contributing/release-process.md @@ -1,25 +1,93 @@ # Release Process -StellarForge CLI uses the release direction defined by ADR-0003: Semantic Versioning, Changesets, protected automation, Git tags, and GitHub Releases, with package-registry publishing added when the CLI is ready for distribution. +StellarForge CLI uses Semantic Versioning, Changesets, npm Trusted Publishing, and GitHub Releases. -## Contributor Responsibilities +## Safety state before first publication -Release-impacting PRs include an appropriate Changeset. Documentation-only, test-only, and internal CI changes usually do not require one unless maintainers request release communication. +Release automation is intentionally merged in a non-publishing state. The workflow can create release PRs and validate/pack release artifacts, but the npm publish job runs only when **all** of these controls are in place: -## Maintainer Flow +1. the `@stellarforge/cli` npm scope/package is owned by the StellarForge maintainers; +2. npm Trusted Publishing is configured for: + - GitHub organization: `DigiNodes` + - repository: `stellarforge-cli` + - workflow filename: `release.yml` + - environment: `npm-release` + - direct `npm publish` is allowed; +3. the GitHub `npm-release` environment exists and requires maintainer approval; +4. the repository variable `NPM_PUBLISH_ENABLED` is set to `true`. -1. Merge reviewed contributor PRs to `main`. -2. Aggregate pending Changesets. -3. Review generated version/changelog updates. -4. Ensure required CI/security checks pass. -5. Merge the release PR. -6. Publish only through the approved protected workflow. -7. Verify package, tag, GitHub Release, and provenance where supported. +Until then, release artifacts are built, tested, inspected, and packed, but publication stops at the protected gate. -## Pre-1.0 +No long-lived `NPM_TOKEN` is used. -Breaking changes remain possible while the CLI contract stabilizes, but they must be intentional, documented, and communicated. Versions should advance according to planned milestone outcomes rather than arbitrary issue counts. +## Normal release flow -## Security +1. User-facing PRs include a Changeset when required. +2. PRs merge to `main` only after normal CI/security review. +3. `.github/workflows/release.yml` selects Changesets mode. +4. If unreleased Changesets exist, the workflow creates or updates a `release: version packages` PR. +5. Maintainers review the generated version and changelog. +6. The release PR passes normal CI/security gates and is merged. +7. The release workflow detects publish mode. +8. A read-only job rebuilds, tests, runs `npm pack --dry-run`, and creates exact packed release artifacts. +9. If publication is enabled, the protected `npm-release` environment must approve the publish job. +10. The publish job obtains an npm OIDC credential, publishes the packed artifact, then creates the matching Git tag and GitHub Release. -Do not publish from contributor-controlled privileged contexts. Prefer short-lived/OIDC registry authentication where supported, least-privilege Actions permissions, and reviewed third-party Actions. +The publish job has `id-token: write` only where OIDC is needed. Versioning and validation jobs do not receive OIDC permission. + +## Manual dry-run + +Run the **Release** workflow manually with `workflow_dispatch`. The readiness job performs: + +```bash +npm ci --ignore-scripts --no-audit --no-fund +npm run typecheck +npm run lint +npm run format:check +npm test +npm run build +npm run release:dry-run +npm run changeset:status +``` + +This path never publishes. + +## First release bootstrap + +The repository starts at `0.0.0`. The CLI-038 Changeset prepares the first release PR as a minor release, producing `0.1.0`. + +Do not set `NPM_PUBLISH_ENABLED=true` merely because the release PR exists. First verify npm namespace ownership and configure the trusted publisher/environment controls above. + +npm Trusted Publishing currently requires a compatible hosted GitHub Actions runner, Node.js 22.14+ and npm 11.5.1+. The release job pins Node 24.8.0 and npm 11.5.1 for this reason. + +## Tag and GitHub Release contract + +The Changesets publish action is responsible for package publication, the package-version Git tag, and the GitHub Release. A release is considered healthy only when all three identify the same version. + +After publication, verify: + +- npm shows `@stellarforge/cli@`; +- Git contains the matching package tag; +- the GitHub Release points to the same version/tag; +- npm displays provenance for the public package. + +## Rollback and hotfixes + +Published npm versions and release tags are immutable release records. Never rewrite or reuse a published version. + +For a bad release: + +1. disable `NPM_PUBLISH_ENABLED` immediately if further automated publication should stop; +2. if appropriate, deprecate the bad npm version with a clear message; +3. prepare a corrective Changeset; +4. ship a new patch version through the same protected release flow. + +For a security hotfix, coordinate privately under `SECURITY.md`, merge the minimal reviewed fix, create a patch Changeset, and use the same protected environment/OIDC publication path. + +## Required repository settings + +GitHub Actions must be allowed to create pull requests for Changesets release PR automation. + +The `npm-release` environment should use required reviewers and should not expose unrelated secrets. The workflow needs no npm publishing secret because authentication is OIDC-based. + +Dependency Graph/Dependency Review should also be enabled at the repository level so the existing Dependency Review workflow can become an enforceable release gate. From 5ced1b305f1a8b02c24455bb0cabcd66e76ff54f Mon Sep 17 00:00:00 2001 From: Ahmed Mahmud Date: Fri, 11 Sep 2026 11:39:34 +0100 Subject: [PATCH 5/9] test: enforce release workflow security policy --- tests/release-workflow.test.ts | 60 ++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 tests/release-workflow.test.ts diff --git a/tests/release-workflow.test.ts b/tests/release-workflow.test.ts new file mode 100644 index 0000000..31eac36 --- /dev/null +++ b/tests/release-workflow.test.ts @@ -0,0 +1,60 @@ +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { describe, expect, it } from 'vitest'; + +const root = join(import.meta.dirname, '..'); + +describe('release automation policy', () => { + const workflow = readFileSync( + join(root, '.github', 'workflows', 'release.yml'), + 'utf8', + ); + const packageJson = JSON.parse( + readFileSync(join(root, 'package.json'), 'utf8'), + ) as { + private?: boolean; + publishConfig?: { access?: string }; + files?: string[]; + scripts?: Record; + }; + + it('keeps the package publishable and explicitly public', () => { + expect(packageJson.private).toBe(false); + expect(packageJson.publishConfig?.access).toBe('public'); + expect(packageJson.files).toContain('dist'); + expect(packageJson.files).toContain('templates'); + expect(packageJson.scripts?.['release:dry-run']).toContain( + 'npm pack --dry-run', + ); + }); + + it('pins release actions and separates OIDC from versioning', () => { + expect(workflow).toContain( + 'changesets/action/select-mode@0138f456ec3d73906fcd11169ce59502d8d241c1', + ); + expect(workflow).toContain( + 'changesets/action/version@0138f456ec3d73906fcd11169ce59502d8d241c1', + ); + expect(workflow).toContain( + 'changesets/action/pack@0138f456ec3d73906fcd11169ce59502d8d241c1', + ); + expect(workflow).toContain( + 'changesets/action/publish@0138f456ec3d73906fcd11169ce59502d8d241c1', + ); + expect(workflow.match(/id-token: write/g)).toHaveLength(1); + expect(workflow).toContain('environment: npm-release'); + }); + + it('requires an explicit publication kill switch and contains no npm token secret', () => { + expect(workflow).toContain("vars.NPM_PUBLISH_ENABLED == 'true'"); + expect(workflow).not.toContain('NPM_TOKEN'); + expect(workflow).not.toContain('NODE_AUTH_TOKEN'); + expect(workflow).toContain('npm@11.5.1'); + }); + + it('provides a non-publishing readiness path', () => { + expect(workflow).toContain('workflow_dispatch'); + expect(workflow).toContain('npm run release:dry-run'); + expect(workflow).toContain('npm run changeset:status'); + }); +}); From 2328c6908f3b341b91eaaef9d8871ab75dcffbe9 Mon Sep 17 00:00:00 2001 From: Ahmed Mahmud Date: Fri, 11 Sep 2026 11:39:37 +0100 Subject: [PATCH 6/9] release: record protected publication automation --- .changeset/protected-release-automation.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/protected-release-automation.md diff --git a/.changeset/protected-release-automation.md b/.changeset/protected-release-automation.md new file mode 100644 index 0000000..b81acf1 --- /dev/null +++ b/.changeset/protected-release-automation.md @@ -0,0 +1,5 @@ +--- +"@stellarforge/cli": minor +--- + +Establish the protected release pipeline for the first public StellarForge CLI package, including Changesets release PRs, deterministic package dry-runs, npm Trusted Publishing/OIDC gating, provenance-ready publication, and matching GitHub tags/releases. From a8273cfafb4ae8446762c1a757a24d2e402dad01 Mon Sep 17 00:00:00 2001 From: Ahmed Mahmud Date: Fri, 11 Sep 2026 11:40:11 +0100 Subject: [PATCH 7/9] docs: define tokenless first-release bootstrap --- docs/contributing/release-process.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/docs/contributing/release-process.md b/docs/contributing/release-process.md index 26b60ea..34176d7 100644 --- a/docs/contributing/release-process.md +++ b/docs/contributing/release-process.md @@ -54,9 +54,19 @@ This path never publishes. ## First release bootstrap -The repository starts at `0.0.0`. The CLI-038 Changeset prepares the first release PR as a minor release, producing `0.1.0`. +The repository starts at `0.0.0`. npm Trusted Publishing is configured from an existing package's settings, so namespace bootstrap is deliberately separate from normal automated releases. -Do not set `NPM_PUBLISH_ENABLED=true` merely because the release PR exists. First verify npm namespace ownership and configure the trusted publisher/environment controls above. +Before merging the generated `0.1.0` release PR: + +1. run the manual Release workflow readiness job and inspect `npm pack --dry-run`; +2. from an approved maintainer workstation, authenticate interactively to the npm account/organization that owns the `@stellarforge` scope; +3. publish the reviewed `0.0.0` package once with a non-default bootstrap tag, for example `npm publish --access public --tag bootstrap`; +4. configure npm Trusted Publishing on the newly existing `@stellarforge/cli` package for `DigiNodes/stellarforge-cli`, workflow `release.yml`, environment `npm-release`, with direct publish allowed; +5. protect the GitHub `npm-release` environment with required maintainer review; +6. set repository variable `NPM_PUBLISH_ENABLED=true`; +7. merge the reviewed release PR so `0.1.0` is the first normal automated release. + +The bootstrap publication uses interactive maintainer authentication only. Do not create or store a long-lived npm CI publishing token. npm Trusted Publishing currently requires a compatible hosted GitHub Actions runner, Node.js 22.14+ and npm 11.5.1+. The release job pins Node 24.8.0 and npm 11.5.1 for this reason. From 4aa3537d43802c02402d520fb10d1262ceebbcff Mon Sep 17 00:00:00 2001 From: Ahmed Mahmud Date: Fri, 11 Sep 2026 11:40:24 +0100 Subject: [PATCH 8/9] docs: accept protected release architecture --- ...ADR-0003-release-and-versioning-strategy.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/adr/ADR-0003-release-and-versioning-strategy.md b/docs/adr/ADR-0003-release-and-versioning-strategy.md index 1080012..4f75c3d 100644 --- a/docs/adr/ADR-0003-release-and-versioning-strategy.md +++ b/docs/adr/ADR-0003-release-and-versioning-strategy.md @@ -1,6 +1,6 @@ # ADR-0003: Release and Versioning Strategy -- **Status:** Proposed +- **Status:** Accepted - **Date:** 2026-09-06 - **Decision owners:** StellarForge Core Team @@ -17,9 +17,9 @@ Adopt the following release strategy: 1. Semantic Versioning for public versions. 2. Changesets for contributor release intent, version updates, and changelog generation. 3. npm remains the selected package-manager/release ecosystem unless changed by a separate architecture decision. -4. GitHub Actions will automate the release flow after the package foundation exists and the workflow can be tested safely. +4. GitHub Actions automate release PR creation, package validation/packing, and protected publication after the package foundation is validated. 5. Git tags and GitHub Releases will correspond to published versions. -6. npm Trusted Publishing/OIDC and package provenance should be preferred when package publication begins and the supported environment permits it. +6. npm Trusted Publishing/OIDC is the required normal publication mechanism; public GitHub-hosted publication retains npm provenance. 7. Release automation must be protected from untrusted pull-request execution and use least-privilege permissions. ## Version Semantics @@ -112,10 +112,10 @@ Rejected because it becomes error-prone as contribution volume increases. Not selected for the MVP because explicit changeset files make release intent reviewable per PR without forcing commit-message conventions to carry the entire release model. -## Follow-up Work +## Implementation Status -- add `.changeset/config.json` and contributor guidance; -- establish package metadata before first package release; -- implement release automation only after the package foundation exists; -- configure trusted publishing/provenance before production npm publication where supported; -- document release operations in `docs/contributing/release-process.md`. +The repository now includes the Changesets configuration, public package metadata, a protected release workflow, release-policy tests, and release operations documentation. + +Production npm publication remains intentionally gated until maintainers complete the one-time namespace bootstrap, configure npm Trusted Publishing for `release.yml`, protect the `npm-release` environment, and explicitly enable `NPM_PUBLISH_ENABLED`. + +See `docs/contributing/release-process.md` for the operational checklist, dry-run procedure, rollback guidance, and first-release bootstrap. From 13bfc2c6a4f2e5049cb37aec0adedfb536ea36af Mon Sep 17 00:00:00 2001 From: Ahmed Mahmud Date: Fri, 11 Sep 2026 11:40:32 +0100 Subject: [PATCH 9/9] docs: link protected release process --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e9e3e67..ea8e99b 100644 --- a/README.md +++ b/README.md @@ -202,7 +202,7 @@ Some implementation directories/files will appear as their corresponding foundat The proposed release strategy uses Semantic Versioning and Changesets. Release-impacting PRs will record their intended version/changelog effect close to the code change. -See [ADR-0003](docs/adr/ADR-0003-release-and-versioning-strategy.md). +See [ADR-0003](docs/adr/ADR-0003-release-and-versioning-strategy.md) and the [Release Process](docs/contributing/release-process.md). ---