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
8 changes: 4 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ jobs:

- name: Select Changesets mode
id: mode
uses: changesets/action/select-mode@0138f456ec3d73906fcd11169ce59502d8d241c1
uses: changesets/action/select-mode@22ccf9aa43179fe9e27dc62e575971d28cce197c # v2.0.0

version:
name: Create or update release PR
Expand Down Expand Up @@ -72,7 +72,7 @@ jobs:
run: npm ci --ignore-scripts --no-audit --no-fund

- name: Create or update release PR
uses: changesets/action/version@0138f456ec3d73906fcd11169ce59502d8d241c1
uses: changesets/action/version@22ccf9aa43179fe9e27dc62e575971d28cce197c # v2.0.0
with:
github-token: ${{ github.token }}
pr-title: 'release: version packages'
Expand Down Expand Up @@ -117,7 +117,7 @@ jobs:

- name: Pack exact Changesets publish artifacts
id: pack
uses: changesets/action/pack@0138f456ec3d73906fcd11169ce59502d8d241c1
uses: changesets/action/pack@22ccf9aa43179fe9e27dc62e575971d28cce197c # v2.0.0
with:
publish-plan-artifact-id: ${{ needs.select-mode.outputs.publish-plan-artifact-id }}

Expand Down Expand Up @@ -157,7 +157,7 @@ jobs:

- name: Publish packed artifacts
id: publish
uses: changesets/action/publish@0138f456ec3d73906fcd11169ce59502d8d241c1
uses: changesets/action/publish@22ccf9aa43179fe9e27dc62e575971d28cce197c # v2.0.0
with:
github-token: ${{ github.token }}
pack-dir-artifact-id: ${{ needs.pack.outputs.pack-dir-artifact-id }}
Expand Down
13 changes: 13 additions & 0 deletions .github/workflows/wave-pr-guard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,19 @@ jobs:
const repo = context.repo.repo;
const pr = context.payload.pull_request;
const body = pr.body || '';
const trustedAssociations = new Set(['OWNER', 'MEMBER', 'COLLABORATOR']);
if (trustedAssociations.has(pr.author_association)) {
core.info('Skipping contributor-only Wave checks for trusted maintainer PR.');
return;
}
const { data: collaborator } = await github.rest.repos.getCollaboratorPermissionLevel({
owner, repo, username: pr.user.login,
});
Comment thread
dDevAhmed marked this conversation as resolved.
const trustedPermissions = new Set(['admin', 'maintain', 'write']);
if (trustedPermissions.has(collaborator.permission)) {
core.info('Skipping contributor-only Wave checks for repository collaborator PR.');
return;
}
const refs = [...body.matchAll(/(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d+)/gi)].map((m) => Number(m[1]));
const uniqueRefs = [...new Set(refs)];
if (uniqueRefs.length !== 1) {
Expand Down
30 changes: 5 additions & 25 deletions docs/contributing/changesets.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Changesets Contributor Workflow

StellarForge CLI uses Changesets to record release intent close to the pull request that introduces a user-visible package change. The project is still pre-release and the npm package remains private, so this workflow currently supports local version and changelog generation only. It does not publish packages or create release tags.
StellarForge CLI uses Changesets to record release intent close to the pull request that introduces a user-visible package change. The package metadata is public-ready, while actual publication remains gated by npm Trusted Publishing, the protected `npm-release` environment, and the `NPM_PUBLISH_ENABLED` repository variable.

## When a Changeset Is Required

Expand Down Expand Up @@ -31,7 +31,7 @@ Review the generated file before committing it. A typical file for this reposito

```markdown
---
"@stellarforge/cli": patch
"@diginodes/stellarforge-cli": patch
---

Describe the user-visible change clearly.
Expand Down Expand Up @@ -71,28 +71,8 @@ This command consumes pending changeset files and updates package versions/chang

For ordinary feature pull requests, contributors should normally commit the changeset file itself rather than committing generated release-version changes.

## Private Package Behavior
## Protected Publishing

`@stellarforge/cli` is currently marked `private: true`. The Changesets configuration therefore explicitly enables private-package versioning while keeping private-package tags disabled:
The package is marked `private: false` and the Changesets configuration declares public access. That metadata alone does not authorize publication. The release workflow publishes only when npm Trusted Publishing is configured, the protected `npm-release` environment approves the job, and `NPM_PUBLISH_ENABLED` is exactly `true`.

```json
"privatePackages": {
"version": true,
"tag": false
}
```

This allows us to prove version/changelog generation during development without enabling package publication or release tagging.

## Publishing Is Out of Scope

The following are intentionally not part of the current Changesets integration:

- `changeset publish`;
- npm publication;
- npm credentials or tokens;
- Git tags;
- GitHub Releases;
- release-publishing GitHub Actions.

Protected publication automation will be implemented separately only after package identity, security gates, trusted publishing, and release-readiness requirements are satisfied.
Publishing uses GitHub Actions OIDC rather than a long-lived `NPM_TOKEN`. Maintainers must not run routine releases manually after the one-time registry bootstrap.
21 changes: 11 additions & 10 deletions docs/contributing/release-process.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ StellarForge CLI uses Semantic Versioning, Changesets, npm Trusted Publishing, a

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:

1. the `@stellarforge/cli` npm scope/package is owned by the StellarForge maintainers;
1. the `@diginodes/stellarforge-cli` npm scope/package is owned by the StellarForge maintainers through the `diginodes` npm organization;
2. npm Trusted Publishing is configured for:
- GitHub organization: `DigiNodes`
- repository: `stellarforge-cli`
Expand Down Expand Up @@ -68,14 +68,15 @@ From an approved maintainer workstation:

1. clone `DigiNodes/stellarforge-cli` and check out the bootstrap commit above;
2. run `npm ci --ignore-scripts --no-audit --no-fund`, `npm run build`, and `npm run release:dry-run`;
3. authenticate interactively to the npm account/organization that owns the `@stellarforge` scope, using the required 2FA flow;
4. publish `@stellarforge/cli@0.0.0` once with a non-default bootstrap tag, for example `npm publish --access public --tag bootstrap`;
5. configure npm Trusted Publishing on the newly existing `@stellarforge/cli` package for organization `DigiNodes`, repository `stellarforge-cli`, workflow `release.yml`, environment `npm-release`, with direct `npm publish` allowed;
6. create/protect the GitHub `npm-release` environment with required maintainer review;
7. set repository Actions variable `NPM_PUBLISH_ENABLED=true`;
8. enable GitHub Dependency Graph so the existing Dependency Review workflow becomes enforceable;
9. merge a reviewed release-activation change to `main` (or another approved `main` push) to trigger the protected publish path for the already-versioned `0.1.0` package;
10. approve the `npm-release` environment deployment and verify npm, Git tag, GitHub Release, and provenance all identify `0.1.0`.
3. apply the reviewed package-identity patch that changes only the npm package name to `@diginodes/stellarforge-cli` while retaining bootstrap version `0.0.0`;
4. authenticate interactively as an owner of the `diginodes` npm organization, using the required 2FA flow;
5. publish `@diginodes/stellarforge-cli@0.0.0` once with a non-default bootstrap tag, for example `npm publish --access public --tag bootstrap`;
6. configure npm Trusted Publishing on the newly existing `@diginodes/stellarforge-cli` package for organization `DigiNodes`, repository `stellarforge-cli`, workflow `release.yml`, environment `npm-release`, with direct `npm publish` allowed;
7. create/protect the GitHub `npm-release` environment with required maintainer review;
8. set repository Actions variable `NPM_PUBLISH_ENABLED=true`;
9. verify GitHub Dependency Graph keeps the existing Dependency Review workflow enforceable;
10. merge a reviewed release-activation change to `main` (or another approved `main` push) to trigger the protected publish path for the already-versioned `0.1.0` package;
11. approve the `npm-release` environment deployment and verify npm, Git tag, GitHub Release, and provenance all identify `0.1.0`.

Do **not** republish, rewrite, or downgrade the `0.1.0` commit on `main` merely to bootstrap the npm namespace.

Expand All @@ -89,7 +90,7 @@ The Changesets publish action is responsible for package publication, the packag

After publication, verify:

- npm shows `@stellarforge/cli@<version>`;
- npm shows `@diginodes/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.
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

StellarForge CLI has completed its initial MVP implementation and the repository contains protected release automation. The source is versioned at `0.1.0`, but the first public npm publication is still pending the one-time registry and Trusted Publishing setup tracked in REL-001.

Until that publication is verified, use a source checkout for development and evaluation rather than assuming `@stellarforge/cli` is available from the npm registry.
Until that publication is verified, use a source checkout for development and evaluation rather than assuming `@diginodes/stellarforge-cli` is available from the npm registry.

## Contributor prerequisites

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

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

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"name": "@stellarforge/cli",
"name": "@diginodes/stellarforge-cli",
"version": "0.1.0",
"private": false,
"description": "Command-line interface for building production-ready Stellar applications.",
"type": "module",
"packageManager": "npm@10.9.2",
"bin": {
"stellarforge": "./dist/cli.js"
"stellarforge": "dist/cli.js"
},
"engines": {
"node": ">=22.13.0 <25",
Expand Down
96 changes: 50 additions & 46 deletions tests/api-service-template.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,57 +20,61 @@ function runNpmScript(projectRoot: string, script: string) {
}

describe('API Service template', () => {
it('generates a runnable dependency-light service with isolated Stellar configuration', () =>
withTempDirectory((root) => {
const captured = createCapturedTerminalOutput();
const command = createNewCommand({
cwd: () => root,
output: captured.output,
});
it(
'generates a runnable dependency-light service with isolated Stellar configuration',
() =>
withTempDirectory((root) => {
const captured = createCapturedTerminalOutput();
const command = createNewCommand({
cwd: () => root,
output: captured.output,
});

command.parse(['node', 'new', 'demo-api', '--template', 'api-service']);
command.parse(['node', 'new', 'demo-api', '--template', 'api-service']);

const projectRoot = join(root, 'demo-api');
expect(existsSync(join(projectRoot, 'src', 'server.js'))).toBe(true);
expect(existsSync(join(projectRoot, 'src', 'stellar-client.js'))).toBe(
true,
);
expect(existsSync(join(projectRoot, 'test', 'server.test.js'))).toBe(
true,
);
expect(captured.stdoutText()).toContain(
'Created demo-api from api-service',
);
const projectRoot = join(root, 'demo-api');
expect(existsSync(join(projectRoot, 'src', 'server.js'))).toBe(true);
expect(existsSync(join(projectRoot, 'src', 'stellar-client.js'))).toBe(
true,
);
expect(existsSync(join(projectRoot, 'test', 'server.test.js'))).toBe(
true,
);
expect(captured.stdoutText()).toContain(
'Created demo-api from api-service',
);

const packageManifest = readFileSync(
join(projectRoot, 'package.json'),
'utf8',
);
expect(packageManifest).toContain('"name": "demo-api"');
expect(packageManifest).not.toContain('dependencies');
const packageManifest = readFileSync(
join(projectRoot, 'package.json'),
'utf8',
);
expect(packageManifest).toContain('"name": "demo-api"');
expect(packageManifest).not.toContain('dependencies');

const environmentExample = readFileSync(
join(projectRoot, '.env.example'),
'utf8',
);
expect(environmentExample).toContain('STELLAR_NETWORK=TESTNET');
expect(environmentExample).toContain('STELLAR_HORIZON_URL=https://');
expect(environmentExample).toContain('STELLAR_RPC_URL=https://');
expect(environmentExample).not.toMatch(/SECRET|SEED|PRIVATE_KEY/);
const environmentExample = readFileSync(
join(projectRoot, '.env.example'),
'utf8',
);
expect(environmentExample).toContain('STELLAR_NETWORK=TESTNET');
expect(environmentExample).toContain('STELLAR_HORIZON_URL=https://');
expect(environmentExample).toContain('STELLAR_RPC_URL=https://');
expect(environmentExample).not.toMatch(/SECRET|SEED|PRIVATE_KEY/);

const check = runNpmScript(projectRoot, 'check');
expect(check.status, check.stderr).toBe(0);
const check = runNpmScript(projectRoot, 'check');
expect(check.status, check.stderr).toBe(0);

const test = runNpmScript(projectRoot, 'test');
expect(test.status, test.stderr).toBe(0);
const test = runNpmScript(projectRoot, 'test');
expect(test.status, test.stderr).toBe(0);

const generatedReadme = readFileSync(
join(projectRoot, 'README.md'),
'utf8',
);
expect(generatedReadme).toContain('GET /health');
expect(generatedReadme).toContain('src/stellar-client.js');
expect(generatedReadme).toContain('Never commit secret keys');
expect(generatedReadme).toContain('official Stellar documentation');
}));
const generatedReadme = readFileSync(
join(projectRoot, 'README.md'),
'utf8',
);
expect(generatedReadme).toContain('GET /health');
expect(generatedReadme).toContain('src/stellar-client.js');
expect(generatedReadme).toContain('Never commit secret keys');
expect(generatedReadme).toContain('official Stellar documentation');
}),
15_000,
);
});
82 changes: 45 additions & 37 deletions tests/basic-app-template.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,41 +20,49 @@ function runNpmScript(projectRoot: string, script: string) {
}

describe('Basic App template', () => {
it('generates through the default new command and passes its documented smoke checks', () =>
withTempDirectory((root) => {
const captured = createCapturedTerminalOutput();
const command = createNewCommand({
cwd: () => root,
output: captured.output,
});

command.parse(['node', 'new', 'demo-app']);

const projectRoot = join(root, 'demo-app');
const packageJson = JSON.parse(
readFileSync(join(projectRoot, 'package.json'), 'utf8'),
) as { name: string; dependencies?: unknown; devDependencies?: unknown };

expect(packageJson.name).toBe('demo-app');
expect(packageJson.dependencies).toBeUndefined();
expect(packageJson.devDependencies).toBeUndefined();
expect(captured.stdoutText()).toContain(
'Created demo-app from basic-app',
);

const check = runNpmScript(projectRoot, 'check');
expect(check.status, check.stderr).toBe(0);

const test = runNpmScript(projectRoot, 'test');
expect(test.status, test.stderr).toBe(0);

const generatedReadme = readFileSync(
join(projectRoot, 'README.md'),
'utf8',
);
expect(generatedReadme).toContain('# demo-app');
expect(generatedReadme).toContain(
'does not require or generate wallet secret keys',
);
}));
it(
'generates through the default new command and passes its documented smoke checks',
() =>
withTempDirectory((root) => {
const captured = createCapturedTerminalOutput();
const command = createNewCommand({
cwd: () => root,
output: captured.output,
});

command.parse(['node', 'new', 'demo-app']);

const projectRoot = join(root, 'demo-app');
const packageJson = JSON.parse(
readFileSync(join(projectRoot, 'package.json'), 'utf8'),
) as {
name: string;
dependencies?: unknown;
devDependencies?: unknown;
};

expect(packageJson.name).toBe('demo-app');
expect(packageJson.dependencies).toBeUndefined();
expect(packageJson.devDependencies).toBeUndefined();
expect(captured.stdoutText()).toContain(
'Created demo-app from basic-app',
);

const check = runNpmScript(projectRoot, 'check');
expect(check.status, check.stderr).toBe(0);

const test = runNpmScript(projectRoot, 'test');
expect(test.status, test.stderr).toBe(0);

const generatedReadme = readFileSync(
join(projectRoot, 'README.md'),
'utf8',
);
expect(generatedReadme).toContain('# demo-app');
expect(generatedReadme).toContain(
'does not require or generate wallet secret keys',
);
}),
15_000,
);
});
2 changes: 1 addition & 1 deletion tests/executable.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ describe('StellarForge CLI executable', () => {
bin?: Record<string, string>;
};

expect(packageJson.bin?.stellarforge).toBe('./dist/cli.js');
expect(packageJson.bin?.stellarforge).toBe('dist/cli.js');
});

it('preserves the Node.js shebang in the built artifact', () => {
Expand Down
Loading
Loading