From 8cc4adcd8c7a4c957e1136d56e9e6bea6d665524 Mon Sep 17 00:00:00 2001 From: Bohdan Pomohaibo Date: Mon, 19 Oct 2026 16:40:46 +0300 Subject: [PATCH 1/9] ci(release): drop the physical acceptance comment gate --- .github/workflows/publish.yml | 11 +--- scripts/release-readiness.mjs | 74 ++------------------- scripts/release-readiness.test.mjs | 100 ++--------------------------- 3 files changed, 12 insertions(+), 173 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index abeb4d0..0120782 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -21,10 +21,6 @@ on: description: 'One successful full CI run ID for this exact SHA' required: true type: string - acceptance_comment: - description: 'GitHub issue comment ID containing maintainer physical acceptance JSON' - required: true - type: string concurrency: group: npm-release @@ -33,7 +29,6 @@ concurrency: permissions: contents: read actions: read - issues: read jobs: release: @@ -44,13 +39,11 @@ jobs: permissions: contents: write actions: read - issues: read id-token: write env: RELEASE_SOURCE: ${{ inputs.source_sha }} RELEASE_VERSION: ${{ inputs.version }} RELEASE_RUNS: ${{ inputs.ci_runs }} - RELEASE_ACCEPTANCE: ${{ inputs.acceptance_comment }} steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: @@ -74,13 +67,13 @@ jobs: - name: Test readiness gate offline run: node --test scripts/release-readiness.test.mjs - - name: Verify source, actual mobile evidence, package and physical acceptance + - name: Verify source, actual mobile evidence and package env: GH_TOKEN: ${{ github.token }} run: >- node scripts/release-readiness.mjs --source "$RELEASE_SOURCE" --version "$RELEASE_VERSION" - --runs "$RELEASE_RUNS" --acceptance "$RELEASE_ACCEPTANCE" + --runs "$RELEASE_RUNS" --out "$RUNNER_TEMP/verified-release" - name: Preserve verified release report and tested tarball diff --git a/scripts/release-readiness.mjs b/scripts/release-readiness.mjs index ee6241c..7f6198d 100644 --- a/scripts/release-readiness.mjs +++ b/scripts/release-readiness.mjs @@ -18,10 +18,6 @@ export const REQUIRED_JOBS = Object.freeze([ 'iOS / performance', 'iOS / rtl', 'iOS / swipe-back', 'iOS / swipeable', 'iOS / two-sided', 'e2e-ios' ]) -export const PHYSICAL_CHECKS = Object.freeze([ - 'defaultAppearance', 'physicalSwipesAndActions', 'autoHaptics', 'hapticsOn', - 'hapticsOff', 'hapticSuppression', 'reduceMotion' -]) const packageName = 'react-native-swipeable-actions' const shaPattern = /^[a-f0-9]{40}$/ const idPattern = /^[1-9][0-9]*$/ @@ -118,61 +114,6 @@ export function validateAndroidLog(log, manifest) { assert.equal(seen.size, 5) } -// REST's user identifies the original author, not whoever last edited the body. -// Resolve its global node ID and require GitHub's edit-specific metadata. -export async function fetchCommentEditMetadata(nodeId, token, request = fetch) { - assert.ok(typeof nodeId === 'string' && nodeId.length > 0, 'Missing comment node ID') - const response = await request('https://api.github.com/graphql', { - method: 'POST', - headers: { Authorization: 'Bearer ' + token, 'Content-Type': 'application/json' }, - body: JSON.stringify({ - query: `query AcceptanceComment($id: ID!) { - node(id: $id) { - __typename - ... on IssueComment { - id body lastEditedAt includesCreatedEdit - author { login } - editor { login } - } - } - }`, - variables: { id: nodeId } - }), - signal: AbortSignal.timeout(120000) - }) - assert.ok(response.ok, 'GitHub comment edit metadata request failed: ' + response.status) - const result = await response.json() - assert.ok(!result.errors?.length, 'GitHub comment edit metadata returned GraphQL errors') - assert.ok(result.data?.node, 'GitHub comment edit metadata unavailable') - return result.data.node -} - -export function validatePhysicalAcceptance(comment, permission, { sourceSha, version, packageIntegrity }) { - assert.equal(comment.user?.type, 'User', 'Physical acceptance must be a human statement') - assert.ok(['admin', 'maintain'].includes(permission), 'Physical acceptance must be from a maintainer') - const metadata = comment.editMetadata - assert.equal(metadata?.__typename, 'IssueComment', 'Authoritative comment edit metadata required') - assert.ok(typeof comment.node_id === 'string' && comment.node_id.length > 0, 'Missing comment node ID') - assert.equal(metadata.id, comment.node_id, 'Edit metadata belongs to another comment') - assert.equal(metadata.body, comment.body, 'Comment changed while fetching acceptance; retry with a new statement') - assert.equal(metadata.author?.login, comment.user.login, 'Comment author differs between API responses') - assert.equal(metadata.lastEditedAt, null, 'Physical acceptance must be unedited; post a new maintainer statement') - assert.equal(metadata.editor, null, 'Physical acceptance has an editor; post a new maintainer statement') - assert.equal(metadata.includesCreatedEdit, false, 'Physical acceptance has edit history; post a new maintainer statement') - const report = JSON.parse(comment.body) - assert.equal(report.schemaVersion, 1) - assert.equal(report.verdict, 'accepted', 'Physical acceptance is incomplete') - assert.equal(report.sourceSha, sourceSha, 'Physical acceptance is for another source') - assert.equal(report.version, version, 'Physical acceptance is for another version') - assert.equal(report.packageIntegrity, packageIntegrity, 'Physical acceptance is for another package') - assert.equal(report.platform, 'physical-ios', 'Simulator evidence cannot establish haptic acceptance') - for (const field of ['device', 'os']) assert.ok(typeof report[field] === 'string' && report[field].trim()) - const testedAt = Date.parse(report.testedAt) - assert.ok(Number.isFinite(testedAt) && testedAt <= Date.parse(comment.created_at), 'Invalid physical test time') - for (const check of PHYSICAL_CHECKS) assert.equal(report.checks?.[check], 'passed', 'Physical check not accepted: ' + check) - return { commentId: comment.id, reviewer: comment.user.login, testedAt: report.testedAt } -} - export function validateReadiness(input) { const release = validateSource(input) assert.equal(input.runs.length, 1, 'Exactly one fresh, fully green full CI run required') @@ -194,15 +135,14 @@ export function validateReadiness(input) { } validateAndroidLog(androidLog, manifest) const packageIntegrity = validatePackage(report, bytes, input.version, input.sourceSha, packedPackage) - const acceptance = validatePhysicalAcceptance(input.comment, input.permission, { ...input, packageIntegrity }) return { schemaVersion: 1, sourceSha: input.sourceSha, version: input.version, ...release, - packageIntegrity, ciRuns: [run.id], acceptance } + packageIntegrity, ciRuns: [run.id] } } async function main(args) { - assert.equal(args.length, 10, 'Expected --source SHA --version VERSION --runs ID --acceptance COMMENT_ID --out DIRECTORY') - const options = Object.fromEntries(Array.from({ length: 5 }, (_, i) => [args[i * 2], args[i * 2 + 1]])) - assert.deepEqual(Object.keys(options).sort(), ['--acceptance', '--out', '--runs', '--source', '--version']) + assert.equal(args.length, 8, 'Expected --source SHA --version VERSION --runs ID --out DIRECTORY') + const options = Object.fromEntries(Array.from({ length: 4 }, (_, i) => [args[i * 2], args[i * 2 + 1]])) + assert.deepEqual(Object.keys(options).sort(), ['--out', '--runs', '--source', '--version']) const sourceSha = options['--source'] const version = options['--version'] const repository = process.env.GITHUB_REPOSITORY @@ -219,7 +159,7 @@ async function main(args) { validateSource(input) const runIds = options['--runs'].split(',') assert.equal(runIds.length, 1, 'Exactly one full CI run ID required') - for (const id of [...runIds, options['--acceptance']]) assert.match(id, idPattern) + for (const id of runIds) assert.match(id, idPattern) const base = 'https://api.github.com/repos/' + repository const request = async (route) => { const response = await fetch(base + route, { @@ -293,10 +233,6 @@ async function main(args) { const packedPackage = JSON.parse(execFileSync('tar', ['-xOf', tarball, 'package/package.json'], { encoding: 'utf8', maxBuffer: 1024 * 1024 })) input.runs.push({ run, jobs, manifest, expectedManifest, bundles, androidLog, report, bytes, packedPackage }) } - input.comment = await json('/issues/comments/' + options['--acceptance']) - input.comment.editMetadata = await fetchCommentEditMetadata(input.comment.node_id, process.env.GH_TOKEN) - const collaborator = await json('/collaborators/' + encodeURIComponent(input.comment.user.login) + '/permission') - input.permission = collaborator.permission === 'admin' ? 'admin' : collaborator.role_name const result = validateReadiness(input) await mkdir(options['--out'], { recursive: true }) await writeFile(path.join(options['--out'], 'package.tgz'), input.runs[0].bytes, { flag: 'wx' }) diff --git a/scripts/release-readiness.test.mjs b/scripts/release-readiness.test.mjs index 084366a..fa545f3 100644 --- a/scripts/release-readiness.test.mjs +++ b/scripts/release-readiness.test.mjs @@ -6,9 +6,9 @@ import { tmpdir } from 'node:os' import path from 'node:path' import test from 'node:test' import { buildManifest, EXPECTED_COUNTS } from '../e2e/helpers/ios-shards.mjs' -import { fetchCommentEditMetadata, PHYSICAL_CHECKS, REQUIRED_JOBS, validateReadiness, validateSource } from './release-readiness.mjs' +import { REQUIRED_JOBS, validateReadiness, validateSource } from './release-readiness.mjs' -// Synthetic records exercise the gate only. They are never release acceptance evidence. +// Synthetic records exercise the gate only. They are never release evidence. const sha = 'a'.repeat(40) const sources = Object.fromEntries(Object.entries(EXPECTED_COUNTS).map(([spec, count]) => [ spec, 'describe("suite", () => { for (let i = 0; i < ' + count + '; i++) it("case " + i, () => {}) })' @@ -52,15 +52,8 @@ function fixture(version = '1.0.0-rc.0', runIds = [101]) { return { run, jobs, manifest, expectedManifest: structuredClone(manifest), bundles, androidLog, report: { sourceSha: sha, cleanSource: true, version, filename: 'package.tgz', integrity: digest, files: ['package.json'] }, bytes, packedPackage: pkg } - }), - permission: 'maintain', comment: { id: 789, node_id: 'IC_fixture', user: { type: 'User', login: 'maintainer' }, - created_at: '2026-10-16T14:00:00Z', updated_at: '2026-10-16T14:00:00Z' } + }) } - value.comment.body = JSON.stringify({ schemaVersion: 1, sourceSha: sha, version, packageIntegrity: digest, - verdict: 'accepted', platform: 'physical-ios', device: 'iPhone test fixture', os: 'iOS fixture', - testedAt: '2026-10-16T13:00:00Z', checks: Object.fromEntries(PHYSICAL_CHECKS.map((name) => [name, 'passed'])) }) - value.comment.editMetadata = { __typename: 'IssueComment', id: value.comment.node_id, body: value.comment.body, - author: { login: 'maintainer' }, lastEditedAt: null, editor: null, includesCreatedEdit: false } return value } @@ -132,22 +125,7 @@ const mutations = { 'wrong package version': (v) => { v.runs[0].report.version = '0.1.12' }, 'modified tarball': (v) => { v.runs[0].bytes = Buffer.from('different') }, 'unsafe filename': (v) => { v.runs[0].report.filename = '../package.tgz' }, - 'actual packed version mismatch': (v) => { v.runs[0].packedPackage = { ...v.pkg, version: '0.1.12' } }, - 'automated acceptance': (v) => { v.comment.user.type = 'Bot' }, - 'non-maintainer acceptance': (v) => { v.permission = 'write' }, - 'missing authoritative edit metadata': (v) => { delete v.comment.editMetadata }, - 'missing lastEditedAt field': (v) => { delete v.comment.editMetadata.lastEditedAt }, - 'missing editor field': (v) => { delete v.comment.editMetadata.editor }, - 'missing edit history flag': (v) => { delete v.comment.editMetadata.includesCreatedEdit }, - 'edited acceptance even with equal REST timestamps': (v) => { v.comment.editMetadata.lastEditedAt = v.comment.created_at }, - 'maintainer edited their own acceptance': (v) => { v.comment.editMetadata.editor = { login: 'maintainer' } }, - 'comment has edit history': (v) => { v.comment.editMetadata.includesCreatedEdit = true }, - 'edit metadata from another comment': (v) => { v.comment.editMetadata.id = 'IC_other' }, - 'edit metadata from another author': (v) => { v.comment.editMetadata.author.login = 'other' }, - 'body changed between REST and GraphQL': (v) => { v.comment.editMetadata.body = '{}' }, - 'wrong GraphQL node type': (v) => { v.comment.editMetadata.__typename = 'Issue' }, - 'missing comment creation time': (v) => { delete v.comment.created_at }, - 'missing human statement': (v) => { v.comment.body = '{}' } + 'actual packed version mismatch': (v) => { v.runs[0].packedPackage = { ...v.pkg, version: '0.1.12' } } } test('accepts exact case multisets despite nested-suite display order, timestamps and ANSI colors', () => { const value = fixture() @@ -172,37 +150,7 @@ for (const [name, mutate] of Object.entries(mutations)) { assert.throws(() => validateReadiness(value)) }) } -for (const [field, replacement] of Object.entries({ - sourceSha: 'b'.repeat(40), version: '0.1.12', packageIntegrity: 'different', verdict: 'pending', - platform: 'simulator', device: '', os: '', testedAt: '2099-01-01T00:00:00Z', checks: {} -})) { - test('rejects physical acceptance mismatch: ' + field, () => { - const value = fixture() - value.comment.body = JSON.stringify({ ...JSON.parse(value.comment.body), [field]: replacement }) - value.comment.editMetadata.body = value.comment.body - assert.throws(() => validateReadiness(value)) - }) -} - -test('rejects a collaborator turning a maintainer pending statement into accepted evidence', () => { - const value = fixture() - // Both APIs now return an accepted body, but REST still identifies the maintainer author. - value.comment.editMetadata.lastEditedAt = '2026-10-16T15:00:00Z' - value.comment.editMetadata.editor = { login: 'write-collaborator' } - value.comment.editMetadata.includesCreatedEdit = true - value.comment.updated_at = value.comment.editMetadata.lastEditedAt - assert.throws(() => validateReadiness(value), /must be unedited/) -}) - -test('uses edit-specific metadata rather than inferring edits from REST updated_at', () => { - const value = fixture() - value.comment.updated_at = '2026-10-16T15:00:00Z' - assert.equal(validateReadiness(value).acceptance.reviewer, 'maintainer') -}) - -test('short physical smoke retains seven grouped checks and both complete 62-case mobile inventories', () => { - assert.deepEqual(PHYSICAL_CHECKS, ['defaultAppearance', 'physicalSwipesAndActions', 'autoHaptics', - 'hapticsOn', 'hapticsOff', 'hapticSuppression', 'reduceMotion']) +test('requires both complete 62-case mobile inventories', () => { const value = fixture() for (const evidence of value.runs) { assert.equal(evidence.manifest.shards.reduce((count, shard) => count + shard.cases.length, 0), 62) @@ -210,44 +158,6 @@ test('short physical smoke retains seven grouped checks and both complete 62-cas assert.deepEqual(validateReadiness(value).ciRuns, [101]) }) -for (const check of PHYSICAL_CHECKS) { - test('requires physical smoke confirmation: ' + check, () => { - const value = fixture() - const report = JSON.parse(value.comment.body) - delete report.checks[check] - value.comment.body = JSON.stringify(report) - value.comment.editMetadata.body = value.comment.body - assert.throws(() => validateReadiness(value), /Physical check not accepted/) - }) -} - -test('fetches edit provenance through the REST comment global node ID', async () => { - const metadata = fixture().comment.editMetadata - const result = await fetchCommentEditMetadata('IC_fixture', 'synthetic-token', async (url, options) => { - assert.equal(url, 'https://api.github.com/graphql') - assert.equal(options.method, 'POST') - assert.equal(options.headers.Authorization, 'Bearer synthetic-token') - const { query, variables } = JSON.parse(options.body) - assert.deepEqual(variables, { id: 'IC_fixture' }) - for (const field of ['IssueComment', 'id', 'body', 'lastEditedAt', 'includesCreatedEdit', 'author', 'editor']) { - assert.ok(query.includes(field), 'Missing GraphQL field: ' + field) - } - return { ok: true, json: async () => ({ data: { node: metadata } }) } - }) - assert.deepEqual(result, metadata) -}) - -for (const [name, response] of Object.entries({ - 'HTTP failure': { ok: false, status: 403 }, - 'partial GraphQL errors': { ok: true, json: async () => ({ data: { node: fixture().comment.editMetadata }, errors: [{ message: 'denied' }] }) }, - 'missing node': { ok: true, json: async () => ({ data: { node: null } }) }, - 'missing data': { ok: true, json: async () => ({}) } -})) { - test('fails closed fetching comment metadata: ' + name, async () => { - await assert.rejects(fetchCommentEditMetadata('IC_fixture', 'synthetic-token', async () => response)) - }) -} - // Exercise the actual workflow shell boundaries with functions that replace git/npm. // These tests never invoke real git, contact a remote, create tags or publish packages. // Directory changes are real: git must resolve the checkout while npm uses the artifact directory. From 9526222c537ef01b3654d8e1ea748ba52fd7e6e7 Mon Sep 17 00:00:00 2001 From: Bohdan Pomohaibo Date: Mon, 19 Oct 2026 16:40:47 +0300 Subject: [PATCH 2/9] docs(releasing): make the iPhone check a manual step for iOS changes --- docs/releasing.md | 75 ++++++++++++++++------------------------------- docs/support.md | 2 +- 2 files changed, 27 insertions(+), 50 deletions(-) diff --git a/docs/releasing.md b/docs/releasing.md index 8e1308b..d91017d 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -3,8 +3,7 @@ The prepared `main` revision is the release candidate. `CHANGELOG.md` is the release-note source of truth. This procedure does not authorize a version change, tag, dispatch, or publication by itself. Historical runs and local or offline -checks cannot replace a successful run of the prepared source or physical -acceptance. +checks cannot replace a successful run of the prepared source. ## Prepare an unpublished exact version @@ -138,57 +137,34 @@ complete WDIO summary and every passing case title (including duplicate counts) A success badge, hand-entered count or summary without case output cannot pass. Missing/expired artifacts or logs and changed reporter formats fail closed. Recycling, detach and RTL remain mandatory in this same-source 62-case E2E -inventory on both platforms. They are automated evidence requirements, -not extra physical smoke confirmations; removing their manual keys does not remove -any automated cases, assertions or required jobs. - -## Explicit physical acceptance - -A repository admin or maintainer must record a JSON issue comment after testing -the exact prepared source/package on a physical iPhone. Supply that comment's -numeric ID, not an issue ID, to the workflow. Simulator, unit, parser and harness -passes cannot create or imply this acceptance. Do not paste a passing template -without performing the checks. The required JSON fields are: - -- `schemaVersion`: `1`; `verdict`: `"accepted"`; `platform`: `"physical-ios"` -- `sourceSha`: full prepared SHA; `version`: exact prepared version -- `packageIntegrity`: the tested package report's SHA-512 integrity -- `device`: model (no serial/UDID); `os`: tested iOS version; `testedAt`: ISO timestamp -- `checks`: an object with each of the following keys explicitly set to `"passed"` - -Keep the physical smoke short, with seven grouped confirmations: - -- `defaultAppearance`: inspect the default row and action appearance. -- `physicalSwipesAndActions`: swipe both edges and tap the revealed actions. -- `autoHaptics`: feel Auto feedback, including repeated threshold crossings within - one gesture to confirm feedback is not repeated. -- `hapticsOn`: feel explicitly enabled feedback. -- `hapticsOff`: confirm explicitly disabled feedback stays silent. -- `hapticSuppression`: confirm programmatic changes, cancellation and auto-close - do not produce unintended feedback. -- `reduceMotion`: change Reduce Motion while the app is running and check its - live effect on motion and Auto feedback. - -The comment author must be a human with current admin/maintain permission, and -the acceptance comment must be **unedited**. Corrections require a new comment -and its new numeric ID, even if the original author makes the correction. -The gate resolves the REST comment's `node_id` through -[GitHub's global node lookup](https://docs.github.com/en/graphql/guides/using-global-node-ids) -and matches the returned comment ID, author and exact body. GitHub's -[IssueComment fields](https://docs.github.com/en/graphql/reference/issues#issuecomment) -must report `lastEditedAt: null`, `editor: null` and `includesCreatedEdit: false`. -Missing metadata, GraphQL errors and mismatched snapshots fail closed. REST's -`user` and `updated_at` alone do not establish who wrote the final body. The test -time must precede or equal comment creation. Keep unresolved observations out of -an accepted record; document them as blockers instead. +inventory on both platforms. They are automated evidence requirements. + +## Physical iPhone check + +Simulators cannot show haptics. Run this check on a physical iPhone before you +prepare a release, if `ios/` or haptics, appearance or Reduce Motion code changed +since the last check. The publish workflow does not check it. + +1. Build the example app in Release configuration on the iPhone. +2. Inspect the default row and action appearance. +3. Swipe both edges and tap the revealed actions. +4. Feel Auto feedback. Cross the threshold several times in one gesture, and make + sure the feedback does not repeat. +5. Feel feedback with haptics on. Make sure nothing plays with haptics off. +6. Make sure programmatic changes, cancellation and auto-close play no feedback. +7. Change Reduce Motion while the app runs, and check its live effect. + +If a check fails, fix it before you prepare the release. Write the device and the +iOS version in the preparation PR. ## Verify, then publish -Configure the GitHub `npm-release` environment with required human reviewers and -main-only deployment rules, and npm trusted publishing for this workflow. Retain +Configure the GitHub `npm-release` environment with a main-only deployment rule, +and npm trusted publishing for this workflow. A required reviewer is optional. It +adds an approval step before each run. Retain the reviewed `main` workflow as the authority. Dispatch **Publish to npm** from the prepared `main` revision with `operation=verify`, full `source_sha`, exact -`version`, `ci_runs=ID`, and `acceptance_comment=ID`. Verification does not tag +`version`, and `ci_runs=ID`. Verification does not tag or publish. It saves a verified report and the tested tarball. The source package, lockfile, changelog entry, tarball and eventual `v` tag must agree. @@ -242,7 +218,8 @@ version and its `v` tag are permanent records. Release that names the problem and the replacement. 5. Fix the problem on `main` through the normal review. Then prepare the next exact version, for example `1.0.0-rc.` after a bad RC. Run the complete procedure - on that source: fresh full CI, physical acceptance, verify, then publish. No + on that source: the physical iPhone check if it applies, fresh full CI, verify, + then publish. No evidence of the bad version carries over. The publish of the corrective version moves its dist-tag to it. 6. The readiness gate and the publish workflow accept only `1.0.0-rc.N` and diff --git a/docs/support.md b/docs/support.md index 9d7d36c..80d723c 100644 --- a/docs/support.md +++ b/docs/support.md @@ -74,7 +74,7 @@ the UIKit view on the simulator. - iOS versions below the simulator runtime above, and Android API levels other than 34. - Physical Android devices for 1.0. Earlier 0.x benchmarks used a Pixel 8 Pro. - Haptics and Reduce Motion on a physical iPhone. The maintainer checks them by - hand on each release candidate. This page will name the device for 1.0.0. + hand when the iOS code changes. This page will name the device for 1.0.0. If you run the package on another configuration, an issue with the result helps, whether it works or not. From 08701a11f144c6e369dfff4285f0bc8bca3b918a Mon Sep 17 00:00:00 2001 From: Bohdan Pomohaibo Date: Mon, 19 Oct 2026 16:47:17 +0300 Subject: [PATCH 3/9] docs(releasing): link the iPhone check from the prepare steps --- docs/releasing.md | 21 ++++++++++++--------- docs/support.md | 3 ++- e2e/README.md | 2 +- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/docs/releasing.md b/docs/releasing.md index d91017d..851b86a 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -7,8 +7,9 @@ checks cannot replace a successful run of the prepared source. ## Prepare an unpublished exact version -1. Finish the implementation and its independent review. Final acceptance must - use the exact prepared source and version. Release candidates use an explicit +1. Finish the implementation and its independent review. If the + [physical iPhone check](#physical-iphone-check) applies, do it now. The full CI + run must use the exact prepared source and version. Release candidates use an explicit `1.0.0-rc.N`, and the stable release uses `1.0.0`. Never use `npm version prerelease` to infer v1. 2. In a separately reviewed preparation change, use @@ -26,8 +27,8 @@ checks cannot replace a successful run of the prepared source. the 120-second case deadlines, or the 45-minute mobile job deadlines. RC and stable preparation each change source identity. Evidence for an RC does -not authorize publishing a subsequently bumped stable package. Repeat full -acceptance on the stable source. If `main` moves before publication, prepare and +not authorize publishing a subsequently bumped stable package. Repeat the full +CI run on the stable source. If `main` moves before publication, prepare and test the intended current revision again; never rewrite evidence to match it. ## Dependency advisories @@ -142,8 +143,8 @@ inventory on both platforms. They are automated evidence requirements. ## Physical iPhone check Simulators cannot show haptics. Run this check on a physical iPhone before you -prepare a release, if `ios/` or haptics, appearance or Reduce Motion code changed -since the last check. The publish workflow does not check it. +prepare a release, if `ios/` code changed since the last check, or if TypeScript +code for haptics, appearance or Reduce Motion changed. The publish workflow does not check it. 1. Build the example app in Release configuration on the iPhone. 2. Inspect the default row and action appearance. @@ -152,10 +153,12 @@ since the last check. The publish workflow does not check it. sure the feedback does not repeat. 5. Feel feedback with haptics on. Make sure nothing plays with haptics off. 6. Make sure programmatic changes, cancellation and auto-close play no feedback. -7. Change Reduce Motion while the app runs, and check its live effect. +7. Change Reduce Motion while the app runs. Check its live effect on motion, and + make sure Auto feedback does not change. -If a check fails, fix it before you prepare the release. Write the device and the -iOS version in the preparation PR. +If a check fails, fix it before you prepare the release. Write the tested commit +SHA, the device and the iOS version in the preparation PR. The next check compares +against that SHA. ## Verify, then publish diff --git a/docs/support.md b/docs/support.md index 80d723c..9e0bf51 100644 --- a/docs/support.md +++ b/docs/support.md @@ -74,7 +74,8 @@ the UIKit view on the simulator. - iOS versions below the simulator runtime above, and Android API levels other than 34. - Physical Android devices for 1.0. Earlier 0.x benchmarks used a Pixel 8 Pro. - Haptics and Reduce Motion on a physical iPhone. The maintainer checks them by - hand when the iOS code changes. This page will name the device for 1.0.0. + hand when the iOS code or the haptics, appearance or Reduce Motion code + changes. This page will name the device for 1.0.0. If you run the package on another configuration, an issue with the result helps, whether it works or not. diff --git a/e2e/README.md b/e2e/README.md index d800cb3..2d99927 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -2,7 +2,7 @@ The mobile suites exercise the demo through WebdriverIO and Appium. The command ownership regressions exercise the installed test framework and HTTP transport -offline; they do not replace Android, iOS, or physical-device acceptance. +offline; they do not replace Android, iOS, or the physical iPhone check. ## Offline harness checks From 3cea6bf3d08fdbfc74d9abf41513385f12795bd9 Mon Sep 17 00:00:00 2001 From: Bohdan Pomohaibo Date: Mon, 19 Oct 2026 16:50:14 +0300 Subject: [PATCH 4/9] docs(releasing): run the iPhone check on the prepared SHA --- docs/releasing.md | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/docs/releasing.md b/docs/releasing.md index 851b86a..03823f8 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -7,9 +7,8 @@ checks cannot replace a successful run of the prepared source. ## Prepare an unpublished exact version -1. Finish the implementation and its independent review. If the - [physical iPhone check](#physical-iphone-check) applies, do it now. The full CI - run must use the exact prepared source and version. Release candidates use an explicit +1. Finish the implementation and its independent review. The full CI run must use + the exact prepared source and version. Release candidates use an explicit `1.0.0-rc.N`, and the stable release uses `1.0.0`. Never use `npm version prerelease` to infer v1. 2. In a separately reviewed preparation change, use @@ -25,6 +24,8 @@ checks cannot replace a successful run of the prepared source. with both mobile suites enabled. The run must completely succeed on attempt 1. A failed-jobs rerun is not release evidence. Do not change assertions, retries, the 120-second case deadlines, or the 45-minute mobile job deadlines. + If the [physical iPhone check](#physical-iphone-check) applies, do it on the + same SHA. RC and stable preparation each change source identity. Evidence for an RC does not authorize publishing a subsequently bumped stable package. Repeat the full @@ -142,9 +143,16 @@ inventory on both platforms. They are automated evidence requirements. ## Physical iPhone check -Simulators cannot show haptics. Run this check on a physical iPhone before you -prepare a release, if `ios/` code changed since the last check, or if TypeScript -code for haptics, appearance or Reduce Motion changed. The publish workflow does not check it. +Simulators cannot show haptics. Run this check on a physical iPhone with the +prepared SHA in these cases. The publish workflow does not check it. + +- No earlier check SHA is recorded. +- Since the last check SHA, one of these changed: `ios/` code, TypeScript code for + haptics, appearance or Reduce Motion, or the React Native or Expo version in + `package-lock.json`. + +Steps: + 1. Build the example app in Release configuration on the iPhone. 2. Inspect the default row and action appearance. @@ -156,9 +164,9 @@ code for haptics, appearance or Reduce Motion changed. The publish workflow does 7. Change Reduce Motion while the app runs. Check its live effect on motion, and make sure Auto feedback does not change. -If a check fails, fix it before you prepare the release. Write the tested commit -SHA, the device and the iOS version in the preparation PR. The next check compares -against that SHA. +If a check fails, fix it and prepare the release again. If all checks pass, write +the tested SHA, the device and the iOS version in a comment on the preparation PR. +The next check compares against that SHA. ## Verify, then publish From 517b20cacc1e96be323ac333cc92cf02304bedb5 Mon Sep 17 00:00:00 2001 From: Bohdan Pomohaibo Date: Mon, 19 Oct 2026 16:52:31 +0300 Subject: [PATCH 5/9] docs(releasing): watch the example app versions for the iPhone check --- docs/releasing.md | 4 ++-- docs/support.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/releasing.md b/docs/releasing.md index 03823f8..bdd3019 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -149,7 +149,7 @@ prepared SHA in these cases. The publish workflow does not check it. - No earlier check SHA is recorded. - Since the last check SHA, one of these changed: `ios/` code, TypeScript code for haptics, appearance or Reduce Motion, or the React Native or Expo version in - `package-lock.json`. + `example/package.json`. Steps: @@ -229,7 +229,7 @@ version and its `v` tag are permanent records. Release that names the problem and the replacement. 5. Fix the problem on `main` through the normal review. Then prepare the next exact version, for example `1.0.0-rc.` after a bad RC. Run the complete procedure - on that source: the physical iPhone check if it applies, fresh full CI, verify, + on that source: fresh full CI, the physical iPhone check if it applies, verify, then publish. No evidence of the bad version carries over. The publish of the corrective version moves its dist-tag to it. diff --git a/docs/support.md b/docs/support.md index 9e0bf51..608be42 100644 --- a/docs/support.md +++ b/docs/support.md @@ -74,8 +74,8 @@ the UIKit view on the simulator. - iOS versions below the simulator runtime above, and Android API levels other than 34. - Physical Android devices for 1.0. Earlier 0.x benchmarks used a Pixel 8 Pro. - Haptics and Reduce Motion on a physical iPhone. The maintainer checks them by - hand when the iOS code or the haptics, appearance or Reduce Motion code - changes. This page will name the device for 1.0.0. + hand when the iOS code, the haptics, appearance or Reduce Motion code, or the + example app's React Native or Expo version changes. This page will name the device for 1.0.0. If you run the package on another configuration, an issue with the result helps, whether it works or not. From 09133a843302e22e36912fc2b3e86e2c9fdc44d8 Mon Sep 17 00:00:00 2001 From: Bohdan Pomohaibo Date: Mon, 19 Oct 2026 16:54:51 +0300 Subject: [PATCH 6/9] docs(support): say haptics are checked by hand, not in CI --- docs/support.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/support.md b/docs/support.md index 608be42..e15ac85 100644 --- a/docs/support.md +++ b/docs/support.md @@ -73,9 +73,10 @@ the UIKit view on the simulator. - iOS versions below the simulator runtime above, and Android API levels other than 34. - Physical Android devices for 1.0. Earlier 0.x benchmarks used a Pixel 8 Pro. -- Haptics and Reduce Motion on a physical iPhone. The maintainer checks them by - hand when the iOS code, the haptics, appearance or Reduce Motion code, or the - example app's React Native or Expo version changes. This page will name the device for 1.0.0. +- Haptics and Reduce Motion in CI, because simulators cannot show haptics. The + maintainer checks them by hand on a physical iPhone when the iOS code, the + haptics, appearance or Reduce Motion code, or the example app's React Native or + Expo version changes. If you run the package on another configuration, an issue with the result helps, whether it works or not. From d19f0f9ed2c5423017353c5874cd014f6254a936 Mon Sep 17 00:00:00 2001 From: Bohdan Pomohaibo Date: Mon, 19 Oct 2026 16:56:09 +0300 Subject: [PATCH 7/9] docs(support): limit the untested item to the physical feel --- docs/support.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/support.md b/docs/support.md index e15ac85..f10ec62 100644 --- a/docs/support.md +++ b/docs/support.md @@ -73,10 +73,10 @@ the UIKit view on the simulator. - iOS versions below the simulator runtime above, and Android API levels other than 34. - Physical Android devices for 1.0. Earlier 0.x benchmarks used a Pixel 8 Pro. -- Haptics and Reduce Motion in CI, because simulators cannot show haptics. The - maintainer checks them by hand on a physical iPhone when the iOS code, the - haptics, appearance or Reduce Motion code, or the example app's React Native or - Expo version changes. +- The physical feel of haptics, and a live Reduce Motion change on a device. CI + tests the haptic and Reduce Motion state logic only. The maintainer checks the + rest by hand on a physical iPhone when the iOS code, the haptics, appearance or + Reduce Motion code, or the example app's React Native or Expo version changes. If you run the package on another configuration, an issue with the result helps, whether it works or not. From 89cb11dd2270ec130aad52e1b9167ed8f5fa0fe4 Mon Sep 17 00:00:00 2001 From: Bohdan Pomohaibo Date: Mon, 19 Oct 2026 16:57:54 +0300 Subject: [PATCH 8/9] docs(support): drop the CI coverage claim for haptics --- docs/support.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/support.md b/docs/support.md index f10ec62..b67f4dc 100644 --- a/docs/support.md +++ b/docs/support.md @@ -73,10 +73,10 @@ the UIKit view on the simulator. - iOS versions below the simulator runtime above, and Android API levels other than 34. - Physical Android devices for 1.0. Earlier 0.x benchmarks used a Pixel 8 Pro. -- The physical feel of haptics, and a live Reduce Motion change on a device. CI - tests the haptic and Reduce Motion state logic only. The maintainer checks the - rest by hand on a physical iPhone when the iOS code, the haptics, appearance or - Reduce Motion code, or the example app's React Native or Expo version changes. +- How haptics feel and how Reduce Motion behaves on a device. The maintainer + checks them by hand on a physical iPhone when the iOS code, the haptics, + appearance or Reduce Motion code, or the example app's React Native or Expo + version changes. If you run the package on another configuration, an issue with the result helps, whether it works or not. From 3858e29b0dc17791bd11abb8327e32f522fa49c2 Mon Sep 17 00:00:00 2001 From: Bohdan Pomohaibo Date: Mon, 19 Oct 2026 16:59:20 +0300 Subject: [PATCH 9/9] docs(support): match the iPhone check triggers --- docs/support.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/support.md b/docs/support.md index b67f4dc..5debc9e 100644 --- a/docs/support.md +++ b/docs/support.md @@ -73,10 +73,11 @@ the UIKit view on the simulator. - iOS versions below the simulator runtime above, and Android API levels other than 34. - Physical Android devices for 1.0. Earlier 0.x benchmarks used a Pixel 8 Pro. -- How haptics feel and how Reduce Motion behaves on a device. The maintainer - checks them by hand on a physical iPhone when the iOS code, the haptics, +- Automated tests of how haptics feel and how Reduce Motion behaves on a device. + The maintainer checks them by hand on a physical iPhone. The check runs when no + earlier check is recorded, and after a change to the iOS code, the haptics, appearance or Reduce Motion code, or the example app's React Native or Expo - version changes. + version. If you run the package on another configuration, an issue with the result helps, whether it works or not.