fix(staging): rotate SSH secret handling and env ownership
This commit is contained in:
@@ -182,7 +182,7 @@ jobs:
|
||||
|
||||
- name: Deploy exact image digests under pinned SSH identity
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.STAGING_SSH_PRIVATE_KEY }}
|
||||
SSH_PRIVATE_KEY_BASE64: ${{ secrets.STAGING_SSH_PRIVATE_KEY }}
|
||||
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
|
||||
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
DEPLOY_SHA: ${{ steps.revision.outputs.sha }}
|
||||
@@ -196,9 +196,11 @@ jobs:
|
||||
known_hosts_path="$ssh_root/known_hosts"
|
||||
incoming=""
|
||||
install -m 700 -d "$ssh_root"
|
||||
printf '%s\n' "$SSH_PRIVATE_KEY" | tr -d '\r' > "$key_path"
|
||||
test -n "$SSH_PRIVATE_KEY_BASE64"
|
||||
printf '%s' "$SSH_PRIVATE_KEY_BASE64" | base64 --decode > "$key_path"
|
||||
printf '%s\n' "$STAGING_KNOWN_HOSTS" | tr -d '\r' > "$known_hosts_path"
|
||||
chmod 600 "$key_path" "$known_hosts_path"
|
||||
ssh-keygen -y -f "$key_path" >/dev/null
|
||||
ssh_options=(-i "$key_path" -p "$DEPLOY_PORT" -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes -o "UserKnownHostsFile=$known_hosts_path")
|
||||
remote="$DEPLOY_USER@$DEPLOY_HOST"
|
||||
require_current_staging_head() {
|
||||
|
||||
@@ -154,7 +154,7 @@ jobs:
|
||||
|
||||
- name: Apply digest-pinned migration under host lock
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.STAGING_SSH_PRIVATE_KEY }}
|
||||
SSH_PRIVATE_KEY_BASE64: ${{ secrets.STAGING_SSH_PRIVATE_KEY }}
|
||||
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
|
||||
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
DEPLOY_SHA: ${{ steps.revision.outputs.sha }}
|
||||
@@ -166,9 +166,11 @@ jobs:
|
||||
known_hosts_path="$ssh_root/known_hosts"
|
||||
incoming=""
|
||||
install -m 700 -d "$ssh_root"
|
||||
printf '%s\n' "$SSH_PRIVATE_KEY" | tr -d '\r' > "$key_path"
|
||||
test -n "$SSH_PRIVATE_KEY_BASE64"
|
||||
printf '%s' "$SSH_PRIVATE_KEY_BASE64" | base64 --decode > "$key_path"
|
||||
printf '%s\n' "$STAGING_KNOWN_HOSTS" | tr -d '\r' > "$known_hosts_path"
|
||||
chmod 600 "$key_path" "$known_hosts_path"
|
||||
ssh-keygen -y -f "$key_path" >/dev/null
|
||||
ssh_options=(-i "$key_path" -p "$DEPLOY_PORT" -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes -o "UserKnownHostsFile=$known_hosts_path")
|
||||
remote="$DEPLOY_USER@$DEPLOY_HOST"
|
||||
require_current_staging_head() {
|
||||
|
||||
@@ -73,13 +73,14 @@ jobs:
|
||||
|
||||
- name: Configure pinned staging SSH
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.STAGING_SSH_PRIVATE_KEY }}
|
||||
SSH_PRIVATE_KEY_BASE64: ${{ secrets.STAGING_SSH_PRIVATE_KEY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
test -n "$SSH_PRIVATE_KEY"
|
||||
test -n "$SSH_PRIVATE_KEY_BASE64"
|
||||
install -d -m 700 ~/.ssh
|
||||
printf '%s\n' "$SSH_PRIVATE_KEY" >~/.ssh/jyotisha-staging
|
||||
printf '%s' "$SSH_PRIVATE_KEY_BASE64" | base64 --decode >~/.ssh/jyotisha-staging
|
||||
chmod 600 ~/.ssh/jyotisha-staging
|
||||
ssh-keygen -y -f ~/.ssh/jyotisha-staging >/dev/null
|
||||
printf '%s\n' "$STAGING_KNOWN_HOSTS" >~/.ssh/known_hosts
|
||||
chmod 600 ~/.ssh/known_hosts
|
||||
|
||||
|
||||
@@ -154,13 +154,14 @@ jobs:
|
||||
|
||||
- name: Configure pinned staging SSH
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.STAGING_SSH_PRIVATE_KEY }}
|
||||
SSH_PRIVATE_KEY_BASE64: ${{ secrets.STAGING_SSH_PRIVATE_KEY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
test -n "$SSH_PRIVATE_KEY"
|
||||
test -n "$SSH_PRIVATE_KEY_BASE64"
|
||||
install -m 700 -d ~/.ssh
|
||||
printf '%s\n' "$SSH_PRIVATE_KEY" >~/.ssh/jyotisha-staging
|
||||
printf '%s' "$SSH_PRIVATE_KEY_BASE64" | base64 --decode >~/.ssh/jyotisha-staging
|
||||
chmod 600 ~/.ssh/jyotisha-staging
|
||||
ssh-keygen -y -f ~/.ssh/jyotisha-staging >/dev/null
|
||||
printf '%s\n' "$STAGING_KNOWN_HOSTS" >~/.ssh/known_hosts
|
||||
chmod 600 ~/.ssh/known_hosts
|
||||
|
||||
|
||||
@@ -116,13 +116,14 @@ jobs:
|
||||
|
||||
- name: Configure pinned staging SSH
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.STAGING_SSH_PRIVATE_KEY }}
|
||||
SSH_PRIVATE_KEY_BASE64: ${{ secrets.STAGING_SSH_PRIVATE_KEY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
test -n "$SSH_PRIVATE_KEY"
|
||||
test -n "$SSH_PRIVATE_KEY_BASE64"
|
||||
install -m 700 -d ~/.ssh
|
||||
printf '%s\n' "$SSH_PRIVATE_KEY" >~/.ssh/jyotisha-staging
|
||||
printf '%s' "$SSH_PRIVATE_KEY_BASE64" | base64 --decode >~/.ssh/jyotisha-staging
|
||||
chmod 600 ~/.ssh/jyotisha-staging
|
||||
ssh-keygen -y -f ~/.ssh/jyotisha-staging >/dev/null
|
||||
printf '%s\n' "$STAGING_KNOWN_HOSTS" >~/.ssh/known_hosts
|
||||
chmod 600 ~/.ssh/known_hosts
|
||||
|
||||
|
||||
@@ -62,13 +62,14 @@ jobs:
|
||||
|
||||
- name: Configure pinned staging SSH
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.STAGING_SSH_PRIVATE_KEY }}
|
||||
SSH_PRIVATE_KEY_BASE64: ${{ secrets.STAGING_SSH_PRIVATE_KEY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
test -n "$SSH_PRIVATE_KEY"
|
||||
test -n "$SSH_PRIVATE_KEY_BASE64"
|
||||
install -d -m 700 ~/.ssh
|
||||
printf '%s\n' "$SSH_PRIVATE_KEY" >~/.ssh/jyotisha-staging
|
||||
printf '%s' "$SSH_PRIVATE_KEY_BASE64" | base64 --decode >~/.ssh/jyotisha-staging
|
||||
chmod 600 ~/.ssh/jyotisha-staging
|
||||
ssh-keygen -y -f ~/.ssh/jyotisha-staging >/dev/null
|
||||
printf '%s\n' "$STAGING_KNOWN_HOSTS" >~/.ssh/known_hosts
|
||||
chmod 600 ~/.ssh/known_hosts
|
||||
|
||||
|
||||
+1
-1
@@ -175,7 +175,7 @@ Staging is isolated from production:
|
||||
| Identity | Better Auth + Resend OTP on the same private PostgreSQL cluster |
|
||||
| Actions control plane | Gitea 1.26.2 (`git.copse.top`) |
|
||||
|
||||
Gitea is the primary source repository and Actions control plane. Gitea automatically injects the per-job `${{ secrets.GITEA_TOKEN }}` token; its access is limited by each workflow's `permissions` block and it must not be configured as a repository secret. Configure repository Actions secrets `REGISTRY_USERNAME`, `REGISTRY_PASSWORD`, and `STAGING_SSH_PRIVATE_KEY`, plus variables `STAGING_HOST`, `STAGING_PORT`, `STAGING_USER`, `STAGING_PATH`, `STAGING_URL`, and `STAGING_KNOWN_HOSTS`. The `workflow_run` controller is loaded from the default `main` branch while separately requiring the successfully tested upstream branch to be `staging`. The controller checks out only `main` with full history, requires the requested staging SHA to be an ancestor of that reviewed history, and uploads only the allowlisted `deploy/` control files. It never executes deployment validators or remote orchestration scripts from the target/rollback revision. The staging key, database, Resend key, and model-provider keys must not be shared with production. Staging image publishing has no Supabase build variables. GitHub workflows are upstream/mirror fallback only, not the normal staging release path.
|
||||
Gitea is the primary source repository and Actions control plane. Gitea automatically injects the per-job `${{ secrets.GITEA_TOKEN }}` token; its access is limited by each workflow's `permissions` block and it must not be configured as a repository secret. Configure repository Actions secrets `REGISTRY_USERNAME`, `REGISTRY_PASSWORD`, and `STAGING_SSH_PRIVATE_KEY`, plus variables `STAGING_HOST`, `STAGING_PORT`, `STAGING_USER`, `STAGING_PATH`, `STAGING_URL`, and `STAGING_KNOWN_HOSTS`. `STAGING_SSH_PRIVATE_KEY` must be the private-key file encoded as one unwrapped base64 line (for example, `base64 < key | tr -d '\n'`), not a multiline PEM/OpenSSH value; staging workflows decode it only into a mode-`0600` temporary file and validate it with `ssh-keygen`. The `workflow_run` controller is loaded from the default `main` branch while separately requiring the successfully tested upstream branch to be `staging`. The controller checks out only `main` with full history, requires the requested staging SHA to be an ancestor of that reviewed history, and uploads only the allowlisted `deploy/` control files. It never executes deployment validators or remote orchestration scripts from the target/rollback revision. The staging key, database, Resend key, and model-provider keys must not be shared with production. Staging image publishing has no Supabase build variables. GitHub workflows are upstream/mirror fallback only, not the normal staging release path.
|
||||
|
||||
`Staging Backend Quality Gate` runs for relevant `pull_request` paths, pushes to `staging`, and `workflow_dispatch`. It validates the Python/database/frontend contract; only a successful push to `staging` publishes the API/web images and a run-bound manifest containing their `sha256` digests. `.gitea/workflows/deploy-staging.yml` consumes that exact successful run, validates its manifest against the full 40-character commit, and deploys digest references rather than trusting the discoverability tags.
|
||||
|
||||
|
||||
@@ -56,10 +56,18 @@ compose_files=(
|
||||
-f deploy/docker-compose.staging.yml
|
||||
)
|
||||
|
||||
[ -f "$env_file" ] || {
|
||||
echo "staging environment file is missing" >&2
|
||||
[ -f "$env_file" ] && [ ! -L "$env_file" ] || {
|
||||
echo "staging environment file is missing or unsafe" >&2
|
||||
exit 1
|
||||
}
|
||||
EXPECTED_STAGING_ENV_OWNER_UID="$(stat -c '%u' "$DEPLOY_PATH" 2>/dev/null || stat -f '%u' "$DEPLOY_PATH")"
|
||||
EXPECTED_STAGING_ENV_OWNER_GID="$(stat -c '%g' "$DEPLOY_PATH" 2>/dev/null || stat -f '%g' "$DEPLOY_PATH")"
|
||||
[[ "$EXPECTED_STAGING_ENV_OWNER_UID" =~ ^[0-9]+$ && "$EXPECTED_STAGING_ENV_OWNER_GID" =~ ^[0-9]+$ ]] || {
|
||||
echo "staging deployment owner is invalid" >&2
|
||||
exit 1
|
||||
}
|
||||
export EXPECTED_STAGING_ENV_OWNER_UID
|
||||
bash "$DEPLOY_PATH/deploy/validate-staging-env.sh" "$env_file"
|
||||
current_sha="$(<"$state_directory/deployed-revision")"
|
||||
[ "$current_sha" = "$EXPECTED_DEPLOY_SHA" ] || {
|
||||
echo "deployed staging revision does not match the approved rollout SHA" >&2
|
||||
@@ -127,6 +135,7 @@ END {
|
||||
for (key in values) if (!(key in written)) print key "=" values[key]
|
||||
}
|
||||
' "$env_file" >"$temporary"
|
||||
chown "$EXPECTED_STAGING_ENV_OWNER_UID:$EXPECTED_STAGING_ENV_OWNER_GID" "$temporary"
|
||||
chmod 600 "$temporary"
|
||||
|
||||
cd "$DEPLOY_PATH"
|
||||
|
||||
@@ -120,6 +120,12 @@ bash "$INCOMING_PATH/deploy/sync-staging-tree.sh" \
|
||||
"$INCOMING_PATH" "$DEPLOY_PATH"
|
||||
|
||||
cd "$DEPLOY_PATH"
|
||||
EXPECTED_STAGING_ENV_OWNER_UID="$(stat -c '%u' "$DEPLOY_PATH" 2>/dev/null || stat -f '%u' "$DEPLOY_PATH")"
|
||||
[[ "$EXPECTED_STAGING_ENV_OWNER_UID" =~ ^[0-9]+$ ]] || {
|
||||
echo "staging deployment owner is invalid" >&2
|
||||
exit 1
|
||||
}
|
||||
export EXPECTED_STAGING_ENV_OWNER_UID
|
||||
bash deploy/validate-staging-env.sh \
|
||||
.env.staging staging.jyotisha.chat deploy/Caddyfile.staging
|
||||
bash deploy/validate-staging-database-env.sh .env.staging.database
|
||||
|
||||
@@ -72,6 +72,12 @@ bash "$INCOMING_PATH/deploy/sync-staging-tree.sh" \
|
||||
"$INCOMING_PATH" "$DEPLOY_PATH"
|
||||
|
||||
cd "$DEPLOY_PATH"
|
||||
EXPECTED_STAGING_ENV_OWNER_UID="$(stat -c '%u' "$DEPLOY_PATH" 2>/dev/null || stat -f '%u' "$DEPLOY_PATH")"
|
||||
[[ "$EXPECTED_STAGING_ENV_OWNER_UID" =~ ^[0-9]+$ ]] || {
|
||||
echo "staging deployment owner is invalid" >&2
|
||||
exit 1
|
||||
}
|
||||
export EXPECTED_STAGING_ENV_OWNER_UID
|
||||
bash deploy/validate-staging-env.sh \
|
||||
.env.staging staging.jyotisha.chat deploy/Caddyfile.staging
|
||||
bash deploy/validate-staging-database-env.sh .env.staging.database
|
||||
|
||||
@@ -35,9 +35,9 @@ if OWNER="$(stat -c '%u' "$ENV_FILE" 2>/dev/null)"; then
|
||||
else
|
||||
OWNER="$(stat -f '%u' "$ENV_FILE")"
|
||||
fi
|
||||
|
||||
if [ "$OWNER" != "$(id -u)" ]; then
|
||||
echo "staging database environment file must be owned by the current user" >&2
|
||||
EXPECTED_OWNER_UID="${EXPECTED_STAGING_ENV_OWNER_UID:-$(id -u)}"
|
||||
if [[ ! "$EXPECTED_OWNER_UID" =~ ^[0-9]+$ ]] || [ "$OWNER" != "$EXPECTED_OWNER_UID" ]; then
|
||||
echo "staging database environment file has an invalid owner" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -24,6 +24,17 @@ if [ "$MODE" != "600" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if OWNER="$(stat -c '%u' "$ENV_FILE" 2>/dev/null)"; then
|
||||
:
|
||||
else
|
||||
OWNER="$(stat -f '%u' "$ENV_FILE")"
|
||||
fi
|
||||
EXPECTED_OWNER_UID="${EXPECTED_STAGING_ENV_OWNER_UID:-$(id -u)}"
|
||||
if [[ ! "$EXPECTED_OWNER_UID" =~ ^[0-9]+$ ]] || [ "$OWNER" != "$EXPECTED_OWNER_UID" ]; then
|
||||
echo "staging environment file has an invalid owner" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
require_selector() {
|
||||
local key="$1"
|
||||
local expected="$2"
|
||||
|
||||
@@ -2208,3 +2208,19 @@
|
||||
- 相关记录:BUG-126
|
||||
- 复发自:无
|
||||
- 修复版本:本次个人报告 staging 发布提交
|
||||
|
||||
## BUG-128 | staging deploy 泄漏多行 SSH secret 且 env owner 契约互相冲突
|
||||
|
||||
- 状态:resolved(local,pending quality-gate rerun)
|
||||
- 首次发现:2026-08-06
|
||||
- 最近更新:2026-08-06
|
||||
- 影响面:Gitea/GitHub staging deploy 与 migration workflow、staging SSH 凭据、`.env.staging*` owner、加密备份和发布门禁;production 未受影响。
|
||||
- 用户现象:exact-SHA 自动 deploy run `1464` 在应用切换前失败;Gitea job 日志把多行 staging SSH 私钥逐行显示,同时远端数据库 env validator 报 owner 不匹配。公网仍运行旧 SHA。
|
||||
- 触发条件:Gitea workflow 将多行 OpenSSH key 直接放入 step env;root 控制脚本验证一个由 `deploy` 持有的 mode-0600 env;此前 root rollout 临时文件又通过 `mv` 把 env owner 改成 root。
|
||||
- 根因:Gitea runner 不能可靠遮蔽多行 secret 的每一行;控制面同时混用了“当前脚本用户”和“部署树 owner”作为 env ownership 事实,rollout 覆盖文件时未保留原 owner/gid。
|
||||
- 修复:立即停止发布,生成并验证新 staging ED25519 key,精确撤销旧 authorized key,证明旧 key 无法登录,删除本地旧 key,更新 Gitea/GitHub staging secrets,并删除 28 个可能含旧 key 的 Gitea deploy/migration runs。`STAGING_SSH_PRIVATE_KEY` 改为单行 base64;所有 staging workflow 解码到 0600 临时文件并用 `ssh-keygen` 验证。deploy/migration 以部署树 UID 校验两个 env;backup helper 继续以 `deploy` 运行;rollout 临时文件显式保留部署树 owner/gid。
|
||||
- 验证:新 key 严格主机校验登录成功,旧 key 登录失败;新 Gitea/GitHub secrets 已更新;泄漏 run `1464` 已删除;focused workflow、owner、shell/YAML 和 governance tests 待本次提交执行,完整 staging gate/deploy 待新 exact SHA。
|
||||
- 防复发:禁止 staging workflow 直接注入多行私钥或打印 decoded secret 变量;env owner 必须由部署树身份决定,root 受控脚本不得用 root 临时文件改变持久 env owner。任何凭据日志暴露先轮换/撤销/清理,再修代码和重跑。
|
||||
- 相关记录:BUG-124、BUG-127、ERR-092、ERR-093、ERR-094
|
||||
- 复发自:无
|
||||
- 修复版本:待安全修复 PR 与 staging 验收
|
||||
|
||||
@@ -139,6 +139,12 @@ Before the personal-report staging migration, the encrypted backup helper correc
|
||||
|
||||
Prevention: before staging backup/migration, verify both env files are regular, non-symlink files owned by `deploy:deploy` with mode `0600`; restore only that documented owner/mode under the shared mutation lock, without printing or copying file contents. Run `backup-staging-postgres.sh` as `deploy`, never weaken its ancestor checks, never create a parallel root backup tree, and never broaden env permissions.
|
||||
|
||||
## ERR-094 | Gitea expanded a multiline staging SSH secret in failed workflow logs | mitigated 2026-08-06
|
||||
|
||||
A failed exact-SHA staging deploy displayed the multiline staging SSH private key in the job environment block instead of masking each line. Release mutations were stopped immediately. The staging-only key was rotated, the new key was verified before the exposed key was removed from `authorized_keys`, the old key was proven unable to authenticate and deleted locally, Gitea and GitHub staging secrets were replaced, and 28 potentially affected Gitea deploy/migration runs were deleted. The successful quality-gate run and immutable image manifest were retained because they never received the SSH secret.
|
||||
|
||||
Prevention: store `STAGING_SSH_PRIVATE_KEY` only as one unwrapped base64 line; workflows decode it into a mode-`0600` temporary key, validate it with `ssh-keygen`, and delete the temporary directory on every exit. Contract tests must reject direct multiline `SSH_PRIVATE_KEY` injection or `printf` of a decoded secret variable. A leaked staging key must be rotated and revoked before any rerun; production keys remain a separate boundary and were not involved in this incident.
|
||||
|
||||
## Fragment Sweep Command Set
|
||||
|
||||
## ERR-086 | Steve Jobs jyotishganit artifacts used non-San-Francisco coordinates | mitigated 2026-07-21
|
||||
|
||||
@@ -192,6 +192,37 @@ test("database env validator rejects symlinks and unsafe modes", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("database env validator enforces an explicit staging owner uid without printing values", () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "jyotisha-database-env-owner-"));
|
||||
const envFile = join(root, ".env.staging.database");
|
||||
|
||||
try {
|
||||
writeFileSync(envFile, `${validEnvironment.join("\n")}\n`, { mode: 0o600 });
|
||||
chmodSync(envFile, 0o600);
|
||||
const currentUid = process.getuid?.();
|
||||
assert.equal(typeof currentUid, "number");
|
||||
|
||||
const accepted = spawnSync("bash", [validator, envFile], {
|
||||
encoding: "utf8",
|
||||
env: { ...process.env, EXPECTED_STAGING_ENV_OWNER_UID: String(currentUid) },
|
||||
});
|
||||
assert.equal(accepted.status, 0, accepted.stderr);
|
||||
|
||||
const rejected = spawnSync("bash", [validator, envFile], {
|
||||
encoding: "utf8",
|
||||
env: { ...process.env, EXPECTED_STAGING_ENV_OWNER_UID: String((currentUid ?? 0) + 1) },
|
||||
});
|
||||
assert.notEqual(rejected.status, 0);
|
||||
assert.match(rejected.stderr, /invalid owner/);
|
||||
assert.doesNotMatch(
|
||||
`${rejected.stdout}${rejected.stderr}`,
|
||||
/postgres-test-password|schema-owner-test-password|staging-backup-test-password/,
|
||||
);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("database env validator accepts a private valid file without printing values", () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "jyotisha-database-env-"));
|
||||
const envFile = join(root, ".env.staging.database");
|
||||
|
||||
@@ -46,6 +46,10 @@ const rolloutWorkflow = new URL(
|
||||
"../../.github/workflows/configure-staging-rectification-rollout.yml",
|
||||
import.meta.url,
|
||||
);
|
||||
const resetStagingAccountWorkflow = new URL(
|
||||
"../../.github/workflows/reset-staging-account.yml",
|
||||
import.meta.url,
|
||||
);
|
||||
const deployScript = new URL(
|
||||
"../../deploy/run-staging-deploy.sh",
|
||||
import.meta.url,
|
||||
@@ -259,6 +263,40 @@ test("Gitea staging mutation workflows use the available runner and pinned Node
|
||||
}
|
||||
});
|
||||
|
||||
test("staging SSH secrets are single-line base64 and never injected as multiline private keys", () => {
|
||||
for (const workflow of [
|
||||
read(giteaDeployWorkflow),
|
||||
read(giteaMigrationWorkflow),
|
||||
read(deployWorkflow),
|
||||
read(migrationWorkflow),
|
||||
read(rolloutWorkflow),
|
||||
read(resetStagingAccountWorkflow),
|
||||
]) {
|
||||
assert.match(workflow, /SSH_PRIVATE_KEY_BASE64: \$\{\{ secrets\.STAGING_SSH_PRIVATE_KEY \}\}/);
|
||||
assert.match(workflow, /printf '%s' "\$SSH_PRIVATE_KEY_BASE64" \| base64 --decode/);
|
||||
assert.match(workflow, /chmod 600 [^\n]*(?:\$key_path|jyotisha-staging)/);
|
||||
assert.match(workflow, /ssh-keygen -y -f [^\n]+>\/dev\/null/);
|
||||
assert.doesNotMatch(workflow, /\n\s+SSH_PRIVATE_KEY: \$\{\{ secrets\.STAGING_SSH_PRIVATE_KEY \}\}/);
|
||||
assert.doesNotMatch(workflow, /printf '%s\\n' "\$SSH_PRIVATE_KEY"/);
|
||||
}
|
||||
});
|
||||
|
||||
test("staging scripts validate deploy-owned env files and rollout preserves their owner", () => {
|
||||
const deployRunner = read(deployScript);
|
||||
const migrationRunner = read(migrationScript);
|
||||
const rolloutRunner = read(rolloutScript);
|
||||
|
||||
for (const runner of [deployRunner, migrationRunner]) {
|
||||
assert.match(runner, /EXPECTED_STAGING_ENV_OWNER_UID=.*stat[^\n]+"\$DEPLOY_PATH"/);
|
||||
assert.match(runner, /export EXPECTED_STAGING_ENV_OWNER_UID/);
|
||||
assert.ok(runner.indexOf("EXPECTED_STAGING_ENV_OWNER_UID=") < runner.indexOf("validate-staging-database-env.sh"));
|
||||
}
|
||||
assert.match(rolloutRunner, /EXPECTED_STAGING_ENV_OWNER_UID=.*stat[^\n]+"\$DEPLOY_PATH"/);
|
||||
assert.match(rolloutRunner, /EXPECTED_STAGING_ENV_OWNER_GID=.*stat[^\n]+"\$DEPLOY_PATH"/);
|
||||
assert.match(rolloutRunner, /chown "\$EXPECTED_STAGING_ENV_OWNER_UID:\$EXPECTED_STAGING_ENV_OWNER_GID" "\$temporary"/);
|
||||
assert.ok(rolloutRunner.indexOf("chown \"$EXPECTED_STAGING_ENV_OWNER_UID") < rolloutRunner.indexOf("mv -f -- \"$temporary\" \"$env_file\""));
|
||||
});
|
||||
|
||||
test("quality gate builds the Python package with its declared backend dependencies", () => {
|
||||
const workflow = read(qualityWorkflow);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user