feat: add setup-alpine package

This commit is contained in:
Amin Yahyaabadi 2025-03-23 00:57:08 -07:00
parent 6cd53aa7e2
commit f737a07371
9 changed files with 266 additions and 0 deletions

View File

@ -0,0 +1,3 @@
{
"extends": "../../.eslintrc.json"
}

View File

@ -0,0 +1,56 @@
{
"name": "setup-alpine",
"version": "1.0.0",
"description": "Setup apk packages and repositories in Alpine Linux distributions",
"repository": "https://github.com/aminya/setup-cpp",
"homepage": "https://github.com/aminya/setup-cpp/tree/master/packages/setup-alpine",
"license": "Apache-2.0",
"author": "Amin Yahyaabadi",
"main": "./dist/index.js",
"source": "./src/index.ts",
"type": "module",
"files": [
"dist",
"src",
"tsconfig.json"
],
"scripts": {
"build": "tsc --pretty",
"dev": "tsc --watch --pretty",
"lint.tsc": "tsc --noEmit --pretty",
"lint.eslint": "eslint '**/*.{ts,tsx,js,jsx,cjs,mjs,json,yaml}' --no-error-on-unmatched-pattern --cache --cache-location ./.cache/eslint/ --fix",
"prepublishOnly": "pnpm run build"
},
"dependencies": {
"@types/node": "22.13.11",
"admina": "^1.0.1",
"ci-info": "^4.0.0",
"path-exists": "^5.0.0",
"ci-log": "workspace:*",
"envosman": "workspace:*",
"which": "4.0.0",
"escape-string-regexp": "^5.0.0",
"node-downloader-helper": "2.1.9",
"memoizee": "^0.4.17"
},
"engines": {
"node": ">=12"
},
"keywords": [
"setup",
"apk",
"apk-add",
"repository",
"alpine",
"install",
"setup-apk",
"repositories",
"linux",
"alpine-linux",
"package"
],
"devDependencies": {
"@types/memoizee": "0.4.11",
"@types/which": "~3.0.4"
}
}

View File

@ -0,0 +1,39 @@
import { execRoot } from "admina"
import { info, warning } from "ci-log"
import { pathExists } from "path-exists"
import { hasApk } from "./has-apk.js"
/**
* Add an APK repository
* @param repoUrl The URL of the repository to add
* @returns Whether the repository was added successfully
*/
export async function addApkRepository(repoUrl: string): Promise<boolean> {
if (!(await hasApk())) {
warning("apk is not available on this system")
return false
}
try {
// Check if repositories file exists
const reposFile = "/etc/apk/repositories"
if (!(await pathExists(reposFile))) {
warning(`APK repositories file not found at ${reposFile}`)
return false
}
// Add repository to the file
info(`Adding repository: ${repoUrl}`)
await execRoot("sh", ["-c", `echo "${repoUrl}" >> ${reposFile}`], { stdio: "inherit" })
// Update package index after adding repository
await execRoot("apk", ["update"], { stdio: "inherit" })
info(`Successfully added repository: ${repoUrl}`)
return true
} catch (error) {
warning(`Failed to add repository ${repoUrl}: ${error}`)
return false
}
}

View File

@ -0,0 +1,15 @@
import memoizee from "memoizee"
import which from "which"
async function hasApk_() {
try {
await which("apk")
return true
} catch (error) {
return false
}
}
/**
* Check if apk is available on the system
*/
export const hasApk = memoizee(hasApk_, { promise: true })

View File

@ -0,0 +1,59 @@
import { execRoot } from "admina"
import { info, warning } from "ci-log"
import { hasApk } from "./has-apk.js"
import { type ApkPackage, filterAndQualifyApkPackages } from "./qualify-install.js"
import { updateApkMemoized } from "./update.js"
export type { ApkPackage }
/**
* Options for installing APK packages
*/
export interface InstallApkPackageOptions {
/** The package name to install */
package: ApkPackage
/** Whether to update the package index before installing */
update?: boolean
}
/**
* Install a package using Alpine's apk package manager
* @param packages The packages to install
* @param update Whether to update the package index before installing
* @returns Whether the installation was successful
*/
export async function installApkPackage(packages: ApkPackage[], update = false): Promise<boolean> {
// Check if apk is available
if (!(await hasApk())) {
warning("apk is not available on this system")
return false
}
try {
// Update package index if requested
if (update) {
// Force update the repos
await updateApkMemoized.clear()
}
// Update the repos if needed
await updateApkMemoized()
const packagesToInstall = await filterAndQualifyApkPackages(packages)
if (packagesToInstall.length === 0) {
info("All packages are already installed")
return true
}
// Install the packages
info(`Installing ${packagesToInstall.map((pack) => pack.name).join(" ")}`)
await execRoot("apk", ["add", ...packagesToInstall.map((pack) => pack.name)], { stdio: "inherit" })
info(`Successfully installed ${packagesToInstall.map((pack) => pack.name).join(" ")}`)
return true
} catch (error) {
warning(`Failed to install ${packages.map((pack) => pack.name).join(" ")}: ${error}`)
return false
}
}

View File

@ -0,0 +1,38 @@
import { execRoot } from "admina"
/**
* The information about an apt package
*/
export type ApkPackage = {
/** The name of the package */
name: string
/** The version of the package (optional) */
version?: string
/** The repository to add before installing the package (optional) */
repository?: string
/**
* If the given version is not available, fall back to the latest version
* @default false
*/
fallBackToLatest?: boolean
}
export async function filterAndQualifyApkPackages(packages: ApkPackage[]) {
// Filter out packages that are already installed
const installedPackages = await Promise.all(packages.map(checkPackageInstalled))
return packages.filter((_pack, index) => !installedPackages[index])
}
/**
* Check if a package is already installed
* @param pack The package to check
* @returns Whether the package is installed
*/
async function checkPackageInstalled(pack: ApkPackage): Promise<boolean> {
try {
const { exitCode } = await execRoot("apk", ["info", "-e", pack.name], { reject: false })
return exitCode === 0
} catch (error) {
return false
}
}

View File

@ -0,0 +1,7 @@
import { execRoot } from "admina"
import memoizee from "memoizee"
async function updateApk() {
await execRoot("apk", ["update"], { stdio: "inherit" })
}
export const updateApkMemoized = memoizee(updateApk, { promise: true })

View File

@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"noEmit": false,
"allowImportingTsExtensions": false
},
"include": ["./src"]
}

View File

@ -364,6 +364,46 @@ importers:
specifier: ^3.0.0
version: 3.0.4
packages/setup-alpine:
dependencies:
'@types/node':
specifier: 22.13.11
version: 22.13.11
admina:
specifier: ^1.0.1
version: 1.0.1
ci-info:
specifier: ^4.0.0
version: 4.2.0
ci-log:
specifier: workspace:*
version: link:../ci-log
envosman:
specifier: workspace:*
version: link:../envosman
escape-string-regexp:
specifier: ^5.0.0
version: 5.0.0
memoizee:
specifier: ^0.4.17
version: 0.4.17
node-downloader-helper:
specifier: 2.1.9
version: 2.1.9
path-exists:
specifier: ^5.0.0
version: 5.0.0
which:
specifier: 4.0.0
version: 4.0.0
devDependencies:
'@types/memoizee':
specifier: 0.4.11
version: 0.4.11
'@types/which':
specifier: ~3.0.4
version: 3.0.4
packages/setup-apt:
dependencies:
'@types/node':