Skip to content
Merged
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
61 changes: 61 additions & 0 deletions .github/workflows/check-cdn-types.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
name: Check CDN Types

on:
schedule:
- cron: '0 10 * * *'
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}
cancel-in-progress: true

jobs:
check:
name: Check for CDN type updates
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write

steps:
- name: Checkout repo
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0

- name: Setup Node.js
uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
with:
node-version-file: .nvmrc

- name: Check for CDN updates
id: check
run: node packages/app-bridge-types/scripts/check-cdn-updates.mjs

- name: Create PR
if: steps.check.outputs.has_changes == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_BODY_FILE: ${{ steps.check.outputs.pr_body_file }}
run: |
BRANCH="automated/update-app-bridge-types"

git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

git checkout -B "$BRANCH"
git add .changeset/automated-cdn-types-update.md
git commit -m "chore: update app-bridge-types CDN types"
git push --force origin "$BRANCH"

EXISTING_PR=$(gh pr list --head "$BRANCH" --state open --json number --jq '.[0].number')

if [ -n "$EXISTING_PR" ]; then
echo "Updating existing PR #$EXISTING_PR"
gh pr edit "$EXISTING_PR" --body-file "$PR_BODY_FILE"
else
echo "Creating new PR"
gh pr create \
--title "Update app-bridge-types CDN types" \
--body-file "$PR_BODY_FILE" \
--base main \
--head "$BRANCH"
fi
186 changes: 186 additions & 0 deletions packages/app-bridge-types/scripts/check-cdn-updates.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import {writeFileSync, readFileSync, mkdirSync, mkdtempSync} from 'node:fs';
import {execSync} from 'node:child_process';
import {fileURLToPath} from 'node:url';
import {resolve, dirname, join} from 'node:path';
import {tmpdir} from 'node:os';

const CDN_URL = 'https://cdn.shopify.com/shopifycloud/app-bridge.d.ts';
const NPM_PACKAGE = '@shopify/app-bridge-types';

const __dirname = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(__dirname, '..', '..', '..');

/**
* Set a GitHub Actions output variable. No-op outside of Actions.
*/
function setOutput(name, value) {
const outputFile = process.env.GITHUB_OUTPUT;
if (outputFile) {
writeFileSync(outputFile, `${name}=${value}\n`, {flag: 'a'});
}
}

/**
* Normalize whitespace so trivial formatting differences don't trigger a diff.
*/
function normalize(text) {
return text.replace(/\r\n/g, '\n').trimEnd() + '\n';
}

/**
* Fetch text from a URL. Returns null if the request fails.
*/
async function fetchText(url) {
try {
const response = await fetch(url);
if (!response.ok) {
console.log(`${url} returned ${response.status}`);
return null;
}
return await response.text();
} catch (error) {
console.log(`Failed to fetch ${url}: ${error.message}`);
return null;
}
}

/**
* Get the published index.d.ts from npm via `npm pack`.
* Returns the file contents as a string, or null if the package isn't published.
*/
function getPublishedTypes(workDir) {
try {
execSync(`npm pack ${NPM_PACKAGE} --pack-destination "${workDir}"`, {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
});
} catch (error) {
console.log(
`npm pack failed (package may not be published yet): ${error.message}`,
);
return null;
}

try {
execSync(
`tar xzf "${workDir}"/*.tgz -C "${workDir}" package/dist/index.d.ts`,
{encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe']},
);
return readFileSync(
join(workDir, 'package', 'dist', 'index.d.ts'),
'utf-8',
);
} catch (error) {
console.log(`Failed to extract types from tarball: ${error.message}`);
return null;
}
}

// --- Main ---

const cdnText = await fetchText(CDN_URL);

if (cdnText == null) {
console.log('CDN is unavailable. Exiting without changes.');
setOutput('has_changes', 'false');
process.exit(0);
}

// If npm package isn't published yet, treat published types as empty
const npmWorkDir = mkdtempSync(join(tmpdir(), 'npm-pack-'));
const npmText = getPublishedTypes(npmWorkDir) ?? '';

const normalizedCdn = normalize(cdnText);
const normalizedNpm = normalize(npmText);

if (normalizedCdn === normalizedNpm) {
console.log('CDN types match published npm types. No changes needed.');
setOutput('has_changes', 'false');
process.exit(0);
}

console.log('CDN types differ from published npm types. Generating changeset.');

// Write both versions to temp files for diffing
const tempDir = mkdtempSync(join(tmpdir(), 'cdn-types-'));
const npmFile = join(tempDir, 'npm.d.ts');
const cdnFile = join(tempDir, 'cdn.d.ts');
writeFileSync(npmFile, normalizedNpm);
writeFileSync(cdnFile, normalizedCdn);

// Generate unified diff
let diff;
try {
// diff exits with 1 when files differ, which is expected
diff = execSync(`diff -u "${npmFile}" "${cdnFile}"`, {
encoding: 'utf-8',
});
} catch (error) {
// diff returns exit code 1 when files differ — that's the expected path
diff = error.stdout ?? '';
}

// Replace temp file paths with readable labels in the diff header
diff = diff
.replace(
new RegExp(npmFile.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'),
'npm (published)',
)
.replace(
new RegExp(cdnFile.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'),
'cdn (latest)',
);

// Count diff stats
const additions = (diff.match(/^\+[^+]/gm) || []).length;
const deletions = (diff.match(/^-[^-]/gm) || []).length;

// Write changeset
const changesetDir = resolve(repoRoot, '.changeset');
mkdirSync(changesetDir, {recursive: true});

const changesetFile = resolve(changesetDir, 'automated-cdn-types-update.md');
const changesetContent = `---
'@shopify/app-bridge-types': patch
---

Automated update of CDN types (+${additions} -${deletions} lines)

See the [App Bridge changelog](https://shopify.dev/changelog?filter=api&api_type=app-bridge) for details.
`;
writeFileSync(changesetFile, changesetContent);
console.log(`Wrote changeset to ${changesetFile}`);

// Write PR body to temp file (truncate large diffs)
const MAX_DIFF_LENGTH = 60_000;
const truncatedDiff =
diff.length > MAX_DIFF_LENGTH
? diff.slice(0, MAX_DIFF_LENGTH) + '\n\n... (diff truncated)'
: diff;

const prBody = `## Automated CDN Types Update

The types at \`${CDN_URL}\` have changed compared to the published \`@shopify/app-bridge-types\` package.

**Stats:** +${additions} -${deletions} lines

<details>
<summary>Full diff</summary>

\`\`\`diff
${truncatedDiff}
\`\`\`

</details>

Merging this PR will trigger a patch release of \`@shopify/app-bridge-types\`.
`;

const prBodyFile = join(tempDir, 'pr-body.md');
writeFileSync(prBodyFile, prBody);

setOutput('has_changes', 'true');
setOutput('pr_body_file', prBodyFile);

console.log(`PR body written to ${prBodyFile}`);
console.log('Done.');
Loading