Skip to content

Commit f5155b7

Browse files
committed
setup
1 parent 4594e72 commit f5155b7

File tree

12 files changed

+1749
-0
lines changed

12 files changed

+1749
-0
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Note: this action will be published separate on the GitHub.
2+
# It's here only temporary for the development purposes.
3+
4+
name: 'Find Artifact'
5+
description: 'Find the latest artifact'
6+
inputs:
7+
name:
8+
description: 'Artifact name'
9+
required: false
10+
re-sign:
11+
description: Re-sign the app bundle with new JS bundle
12+
required: false
13+
github-token:
14+
description: A GitHub Personal Access Token with write access to the project
15+
required: false
16+
default: ${{ github.token }}
17+
repository:
18+
description:
19+
'The repository owner and the repository name joined together by "/".
20+
If github-token is specified, this is the repository that artifacts will be downloaded from.'
21+
required: false
22+
default: ${{ github.repository }}
23+
outputs:
24+
artifact-id:
25+
description: 'The ID of the artifact'
26+
artifact-url:
27+
description: 'The URL of the artifact'
28+
artifact-ids:
29+
description: 'All IDs of the artifacts matching the name'
30+
runs:
31+
using: 'node20'
32+
main: 'index.js'
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
const core = require('@actions/core');
2+
const github = require('@actions/github');
3+
4+
const perPage = 100; // Maximum allowed by GitHub API
5+
6+
async function fetchArtifacts(octokit, repository, name) {
7+
const result = [];
8+
let page = 1;
9+
10+
while (true) {
11+
const response = await octokit.rest.actions.listArtifactsForRepo({
12+
owner: repository.split('/')[0],
13+
repo: repository.split('/')[1],
14+
name,
15+
per_page: perPage,
16+
page,
17+
});
18+
19+
const artifacts = response.data.artifacts;
20+
result.push(...artifacts);
21+
22+
if (artifacts.length < perPage) {
23+
break;
24+
}
25+
26+
page++;
27+
}
28+
29+
result.sort((a, b) => new Date(b.expires_at) - new Date(a.expires_at));
30+
return result;
31+
}
32+
33+
function getPrNumber() {
34+
if (github.context.eventName === 'pull_request') {
35+
return github.context.payload.pull_request.number;
36+
}
37+
return undefined;
38+
}
39+
40+
async function run() {
41+
try {
42+
const token = core.getInput('github-token');
43+
const repository = core.getInput('repository');
44+
const name = core.getInput('name');
45+
const reSign = core.getInput('re-sign');
46+
const prNumber = getPrNumber();
47+
48+
const octokit = github.getOctokit(token);
49+
const artifactsByName = await fetchArtifacts(octokit, repository, name);
50+
const artifactsByPrNumber =
51+
prNumber && reSign
52+
? await fetchArtifacts(octokit, repository, `${name}-${prNumber}`)
53+
: [];
54+
const artifacts = [...artifactsByPrNumber, ...artifactsByName];
55+
56+
if (artifacts.length === 0) {
57+
return;
58+
}
59+
60+
console.log(`Found ${artifacts.length} related artifacts:`);
61+
for (const artifact of artifacts) {
62+
console.log(
63+
`- ID: ${artifact.id}, Name: ${artifact.name}, Size: ${formatSize(
64+
artifact.size_in_bytes,
65+
)}, Expires at: ${artifact.expires_at}`,
66+
);
67+
}
68+
69+
const firstArtifact = artifacts.find(artifact => !artifact.expired);
70+
console.log(`First artifact: ${JSON.stringify(firstArtifact, null, 2)}`);
71+
72+
const url = formatDownloadUrl(
73+
repository,
74+
firstArtifact.workflow_run.id,
75+
firstArtifact.id,
76+
);
77+
console.log('Stable download URL:', url);
78+
79+
let artifactName = name;
80+
// There are artifacts from PR but the base artifact is gone, recreate with the original name
81+
if (artifactsByName.length === 0) {
82+
artifactName = name;
83+
// First time an artifact is re-signed, it's not yet in artifact storage, setting the name explicitly.
84+
} else if (prNumber && reSign) {
85+
artifactName = `${name}-${prNumber}`;
86+
}
87+
core.setOutput('artifact-name', artifactName);
88+
core.setOutput('artifact-id', firstArtifact.id);
89+
core.setOutput('artifact-url', url);
90+
core.setOutput(
91+
'artifact-ids',
92+
artifactsByPrNumber.map(artifact => artifact.id).join(' '),
93+
);
94+
} catch (error) {
95+
core.setFailed(`Action failed with error: ${error.message}`);
96+
}
97+
}
98+
99+
// The artifact URL returned by the GitHub API expires in 1 minute, we need to generate a permanent one.
100+
function formatDownloadUrl(repository, workflowRunId, artifactId) {
101+
return `https://github.com/${repository}/actions/runs/${workflowRunId}/artifacts/${artifactId}`;
102+
}
103+
104+
function formatSize(size) {
105+
if (size > 0.75 * 1024 * 1024) {
106+
return `${(size / 1024 / 1024).toFixed(2)} MB`;
107+
}
108+
109+
return `${(size / 1024).toFixed(2)} KB`;
110+
}
111+
112+
run();
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Note: this action will be published separate on the GitHub.
2+
# It's here only temporary for the development purposes.
3+
4+
name: 'Fingerprint'
5+
description: 'Fingerprint the current native-related files'
6+
inputs:
7+
platform:
8+
description: 'The platform to fingerprint: android or ios'
9+
required: true
10+
working-directory:
11+
description: 'The working directory to fingerprint, where the rnef.config.mjs is located'
12+
required: true
13+
default: '.'
14+
outputs:
15+
hash:
16+
description: 'The fingerprint hash'
17+
runs:
18+
using: 'node20'
19+
main: 'index.mjs'
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import path from 'node:path';
2+
import core from '@actions/core';
3+
import {getConfig} from '@rnef/config';
4+
import {nativeFingerprint} from '@rnef/tools';
5+
6+
const ALLOWED_PLATFORMS = ['android', 'ios'];
7+
8+
async function run() {
9+
const platform = core.getInput('platform');
10+
const workingDirectory = core.getInput('working-directory');
11+
if (!ALLOWED_PLATFORMS.includes(platform)) {
12+
throw new Error(`Invalid platform: ${platform}`);
13+
}
14+
const dir = path.isAbsolute(workingDirectory)
15+
? workingDirectory
16+
: path.join(process.cwd(), workingDirectory);
17+
const config = await getConfig(dir);
18+
const fingerprintOptions = config.getFingerprintOptions();
19+
20+
const fingerprint = await nativeFingerprint(dir, {
21+
platform,
22+
...fingerprintOptions,
23+
});
24+
25+
console.log('Hash:', fingerprint.hash);
26+
console.log('Sources:', fingerprint.sources);
27+
28+
core.setOutput('hash', fingerprint.hash);
29+
}
30+
31+
await run();
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Note: this action will be published separate on the GitHub.
2+
# It's here only temporary for the development purposes.
3+
4+
name: 'Post Build'
5+
description: 'Post Build info'
6+
7+
inputs:
8+
artifact-url:
9+
description: 'The URL of the artifact to post'
10+
required: true
11+
title:
12+
description: 'The title of the GitHub comment'
13+
required: true
14+
github-token:
15+
description: A GitHub Personal Access Token with write access to the project
16+
required: false
17+
default: ${{ github.token }}
18+
runs:
19+
using: 'node20'
20+
main: 'index.js'
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
const core = require('@actions/core');
2+
const github = require('@actions/github');
3+
4+
async function run() {
5+
const token = core.getInput('github-token');
6+
const titleInput = core.getInput('title');
7+
const artifactUrl = core.getInput('artifact-url');
8+
9+
const title = `## ${titleInput}`;
10+
const body = `🔗 [Download link](${artifactUrl}).\n\n
11+
Note: if the download link expires, please re-run the workflow to generate a new build.\n\n
12+
*Generated at ${new Date().toISOString()} UTC*\n`;
13+
14+
const octokit = github.getOctokit(token);
15+
const {data: comments} = await octokit.rest.issues.listComments({
16+
...github.context.repo,
17+
issue_number: github.context.issue.number,
18+
});
19+
20+
const botComment = comments.find(
21+
comment =>
22+
comment.user.login === 'github-actions[bot]' &&
23+
comment.body.includes(title),
24+
);
25+
26+
if (botComment) {
27+
await octokit.rest.issues.updateComment({
28+
...github.context.repo,
29+
comment_id: botComment.id,
30+
body: `${title}\n\n${body}`,
31+
});
32+
console.log('Updated comment');
33+
} else {
34+
await octokit.rest.issues.createComment({
35+
...github.context.repo,
36+
issue_number: github.context.issue.number,
37+
body: `${title}\n\n${body}`,
38+
});
39+
console.log('Created comment');
40+
}
41+
}
42+
43+
run();

.github/workflows/example.yml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: iOS Build Example
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
branches: [ main ]
8+
9+
jobs:
10+
build-simulator:
11+
runs-on: macos-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
15+
- name: Build iOS for Simulator
16+
uses: ./
17+
with:
18+
destination: 'simulator'
19+
scheme: 'YourScheme'
20+
configuration: 'Debug'
21+
working-directory: './packages/mobile' # Adjust this path based on your project structure
22+
23+
build-device:
24+
runs-on: macos-latest
25+
steps:
26+
- uses: actions/checkout@v4
27+
28+
- name: Build iOS for Device
29+
uses: ./
30+
with:
31+
destination: 'device'
32+
scheme: 'YourScheme'
33+
configuration: 'Release'
34+
certificate-base64: ${{ secrets.CERTIFICATE_BASE64 }}
35+
certificate-password: ${{ secrets.CERTIFICATE_PASSWORD }}
36+
provisioning-profile-base64: ${{ secrets.PROVISIONING_PROFILE_BASE64 }}
37+
provisioning-profile-name: 'YourProfileName'
38+
keychain-password: ${{ secrets.KEYCHAIN_PASSWORD }}
39+
working-directory: './packages/mobile' # Adjust this path based on your project structure
40+
re-sign: true # Enable re-signing for PR builds

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules

README.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# RNEF iOS GitHub Action
2+
3+
This GitHub Action enables remote building of iOS applications using RNEF (React Native Enterprise Framework). It supports both simulator and device builds, with automatic artifact caching and code signing capabilities.
4+
5+
## Features
6+
7+
- Build iOS apps for simulator or device
8+
- Automatic artifact caching to speed up builds
9+
- Code signing support for device builds
10+
- Re-signing capability for PR builds
11+
- Native fingerprint-based caching
12+
- Configurable build parameters
13+
14+
## Usage
15+
16+
```yaml
17+
name: iOS Build
18+
on:
19+
push:
20+
branches: [main]
21+
pull_request:
22+
branches: ['**']
23+
24+
jobs:
25+
build:
26+
runs-on: macos-latest
27+
steps:
28+
- uses: actions/checkout@v4
29+
30+
- name: Build iOS
31+
uses: callstackincubator/ios@v1
32+
with:
33+
destination: 'simulator' # or 'device'
34+
scheme: 'YourScheme'
35+
configuration: 'Debug'
36+
# For device builds, add these:
37+
# certificate-base64: ${{ secrets.CERTIFICATE_BASE64 }}
38+
# certificate-password: ${{ secrets.CERTIFICATE_PASSWORD }}
39+
# provisioning-profile-base64: ${{ secrets.PROVISIONING_PROFILE_BASE64 }}
40+
# provisioning-profile-name: 'YourProfileName'
41+
# keychain-password: ${{ secrets.KEYCHAIN_PASSWORD }}
42+
```
43+
44+
## Inputs
45+
46+
| Input | Description | Required | Default |
47+
| ----------------------------- | ------------------------------------------ | -------- | --------------------- |
48+
| `github-token` | GitHub Token | No | `${{ github.token }}` |
49+
| `working-directory` | Working directory for the build command | No | `.` |
50+
| `destination` | Build destination: "simulator" or "device" | Yes | `simulator` |
51+
| `scheme` | Xcode scheme | Yes | - |
52+
| `configuration` | Xcode configuration | Yes | - |
53+
| `re-sign` | Re-sign the app bundle with new JS bundle | No | `false` |
54+
| `certificate-base64` | Base64 encoded P12 file for device builds | No | - |
55+
| `certificate-password` | Password for the P12 file | No | - |
56+
| `provisioning-profile-base64` | Base64 encoded provisioning profile | No | - |
57+
| `provisioning-profile-name` | Name of the provisioning profile | No | - |
58+
| `keychain-password` | Password for temporary keychain | No | - |
59+
| `rnef-build-extra-params` | Extra parameters for rnef build:ios | No | - |
60+
| `comment-bot` | Whether to comment PR with build link | No | `true` |
61+
62+
## Outputs
63+
64+
| Output | Description |
65+
| -------------- | ------------------------- |
66+
| `artifact-url` | URL of the build artifact |
67+
| `artifact-id` | ID of the build artifact |
68+
69+
## Prerequisites
70+
71+
- macOS runner
72+
- RNEF CLI installed in your project
73+
- For device builds:
74+
- Valid Apple Developer certificate
75+
- Valid provisioning profile
76+
- Proper code signing setup
77+
78+
## License
79+
80+
MIT

0 commit comments

Comments
 (0)