Skip to content
Open
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
20 changes: 0 additions & 20 deletions .changeset/fetch-timeout-options.md

This file was deleted.

7 changes: 0 additions & 7 deletions .changeset/observer-cjs-rebuild.md

This file was deleted.

20 changes: 20 additions & 0 deletions .changeset/pre.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"mode": "pre",
"tag": "beta",
"initialVersions": {
"@logosdx/dom": "2.0.15",
"@logosdx/fetch": "7.0.3",
"@logosdx/hooks": "0.0.1",
"@logosdx/kit": "4.0.3",
"@logosdx/localize": "1.0.19",
"@logosdx/observer": "2.2.0",
"@logosdx/state-machine": "1.0.19",
"@logosdx/storage": "1.0.19",
"@logosdx/utils": "5.0.0",
"@logosdx/tests": "0.0.1"
},
"changesets": [
"rich-pears-jam",
"thin-crews-vanish"
]
}
13 changes: 13 additions & 0 deletions .changeset/rich-pears-jam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@logosdx/hooks": major
---

Initial beta release of @logosdx/hooks - a lightweight, type-safe hook system for extending function behavior.

Features:

- `HookEngine` class for wrapping functions with before/after/error extension points
- `make()` and `wrap()` methods for creating hookable functions
- Extension options: `once`, `ignoreOnFail`
- Context methods: `setArgs`, `setResult`, `returnEarly`, `fail`, `removeHook`
- `HookError` and `isHookError()` for typed error handling
35 changes: 35 additions & 0 deletions .changeset/thin-crews-vanish.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
"@logosdx/fetch": major
---

## Breaking Changes

### `.headers` and `.params` getters now return lowercase method keys

Method keys in the headers/params getters are now normalized to lowercase.

**Before:**
```ts
const { POST: postHeaders } = api.headers;
const { GET: getParams } = api.params;
```

**After:**
```ts
const { post: postHeaders } = api.headers;
const { get: getParams } = api.params;
```

**Migration:** Update any code accessing method-specific headers/params via the getters to use lowercase method names.

## Added

* `feat(fetch):` Add `PropertyStore` for unified header/param management with method-specific overrides
* `feat(fetch):` Add predicate function support to `invalidatePath()` for custom cache key matching
* `feat(fetch):` Add `endpointSerializer` and `requestSerializer` for customizable cache/dedupe keys
* `feat(fetch):` Export `ResiliencePolicy`, `DedupePolicy`, `CachePolicy`, `RateLimitPolicy` classes

## Changed

* `refactor(fetch):` Internal refactor to use `PropertyStore` for headers/params (API unchanged)
* `refactor(fetch):` Normalize HTTP methods to lowercase internally for consistent storage
48 changes: 48 additions & 0 deletions .claude/skills/release-workflow/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
name: release-workflow
description: "Use when releasing changes to npm. Automates changeset creation, PR workflow, and publish to release branch."
---

# Release Workflow


## Quick Start

# From a feature branch with changes ready:
/release-workflow


## Critical Rules

1. **Must be on feature branch** - Not master/main/release. Create branch first if needed.
2. **Session reports drive changesets** - Reads `tmp/reports/new/` for context.
3. **Two-phase process** - First: changesets + commit. Second: automated PR flow.
4. **Script handles git/github** - `release.mjs` automates CI waits and merges.


## Workflow

1. Read session reports from `tmp/reports/new/` to understand changes
2. Invoke `/changeset-writer` skill to create changesets
3. Invoke `/git-committer` skill to commit all changes
4. Run `pnpm zx .claude/skills/release-workflow/release.mjs` to automate:
- Push branch, create PR to master
- Wait for CI, merge PR (squash)
- Wait for Version Packages PR, merge it
- Merge master to release, wait for publish


## Manual Steps (if script fails)

| Step | Command |
|------|---------|
| Push & PR | `git push -u origin HEAD && gh pr create --base master --fill` |
| Watch CI | `gh run watch $(gh run list -b BRANCH -L1 --json databaseId -q '.[0].databaseId')` |
| Merge PR | `gh pr merge --squash --delete-branch` |
| Version PR | `gh pr list --search "Version Packages"` then `gh pr merge NUM --squash` |
| To release | `git checkout release && git merge master && git push` |


## References

- [release.mjs](release.mjs) - Automated workflow script
244 changes: 244 additions & 0 deletions .claude/skills/release-workflow/release.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
#!/usr/bin/env zx

import 'zx/globals';

$.verbose = false;

const log = {
info: (msg) => console.log(chalk.cyan(`→ ${msg}`)),
success: (msg) => console.log(chalk.green(`✓ ${msg}`)),
warn: (msg) => console.log(chalk.yellow(`⚠ ${msg}`)),
error: (msg) => console.log(chalk.red(`✗ ${msg}`)),
step: (num, msg) => console.log(chalk.blue(`\n[${num}] ${msg}`)),
};

const PROTECTED_BRANCHES = ['master', 'main', 'release'];

/**
* Get current git branch
*/
const getCurrentBranch = async () => {

const { stdout } = await $`git branch --show-current`;
return stdout.trim();
};

/**
* Check if on a protected branch
*/
const isProtectedBranch = (branch) => {

return PROTECTED_BRANCHES.includes(branch);
};

/**
* Get the latest CI run ID for a branch
*/
const getLatestRunId = async (branch) => {

const { stdout } = await $`gh run list --branch ${branch} --limit 1 --json databaseId -q '.[0].databaseId'`;
return stdout.trim();
};

/**
* Wait for a CI run to complete
*/
const waitForCI = async (runId) => {

log.info(`Waiting for CI run ${runId}...`);

try {

await $`gh run watch ${runId} --exit-status`;
log.success('CI passed');
return true;
}
catch (err) {

log.error('CI failed');
await $`gh run view ${runId} --log-failed`.pipe(process.stderr);
return false;
}
};

/**
* Find the Version Packages PR
*/
const findVersionPR = async (maxAttempts = 10) => {

log.info('Waiting for Version Packages PR...');

for (let i = 0; i < maxAttempts; i++) {

await sleep(3000);

const { stdout } = await $`gh pr list --state open --search "Version Packages" --json number -q '.[0].number'`;
const prNum = stdout.trim();

if (prNum) {

log.success(`Found Version Packages PR #${prNum}`);
return prNum;
}

log.info(`Attempt ${i + 1}/${maxAttempts}...`);
}

return null;
};

/**
* Main release workflow
*/
const main = async () => {

console.log(chalk.bold.magenta('\n🚀 Release Workflow\n'));

// Step 1: Verify branch
log.step(1, 'Verifying branch');
const branch = await getCurrentBranch();

if (isProtectedBranch(branch)) {

log.error(`Cannot run release workflow from protected branch: ${branch}`);
log.info('Create a feature branch first: git checkout -b feat/your-feature');
process.exit(1);
}

log.success(`On feature branch: ${branch}`);

// Step 2: Check for uncommitted changes
log.step(2, 'Checking for uncommitted changes');
const { stdout: status } = await $`git status --porcelain`;

if (status.trim()) {

log.warn('You have uncommitted changes:');
console.log(status);
log.info('Please commit or stash changes before running release workflow');
log.info('Use /changeset-writer and /git-committer skills first');
process.exit(1);
}

log.success('Working directory clean');

// Step 3: Push and create PR
log.step(3, 'Pushing branch and creating PR');

try {

await $`git push -u origin ${branch}`;
log.success('Branch pushed');
}
catch (err) {

log.error('Failed to push branch');
throw err;
}

// Check if PR already exists
const { stdout: existingPR } = await $`gh pr list --head ${branch} --json number -q '.[0].number'`;

let prNumber;

if (existingPR.trim()) {

prNumber = existingPR.trim();
log.info(`PR #${prNumber} already exists`);
}
else {

const { stdout: prUrl } = await $`gh pr create --base master --fill`;
prNumber = prUrl.trim().split('/').pop();
log.success(`Created PR #${prNumber}`);
}

// Step 4: Wait for CI on feature PR
log.step(4, 'Waiting for CI on feature PR');
await sleep(5000); // Give CI time to start

const runId = await getLatestRunId(branch);

if (!runId) {

log.error('Could not find CI run');
process.exit(1);
}

const ciPassed = await waitForCI(runId);

if (!ciPassed) {

log.error('CI failed. Fix issues and push again.');
process.exit(1);
}

// Step 5: Merge feature PR
log.step(5, 'Merging feature PR');

await $`gh pr merge ${prNumber} --squash --delete-branch`;
log.success('Feature PR merged');

// Switch to master and pull
await $`git checkout master`;
await $`git pull origin master`;
log.success('Switched to master and pulled');

// Step 6: Wait for Version Packages PR
log.step(6, 'Waiting for Version Packages PR');

const versionPRNumber = await findVersionPR();

if (!versionPRNumber) {

log.error('Version Packages PR not found after 30 seconds');
log.info('Check if changesets exist and CI completed successfully');
process.exit(1);
}

// Step 7: Merge Version Packages PR
log.step(7, 'Merging Version Packages PR');

await $`gh pr merge ${versionPRNumber} --squash --delete-branch`;
log.success('Version Packages PR merged');

await $`git pull origin master`;
log.success('Pulled version changes');

// Step 8: Merge to release branch
log.step(8, 'Merging master to release branch');

await $`git checkout release`;
await $`git merge master`;
await $`git push origin release`;
log.success('Pushed to release branch');

// Step 9: Wait for publish
log.step(9, 'Waiting for publish workflow');
await sleep(5000);

const publishRunId = await getLatestRunId('release');

if (publishRunId) {

const publishPassed = await waitForCI(publishRunId);

if (!publishPassed) {

log.error('Publish failed. Check logs and retry.');
process.exit(1);
}
}

// Switch back to master
await $`git checkout master`;

console.log(chalk.bold.green('\n✅ Release workflow complete!\n'));
log.info('Packages have been published to npm');
};

main().catch((err) => {

log.error(`Workflow failed: ${err.message}`);
process.exit(1);
});
Loading