mirror of
https://github.com/unraid/api.git
synced 2025-12-30 21:19:49 -06:00
build: replace hash with build increment in slackware txz pkg (#1449)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Introduced support for specifying and propagating a build number throughout the build process, including command-line options and workflow inputs. * TXZ package naming and URLs now include the build number for improved traceability. * **Improvements** * Enhanced robustness in locating TXZ files with improved fallback logic, especially in CI environments. * Improved flexibility and validation of environment schema for plugin builds. * **Style** * Minor formatting corrections for consistency and readability. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --- - To see the specific tasks where the Asana app for GitHub is being used, see below: - https://app.asana.com/0/0/1210677942019563
This commit is contained in:
8
.github/workflows/build-plugin.yml
vendored
8
.github/workflows/build-plugin.yml
vendored
@@ -23,6 +23,10 @@ on:
|
||||
type: string
|
||||
required: true
|
||||
description: "Base URL for the plugin builds"
|
||||
BUILD_NUMBER:
|
||||
type: string
|
||||
required: true
|
||||
description: "Build number for the plugin builds"
|
||||
secrets:
|
||||
CF_ACCESS_KEY_ID:
|
||||
required: true
|
||||
@@ -108,8 +112,8 @@ jobs:
|
||||
id: build-plugin
|
||||
run: |
|
||||
cd ${{ github.workspace }}/plugin
|
||||
pnpm run build:txz --tag="${{ inputs.TAG }}" --base-url="${{ inputs.BASE_URL }}" --api-version="${{ steps.vars.outputs.API_VERSION }}"
|
||||
pnpm run build:plugin --tag="${{ inputs.TAG }}" --base-url="${{ inputs.BASE_URL }}" --api-version="${{ steps.vars.outputs.API_VERSION }}"
|
||||
pnpm run build:txz --tag="${{ inputs.TAG }}" --base-url="${{ inputs.BASE_URL }}" --api-version="${{ steps.vars.outputs.API_VERSION }}" --build-number="${{ inputs.BUILD_NUMBER }}"
|
||||
pnpm run build:plugin --tag="${{ inputs.TAG }}" --base-url="${{ inputs.BASE_URL }}" --api-version="${{ steps.vars.outputs.API_VERSION }}" --build-number="${{ inputs.BUILD_NUMBER }}"
|
||||
|
||||
- name: Ensure Plugin Files Exist
|
||||
run: |
|
||||
|
||||
12
.github/workflows/main.yml
vendored
12
.github/workflows/main.yml
vendored
@@ -156,6 +156,8 @@ jobs:
|
||||
build-api:
|
||||
name: Build API
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
build_number: ${{ steps.buildnumber.outputs.build_number }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: api
|
||||
@@ -210,6 +212,14 @@ jobs:
|
||||
API_VERSION=$([[ -n "$IS_TAGGED" ]] && echo "$PACKAGE_LOCK_VERSION" || echo "${PACKAGE_LOCK_VERSION}+${GIT_SHA}")
|
||||
export API_VERSION
|
||||
echo "API_VERSION=${API_VERSION}" >> $GITHUB_ENV
|
||||
echo "PACKAGE_LOCK_VERSION=${PACKAGE_LOCK_VERSION}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Generate build number
|
||||
id: buildnumber
|
||||
uses: onyxmueller/build-tag-number@v1
|
||||
with:
|
||||
token: ${{secrets.github_token}}
|
||||
prefix: ${{steps.vars.outputs.PACKAGE_LOCK_VERSION}}
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
@@ -365,6 +375,7 @@ jobs:
|
||||
TAG: ${{ github.event.pull_request.number && format('PR{0}', github.event.pull_request.number) || '' }}
|
||||
BUCKET_PATH: ${{ github.event.pull_request.number && format('unraid-api/tag/PR{0}', github.event.pull_request.number) || 'unraid-api' }}
|
||||
BASE_URL: "https://preview.dl.unraid.net/unraid-api"
|
||||
BUILD_NUMBER: ${{ needs.build-api.outputs.build_number }}
|
||||
secrets:
|
||||
CF_ACCESS_KEY_ID: ${{ secrets.CF_ACCESS_KEY_ID }}
|
||||
CF_SECRET_ACCESS_KEY: ${{ secrets.CF_SECRET_ACCESS_KEY }}
|
||||
@@ -387,6 +398,7 @@ jobs:
|
||||
TAG: ""
|
||||
BUCKET_PATH: unraid-api
|
||||
BASE_URL: "https://stable.dl.unraid.net/unraid-api"
|
||||
BUILD_NUMBER: ${{ needs.build-api.outputs.build_number }}
|
||||
secrets:
|
||||
CF_ACCESS_KEY_ID: ${{ secrets.CF_ACCESS_KEY_ID }}
|
||||
CF_SECRET_ACCESS_KEY: ${{ secrets.CF_SECRET_ACCESS_KEY }}
|
||||
|
||||
@@ -2,7 +2,13 @@ import { readFile, writeFile, mkdir, rename } from "fs/promises";
|
||||
import { $ } from "zx";
|
||||
import { escape as escapeHtml } from "html-sloppy-escaper";
|
||||
import { dirname, join } from "node:path";
|
||||
import { getTxzName, pluginName, startingDir, defaultArch, defaultBuild } from "./utils/consts";
|
||||
import {
|
||||
getTxzName,
|
||||
pluginName,
|
||||
startingDir,
|
||||
defaultArch,
|
||||
defaultBuild,
|
||||
} from "./utils/consts";
|
||||
import { getPluginUrl } from "./utils/bucket-urls";
|
||||
import { getMainTxzUrl } from "./utils/bucket-urls";
|
||||
import {
|
||||
@@ -25,10 +31,17 @@ const checkGit = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const moveTxzFile = async ({txzPath, apiVersion}: Pick<PluginEnv, "txzPath" | "apiVersion">) => {
|
||||
const txzName = getTxzName(apiVersion);
|
||||
const moveTxzFile = async ({
|
||||
txzPath,
|
||||
apiVersion,
|
||||
buildNumber,
|
||||
}: Pick<PluginEnv, "txzPath" | "apiVersion" | "buildNumber">) => {
|
||||
const txzName = getTxzName({
|
||||
version: apiVersion,
|
||||
build: buildNumber.toString(),
|
||||
});
|
||||
const targetPath = join(deployDir, txzName);
|
||||
|
||||
|
||||
// Ensure the txz always has the full version name
|
||||
if (txzPath !== targetPath) {
|
||||
console.log(`Ensuring TXZ has correct name: ${txzPath} -> ${targetPath}`);
|
||||
@@ -54,13 +67,14 @@ function updateEntityValue(
|
||||
const buildPlugin = async ({
|
||||
pluginVersion,
|
||||
baseUrl,
|
||||
buildNumber,
|
||||
tag,
|
||||
txzSha256,
|
||||
releaseNotes,
|
||||
apiVersion,
|
||||
}: PluginEnv) => {
|
||||
console.log(`API version: ${apiVersion}`);
|
||||
|
||||
|
||||
// Update plg file
|
||||
let plgContent = await readFile(getRootPluginPath({ startingDir }), "utf8");
|
||||
|
||||
@@ -70,11 +84,19 @@ const buildPlugin = async ({
|
||||
version: pluginVersion,
|
||||
api_version: apiVersion,
|
||||
arch: defaultArch,
|
||||
build: defaultBuild,
|
||||
build: buildNumber.toString(),
|
||||
plugin_url: getPluginUrl({ baseUrl, tag }),
|
||||
txz_url: getMainTxzUrl({ baseUrl, apiVersion, tag }),
|
||||
txz_url: getMainTxzUrl({
|
||||
baseUrl,
|
||||
tag,
|
||||
version: apiVersion,
|
||||
build: buildNumber.toString(),
|
||||
}),
|
||||
txz_sha256: txzSha256,
|
||||
txz_name: getTxzName(apiVersion),
|
||||
txz_name: getTxzName({
|
||||
version: apiVersion,
|
||||
build: buildNumber.toString(),
|
||||
}),
|
||||
...(tag ? { tag } : {}),
|
||||
};
|
||||
|
||||
|
||||
@@ -158,7 +158,7 @@ const buildTxz = async (validatedEnv: TxzEnv) => {
|
||||
const version = validatedEnv.apiVersion;
|
||||
|
||||
// Always use version when getting txz name
|
||||
const txzName = getTxzName(version);
|
||||
const txzName = getTxzName({ version, build: validatedEnv.buildNumber.toString() });
|
||||
console.log(`Package name: ${txzName}`);
|
||||
const txzPath = join(validatedEnv.txzOutputDir, txzName);
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ export const baseEnvSchema = z.object({
|
||||
apiVersion: z.string(),
|
||||
baseUrl: z.string().url(),
|
||||
tag: z.string().optional().default(""),
|
||||
/** i.e. Slackware build number */
|
||||
buildNumber: z.coerce.number().int().default(1),
|
||||
});
|
||||
|
||||
export type BaseEnv = z.infer<typeof baseEnvSchema>;
|
||||
@@ -43,5 +45,6 @@ export const addCommonOptions = (program: Command) => {
|
||||
"--tag <tag>",
|
||||
"Tag (used for PR and staging builds)",
|
||||
process.env.TAG
|
||||
);
|
||||
)
|
||||
.option("--build-number <number>", "Build number");
|
||||
};
|
||||
|
||||
@@ -8,22 +8,47 @@ import { existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { baseEnvSchema, addCommonOptions } from "./common-environment";
|
||||
|
||||
const safeParseEnvSchema = baseEnvSchema.extend({
|
||||
txzPath: z.string().refine((val) => val.endsWith(".txz"), {
|
||||
message: "TXZ Path must end with .txz",
|
||||
}),
|
||||
const basePluginSchema = baseEnvSchema.extend({
|
||||
txzPath: z
|
||||
.string()
|
||||
.refine((val) => val.endsWith(".txz"), {
|
||||
message: "TXZ Path must end with .txz",
|
||||
})
|
||||
.optional(),
|
||||
pluginVersion: z.string().regex(/^\d{4}\.\d{2}\.\d{2}\.\d{4}$/, {
|
||||
message: "Plugin version must be in the format YYYY.MM.DD.HHMM",
|
||||
}),
|
||||
releaseNotesPath: z.string().optional(),
|
||||
});
|
||||
|
||||
const pluginEnvSchema = safeParseEnvSchema.extend({
|
||||
releaseNotes: z.string().nonempty("Release notes are required"),
|
||||
txzSha256: z.string().refine((val) => val.length === 64, {
|
||||
message: "TXZ SHA256 must be 64 characters long",
|
||||
}),
|
||||
});
|
||||
const safeParseEnvSchema = basePluginSchema.transform((data) => ({
|
||||
...data,
|
||||
txzPath:
|
||||
data.txzPath ||
|
||||
getTxzPath({
|
||||
startingDir: process.cwd(),
|
||||
version: data.apiVersion,
|
||||
build: data.buildNumber.toString(),
|
||||
}),
|
||||
}));
|
||||
|
||||
const pluginEnvSchema = basePluginSchema
|
||||
.extend({
|
||||
releaseNotes: z.string().nonempty("Release notes are required"),
|
||||
txzSha256: z.string().refine((val) => val.length === 64, {
|
||||
message: "TXZ SHA256 must be 64 characters long",
|
||||
}),
|
||||
})
|
||||
.transform((data) => ({
|
||||
...data,
|
||||
txzPath:
|
||||
data.txzPath ||
|
||||
getTxzPath({
|
||||
startingDir: process.cwd(),
|
||||
version: data.apiVersion,
|
||||
build: data.buildNumber.toString(),
|
||||
}),
|
||||
}));
|
||||
|
||||
export type PluginEnv = z.infer<typeof pluginEnvSchema>;
|
||||
|
||||
@@ -36,7 +61,11 @@ export type PluginEnv = z.infer<typeof pluginEnvSchema>;
|
||||
* @returns Object containing the resolved txz path and SHA256 hash
|
||||
* @throws Error if no valid txz file can be found
|
||||
*/
|
||||
export const resolveTxzPath = async (txzPath: string, apiVersion: string, isCi?: boolean): Promise<{path: string, sha256: string}> => {
|
||||
export const resolveTxzPath = async (
|
||||
txzPath: string,
|
||||
apiVersion: string,
|
||||
isCi?: boolean
|
||||
): Promise<{ path: string; sha256: string }> => {
|
||||
if (existsSync(txzPath)) {
|
||||
await access(txzPath, constants.F_OK);
|
||||
console.log("Reading txz file from:", txzPath);
|
||||
@@ -46,35 +75,37 @@ export const resolveTxzPath = async (txzPath: string, apiVersion: string, isCi?:
|
||||
}
|
||||
return {
|
||||
path: txzPath,
|
||||
sha256: getSha256(txzFile)
|
||||
sha256: getSha256(txzFile),
|
||||
};
|
||||
}
|
||||
|
||||
console.log(`TXZ path not found at: ${txzPath}`);
|
||||
console.log(`Attempting to find TXZ using apiVersion: ${apiVersion}`);
|
||||
|
||||
|
||||
// Try different formats of generated TXZ name
|
||||
const deployDir = join(process.cwd(), "deploy");
|
||||
|
||||
|
||||
// Try with exact apiVersion format
|
||||
const alternativePaths = [
|
||||
join(deployDir, `dynamix.unraid.net-${apiVersion}-x86_64-1.txz`),
|
||||
];
|
||||
|
||||
|
||||
// In CI, we sometimes see unusual filenames, so try a glob-like approach
|
||||
if (isCi) {
|
||||
console.log("Checking for possible TXZ files in deploy directory");
|
||||
|
||||
|
||||
try {
|
||||
// Using node's filesystem APIs to scan the directory
|
||||
const fs = require('fs');
|
||||
const fs = require("fs");
|
||||
const deployFiles = fs.readdirSync(deployDir);
|
||||
|
||||
|
||||
// Find any txz file that contains the apiVersion
|
||||
for (const file of deployFiles) {
|
||||
if (file.endsWith('.txz') &&
|
||||
file.includes('dynamix.unraid.net') &&
|
||||
file.includes(apiVersion.split('+')[0])) {
|
||||
if (
|
||||
file.endsWith(".txz") &&
|
||||
file.includes("dynamix.unraid.net") &&
|
||||
file.includes(apiVersion.split("+")[0])
|
||||
) {
|
||||
alternativePaths.push(join(deployDir, file));
|
||||
}
|
||||
}
|
||||
@@ -82,7 +113,7 @@ export const resolveTxzPath = async (txzPath: string, apiVersion: string, isCi?:
|
||||
console.log(`Error scanning deploy directory: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Check each path
|
||||
for (const path of alternativePaths) {
|
||||
if (existsSync(path)) {
|
||||
@@ -96,14 +127,16 @@ export const resolveTxzPath = async (txzPath: string, apiVersion: string, isCi?:
|
||||
}
|
||||
return {
|
||||
path,
|
||||
sha256: getSha256(txzFile)
|
||||
sha256: getSha256(txzFile),
|
||||
};
|
||||
}
|
||||
console.log(`Could not find TXZ at: ${path}`);
|
||||
}
|
||||
|
||||
|
||||
// If we get here, we couldn't find a valid txz file
|
||||
throw new Error(`Could not find any valid TXZ file. Tried original path: ${txzPath} and alternatives.`);
|
||||
throw new Error(
|
||||
`Could not find any valid TXZ file. Tried original path: ${txzPath} and alternatives.`
|
||||
);
|
||||
};
|
||||
|
||||
export const validatePluginEnv = async (
|
||||
@@ -127,7 +160,11 @@ export const validatePluginEnv = async (
|
||||
}
|
||||
|
||||
// Resolve and validate the txz path
|
||||
const { path, sha256 } = await resolveTxzPath(safeEnv.txzPath, safeEnv.apiVersion, safeEnv.ci);
|
||||
const { path, sha256 } = await resolveTxzPath(
|
||||
safeEnv.txzPath,
|
||||
safeEnv.apiVersion,
|
||||
safeEnv.ci
|
||||
);
|
||||
envArgs.txzPath = path;
|
||||
envArgs.txzSha256 = sha256;
|
||||
|
||||
@@ -142,8 +179,9 @@ export const validatePluginEnv = async (
|
||||
|
||||
export const getPluginVersion = () => {
|
||||
const now = new Date();
|
||||
|
||||
const formatUtcComponent = (component: number) => String(component).padStart(2, '0');
|
||||
|
||||
const formatUtcComponent = (component: number) =>
|
||||
String(component).padStart(2, "0");
|
||||
|
||||
const year = now.getUTCFullYear();
|
||||
const month = formatUtcComponent(now.getUTCMonth() + 1);
|
||||
@@ -162,13 +200,12 @@ export const setupPluginEnv = async (argv: string[]): Promise<PluginEnv> => {
|
||||
|
||||
// Add common options
|
||||
addCommonOptions(program);
|
||||
|
||||
|
||||
// Add plugin-specific options
|
||||
program
|
||||
.option(
|
||||
"--txz-path <path>",
|
||||
"Path to built package, will be used to generate the SHA256 and renamed with the plugin version",
|
||||
getTxzPath({ startingDir: process.cwd(), pluginVersion: process.env.API_VERSION })
|
||||
"Path to built package, will be used to generate the SHA256 and renamed with the plugin version"
|
||||
)
|
||||
.option(
|
||||
"--plugin-version <version>",
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { getTxzName, LOCAL_BUILD_TAG, pluginNameWithExt, defaultArch, defaultBuild } from "./consts";
|
||||
import {
|
||||
getTxzName,
|
||||
LOCAL_BUILD_TAG,
|
||||
pluginNameWithExt,
|
||||
defaultArch,
|
||||
defaultBuild,
|
||||
TxzNameParams,
|
||||
} from "./consts";
|
||||
|
||||
// Define a common interface for URL parameters
|
||||
interface UrlParams {
|
||||
@@ -6,9 +13,7 @@ interface UrlParams {
|
||||
tag?: string;
|
||||
}
|
||||
|
||||
interface TxzUrlParams extends UrlParams {
|
||||
apiVersion: string;
|
||||
}
|
||||
interface TxzUrlParams extends UrlParams, TxzNameParams {}
|
||||
|
||||
/**
|
||||
* Get the bucket path for the given tag
|
||||
@@ -47,4 +52,4 @@ export const getPluginUrl = (params: UrlParams): string =>
|
||||
* ex. returns = BASE_URL/TAG/dynamix.unraid.net-4.1.3-x86_64-1.txz
|
||||
*/
|
||||
export const getMainTxzUrl = (params: TxzUrlParams): string =>
|
||||
getAssetUrl(params, getTxzName(params.apiVersion, defaultArch, defaultBuild));
|
||||
getAssetUrl(params, getTxzName(params));
|
||||
|
||||
@@ -5,9 +5,21 @@ export const pluginNameWithExt = `${pluginName}.plg` as const;
|
||||
export const defaultArch = "x86_64" as const;
|
||||
export const defaultBuild = "1" as const;
|
||||
|
||||
export interface TxzNameParams {
|
||||
version?: string;
|
||||
arch?: string;
|
||||
build?: string;
|
||||
}
|
||||
|
||||
// Get the txz name following Slackware naming convention: name-version-arch-build.txz
|
||||
export const getTxzName = (version?: string, arch: string = defaultArch, build: string = defaultBuild) =>
|
||||
version ? `${pluginName}-${version}-${arch}-${build}.txz` : `${pluginName}.txz`;
|
||||
export const getTxzName = ({
|
||||
version,
|
||||
arch = defaultArch,
|
||||
build = defaultBuild,
|
||||
}: TxzNameParams) =>
|
||||
version
|
||||
? `${pluginName}-${version}-${arch}-${build}.txz`
|
||||
: `${pluginName}.txz`;
|
||||
export const startingDir = process.cwd();
|
||||
|
||||
export const BASE_URLS = {
|
||||
@@ -15,4 +27,4 @@ export const BASE_URLS = {
|
||||
PREVIEW: "https://preview.dl.unraid.net/unraid-api",
|
||||
} as const;
|
||||
|
||||
export const LOCAL_BUILD_TAG = "LOCAL_PLUGIN_BUILD" as const;
|
||||
export const LOCAL_BUILD_TAG = "LOCAL_PLUGIN_BUILD" as const;
|
||||
|
||||
@@ -4,15 +4,14 @@ import {
|
||||
pluginName,
|
||||
pluginNameWithExt,
|
||||
startingDir,
|
||||
TxzNameParams,
|
||||
} from "./consts";
|
||||
|
||||
export interface PathConfig {
|
||||
startingDir: string;
|
||||
}
|
||||
|
||||
export interface TxzPathConfig extends PathConfig {
|
||||
pluginVersion?: string;
|
||||
}
|
||||
export interface TxzPathConfig extends PathConfig, TxzNameParams {}
|
||||
|
||||
export const deployDir = "deploy" as const;
|
||||
|
||||
@@ -53,7 +52,8 @@ export function getDeployPluginPath({ startingDir }: PathConfig): string {
|
||||
*/
|
||||
export function getTxzPath({
|
||||
startingDir,
|
||||
pluginVersion,
|
||||
version,
|
||||
build,
|
||||
}: TxzPathConfig): string {
|
||||
return join(startingDir, deployDir, getTxzName(pluginVersion));
|
||||
return join(startingDir, deployDir, getTxzName({ version, build }));
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"tsx": "^4.19.2",
|
||||
"zod": "^3.24.1",
|
||||
"zx": "^8.3.2"
|
||||
},
|
||||
},
|
||||
"type": "module",
|
||||
"license": "GPL-2.0-or-later",
|
||||
"scripts": {
|
||||
|
||||
7
pnpm-lock.yaml
generated
7
pnpm-lock.yaml
generated
@@ -11107,6 +11107,7 @@ packages:
|
||||
engines: {node: '>=0.6.0', teleport: '>=0.2.0'}
|
||||
deprecated: |-
|
||||
You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.
|
||||
|
||||
(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)
|
||||
|
||||
qs@6.13.0:
|
||||
@@ -21480,6 +21481,8 @@ snapshots:
|
||||
dependencies:
|
||||
tabbable: 6.2.0
|
||||
|
||||
follow-redirects@1.15.9: {}
|
||||
|
||||
follow-redirects@1.15.9(debug@4.3.7):
|
||||
optionalDependencies:
|
||||
debug: 4.3.7
|
||||
@@ -22152,7 +22155,7 @@ snapshots:
|
||||
http-proxy@1.18.1:
|
||||
dependencies:
|
||||
eventemitter3: 4.0.7
|
||||
follow-redirects: 1.15.9(debug@4.3.7)
|
||||
follow-redirects: 1.15.9
|
||||
requires-port: 1.0.0
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
@@ -26301,7 +26304,7 @@ snapshots:
|
||||
terser@5.43.1:
|
||||
dependencies:
|
||||
'@jridgewell/source-map': 0.3.6
|
||||
acorn: 8.14.1
|
||||
acorn: 8.15.0
|
||||
commander: 2.20.3
|
||||
source-map-support: 0.5.21
|
||||
|
||||
|
||||
Reference in New Issue
Block a user