| | | 1 | | import type { ISecretMasker } from '../../domain/ports/ISecretMasker.js'; |
| | | 2 | | |
| | | 3 | | /** |
| | | 4 | | * Registers values with GitHub Actions log masking through `::add-mask::` |
| | | 5 | | * workflow commands, on stdout unless another stream is given. |
| | | 6 | | * |
| | | 7 | | * Writes to a piped stdout are asynchronous on POSIX, so each write is |
| | | 8 | | * tracked through its callback: `flush()` resolves once every registration |
| | | 9 | | * so far has been written, letting a caller start a process that shares |
| | | 10 | | * stdout only after its values are masked. It rejects as soon as a failed |
| | | 11 | | * write is reached, without waiting for later registrations: the caller must |
| | | 12 | | * not start that process anyway. |
| | | 13 | | */ |
| | | 14 | | export class GitHubActionsSecretMasker implements ISecretMasker { |
| | 23 | 15 | | private pending: Promise<void> = Promise.resolve(); |
| | | 16 | | private readonly stream: NodeJS.WritableStream; |
| | | 17 | | |
| | | 18 | | /** |
| | | 19 | | * @param options.stream - Where workflow commands are written (default: |
| | | 20 | | * stdout). The runner reads them from stdout and stderr; exec mode uses |
| | | 21 | | * stderr so stdout carries only the child's output. |
| | | 22 | | */ |
| | | 23 | | constructor(options: { stream?: NodeJS.WritableStream } = {}) { |
| | 23 | 24 | | this.stream = options.stream ?? process.stdout; |
| | | 25 | | } |
| | | 26 | | |
| | | 27 | | mask(value: string): void { |
| | 20 | 28 | | const escapedValue = value |
| | | 29 | | .replace(/%/g, '%25') |
| | | 30 | | .replace(/\r/g, '%0D') |
| | | 31 | | .replace(/\n/g, '%0A'); |
| | 20 | 32 | | const written = new Promise<void>((resolve, reject) => { |
| | 20 | 33 | | this.stream.write(`::add-mask::${escapedValue}\n`, (error) => { |
| | 18 | 34 | | if (error) { |
| | 7 | 35 | | reject(error); |
| | 7 | 36 | | return; |
| | | 37 | | } |
| | 11 | 38 | | resolve(); |
| | | 39 | | }); |
| | | 40 | | }); |
| | 20 | 41 | | this.pending = this.pending.then(() => written); |
| | | 42 | | // Modes that never flush (pull, the GitHub Action) must not crash on an |
| | | 43 | | // unhandled rejection; flush() still reports the failure. |
| | 20 | 44 | | written.catch(() => undefined); |
| | 20 | 45 | | this.pending.catch(() => undefined); |
| | | 46 | | } |
| | | 47 | | |
| | | 48 | | flush(): Promise<void> { |
| | 10 | 49 | | return this.pending; |
| | | 50 | | } |
| | | 51 | | } |