From 42c734b5381c4bffb6e5cba725f9ce49a04e6b65 Mon Sep 17 00:00:00 2001 From: ArekAras Date: Mon, 19 Oct 2026 10:08:02 +0200 Subject: [PATCH] fix: publish one combined GitHub release --- .github/scripts/create-release.mjs | 119 ++++++++++++++ .github/scripts/create-release.test.mjs | 166 ++++++++++++++++++++ .github/scripts/fixtures/release-5.4.2.json | 32 ++++ .github/scripts/release-notes.mjs | 98 ++++++++++++ .github/scripts/release-notes.test.mjs | 116 ++++++++++++++ .github/workflows/release.yml | 15 +- .github/workflows/test.yml | 11 ++ CONTRIBUTING.md | 32 +++- package.json | 1 + 9 files changed, 587 insertions(+), 3 deletions(-) create mode 100644 .github/scripts/create-release.mjs create mode 100644 .github/scripts/create-release.test.mjs create mode 100644 .github/scripts/fixtures/release-5.4.2.json create mode 100644 .github/scripts/release-notes.mjs create mode 100644 .github/scripts/release-notes.test.mjs diff --git a/.github/scripts/create-release.mjs b/.github/scripts/create-release.mjs new file mode 100644 index 000000000..5839ca95f --- /dev/null +++ b/.github/scripts/create-release.mjs @@ -0,0 +1,119 @@ +// biome-ignore-all lint/suspicious/noUndeclaredEnvVars: This script runs directly in GitHub Actions outside Turborepo. +import { fileURLToPath, pathToFileURL } from 'node:url'; + +import { readRelease } from './release-notes.mjs'; + +export async function publishRelease({ + release, + packages, + repository, + sha, + token, + publishedPackages = [], + request = fetch, +}) { + if (!repository || !sha || !token) { + throw new Error('GITHUB_REPOSITORY, GITHUB_SHA and GH_TOKEN are required'); + } + + for (const published of publishedPackages) { + if ( + !packages.some( + (pkg) => + pkg.name === published.name && pkg.version === published.version + ) + ) { + throw new Error( + `Published package is outside the fixed release group: ${published.name}@${published.version}` + ); + } + } + + async function api(endpoint, body) { + const method = body ? 'POST' : 'GET'; + const response = await request( + `https://api.github.com/repos/${repository}/${endpoint}`, + { + method, + headers: { + Authorization: `Bearer ${token}`, + Accept: 'application/vnd.github+json', + 'X-GitHub-Api-Version': '2022-11-28', + ...(body ? { 'Content-Type': 'application/json' } : {}), + }, + ...(body ? { body: JSON.stringify(body) } : {}), + signal: AbortSignal.timeout(30_000), + } + ); + if (response.status === 404 && method === 'GET') return null; + if (!response.ok) { + throw new Error( + `GitHub ${method} ${endpoint} failed (${response.status})` + ); + } + return response.json(); + } + + // The commits endpoint resolves both lightweight and annotated package tags. + const mainCommit = await api( + `commits/${encodeURIComponent(release.tag_name)}` + ); + if (mainCommit?.sha !== sha) { + if (publishedPackages.length) { + throw new Error( + 'The main package tag does not point to this release commit. Verify package publication and release tags before retrying.' + ); + } + return 'No release tags for this commit; skipping'; + } + + const existing = await api( + `releases/tags/${encodeURIComponent(release.tag_name)}` + ); + if (existing) return `Release already exists: ${existing.html_url}`; + + // On retries published-packages can be empty or partial. Check the whole + // fixed group so the release never advertises an incomplete publication. + for (const pkg of packages) { + const tag = `${pkg.name}@${pkg.version}`; + if (tag === release.tag_name) continue; + const commit = await api(`commits/${encodeURIComponent(tag)}`); + if (commit?.sha !== sha) { + throw new Error( + `Package tag is missing or points to another commit: ${tag}. Verify package publication and release tags before retrying.` + ); + } + } + + const created = await api('releases', release); + return `Created release: ${created.html_url}`; +} + +async function main() { + const data = await readRelease( + fileURLToPath(new URL('../../', import.meta.url)) + ); + if (process.argv.includes('--dry-run')) { + console.log(JSON.stringify(data.release, null, 2)); + return; + } + console.log( + await publishRelease({ + ...data, + repository: process.env.GITHUB_REPOSITORY, + sha: process.env.GITHUB_SHA, + token: process.env.GH_TOKEN, + publishedPackages: JSON.parse(process.env.PUBLISHED_PACKAGES || '[]'), + }) + ); +} + +if ( + process.argv[1] && + import.meta.url === pathToFileURL(process.argv[1]).href +) { + main().catch((error) => { + console.error(error.message); + process.exitCode = 1; + }); +} diff --git a/.github/scripts/create-release.test.mjs b/.github/scripts/create-release.test.mjs new file mode 100644 index 000000000..8dbdb5bf3 --- /dev/null +++ b/.github/scripts/create-release.test.mjs @@ -0,0 +1,166 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import test from 'node:test'; + +import { publishRelease } from './create-release.mjs'; +import { generateReleaseNotes } from './release-notes.mjs'; + +const packages = JSON.parse( + await readFile( + new URL('./fixtures/release-5.4.2.json', import.meta.url), + 'utf8' + ) +); +const release = generateReleaseNotes(packages); +const sha = 'a'.repeat(40); + +function mockGitHub({ + missingTag, + wrongTag, + existing = false, + failCreateOnce = false, + denied = false, +} = {}) { + const calls = []; + let exists = existing; + let failed = false; + return { + calls, + request: async (url, options) => { + const endpoint = decodeURIComponent( + url.split('/repos/callstack/repack/')[1] + ); + calls.push({ endpoint, ...options }); + let status = 200; + let body; + if (denied) { + status = 403; + } else if (endpoint.startsWith('commits/')) { + const tag = endpoint.slice('commits/'.length); + status = tag === missingTag ? 404 : 200; + body = { sha: tag === wrongTag ? 'b'.repeat(40) : sha }; + } else if (endpoint.startsWith('releases/tags/')) { + status = exists ? 200 : 404; + body = { + html_url: 'https://github.com/callstack/repack/releases/tag/existing', + }; + } else if (endpoint === 'releases' && options.method === 'POST') { + if (failCreateOnce && !failed) { + status = 500; + failed = true; + } else { + status = 201; + exists = true; + body = { + html_url: + 'https://github.com/callstack/repack/releases/tag/created', + }; + } + } else { + assert.fail(`Unexpected GitHub request: ${options.method} ${endpoint}`); + } + return new Response(JSON.stringify(body ?? {}), { status }); + }, + }; +} + +function publish(github, overrides = {}) { + return publishRelease({ + release, + packages, + repository: 'callstack/repack', + sha, + token: 'test-token', + request: github.request, + ...overrides, + }); +} + +test('six published packages produce exactly one release on the existing main tag', async () => { + const github = mockGitHub(); + await publish(github, { publishedPackages: packages }); + const posts = github.calls.filter((call) => call.method === 'POST'); + assert.equal(posts.length, 1); + assert.equal( + github.calls.filter((call) => call.endpoint.startsWith('commits/')).length, + 6 + ); + assert.deepEqual(JSON.parse(posts[0].body), release); + assert.equal(posts[0].headers.Authorization, 'Bearer test-token'); +}); + +test('an existing release is preserved without an update or duplicate', async () => { + const github = mockGitHub({ existing: true }); + assert.match(await publish(github), /already exists/); + assert.equal(github.calls.filter((call) => call.method !== 'GET').length, 0); +}); + +test('retry after GitHub failure works without new npm publications and keeps all notes', async () => { + const github = mockGitHub({ failCreateOnce: true }); + await assert.rejects( + publish(github, { publishedPackages: packages }), + /failed \(500\)/ + ); + assert.match( + await publish(github, { publishedPackages: [] }), + /Created release/ + ); + assert.match(await publish(github), /already exists/); + const posts = github.calls.filter((call) => call.method === 'POST'); + assert.equal(posts.length, 2); + assert.deepEqual(JSON.parse(posts[1].body), release); +}); + +test('a partial publish report still uses notes from the complete fixed group', async () => { + const github = mockGitHub(); + await publish(github, { publishedPackages: [packages[1]] }); + const posted = JSON.parse( + github.calls.find((call) => call.method === 'POST').body + ); + assert.match(posted.body, /Bootstrap new projects/); + assert.match(posted.body, /Fix `\.jpeg` images/); +}); + +test('a normal push without release tags does not create a release', async () => { + const github = mockGitHub({ wrongTag: release.tag_name }); + assert.match(await publish(github), /skipping/); + assert.equal(github.calls.length, 1); +}); + +test('a reported publication without its main tag fails visibly', async () => { + const github = mockGitHub({ missingTag: release.tag_name }); + await assert.rejects( + publish(github, { publishedPackages: packages }), + /main package tag/ + ); +}); + +for (const option of ['missingTag', 'wrongTag']) { + test(`${option} prevents advertising an incomplete or mixed-commit release`, async () => { + const github = mockGitHub({ [option]: `${packages[1].name}@5.4.2` }); + await assert.rejects( + publish(github), + /Package tag is missing or points to another commit/ + ); + assert.equal( + github.calls.filter((call) => call.method === 'POST').length, + 0 + ); + }); +} + +test('permission errors are not treated as a missing release', async () => { + const github = mockGitHub({ denied: true }); + await assert.rejects(publish(github), /failed \(403\)/); +}); + +test('independently versioned packages require an explicit release policy', async () => { + const github = mockGitHub(); + await assert.rejects( + publish(github, { + publishedPackages: [{ name: '@callstack/repack-expo', version: '0.0.2' }], + }), + /outside the fixed release group/ + ); + assert.equal(github.calls.length, 0); +}); diff --git a/.github/scripts/fixtures/release-5.4.2.json b/.github/scripts/fixtures/release-5.4.2.json new file mode 100644 index 000000000..fb7fd83e2 --- /dev/null +++ b/.github/scripts/fixtures/release-5.4.2.json @@ -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" + } +] diff --git a/.github/scripts/release-notes.mjs b/.github/scripts/release-notes.mjs new file mode 100644 index 000000000..e25bcb7b9 --- /dev/null +++ b/.github/scripts/release-notes.mjs @@ -0,0 +1,98 @@ +import { readdir, readFile } from 'node:fs/promises'; +import path from 'node:path'; + +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 sections = []; + 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 }); + } + if (entry) sections.push(`## ${pkg.name}@${pkg.version}\n\n${entry}`); + } + + return { + name: main.version, + tag_name: `${main.name}@${main.version}`, + prerelease: main.version.includes('-'), + body: [ + '## Packages', + packages.map((pkg) => `- \`${pkg.name}@${pkg.version}\``).join('\n'), + ...sections, + ].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 { packages, release: generateReleaseNotes(packages) }; +} diff --git a/.github/scripts/release-notes.test.mjs b/.github/scripts/release-notes.test.mjs new file mode 100644 index 000000000..5b2483bde --- /dev/null +++ b/.github/scripts/release-notes.test.mjs @@ -0,0 +1,116 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import test from 'node:test'; +import { fileURLToPath } from 'node:url'; + +import { generateReleaseNotes, readRelease } from './release-notes.mjs'; + +const fixture = JSON.parse( + await readFile( + new URL('./fixtures/release-5.4.2.json', import.meta.url), + 'utf8' + ) +); + +test('combines the actual 5.4.2 core and init notes into one release', () => { + const release = generateReleaseNotes(fixture); + assert.equal(release.name, '5.4.2'); + assert.equal(release.tag_name, '@callstack/repack@5.4.2'); + assert.equal(release.prerelease, false); + assert.match(release.body, /Fix `\.jpeg` images/); + assert.match( + release.body, + /Bootstrap new projects with React Native 0\.87\.1/ + ); + assert.equal((release.body.match(/^## @callstack\//gm) ?? []).length, 2); + assert.equal((release.body.match(/^- `@callstack\//gm) ?? []).length, 6); + assert.doesNotMatch(release.body, /5\.4\.1|#1479|#1476|consumer ProGuard/); +}); + +test('does not generate empty sections for version-aligned packages', () => { + const release = generateReleaseNotes(fixture); + assert.match( + release.body, + /- `@callstack\/repack-plugin-reanimated@5\.4\.2`/ + ); + assert.doesNotMatch( + release.body, + /^## @callstack\/repack-plugin-reanimated/m + ); +}); + +test('fails rather than publishing the entire changelog when a version 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/ + ); +}); + +test('preserves fenced code and nested headings without reading an older version', () => { + const packages = [ + { + name: '@callstack/repack', + version: '5.4.2', + changelog: [ + '# Package', + '```md', + '## 5.4.2', + 'wrong', + '```', + '## 5.4.2', + '### Patch Changes', + 'Notes', + '````md', + '## 5.0.0', + '```', + 'more code', + '````', + '~~~md', + '# Example', + '~~~', + '## 5.4.1', + 'Old notes', + ].join('\r\n'), + }, + ]; + const release = generateReleaseNotes(packages); + assert.match(release.body, /### Patch Changes\nNotes/); + assert.match(release.body, /## 5\.0\.0\n```\nmore code/); + assert.match(release.body, /~~~md\n# Example\n~~~/); + assert.doesNotMatch(release.body, /wrong|Old notes/); +}); + +test('rejects mixed versions instead of selecting a misleading common tag', () => { + const packages = structuredClone(fixture); + packages[1].version = '5.4.3'; + assert.throws(() => generateReleaseNotes(packages), /version mismatch/); +}); + +test('requires the main package for the canonical release tag', () => { + assert.throws( + () => generateReleaseNotes(fixture.slice(1)), + /Missing @callstack\/repack/ + ); +}); + +test('marks a Changesets prerelease correctly', () => { + const release = generateReleaseNotes([ + { + name: '@callstack/repack', + version: '6.0.0-rc.0', + changelog: '## 6.0.0-rc.0\n\nPreview', + }, + ]); + assert.equal(release.prerelease, true); +}); + +test('reads the configured release group from the repository', async () => { + const { packages, release } = await readRelease( + fileURLToPath(new URL('../../', import.meta.url)) + ); + assert.equal(packages[0].name, '@callstack/repack'); + assert.equal(release.tag_name, `@callstack/repack@${packages[0].version}`); + assert.equal(packages.length, 6); +}); diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 70056d8c8..1b82cc003 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -44,6 +44,9 @@ jobs: - name: Test packages run: pnpm test + - name: Test release automation + run: pnpm test:release + - name: Create release PR or publish packages id: changesets uses: changesets/action@ae32849d5ba541f9ae29e40e22a623bc13562f51 # tag=v2.1.2 @@ -52,12 +55,22 @@ 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 }} + # Retry GitHub release creation after all package tags have been pushed. + # Changesets can report published=false when npm is already up to date. + - name: Create combined GitHub release + if: steps.changesets.outputs.has-changesets == 'false' + run: node .github/scripts/create-release.mjs + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PUBLISHED_PACKAGES: ${{ steps.changesets.outputs.published-packages }} + canary-release: name: Canary Release runs-on: ubuntu-latest diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fddae009c..792b71f30 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -56,3 +56,14 @@ jobs: - name: Run tests run: pnpm test + + release-automation: + name: Release automation tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # tag=v4.2.2 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # tag=v4.4.0 + with: + node-version: '24' + - name: Test combined release generation and publishing + run: node --test .github/scripts/*.test.mjs diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c9c81d5bc..d4380d755 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -91,8 +91,36 @@ 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 include their nonempty changelog entries +for that version. Package changelogs and npm publishing are still managed by +Changesets. + +Preview the combined release without network access or credentials: + +```sh +node .github/scripts/create-release.mjs --dry-run +pnpm test:release +``` + +If GitHub release creation fails after all package tags have reached GitHub, +rerun the failed release job at the same commit. The combined release step also +runs when Changesets has no new packages to publish. It verifies that all +fixed-group package tags resolve to `GITHUB_SHA` before creating the release, +and leaves an existing release untouched. A normal push with no release tags +at its SHA does not create a release. + +This step does not recover npm publications or recreate package tags. If +publishing or pushing tags failed, check the npm publication status and restore +the missing release tags at the original release commit before retrying. A +missing tag or a tag pointing to another commit prevents release creation. + +The combined release currently covers the fixed group containing +`@callstack/repack`. Before adding independently versioned packages to publishing, +extend the release selection policy; unexpected published packages fail this +step instead of being silently omitted. #### Canary releases diff --git a/package.json b/package.json index c88f182d6..e0033862e 100644 --- a/package.json +++ b/package.json @@ -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",