Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .github/workflows/build-plugin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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: |
Expand Down
12 changes: 12 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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: |
Expand Down Expand Up @@ -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 }}
Expand All @@ -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 }}
Expand Down
38 changes: 30 additions & 8 deletions plugin/builder/build-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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}`);
Expand All @@ -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");

Expand All @@ -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 } : {}),
};

Expand Down
2 changes: 1 addition & 1 deletion plugin/builder/build-txz.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
5 changes: 4 additions & 1 deletion plugin/builder/cli/common-environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
Expand Down Expand Up @@ -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");
};
99 changes: 68 additions & 31 deletions plugin/builder/cli/setup-plugin-environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>;

Expand All @@ -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);
Expand All @@ -46,43 +75,45 @@ 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));
}
}
} catch (error) {
console.log(`Error scanning deploy directory: ${error}`);
}
}

// Check each path
for (const path of alternativePaths) {
if (existsSync(path)) {
Expand All @@ -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 (
Expand All @@ -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;

Expand All @@ -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);
Expand All @@ -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>",
Expand Down
15 changes: 10 additions & 5 deletions plugin/builder/utils/bucket-urls.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
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 {
baseUrl: string;
tag?: string;
}

interface TxzUrlParams extends UrlParams {
apiVersion: string;
}
interface TxzUrlParams extends UrlParams, TxzNameParams {}

/**
* Get the bucket path for the given tag
Expand Down Expand Up @@ -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));
Loading
Loading