From 6ca9c18f3c7abe6d0e38124f7f5b193e81d10ab1 Mon Sep 17 00:00:00 2001 From: scroll Date: Wed, 29 Jul 2026 13:34:00 +0500 Subject: [PATCH] feat(ci): add package contents verification to CI and publish workflows --- .github/workflows/ci.yml | 27 +++++++++ .github/workflows/publish.yml | 3 + package.json | 4 +- scripts/check-package-contents.mjs | 97 ++++++++++++++++++++++++++++++ 4 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 scripts/check-package-contents.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 69bdb44..f76123f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,6 +65,31 @@ jobs: with: fail_ci_if_error: false + package-contents: + name: package-contents + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + actions: write + + steps: + - name: Checkout + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false + + - name: Setup + uses: ./.github/actions/setup + + - name: Build package + run: yarn build + + # The iOS jobs build the example straight from the workspace, where the + # podspec is always present. Only this job looks at what npm would ship. + - name: Verify published package contents + run: yarn check:package + android-build: name: android-build runs-on: ubuntu-latest @@ -325,6 +350,7 @@ jobs: needs: - lint-and-typecheck - js-tests + - package-contents - android-build - ios-build - android-instrumented @@ -335,6 +361,7 @@ jobs: run: | if [[ "${{ needs.lint-and-typecheck.result }}" != "success" || \ "${{ needs.js-tests.result }}" != "success" || \ + "${{ needs.package-contents.result }}" != "success" || \ "${{ needs.android-build.result }}" != "success" || \ "${{ needs.ios-build.result }}" != "success" || \ "${{ needs.android-instrumented.result }}" != "success" || \ diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 49c91d2..6e9fcdb 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -41,6 +41,9 @@ jobs: - name: Build run: yarn build + - name: Verify published package contents + run: yarn check:package + - name: Verify version matches tag run: | PKG_VERSION=$(node -p "require('./package.json').version") diff --git a/package.json b/package.json index 680534e..8abb568 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ }, "files": [ "README.md", + "DrumPicker.podspec", "img", "func-gif", "LICENSE", @@ -43,7 +44,8 @@ "example": "yarn workspace react-native-drum-picker-example", "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib", "build": "bob build", - "prepublishOnly": "bob build", + "check:package": "node scripts/check-package-contents.mjs", + "prepublishOnly": "bob build && node scripts/check-package-contents.mjs", "typecheck": "tsc", "lint": "eslint \"**/*.{js,ts,tsx}\"", "test": "jest" diff --git a/scripts/check-package-contents.mjs b/scripts/check-package-contents.mjs new file mode 100644 index 0000000..ff6163c --- /dev/null +++ b/scripts/check-package-contents.mjs @@ -0,0 +1,97 @@ +/** + * Verifies that the tarball npm would publish actually contains everything a + * consumer needs to build the native code. + * + * This exists because the `files` array in package.json is an allow-list: a file + * can be present in git, exercised by CI (which builds the example straight from + * the workspace) and still be missing from the published package. That is exactly + * how DrumPicker.podspec went missing in 0.2.4/0.3.0 — without it CocoaPods + * autolinking cannot find the pod and iOS renders + * "Unimplemented component: ". See issue #21. + * + * Run after `yarn build`, since the JS entry points are checked too. + */ + +const { execFileSync } = await import('node:child_process'); +const { readFileSync } = await import('node:fs'); + +const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url))); + +// Files that must be at an exact path in the tarball. +const REQUIRED_FILES = [ + // Resolved by the RN CLI via a `*.podspec` glob over the package root, so the + // path has to stay at the root — a nested copy would not be autolinked. + 'DrumPicker.podspec', + 'react-native.config.js', + 'android/build.gradle', + 'android/src/main/AndroidManifest.xml', + 'android/src/main/java/com/drumpicker/DrumPickerPackage.kt', + 'android/src/main/java/com/drumpicker/DrumPickerViewManager.kt', + // Entry points declared in package.json, checked wherever they point. + pkg.main, + pkg.module, + pkg.types, + pkg.source, +].filter(Boolean); + +// At least one file must match each of these, otherwise the podspec/gradle +// globs resolve to nothing and the native target compiles empty. +const REQUIRED_PATTERNS = [ + { + label: 'iOS sources (DrumPicker.podspec source_files)', + test: (p) => /^ios\/.+\.(h|m|mm|swift|cpp)$/.test(p), + }, + { + label: 'Android Kotlin sources', + test: (p) => /^android\/src\/main\/java\/com\/drumpicker\/.+\.kt$/.test(p), + }, +]; + +const normalize = (p) => p.replace(/\\/g, '/').replace(/^\.\//, ''); + +let raw; +try { + raw = execFileSync('npm', ['pack', '--dry-run', '--json'], { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'inherit'], + shell: process.platform === 'win32', + }); +} catch { + console.error('❌ `npm pack --dry-run --json` failed — see the output above.'); + process.exit(1); +} + +let packed; +try { + packed = JSON.parse(raw)[0].files.map((f) => normalize(f.path)); +} catch { + console.error('❌ Could not parse the output of `npm pack --dry-run --json`.'); + process.exit(1); +} + +const missingFiles = [...new Set(REQUIRED_FILES.map(normalize))].filter( + (f) => !packed.includes(f) +); +const missingPatterns = REQUIRED_PATTERNS.filter( + (p) => !packed.some((f) => p.test(f)) +); + +if (missingFiles.length === 0 && missingPatterns.length === 0) { + console.log( + `✅ Package contents OK — ${packed.length} files, all required native and JS artifacts present.` + ); + process.exit(0); +} + +console.error('❌ The published package would be missing required files:\n'); +for (const f of missingFiles) { + console.error(` - ${f}`); +} +for (const p of missingPatterns) { + console.error(` - no file matching: ${p.label}`); +} +console.error( + '\nAdd the missing paths to the "files" array in package.json.' + + '\nIf a JS entry point is missing, run `yarn build` first.' +); +process.exit(1);