Skip to content

Commit 3997067

Browse files
authored
Merge pull request #165 from appodeal/feature/dynamic-readme-deps
feat: auto-generate README dependency lists from Wizard API
2 parents 2dd2524 + a70cd45 commit 3997067

4 files changed

Lines changed: 219 additions & 19 deletions

File tree

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
name: Update README dependencies
2+
3+
# Keeps the Appodeal dependency lists in README.md in sync with the live
4+
# Dependencies Wizard API (URL from the APPODEAL_API_URL repository variable).
5+
# The SDK version comes from package.json, so a version bump (push) refreshes the
6+
# lists; the daily schedule catches adapter-version changes published between releases.
7+
8+
on:
9+
workflow_dispatch: {}
10+
push:
11+
branches: [master]
12+
paths:
13+
- 'package.json'
14+
- 'scripts/update-readme-deps.mjs'
15+
schedule:
16+
# Daily at 06:00 UTC.
17+
- cron: '0 6 * * *'
18+
19+
permissions:
20+
contents: write
21+
22+
concurrency:
23+
group: update-readme-deps
24+
cancel-in-progress: true
25+
26+
jobs:
27+
update:
28+
runs-on: ubuntu-latest
29+
steps:
30+
- uses: actions/checkout@v4
31+
32+
- uses: actions/setup-node@v4
33+
with:
34+
node-version: 20
35+
36+
- name: Regenerate README dependency lists
37+
# URL comes only from the APPODEAL_API_URL repository variable
38+
# (Settings → Secrets and variables → Actions → Variables). No hardcoded fallback —
39+
# if the variable is unset the script fails loudly.
40+
env:
41+
APPODEAL_API_URL: ${{ vars.APPODEAL_API_URL }}
42+
run: node scripts/update-readme-deps.mjs
43+
44+
- name: Commit changes if any
45+
run: |
46+
if git diff --quiet -- README.md; then
47+
echo "README dependencies already up to date."
48+
exit 0
49+
fi
50+
git config user.name "github-actions[bot]"
51+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
52+
git add README.md
53+
git commit -m "chore: sync README dependency lists from Wizard API"
54+
git push

README.md

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -90,31 +90,30 @@ yarn add react-native-appodeal
9090
1. Go to `ios/` folder and open *Podfile*
9191
2. Add Appodeal adapters. Add pods into `./ios/Podfile`:
9292

93+
<!-- appodeal-deps:ios:start -->
9394
```ruby
94-
source 'https://cdn.cocoapods.org'
95+
platform :ios, '15.0'
96+
9597
source 'https://github.com/appodeal/CocoaPods.git'
9698
source 'https://github.com/bidon-io/CocoaPods-Specs.git'
97-
98-
platform :ios, '13.0'
99+
source 'https://cdn.cocoapods.org'
99100

100101
use_frameworks!
101102

102103
def appodeal
103-
pod 'Appodeal', '4.1.0'
104-
# Add adapter pods here.
105-
# Use the Appodeal Dependencies Wizard to generate an up-to-date list:
106-
# https://docs.appodeal.com/ios/advanced/configure-mediated-networks
104+
pod 'Appodeal', '4.1.0'
107105
end
108106

109-
target 'YourAppName' do
110-
use_frameworks!
111-
use_modular_headers!
112-
appodeal
107+
target 'Sample' do
108+
project 'Sample/Sample.xcodeproj'
109+
appodeal
110+
use_modular_headers!
113111

114-
config = use_native_modules!
115-
use_react_native!(:path => config[:reactNativePath])
112+
config = use_native_modules!
113+
use_react_native!(:path => config[:reactNativePath])
116114
end
117115
```
116+
<!-- appodeal-deps:ios:end -->
118117

119118
> [!TIP]
120119
> Use the [Appodeal Dependencies Wizard](https://docs.appodeal.com/ios/advanced/configure-mediated-networks) to configure mediated networks and generate an always up-to-date dependency list for your Podfile.
@@ -195,16 +194,17 @@ To improve ad performance the following entries should be added:
195194

196195
Add dependencies into `android/app/build.gradle`
197196

198-
``` groovy
197+
<!-- appodeal-deps:android:start -->
198+
``` kotlin
199+
repositories {
200+
maven { url = uri("https://artifactory.appodeal.com/appodeal") }
201+
}
202+
/* build.gradle.kts */
199203
dependencies {
200-
// ... other project dependencies
201-
// Appodeal SDK 4.1.0
202204
implementation("com.appodeal.ads.sdk:core:4.1.0")
203-
// Add adapter dependencies here.
204-
// Use the Appodeal Dependencies Wizard to generate an up-to-date list:
205-
// https://docs.appodeal.com/android/advanced/configure-mediated-networks
206205
}
207206
```
207+
<!-- appodeal-deps:android:end -->
208208

209209
Add repository into `android/build.gradle`
210210

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"typecheck": "tsc",
3838
"lint": "eslint \"**/*.{js,ts,tsx}\"",
3939
"clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib",
40+
"update-readme-deps": "node scripts/update-readme-deps.mjs",
4041
"prepare": "bob build",
4142
"release": "release-it --only-version"
4243
},

scripts/update-readme-deps.mjs

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
#!/usr/bin/env node
2+
// Updates the README.md dependency blocks from the Appodeal Wizard API.
3+
// API output is used verbatim; only the iOS Podfile target gets RN linking injected.
4+
5+
import { readFile, writeFile } from 'node:fs/promises';
6+
import { fileURLToPath } from 'node:url';
7+
import { dirname, join } from 'node:path';
8+
9+
// API base URL — required, supplied via the APPODEAL_API_URL env var (set by the
10+
// GitHub Action / your shell). No fallback: fail loudly rather than hit a guessed host.
11+
const API = (() => {
12+
const raw = process.env.APPODEAL_API_URL;
13+
if (!raw) {
14+
throw new Error('APPODEAL_API_URL env var is required');
15+
}
16+
return raw.replace(/\/+$/, '');
17+
})();
18+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
19+
const README = join(ROOT, 'README.md');
20+
21+
// Category codes returned by the /sdks endpoint.
22+
const CATEGORY_NETWORK = 2;
23+
const CATEGORY_SERVICE = 3;
24+
25+
/** Read the single source-of-truth version from package.json. */
26+
async function getVersion() {
27+
const pkg = JSON.parse(await readFile(join(ROOT, 'package.json'), 'utf8'));
28+
if (!pkg.version) throw new Error('package.json has no "version" field');
29+
return pkg.version;
30+
}
31+
32+
async function apiFetch(path, options = {}) {
33+
const res = await fetch(`${API}${path}`, {
34+
headers: { 'content-type': 'application/json', accept: '*/*' },
35+
signal: AbortSignal.timeout(30_000),
36+
...options,
37+
});
38+
if (!res.ok) {
39+
throw new Error(`API ${path} -> HTTP ${res.status} ${res.statusText}`);
40+
}
41+
return res;
42+
}
43+
44+
/** Collect every version id from a list of {versions:[{id}]} entries. */
45+
function collectVersionIds(entries) {
46+
return (entries ?? []).flatMap((e) => (e.versions ?? []).map((v) => v.id));
47+
}
48+
49+
/** Run the 3-step recommended pipeline and return the rendered dependency text. */
50+
async function fetchDependencyBlock(platform, version, lang) {
51+
// Step 1 — recommended mediations.
52+
const mediationsRes = await apiFetch(
53+
`/v4/${platform}/${version}/mediations?recommended=true`
54+
);
55+
const mediations = collectVersionIds((await mediationsRes.json()).mediations);
56+
57+
// Step 2 — recommended sdks for those mediations, split into networks / services.
58+
const sdksRes = await apiFetch(`/v4/${platform}/${version}/sdks?recommended=true`, {
59+
method: 'POST',
60+
body: JSON.stringify({ mediations, networks: [], services: [] }),
61+
});
62+
const sdks = (await sdksRes.json()).sdks ?? [];
63+
const networks = collectVersionIds(sdks.filter((s) => s.category === CATEGORY_NETWORK));
64+
const services = collectVersionIds(sdks.filter((s) => s.category === CATEGORY_SERVICE));
65+
66+
// Step 3 — render. iOS has no language suffix (always Ruby).
67+
const path =
68+
platform === 'ios'
69+
? `/v4/${platform}/${version}/dependencies`
70+
: `/v4/${platform}/${version}/dependencies/${lang}`;
71+
const depsRes = await apiFetch(path, {
72+
method: 'POST',
73+
body: JSON.stringify({ mediations, networks, services }),
74+
});
75+
return (await depsRes.text()).replace(/\t/g, ' ');
76+
}
77+
78+
/** Wrap rendered code in a fenced Markdown block. */
79+
function fenced(lang, body) {
80+
return `${lang}\n${body.trimEnd()}\n\`\`\``;
81+
}
82+
83+
/**
84+
* Inject the React Native linking lines into the iOS Podfile target. The Wizard renders
85+
* a plain native target (`target 'Sample' do ... end`); RN apps additionally need the
86+
* autolinking calls, otherwise the copied Podfile won't build. Everything else from the
87+
* API response is left untouched.
88+
*/
89+
function addReactNativeLinking(podfile) {
90+
const linking = [
91+
'',
92+
' use_modular_headers!',
93+
'',
94+
' config = use_native_modules!',
95+
' use_react_native!(:path => config[:reactNativePath])',
96+
].join('\n');
97+
// The target block is the last one in the response; insert before its closing `end`.
98+
const trimmed = podfile.trimEnd();
99+
const patched = trimmed.replace(/\n[ \t]*end$/, `${linking}\nend`);
100+
if (patched === trimmed) {
101+
throw new Error('Could not locate the iOS Podfile target `end` to inject RN linking');
102+
}
103+
return patched;
104+
}
105+
106+
/**
107+
* Replace everything between the HTML-comment markers for `name` with `block`.
108+
* Markers live OUTSIDE the fenced code block so they stay invisible in rendered Markdown:
109+
* <!-- appodeal-deps:NAME:start ... --> ...block... <!-- appodeal-deps:NAME:end -->
110+
*/
111+
function replaceBetweenMarkers(readme, name, block) {
112+
const startRe = new RegExp(`<!--\\s*appodeal-deps:${name}:start\\b[^>]*-->`);
113+
const endRe = new RegExp(`<!--\\s*appodeal-deps:${name}:end\\s*-->`);
114+
const startMatch = readme.match(startRe);
115+
if (!startMatch) throw new Error(`Marker appodeal-deps:${name}:start not found in README.md`);
116+
const startIdx = startMatch.index + startMatch[0].length;
117+
118+
const endMatch = readme.slice(startIdx).match(endRe);
119+
if (!endMatch) throw new Error(`Marker appodeal-deps:${name}:end not found after start in README.md`);
120+
const endIdx = startIdx + endMatch.index;
121+
122+
return `${readme.slice(0, startIdx)}\n${block}\n${readme.slice(endIdx)}`;
123+
}
124+
125+
async function main() {
126+
const version = await getVersion();
127+
console.log(`Updating README dependency lists for Appodeal SDK ${version}`);
128+
129+
// android uses kts (`kt`) verbatim; ios ignores the language, returns Ruby, and gets
130+
// the React Native linking injected into its target.
131+
const android = await fetchDependencyBlock('android', version, 'kt');
132+
const ios = addReactNativeLinking(await fetchDependencyBlock('ios', version));
133+
134+
let readme = await readFile(README, 'utf8');
135+
readme = replaceBetweenMarkers(readme, 'android', fenced('``` kotlin', android));
136+
readme = replaceBetweenMarkers(readme, 'ios', fenced('```ruby', ios));
137+
138+
await writeFile(README, readme, 'utf8');
139+
console.log('Done: README dependency blocks updated.');
140+
}
141+
142+
main().catch((err) => {
143+
console.error(`\n✖ ${err.message}`);
144+
process.exit(1);
145+
});

0 commit comments

Comments
 (0)