1 Commits

Author SHA1 Message Date
Solomon
21e6f8327e Add parallel processing of inputs
Support processing multiple inputs in parallel to reduce CI time
2026-01-06 14:41:27 +00:00
4 changed files with 409 additions and 15 deletions

View File

@@ -28,8 +28,12 @@ It is up to the caller to do something with the file/directory operated on.
See [action.yml](action.yml) for the set of inputs. The file should be
self-documenting.
The only output is `output_path`, which holds the filesystem path of the
signed/notarized/stapled entity.
The outputs are:
- `output_path`: filesystem path of the signed/notarized/stapled entity (or the
first path when multiple `input_path` values are provided).
- `output_paths`: newline-separated list of output paths when multiple
`input_path` values are provided.
## Examples
@@ -147,3 +151,21 @@ steps:
app_store_connect_api_issuer: 'abcdef-42-2411312...'
app_store_connect_api_key: 'DEADBEEF'
```
Notarize multiple signed assets in parallel.
```yaml
steps:
# Add steps here to materialize signed assets (.app/.zip/.dmg/etc).
- name: Notarize (parallel)
uses: BloopAI/apple-code-sign-action@v1
with:
input_path: |
MyApp.zip
MyOtherApp.zip
sign: false
notarize: true
notarize_concurrency: 2
app_store_connect_api_key_json_file: app_store_key.json
```

View File

@@ -26,6 +26,10 @@ inputs:
description: 'Whether to notarize'
default: 'false'
notarize_concurrency:
description: 'Max parallel notarization submissions when input_path is multi-line (0 = unlimited)'
default: '0'
# Attaches a pre-issued "notarization ticket" to an entity.
staple:
description: 'Whether to staple a notarization ticket'
@@ -113,7 +117,9 @@ inputs:
outputs:
output_path:
description: 'Path to signed/notarized/stapled entity'
description: 'Path to signed/notarized/stapled entity (first when multiple input_path values)'
output_paths:
description: 'Newline-separated list of output paths when multiple input_path values are provided'
runs:
using: node20

195
dist/index.js generated vendored
View File

@@ -27593,6 +27593,49 @@ const exec = __nccwpck_require__(5236)
const toolCache = __nccwpck_require__(3472)
const os = __nccwpck_require__(857)
async function mapWithConcurrency(items, concurrency, fn) {
if (!Number.isInteger(concurrency) || concurrency < 1) {
throw new Error('concurrency must be a positive integer')
}
const results = new Array(items.length)
let nextIndex = 0
async function worker() {
for (let index = nextIndex++; index < items.length; index = nextIndex++) {
results[index] = await fn(items[index], index)
}
}
const workers = Array.from(
{ length: Math.min(concurrency, items.length) },
() => worker()
)
await Promise.all(workers)
return results
}
async function execRcodesign(rcodesign, args) {
let stdout = ''
let stderr = ''
const exitCode = await exec.exec(rcodesign, args, {
silent: true,
ignoreReturnCode: true,
listeners: {
stdout: data => {
stdout += data.toString()
},
stderr: data => {
stderr += data.toString()
}
}
})
return { exitCode, stdout, stderr }
}
async function getRcodesign(version) {
const platform = os.platform()
const arch = os.arch()
@@ -27658,10 +27701,29 @@ async function getRcodesign(version) {
async function run() {
try {
const inputPath = core.getInput('input_path', { required: true })
const inputPathRaw = core.getInput('input_path', { required: true })
let inputPaths = core.getMultilineInput('input_path')
if (inputPaths.length === 1 && inputPaths[0].includes('\n')) {
inputPaths = inputPaths[0]
.split(/\r?\n/)
.map(line => line.trim())
.filter(Boolean)
}
if (inputPaths.length === 0) {
throw new Error('input_path is required')
}
const hasMultipleInputPaths = inputPaths.length > 1
const inputPath = inputPaths[0] || inputPathRaw.trim()
const outputPath = core.getInput('output_path')
const sign = core.getBooleanInput('sign')
const notarize = core.getBooleanInput('notarize')
const notarizeConcurrencyInput = core.getInput('notarize_concurrency')
const notarizeConcurrency = parseInt(notarizeConcurrencyInput || '0', 10)
if (Number.isNaN(notarizeConcurrency) || notarizeConcurrency < 0) {
throw new Error('notarize_concurrency must be a non-negative integer')
}
const staple = core.getBooleanInput('staple')
const configFiles = core.getMultilineInput('config_file')
const profile = core.getInput('profile')
@@ -27686,9 +27748,22 @@ async function run() {
const rcodesign = await getRcodesign(rcodesignVersion)
let signedPaths = inputPaths
let signedPath = inputPath
if (hasMultipleInputPaths && outputPath) {
throw new Error(
'output_path cannot be used with multiple input_path values'
)
}
if (sign) {
if (hasMultipleInputPaths) {
throw new Error(
'Multiple input_path values are not supported when sign=true'
)
}
const args = ['sign']
for (const path of configFiles) {
@@ -27733,6 +27808,7 @@ async function run() {
}
await exec.exec(rcodesign, args)
signedPaths = [signedPath]
}
let stapled = false
@@ -27766,9 +27842,64 @@ async function run() {
args.push('--wait')
}
args.push(signedPath)
const concurrency =
notarizeConcurrency > 0 ? notarizeConcurrency : signedPaths.length
await exec.exec(rcodesign, args)
core.info(`Submitting ${signedPaths.length} file(s) for notarization`)
const results = await mapWithConcurrency(
signedPaths,
concurrency,
async path => {
core.info(`Starting notarization: ${path}`)
const { exitCode, stdout, stderr } = await execRcodesign(rcodesign, [
...args,
path
])
if (exitCode === 0) {
return { path, ok: true, stdout, stderr }
}
return { path, ok: false, exitCode, stdout, stderr }
}
)
const failures = results.filter(r => !r.ok)
for (const result of results) {
core.startGroup(
result.ok
? `notary-submit: ${result.path}`
: `notary-submit failed: ${result.path}`
)
if (!result.ok) {
core.error(`exit code: ${result.exitCode}`)
}
if (result.stdout.trim()) {
if (result.ok) {
core.info(result.stdout.trim())
} else {
core.error(result.stdout.trim())
}
}
if (result.stderr.trim()) {
if (result.ok) {
core.info(result.stderr.trim())
} else {
core.error(result.stderr.trim())
}
}
core.endGroup()
}
if (failures.length > 0) {
throw new Error(
`Notarization failed for: ${failures.map(f => f.path).join(', ')}`
)
}
if (staple) {
stapled = true
@@ -27782,12 +27913,64 @@ async function run() {
args.push('--config-file', path)
}
args.push(signedPath)
const concurrency =
notarizeConcurrency > 0 ? notarizeConcurrency : signedPaths.length
await exec.exec(rcodesign, args)
const results = await mapWithConcurrency(
signedPaths,
concurrency,
async path => {
core.info(`Stapling notarization ticket: ${path}`)
const { exitCode, stdout, stderr } = await execRcodesign(rcodesign, [
...args,
path
])
if (exitCode === 0) {
return { path, ok: true, stdout, stderr }
}
return { path, ok: false, exitCode, stdout, stderr }
}
)
const failures = results.filter(r => !r.ok)
for (const result of results) {
core.startGroup(
result.ok ? `staple: ${result.path}` : `staple failed: ${result.path}`
)
if (!result.ok) {
core.error(`exit code: ${result.exitCode}`)
}
if (result.stdout.trim()) {
if (result.ok) {
core.info(result.stdout.trim())
} else {
core.error(result.stdout.trim())
}
}
if (result.stderr.trim()) {
if (result.ok) {
core.info(result.stderr.trim())
} else {
core.error(result.stderr.trim())
}
}
core.endGroup()
}
if (failures.length > 0) {
throw new Error(
`Stapling failed for: ${failures.map(f => f.path).join(', ')}`
)
}
}
core.setOutput('output_path', signedPath)
core.setOutput('output_path', signedPaths[0])
core.setOutput('output_paths', signedPaths.join('\n'))
} catch (error) {
core.setFailed(error.message)
}

View File

@@ -3,6 +3,49 @@ const exec = require('@actions/exec')
const toolCache = require('@actions/tool-cache')
const os = require('os')
async function mapWithConcurrency(items, concurrency, fn) {
if (!Number.isInteger(concurrency) || concurrency < 1) {
throw new Error('concurrency must be a positive integer')
}
const results = new Array(items.length)
let nextIndex = 0
async function worker() {
for (let index = nextIndex++; index < items.length; index = nextIndex++) {
results[index] = await fn(items[index], index)
}
}
const workers = Array.from(
{ length: Math.min(concurrency, items.length) },
() => worker()
)
await Promise.all(workers)
return results
}
async function execRcodesign(rcodesign, args) {
let stdout = ''
let stderr = ''
const exitCode = await exec.exec(rcodesign, args, {
silent: true,
ignoreReturnCode: true,
listeners: {
stdout: data => {
stdout += data.toString()
},
stderr: data => {
stderr += data.toString()
}
}
})
return { exitCode, stdout, stderr }
}
async function getRcodesign(version) {
const platform = os.platform()
const arch = os.arch()
@@ -68,10 +111,29 @@ async function getRcodesign(version) {
async function run() {
try {
const inputPath = core.getInput('input_path', { required: true })
const inputPathRaw = core.getInput('input_path', { required: true })
let inputPaths = core.getMultilineInput('input_path')
if (inputPaths.length === 1 && inputPaths[0].includes('\n')) {
inputPaths = inputPaths[0]
.split(/\r?\n/)
.map(line => line.trim())
.filter(Boolean)
}
if (inputPaths.length === 0) {
throw new Error('input_path is required')
}
const hasMultipleInputPaths = inputPaths.length > 1
const inputPath = inputPaths[0] || inputPathRaw.trim()
const outputPath = core.getInput('output_path')
const sign = core.getBooleanInput('sign')
const notarize = core.getBooleanInput('notarize')
const notarizeConcurrencyInput = core.getInput('notarize_concurrency')
const notarizeConcurrency = parseInt(notarizeConcurrencyInput || '0', 10)
if (Number.isNaN(notarizeConcurrency) || notarizeConcurrency < 0) {
throw new Error('notarize_concurrency must be a non-negative integer')
}
const staple = core.getBooleanInput('staple')
const configFiles = core.getMultilineInput('config_file')
const profile = core.getInput('profile')
@@ -96,9 +158,22 @@ async function run() {
const rcodesign = await getRcodesign(rcodesignVersion)
let signedPaths = inputPaths
let signedPath = inputPath
if (hasMultipleInputPaths && outputPath) {
throw new Error(
'output_path cannot be used with multiple input_path values'
)
}
if (sign) {
if (hasMultipleInputPaths) {
throw new Error(
'Multiple input_path values are not supported when sign=true'
)
}
const args = ['sign']
for (const path of configFiles) {
@@ -143,6 +218,7 @@ async function run() {
}
await exec.exec(rcodesign, args)
signedPaths = [signedPath]
}
let stapled = false
@@ -176,9 +252,64 @@ async function run() {
args.push('--wait')
}
args.push(signedPath)
const concurrency =
notarizeConcurrency > 0 ? notarizeConcurrency : signedPaths.length
await exec.exec(rcodesign, args)
core.info(`Submitting ${signedPaths.length} file(s) for notarization`)
const results = await mapWithConcurrency(
signedPaths,
concurrency,
async path => {
core.info(`Starting notarization: ${path}`)
const { exitCode, stdout, stderr } = await execRcodesign(rcodesign, [
...args,
path
])
if (exitCode === 0) {
return { path, ok: true, stdout, stderr }
}
return { path, ok: false, exitCode, stdout, stderr }
}
)
const failures = results.filter(r => !r.ok)
for (const result of results) {
core.startGroup(
result.ok
? `notary-submit: ${result.path}`
: `notary-submit failed: ${result.path}`
)
if (!result.ok) {
core.error(`exit code: ${result.exitCode}`)
}
if (result.stdout.trim()) {
if (result.ok) {
core.info(result.stdout.trim())
} else {
core.error(result.stdout.trim())
}
}
if (result.stderr.trim()) {
if (result.ok) {
core.info(result.stderr.trim())
} else {
core.error(result.stderr.trim())
}
}
core.endGroup()
}
if (failures.length > 0) {
throw new Error(
`Notarization failed for: ${failures.map(f => f.path).join(', ')}`
)
}
if (staple) {
stapled = true
@@ -192,12 +323,64 @@ async function run() {
args.push('--config-file', path)
}
args.push(signedPath)
const concurrency =
notarizeConcurrency > 0 ? notarizeConcurrency : signedPaths.length
await exec.exec(rcodesign, args)
const results = await mapWithConcurrency(
signedPaths,
concurrency,
async path => {
core.info(`Stapling notarization ticket: ${path}`)
const { exitCode, stdout, stderr } = await execRcodesign(rcodesign, [
...args,
path
])
if (exitCode === 0) {
return { path, ok: true, stdout, stderr }
}
return { path, ok: false, exitCode, stdout, stderr }
}
)
const failures = results.filter(r => !r.ok)
for (const result of results) {
core.startGroup(
result.ok ? `staple: ${result.path}` : `staple failed: ${result.path}`
)
if (!result.ok) {
core.error(`exit code: ${result.exitCode}`)
}
if (result.stdout.trim()) {
if (result.ok) {
core.info(result.stdout.trim())
} else {
core.error(result.stdout.trim())
}
}
if (result.stderr.trim()) {
if (result.ok) {
core.info(result.stderr.trim())
} else {
core.error(result.stderr.trim())
}
}
core.endGroup()
}
if (failures.length > 0) {
throw new Error(
`Stapling failed for: ${failures.map(f => f.path).join(', ')}`
)
}
}
core.setOutput('output_path', signedPath)
core.setOutput('output_path', signedPaths[0])
core.setOutput('output_paths', signedPaths.join('\n'))
} catch (error) {
core.setFailed(error.message)
}