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
10 changes: 10 additions & 0 deletions .github/actions/setup-toolchains/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ inputs:
description: Node.js version.
required: false
default: '20'
node-cache:
description: Optional actions/setup-node cache backend.
required: false
default: ''
node-cache-dependency-path:
description: Optional dependency file path used by actions/setup-node cache.
required: false
default: ''
setup-python:
description: Whether to setup Python.
required: false
Expand All @@ -27,6 +35,8 @@ runs:
uses: actions/setup-node@v6.2.0
with:
node-version: ${{ inputs.node-version }}
cache: ${{ inputs.node-cache }}
cache-dependency-path: ${{ inputs.node-cache-dependency-path }}

- name: Setup Python
if: ${{ inputs.setup-python == 'true' }}
Expand Down
11 changes: 8 additions & 3 deletions .github/workflows/build-desktop-tauri.yml
Original file line number Diff line number Diff line change
Expand Up @@ -315,8 +315,7 @@ jobs:
printf 'signing_identity=%s\n' "$CERT_ID" >> "$GITHUB_OUTPUT"
echo "Imported signing identity: $CERT_ID"

- name: Pre-sign backend resources (macOS)
if: ${{ steps.import_apple_certificate.outputs.signing_identity != '' }}
- name: Prepare desktop resources (macOS)
env:
ASTRBOT_SOURCE_GIT_URL: ${{ needs.resolve_build_context.outputs.source_git_url }}
ASTRBOT_SOURCE_GIT_REF: ${{ needs.resolve_build_context.outputs.source_git_ref }}
Comment on lines +318 to 321
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Align shell configuration for steps that rely on bash-specific options like pipefail

This step uses set -euo pipefail but doesn’t declare shell: bash, while the following "Pre-sign backend resources" step does for the same pattern. Please also set shell: bash here to avoid dependence on the current macOS default and keep the related steps consistent.

Suggested implementation:

      - name: Prepare desktop resources (macOS)
        shell: bash
        env:
          ASTRBOT_SOURCE_GIT_URL: ${{ needs.resolve_build_context.outputs.source_git_url }}
          ASTRBOT_SOURCE_GIT_REF: ${{ needs.resolve_build_context.outputs.source_git_ref }}

From the snippet, it looks like the run: | block for "Prepare desktop resources (macOS)" is omitted or partially shown. Ensure that this step’s script (the part that uses set -euo pipefail) is indeed under this step and remains unchanged, now executing under bash due to the new shell: bash line.

Expand All @@ -332,6 +331,12 @@ jobs:
echo "::error::resources/backend not found after prepare:resources." >&2
exit 1
fi

- name: Pre-sign backend resources (macOS)
if: ${{ steps.import_apple_certificate.outputs.signing_identity != '' }}
shell: bash
run: |
set -euo pipefail
echo "Pre-signing Mach-O binaries in resources/backend..."
bash scripts/ci/codesign-macos-nested.sh "resources/backend" "src-tauri/entitlements.plist"

Expand Down Expand Up @@ -380,7 +385,7 @@ jobs:

max_attempts="${ASTRBOT_MACOS_BUILD_MAX_ATTEMPTS}"
retry_sleep_seconds="${ASTRBOT_MACOS_BUILD_RETRY_SLEEP_SECONDS}"
# Resources are already prepared and pre-signed in the previous step.
# Resources are already prepared and, when available, pre-signed in earlier steps.
tauri_config_override='{"build":{"beforeBuildCommand":""}}'
# Retry only for known transient cargo/crates network failures.
# Spurious retry hints emitted by cargo.
Expand Down
12 changes: 12 additions & 0 deletions .github/workflows/check-scripts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,17 @@ jobs:
- name: Checkout
uses: actions/checkout@v6.0.2

- name: Setup pnpm
uses: pnpm/action-setup@v4.4.0
with:
version: 10.28.2

- name: Setup Toolchains
uses: ./.github/actions/setup-toolchains
with:
node-version: '20'
node-cache: 'pnpm'
node-cache-dependency-path: pnpm-lock.yaml
python-version: '3.12'

- name: Check Node scripts syntax
Expand All @@ -36,6 +43,11 @@ jobs:
node --check "${file}"
done < <(find scripts -type f \( -name '*.mjs' -o -name '*.cjs' -o -name '*.js' \) -print0 | sort -z)

- name: Install Node test dependencies
run: |
set -euo pipefail
pnpm install --frozen-lockfile --ignore-scripts

- name: Run Node script behavior tests
run: |
set -euo pipefail
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,8 @@
"prepare:resources": "pnpm run prepare:webui && pnpm run prepare:backend",
"dev": "cargo tauri dev",
"build": "cargo tauri build"
},
"devDependencies": {
"yaml": "^2.8.1"
}
}
17 changes: 16 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

70 changes: 70 additions & 0 deletions scripts/ci/build-desktop-tauri-workflow.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
import {
extractWorkflowJobSteps,
findStep,
findStepIndex,
readWorkflowObject,
} from './workflow-test-utils.mjs';

const WORKFLOW_FILE = 'build-desktop-tauri.yml';
const BUILD_MACOS_JOB = 'build-macos';
const PREPARE_RESOURCES_RUN = /pnpm run prepare:resources/;
const PRESIGN_BACKEND_RUN = /codesign-macos-nested\.sh\s+"resources\/backend"/;
const BUILD_APP_BUNDLE_RUN = /cargo tauri build --verbose --target/;

test('findStep supports predicate and regex matching', () => {
const steps = [
{ name: 'Prepare desktop resources (macOS) [unsigned-compatible]', run: 'pnpm run prepare:resources' },
{ name: 'Build desktop app bundle (macOS) release artifacts', run: 'cargo tauri build --verbose --target x86_64-apple-darwin' },
];

assert.equal(findStep(steps, 'prepare resources run', (step) => PREPARE_RESOURCES_RUN.test(step.run ?? '')), steps[0]);
assert.equal(findStep(steps, /Build desktop app bundle/, (step) => BUILD_APP_BUNDLE_RUN.test(step.run ?? '')), steps[1]);
});

test('macOS workflow exposes structured build-macos steps', async () => {
const workflowObject = await readWorkflowObject(WORKFLOW_FILE);
const steps = extractWorkflowJobSteps(workflowObject, BUILD_MACOS_JOB);

assert.ok(findStep(steps, 'prepare resources step', (step) => PREPARE_RESOURCES_RUN.test(step.run ?? '')));
assert.ok(findStep(steps, 'pre-sign resources step', (step) => PRESIGN_BACKEND_RUN.test(step.run ?? '')));
assert.ok(findStep(steps, 'build app bundle step', (step) => BUILD_APP_BUNDLE_RUN.test(step.run ?? '')));
});

test('macOS workflow prepares resources before optional pre-signing', async () => {
const workflowObject = await readWorkflowObject(WORKFLOW_FILE);
const steps = extractWorkflowJobSteps(workflowObject, BUILD_MACOS_JOB);
const prepareStepIndex = findStepIndex(
steps,
(step) => PREPARE_RESOURCES_RUN.test(step.run ?? ''),
'prepare resources step',
);
const preSignStepIndex = findStepIndex(
steps,
(step) => PRESIGN_BACKEND_RUN.test(step.run ?? ''),
'pre-sign resources step',
);
const buildStepIndex = findStepIndex(
steps,
(step) => BUILD_APP_BUNDLE_RUN.test(step.run ?? ''),
'build app bundle step',
);
const prepareStep = steps[prepareStepIndex];
const preSignStep = steps[preSignStepIndex];
const buildStep = steps[buildStepIndex];

assert.equal(prepareStep.if, undefined);
assert.match(prepareStep.run, /pnpm run prepare:resources/);
assert.match(prepareStep.run, /resources\/backend not found after prepare:resources/);

assert.match(preSignStep.if ?? '', /import_apple_certificate\.outputs\.signing_identity/);
assert.doesNotMatch(preSignStep.run, /pnpm run prepare:resources/);

assert.ok(prepareStepIndex < preSignStepIndex);
assert.ok(preSignStepIndex < buildStepIndex);
assert.match(
buildStep.run,
/Resources are already prepared/,
);
Comment on lines +66 to +69
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current regex-based assertions are somewhat fragile because [\s\S]*? can match across multiple YAML steps if the expected pattern is missing in the target step but present in a subsequent one. Additionally, the test name implies a specific order of operations ("prepares resources before any conditional pre-signing"), but the current assertions do not verify the relative order of these steps in the workflow file.

Consider using indexOf to identify step boundaries and then asserting on the content within those specific slices. This ensures that the commands are found in the correct steps and that the steps appear in the intended order.

  const prepareIdx = workflow.indexOf('- name: Prepare desktop resources (macOS)');
  const presignIdx = workflow.indexOf('- name: Pre-sign backend resources (macOS)');
  const buildIdx = workflow.indexOf('Build desktop app bundle (macOS)');

  assert.ok(prepareIdx !== -1 && presignIdx !== -1 && buildIdx !== -1, 'Required workflow steps not found');
  assert.ok(prepareIdx < presignIdx, 'Prepare step should come before Pre-sign step');
  assert.ok(presignIdx < buildIdx, 'Pre-sign step should come before Build step');

  assert.match(
    workflow.slice(prepareIdx, presignIdx),
    /run: \|[\s\S]*?pnpm run prepare:resources/,
  );
  assert.doesNotMatch(
    workflow.slice(presignIdx, buildIdx),
    /pnpm run prepare:resources/,
  );
  assert.match(
    workflow.slice(buildIdx),
    /# Resources are already prepared and, when available, pre-signed in earlier steps\./,
  );

});
57 changes: 57 additions & 0 deletions scripts/ci/check-scripts-workflow.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
import {
extractWorkflowJobSteps,
findStep,
findStepIndex,
readWorkflowObject,
} from './workflow-test-utils.mjs';

const WORKFLOW_FILE = 'check-scripts.yml';
const SCRIPTS_JOB = 'scripts';

test('check-scripts workflow installs node dependencies before running node tests', async () => {
const workflowObject = await readWorkflowObject(WORKFLOW_FILE);
const steps = extractWorkflowJobSteps(workflowObject, SCRIPTS_JOB);
const setupToolchainsStep = findStep(
steps,
'setup toolchains step',
(step) => (step.uses ?? '').includes('./.github/actions/setup-toolchains'),
);
const installStep = findStep(
steps,
'installing node dependencies',
(step) => /pnpm install/.test(step.run ?? ''),
);

const pnpmSetupIndex = findStepIndex(
steps,
(step) => (step.uses ?? '').includes('pnpm/action-setup'),
'using pnpm/action-setup',
);
const setupToolchainsIndex = findStepIndex(
steps,
(step) => step === setupToolchainsStep,
'using setup toolchains',
);
const installIndex = findStepIndex(
steps,
(step) => step === installStep,
'installing node dependencies',
);
const nodeTestIndex = findStepIndex(
steps,
(step) => /node --test/.test(step.run ?? ''),
'running Node script behavior tests',
);

assert.equal(setupToolchainsStep.with['node-cache'], 'pnpm');
assert.equal(setupToolchainsStep.with['node-cache-dependency-path'], 'pnpm-lock.yaml');
assert.match(installStep.run, /pnpm install/);
assert.match(installStep.run, /--frozen-lockfile/);
assert.match(installStep.run, /--ignore-scripts/);

assert.ok(pnpmSetupIndex < setupToolchainsIndex);
assert.ok(pnpmSetupIndex < installIndex);
assert.ok(installIndex < nodeTestIndex);
});
27 changes: 27 additions & 0 deletions scripts/ci/setup-toolchains-action.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
import {
extractCompositeActionSteps,
findStep,
readActionObject,
} from './workflow-test-utils.mjs';

const ACTION_DIR = 'setup-toolchains';

test('setup-toolchains action forwards optional node cache settings to setup-node', async () => {
const actionObject = await readActionObject(ACTION_DIR);
const steps = extractCompositeActionSteps(actionObject, ACTION_DIR);
const setupNodeStep = findStep(
steps,
'setup-node step',
(step) => (step.uses ?? '').includes('actions/setup-node'),
);

assert.equal(actionObject.inputs['node-cache'].default, '');
assert.equal(actionObject.inputs['node-cache-dependency-path'].default, '');
assert.equal(setupNodeStep.with.cache, '${{ inputs.node-cache }}');
assert.equal(
setupNodeStep.with['cache-dependency-path'],
'${{ inputs.node-cache-dependency-path }}',
);
});
58 changes: 58 additions & 0 deletions scripts/ci/workflow-test-utils.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import assert from 'node:assert/strict';
import path from 'node:path';
import { readFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { parse } from 'yaml';

const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const projectRoot = path.resolve(scriptDir, '..', '..');
const workflowsDir = path.join(projectRoot, '.github', 'workflows');
const actionsDir = path.join(projectRoot, '.github', 'actions');

const readYamlObject = async (targetPath) => {
const content = await readFile(targetPath, 'utf8');
return parse(content);
};

export const readWorkflowObject = async (workflowFileName) => {
const workflowPath = path.join(workflowsDir, workflowFileName);
return readYamlObject(workflowPath);
};

export const readActionObject = async (actionDirName) => {
const actionPath = path.join(actionsDir, actionDirName, 'action.yml');
return readYamlObject(actionPath);
};

export const extractWorkflowJobSteps = (workflowObject, jobName) => {
assert.ok(workflowObject.jobs, 'Expected workflow to define jobs.');
const job = workflowObject.jobs[jobName];
assert.ok(job, `Expected workflow job ${jobName} to exist.`);
assert.ok(Array.isArray(job.steps), `Expected workflow job ${jobName} to define steps.`);
return job.steps;
};

export const findStep = (steps, label, predicate) => {
const step = steps.find(predicate);
assert.ok(step, `Expected workflow step ${String(label)} to exist.`);
return step;
};

export const findStepIndex = (steps, predicate, label) => {
const index = steps.findIndex(predicate);
assert.notEqual(index, -1, `Expected workflow step ${String(label)} to exist.`);
return index;
};

export const extractCompositeActionSteps = (actionObject, actionLabel) => {
assert.equal(
actionObject.runs?.using,
'composite',
`Expected action ${actionLabel} to be a composite action.`,
);
assert.ok(
Array.isArray(actionObject.runs?.steps),
`Expected action ${actionLabel} to define composite steps.`,
);
return actionObject.runs.steps;
};