Skip to content

Commit 5e65ade

Browse files
committed
init
0 parents  commit 5e65ade

File tree

13 files changed

+1714
-0
lines changed

13 files changed

+1714
-0
lines changed
+32
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'
+112
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();
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'
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();
+20
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'
+43
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/main.yml

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
name: Test
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: ['**']
8+
9+
jobs:
10+
build-simulator:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
15+
- name: Setup Node
16+
uses: actions/setup-node@v4
17+
with:
18+
cache: 'npm'
19+
20+
- name: Install dependencies
21+
run: npm install

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules

LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2025 Callstack Incubator
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# RNEF Android GitHub Action
2+
3+
This GitHub Action enables remote building of Android applications using RNEF (React Native Enterprise Framework). It supports both debug and release builds, with automatic artifact caching and code signing capabilities.
4+
5+
## Features
6+
7+
- Build Android apps in debug or release mode
8+
- Automatic artifact caching to speed up builds
9+
- Code signing support for release builds
10+
- Re-signing capability for PR builds
11+
- Native fingerprint-based caching
12+
- Configurable build parameters
13+
- Gradle wrapper validation
14+
15+
## Usage
16+
17+
```yaml
18+
name: Android Build
19+
on:
20+
push:
21+
branches: [main]
22+
pull_request:
23+
branches: ['**']
24+
25+
jobs:
26+
build:
27+
runs-on: ubuntu-latest
28+
steps:
29+
- uses: actions/checkout@v4
30+
31+
- name: Build Android
32+
uses: callstackincubator/android@v1
33+
with:
34+
variant: 'debug' # or else
35+
# For release builds, add these:
36+
# sign: true
37+
# keystore-base64: ${{ secrets.KEYSTORE_BASE64 }}
38+
# keystore-store-file: 'your-keystore.jks'
39+
# keystore-store-password: ${{ secrets.KEYSTORE_STORE_PASSWORD }}
40+
# keystore-key-alias: 'your-key-alias'
41+
# keystore-key-password: ${{ secrets.KEYSTORE_KEY_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+
| `validate-gradle-wrapper` | Whether to validate the Gradle wrapper | No | `true` |
51+
| `variant` | Build variant (debug/release) | No | `debug` |
52+
| `sign` | Whether to sign the build with keystore | No | - |
53+
| `re-sign` | Re-sign the APK with new JS bundle | No | `false` |
54+
| `keystore-base64` | Base64 encoded keystore file | No | - |
55+
| `keystore-store-file` | Keystore store file name | No | - |
56+
| `keystore-store-password` | Keystore store password | No | - |
57+
| `keystore-key-alias` | Keystore key alias | No | - |
58+
| `keystore-key-password` | Keystore key password | No | - |
59+
| `rnef-build-extra-params` | Extra parameters for rnef build:android | 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+
- Ubuntu runner
72+
- RNEF CLI installed in your project
73+
- For release builds:
74+
- Valid Android keystore file
75+
- Proper code signing setup
76+
77+
## License
78+
79+
MIT

0 commit comments

Comments
 (0)