132 lines · 5.5 KB
import { MARKER_PREFIX } from "./script";
import type { JobExecution } from "./jobs";

const MIN_MASK_LENGTH = 4;
/** Single lines of multi-line secrets are only masked when long enough not to redact ordinary log text. */
const MIN_LINE_MASK_LENGTH = 8;
const BASE64_GROUP_BYTES = 3;

type Marker =
  | { type: "step-start"; index: number }
  | { type: "step-end"; index: number; code: number }
  | { type: "step-skip"; index: number }
  | { type: "artifact"; name: string }
  | { type: "output"; name: string; value: string }
  | { type: "job-end"; failed: boolean }
  | { type: "setup-start" }
  | { type: "setup-end"; code: number };

function decodeBase64(value: string): string {
  try {
    return new TextDecoder().decode(Uint8Array.from(atob(value), (char) => char.charCodeAt(0)));
  } catch {
    return "";
  }
}

/** Markers share stdout with user steps, so only lines carrying the job's secret nonce count. */
export function parseMarker(line: string, nonce: string | null): Marker | null {
  const prefix = nonce === null ? MARKER_PREFIX : `${MARKER_PREFIX}${nonce} `;
  if (!line.startsWith(prefix)) return null;
  const [type, ...args] = line.slice(prefix.length).trim().split(" ");
  const number = (value: string | undefined) => Number(value ?? "0");
  switch (type) {
    case "step-start":
      return { type, index: number(args[0]) };
    case "step-end":
      return { type, index: number(args[0]), code: number(args[1]) };
    case "step-skip":
      return { type, index: number(args[0]) };
    case "artifact":