Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
efb44fd
feat(common): parse the immutable brand identity artifact v1
AprilNEA Aug 3, 2026
2f32ac1
feat(common): render brand identity and stage assets through the pinn…
AprilNEA Aug 3, 2026
635dfa2
feat(desktop): derive app identity and isolated storage from the bran…
AprilNEA Aug 3, 2026
09dd504
feat(desktop): generate the electron-builder overlay from the rendere…
AprilNEA Aug 3, 2026
97acb9c
feat(mobile): apply the rendered brand to Expo config before prebuild
AprilNEA Aug 3, 2026
cce8705
fix(mobile): render startup mark from brand assets
AprilNEA Aug 4, 2026
77f3fb8
fix(desktop): bind packaged identity to rendered brand
AprilNEA Aug 4, 2026
99bc3ff
feat(release): validate brand build matrix
AprilNEA Aug 7, 2026
541bdcf
test(release): cover brand matrix isolation
AprilNEA Aug 7, 2026
2633dbd
feat(release): validate protected release inputs
AprilNEA Aug 7, 2026
153760d
feat(mobile): bind branded release destinations
AprilNEA Aug 7, 2026
7a4a333
feat(release): enforce store configuration compliance
AprilNEA Aug 7, 2026
60ea0fe
feat(release): bind artifact provenance
AprilNEA Aug 7, 2026
4ef5517
feat(release): add provenance verification CLI
AprilNEA Aug 7, 2026
16cb7ae
test(release): cover provenance isolation
AprilNEA Aug 7, 2026
7bcc36e
feat(release): consume rendered brand artifacts
AprilNEA Aug 7, 2026
e70f084
feat(release): build isolated brand matrix
AprilNEA Aug 7, 2026
567ca6c
feat(release): gate matrix publication
AprilNEA Aug 7, 2026
af2978a
docs(release): document brand matrix contract
AprilNEA Aug 7, 2026
b54ea67
merge: sync master into code-559
lucas77778 Aug 8, 2026
dab0be6
fix(release): harden brand matrix trust
lucas77778 Aug 8, 2026
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
16 changes: 14 additions & 2 deletions .github/actions/render-release-config/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ inputs:
description: Public keyrings JSON content (vars.CONFIG_RELEASE_KEYRINGS)
required: false
default: ""
brand-artifacts:
description: Render the desktop brand identity, assets, and builder overlay
required: false
default: "false"
release-manifest:
description: Desktop release-render manifest JSON content (app == desktop)
required: false
Expand Down Expand Up @@ -52,6 +56,7 @@ runs:
MANIFEST_DESKTOP: ${{ inputs.release-manifest }}
MANIFEST_IOS: ${{ inputs.release-manifest-ios }}
MANIFEST_ANDROID: ${{ inputs.release-manifest-android }}
BRAND_ARTIFACTS: ${{ inputs.brand-artifacts }}
run: |
set -euo pipefail

Expand All @@ -73,7 +78,7 @@ runs:
exit 1
fi

work="$RUNNER_TEMP/config-render"
work="$RUNNER_TEMP/config-render-$APP"
mkdir -p "$work"
printf '%s' "$REVISION_JSON" > "$work/revision.json"
printf '%s' "$KEYRINGS_JSON" > "$work/keyrings.json"
Expand Down Expand Up @@ -144,8 +149,15 @@ runs:
--telemetry-endpoint "$telemetry"
)
if [ "$APP" = desktop ]; then
brand_args=()
if [ "$BRAND_ARTIFACTS" = true ]; then
brand_args=(--brand-artifacts)
elif [ "$BRAND_ARTIFACTS" != false ]; then
echo "::error::brand-artifacts must be true or false"
exit 1
fi
pnpm -F @linkcode/desktop config:render "${common_args[@]}" \
--release-manifest "$work/manifest-desktop.json"
--release-manifest "$work/manifest-desktop.json" "${brand_args[@]}"
else
pnpm -F @linkcode/mobile config:render "${common_args[@]}" \
--release-manifest-ios "$work/manifest-ios.json" \
Expand Down
343 changes: 343 additions & 0 deletions .github/scripts/brand-matrix.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,343 @@
const process = require('node:process');

const BUILD_MATRIX_VERSION = 1;
const PLATFORMS = ['desktop', 'ios', 'android'];
const CHECKLIST_KEYS = [
'configurableFeaturesDisclosed',
'dataPracticesReviewed',
'noExecutableCode',
'permissionsReviewed',
'storeMetadataReviewed',
];
const RELEASE_MANIFEST_KEYS = [
'brandId',
'channel',
'configRevisionId',
'expectedSnapshotSha256',
'platform',
'publicKeyringsSha256',
'publisherGitSha',
'releaseManifestFormatVersion',
'revisionSha256',
'sourceGitSha',
'telemetryEndpoint',
];

const RE_BRAND_ID = /^[a-z][a-z0-9-]{0,62}$/;
const RE_GIT_SHA = /^[0-9a-f]{40}$/;
const RE_SHA256 = /^[0-9a-f]{64}$/;
const RE_REVISION = /^[A-Z0-9][\w.-]{0,127}$/i;
const RE_BUCKET = /^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/;
const RE_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
const RE_DISCLOSED_FEATURE = /^(?:feature|modules)\.[\w.-]+$/;
const RE_R2_PREFIX = /^[a-z0-9][a-z0-9/-]*$/;
const RE_TRAILING_SLASH = /\/$/;
const RE_TEAM_ID = /^[A-Z0-9]{10}$/;
const RE_ASC_APP_ID = /^\d+$/;
const RE_SECRET_PREFIX = /^[A-Z][A-Z0-9_]{1,31}$/;

function fail(path, message) {
throw new TypeError(`${path}: ${message}`);
}

function record(value, path) {
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
fail(path, 'must be an object');
}
return value;
}

function exact(value, keys, path) {
const actual = Object.keys(value).sort();
const expected = [...keys].sort();
if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) {
fail(path, `must contain exactly: ${expected.join(', ')}`);
}
}

function string(value, path, pattern) {
if (typeof value !== 'string' || value.length === 0) fail(path, 'must be a non-empty string');
if (pattern && !pattern.test(value)) fail(path, 'has an invalid format');
return value;
}

function httpsUrl(value, path) {
const text = string(value, path);
let url;
try {
url = new URL(text);
} catch {
fail(path, 'must be an absolute HTTPS URL');
}
if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash) {
fail(path, 'must be HTTPS without credentials, query, or fragment');
}
return text;
}

function releaseManifest(value, path, platform, brandId, channel) {
const manifest = record(value, path);
exact(manifest, RELEASE_MANIFEST_KEYS, path);
if (manifest.releaseManifestFormatVersion !== 1) fail(path, 'format version must be 1');
for (const field of ['brandId', 'channel', 'configRevisionId', 'platform']) {
string(manifest[field], `${path}.${field}`);
}
for (const field of ['publisherGitSha', 'sourceGitSha']) {
string(manifest[field], `${path}.${field}`, RE_GIT_SHA);
}
for (const field of ['expectedSnapshotSha256', 'publicKeyringsSha256', 'revisionSha256']) {
string(manifest[field], `${path}.${field}`, RE_SHA256);
}
string(manifest.configRevisionId, `${path}.configRevisionId`, RE_REVISION);
httpsUrl(manifest.telemetryEndpoint, `${path}.telemetryEndpoint`);
if (
manifest.brandId !== brandId ||
manifest.channel !== channel ||
manifest.platform !== platform
) {
fail(path, `must target ${brandId}/${platform}/${channel}`);
}
return manifest;
}

function compliance(value, path) {
const declaration = record(value, path);
exact(declaration, ['checklist', 'disclosedFeatures'], path);
if (!Array.isArray(declaration.disclosedFeatures)) {
fail(`${path}.disclosedFeatures`, 'must be an array');
}
const features = declaration.disclosedFeatures.map((entry, index) =>
string(entry, `${path}.disclosedFeatures[${index}]`, RE_DISCLOSED_FEATURE),
);
if (
new Set(features).size !== features.length ||
features.some((entry, i) => entry !== [...features].sort()[i])
) {
fail(`${path}.disclosedFeatures`, 'must be unique and lexicographically sorted');
}
const checklist = record(declaration.checklist, `${path}.checklist`);
exact(checklist, CHECKLIST_KEYS, `${path}.checklist`);
for (const key of CHECKLIST_KEYS) {
if (checklist[key] !== true) fail(`${path}.checklist.${key}`, 'must be true');
}
return declaration;
}

function desktopDistribution(value, path, brandId, channel) {
if (value === null) return null;
const distribution = record(value, path);
exact(distribution, ['credentialSecretPrefix', 'r2Bucket', 'r2Prefix', 'updateUrl'], path);
const updateUrl = httpsUrl(distribution.updateUrl, `${path}.updateUrl`);
const credentialSecretPrefix = string(
distribution.credentialSecretPrefix,
`${path}.credentialSecretPrefix`,
RE_SECRET_PREFIX,
);
const r2Bucket = string(distribution.r2Bucket, `${path}.r2Bucket`, RE_BUCKET);
const r2Prefix = string(distribution.r2Prefix, `${path}.r2Prefix`, RE_R2_PREFIX);
const expectedSuffix = `/${r2Prefix.replace(RE_TRAILING_SLASH, '')}`;
if (!r2Prefix.split('/').includes(brandId) || !r2Prefix.split('/').includes(channel)) {
fail(`${path}.r2Prefix`, 'must include the brand id and channel as path segments');
}
if (!new URL(updateUrl).pathname.replace(RE_TRAILING_SLASH, '').endsWith(expectedSuffix)) {
fail(path, 'updateUrl path must end with r2Prefix');
}
return {
credentialSecretPrefix,
r2Bucket,
r2Prefix: r2Prefix.replace(RE_TRAILING_SLASH, ''),
updateUrl,
};
}

function mobileDistribution(value, path) {
if (value === null) return null;
const distribution = record(value, path);
exact(distribution, ['android', 'easProjectId', 'ios', 'updatesUrl'], path);
const easProjectId = string(distribution.easProjectId, `${path}.easProjectId`, RE_UUID);
const updatesUrl = httpsUrl(distribution.updatesUrl, `${path}.updatesUrl`);
if (updatesUrl !== `https://u.expo.dev/${easProjectId}`) {
fail(`${path}.updatesUrl`, 'must be the EAS update URL for easProjectId');
}
const ios = record(distribution.ios, `${path}.ios`);
exact(ios, ['appleTeamId', 'ascAppId'], `${path}.ios`);
string(ios.appleTeamId, `${path}.ios.appleTeamId`, RE_TEAM_ID);
string(ios.ascAppId, `${path}.ios.ascAppId`, RE_ASC_APP_ID);
const android = record(distribution.android, `${path}.android`);
exact(android, ['track'], `${path}.android`);
if (android.track !== 'internal') fail(`${path}.android.track`, 'must be internal');
return distribution;
}

function parseBrandBuildMatrix(value, options = {}) {
const matrix = structuredClone(record(value, 'matrix'));
exact(matrix, ['brandBuildMatrixVersion', 'brands'], 'matrix');
if (matrix.brandBuildMatrixVersion !== BUILD_MATRIX_VERSION) {
fail('matrix.brandBuildMatrixVersion', 'must be 1');
}
if (!Array.isArray(matrix.brands) || matrix.brands.length === 0) {
fail('matrix.brands', 'must be a non-empty array');
}
if (options.sign && !options.build) fail('options.sign', 'sign requires build=true');
if (options.upload && !options.sign) fail('options.upload', 'upload requires sign=true');
const seenBrands = new Set();
const destinations = [];
const credentialPrefixes = new Set();
const projects = new Set();
const appStoreApps = new Set();
const brands = matrix.brands.map((raw, index) => {
const path = `matrix.brands[${index}]`;
const brand = record(raw, path);
exact(brand, ['brandId', 'channel', 'compliance', 'distribution', 'releaseManifests'], path);
const brandId = string(brand.brandId, `${path}.brandId`, RE_BRAND_ID);
if (seenBrands.has(brandId)) fail(`${path}.brandId`, 'must be unique');
seenBrands.add(brandId);
if (brand.channel !== 'canary' && brand.channel !== 'stable') {
fail(`${path}.channel`, 'must be canary or stable');
}
const manifests = record(brand.releaseManifests, `${path}.releaseManifests`);
exact(manifests, PLATFORMS, `${path}.releaseManifests`);
const declarations = record(brand.compliance, `${path}.compliance`);
exact(declarations, PLATFORMS, `${path}.compliance`);
for (const platform of PLATFORMS) {
manifests[platform] = releaseManifest(
manifests[platform],
`${path}.releaseManifests.${platform}`,
platform,
brandId,
brand.channel,
);
declarations[platform] = compliance(declarations[platform], `${path}.compliance.${platform}`);
}
for (const field of [
'publisherGitSha',
'sourceGitSha',
'configRevisionId',
'revisionSha256',
'publicKeyringsSha256',
]) {
if (PLATFORMS.some((platform) => manifests[platform][field] !== manifests.desktop[field])) {
fail(`${path}.releaseManifests`, `all platforms must share ${field}`);
}
}
const distribution = record(brand.distribution, `${path}.distribution`);
exact(distribution, ['desktop', 'mobile'], `${path}.distribution`);
distribution.desktop = desktopDistribution(
distribution.desktop,
`${path}.distribution.desktop`,
brandId,
brand.channel,
);
distribution.mobile = mobileDistribution(distribution.mobile, `${path}.distribution.mobile`);
if (options.build && (distribution.desktop === null || distribution.mobile === null)) {
fail(
`${path}.distribution`,
'desktop and mobile delivery inputs are required when build=true',
);
}
if (distribution.desktop) {
const collision = destinations.some(
({ bucket, prefix }) =>
bucket === distribution.desktop.r2Bucket &&
(prefix === distribution.desktop.r2Prefix ||
prefix.startsWith(`${distribution.desktop.r2Prefix}/`) ||
distribution.desktop.r2Prefix.startsWith(`${prefix}/`)),
);
if (collision) {
fail(`${path}.distribution.desktop`, 'R2 prefixes in one bucket must not overlap');
}
}
if (
distribution.desktop &&
credentialPrefixes.has(distribution.desktop.credentialSecretPrefix)
) {
fail(`${path}.distribution.desktop.credentialSecretPrefix`, 'must be unique');
}
if (distribution.mobile && projects.has(distribution.mobile.easProjectId)) {
fail(`${path}.distribution.mobile.easProjectId`, 'must be unique');
}
if (distribution.mobile && appStoreApps.has(distribution.mobile.ios.ascAppId)) {
fail(`${path}.distribution.mobile.ios.ascAppId`, 'must be unique');
}
if (distribution.desktop) {
destinations.push({
bucket: distribution.desktop.r2Bucket,
prefix: distribution.desktop.r2Prefix,
});
credentialPrefixes.add(distribution.desktop.credentialSecretPrefix);
}
if (distribution.mobile) {
projects.add(distribution.mobile.easProjectId);
appStoreApps.add(distribution.mobile.ios.ascAppId);
}
return brand;
});
return { brandBuildMatrixVersion: BUILD_MATRIX_VERSION, brands };
}

function buildMatrixPlan(matrix, options = {}) {
const parsed = parseBrandBuildMatrix(matrix, options);
return {
brands: { include: parsed.brands },
targets: {
include: parsed.brands.flatMap((brand) =>
PLATFORMS.map((platform) => ({ brandId: brand.brandId, channel: brand.channel, platform })),
),
},
};
}

function strictBoolean(value, name) {
if (value === 'true') return true;
if (value === 'false') return false;
fail(name, 'must be true or false');
}

function runCli(argv = process.argv.slice(2), env = process.env) {
const { createHash } = require('node:crypto');
const { appendFileSync, readFileSync } = require('node:fs');
const { parseArgs } = require('node:util');
const { values } = parseArgs({
args: argv,
options: {
build: { type: 'string', default: 'false' },
'matrix-file': { type: 'string' },
sign: { type: 'string', default: 'false' },
upload: { type: 'string', default: 'false' },
},
strict: true,
});
if (!values['matrix-file']) fail('--matrix-file', 'is required');
const bytes = readFileSync(values['matrix-file']);
const text = bytes.toString('utf8');
let matrix;
try {
matrix = JSON.parse(text);
} catch {
fail('--matrix-file', 'must contain valid JSON');
}
const plan = buildMatrixPlan(matrix, {
build: strictBoolean(values.build, '--build'),
sign: strictBoolean(values.sign, '--sign'),
upload: strictBoolean(values.upload, '--upload'),
});
const outputs = [
`brands=${JSON.stringify(plan.brands)}`,
`delivery_descriptor_sha256=${createHash('sha256').update(bytes).digest('hex')}`,
`targets=${JSON.stringify(plan.targets)}`,
];
if (env.GITHUB_OUTPUT) appendFileSync(env.GITHUB_OUTPUT, `${outputs.join('\n')}\n`);
else console.log(outputs.join('\n'));
return plan;
}

if (require.main === module) runCli();

module.exports = {
BUILD_MATRIX_VERSION,
CHECKLIST_KEYS,
PLATFORMS,
buildMatrixPlan,
parseBrandBuildMatrix,
runCli,
};
Loading