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
32 changes: 32 additions & 0 deletions .github/scripts/fixtures/release-5.4.2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
[
{
"name": "@callstack/repack",
"version": "5.4.2",
"changelog": "# @callstack/repack\n\n## 5.4.2\n\n### Patch Changes\n\n- [#1480](https://github.com/callstack/repack/pull/1480) [`166388b`](https://github.com/callstack/repack/commit/166388b76fc4641f15cd63d3e96aec9d7e582c84) Thanks [@CristiCeban](https://github.com/CristiCeban)! - Fix `.jpeg` images not showing in Android release builds. They were emitted to `raw` instead of `drawable-*`, where React Native looks for them.\n\n If you have a `.jpeg` and a `.png`/`.jpg`/`.gif`/`.webp` image with the same name in the same directory (e.g. `logo.png` and `logo.jpeg`), rename one of them. Both now resolve to the same Android drawable resource name, and the build fails with a duplicate resources error.\n\n- Updated dependencies []:\n - @callstack/repack-dev-server@5.4.2\n\n## 5.4.1\n\n### Patch Changes\n\n- [#1479](https://github.com/callstack/repack/pull/1479) [`811a6d8`](https://github.com/callstack/repack/commit/811a6d84ed09ad83f4395c85218e055e7e9abf91) Thanks [@CristiCeban](https://github.com/CristiCeban)! - Ship consumer ProGuard rules for the Android library so release builds with R8 (`minifyEnabled true`) no longer fail with missing `com.google.errorprone.annotations` classes referenced by `nimbus-jose-jwt`.\n\n- [#1476](https://github.com/callstack/repack/pull/1476) [`5bb41f0`](https://github.com/callstack/repack/commit/5bb41f045f43d935fc79b153f889a1195a0f19ad) Thanks [@whydidoo](https://github.com/whydidoo)! - Support React Native 0.88's Flow parser in Babel loaders, native component codegen, and Reanimated transforms while retaining Hermes parser compatibility with older React Native presets.\n\n- [#1473](https://github.com/callstack/repack/pull/1473) [`f9326a0`](https://github.com/callstack/repack/commit/f9326a0a5fcc7de753190fc7576b91320498dfa8) Thanks [@whydidoo](https://github.com/whydidoo)! - Emit an additional `res/raw` copy of bundled Android font binaries so Metro-compatible consumers such as Skia can load them by name. Preserve the existing `res/font` output for native `@font/...`, `R.font`, and XML font-family references. Generate `res/raw/keep.xml` for bundled Android resources so resource shrinking preserves both font copies and other assets loaded by name at runtime.\n\n XML font-family resources continue to use `res/font`. Asset metadata and iOS, development-server, inline, and remote-loader output paths are unchanged.\n\n- Updated dependencies []:\n - @callstack/repack-dev-server@5.4.1\n\n"
},
{
"name": "@callstack/repack-dev-server",
"version": "5.4.2",
"changelog": "# @callstack/repack-dev-server\n\n## 5.4.2\n\n## 5.4.1\n\n"
},
{
"name": "@callstack/repack-init",
"version": "5.4.2",
"changelog": "# @callstack/repack-init\n\n## 5.4.2\n\n### Patch Changes\n\n- [#1482](https://github.com/callstack/repack/pull/1482) [`4139cf3`](https://github.com/callstack/repack/commit/4139cf3b9fa5f0f80260bfd0b783621543bf3949) Thanks [@jacekAtCK](https://github.com/jacekAtCK)! - Bootstrap new projects with React Native 0.87.1 and refresh the default bundler dependencies to `@rspack/core@^1.7.12`, `@swc/helpers@^0.5.23`, `webpack@^5.111.1` and `terser-webpack-plugin@^5.6.1`. Generated Rspack and webpack configs now enable package exports (`getResolveOptions({ enablePackageExports: true })`).\n\n## 5.4.1\n\n"
},
{
"name": "@callstack/repack-plugin-reanimated",
"version": "5.4.2",
"changelog": "# @callstack/repack-plugin-reanimated\n\n## 5.4.2\n\n## 5.4.1\n\n### Patch Changes\n\n- [#1476](https://github.com/callstack/repack/pull/1476) [`5bb41f0`](https://github.com/callstack/repack/commit/5bb41f045f43d935fc79b153f889a1195a0f19ad) Thanks [@whydidoo](https://github.com/whydidoo)! - Support React Native 0.88's Flow parser in Babel loaders, native component codegen, and Reanimated transforms while retaining Hermes parser compatibility with older React Native presets.\n\n"
},
{
"name": "@callstack/repack-plugin-nativewind",
"version": "5.4.2",
"changelog": "# @callstack/repack-plugin-nativewind\n\n## 5.4.2\n\n## 5.4.1\n\n"
},
{
"name": "@callstack/repack-plugin-expo-modules",
"version": "5.4.2",
"changelog": "# @callstack/repack-plugin-expo-modules\n\n## 5.4.2\n\n## 5.4.1\n\n"
}
]
120 changes: 120 additions & 0 deletions .github/scripts/release-notes.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { readdir, readFile } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';

const mainPackage = '@callstack/repack';

function changelogEntry(changelog, version) {
const content = [];
let found = false;
let fence;

for (const line of changelog.split(/\r?\n/)) {
const marker = /^ {0,3}(`{3,}|~{3,})/.exec(line)?.[1];
if (fence) {
if (new RegExp(`^ {0,3}${fence[0]}{${fence.length},}\\s*$`).test(line)) {
fence = undefined;
}
} else if (marker) {
fence = marker;
} else {
const heading = /^ {0,3}(#{1,2})\s+(.+?)\s*#*\s*$/.exec(line);
if (heading) {
if (found) break;
found = heading[1] === '##' && heading[2] === version;
continue;
}
}
if (found) content.push(line);
}

if (!found) throw new Error(`Missing changelog entry for ${version}`);
return content.join('\n').trim();
}

export function generateReleaseNotes(packages) {
const main = packages.find((pkg) => pkg.name === mainPackage);
if (!main) throw new Error(`Missing ${mainPackage} in the release group`);

const changes = { Major: new Set(), Minor: new Set(), Patch: new Set() };
for (const pkg of packages) {
if (pkg.version !== main.version) {
throw new Error(
`Release group version mismatch: ${pkg.name}@${pkg.version}`
);
}
let entry;
try {
entry = changelogEntry(pkg.changelog, pkg.version);
} catch (error) {
throw new Error(`${pkg.name}: ${error.message}`, { cause: error });
}
const sections = entry.split(/^### (Major|Minor|Patch) Changes\s*$/m);
for (let index = 1; index < sections.length; index += 2) {
for (const item of sections[index + 1].split(/(?=^- )/m)) {
const note = item.trim();
if (note && !note.startsWith('- Updated dependencies')) {
changes[sections[index]].add(note);
}
}
}
}

return [
'## Packages',
packages.map((pkg) => `- \`${pkg.name}@${pkg.version}\``).join('\n'),
...Object.entries(changes)
.filter(([, notes]) => notes.size)
.map(
([type, notes]) => `## ${type} Changes\n\n${[...notes].join('\n\n')}`
),
].join('\n\n');
}

export async function readRelease(root) {
const config = JSON.parse(
await readFile(path.join(root, '.changeset/config.json'), 'utf8')
);
const group = config.fixed.find((names) => names.includes(mainPackage));
if (!group) throw new Error(`Missing fixed release group for ${mainPackage}`);

const manifests = await Promise.all(
(await readdir(path.join(root, 'packages'))).map(async (directory) => {
const dir = path.join(root, 'packages', directory);
const manifest = JSON.parse(
await readFile(path.join(dir, 'package.json'), 'utf8')
);
return { dir, ...manifest };
})
);
const packages = await Promise.all(
group.map(async (name) => {
const pkg = manifests.find((manifest) => manifest.name === name);
if (!pkg || pkg.private || config.ignore.includes(name)) {
throw new Error(
`Release group package is missing or excluded: ${name}`
);
}
return {
name,
version: pkg.version,
changelog: await readFile(path.join(pkg.dir, 'CHANGELOG.md'), 'utf8'),
};
})
);
return generateReleaseNotes(packages);
}

if (
process.argv[1] &&
import.meta.url === pathToFileURL(process.argv[1]).href
) {
try {
console.log(
await readRelease(fileURLToPath(new URL('../../', import.meta.url)))
);
} catch (error) {
console.error(error.message);
process.exitCode = 1;
}
}
74 changes: 74 additions & 0 deletions .github/scripts/release-notes.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import test from 'node:test';

import { generateReleaseNotes } from './release-notes.mjs';

const fixture = JSON.parse(
await readFile(
new URL('./fixtures/release-5.4.2.json', import.meta.url),
'utf8'
)
);

test('merges the actual 5.4.2 notes without dependency updates or empty sections', () => {
const notes = generateReleaseNotes(fixture);
assert.match(notes, /Fix `\.jpeg` images/);
assert.match(notes, /Bootstrap new projects with React Native 0\.87\.1/);
assert.match(notes, /\n\n {2}If you have a `\.jpeg`/);
assert.equal((notes.match(/^## Patch Changes$/gm) ?? []).length, 1);
for (const pkg of fixture) {
assert.ok(notes.includes(`- \`${pkg.name}@${pkg.version}\``));
}
assert.doesNotMatch(
notes,
/Updated dependencies|^## @callstack\/|5\.4\.1|#1479|#1476|consumer ProGuard/m
);
});

test('includes the shared 5.4.1 changeset only once across packages', () => {
const notes = generateReleaseNotes(
fixture.map((pkg) => ({ ...pkg, version: '5.4.1' }))
);
assert.equal((notes.match(/\[#1476\]/g) ?? []).length, 1);
assert.equal((notes.match(/^## Patch Changes$/gm) ?? []).length, 1);
assert.match(notes, /consumer ProGuard/);
assert.match(notes, /XML font-family resources continue to use `res\/font`/);
assert.doesNotMatch(notes, /Updated dependencies|#1480|#1482|5\.4\.2/);
});

test('groups changes by type and preserves multiline notes including fenced headings', () => {
const example =
'- Example\n\n ```md\n ## 0.1.0\n ### Patch Changes\n - Nested item\n ```';
const notes = generateReleaseNotes([
{
name: '@callstack/repack',
version: '6.0.0',
changelog: `## 6.0.0\n\n### Patch Changes\n\n${example}\n\n### Major Changes\n\n- Breaking change\n\n## 5.4.2\n\nOld notes`,
},
{
name: '@callstack/repack-init',
version: '6.0.0',
changelog:
'## 6.0.0\n\n### Minor Changes\n\n- New option\n\n### Patch Changes\n\n- Another fix',
},
]);
assert.ok(notes.includes(example));
assert.ok(
notes.indexOf('## Major Changes') < notes.indexOf('## Minor Changes')
);
assert.ok(
notes.indexOf('## Minor Changes') < notes.indexOf('## Patch Changes')
);
assert.match(notes, /- Another fix/);
assert.doesNotMatch(notes, /Old notes/);
});

test('fails instead of including another version when the requested entry is missing', () => {
const packages = structuredClone(fixture);
packages[0].changelog = '# Package\n\n## 5.4.20\n\nWrong release';
assert.throws(
() => generateReleaseNotes(packages),
/repack: Missing changelog entry for 5\.4\.2/
);
});
21 changes: 20 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,31 @@ jobs:
publish-script: pnpm release
commit-message: 'chore: changeset version'
pr-title: 'chore: changeset version'
create-github-releases: true
create-github-releases: false
push-git-tags: true
github-token: ${{ secrets.GITHUB_TOKEN }}
env:
# @changesets/changelog-github reads the token from the environment.
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Create combined GitHub release
if: steps.changesets.outputs.published == 'true'
run: |
version=$(node -p "require('./packages/repack/package.json').version")
tag="@callstack/repack@${version}"
if gh release view "$tag" >/dev/null 2>&1; then
exit 0
fi
node .github/scripts/release-notes.mjs > "$RUNNER_TEMP/release-notes.md"
args=()
if [[ "$version" == *-* ]]; then
args+=(--prerelease)
fi
gh release create "$tag" --verify-tag --title "$version" \
--notes-file "$RUNNER_TEMP/release-notes.md" "${args[@]}"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

canary-release:
name: Canary Release
runs-on: ubuntu-latest
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,6 @@ jobs:

- name: Run tests
run: pnpm test

- name: Test release notes
run: pnpm test:release
35 changes: 33 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,39 @@ package version changes and generated changelog entries for the next release.

Review the version numbers and changelog entries in that PR before merging it.
When the version PR is merged to `main`, the same workflow runs `pnpm release`,
publishes the packages to npm, pushes the release tags, and creates GitHub
releases from the generated changelog entries.
publishes the packages to npm and pushes all package release tags. A separate
step creates one GitHub release on the main package's tag (for example,
`@callstack/repack@5.4.2`), named after the shared version. Its notes list all
packages in the fixed release group and merge their changelog entries by change
type, without duplicate entries or dependency update bullets. Package changelogs
and npm publishing are still managed by Changesets. Existing GitHub releases
are left untouched.

Preview the combined notes without network access or credentials:

```sh
node .github/scripts/release-notes.mjs
pnpm test:release
```

The GitHub release step runs only when Changesets publishes packages. If it
fails after npm publication, rerunning the workflow may not retry that step,
because there are no new packages to publish. To create the missing GitHub
release manually, check out the original release commit, confirm the packages
are published and their release tags exist, then run:

```sh
version=$(node -p "require('./packages/repack/package.json').version")
tag="@callstack/repack@${version}"
node .github/scripts/release-notes.mjs > /tmp/repack-release-notes.md
gh release create "$tag" --verify-tag --title "$version" \
--notes-file /tmp/repack-release-notes.md
```

For a prerelease version, add `--prerelease` to the `gh release create` command.
This requires an authenticated GitHub CLI with permission to create releases.
It does not recover npm publications or recreate package tags. The combined
release covers the fixed group containing `@callstack/repack`.

#### Canary releases

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"lint:ci": "biome check",
"typecheck": "turbo run typecheck",
"test": "turbo run test",
"test:release": "node --test .github/scripts/*.test.mjs",
"version-packages": "pnpm changeset version && pnpm lint",
"version-packages:canary": "pnpm changeset version --snapshot canary && pnpm lint",
"release": "pnpm changeset publish",
Expand Down
Loading