-
Notifications
You must be signed in to change notification settings - Fork 3
chore: fix-local-release #538
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,67 +1,201 @@ | ||
| import { Effect, Stream, Console } from 'effect'; | ||
| import { Effect } from 'effect'; | ||
| import { Command } from '@effect/platform'; | ||
| import { FileSystem, Path } from '@effect/platform'; | ||
| import { | ||
| CommandExitError, | ||
| GitStatusError, | ||
| ChangesetError, | ||
| ChangesetConfigError, | ||
| GitRestoreError, | ||
| } from '../errors'; | ||
|
|
||
| export const buildPackages = Command.make('pnpm', 'build').pipe( | ||
| Command.string, | ||
| Stream.tap((line) => Console.log(`Build: ${line}`)), | ||
| Stream.runDrain, | ||
| ); | ||
|
|
||
| // Effect to check git status for staged files | ||
| export const checkGitStatus = Command.make('git', 'status', '--porcelain').pipe( | ||
| Command.string, | ||
| Effect.flatMap((output) => { | ||
| // Check if the output contains lines indicating staged changes (e.g., starting with M, A, D, R, C, U followed by a space) | ||
| const stagedChanges = output.split('\n').some((line) => /^[MADRCU] /.test(line.trim())); | ||
| if (stagedChanges) { | ||
| return Effect.fail( | ||
| 'Git repository has staged changes. Please commit or stash them before releasing.', | ||
| ); | ||
| } | ||
| return Effect.void; // No staged changes | ||
| }), | ||
| // If the command fails (e.g., not a git repo), treat it as an error too. | ||
| Effect.catchAll((error) => Effect.fail(`Git status check command failed: ${error}`)), | ||
| Effect.tapError((error) => Console.error(error)), // Log the specific error message | ||
| Effect.asVoid, // Don't need the output on success | ||
| ); | ||
|
|
||
| // Effect to run changesets snapshot | ||
| export const runChangesetsSnapshot = Command.make( | ||
| 'pnpm', | ||
| 'changeset', | ||
| 'version', | ||
| '--snapshot', | ||
| 'beta', | ||
| ).pipe(Command.exitCode); | ||
|
|
||
| // Effect to start local registry (run in background) | ||
| export const startLocalRegistry = Command.make('pnpm', 'nx', 'local-registry').pipe( | ||
| Command.start, // Starts the process and returns immediately | ||
| Effect.tap(() => | ||
| Console.log('Attempting to start local registry (Verdaccio) in the background...'), | ||
| ), | ||
| Effect.tapError((error) => Console.error(`Failed to start local registry: ${error}`)), | ||
| Effect.asVoid, // We don't need the Process handle for this script's logic | ||
| ); | ||
|
|
||
| export const restoreGitFiles = Command.make('git', 'restore', '.').pipe(Command.start); | ||
|
|
||
| export const publishPackages = Command.make( | ||
| 'pnpm', | ||
| 'publish', | ||
| '-r', | ||
| '--tag', | ||
| 'beta', | ||
| '--registry=http://localhost:4873', | ||
| '--no-git-checks', | ||
| ).pipe( | ||
| Command.string, | ||
| Stream.tap((line) => Console.log(`Publish: ${line}`)), | ||
| Stream.runDrain, | ||
| Effect.tapBoth({ | ||
| onFailure: (error) => Effect.fail(() => Console.error(`Publishing failed: ${error}`)), | ||
| onSuccess: () => Console.log('Packages were published successfully to the local registry.'), | ||
| }), | ||
| Effect.asVoid, | ||
| ); | ||
| const SNAPSHOT_TAG = 'beta'; | ||
| const LOCAL_REGISTRY_URL = 'http://localhost:4873'; | ||
|
|
||
| /** | ||
| * Runs a command with fully inherited stdio and `CI=true` so that tools | ||
| * like Nx use non-interactive output. Fails with `CommandExitError` if | ||
| * the exit code is non-zero. | ||
| * | ||
| * @param description - Human-readable label for the command (e.g. "pnpm build") | ||
| * @param cmd - The Effect Command to execute | ||
| */ | ||
| const runInherited = (description: string, cmd: Command.Command) => | ||
| cmd.pipe( | ||
| Command.env({ CI: 'true' }), | ||
| Command.stdin('inherit'), | ||
| Command.stdout('inherit'), | ||
| Command.stderr('inherit'), | ||
| Command.exitCode, | ||
| Effect.flatMap((code) => | ||
| code === 0 | ||
| ? Effect.void | ||
| : Effect.fail( | ||
| new CommandExitError({ | ||
| message: `${description} exited with code ${code}`, | ||
| cause: `Non-zero exit code: ${code}`, | ||
| command: description, | ||
| exitCode: code, | ||
| }), | ||
| ), | ||
| ), | ||
| ); | ||
|
|
||
| /** Fails with `GitStatusError` if the git working tree has staged changes. */ | ||
| export const assertCleanGitStatus = Effect.gen(function* () { | ||
| const output = yield* Command.make('git', 'status', '--porcelain').pipe(Command.string); | ||
|
|
||
| const hasStagedChanges = output.split('\n').some((line) => /^[MADRCU] /.test(line)); | ||
|
|
||
| if (hasStagedChanges) { | ||
| yield* Effect.fail( | ||
| new GitStatusError({ | ||
| message: 'Git has staged changes. Commit or stash them before releasing.', | ||
| cause: 'Staged changes detected in git working tree', | ||
| }), | ||
| ); | ||
| } | ||
|
|
||
| yield* Effect.log('Git status clean — no staged changes.'); | ||
| }); | ||
|
|
||
| /** Fails with `ChangesetError` if the `.changeset/` directory contains no changeset markdown files. */ | ||
| export const assertChangesetsExist = Effect.gen(function* () { | ||
| const fs = yield* FileSystem.FileSystem; | ||
| const path = yield* Path.Path; | ||
|
|
||
| const changesetDir = path.join(process.cwd(), '.changeset'); | ||
|
|
||
| const files = yield* fs.readDirectory(changesetDir).pipe( | ||
| Effect.catchTag('SystemError', (e) => | ||
| Effect.fail( | ||
| new ChangesetError({ | ||
| message: | ||
| e.reason === 'NotFound' | ||
| ? 'No .changeset directory found.' | ||
| : `Failed to read .changeset directory: ${e.message}`, | ||
| cause: e.reason, | ||
| }), | ||
| ), | ||
| ), | ||
| ); | ||
|
|
||
| const hasChangesets = files | ||
| .filter((f) => f !== 'README.md' && f !== 'config.json') | ||
| .some((f) => f.endsWith('.md')); | ||
|
|
||
| if (!hasChangesets) { | ||
| yield* Effect.fail( | ||
| new ChangesetError({ | ||
| message: 'No changeset files found. Add a changeset before releasing.', | ||
| cause: 'No markdown files in .changeset directory', | ||
| }), | ||
| ); | ||
| } | ||
|
|
||
| yield* Effect.log('Changeset files found.'); | ||
| }); | ||
|
|
||
| /** | ||
| * Versions all packages as snapshot releases via `changeset version --snapshot`. | ||
| * Temporarily disables the GitHub changelog in `.changeset/config.json` to avoid | ||
| * requiring a GITHUB_TOKEN for local releases. The modified config is reverted | ||
| * by `restoreGitFiles`. | ||
| */ | ||
| export const versionSnapshotPackages = Effect.gen(function* () { | ||
| const fs = yield* FileSystem.FileSystem; | ||
| const path = yield* Path.Path; | ||
|
|
||
| const configPath = path.join(process.cwd(), '.changeset', 'config.json'); | ||
|
|
||
| const raw = yield* fs.readFileString(configPath).pipe( | ||
| Effect.catchTag('SystemError', (e) => | ||
| Effect.fail( | ||
| new ChangesetConfigError({ | ||
| message: `Failed to read .changeset/config.json: ${e.message}`, | ||
| cause: e.reason, | ||
| }), | ||
| ), | ||
| ), | ||
| ); | ||
|
|
||
| const config: Record<string, unknown> = yield* Effect.try({ | ||
| try: () => JSON.parse(raw) as Record<string, unknown>, | ||
| catch: (e) => | ||
| new ChangesetConfigError({ | ||
| message: 'Invalid JSON in .changeset/config.json', | ||
| cause: String(e), | ||
| }), | ||
| }); | ||
|
|
||
| if (typeof config !== 'object' || config === null || Array.isArray(config)) { | ||
| yield* Effect.fail( | ||
| new ChangesetConfigError({ | ||
| message: '.changeset/config.json must be a JSON object', | ||
| cause: `Unexpected shape: ${typeof config}`, | ||
| }), | ||
| ); | ||
| } | ||
|
|
||
| config.changelog = false; | ||
| yield* fs.writeFileString(configPath, JSON.stringify(config, null, 2) + '\n').pipe( | ||
| Effect.catchTag('SystemError', (e) => | ||
| Effect.fail( | ||
| new ChangesetConfigError({ | ||
| message: `Failed to write .changeset/config.json: ${e.message}`, | ||
| cause: e.reason, | ||
| }), | ||
| ), | ||
| ), | ||
| ); | ||
|
|
||
| yield* Effect.log('Running changeset version --snapshot...'); | ||
| yield* runInherited( | ||
| 'changeset version --snapshot', | ||
| Command.make('pnpm', 'changeset', 'version', '--snapshot', SNAPSHOT_TAG), | ||
| ); | ||
| yield* Effect.log('Snapshot versioning complete.'); | ||
| }); | ||
|
|
||
| /** Runs `pnpm build` with output visible in the terminal. */ | ||
| export const buildPackages = Effect.gen(function* () { | ||
| yield* runInherited('pnpm build', Command.make('pnpm', 'build')); | ||
| yield* Effect.log('Build complete.'); | ||
| }); | ||
|
|
||
| /** Starts the Verdaccio local registry as a background process. */ | ||
| export const startLocalRegistry = Effect.gen(function* () { | ||
| yield* Command.make('pnpm', 'nx', 'local-registry').pipe(Command.start, Effect.asVoid); | ||
| yield* Effect.log('Verdaccio local registry starting...'); | ||
| }); | ||
|
|
||
| /** Publishes all packages to the local Verdaccio registry. */ | ||
| export const publishToLocalRegistry = Effect.gen(function* () { | ||
| yield* runInherited( | ||
| 'pnpm publish', | ||
| Command.make( | ||
| 'pnpm', | ||
| 'publish', | ||
| '-r', | ||
| '--tag', | ||
| SNAPSHOT_TAG, | ||
| `--registry=${LOCAL_REGISTRY_URL}`, | ||
| '--no-git-checks', | ||
| ), | ||
| ); | ||
| yield* Effect.log('Packages published to local registry.'); | ||
| }); | ||
|
|
||
| /** Restores all modified files in the working tree via `git restore .`. */ | ||
| export const restoreGitFiles = Effect.gen(function* () { | ||
| const code = yield* Command.make('git', 'restore', '.').pipe(Command.exitCode); | ||
|
|
||
| if (code !== 0) { | ||
| yield* Effect.fail( | ||
| new GitRestoreError({ | ||
| message: `git restore exited with code ${code}`, | ||
| cause: `Non-zero exit code: ${code}`, | ||
| }), | ||
| ); | ||
| } | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| import { Data } from 'effect'; | ||
|
|
||
| /** Staged changes detected in git working tree. */ | ||
| export class GitStatusError extends Data.TaggedError('GitStatusError')<{ | ||
| message: string; | ||
| cause: string; | ||
| }> {} | ||
|
|
||
| /** Missing `.changeset/` directory or no changeset markdown files found. */ | ||
| export class ChangesetError extends Data.TaggedError('ChangesetError')<{ | ||
| message: string; | ||
| cause: string; | ||
| }> {} | ||
|
|
||
| /** A shell command exited with a non-zero exit code. */ | ||
| export class CommandExitError extends Data.TaggedError('CommandExitError')<{ | ||
| message: string; | ||
| cause: string; | ||
| command: string; | ||
| exitCode: number; | ||
| }> {} | ||
|
|
||
| /** `.changeset/config.json` is unreadable, contains invalid JSON, or has an unexpected shape. */ | ||
| export class ChangesetConfigError extends Data.TaggedError('ChangesetConfigError')<{ | ||
| message: string; | ||
| cause: string; | ||
| }> {} | ||
|
|
||
| /** Verdaccio registry did not respond within the retry timeout. */ | ||
| export class RegistryNotReadyError extends Data.TaggedError('RegistryNotReadyError')<{ | ||
| message: string; | ||
| cause: string; | ||
| }> {} | ||
|
|
||
| /** `git restore .` failed during cleanup. */ | ||
| export class GitRestoreError extends Data.TaggedError('GitRestoreError')<{ | ||
| message: string; | ||
| cause: string; | ||
| }> {} | ||
|
|
||
| /** Union of all typed errors the release pipeline can produce. */ | ||
| export type ReleaseError = | ||
| | GitStatusError | ||
| | ChangesetError | ||
| | CommandExitError | ||
| | ChangesetConfigError | ||
| | RegistryNotReadyError | ||
| | GitRestoreError; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: ForgeRock/ping-javascript-sdk
Length of output: 1227
🏁 Script executed:
Repository: ForgeRock/ping-javascript-sdk
Length of output: 306
🏁 Script executed:
Repository: ForgeRock/ping-javascript-sdk
Length of output: 241
🏁 Script executed:
Repository: ForgeRock/ping-javascript-sdk
Length of output: 2689
🏁 Script executed:
Repository: ForgeRock/ping-javascript-sdk
Length of output: 4572
🏁 Script executed:
Repository: ForgeRock/ping-javascript-sdk
Length of output: 96
🏁 Script executed:
Repository: ForgeRock/ping-javascript-sdk
Length of output: 55
Add explicit
.jsextension to relative import.The ESLint rule
import/extensions: [2, 'ignorePackages']requires file extensions for relative imports. Change../errorsto../errors.jsto resolve this linting error.Suggested fix
import { CommandExitError, GitStatusError, ChangesetError, ChangesetConfigError, GitRestoreError, -} from '../errors'; +} from '../errors.js';📝 Committable suggestion
🧰 Tools
🪛 ESLint
[error] 10-10: Missing file extension for "../errors"
(import/extensions)
🤖 Prompt for AI Agents