Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
claude/adoring-hopper-5x74bqclaude/affectionate-feynman-ykrf1hclaude/architecture-audit-design-wxprenclaude/build-status-update-3MXsfclaude/charming-meitner-mllb5rclaude/compare-gate-gluecron-s4mFQclaude/confident-faraday-tikcwbclaude/continue-work-XMTlIclaude/crontech-gluecron-deploy-7MIECclaude/crontech-platform-setup-SeKfwclaude/design-2026claude/ecstatic-ptolemy-jMdigclaude/enhance-github-integration-QNHdGclaude/fix-aa-loop-issue-PonMQclaude/fix-actions-and-processclaude/fix-desktop-errors-XqoW8claude/fix-red-workflowsclaude/fix-website-access-6FKJNclaude/gatetest-integration-hardeningclaude/github-audit-improvements-bDFr9claude/gluecron-launch-status-FoMRlclaude/hopeful-lamport-olfCTclaude/issue-to-pr-and-protectionsclaude/jolly-heisenberg-2sg1Qclaude/launch-preparation-QmTb6claude/new-session-xk1l7claude/plan-platform-architecture-kkN4yclaude/platform-analysis-roadmap-1nUGLclaude/platform-launch-assessment-8dWV8claude/polish-platform-release-AeDrUclaude/resume-previous-work-KzyLwclaude/review-crontech-handoff-qYEVqclaude/review-project-completeness-lHhS2claude/review-readme-docs-ulqPKclaude/serene-edison-rj87weclaude/setup-multi-repo-dev-BCwNQclaude/ship-fixes-and-tests-Jvz1cclaude/site-audit-competitive-pctlwgclaude/site-migration-vercel-XstpKclaude/standalone-product-repos-XHFTDcopilot/feat-smart-empty-states-keyboard-first-enhancementcopilot/feat-smart-morning-digest-review-context-restorecopilot/fix-and-process-workflowscopilot/update-ai-powered-code-reviewfeat/debt-mapfeat/push-policy-codeowners-hardeningfeat/smart-digest-contextfeat/stage-impactfeat/t1-secret-migrationfeat/u-polishfeat/w-self-hostfeat/w2-claude-configfix/agent-journey-orphan-sweepfix/audit-sweep-2026-07-26gatetest/auto-fix-1776586424172gatetest/auto-fix-1776586534814gatetest/auto-fix-1776590685143gatetest/auto-fix-1776590808199mainops/redeploy-retriggerstyle/dxt-cta-themeworktree-agent-a3377aad30d55da26worktree-agent-a7ef607b7ee1d6c74
self-deploy-xtrace.test.ts5.7 KB · 148 lines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
/**
 * Regression guard: self-deploy.sh must not trace secrets into its log.
 *
 * The script runs under `set -Eeuxo pipefail`. The `-x` is deliberate — the
 * header explains it was added after 17 hours of blind Hetzner deploy
 * failures — and its own comment states the trace is captured into $LOG by
 * the detached re-exec's `>>"$LOG" 2>&1`.
 *
 * With xtrace active, bash traces every assignment performed by a sourced
 * file. So `set -a; source /etc/gluecron.env` wrote the whole env file to
 * stderr, and on the live box that file holds GOOGLE_OAUTH_CLIENT_SECRET
 * while the log sits at mode 0644 — world-readable. The four curl calls that
 * pass `Bearer $DEPLOY_EVENT_TOKEN` on the command line traced the token the
 * same way, two of them redirecting straight into $LOG.
 *
 * Verified read-only on the box that NO secrets had actually leaked: the log
 * contained zero xtrace lines and zero matches for any credential pattern.
 * The hazard was live in the code, the exposure had not occurred.
 *
 * Also verified: the script is NOT currently reachable. post-receive.ts only
 * dispatches when process.env.SELF_HOST_REPO matches, and that variable is
 * absent from the container the app runs in; no systemd unit invokes it
 * either. It is kept rather than deleted because admin-diagnose and the
 * runbook still present it as the self-host path — and a dormant script is
 * the one most likely to be switched back on without a fresh review, which
 * is precisely why the holes were worth closing.
 */

import { describe, it, expect } from "bun:test";
import { readFileSync } from "node:fs";
import { join } from "node:path";

const src = (() => {
  const raw = readFileSync(
    join(import.meta.dir, "..", "..", "scripts", "self-deploy.sh"),
    "utf8"
  );
  expect(raw.length).toBeGreaterThan(0);
  return raw;
})();

/** Slice by anchor, never by character count. */
function slice(startAnchor: string, endAnchor: string): string {
  const start = src.indexOf(startAnchor);
  expect(start).toBeGreaterThan(-1);
  const end = src.indexOf(endAnchor, start + startAnchor.length);
  expect(end).toBeGreaterThan(start);
  const body = src.slice(start, end);
  expect(body.length).toBeGreaterThan(0);
  return body;
}

describe("self-deploy.sh keeps xtrace on for debuggability", () => {
  it("still traces by default — the fix must not blind the deploy", () => {
    // Removing -x entirely would "fix" the leak by discarding the diagnostic
    // the script exists to provide. The fix is scoped suppression, not that.
    expect(src).toContain("set -Eeuxo pipefail");
  });
});

describe("secrets are not traced", () => {
  it("disables xtrace across the env-file source", () => {
    const body = slice('if [ -f "$ENV_FILE" ]; then', "log \"    v sourced");
    const offAt = body.indexOf("set +x");
    const sourceAt = body.indexOf('source "$ENV_FILE"');
    expect(offAt).toBeGreaterThan(-1);
    expect(sourceAt).toBeGreaterThan(-1);
    expect(offAt).toBeLessThan(sourceAt);
  });

  it("re-enables xtrace after the source, so later steps stay traced", () => {
    const body = slice('if [ -f "$ENV_FILE" ]; then', "log \"    v sourced");
    const sourceAt = body.indexOf('source "$ENV_FILE"');
    const onAt = body.lastIndexOf("set -x");
    expect(onAt).toBeGreaterThan(sourceAt);
  });

  it("has xtrace OFF at every bearer-token curl", () => {
    // Track the flag rather than peeking at a fixed window: one `set +x` can
    // legitimately cover several call sites (the finished-notify guards both
    // its success and failure branches), and a window scan wrongly flags
    // that. Walk the file and assert the state at each token line.
    const lines = src.split("\n");
    let tracing = false;
    let tokenLines = 0;

    for (const line of lines) {
      const t = line.trim();
      if (t === "set -Eeuxo pipefail" || t === "set -x") tracing = true;
      else if (t === "set +x") tracing = false;

      if (line.includes("Bearer $DEPLOY_EVENT_TOKEN")) {
        tokenLines++;
        expect(tracing).toBe(false);
      }
    }

    // Guards against the loop passing vacuously if the calls are renamed.
    expect(tokenLines).toBeGreaterThanOrEqual(4);
  });

  it("has xtrace OFF at the env-file source", () => {
    const lines = src.split("\n");
    let tracing = false;
    let sourceSeen = 0;

    for (const line of lines) {
      const t = line.trim();
      if (t === "set -Eeuxo pipefail" || t === "set -x") tracing = true;
      else if (t === "set +x") tracing = false;

      if (line.includes('source "$ENV_FILE"')) {
        sourceSeen++;
        expect(tracing).toBe(false);
      }
    }
    expect(sourceSeen).toBe(1);
  });

  it("balances every suppression with a re-enable", () => {
    const off = (src.match(/^\s*set \+x\s*$/gm) || []).length;
    const on = (src.match(/^\s*set -x\s*$/gm) || []).length;
    expect(off).toBeGreaterThan(0);
    expect(on).toBe(off);
  });
});

describe("the trace destination is not world-readable", () => {
  it("creates the log 0600 before anything writes to it", () => {
    expect(src).toContain("umask 077");
    expect(src).toContain('chmod 600 "$LOG"');
    // Must happen before the first log() call, or the first write creates
    // the file with the default mode.
    const chmodAt = src.indexOf('chmod 600 "$LOG"');
    const firstLogAt = src.indexOf('log "==> gluecron self-deploy starting');
    expect(chmodAt).toBeGreaterThan(-1);
    expect(firstLogAt).toBeGreaterThan(chmodAt);
  });
});

describe("reachability is documented", () => {
  it("records that the hook does not currently fire this script", () => {
    // A dormant deploy script that reads as live is how it gets re-armed
    // without review.
    expect(src).toContain("SELF_HOST_REPO");
    expect(src).toContain("auto-update.sh");
  });
});