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
14 changes: 7 additions & 7 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: 'Build / Test / Artifacts'
name: 'Build & Test'
on:
push:
branches:
Expand Down Expand Up @@ -42,8 +42,8 @@ jobs:
- name: Test
run: pnpm test

artifacts:
name: Artifacts Upload
build:
name: Build
needs: test
runs-on: ubuntu-latest
permissions:
Expand Down Expand Up @@ -77,7 +77,7 @@ jobs:
run: ./dist/craft --help
- name: NPM Pack
run: npm pack
- name: Archive Artifacts
- name: Upload Build Artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: ${{ github.sha }}-build
Expand Down Expand Up @@ -111,7 +111,7 @@ jobs:
run: |
cp .nojekyll docs/dist/
cd docs/dist && zip -r ../../gh-pages.zip .
- name: Archive Docs Artifacts
- name: Upload Docs Artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: ${{ github.sha }}-docs
Expand All @@ -120,9 +120,9 @@ jobs:
merge-artifacts:
name: Merge Artifacts
needs:
- artifacts
- build
- docs
if: always() && needs.artifacts.result == 'success' && needs.docs.result == 'success'
if: always() && needs.build.result == 'success' && needs.docs.result == 'success'
runs-on: ubuntu-latest
permissions:
contents: read
Expand Down
1 change: 1 addition & 0 deletions build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ if (process.env.SENTRY_AUTH_TOKEN) {
project: 'craft',
authToken: process.env.SENTRY_AUTH_TOKEN,
sourcemaps: {
assets: ['dist/craft', 'dist/craft.map'],
filesToDeleteAfterUpload: ['dist/**/*.map'],
},
})
Expand Down
33 changes: 24 additions & 9 deletions docs/src/content/docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,17 +207,32 @@ CRAFT_NO_INPUT=0

### Dry-Run Mode

The `--dry-run` flag prevents destructive operations while still allowing reads:
The `--dry-run` flag lets you preview what would happen without making real changes.

**Blocked:**
- File writes (create, modify, delete)
- Git mutations (commit, push, checkout, merge, tag)
- GitHub API mutations (create release, upload assets)
**How it works:**

**Allowed:**
- Reading files and git history
- Fetching from GitHub API
- Git fetch and status checks
Craft creates a temporary git worktree where all local operations run normally (branch creation, file modifications, commits). At the end, it shows a diff of what would change:

```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Dry-run complete. Here's what would change:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Files changed: 2
M CHANGELOG.md
M package.json

diff --git a/CHANGELOG.md b/CHANGELOG.md
...
```

**What's blocked:**
- Git push (nothing leaves your machine)
- GitHub API mutations (no releases, uploads, or changes)

**What's allowed:**
- All local operations (in a temporary worktree)
- Reading from GitHub API (requires `GITHUB_TOKEN`)

:::note
Dry-run still requires `GITHUB_TOKEN` for commands that fetch PR information from GitHub.
Expand Down
48 changes: 38 additions & 10 deletions pnpm-lock.yaml

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

57 changes: 57 additions & 0 deletions src/__tests__/__snapshots__/prepare-dry-run.e2e.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`prepare --dry-run e2e > executes pre-release command and shows diff of changes > pre-release-diff 1`] = `
"
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Dry-run complete. Here's what would change:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Files changed: 1
M package.json

diff --git a/package.json b/package.json
index HASH..HASH 100644
--- a/package.json
+++ b/package.json
@@ -X,Y +X,Y @@
{
"name": "test-package",
- "version": "1.0.0"
+ "version": "1.0.1"
}
\\ No newline at end of file

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[dry-run] No actual changes were made to your repository.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

[info] Checking the local repository status...
[info] Releasing version 1.0.1 from DEFAULT_BRANCH
[info] Preparing to release the version: 1.0.1
[info] [dry-run] Creating temporary worktree at /tmp/craft-XXXXX
[info] Created a new release branch: "release/1.0.1"
[info] Switched to branch "release/1.0.1"
[info] Running the pre-release command...
[info] Pushing the release branch "release/1.0.1"...
[info] [dry-run] Would execute: git.push(origin release/1.0.1 ["--set-upstream"])
[info] View diff at: https://github.com/test-owner/test-repo/compare/release/1.0.1
[success] Done. Do not forget to run "craft publish" to publish the artifacts: $ craft publish 1.0.1
"
`;

exports[`prepare --dry-run e2e > produces consistent output format > dry-run-output 1`] = `
"[info] Checking the local repository status...
[info] Releasing version 1.0.1 from DEFAULT_BRANCH
[info] Preparing to release the version: 1.0.1
[info] [dry-run] Creating temporary worktree at /tmp/craft-XXXXX
[info] Created a new release branch: "release/1.0.1"
[info] Switched to branch "release/1.0.1"
[warn] Not running the pre-release command: no command specified
[info]
[dry-run] No changes would be made.
[info] Pushing the release branch "release/1.0.1"...
[info] [dry-run] Would execute: git.push(origin release/1.0.1 ["--set-upstream"])
[info] View diff at: https://github.com/test-owner/test-repo/compare/release/1.0.1
[success] Done. Do not forget to run "craft publish" to publish the artifacts: $ craft publish 1.0.1
"
`;
53 changes: 26 additions & 27 deletions src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
import { vi, describe, test, expect } from 'vitest';
import { execFile } from 'child_process';
import { describe, test, expect, beforeAll } from 'vitest';
import { execFile, execSync } from 'child_process';
import { promisify } from 'util';
import { resolve } from 'path';
import { existsSync } from 'fs';

const execFileAsync = promisify(execFile);

// Path to the TypeScript source file - we use ts-node to run it directly
const CLI_ENTRY = resolve(__dirname, '../index.ts');
// Path to the built CLI binary - e2e tests should use the actual artifact
const CLI_BIN = resolve(__dirname, '../../dist/craft');

// Ensure the binary is built before running e2e tests
beforeAll(() => {
if (!existsSync(CLI_BIN)) {
console.log('Building craft binary for e2e tests...');
execSync('pnpm build', { cwd: resolve(__dirname, '../..'), stdio: 'inherit' });
}
}, 60000);

describe('CLI smoke tests', () => {
test('CLI starts and shows help without runtime errors', async () => {
// This catches issues like:
// - Missing dependencies
// - Syntax errors
// - Runtime initialization errors (e.g., yargs singleton usage in v18)
const { stdout, stderr } = await execFileAsync(
'npx',
['ts-node', '--transpile-only', CLI_ENTRY, '--help'],
{ env: { ...process.env, NODE_ENV: 'test' } }
);
const { stdout, stderr } = await execFileAsync(CLI_BIN, ['--help'], {
env: { ...process.env, NODE_ENV: 'test' },
});

expect(stdout).toMatch(/<command>/);
expect(stdout).toContain('prepare NEW-VERSION');
Expand All @@ -30,11 +37,9 @@ describe('CLI smoke tests', () => {
}, 30000);

test('CLI shows version without errors', async () => {
const { stdout } = await execFileAsync(
'npx',
['ts-node', '--transpile-only', CLI_ENTRY, '--version'],
{ env: { ...process.env, NODE_ENV: 'test' } }
);
const { stdout } = await execFileAsync(CLI_BIN, ['--version'], {
env: { ...process.env, NODE_ENV: 'test' },
});

// Version should be a semver-like string
expect(stdout.trim()).toMatch(/^\d+\.\d+\.\d+/);
Expand All @@ -43,11 +48,9 @@ describe('CLI smoke tests', () => {
test('CLI exits with error for unknown command', async () => {
// This ensures yargs command parsing works and async handlers are awaited
await expect(
execFileAsync(
'npx',
['ts-node', '--transpile-only', CLI_ENTRY, 'nonexistent-command'],
{ env: { ...process.env, NODE_ENV: 'test' } }
)
execFileAsync(CLI_BIN, ['nonexistent-command'], {
env: { ...process.env, NODE_ENV: 'test' },
})
).rejects.toMatchObject({
code: 1,
});
Expand All @@ -59,14 +62,10 @@ describe('CLI smoke tests', () => {
// We expect it to fail due to missing config, but it should fail gracefully
// not due to premature exit
try {
await execFileAsync(
'npx',
['ts-node', '--transpile-only', CLI_ENTRY, 'targets'],
{
env: { ...process.env, NODE_ENV: 'test' },
cwd: '/tmp', // No .craft.yml here
}
);
await execFileAsync(CLI_BIN, ['targets'], {
env: { ...process.env, NODE_ENV: 'test' },
cwd: '/tmp', // No .craft.yml here
});
} catch (error: any) {
// Should fail with a config error, not a silent exit or unhandled promise
expect(error.stderr || error.stdout).toMatch(
Expand Down
Loading
Loading