Skip to content
Merged
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/protected-release-automation.md
Original file line number Diff line number Diff line change
@@ -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.
215 changes: 215 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

---

Expand Down
18 changes: 9 additions & 9 deletions docs/adr/ADR-0003-release-and-versioning-strategy.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ADR-0003: Release and Versioning Strategy

- **Status:** Proposed
- **Status:** Accepted
- **Date:** 2026-09-06
- **Decision owners:** StellarForge Core Team

Expand All @@ -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
Expand Down Expand Up @@ -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.
108 changes: 93 additions & 15 deletions docs/contributing/release-process.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,103 @@
# 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`. npm Trusted Publishing is configured from an existing package's settings, so namespace bootstrap is deliberately separate from normal automated releases.

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.

## 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@<version>`;
- 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.
3 changes: 2 additions & 1 deletion package-lock.json

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

Loading