react-native-cache-build-gitlab 1.0.5 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,10 +4,13 @@ GitLab Generic Package provider for [RockJS](https://rockjs.dev) with single pac
4
4
 
5
5
  ## Features
6
6
 
7
- **Single Package Storage**: All native builds (iOS & Android) stored in one package
8
- ✅ **Fingerprint-based Lookup**: Fast artifact retrieval by filename matching
9
- **CI/CD Ready**: Works seamlessly with GitLab CI
10
- ✅ **Cost Effective**: Reduces package registry clutter
7
+ 📦 **Single Package Storage**: Stores all native builds (iOS & Android) in a single GitLab Generic Package.
8
+
9
+ 🧾 **Fingerprint-based Lookup**: Fast artifact retrieval by filename matching.
10
+
11
+ 🤖️ **CI/CD Ready**: Works seamlessly with GitLab CI.
12
+
13
+ 🪙 **Cost Effective**: Reduces package registry clutter by grouping artifacts.
11
14
 
12
15
  ## Installation
13
16
 
@@ -17,56 +20,100 @@ npm install react-native-cache-build-gitlab
17
20
  yarn add react-native-cache-build-gitlab
18
21
  ```
19
22
 
23
+ ## ⚠️ Prerequisites & Configuration
24
+
25
+ **Important:** You must configure your project according to the [RockJS documentation](https://www.rockjs.dev/docs/cli/migrating-from-community-cli).
26
+
27
+ Before using this provider, you need to set up authentication. This provider uses `CI_JOB_TOKEN` as the default environment variable for authentication.
28
+
29
+ ### **1. Create a Personal Access Token**
30
+
31
+ - Go to **GitLab** → **Edit profile** → **Personal access tokens**.
32
+ - Click **Add new token**.
33
+ - Give the token a name and expiration date (recommended).
34
+ - Select scopes you need (commonly: `read_api`, `read_repository`, `write_repository`).
35
+ - Create the token and copy it now — GitLab shows it only once.
36
+
37
+ ![img.png](assets/example_PAT.png)
38
+
39
+ ### **2. Expose the token as an environment variable**
40
+
41
+ The provider expects the token in the `CI_JOB_TOKEN` environment variable
42
+
43
+ #### Temporary (Current Shell)
44
+
45
+ ```bash
46
+ export CI_JOB_TOKEN=glt-abc123...
47
+ ```
48
+
49
+ #### Permanent (macOS/Linux - Zsh):
50
+
51
+ ```bash
52
+ echo 'export CI_JOB_TOKEN=glt-abc123...' >> ~/.zshrc
53
+ source ~/.zshrc
54
+ ```
55
+
56
+ **Note:** In GitLab CI pipelines, `CI_JOB_TOKEN` is automatically injected, so you don't need to configure it manually there.
57
+
20
58
  ## Usage
21
59
 
22
60
  In your `rock.config.mjs`:
23
61
 
24
62
  ```ts
25
- import {platformIOS} from "@rock-js/platform-ios";
26
- import {platformAndroid} from "@rock-js/platform-android";
27
- import {providerGitLab} from "react-native-cache-build-gitlab";
28
- import {pluginMetro} from "@rock-js/plugin-metro";
63
+ import { platformIOS } from "@rock-js/platform-ios";
64
+ import { platformAndroid } from "@rock-js/platform-android";
65
+ import { providerGitLab } from "react-native-cache-build-gitlab";
66
+ import { pluginMetro } from "@rock-js/plugin-metro";
29
67
 
30
68
  export default {
31
- bundler: pluginMetro(),
32
- platforms: {
33
- ios: platformIOS(),
34
- android: platformAndroid(),
35
- },
36
- remoteCacheProvider: providerGitLab({
37
- packageName: "mobile-artifacts",
38
- baseUrl: "https://gitlab.example.com",
39
- projectId: 1234,
40
- token: process.env.CI_JOB_TOKEN,
41
- tokenHeader: process.env.CI ? "JOB-TOKEN" : "PRIVATE-TOKEN",
42
- }),
43
- fingerprint: {
44
- ignorePaths: [
45
- "ios/Podfile.lock",
46
- "ios/**/xcuserdata",
47
- "ios/**/project.pbxproj",
48
- // Add more paths to ignore as needed
49
- ],
50
- },
69
+ bundler: pluginMetro(),
70
+ platforms: {
71
+ ios: platformIOS(),
72
+ android: platformAndroid(),
73
+ },
74
+ remoteCacheProvider: providerGitLab({
75
+ packageName: "mobile-artifacts",
76
+ registryServer: "https://your-gitlab-instance.com",
77
+ projectId: 1234,
78
+ /*
79
+ * token: process.env.CI_JOB_TOKEN (default)
80
+ * tokenHeader: process.env.CI ? "JOB-TOKEN" : "PRIVATE-TOKEN" (default)
81
+ * */
82
+ }),
83
+ fingerprint: {
84
+ ignorePaths: [
85
+ "ios/Podfile.lock",
86
+ "ios/**/xcuserdata",
87
+ "ios/**/project.pbxproj",
88
+ // Add more paths to ignore as needed
89
+ ],
90
+ },
51
91
  };
52
92
  ```
53
93
 
54
94
  ## Configuration
55
95
 
56
- | Option | Type | Description |
57
- | ------------- | -------------------------------- |-------------------------------------------------------------------------------------------------|
58
- | `packageName` | `string` | Package name in GitLab Generic Package Registry |
59
- | `baseUrl` | `string` | GitLab instance URL |
60
- | `projectId` | `number` | GitLab project ID |
61
- | `token` | `string` | GitLab personal access token (`CI_JOB_TOKEN` on CI), You have to export `CI_JOB_TOKEN` in local |
62
- | | | |
63
- | `tokenHeader` | `"JOB-TOKEN" \| "PRIVATE-TOKEN"` | Token type |
96
+ | Option | Type | Description |
97
+ | ---------------- | -------- | ------------------------------------------------------------------------------------------ |
98
+ | `packageName` | `string` | Package name in GitLab Generic Package Registry |
99
+ | `registryServer` | `string` | GitLab instance URL |
100
+ | `projectId` | `number` | GitLab project ID |
101
+ | `token` | `string` | (Optional) Auth token. Defaults to `process.env.CI_JOB_TOKEN` |
102
+ | `tokenHeader` | `string` | (Optional) Auth token header. Defaults to `process.env.CI ? "JOB-TOKEN" : "PRIVATE-TOKEN"` |
64
103
 
65
104
  ## How It Works
66
105
 
67
106
  ### Upload (CI)
68
107
 
69
- All builds are uploaded to a **single package** with version `1.0.0` (Can change version if needed) at *Package Registry*.:
108
+ When your CI pipeline runs, you can use a script to upload the build artifacts to the GitLab Package Registry. (See the `example` folder in the repository for the `upload-cache-remote.sh` script).
109
+
110
+ ![img.png](assets/ci.png)
111
+
112
+ ### Registry Structure
113
+
114
+ All builds are uploaded to a single package version (e.g., 1.0.0).
115
+
116
+ ![img.png](assets/registry.png)
70
117
 
71
118
  ```
72
119
  mobile-artifacts@1.0.0/
@@ -75,13 +122,19 @@ mobile-artifacts@1.0.0/
75
122
  └── ...
76
123
  ```
77
124
 
78
- ### Download (Local/CI)
125
+ ### Download (Local)
79
126
 
80
- When running:
127
+ When you run `bun rock:run-ios` or `bun rock:run-android`:
81
128
 
82
- 1. Calculate project fingerprint
83
- 2. Search for file containing the fingerprint
84
- 3. Download and extract
129
+ 1. Rock calculates the **project fingerprint**.
130
+ 2. The provider searches the GitLab Package for a file containing that fingerprint.
131
+ 3. If found, it downloads and extracts the artifact automatically.
132
+
133
+ > Android:
134
+ > ![img.png](assets/run-android.png)
135
+
136
+ > iOS:
137
+ > ![img.png](assets/run-ios.png)
85
138
 
86
139
  ## GitLab CI Example
87
140
 
@@ -89,16 +142,33 @@ When running:
89
142
  build_android_cache:
90
143
  stage: build
91
144
  script:
92
- - bun run build:android --variant=devDebug
93
- - CACHE_DIR="$(ls -1dt .rock/cache/remote-build/rock-android-* | head -n1)"
94
- - sh scripts/upload-cache-remote.sh "${CACHE_DIR}" rock-android-devDebug-{FP}.zip android
95
- ```
145
+ - |
146
+ bun run rock:build-android
147
+
148
+ CACHE_DIR="$(ls -1dt .rock/cache/remote-build/${NAME_PREFIX}-* | head -n1 || true)"
96
149
 
97
- ## Differences from Official Provider
150
+ if [[ -z "${CACHE_DIR}" || ! -d "${CACHE_DIR}" ]]; then
151
+ echo "No cache output under .rock/cache/remote-build/${NAME_PREFIX}-*"
152
+ exit 1
153
+ fi
154
+ echo "CACHE_DIR=${CACHE_DIR}"
98
155
 
99
- The official `@rock-js/provider-gitlab` creates **one package per fingerprint**, which can clutter the package registry.
156
+ FP="${CACHE_DIR##*-}"
157
+ echo "Fingerprint: ${FP}"
158
+
159
+ if ! compgen -G "${CACHE_DIR}"/*.apk > /dev/null; then
160
+ echo "No .apk found in ${CACHE_DIR}"
161
+ ls -la "${CACHE_DIR}" || true
162
+ exit 1
163
+ fi
164
+
165
+ FILE_UPLOAD_NAME="${NAME_PREFIX}-${FP}.zip"
166
+ echo "FILE_UPLOAD_NAME=${FILE_UPLOAD_NAME}"
167
+
168
+ sh scripts/upload-cache-remote.sh "${CACHE_DIR}" "${FILE_UPLOAD_NAME}" android
169
+ ```
100
170
 
101
- This provider stores **all builds in one package** (version `1.0.4`), using filename-based lookup instead of version-based lookup.
171
+ > **_You can refer to the CI gitlab config in folder example_**
102
172
 
103
173
  ## License
104
174
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-cache-build-gitlab",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "main": "dist/index.js",
5
5
  "type": "module",
6
6
  "types": "dist/index.d.ts",
package/dist/index.d.ts DELETED
@@ -1 +0,0 @@
1
- export { providerGitLab } from "./provider-gitlab/providerGitlab.js";
package/dist/index.js DELETED
@@ -1 +0,0 @@
1
- export { providerGitLab } from "./provider-gitlab/providerGitlab.js";
@@ -1,19 +0,0 @@
1
- import { type RemoteArtifact } from "@rock-js/tools";
2
- export type GitLabRepoDetails = {
3
- baseUrl: string;
4
- projectId: number | string;
5
- token: string;
6
- tokenHeader: "PRIVATE-TOKEN" | "JOB-TOKEN";
7
- packageName: string;
8
- };
9
- export type GitLabArtifact = {
10
- packageId: number;
11
- fileId: number;
12
- name: string;
13
- version: string;
14
- sizeInBytes: number;
15
- createdAt: string;
16
- downloadUrl: string;
17
- };
18
- export declare function fetchGitLabArtifactsByName(name: string | undefined, repo: GitLabRepoDetails, limit?: number, version?: string): Promise<GitLabArtifact[]>;
19
- export declare function deleteGitLabArtifacts(artifacts: GitLabArtifact[], repo: GitLabRepoDetails, artifactNameForLog?: string): Promise<RemoteArtifact[]>;
@@ -1,106 +0,0 @@
1
- import { cacheManager, color, colorLink, logger, RockError, } from "@rock-js/tools";
2
- const PAGE_SIZE = 100;
3
- function headersFor(repo) {
4
- return {
5
- [repo.tokenHeader]: repo.token,
6
- Accept: "application/json",
7
- };
8
- }
9
- async function httpJson(url, repo) {
10
- const res = await fetch(url, { headers: headersFor(repo) });
11
- if (!res.ok) {
12
- const body = await res.text().catch(() => "");
13
- const info = `${res.status} ${res.statusText}${body ? ` — ${body}` : ""}`;
14
- if (res.status === 401 || res.status === 403) {
15
- cacheManager.remove("gitlabToken");
16
- throw new RockError(`GitLab auth failed.\nUpdate token under ${color.bold("remoteCacheProvider")} in ${colorLink("rock.config.mjs")}. Local uses ${color.bold("PRIVATE-TOKEN")}, CI uses ${color.bold("JOB-TOKEN")}.\nURL: ${colorLink(url)}\nError: ${info}`);
17
- }
18
- if (res.status === 404) {
19
- throw new RockError(`GitLab 404.\nCheck baseUrl/projectId/packageName or token perms.\nURL: ${colorLink(url)}`);
20
- }
21
- throw new RockError(`GitLab request failed: ${info}`);
22
- }
23
- return (await res.json());
24
- }
25
- function genericDownloadUrl(repo, packageName, version, fileName) {
26
- const { baseUrl, projectId } = repo;
27
- return `${baseUrl}/api/v4/projects/${encodeURIComponent(String(projectId))}/packages/generic/${encodeURIComponent(packageName)}/${encodeURIComponent(version)}/${encodeURIComponent(fileName)}`;
28
- }
29
- export async function fetchGitLabArtifactsByName(name, repo, limit, version) {
30
- if (!repo?.token) {
31
- throw new RockError("Missing GitLab token. Valid repoDetails.token & tokenHeader pass.");
32
- }
33
- const perPage = Math.min(limit ?? PAGE_SIZE, PAGE_SIZE);
34
- let page = 1;
35
- const packages = [];
36
- while (true) {
37
- const url = `${repo.baseUrl}/api/v4/projects/${encodeURIComponent(String(repo.projectId))}/packages` +
38
- `?package_type=generic&per_page=${perPage}&page=${page}` +
39
- (name ? `&package_name=${encodeURIComponent(name)}` : "");
40
- const chunk = await httpJson(url, repo);
41
- if (!chunk.length)
42
- break;
43
- packages.push(...chunk);
44
- if (chunk.length < perPage)
45
- break;
46
- page += 1;
47
- }
48
- const artifacts = [];
49
- for (const pkg of packages) {
50
- if (pkg.package_type !== "generic" || !pkg.version)
51
- continue;
52
- // Don't filter by version here - we'll filter by filename later
53
- let fPage = 1;
54
- while (true) {
55
- const filesUrl = `${repo.baseUrl}/api/v4/projects/${encodeURIComponent(String(repo.projectId))}` +
56
- `/packages/${pkg.id}/package_files?per_page=${PAGE_SIZE}&page=${fPage}`;
57
- const files = await httpJson(filesUrl, repo);
58
- if (!files.length)
59
- break;
60
- for (const file of files) {
61
- // If version (fingerprint) is provided, filter by filename containing it
62
- if (version && !file.file_name.includes(version))
63
- continue;
64
- artifacts.push({
65
- packageId: pkg.id,
66
- fileId: file.id,
67
- name: pkg.name,
68
- version: pkg.version,
69
- sizeInBytes: file.size,
70
- createdAt: file.created_at,
71
- downloadUrl: genericDownloadUrl(repo, pkg.name, pkg.version, file.file_name),
72
- });
73
- }
74
- if (files.length < PAGE_SIZE)
75
- break;
76
- fPage += 1;
77
- }
78
- }
79
- artifacts.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
80
- return artifacts;
81
- }
82
- export async function deleteGitLabArtifacts(artifacts, repo, artifactNameForLog) {
83
- const deleted = [];
84
- try {
85
- for (const artifact of artifacts) {
86
- const url = `${repo.baseUrl}/api/v4/projects/${encodeURIComponent(String(repo.projectId))}/packages/${artifact.packageId}/package_files/${artifact.fileId}`;
87
- const res = await fetch(url, {
88
- method: "DELETE",
89
- headers: headersFor(repo),
90
- });
91
- if (!res.ok) {
92
- logger.warn(`Delete failed packageId=${artifact.packageId}, fileId=${artifact.fileId}: ${res.status} ${res.statusText}`);
93
- continue;
94
- }
95
- deleted.push({
96
- name: `${artifact.name}@${artifact.version}`,
97
- url: artifact.downloadUrl,
98
- id: String(artifact.fileId),
99
- });
100
- }
101
- return deleted;
102
- }
103
- catch (error) {
104
- throw new RockError(`Failed to delete GitLab artifacts${artifactNameForLog ? ` for "${artifactNameForLog}"` : ""}`, { cause: error });
105
- }
106
- }
@@ -1,8 +0,0 @@
1
- import { GitLabRepoDetails } from "./artifacts";
2
- export type DetectGitLabRepoInput = Partial<GitLabRepoDetails> & {
3
- baseUrl?: string;
4
- projectId?: number | string;
5
- token?: string;
6
- tokenHeader?: "JOB-TOKEN" | "PRIVATE-TOKEN";
7
- };
8
- export declare function detectGitLabRepoDetails(override?: DetectGitLabRepoInput): Promise<GitLabRepoDetails>;
@@ -1,21 +0,0 @@
1
- import { RockError } from "@rock-js/tools";
2
- export async function detectGitLabRepoDetails(override) {
3
- const baseUrl = override?.baseUrl;
4
- const projectId = override?.projectId;
5
- const token = override?.token;
6
- const tokenHeader = override?.tokenHeader;
7
- const packageName = override?.packageName;
8
- if (!projectId) {
9
- throw new RockError("Missing GitLab projectId. Set GITLAB_PROJECT_ID or pass in override.");
10
- }
11
- if (!token) {
12
- throw new RockError("Missing GitLab token. Use CI_JOB_TOKEN or GITLAB_TOKEN.");
13
- }
14
- return {
15
- baseUrl,
16
- projectId,
17
- token,
18
- tokenHeader,
19
- packageName,
20
- };
21
- }
@@ -1,36 +0,0 @@
1
- import type { RemoteArtifact, RemoteBuildCache } from "@rock-js/tools";
2
- import { type GitLabRepoDetails } from "./artifacts.js";
3
- export declare class GitLabBuildCache implements RemoteBuildCache {
4
- name: string;
5
- repoDetails: GitLabRepoDetails | null;
6
- constructor(config?: {
7
- baseUrl: string;
8
- projectId: number | string;
9
- token?: string;
10
- tokenHeader?: "JOB-TOKEN" | "PRIVATE-TOKEN";
11
- packageName: string;
12
- });
13
- getRepoDetails(): Promise<GitLabRepoDetails>;
14
- list({ artifactName, limit, }: {
15
- artifactName?: string;
16
- limit?: number;
17
- }): Promise<RemoteArtifact[]>;
18
- download({ artifactName, }: {
19
- artifactName: string;
20
- }): Promise<Response>;
21
- delete({ artifactName, limit, skipLatest, }: {
22
- artifactName: string;
23
- limit?: number;
24
- skipLatest?: boolean;
25
- }): Promise<RemoteArtifact[]>;
26
- upload(): Promise<RemoteArtifact & {
27
- getResponse: (buffer: Buffer | ((baseUrl: string) => Buffer), contentType?: string | undefined) => Response;
28
- }>;
29
- }
30
- export declare const providerGitLab: (options?: {
31
- baseUrl: string;
32
- projectId: number;
33
- token?: string;
34
- tokenHeader?: "PRIVATE-TOKEN" | "JOB-TOKEN";
35
- packageName: string;
36
- }) => () => RemoteBuildCache;
@@ -1,78 +0,0 @@
1
- import { deleteGitLabArtifacts, fetchGitLabArtifactsByName, } from "./artifacts.js";
2
- import { detectGitLabRepoDetails } from "./config.js";
3
- function parseRockArtifact(artifactName) {
4
- if (!artifactName)
5
- return {};
6
- const maybe = artifactName.split("-").pop();
7
- if (/^[a-f0-9]{40}$/.test(maybe))
8
- return { fingerprint: maybe };
9
- return {};
10
- }
11
- export class GitLabBuildCache {
12
- name = "GitLab";
13
- repoDetails = null;
14
- constructor(config) {
15
- if (config) {
16
- const token = config.token ?? process.env.CI_JOB_TOKEN;
17
- const tokenHeader = config.tokenHeader ?? (process.env.CI ? "JOB-TOKEN" : "PRIVATE-TOKEN");
18
- if (!token) {
19
- throw new Error("GitLab token is required. Set CI_JOB_TOKEN (CI) or GITLAB_PRIVATE_TOKEN/PRIVATE_TOKEN (local).");
20
- }
21
- this.repoDetails = {
22
- packageName: config.packageName,
23
- baseUrl: config.baseUrl,
24
- projectId: config.projectId,
25
- token,
26
- tokenHeader,
27
- };
28
- }
29
- }
30
- async getRepoDetails() {
31
- if (!this.repoDetails) {
32
- this.repoDetails = await detectGitLabRepoDetails();
33
- }
34
- return this.repoDetails;
35
- }
36
- async list({ artifactName, limit, }) {
37
- const repo = await this.getRepoDetails();
38
- const { fingerprint } = parseRockArtifact(artifactName);
39
- const artifacts = await fetchGitLabArtifactsByName(repo.packageName, repo, limit, fingerprint);
40
- return artifacts.map((artifact) => ({
41
- name: `${artifact.name}@${artifact.version}`,
42
- url: artifact.downloadUrl,
43
- id: `${artifact.packageId}:${artifact.fileId}`,
44
- }));
45
- }
46
- async download({ artifactName, }) {
47
- const repo = await this.getRepoDetails();
48
- const { fingerprint } = parseRockArtifact(artifactName);
49
- if (!fingerprint)
50
- throw new Error("artifactName is required");
51
- const list = await fetchGitLabArtifactsByName(repo.packageName, repo, 1, fingerprint);
52
- if (list.length === 0) {
53
- throw new Error(`No GitLab artifact found for "${artifactName}"${fingerprint ? ` (version=${fingerprint})` : ""}`);
54
- }
55
- const url = list[0].downloadUrl;
56
- return fetch(url, {
57
- headers: {
58
- [repo.tokenHeader]: repo.token,
59
- "Accept-Encoding": "identity",
60
- },
61
- redirect: "follow",
62
- });
63
- }
64
- async delete({ artifactName, limit, skipLatest, }) {
65
- const repo = await this.getRepoDetails();
66
- const { fingerprint } = parseRockArtifact(artifactName);
67
- const artifacts = await fetchGitLabArtifactsByName(repo.packageName, repo, limit, fingerprint);
68
- if (artifacts.length === 0) {
69
- throw new Error(`No GitLab artifact found for "${artifactName}"${fingerprint ? ` (version=${fingerprint})` : ""}`);
70
- }
71
- const list = skipLatest ? artifacts.slice(1) : artifacts;
72
- return deleteGitLabArtifacts(list, repo, artifactName);
73
- }
74
- async upload() {
75
- throw new Error("Uploading via RemoteBuildCache is not yet implemented.");
76
- }
77
- }
78
- export const providerGitLab = (options) => () => new GitLabBuildCache(options);