From ddba720260f58f1f74c3e670489087019571fd96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Zori=C4=87?= Date: Fri, 8 May 2026 06:33:28 +0200 Subject: [PATCH 01/12] docs: add GitHub release design spec --- .../specs/2026-05-08-github-release-design.md | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-08-github-release-design.md diff --git a/docs/superpowers/specs/2026-05-08-github-release-design.md b/docs/superpowers/specs/2026-05-08-github-release-design.md new file mode 100644 index 0000000..94e6ad4 --- /dev/null +++ b/docs/superpowers/specs/2026-05-08-github-release-design.md @@ -0,0 +1,115 @@ +# GitHub Release Design + +## Goal + +Create a GitHub release (with the new version's changelog entry as the body) as the final step of `node scripts/publishPackages.ts --publish`. Dry-run mode validates configuration (token, remote URL) but skips the API call. + +## Architecture + +### Modified files + +| File | Change | +|------|--------| +| `scripts/features/PublishPackages/abstractions/ChangelogWriter.ts` | `write()` return type `void` → `string` | +| `scripts/features/PublishPackages/ChangelogWriter.ts` | Return the formatted entry from `write()` | +| `scripts/features/PublishPackages/abstractions/GitRepository.ts` | Add `getRemoteUrl(name: string): string` | +| `scripts/features/PublishPackages/GitRepository.ts` | Implement via `git remote get-url ` | +| `scripts/features/PublishPackages/abstractions/index.ts` | Re-export `GithubRelease` | +| `scripts/features/PublishPackages/PublishOrchestrator.ts` | Capture entry from `changelogWriter.write()`, pass to `githubRelease.createRelease()`; add `GithubRelease` as 6th constructor dependency; `run()` becomes `async` | +| `scripts/features/PublishPackages/index.ts` | Register `GithubReleaseImpl`; `await` the `run()` call | + +### New files + +| File | Responsibility | +|------|---------------| +| `scripts/features/PublishPackages/abstractions/GithubRelease.ts` | `IGithubRelease` with `createRelease(tag, title, body): Promise` | +| `scripts/features/PublishPackages/GithubRelease.ts` | `@octokit/rest` implementation | + +### New dependency + +```sh +yarn add --dev @octokit/rest +``` + +`@octokit/rest` is a devDependency — it's only used in scripts, never in the published package. + +--- + +## Data Flow + +Publish sequence (real run): + +``` +getLatestVersion + → computeVersion + → changelogWriter.write(newVersion, commits) ← now returns formatted entry string + → updateDistPackageJson + → npm.publish(distDir) + → git.createTag(`v${newVersion}`) + → githubRelease.createRelease(`v${newVersion}`, `v${newVersion}`, entryString) +``` + +`changelogWriter.write()` returns the formatted changelog entry (e.g. `"## [1.2.0] — 2026-05-08\n### Added\n- ..."`) — the same text it already builds before writing to disk. The orchestrator captures this and passes it directly as the release body. + +--- + +## GithubRelease Implementation + +`GithubRelease` takes `ProjectConfig` and `GitRepository` as constructor dependencies. + +`createRelease(tag, title, body)` flow: + +1. **Parse owner/repo** — call `git.getRemoteUrl("origin")`, match against: + - HTTPS: `https://github.com/([^/]+)/([^/.]+)(?:\.git)?` + - SSH: `git@github\.com:([^/]+)/([^/.]+)(?:\.git)?` + - Throw `Error("Cannot parse GitHub owner/repo from remote URL: ")` if neither matches. + +2. **Read token** — `process.env.GITHUB_TOKEN`. Throw `Error("GITHUB_TOKEN env var is required to create a GitHub release")` if absent. + +3. **Dry-run gate** — if `config.dryRun`: log `[dry run] would create GitHub release for /` and return. Steps 1 and 2 always run, so misconfiguration is caught before a real publish is attempted. + +4. **Create release** — `new Octokit({ auth: token }).rest.repos.createRelease({ owner, repo, tag_name: tag, name: title, body })`. + +--- + +## Error Handling + +| Failure | Behaviour | +|---------|-----------| +| Remote URL unparseable | Throw before any API call; operator fixes git remote | +| `GITHUB_TOKEN` missing | Throw before any API call | +| Octokit call fails | Error propagates; npm publish + git tag already succeeded — operator creates release manually via `gh release create` | + +Errors in steps 1–2 fail fast in both dry-run and real-run, so CI catches misconfiguration during the safe dry-run pass. + +--- + +## Dry-Run Behaviour + +Full dry-run log output (all existing lines plus new line): + +``` +Dry run — pass --publish to actually publish. +Latest published: 1.1.0 +minor bump: 1.1.0 → 1.2.0 +Commits: + feat(stdlib): add HttpTool +[dry run] would update CHANGELOG.md +[dry run] would publish @webiny/stdlib@1.2.0 +[dry run] would tag v1.2.0 +[dry run] would create GitHub release v1.2.0 for webiny/webiny-node-tools +``` + +URL parsing and token presence are validated before the dry-run gate, so the last line only appears when config is correct. + +--- + +## Testing + +**`ChangelogWriter.write` return value** — existing tests updated to assert the returned string matches the expected entry format (same text that gets prepended to `CHANGELOG.md`). + +**`GithubRelease`** — no script test suite exists. Dry-run mode is the practical test: with `GITHUB_TOKEN` set and a parseable remote, `node scripts/publishPackages.ts` must log the dry-run release line. Octokit call is not exercised in dry-run. + +**`GitRepository.getRemoteUrl`** — manually verified against the actual repo; both SSH and HTTPS URL forms handled. + +**End-to-end dry-run** — the primary verification gate before a real release. From e52e18dc91c8ee5684906a37a3082c17446e0ae2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Zori=C4=87?= Date: Fri, 8 May 2026 06:43:18 +0200 Subject: [PATCH 02/12] docs: add GitHub release implementation plan --- .../plans/2026-05-08-github-release.md | 637 ++++++++++++++++++ 1 file changed, 637 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-08-github-release.md diff --git a/docs/superpowers/plans/2026-05-08-github-release.md b/docs/superpowers/plans/2026-05-08-github-release.md new file mode 100644 index 0000000..527918d --- /dev/null +++ b/docs/superpowers/plans/2026-05-08-github-release.md @@ -0,0 +1,637 @@ +# GitHub Release Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Create a GitHub release (with the new version's changelog entry as the body) as the final step of `node scripts/publishPackages.ts --publish`. + +**Architecture:** `ChangelogWriter.write()` returns the formatted entry string it already builds internally. A new `GithubRelease` abstraction + Octokit implementation is added to the DI container. `PublishOrchestrator` captures the entry, calls `githubRelease.createRelease()` as its last step, and becomes `async`. Dry-run validates token + remote URL but skips the API call. + +**Tech Stack:** `@octokit/rest`, Node 24 ESM, `@webiny/di`, existing scripts DI patterns + +--- + +## Constraints (read before touching any file) + +- **Scripts run under Node 24 strip-only mode.** No TypeScript parameter properties (`private readonly x` in constructor signature). Always declare fields explicitly above the constructor and assign in the constructor body. +- **Script imports use `.ts` extensions**, not `.js`. (`import { X } from "./Foo.ts"`) +- **`@octokit/rest` goes in `devDependencies`** — it's only used in scripts, never in the published package. + +--- + +## File Map + +| File | Action | What changes | +|------|--------|-------------| +| `package.json` | Modify | Add `@octokit/rest` devDep | +| `scripts/features/PublishPackages/abstractions/ChangelogWriter.ts` | Modify | `write()` return type `void` → `string` | +| `scripts/features/PublishPackages/ChangelogWriter.ts` | Modify | Return the formatted entry from `write()` | +| `scripts/features/PublishPackages/abstractions/GitRepository.ts` | Modify | Add `getRemoteUrl(name: string): string` | +| `scripts/features/PublishPackages/GitRepository.ts` | Modify | Implement `getRemoteUrl` via `git remote get-url` | +| `scripts/features/PublishPackages/abstractions/GithubRelease.ts` | Create | `IGithubRelease` with `createRelease()` | +| `scripts/features/PublishPackages/abstractions/index.ts` | Modify | Re-export `GithubRelease` | +| `scripts/features/PublishPackages/GithubRelease.ts` | Create | Octokit implementation | +| `scripts/features/PublishPackages/abstractions/PublishOrchestrator.ts` | Modify | `run(): void` → `run(): Promise` | +| `scripts/features/PublishPackages/PublishOrchestrator.ts` | Modify | Add `GithubRelease` dep, capture entry, call `createRelease`, make async | +| `scripts/features/PublishPackages/index.ts` | Modify | Register `GithubReleaseImpl`, `await run()` | +| `scripts/publishPackages.ts` | Modify | Top-level `await publish(root)` | + +--- + +## Task 1: Install @octokit/rest + +**Files:** +- Modify: `package.json` + +- [ ] **Step 1: Add the devDependency** + +```sh +yarn add --dev @octokit/rest +``` + +- [ ] **Step 2: Verify it installed** + +```sh +node -e "import('@octokit/rest').then(m => console.log('ok:', typeof m.Octokit))" +``` + +Expected: `ok: function` + +- [ ] **Step 3: Commit** + +```sh +git add package.json yarn.lock +git commit -m "chore(scripts): add @octokit/rest devDependency" +``` + +--- + +## Task 2: ChangelogWriter returns the entry string + +**Files:** +- Modify: `scripts/features/PublishPackages/abstractions/ChangelogWriter.ts` +- Modify: `scripts/features/PublishPackages/ChangelogWriter.ts` + +- [ ] **Step 1: Update the abstraction** + +Full contents of `scripts/features/PublishPackages/abstractions/ChangelogWriter.ts`: + +```ts +import { Abstraction } from "@webiny/di"; + +export interface IChangelogWriter { + /** + * Prepends a new release entry to CHANGELOG.md at the repo root. + * Returns the formatted entry string (same text that was prepended). + */ + write(version: string, commits: string[]): string; +} + +export const ChangelogWriter = new Abstraction("Scripts/ChangelogWriter"); + +export namespace ChangelogWriter { + export type Interface = IChangelogWriter; +} +``` + +- [ ] **Step 2: Update the implementation** + +In `scripts/features/PublishPackages/ChangelogWriter.ts`, change only the `write` method body: + +```ts +public write(version: string, commits: string[]): string { + const sections = this.groupBySection(commits); + const entry = this.formatEntry(version, sections); + this.prepend(entry); + return entry; +} +``` + +- [ ] **Step 3: Typecheck** + +```sh +yarn typecheck +``` + +Expected: no errors. + +- [ ] **Step 4: Commit** + +```sh +git add scripts/features/PublishPackages/abstractions/ChangelogWriter.ts \ + scripts/features/PublishPackages/ChangelogWriter.ts +git commit -m "refactor(scripts): ChangelogWriter.write() returns the formatted entry" +``` + +--- + +## Task 3: GitRepository.getRemoteUrl() + +**Files:** +- Modify: `scripts/features/PublishPackages/abstractions/GitRepository.ts` +- Modify: `scripts/features/PublishPackages/GitRepository.ts` + +- [ ] **Step 1: Update the abstraction** + +Full contents of `scripts/features/PublishPackages/abstractions/GitRepository.ts`: + +```ts +import { Abstraction } from "@webiny/di"; + +export interface IGitRepository { + /** Returns true if the given tag exists in the repository. */ + tagExists(tag: string): boolean; + /** Returns commit subjects since the given ref, or all commits if ref is null. */ + commitsSince(ref: string | null): string[]; + /** Creates a lightweight tag at HEAD. */ + createTag(tag: string): void; + /** Returns the fetch URL of the named remote. Throws if the remote does not exist. */ + getRemoteUrl(name: string): string; +} + +export const GitRepository = new Abstraction("Scripts/GitRepository"); + +export namespace GitRepository { + export type Interface = IGitRepository; +} +``` + +- [ ] **Step 2: Add getRemoteUrl to GitRepositoryImpl** + +Full contents of `scripts/features/PublishPackages/GitRepository.ts`: + +```ts +import { execFileSync } from "node:child_process"; +import { GitRepository as GitRepositoryAbstraction } from "./abstractions/GitRepository.ts"; +import { ProjectConfig } from "./abstractions/ProjectConfig.ts"; + +class GitRepositoryImpl implements GitRepositoryAbstraction.Interface { + private readonly config: ProjectConfig.Interface; + + public constructor(config: ProjectConfig.Interface) { + this.config = config; + } + + public tagExists(tag: string): boolean { + try { + execFileSync("git", ["rev-parse", "--verify", tag], { + cwd: this.config.rootDir, + stdio: "pipe" + }); + return true; + } catch { + return false; + } + } + + public commitsSince(ref: string | null): string[] { + const args = ref ? ["log", `${ref}..HEAD`, "--format=%s"] : ["log", "--format=%s"]; + return execFileSync("git", args, { cwd: this.config.rootDir, encoding: "utf8" }) + .trim() + .split("\n") + .filter(Boolean); + } + + public createTag(tag: string): void { + execFileSync("git", ["tag", tag], { cwd: this.config.rootDir }); + } + + public getRemoteUrl(name: string): string { + return execFileSync("git", ["remote", "get-url", name], { + cwd: this.config.rootDir, + encoding: "utf8" + }).trim(); + } +} + +export const GitRepository = GitRepositoryAbstraction.createImplementation({ + implementation: GitRepositoryImpl, + dependencies: [ProjectConfig] +}); +``` + +- [ ] **Step 3: Typecheck** + +```sh +yarn typecheck +``` + +Expected: no errors. + +- [ ] **Step 4: Commit** + +```sh +git add scripts/features/PublishPackages/abstractions/GitRepository.ts \ + scripts/features/PublishPackages/GitRepository.ts +git commit -m "feat(scripts): add GitRepository.getRemoteUrl()" +``` + +--- + +## Task 4: GithubRelease abstraction + +**Files:** +- Create: `scripts/features/PublishPackages/abstractions/GithubRelease.ts` +- Modify: `scripts/features/PublishPackages/abstractions/index.ts` + +- [ ] **Step 1: Create the abstraction** + +`scripts/features/PublishPackages/abstractions/GithubRelease.ts`: + +```ts +import { Abstraction } from "@webiny/di"; + +export interface IGithubRelease { + /** + * Creates a GitHub release for the given tag. + * In dry-run mode, validates config (token + remote URL) but skips the API call. + */ + createRelease(tag: string, title: string, body: string): Promise; +} + +export const GithubRelease = new Abstraction("Scripts/GithubRelease"); + +export namespace GithubRelease { + export type Interface = IGithubRelease; +} +``` + +- [ ] **Step 2: Re-export from abstractions/index.ts** + +Full contents of `scripts/features/PublishPackages/abstractions/index.ts`: + +```ts +export { ProjectConfig } from "./ProjectConfig.ts"; +export { NpmRegistry } from "./NpmRegistry.ts"; +export { GitRepository } from "./GitRepository.ts"; +export { VersionStrategy } from "./VersionStrategy.ts"; +export type { VersionResult } from "./VersionStrategy.ts"; +export { ChangelogWriter } from "./ChangelogWriter.ts"; +export { PublishOrchestrator } from "./PublishOrchestrator.ts"; +export { GithubRelease } from "./GithubRelease.ts"; +``` + +- [ ] **Step 3: Typecheck** + +```sh +yarn typecheck +``` + +Expected: no errors. + +- [ ] **Step 4: Commit** + +```sh +git add scripts/features/PublishPackages/abstractions/GithubRelease.ts \ + scripts/features/PublishPackages/abstractions/index.ts +git commit -m "feat(scripts): add GithubRelease abstraction" +``` + +--- + +## Task 5: GithubRelease Octokit implementation + +**Files:** +- Create: `scripts/features/PublishPackages/GithubRelease.ts` + +- [ ] **Step 1: Create the implementation** + +`scripts/features/PublishPackages/GithubRelease.ts`: + +```ts +import { Octokit } from "@octokit/rest"; +import { GithubRelease as GithubReleaseAbstraction } from "./abstractions/GithubRelease.ts"; +import { ProjectConfig } from "./abstractions/ProjectConfig.ts"; +import { GitRepository } from "./abstractions/GitRepository.ts"; + +const HTTPS_RE = /https:\/\/github\.com\/([^/]+)\/([^/.]+?)(?:\.git)?$/; +const SSH_RE = /git@github\.com:([^/]+)\/([^/.]+?)(?:\.git)?$/; + +function parseGithubRepo(url: string): { owner: string; repo: string } { + const https = HTTPS_RE.exec(url); + if (https) { + return { owner: https[1]!, repo: https[2]! }; + } + const ssh = SSH_RE.exec(url); + if (ssh) { + return { owner: ssh[1]!, repo: ssh[2]! }; + } + throw new Error(`Cannot parse GitHub owner/repo from remote URL: ${url}`); +} + +class GithubReleaseImpl implements GithubReleaseAbstraction.Interface { + private readonly config: ProjectConfig.Interface; + private readonly git: GitRepository.Interface; + + public constructor(config: ProjectConfig.Interface, git: GitRepository.Interface) { + this.config = config; + this.git = git; + } + + public async createRelease(tag: string, title: string, body: string): Promise { + const url = this.git.getRemoteUrl("origin"); + const { owner, repo } = parseGithubRepo(url); + + const token = process.env["GITHUB_TOKEN"]; + if (!token) { + throw new Error("GITHUB_TOKEN env var is required to create a GitHub release"); + } + + if (this.config.dryRun) { + console.log(`[dry run] would create GitHub release ${tag} for ${owner}/${repo}`); + return; + } + + const octokit = new Octokit({ auth: token }); + await octokit.rest.repos.createRelease({ + owner, + repo, + tag_name: tag, + name: title, + body + }); + console.log(`Created GitHub release ${tag}`); + } +} + +export const GithubRelease = GithubReleaseAbstraction.createImplementation({ + implementation: GithubReleaseImpl, + dependencies: [ProjectConfig, GitRepository] +}); +``` + +- [ ] **Step 2: Typecheck** + +```sh +yarn typecheck +``` + +Expected: no errors. + +- [ ] **Step 3: Commit** + +```sh +git add scripts/features/PublishPackages/GithubRelease.ts +git commit -m "feat(scripts): add GithubRelease Octokit implementation" +``` + +--- + +## Task 6: Wire into PublishOrchestrator and entry points + +**Files:** +- Modify: `scripts/features/PublishPackages/abstractions/PublishOrchestrator.ts` +- Modify: `scripts/features/PublishPackages/PublishOrchestrator.ts` +- Modify: `scripts/features/PublishPackages/index.ts` +- Modify: `scripts/publishPackages.ts` + +- [ ] **Step 1: Make run() async in the abstraction** + +Full contents of `scripts/features/PublishPackages/abstractions/PublishOrchestrator.ts`: + +```ts +import { Abstraction } from "@webiny/di"; + +export interface IPublishOrchestrator { + run(): Promise; +} + +export const PublishOrchestrator = new Abstraction( + "Scripts/PublishOrchestrator" +); + +export namespace PublishOrchestrator { + export type Interface = IPublishOrchestrator; +} +``` + +- [ ] **Step 2: Update PublishOrchestrator.ts** + +Full contents of `scripts/features/PublishPackages/PublishOrchestrator.ts`: + +```ts +import { readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; + +interface DistPackageJson { + version: string; + dependencies?: Record; + [key: string]: unknown; +} + +import { PublishOrchestrator as PublishOrchestratorAbstraction } from "./abstractions/PublishOrchestrator.ts"; +import { ProjectConfig } from "./abstractions/ProjectConfig.ts"; +import { NpmRegistry } from "./abstractions/NpmRegistry.ts"; +import { GitRepository } from "./abstractions/GitRepository.ts"; +import { VersionStrategy } from "./abstractions/VersionStrategy.ts"; +import { ChangelogWriter } from "./abstractions/ChangelogWriter.ts"; +import { GithubRelease } from "./abstractions/GithubRelease.ts"; + +class PublishOrchestratorImpl implements PublishOrchestratorAbstraction.Interface { + private readonly config: ProjectConfig.Interface; + private readonly npm: NpmRegistry.Interface; + private readonly git: GitRepository.Interface; + private readonly versionStrategy: VersionStrategy.Interface; + private readonly changelogWriter: ChangelogWriter.Interface; + private readonly githubRelease: GithubRelease.Interface; + + public constructor( + config: ProjectConfig.Interface, + npm: NpmRegistry.Interface, + git: GitRepository.Interface, + versionStrategy: VersionStrategy.Interface, + changelogWriter: ChangelogWriter.Interface, + githubRelease: GithubRelease.Interface + ) { + this.config = config; + this.npm = npm; + this.git = git; + this.versionStrategy = versionStrategy; + this.changelogWriter = changelogWriter; + this.githubRelease = githubRelease; + } + + public async run(): Promise { + const { rootDir, packageName } = this.config; + + const published = this.npm.getLatestVersion(packageName) ?? "0.0.0"; + console.log(`Latest published: ${published}`); + + const releaseTag = `v${published}`; + const since = this.git.tagExists(releaseTag) ? releaseTag : null; + const commits = this.git.commitsSince(since); + + if (commits.length === 0) { + console.log("No new commits since last release. Nothing to publish."); + return; + } + + const result = this.versionStrategy.computeVersion(published, commits); + if ("error" in result) { + console.error(`Publish aborted: ${result.error}`); + process.exit(1); + } + + const { newVersion, bumpType } = result; + console.log(`${bumpType} bump: ${published} → ${newVersion}`); + console.log("Commits:"); + for (const commit of commits) { + console.log(` ${commit}`); + } + + if (this.config.dryRun) { + console.log("[dry run] would update CHANGELOG.md"); + console.log(`[dry run] would publish ${packageName}@${newVersion}`); + console.log(`[dry run] would tag v${newVersion}`); + await this.githubRelease.createRelease(`v${newVersion}`, `v${newVersion}`, ""); + return; + } + + const entry = this.changelogWriter.write(newVersion, commits); + console.log("Updated CHANGELOG.md"); + + const distDir = join(rootDir, "dist"); + const pkgJsonPath = join(distDir, "package.json"); + const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf8")) as DistPackageJson; + pkgJson.version = newVersion; + if (pkgJson.dependencies !== undefined) { + for (const dep of Object.keys(pkgJson.dependencies)) { + if (pkgJson.dependencies[dep] === "0.0.0") { + pkgJson.dependencies[dep] = newVersion; + } + } + } + writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2) + "\n"); + + console.log(`Publishing ${packageName}@${newVersion}...`); + this.npm.publish(distDir); + + this.git.createTag(`v${newVersion}`); + console.log(`Tagged v${newVersion}`); + + await this.githubRelease.createRelease(`v${newVersion}`, `v${newVersion}`, entry); + } +} + +export const PublishOrchestrator = PublishOrchestratorAbstraction.createImplementation({ + implementation: PublishOrchestratorImpl, + dependencies: [ProjectConfig, NpmRegistry, GitRepository, VersionStrategy, ChangelogWriter, GithubRelease] +}); +``` + +- [ ] **Step 3: Update index.ts** + +Full contents of `scripts/features/PublishPackages/index.ts`: + +```ts +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { Container } from "@webiny/di"; +import { ProjectConfig, PublishOrchestrator } from "./abstractions/index.ts"; +import { NpmRegistry as NpmRegistryImpl } from "./NpmRegistry.ts"; +import { GitRepository as GitRepositoryImpl } from "./GitRepository.ts"; +import { VersionStrategy as VersionStrategyImpl } from "./VersionStrategy.ts"; +import { ChangelogWriter as ChangelogWriterImpl } from "./ChangelogWriter.ts"; +import { PublishOrchestrator as PublishOrchestratorImpl } from "./PublishOrchestrator.ts"; +import { GithubRelease as GithubReleaseImpl } from "./GithubRelease.ts"; + +export async function run(rootDir: string): Promise { + const dryRun = !process.argv.includes("--publish"); + if (dryRun) { + console.log("Dry run — pass --publish to actually publish."); + } + + const pkgJson = JSON.parse(readFileSync(join(rootDir, "package.json"), "utf8")) as { + name: string; + }; + const container = new Container(); + container.registerInstance(ProjectConfig, { rootDir, packageName: pkgJson.name, dryRun }); + container.register(NpmRegistryImpl).inSingletonScope(); + container.register(GitRepositoryImpl).inSingletonScope(); + container.register(VersionStrategyImpl).inSingletonScope(); + container.register(ChangelogWriterImpl).inSingletonScope(); + container.register(GithubReleaseImpl).inSingletonScope(); + container.register(PublishOrchestratorImpl).inSingletonScope(); + await container.resolve(PublishOrchestrator).run(); +} +``` + +- [ ] **Step 4: Update scripts/publishPackages.ts** + +Full contents of `scripts/publishPackages.ts`: + +```ts +import { fileURLToPath } from "node:url"; +import { run as build } from "./features/BuildPackages/index.ts"; +import { run as publish } from "./features/PublishPackages/index.ts"; + +const root = fileURLToPath(new URL("..", import.meta.url)); +build(root); +await publish(root); +``` + +- [ ] **Step 5: Typecheck** + +```sh +yarn typecheck +``` + +Expected: no errors. + +- [ ] **Step 6: Dry-run end-to-end test** + +```sh +GITHUB_TOKEN=test-token node scripts/publishPackages.ts +``` + +Expected output ends with: + +``` +[dry run] would create GitHub release v for webiny/webiny-node-tools +``` + +(If there are no new commits since the last tag, you'll see `No new commits since last release` instead — that's correct behavior. The token/URL checks only run when there are commits to release.) + +- [ ] **Step 7: Run full pre-commit chain** + +```sh +yarn format:fix && yarn lint:fix && yarn typecheck && yarn build && yarn test:coverage +``` + +Expected: all pass. + +- [ ] **Step 8: Commit** + +```sh +git add scripts/features/PublishPackages/abstractions/PublishOrchestrator.ts \ + scripts/features/PublishPackages/PublishOrchestrator.ts \ + scripts/features/PublishPackages/index.ts \ + scripts/publishPackages.ts +git commit -m "feat(scripts): create GitHub release as final publish step + +PublishOrchestrator.run() is now async. createRelease() is the last +action after npm publish + git tag. Dry-run validates token and remote +URL but skips the API call." +``` + +--- + +## Self-Review + +**Spec coverage:** + +| Requirement | Task | +|-------------|------| +| `@octokit/rest` as devDep | Task 1 | +| `ChangelogWriter.write()` returns string | Task 2 | +| `GitRepository.getRemoteUrl()` | Task 3 | +| `GithubRelease` abstraction | Task 4 | +| `GithubRelease` Octokit implementation | Task 5 | +| Parse HTTPS + SSH remote URL | Task 5 | +| Throw on unparseable URL | Task 5 | +| Read `GITHUB_TOKEN`, throw if absent | Task 5 | +| Dry-run validates config, skips API call | Task 5 + Task 6 | +| `PublishOrchestrator` captures entry, calls `createRelease` | Task 6 | +| `run()` becomes async throughout | Task 6 | +| `@octokit/rest` registered in container | Task 6 | + +All spec requirements covered. ✓ From 3ceb278dad29a58e46284f82c3ebde518f437672 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Zori=C4=87?= Date: Fri, 8 May 2026 07:26:45 +0200 Subject: [PATCH 03/12] chore(scripts): add @octokit/rest devDependency --- package.json | 1 + yarn.lock | 154 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 155 insertions(+) diff --git a/package.json b/package.json index b0ceed8..2eae701 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "zod": "^4.4.3" }, "devDependencies": { + "@octokit/rest": "^22.0.1", "@types/node": ">=24", "@typescript/native-preview": "beta", "@vitest/coverage-v8": "^4.1.5", diff --git a/yarn.lock b/yarn.lock index c5b8090..6178757 100644 --- a/yarn.lock +++ b/yarn.lock @@ -169,6 +169,131 @@ __metadata: languageName: node linkType: hard +"@octokit/auth-token@npm:^6.0.0": + version: 6.0.0 + resolution: "@octokit/auth-token@npm:6.0.0" + checksum: 10c0/32ecc904c5f6f4e5d090bfcc679d70318690c0a0b5040cd9a25811ad9dcd44c33f2cf96b6dbee1cd56cf58fde28fb1819c01b58718aa5c971f79c822357cb5c0 + languageName: node + linkType: hard + +"@octokit/core@npm:^7.0.6": + version: 7.0.6 + resolution: "@octokit/core@npm:7.0.6" + dependencies: + "@octokit/auth-token": "npm:^6.0.0" + "@octokit/graphql": "npm:^9.0.3" + "@octokit/request": "npm:^10.0.6" + "@octokit/request-error": "npm:^7.0.2" + "@octokit/types": "npm:^16.0.0" + before-after-hook: "npm:^4.0.0" + universal-user-agent: "npm:^7.0.0" + checksum: 10c0/95a328ff7c7223d9eb4aa778c63171828514ae0e0f588d33beb81a4dc03bbeae055382f6060ce23c979ab46272409942ff2cf3172109999e48429c47055b1fbe + languageName: node + linkType: hard + +"@octokit/endpoint@npm:^11.0.3": + version: 11.0.3 + resolution: "@octokit/endpoint@npm:11.0.3" + dependencies: + "@octokit/types": "npm:^16.0.0" + universal-user-agent: "npm:^7.0.2" + checksum: 10c0/3f9b67e6923ece5009aebb0dcbae5837fb574bc422561424049a43ead7fea6f132234edb72239d6ec067cf734937a608e4081af81c109de2cb754528f0d00520 + languageName: node + linkType: hard + +"@octokit/graphql@npm:^9.0.3": + version: 9.0.3 + resolution: "@octokit/graphql@npm:9.0.3" + dependencies: + "@octokit/request": "npm:^10.0.6" + "@octokit/types": "npm:^16.0.0" + universal-user-agent: "npm:^7.0.0" + checksum: 10c0/58588d3fb2834f64244fa5376ca7922a30117b001b621e141fab0d52806370803ab0c046ac99b120fa5f45b770f52a815157fb6ffc147fc6c1da4047c1f1af49 + languageName: node + linkType: hard + +"@octokit/openapi-types@npm:^27.0.0": + version: 27.0.0 + resolution: "@octokit/openapi-types@npm:27.0.0" + checksum: 10c0/602d1de033da180a2e982cdbd3646bd5b2e16ecf36b9955a0f23e37ae9e6cb086abb48ff2ae6f2de000fce03e8ae9051794611ae4a95a8f5f6fb63276e7b8e31 + languageName: node + linkType: hard + +"@octokit/plugin-paginate-rest@npm:^14.0.0": + version: 14.0.0 + resolution: "@octokit/plugin-paginate-rest@npm:14.0.0" + dependencies: + "@octokit/types": "npm:^16.0.0" + peerDependencies: + "@octokit/core": ">=6" + checksum: 10c0/841d79d4ccfe18fc809a4a67529b75c1dcdda13399bf4bf5b48ce7559c8b4b2cd422e3204bad4cbdea31c0cf0943521067415268e5bcfc615a3b813e058cad6b + languageName: node + linkType: hard + +"@octokit/plugin-request-log@npm:^6.0.0": + version: 6.0.0 + resolution: "@octokit/plugin-request-log@npm:6.0.0" + peerDependencies: + "@octokit/core": ">=6" + checksum: 10c0/40e46ad0c77235742d0bf698ab4e17df1ae06e0d7824ffc5867ed71e27de860875adb73d89629b823fe8647459a8f262c26ed1aa6ee374873fa94095f37df0bb + languageName: node + linkType: hard + +"@octokit/plugin-rest-endpoint-methods@npm:^17.0.0": + version: 17.0.0 + resolution: "@octokit/plugin-rest-endpoint-methods@npm:17.0.0" + dependencies: + "@octokit/types": "npm:^16.0.0" + peerDependencies: + "@octokit/core": ">=6" + checksum: 10c0/cf9984d7cf6a36ff7ff1b86078ae45fe246e3df10fcef0bccf20c8cfd27bf5e7d98dcb9cf5a7b56332b9c6fa30be28d159c2987d272a4758f77056903d94402f + languageName: node + linkType: hard + +"@octokit/request-error@npm:^7.0.2": + version: 7.1.0 + resolution: "@octokit/request-error@npm:7.1.0" + dependencies: + "@octokit/types": "npm:^16.0.0" + checksum: 10c0/62b90a54545c36a30b5ffdda42e302c751be184d85b68ffc7f1242c51d7ca54dbd185b7d0027b491991776923a910c85c9c51269fe0d86111bac187507a5abc4 + languageName: node + linkType: hard + +"@octokit/request@npm:^10.0.6": + version: 10.0.8 + resolution: "@octokit/request@npm:10.0.8" + dependencies: + "@octokit/endpoint": "npm:^11.0.3" + "@octokit/request-error": "npm:^7.0.2" + "@octokit/types": "npm:^16.0.0" + fast-content-type-parse: "npm:^3.0.0" + json-with-bigint: "npm:^3.5.3" + universal-user-agent: "npm:^7.0.2" + checksum: 10c0/7ee384dbeb489d4e00856eeaaf6a70060c61b036919c539809c3288e2ba14b8f3f63a5b16b8d5b7fdc93d7b6fa5c45bc3d181a712031279f6e192f019e52d7fe + languageName: node + linkType: hard + +"@octokit/rest@npm:^22.0.1": + version: 22.0.1 + resolution: "@octokit/rest@npm:22.0.1" + dependencies: + "@octokit/core": "npm:^7.0.6" + "@octokit/plugin-paginate-rest": "npm:^14.0.0" + "@octokit/plugin-request-log": "npm:^6.0.0" + "@octokit/plugin-rest-endpoint-methods": "npm:^17.0.0" + checksum: 10c0/f3abd84e887cc837973214ce70720a9bba53f5575f40601c6122aa25206e9055d859c0388437f0a137f6cd0e4ff405e1b46b903475b0db32a17bada0c6513d5b + languageName: node + linkType: hard + +"@octokit/types@npm:^16.0.0": + version: 16.0.0 + resolution: "@octokit/types@npm:16.0.0" + dependencies: + "@octokit/openapi-types": "npm:^27.0.0" + checksum: 10c0/b8d41098ba6fc194d13d641f9441347e3a3b96c0efabac0e14f57319340a2d4d1c8676e4cb37ab3062c5c323c617e790b0126916e9bf7b201b0cced0826f8ae2 + languageName: node + linkType: hard + "@oxc-parser/binding-darwin-arm64@npm:0.64.0": version: 0.64.0 resolution: "@oxc-parser/binding-darwin-arm64@npm:0.64.0" @@ -909,6 +1034,7 @@ __metadata: version: 0.0.0-use.local resolution: "@webiny/stdlib@workspace:." dependencies: + "@octokit/rest": "npm:^22.0.1" "@types/node": "npm:>=24" "@typescript/native-preview": "npm:beta" "@vitest/coverage-v8": "npm:^4.1.5" @@ -986,6 +1112,13 @@ __metadata: languageName: node linkType: hard +"before-after-hook@npm:^4.0.0": + version: 4.0.0 + resolution: "before-after-hook@npm:4.0.0" + checksum: 10c0/9f8ae8d1b06142bcfb9ef6625226b5e50348bb11210f266660eddcf9734e0db6f9afc4cb48397ee3f5ac0a3728f3ae401cdeea88413f7bed748a71db84657be2 + languageName: node + linkType: hard + "brace-expansion@npm:^5.0.5": version: 5.0.5 resolution: "brace-expansion@npm:5.0.5" @@ -1139,6 +1272,13 @@ __metadata: languageName: node linkType: hard +"fast-content-type-parse@npm:^3.0.0": + version: 3.0.0 + resolution: "fast-content-type-parse@npm:3.0.0" + checksum: 10c0/06251880c83b7118af3a5e66e8bcee60d44f48b39396fc60acc2b4630bd5f3e77552b999b5c8e943d45a818854360e5e97164c374ec4b562b4df96a2cdf2e188 + languageName: node + linkType: hard + "fast-copy@npm:^4.0.0": version: 4.0.3 resolution: "fast-copy@npm:4.0.3" @@ -1391,6 +1531,13 @@ __metadata: languageName: node linkType: hard +"json-with-bigint@npm:^3.5.3": + version: 3.5.8 + resolution: "json-with-bigint@npm:3.5.8" + checksum: 10c0/a0c4e37626d74a9a493539f9f9a94855933fa15ea2f028859a787229a42c5f11803db6f94f1ce7b1d89756c1e80a7c1f11006bac266ec7ce819b75701765ca0a + languageName: node + linkType: hard + "lightningcss-android-arm64@npm:1.32.0": version: 1.32.0 resolution: "lightningcss-android-arm64@npm:1.32.0" @@ -2300,6 +2447,13 @@ __metadata: languageName: node linkType: hard +"universal-user-agent@npm:^7.0.0, universal-user-agent@npm:^7.0.2": + version: 7.0.3 + resolution: "universal-user-agent@npm:7.0.3" + checksum: 10c0/6043be466a9bb96c0ce82392842d9fddf4c37e296f7bacc2cb25f47123990eb436c82df824644f9c5070a94dbdb117be17f66d54599ab143648ec57ef93dbcc8 + languageName: node + linkType: hard + "vite@npm:^6.0.0 || ^7.0.0 || ^8.0.0": version: 8.0.11 resolution: "vite@npm:8.0.11" From c80af82641506c8e5a85d2a48ef99b097b0d33f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Zori=C4=87?= Date: Fri, 8 May 2026 07:27:13 +0200 Subject: [PATCH 04/12] refactor(scripts): ChangelogWriter.write() returns the formatted entry --- scripts/features/PublishPackages/ChangelogWriter.ts | 3 ++- .../PublishPackages/abstractions/ChangelogWriter.ts | 7 +++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/scripts/features/PublishPackages/ChangelogWriter.ts b/scripts/features/PublishPackages/ChangelogWriter.ts index 0d0160e..bbdf603 100644 --- a/scripts/features/PublishPackages/ChangelogWriter.ts +++ b/scripts/features/PublishPackages/ChangelogWriter.ts @@ -36,10 +36,11 @@ class ChangelogWriterImpl implements ChangelogWriterAbstraction.Interface { this.config = config; } - public write(version: string, commits: string[]): void { + public write(version: string, commits: string[]): string { const sections = this.groupBySection(commits); const entry = this.formatEntry(version, sections); this.prepend(entry); + return entry; } private groupBySection(commits: string[]): Map { diff --git a/scripts/features/PublishPackages/abstractions/ChangelogWriter.ts b/scripts/features/PublishPackages/abstractions/ChangelogWriter.ts index c56b0c3..57af5df 100644 --- a/scripts/features/PublishPackages/abstractions/ChangelogWriter.ts +++ b/scripts/features/PublishPackages/abstractions/ChangelogWriter.ts @@ -1,8 +1,11 @@ import { Abstraction } from "@webiny/di"; export interface IChangelogWriter { - /** Prepends a new release entry to CHANGELOG.md at the repo root. */ - write(version: string, commits: string[]): void; + /** + * Prepends a new release entry to CHANGELOG.md at the repo root. + * Returns the formatted entry string (same text that was prepended). + */ + write(version: string, commits: string[]): string; } export const ChangelogWriter = new Abstraction("Scripts/ChangelogWriter"); From aa31f3ce1c41e7f58a1e425f34387ca92888522c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Zori=C4=87?= Date: Fri, 8 May 2026 07:28:05 +0200 Subject: [PATCH 05/12] feat(scripts): add GitRepository.getRemoteUrl() --- scripts/features/PublishPackages/GitRepository.ts | 7 +++++++ .../features/PublishPackages/abstractions/GitRepository.ts | 2 ++ 2 files changed, 9 insertions(+) diff --git a/scripts/features/PublishPackages/GitRepository.ts b/scripts/features/PublishPackages/GitRepository.ts index eab5c1f..4b2c5a9 100644 --- a/scripts/features/PublishPackages/GitRepository.ts +++ b/scripts/features/PublishPackages/GitRepository.ts @@ -32,6 +32,13 @@ class GitRepositoryImpl implements GitRepositoryAbstraction.Interface { public createTag(tag: string): void { execFileSync("git", ["tag", tag], { cwd: this.config.rootDir }); } + + public getRemoteUrl(name: string): string { + return execFileSync("git", ["remote", "get-url", name], { + cwd: this.config.rootDir, + encoding: "utf8" + }).trim(); + } } export const GitRepository = GitRepositoryAbstraction.createImplementation({ diff --git a/scripts/features/PublishPackages/abstractions/GitRepository.ts b/scripts/features/PublishPackages/abstractions/GitRepository.ts index 0f9f4df..ef7d976 100644 --- a/scripts/features/PublishPackages/abstractions/GitRepository.ts +++ b/scripts/features/PublishPackages/abstractions/GitRepository.ts @@ -7,6 +7,8 @@ export interface IGitRepository { commitsSince(ref: string | null): string[]; /** Creates a lightweight tag at HEAD. */ createTag(tag: string): void; + /** Returns the fetch URL of the named remote. Throws if the remote does not exist. */ + getRemoteUrl(name: string): string; } export const GitRepository = new Abstraction("Scripts/GitRepository"); From 1901876086574e5b52bf42f04f07e94b6266d470 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Zori=C4=87?= Date: Fri, 8 May 2026 07:28:52 +0200 Subject: [PATCH 06/12] feat(scripts): add GithubRelease abstraction --- .../PublishPackages/abstractions/GithubRelease.ts | 15 +++++++++++++++ .../PublishPackages/abstractions/index.ts | 1 + 2 files changed, 16 insertions(+) create mode 100644 scripts/features/PublishPackages/abstractions/GithubRelease.ts diff --git a/scripts/features/PublishPackages/abstractions/GithubRelease.ts b/scripts/features/PublishPackages/abstractions/GithubRelease.ts new file mode 100644 index 0000000..75341d7 --- /dev/null +++ b/scripts/features/PublishPackages/abstractions/GithubRelease.ts @@ -0,0 +1,15 @@ +import { Abstraction } from "@webiny/di"; + +export interface IGithubRelease { + /** + * Creates a GitHub release for the given tag. + * In dry-run mode, validates config (token + remote URL) but skips the API call. + */ + createRelease(tag: string, title: string, body: string): Promise; +} + +export const GithubRelease = new Abstraction("Scripts/GithubRelease"); + +export namespace GithubRelease { + export type Interface = IGithubRelease; +} diff --git a/scripts/features/PublishPackages/abstractions/index.ts b/scripts/features/PublishPackages/abstractions/index.ts index 775808d..d1129bb 100644 --- a/scripts/features/PublishPackages/abstractions/index.ts +++ b/scripts/features/PublishPackages/abstractions/index.ts @@ -5,3 +5,4 @@ export { VersionStrategy } from "./VersionStrategy.ts"; export type { VersionResult } from "./VersionStrategy.ts"; export { ChangelogWriter } from "./ChangelogWriter.ts"; export { PublishOrchestrator } from "./PublishOrchestrator.ts"; +export { GithubRelease } from "./GithubRelease.ts"; From 98e2a5046e8e8f693e94e3f8d1ed1fd513a3dae0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Zori=C4=87?= Date: Fri, 8 May 2026 07:31:19 +0200 Subject: [PATCH 07/12] feat(scripts): add GithubRelease Octokit implementation --- .../features/PublishPackages/GithubRelease.ts | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 scripts/features/PublishPackages/GithubRelease.ts diff --git a/scripts/features/PublishPackages/GithubRelease.ts b/scripts/features/PublishPackages/GithubRelease.ts new file mode 100644 index 0000000..e78b57e --- /dev/null +++ b/scripts/features/PublishPackages/GithubRelease.ts @@ -0,0 +1,60 @@ +import { Octokit } from "@octokit/rest"; +import { GithubRelease as GithubReleaseAbstraction } from "./abstractions/GithubRelease.ts"; +import { ProjectConfig } from "./abstractions/ProjectConfig.ts"; +import { GitRepository } from "./abstractions/GitRepository.ts"; + +const HTTPS_RE = /https:\/\/github\.com\/([^/]+)\/([^/.]+?)(?:\.git)?$/; +const SSH_RE = /git@github\.com:([^/]+)\/([^/.]+?)(?:\.git)?$/; + +function parseGithubRepo(url: string): { owner: string; repo: string } { + const https = HTTPS_RE.exec(url); + if (https) { + return { owner: https[1]!, repo: https[2]! }; + } + const ssh = SSH_RE.exec(url); + if (ssh) { + return { owner: ssh[1]!, repo: ssh[2]! }; + } + throw new Error(`Cannot parse GitHub owner/repo from remote URL: ${url}`); +} + +class GithubReleaseImpl implements GithubReleaseAbstraction.Interface { + private readonly config: ProjectConfig.Interface; + private readonly owner: string; + private readonly repo: string; + private readonly octokit: Octokit; + + public constructor(config: ProjectConfig.Interface, git: GitRepository.Interface) { + this.config = config; + const url = git.getRemoteUrl("origin"); + const { owner, repo } = parseGithubRepo(url); + this.owner = owner; + this.repo = repo; + const token = process.env["GITHUB_TOKEN"]; + if (!token) { + throw new Error("GITHUB_TOKEN env var is required to create a GitHub release"); + } + this.octokit = new Octokit({ auth: token }); + } + + public async createRelease(tag: string, title: string, body: string): Promise { + if (this.config.dryRun) { + console.log(`[dry run] would create GitHub release ${tag} for ${this.owner}/${this.repo}`); + return; + } + + await this.octokit.rest.repos.createRelease({ + owner: this.owner, + repo: this.repo, + tag_name: tag, + name: title, + body + }); + console.log(`Created GitHub release ${tag} for ${this.owner}/${this.repo}`); + } +} + +export const GithubRelease = GithubReleaseAbstraction.createImplementation({ + implementation: GithubReleaseImpl, + dependencies: [ProjectConfig, GitRepository] +}); From b7bad0ce654a646af7db103959a630e28122bf35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Zori=C4=87?= Date: Fri, 8 May 2026 07:36:49 +0200 Subject: [PATCH 08/12] feat(scripts): create GitHub release as final publish step PublishOrchestrator.run() is now async. createRelease() is the last action after npm publish + git tag. Dry-run validates token and remote URL but skips the API call. --- .../PublishPackages/PublishOrchestrator.ts | 33 ++++++++++++++----- .../abstractions/PublishOrchestrator.ts | 2 +- scripts/features/PublishPackages/index.ts | 6 ++-- scripts/publishPackages.ts | 2 +- 4 files changed, 30 insertions(+), 13 deletions(-) diff --git a/scripts/features/PublishPackages/PublishOrchestrator.ts b/scripts/features/PublishPackages/PublishOrchestrator.ts index b9d1e59..c204067 100644 --- a/scripts/features/PublishPackages/PublishOrchestrator.ts +++ b/scripts/features/PublishPackages/PublishOrchestrator.ts @@ -1,17 +1,19 @@ import { readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; -interface DistPackageJson { - version: string; - dependencies?: Record; - [key: string]: unknown; -} import { PublishOrchestrator as PublishOrchestratorAbstraction } from "./abstractions/PublishOrchestrator.ts"; import { ProjectConfig } from "./abstractions/ProjectConfig.ts"; import { NpmRegistry } from "./abstractions/NpmRegistry.ts"; import { GitRepository } from "./abstractions/GitRepository.ts"; import { VersionStrategy } from "./abstractions/VersionStrategy.ts"; import { ChangelogWriter } from "./abstractions/ChangelogWriter.ts"; +import { GithubRelease } from "./abstractions/GithubRelease.ts"; + +interface DistPackageJson { + version: string; + dependencies?: Record; + [key: string]: unknown; +} class PublishOrchestratorImpl implements PublishOrchestratorAbstraction.Interface { private readonly config: ProjectConfig.Interface; @@ -19,22 +21,25 @@ class PublishOrchestratorImpl implements PublishOrchestratorAbstraction.Interfac private readonly git: GitRepository.Interface; private readonly versionStrategy: VersionStrategy.Interface; private readonly changelogWriter: ChangelogWriter.Interface; + private readonly githubRelease: GithubRelease.Interface; public constructor( config: ProjectConfig.Interface, npm: NpmRegistry.Interface, git: GitRepository.Interface, versionStrategy: VersionStrategy.Interface, - changelogWriter: ChangelogWriter.Interface + changelogWriter: ChangelogWriter.Interface, + githubRelease: GithubRelease.Interface ) { this.config = config; this.npm = npm; this.git = git; this.versionStrategy = versionStrategy; this.changelogWriter = changelogWriter; + this.githubRelease = githubRelease; } - public run(): void { + public async run(): Promise { const { rootDir, packageName } = this.config; const published = this.npm.getLatestVersion(packageName) ?? "0.0.0"; @@ -66,10 +71,11 @@ class PublishOrchestratorImpl implements PublishOrchestratorAbstraction.Interfac console.log("[dry run] would update CHANGELOG.md"); console.log(`[dry run] would publish ${packageName}@${newVersion}`); console.log(`[dry run] would tag v${newVersion}`); + await this.githubRelease.createRelease(`v${newVersion}`, `v${newVersion}`, ""); return; } - this.changelogWriter.write(newVersion, commits); + const entry = this.changelogWriter.write(newVersion, commits); console.log("Updated CHANGELOG.md"); const distDir = join(rootDir, "dist"); @@ -90,10 +96,19 @@ class PublishOrchestratorImpl implements PublishOrchestratorAbstraction.Interfac this.git.createTag(`v${newVersion}`); console.log(`Tagged v${newVersion}`); + + await this.githubRelease.createRelease(`v${newVersion}`, `v${newVersion}`, entry); } } export const PublishOrchestrator = PublishOrchestratorAbstraction.createImplementation({ implementation: PublishOrchestratorImpl, - dependencies: [ProjectConfig, NpmRegistry, GitRepository, VersionStrategy, ChangelogWriter] + dependencies: [ + ProjectConfig, + NpmRegistry, + GitRepository, + VersionStrategy, + ChangelogWriter, + GithubRelease + ] }); diff --git a/scripts/features/PublishPackages/abstractions/PublishOrchestrator.ts b/scripts/features/PublishPackages/abstractions/PublishOrchestrator.ts index 0688e1e..a33dc71 100644 --- a/scripts/features/PublishPackages/abstractions/PublishOrchestrator.ts +++ b/scripts/features/PublishPackages/abstractions/PublishOrchestrator.ts @@ -1,7 +1,7 @@ import { Abstraction } from "@webiny/di"; export interface IPublishOrchestrator { - run(): void; + run(): Promise; } export const PublishOrchestrator = new Abstraction( diff --git a/scripts/features/PublishPackages/index.ts b/scripts/features/PublishPackages/index.ts index b9d5e5d..f60803a 100644 --- a/scripts/features/PublishPackages/index.ts +++ b/scripts/features/PublishPackages/index.ts @@ -7,8 +7,9 @@ import { GitRepository as GitRepositoryImpl } from "./GitRepository.ts"; import { VersionStrategy as VersionStrategyImpl } from "./VersionStrategy.ts"; import { ChangelogWriter as ChangelogWriterImpl } from "./ChangelogWriter.ts"; import { PublishOrchestrator as PublishOrchestratorImpl } from "./PublishOrchestrator.ts"; +import { GithubRelease as GithubReleaseImpl } from "./GithubRelease.ts"; -export function run(rootDir: string): void { +export async function run(rootDir: string): Promise { const dryRun = !process.argv.includes("--publish"); if (dryRun) { console.log("Dry run — pass --publish to actually publish."); @@ -23,6 +24,7 @@ export function run(rootDir: string): void { container.register(GitRepositoryImpl).inSingletonScope(); container.register(VersionStrategyImpl).inSingletonScope(); container.register(ChangelogWriterImpl).inSingletonScope(); + container.register(GithubReleaseImpl).inSingletonScope(); container.register(PublishOrchestratorImpl).inSingletonScope(); - container.resolve(PublishOrchestrator).run(); + await container.resolve(PublishOrchestrator).run(); } diff --git a/scripts/publishPackages.ts b/scripts/publishPackages.ts index 7852c9e..829ead9 100644 --- a/scripts/publishPackages.ts +++ b/scripts/publishPackages.ts @@ -4,4 +4,4 @@ import { run as publish } from "./features/PublishPackages/index.ts"; const root = fileURLToPath(new URL("..", import.meta.url)); build(root); -publish(root); +await publish(root); From 2edae2a0754309d97850d3ffe4fca79a7bf5e47f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Zori=C4=87?= Date: Fri, 8 May 2026 07:51:07 +0200 Subject: [PATCH 09/12] test(scripts): add GithubRelease unit tests with mocked Octokit Cover constructor validation (missing token, HTTPS/SSH URL parsing, unrecognised host) and createRelease behaviour (dry-run skip + real-publish Octokit call), using vi.hoisted + vi.mock so no real HTTP traffic is possible. Also exclude __tests__/scripts from the common typecheck config and include it in the scripts typecheck config so the .ts-extension imports are correctly allowed. Co-Authored-By: Claude Sonnet 4.6 --- .../PublishPackages/GithubRelease.test.ts | 118 ++++++++++++++++++ .../features/PublishPackages/GithubRelease.ts | 4 +- tsconfig.check.common.json | 2 +- tsconfig.check.scripts.json | 2 +- 4 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 __tests__/scripts/PublishPackages/GithubRelease.test.ts diff --git a/__tests__/scripts/PublishPackages/GithubRelease.test.ts b/__tests__/scripts/PublishPackages/GithubRelease.test.ts new file mode 100644 index 0000000..ada42a0 --- /dev/null +++ b/__tests__/scripts/PublishPackages/GithubRelease.test.ts @@ -0,0 +1,118 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import "@webiny/di"; // loads reflect-metadata side-effect +import { Container } from "@webiny/di"; +import { ProjectConfig } from "../../../scripts/features/PublishPackages/abstractions/ProjectConfig.ts"; +import { GitRepository } from "../../../scripts/features/PublishPackages/abstractions/GitRepository.ts"; +import { GithubRelease } from "../../../scripts/features/PublishPackages/abstractions/GithubRelease.ts"; +import { GithubRelease as GithubReleaseImpl } from "../../../scripts/features/PublishPackages/GithubRelease.ts"; + +// Hoist mock functions so they are available both inside vi.mock factory and in tests. +const { mockCreateRelease } = vi.hoisted(() => ({ + mockCreateRelease: vi.fn().mockResolvedValue({}) +})); + +vi.mock("@octokit/rest", () => { + class MockOctokit { + public readonly rest = { repos: { createRelease: mockCreateRelease } }; + } + return { Octokit: MockOctokit }; +}); + +/** + * Builds a DI container with mock ProjectConfig and GitRepository instances, + * registers GithubReleaseImpl, and returns the resolved IGithubRelease. + */ +function makeContainer(opts: { dryRun?: boolean; remoteUrl?: string }): GithubRelease.Interface { + const container = new Container(); + container.registerInstance(ProjectConfig, { + rootDir: "/tmp", + packageName: "@test/pkg", + dryRun: opts.dryRun ?? false + }); + container.registerInstance(GitRepository, { + tagExists: vi.fn(), + commitsSince: vi.fn(), + createTag: vi.fn(), + getRemoteUrl: vi + .fn() + .mockReturnValue(opts.remoteUrl ?? "https://github.com/acme/my-repo.git") + }); + container.register(GithubReleaseImpl).inSingletonScope(); + return container.resolve(GithubRelease); +} + +describe("GithubRelease", () => { + let savedToken: string | undefined; + + beforeEach(() => { + savedToken = process.env["GITHUB_TOKEN"]; + process.env["GITHUB_TOKEN"] = "test-token"; + mockCreateRelease.mockClear(); + }); + + afterEach(() => { + if (savedToken === undefined) { + delete process.env["GITHUB_TOKEN"]; + } else { + process.env["GITHUB_TOKEN"] = savedToken; + } + }); + + describe("constructor", () => { + it("throws when GITHUB_TOKEN is not set", () => { + delete process.env["GITHUB_TOKEN"]; + expect(() => makeContainer({})).toThrow("GITHUB_TOKEN env var is required"); + }); + + it("parses HTTPS remote URL without throwing", () => { + expect(() => + makeContainer({ remoteUrl: "https://github.com/acme/my-repo.git" }) + ).not.toThrow(); + }); + + it("parses SSH remote URL without throwing", () => { + expect(() => + makeContainer({ remoteUrl: "git@github.com:acme/my-repo.git" }) + ).not.toThrow(); + }); + + it("throws for unrecognised remote URL", () => { + expect(() => makeContainer({ remoteUrl: "https://gitlab.com/acme/repo.git" })).toThrow( + "Cannot parse GitHub owner/repo" + ); + }); + }); + + describe("createRelease", () => { + it("dry-run: logs and skips API call", async () => { + const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + try { + const release = makeContainer({ dryRun: true }); + await release.createRelease("v1.2.3", "v1.2.3", "body"); + + expect(mockCreateRelease).not.toHaveBeenCalled(); + const calls = logSpy.mock.calls.map(args => args.join(" ")); + const hasExpectedLog = calls.some( + msg => msg.includes("would create GitHub release") && msg.includes("v1.2.3") + ); + expect(hasExpectedLog).toBe(true); + } finally { + logSpy.mockRestore(); + } + }); + + it("real publish: calls Octokit with correct params", async () => { + const release = makeContainer({ dryRun: false }); + await release.createRelease("v1.2.3", "v1.2.3", "body text"); + + expect(mockCreateRelease).toHaveBeenCalledOnce(); + expect(mockCreateRelease).toHaveBeenCalledWith({ + owner: "acme", + repo: "my-repo", + tag_name: "v1.2.3", + name: "v1.2.3", + body: "body text" + }); + }); + }); +}); diff --git a/scripts/features/PublishPackages/GithubRelease.ts b/scripts/features/PublishPackages/GithubRelease.ts index e78b57e..a901450 100644 --- a/scripts/features/PublishPackages/GithubRelease.ts +++ b/scripts/features/PublishPackages/GithubRelease.ts @@ -39,7 +39,9 @@ class GithubReleaseImpl implements GithubReleaseAbstraction.Interface { public async createRelease(tag: string, title: string, body: string): Promise { if (this.config.dryRun) { - console.log(`[dry run] would create GitHub release ${tag} for ${this.owner}/${this.repo}`); + console.log( + `[dry run] would create GitHub release ${tag} for ${this.owner}/${this.repo}` + ); return; } diff --git a/tsconfig.check.common.json b/tsconfig.check.common.json index a79ae6e..081a07d 100644 --- a/tsconfig.check.common.json +++ b/tsconfig.check.common.json @@ -1,5 +1,5 @@ { "extends": ["./tsconfig.common.json", "./tsconfig.checkmode.json"], "include": ["src", "__tests__", "vitest.config.ts"], - "exclude": ["src/node", "src/browser", "__tests__/node", "__tests__/browser"] + "exclude": ["src/node", "src/browser", "__tests__/node", "__tests__/browser", "__tests__/scripts"] } diff --git a/tsconfig.check.scripts.json b/tsconfig.check.scripts.json index 56297ce..a40627e 100644 --- a/tsconfig.check.scripts.json +++ b/tsconfig.check.scripts.json @@ -3,5 +3,5 @@ "compilerOptions": { "allowImportingTsExtensions": true }, - "include": ["scripts"] + "include": ["scripts", "__tests__/scripts"] } From 33df4406d7183088cbd032849df5f2a5be71c76c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Zori=C4=87?= Date: Fri, 8 May 2026 09:38:15 +0200 Subject: [PATCH 10/12] refactor(scripts): extract GithubToken DI abstraction from GithubRelease Co-Authored-By: Claude Sonnet 4.6 --- .../PublishPackages/GithubRelease.test.ts | 51 +++++++------------ .../PublishPackages/GithubToken.test.ts | 39 ++++++++++++++ .../features/PublishPackages/GithubRelease.ts | 15 +++--- .../features/PublishPackages/GithubToken.ts | 16 ++++++ .../abstractions/GithubToken.ts | 12 +++++ .../PublishPackages/abstractions/index.ts | 1 + scripts/features/PublishPackages/index.ts | 4 +- 7 files changed, 98 insertions(+), 40 deletions(-) create mode 100644 __tests__/scripts/PublishPackages/GithubToken.test.ts create mode 100644 scripts/features/PublishPackages/GithubToken.ts create mode 100644 scripts/features/PublishPackages/abstractions/GithubToken.ts diff --git a/__tests__/scripts/PublishPackages/GithubRelease.test.ts b/__tests__/scripts/PublishPackages/GithubRelease.test.ts index ada42a0..987173d 100644 --- a/__tests__/scripts/PublishPackages/GithubRelease.test.ts +++ b/__tests__/scripts/PublishPackages/GithubRelease.test.ts @@ -1,12 +1,12 @@ -import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; -import "@webiny/di"; // loads reflect-metadata side-effect +import { describe, it, expect, beforeEach, vi } from "vitest"; +import "@webiny/di"; import { Container } from "@webiny/di"; import { ProjectConfig } from "../../../scripts/features/PublishPackages/abstractions/ProjectConfig.ts"; import { GitRepository } from "../../../scripts/features/PublishPackages/abstractions/GitRepository.ts"; +import { GithubToken } from "../../../scripts/features/PublishPackages/abstractions/GithubToken.ts"; import { GithubRelease } from "../../../scripts/features/PublishPackages/abstractions/GithubRelease.ts"; import { GithubRelease as GithubReleaseImpl } from "../../../scripts/features/PublishPackages/GithubRelease.ts"; -// Hoist mock functions so they are available both inside vi.mock factory and in tests. const { mockCreateRelease } = vi.hoisted(() => ({ mockCreateRelease: vi.fn().mockResolvedValue({}) })); @@ -18,11 +18,11 @@ vi.mock("@octokit/rest", () => { return { Octokit: MockOctokit }; }); -/** - * Builds a DI container with mock ProjectConfig and GitRepository instances, - * registers GithubReleaseImpl, and returns the resolved IGithubRelease. - */ -function makeContainer(opts: { dryRun?: boolean; remoteUrl?: string }): GithubRelease.Interface { +function makeContainer(opts: { + dryRun?: boolean; + remoteUrl?: string; + token?: string; +}): GithubRelease.Interface { const container = new Container(); container.registerInstance(ProjectConfig, { rootDir: "/tmp", @@ -37,33 +37,19 @@ function makeContainer(opts: { dryRun?: boolean; remoteUrl?: string }): GithubRe .fn() .mockReturnValue(opts.remoteUrl ?? "https://github.com/acme/my-repo.git") }); + container.registerInstance(GithubToken, { + getToken: () => opts.token ?? "test-token" + }); container.register(GithubReleaseImpl).inSingletonScope(); return container.resolve(GithubRelease); } describe("GithubRelease", () => { - let savedToken: string | undefined; - beforeEach(() => { - savedToken = process.env["GITHUB_TOKEN"]; - process.env["GITHUB_TOKEN"] = "test-token"; mockCreateRelease.mockClear(); }); - afterEach(() => { - if (savedToken === undefined) { - delete process.env["GITHUB_TOKEN"]; - } else { - process.env["GITHUB_TOKEN"] = savedToken; - } - }); - describe("constructor", () => { - it("throws when GITHUB_TOKEN is not set", () => { - delete process.env["GITHUB_TOKEN"]; - expect(() => makeContainer({})).toThrow("GITHUB_TOKEN env var is required"); - }); - it("parses HTTPS remote URL without throwing", () => { expect(() => makeContainer({ remoteUrl: "https://github.com/acme/my-repo.git" }) @@ -77,9 +63,9 @@ describe("GithubRelease", () => { }); it("throws for unrecognised remote URL", () => { - expect(() => makeContainer({ remoteUrl: "https://gitlab.com/acme/repo.git" })).toThrow( - "Cannot parse GitHub owner/repo" - ); + expect(() => + makeContainer({ remoteUrl: "https://gitlab.com/acme/repo.git" }) + ).toThrow("Cannot parse GitHub owner/repo"); }); }); @@ -92,10 +78,11 @@ describe("GithubRelease", () => { expect(mockCreateRelease).not.toHaveBeenCalled(); const calls = logSpy.mock.calls.map(args => args.join(" ")); - const hasExpectedLog = calls.some( - msg => msg.includes("would create GitHub release") && msg.includes("v1.2.3") - ); - expect(hasExpectedLog).toBe(true); + expect( + calls.some( + msg => msg.includes("would create GitHub release") && msg.includes("v1.2.3") + ) + ).toBe(true); } finally { logSpy.mockRestore(); } diff --git a/__tests__/scripts/PublishPackages/GithubToken.test.ts b/__tests__/scripts/PublishPackages/GithubToken.test.ts new file mode 100644 index 0000000..2829ec3 --- /dev/null +++ b/__tests__/scripts/PublishPackages/GithubToken.test.ts @@ -0,0 +1,39 @@ +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import "@webiny/di"; +import { Container } from "@webiny/di"; +import { GithubToken } from "../../../scripts/features/PublishPackages/abstractions/GithubToken.ts"; +import { GithubToken as GithubTokenImpl } from "../../../scripts/features/PublishPackages/GithubToken.ts"; + +function makeToken(): GithubToken.Interface { + const container = new Container(); + container.register(GithubTokenImpl).inSingletonScope(); + return container.resolve(GithubToken); +} + +describe("GithubToken", () => { + let savedToken: string | undefined; + + beforeEach(() => { + savedToken = process.env["GITHUB_TOKEN"]; + }); + + afterEach(() => { + if (savedToken === undefined) { + delete process.env["GITHUB_TOKEN"]; + } else { + process.env["GITHUB_TOKEN"] = savedToken; + } + }); + + it("returns the token from GITHUB_TOKEN env var", () => { + process.env["GITHUB_TOKEN"] = "ghp_test123"; + const token = makeToken(); + expect(token.getToken()).toBe("ghp_test123"); + }); + + it("throws when GITHUB_TOKEN is not set", () => { + delete process.env["GITHUB_TOKEN"]; + const token = makeToken(); + expect(() => token.getToken()).toThrow("GITHUB_TOKEN env var is required"); + }); +}); diff --git a/scripts/features/PublishPackages/GithubRelease.ts b/scripts/features/PublishPackages/GithubRelease.ts index a901450..e643519 100644 --- a/scripts/features/PublishPackages/GithubRelease.ts +++ b/scripts/features/PublishPackages/GithubRelease.ts @@ -2,6 +2,7 @@ import { Octokit } from "@octokit/rest"; import { GithubRelease as GithubReleaseAbstraction } from "./abstractions/GithubRelease.ts"; import { ProjectConfig } from "./abstractions/ProjectConfig.ts"; import { GitRepository } from "./abstractions/GitRepository.ts"; +import { GithubToken } from "./abstractions/GithubToken.ts"; const HTTPS_RE = /https:\/\/github\.com\/([^/]+)\/([^/.]+?)(?:\.git)?$/; const SSH_RE = /git@github\.com:([^/]+)\/([^/.]+?)(?:\.git)?$/; @@ -24,17 +25,17 @@ class GithubReleaseImpl implements GithubReleaseAbstraction.Interface { private readonly repo: string; private readonly octokit: Octokit; - public constructor(config: ProjectConfig.Interface, git: GitRepository.Interface) { + public constructor( + config: ProjectConfig.Interface, + git: GitRepository.Interface, + token: GithubToken.Interface + ) { this.config = config; const url = git.getRemoteUrl("origin"); const { owner, repo } = parseGithubRepo(url); this.owner = owner; this.repo = repo; - const token = process.env["GITHUB_TOKEN"]; - if (!token) { - throw new Error("GITHUB_TOKEN env var is required to create a GitHub release"); - } - this.octokit = new Octokit({ auth: token }); + this.octokit = new Octokit({ auth: token.getToken() }); } public async createRelease(tag: string, title: string, body: string): Promise { @@ -58,5 +59,5 @@ class GithubReleaseImpl implements GithubReleaseAbstraction.Interface { export const GithubRelease = GithubReleaseAbstraction.createImplementation({ implementation: GithubReleaseImpl, - dependencies: [ProjectConfig, GitRepository] + dependencies: [ProjectConfig, GitRepository, GithubToken] }); diff --git a/scripts/features/PublishPackages/GithubToken.ts b/scripts/features/PublishPackages/GithubToken.ts new file mode 100644 index 0000000..dc3adac --- /dev/null +++ b/scripts/features/PublishPackages/GithubToken.ts @@ -0,0 +1,16 @@ +import { GithubToken as GithubTokenAbstraction } from "./abstractions/GithubToken.ts"; + +class GithubTokenImpl implements GithubTokenAbstraction.Interface { + public getToken(): string { + const token = process.env["GITHUB_TOKEN"]; + if (!token) { + throw new Error("GITHUB_TOKEN env var is required to create a GitHub release"); + } + return token; + } +} + +export const GithubToken = GithubTokenAbstraction.createImplementation({ + implementation: GithubTokenImpl, + dependencies: [] +}); diff --git a/scripts/features/PublishPackages/abstractions/GithubToken.ts b/scripts/features/PublishPackages/abstractions/GithubToken.ts new file mode 100644 index 0000000..7bb4f9f --- /dev/null +++ b/scripts/features/PublishPackages/abstractions/GithubToken.ts @@ -0,0 +1,12 @@ +import { Abstraction } from "@webiny/di"; + +export interface IGithubToken { + /** Returns the GitHub personal access token. Throws if unavailable. */ + getToken(): string; +} + +export const GithubToken = new Abstraction("Scripts/GithubToken"); + +export namespace GithubToken { + export type Interface = IGithubToken; +} diff --git a/scripts/features/PublishPackages/abstractions/index.ts b/scripts/features/PublishPackages/abstractions/index.ts index d1129bb..773caad 100644 --- a/scripts/features/PublishPackages/abstractions/index.ts +++ b/scripts/features/PublishPackages/abstractions/index.ts @@ -6,3 +6,4 @@ export type { VersionResult } from "./VersionStrategy.ts"; export { ChangelogWriter } from "./ChangelogWriter.ts"; export { PublishOrchestrator } from "./PublishOrchestrator.ts"; export { GithubRelease } from "./GithubRelease.ts"; +export { GithubToken } from "./GithubToken.ts"; diff --git a/scripts/features/PublishPackages/index.ts b/scripts/features/PublishPackages/index.ts index f60803a..6f8b9dd 100644 --- a/scripts/features/PublishPackages/index.ts +++ b/scripts/features/PublishPackages/index.ts @@ -6,8 +6,9 @@ import { NpmRegistry as NpmRegistryImpl } from "./NpmRegistry.ts"; import { GitRepository as GitRepositoryImpl } from "./GitRepository.ts"; import { VersionStrategy as VersionStrategyImpl } from "./VersionStrategy.ts"; import { ChangelogWriter as ChangelogWriterImpl } from "./ChangelogWriter.ts"; -import { PublishOrchestrator as PublishOrchestratorImpl } from "./PublishOrchestrator.ts"; +import { GithubToken as GithubTokenImpl } from "./GithubToken.ts"; import { GithubRelease as GithubReleaseImpl } from "./GithubRelease.ts"; +import { PublishOrchestrator as PublishOrchestratorImpl } from "./PublishOrchestrator.ts"; export async function run(rootDir: string): Promise { const dryRun = !process.argv.includes("--publish"); @@ -24,6 +25,7 @@ export async function run(rootDir: string): Promise { container.register(GitRepositoryImpl).inSingletonScope(); container.register(VersionStrategyImpl).inSingletonScope(); container.register(ChangelogWriterImpl).inSingletonScope(); + container.register(GithubTokenImpl).inSingletonScope(); container.register(GithubReleaseImpl).inSingletonScope(); container.register(PublishOrchestratorImpl).inSingletonScope(); await container.resolve(PublishOrchestrator).run(); From f088749372985249581261d25bdb95fa6c9a5507 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Zori=C4=87?= Date: Fri, 8 May 2026 09:53:24 +0200 Subject: [PATCH 11/12] chore: format fixes, pin @typescript/native-preview version, remove stray char Co-Authored-By: Claude Sonnet 4.6 --- .../PublishPackages/GithubRelease.test.ts | 6 +- package.json | 2 +- yarn.lock | 84 +++++++++---------- 3 files changed, 46 insertions(+), 46 deletions(-) diff --git a/__tests__/scripts/PublishPackages/GithubRelease.test.ts b/__tests__/scripts/PublishPackages/GithubRelease.test.ts index 987173d..655a2ad 100644 --- a/__tests__/scripts/PublishPackages/GithubRelease.test.ts +++ b/__tests__/scripts/PublishPackages/GithubRelease.test.ts @@ -63,9 +63,9 @@ describe("GithubRelease", () => { }); it("throws for unrecognised remote URL", () => { - expect(() => - makeContainer({ remoteUrl: "https://gitlab.com/acme/repo.git" }) - ).toThrow("Cannot parse GitHub owner/repo"); + expect(() => makeContainer({ remoteUrl: "https://gitlab.com/acme/repo.git" })).toThrow( + "Cannot parse GitHub owner/repo" + ); }); }); diff --git a/package.json b/package.json index 2eae701..25febd7 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "devDependencies": { "@octokit/rest": "^22.0.1", "@types/node": ">=24", - "@typescript/native-preview": "beta", + "@typescript/native-preview": "^7.0.0-dev.20260507.1", "@vitest/coverage-v8": "^4.1.5", "adio": "^3.0.0", "happy-dom": "^20.9.0", diff --git a/yarn.lock b/yarn.lock index 6178757..0d095b6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -810,11 +810,11 @@ __metadata: linkType: hard "@types/node@npm:*, @types/node@npm:>=20.0.0, @types/node@npm:>=24": - version: 25.6.0 - resolution: "@types/node@npm:25.6.0" + version: 25.6.2 + resolution: "@types/node@npm:25.6.2" dependencies: undici-types: "npm:~7.19.0" - checksum: 10c0/d2d2015630ff098a201407f55f5077a20270ae4f465c739b40865cd9933b91b9c5d2b85568eadaf3db0801b91e267333ca7eb39f007428b173d1cdab4b339ac5 + checksum: 10c0/7f540331aa3ab88c285aeaf2eb43e3992f54f0cdb7f3593d156af67b199d4eaf56590fa1c310a00aa58ff69dba668cb3915a157fe83cd6b40a73bb338a12f09a languageName: node linkType: hard @@ -834,66 +834,66 @@ __metadata: languageName: node linkType: hard -"@typescript/native-preview-darwin-arm64@npm:7.0.0-dev.20260421.2": - version: 7.0.0-dev.20260421.2 - resolution: "@typescript/native-preview-darwin-arm64@npm:7.0.0-dev.20260421.2" +"@typescript/native-preview-darwin-arm64@npm:7.0.0-dev.20260507.1": + version: 7.0.0-dev.20260507.1 + resolution: "@typescript/native-preview-darwin-arm64@npm:7.0.0-dev.20260507.1" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@typescript/native-preview-darwin-x64@npm:7.0.0-dev.20260421.2": - version: 7.0.0-dev.20260421.2 - resolution: "@typescript/native-preview-darwin-x64@npm:7.0.0-dev.20260421.2" +"@typescript/native-preview-darwin-x64@npm:7.0.0-dev.20260507.1": + version: 7.0.0-dev.20260507.1 + resolution: "@typescript/native-preview-darwin-x64@npm:7.0.0-dev.20260507.1" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@typescript/native-preview-linux-arm64@npm:7.0.0-dev.20260421.2": - version: 7.0.0-dev.20260421.2 - resolution: "@typescript/native-preview-linux-arm64@npm:7.0.0-dev.20260421.2" +"@typescript/native-preview-linux-arm64@npm:7.0.0-dev.20260507.1": + version: 7.0.0-dev.20260507.1 + resolution: "@typescript/native-preview-linux-arm64@npm:7.0.0-dev.20260507.1" conditions: os=linux & cpu=arm64 languageName: node linkType: hard -"@typescript/native-preview-linux-arm@npm:7.0.0-dev.20260421.2": - version: 7.0.0-dev.20260421.2 - resolution: "@typescript/native-preview-linux-arm@npm:7.0.0-dev.20260421.2" +"@typescript/native-preview-linux-arm@npm:7.0.0-dev.20260507.1": + version: 7.0.0-dev.20260507.1 + resolution: "@typescript/native-preview-linux-arm@npm:7.0.0-dev.20260507.1" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@typescript/native-preview-linux-x64@npm:7.0.0-dev.20260421.2": - version: 7.0.0-dev.20260421.2 - resolution: "@typescript/native-preview-linux-x64@npm:7.0.0-dev.20260421.2" +"@typescript/native-preview-linux-x64@npm:7.0.0-dev.20260507.1": + version: 7.0.0-dev.20260507.1 + resolution: "@typescript/native-preview-linux-x64@npm:7.0.0-dev.20260507.1" conditions: os=linux & cpu=x64 languageName: node linkType: hard -"@typescript/native-preview-win32-arm64@npm:7.0.0-dev.20260421.2": - version: 7.0.0-dev.20260421.2 - resolution: "@typescript/native-preview-win32-arm64@npm:7.0.0-dev.20260421.2" +"@typescript/native-preview-win32-arm64@npm:7.0.0-dev.20260507.1": + version: 7.0.0-dev.20260507.1 + resolution: "@typescript/native-preview-win32-arm64@npm:7.0.0-dev.20260507.1" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@typescript/native-preview-win32-x64@npm:7.0.0-dev.20260421.2": - version: 7.0.0-dev.20260421.2 - resolution: "@typescript/native-preview-win32-x64@npm:7.0.0-dev.20260421.2" +"@typescript/native-preview-win32-x64@npm:7.0.0-dev.20260507.1": + version: 7.0.0-dev.20260507.1 + resolution: "@typescript/native-preview-win32-x64@npm:7.0.0-dev.20260507.1" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@typescript/native-preview@npm:beta": - version: 7.0.0-dev.20260421.2 - resolution: "@typescript/native-preview@npm:7.0.0-dev.20260421.2" +"@typescript/native-preview@npm:^7.0.0-dev.20260507.1": + version: 7.0.0-dev.20260507.1 + resolution: "@typescript/native-preview@npm:7.0.0-dev.20260507.1" dependencies: - "@typescript/native-preview-darwin-arm64": "npm:7.0.0-dev.20260421.2" - "@typescript/native-preview-darwin-x64": "npm:7.0.0-dev.20260421.2" - "@typescript/native-preview-linux-arm": "npm:7.0.0-dev.20260421.2" - "@typescript/native-preview-linux-arm64": "npm:7.0.0-dev.20260421.2" - "@typescript/native-preview-linux-x64": "npm:7.0.0-dev.20260421.2" - "@typescript/native-preview-win32-arm64": "npm:7.0.0-dev.20260421.2" - "@typescript/native-preview-win32-x64": "npm:7.0.0-dev.20260421.2" + "@typescript/native-preview-darwin-arm64": "npm:7.0.0-dev.20260507.1" + "@typescript/native-preview-darwin-x64": "npm:7.0.0-dev.20260507.1" + "@typescript/native-preview-linux-arm": "npm:7.0.0-dev.20260507.1" + "@typescript/native-preview-linux-arm64": "npm:7.0.0-dev.20260507.1" + "@typescript/native-preview-linux-x64": "npm:7.0.0-dev.20260507.1" + "@typescript/native-preview-win32-arm64": "npm:7.0.0-dev.20260507.1" + "@typescript/native-preview-win32-x64": "npm:7.0.0-dev.20260507.1" dependenciesMeta: "@typescript/native-preview-darwin-arm64": optional: true @@ -911,7 +911,7 @@ __metadata: optional: true bin: tsgo: bin/tsgo.js - checksum: 10c0/79dbb7e5204c5906bcb3a2946209162efbaac9c8fa78ffeccb8b6718661a5b40f988d60b27350ad538cc1a4143524e5fc97b04fa4767bd42a036000f226438db + checksum: 10c0/539f4da0df37110f33f51be77d8c72d0c206127dbc9f1f86e12ccad39a31e26905edb243c0b54997bd0b679e128a646b7c77b7e38ae1320db7637134cfda033b languageName: node linkType: hard @@ -1036,7 +1036,7 @@ __metadata: dependencies: "@octokit/rest": "npm:^22.0.1" "@types/node": "npm:>=24" - "@typescript/native-preview": "npm:beta" + "@typescript/native-preview": "npm:^7.0.0-dev.20260507.1" "@vitest/coverage-v8": "npm:^4.1.5" "@webiny/di": "npm:^0.2.3" adio: "npm:^3.0.0" @@ -1120,11 +1120,11 @@ __metadata: linkType: hard "brace-expansion@npm:^5.0.5": - version: 5.0.5 - resolution: "brace-expansion@npm:5.0.5" + version: 5.0.6 + resolution: "brace-expansion@npm:5.0.6" dependencies: balanced-match: "npm:^4.0.2" - checksum: 10c0/4d238e14ed4f5cc9c07285550a41cef23121ca08ba99fa9eb5b55b580dcb6bf868b8210aa10526bdc9f8dc97f33ca2a7259039c4cc131a93042beddb424c48e3 + checksum: 10c0/8c919869b90f61d533b341d3340be5ee4413232ea89b8246cbc2f38eb014f1d8182785c98a006eaf6111d02dc9eeffefdc240d5ac158625b2ed084dccd4bbf9b languageName: node linkType: hard @@ -2349,15 +2349,15 @@ __metadata: linkType: hard "tar@npm:^7.5.4": - version: 7.5.14 - resolution: "tar@npm:7.5.14" + version: 7.5.15 + resolution: "tar@npm:7.5.15" dependencies: "@isaacs/fs-minipass": "npm:^4.0.0" chownr: "npm:^3.0.0" minipass: "npm:^7.1.2" minizlib: "npm:^3.1.0" yallist: "npm:^5.0.0" - checksum: 10c0/619573265fa45295ff0b378f1097ab43187ab7b66e9483d3ad8f467c287674fb182ec878ef50a08761b8ab487863cb429902cf65fe361d47e330a95bfc4ca9e8 + checksum: 10c0/8f039edb1d12fdd7df6c6f9877d125afe9f3da3f5f9317df326fdd090d48793d6998cede1506a1471f3e3a250db270a89dace28005eb5e99c5a9132d704ac956 languageName: node linkType: hard From b188c712b706bbdda896c4468d0cd09284ab794a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Zori=C4=87?= Date: Fri, 8 May 2026 10:45:34 +0200 Subject: [PATCH 12/12] chore: tighten coverage thresholds to current measured levels Co-Authored-By: Claude Sonnet 4.6 --- package.json | 2 +- vitest.config.ts | 10 ++++---- yarn.lock | 66 ++++++++++++++++++++++++------------------------ 3 files changed, 39 insertions(+), 39 deletions(-) diff --git a/package.json b/package.json index 25febd7..0c479b9 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "devDependencies": { "@octokit/rest": "^22.0.1", "@types/node": ">=24", - "@typescript/native-preview": "^7.0.0-dev.20260507.1", + "@typescript/native-preview": "^7.0.0-dev.20260508.1", "@vitest/coverage-v8": "^4.1.5", "adio": "^3.0.0", "happy-dom": "^20.9.0", diff --git a/vitest.config.ts b/vitest.config.ts index 0429f92..fc283dd 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -13,12 +13,12 @@ export default defineConfig({ coverage: { provider: "v8", include: ["src/**/*.ts"], - exclude: ["**/__tests__/**", "**/index.ts", "**/abstractions/**"], + exclude: ["**/__tests__/**", "**/index.ts", "**/abstractions/**", "**/feature.ts"], thresholds: { - statements: 90, - branches: 80, - functions: 90, - lines: 90 + statements: 96, + branches: 93, + functions: 96, + lines: 96 } } } diff --git a/yarn.lock b/yarn.lock index 0d095b6..1c0851f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -834,66 +834,66 @@ __metadata: languageName: node linkType: hard -"@typescript/native-preview-darwin-arm64@npm:7.0.0-dev.20260507.1": - version: 7.0.0-dev.20260507.1 - resolution: "@typescript/native-preview-darwin-arm64@npm:7.0.0-dev.20260507.1" +"@typescript/native-preview-darwin-arm64@npm:7.0.0-dev.20260508.1": + version: 7.0.0-dev.20260508.1 + resolution: "@typescript/native-preview-darwin-arm64@npm:7.0.0-dev.20260508.1" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@typescript/native-preview-darwin-x64@npm:7.0.0-dev.20260507.1": - version: 7.0.0-dev.20260507.1 - resolution: "@typescript/native-preview-darwin-x64@npm:7.0.0-dev.20260507.1" +"@typescript/native-preview-darwin-x64@npm:7.0.0-dev.20260508.1": + version: 7.0.0-dev.20260508.1 + resolution: "@typescript/native-preview-darwin-x64@npm:7.0.0-dev.20260508.1" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@typescript/native-preview-linux-arm64@npm:7.0.0-dev.20260507.1": - version: 7.0.0-dev.20260507.1 - resolution: "@typescript/native-preview-linux-arm64@npm:7.0.0-dev.20260507.1" +"@typescript/native-preview-linux-arm64@npm:7.0.0-dev.20260508.1": + version: 7.0.0-dev.20260508.1 + resolution: "@typescript/native-preview-linux-arm64@npm:7.0.0-dev.20260508.1" conditions: os=linux & cpu=arm64 languageName: node linkType: hard -"@typescript/native-preview-linux-arm@npm:7.0.0-dev.20260507.1": - version: 7.0.0-dev.20260507.1 - resolution: "@typescript/native-preview-linux-arm@npm:7.0.0-dev.20260507.1" +"@typescript/native-preview-linux-arm@npm:7.0.0-dev.20260508.1": + version: 7.0.0-dev.20260508.1 + resolution: "@typescript/native-preview-linux-arm@npm:7.0.0-dev.20260508.1" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@typescript/native-preview-linux-x64@npm:7.0.0-dev.20260507.1": - version: 7.0.0-dev.20260507.1 - resolution: "@typescript/native-preview-linux-x64@npm:7.0.0-dev.20260507.1" +"@typescript/native-preview-linux-x64@npm:7.0.0-dev.20260508.1": + version: 7.0.0-dev.20260508.1 + resolution: "@typescript/native-preview-linux-x64@npm:7.0.0-dev.20260508.1" conditions: os=linux & cpu=x64 languageName: node linkType: hard -"@typescript/native-preview-win32-arm64@npm:7.0.0-dev.20260507.1": - version: 7.0.0-dev.20260507.1 - resolution: "@typescript/native-preview-win32-arm64@npm:7.0.0-dev.20260507.1" +"@typescript/native-preview-win32-arm64@npm:7.0.0-dev.20260508.1": + version: 7.0.0-dev.20260508.1 + resolution: "@typescript/native-preview-win32-arm64@npm:7.0.0-dev.20260508.1" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@typescript/native-preview-win32-x64@npm:7.0.0-dev.20260507.1": - version: 7.0.0-dev.20260507.1 - resolution: "@typescript/native-preview-win32-x64@npm:7.0.0-dev.20260507.1" +"@typescript/native-preview-win32-x64@npm:7.0.0-dev.20260508.1": + version: 7.0.0-dev.20260508.1 + resolution: "@typescript/native-preview-win32-x64@npm:7.0.0-dev.20260508.1" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@typescript/native-preview@npm:^7.0.0-dev.20260507.1": - version: 7.0.0-dev.20260507.1 - resolution: "@typescript/native-preview@npm:7.0.0-dev.20260507.1" +"@typescript/native-preview@npm:^7.0.0-dev.20260508.1": + version: 7.0.0-dev.20260508.1 + resolution: "@typescript/native-preview@npm:7.0.0-dev.20260508.1" dependencies: - "@typescript/native-preview-darwin-arm64": "npm:7.0.0-dev.20260507.1" - "@typescript/native-preview-darwin-x64": "npm:7.0.0-dev.20260507.1" - "@typescript/native-preview-linux-arm": "npm:7.0.0-dev.20260507.1" - "@typescript/native-preview-linux-arm64": "npm:7.0.0-dev.20260507.1" - "@typescript/native-preview-linux-x64": "npm:7.0.0-dev.20260507.1" - "@typescript/native-preview-win32-arm64": "npm:7.0.0-dev.20260507.1" - "@typescript/native-preview-win32-x64": "npm:7.0.0-dev.20260507.1" + "@typescript/native-preview-darwin-arm64": "npm:7.0.0-dev.20260508.1" + "@typescript/native-preview-darwin-x64": "npm:7.0.0-dev.20260508.1" + "@typescript/native-preview-linux-arm": "npm:7.0.0-dev.20260508.1" + "@typescript/native-preview-linux-arm64": "npm:7.0.0-dev.20260508.1" + "@typescript/native-preview-linux-x64": "npm:7.0.0-dev.20260508.1" + "@typescript/native-preview-win32-arm64": "npm:7.0.0-dev.20260508.1" + "@typescript/native-preview-win32-x64": "npm:7.0.0-dev.20260508.1" dependenciesMeta: "@typescript/native-preview-darwin-arm64": optional: true @@ -911,7 +911,7 @@ __metadata: optional: true bin: tsgo: bin/tsgo.js - checksum: 10c0/539f4da0df37110f33f51be77d8c72d0c206127dbc9f1f86e12ccad39a31e26905edb243c0b54997bd0b679e128a646b7c77b7e38ae1320db7637134cfda033b + checksum: 10c0/5204caa4bde4064acd6e90334ef5746bb5ab148ccc427a1dc887b5196c0c87f86f0abb625086144de649a2ffc3e7351d6439dde25c2f5e7cdb0f47ef802b9c18 languageName: node linkType: hard @@ -1036,7 +1036,7 @@ __metadata: dependencies: "@octokit/rest": "npm:^22.0.1" "@types/node": "npm:>=24" - "@typescript/native-preview": "npm:^7.0.0-dev.20260507.1" + "@typescript/native-preview": "npm:^7.0.0-dev.20260508.1" "@vitest/coverage-v8": "npm:^4.1.5" "@webiny/di": "npm:^0.2.3" adio: "npm:^3.0.0"