Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
# Don't allow people to merge changes to these generated files, because the result
# may be invalid. You need to run "rush update" again.
# Lockfiles for package managers we don't use must never be text-merged: a bad
# merge silently corrupts them, so keep them as hard conflicts.
shrinkwrap.yaml merge=binary
npm-shrinkwrap.json merge=binary
yarn.lock merge=binary
pnpm-lock.yaml merge=binary

# pnpm-lock.yaml is regenerated on merge by a local driver that ships with the
# repo (registered by ts/tools/scripts/setup-merge-driver.mjs on `pnpm install`).
# Hosts without the driver (e.g. GitHub's server-side merges and the merge
# queue) don't recognize the name and fall back to git's normal 3-way text
# merge; CI's `pnpm install --frozen-lockfile` then catches any bad merge.
pnpm-lock.yaml merge=pnpm-lock

# Autogenerated package READMEs carry a commit-stamped footer, so every branch
# regenerates a different last line and they conflict constantly. Keep our copy
# on merge (the local keep-ours driver); the docs pipeline regenerates them.
# Where the driver isn't registered the name is unknown and git falls back to a
# text merge, and the heal-generated-files workflow clears the leftover conflict.
README.AUTOGEN.md merge=keep-ours

# Make text consistently using LF
* text eol=lf
Expand Down
76 changes: 76 additions & 0 deletions .github/workflows/heal-generated-files.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

# Auto-heal generated-file conflicts on open pull requests. When main moves,
# open PRs can start conflicting on generated files - the pnpm lockfile and the
# autogenerated README.AUTOGEN.md companions - and get ejected from the merge
# queue. This job merges main into each affected same-repo PR branch and
# resolves those files so no one has to do it by hand. The logic lives in
# ts/tools/scripts/heal-generated-files.mjs.

name: Heal Generated Files

on:
push:
branches: ["main"]
workflow_dispatch:
inputs:
pr:
description: "Single PR number to heal (default: scan all open PRs)"
type: string

concurrency:
# Serialize runs so two never push to the same branch at once, and never
# cancel a run mid-push.
group: heal-generated-files
cancel-in-progress: false

permissions:
contents: write
pull-requests: write

jobs:
heal:
runs-on: ubuntu-latest
env:
# `vars`/`secrets` cannot be used directly in step `if` conditions, so
# expose whether the app credentials are configured as a string. The App
# id is a repo variable (as in docs-pr-amend.yml / fix-dependabot-alerts).
HAS_APP_CREDS: ${{ vars.DEPENDABOT_APP_ID != '' }}
steps:
- name: Setup Git LF
run: git config --global core.autocrlf false

# Push to PR branches with the typeagent-bot app token (not the default
# GITHUB_TOKEN), so the healing commit re-triggers the PR's normal checks.
- name: Generate app token
id: app-token
if: env.HAS_APP_CREDS == 'true'
uses: actions/create-github-app-token@v1
with:
app-id: ${{ vars.DEPENDABOT_APP_ID }}
private-key: ${{ secrets.DEPENDABOT_APP_PRIVATE_KEY }}

- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ steps.app-token.outputs.token || secrets.GITHUB_TOKEN }}

- uses: pnpm/action-setup@v4
name: Install pnpm
with:
package_json_file: ts/package.json

- uses: actions/setup-node@v4
with:
node-version: 22
cache: "pnpm"
cache-dependency-path: ts/pnpm-lock.yaml

- name: Heal conflicted generated files
if: env.HAS_APP_CREDS == 'true'
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
GITHUB_REPOSITORY: ${{ github.repository }}
INPUT_PR: ${{ github.event.inputs.pr }}
run: node ts/tools/scripts/heal-generated-files.mjs
3 changes: 2 additions & 1 deletion ts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"getKeys": "node tools/scripts/getKeys.mjs",
"getKeys:build": "node tools/scripts/getKeys.mjs --vault build-pipeline-kv",
"getNPMRC": "node tools/scripts/getNPMRC.mjs",
"postinstall": "pnpm run postinstall:better-sqlite3-node-copy && pnpm run postinstall:better-sqlite3-node-restore",
"postinstall": "pnpm run postinstall:better-sqlite3-node-copy && pnpm run postinstall:better-sqlite3-node-restore && pnpm run postinstall:merge-driver",
"install:ext:vscode": "fluid-build . -t install:ext:vscode",
"knowledgeVisualizer": "pnpm -C packages/knowledgeVisualizer exec npm run start",
"kv": "pnpm -C packages/knowledgeVisualizer exec npm run start",
Expand All @@ -56,6 +56,7 @@
"montage": "pnpm -C packages/montage exec npm run start",
"postinstall:better-sqlite3-node-copy": "node ./tools/scripts/copy-better-sqlite3-node.js",
"postinstall:better-sqlite3-node-restore": "node ./tools/scripts/restore-better-sqlite3-node.js",
"postinstall:merge-driver": "node tools/scripts/setup-merge-driver.mjs",
"prettier": "prettier --check .",
"prettier:changed": "node tools/scripts/prettier-changed.mjs",
"prettier:changed:fix": "node tools/scripts/prettier-changed.mjs --write",
Expand Down
117 changes: 85 additions & 32 deletions ts/tools/scripts/getKeys.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,26 @@ async function getPIMClient() {
return getClient();
}

// Self-activate a PIM role for a short window so the following data-plane
// operation can succeed. Throws if PIM is unavailable or activation fails, so
// callers can fall back to another role or an unelevated retry.
async function elevate(roleName) {
console.warn(chalk.yellowBright(`Elevating to '${roleName}'...`));
const pimClient = await getPIMClient();
await pimClient.elevate({
requestType: "SelfActivate",
roleName,
expirationType: "AfterDuration",
expirationDuration: "PT5M", // activate for 5 minutes
continueOnFailure: true,
});

// Wait for the role to be activated
console.log(chalk.green("Elevation successful."));
console.warn(chalk.yellowBright("Waiting 5 seconds..."));
await new Promise((res) => setTimeout(res, 5000));
}

async function getSecretListWithElevation(keyVaultClient, vaultName) {
try {
return await keyVaultClient.getSecrets(vaultName);
Expand All @@ -118,23 +138,9 @@ async function getSecretListWithElevation(keyVaultClient, vaultName) {
}

try {
console.warn(chalk.yellowBright("Elevating to get secrets..."));
const pimClient = await getPIMClient();
await pimClient.elevate({
requestType: "SelfActivate",
roleName: "Key Vault Administrator",
expirationType: "AfterDuration",
expirationDuration: "PT5M", // activate for 5 minutes
continueOnFailure: true,
});

// Wait for the role to be activated
console.log(chalk.green("Elevation successful."));
console.warn(chalk.yellowBright("Waiting 5 seconds..."));
await new Promise((res) => setTimeout(res, 5000));

await elevate("Key Vault Administrator");
return await keyVaultClient.getSecrets(vaultName);
} catch (e) {
} catch {
console.warn(
chalk.yellow(
"Elevation to key vault admin failed...attempting to get secrets as key vault reader.",
Expand All @@ -143,21 +149,8 @@ async function getSecretListWithElevation(keyVaultClient, vaultName) {
}

try {
console.warn(chalk.yellowBright("Elevating to get secrets..."));
const pimClient = await getPIMClient();
await pimClient.elevate({
requestType: "SelfActivate",
roleName: "Key Vault Secrets User",
expirationType: "AfterDuration",
expirationDuration: "PT5M", // activate for 5 minutes
continueOnFailure: true,
});

// Wait for the role to be activated
console.log(chalk.green("Elevation successful."));
console.warn(chalk.yellowBright("Waiting 5 seconds..."));
await new Promise((res) => setTimeout(res, 5000));
} catch (e) {
await elevate("Key Vault Secrets User");
} catch {
console.warn(
chalk.yellow(
"Elevation failed...attempting to get secrets without elevation.",
Expand All @@ -169,6 +162,61 @@ async function getSecretListWithElevation(keyVaultClient, vaultName) {
}
}

// Write a secret, self-elevating via PIM if the vault rejects the write with a
// 403 (the signed-in user lacks STANDING write access). Mirrors
// getSecretListWithElevation, but targets roles that grant write (set)
// permission: "Key Vault Secrets User" is read-only and useless here, so we try
// "Key Vault Administrator" then "Key Vault Secrets Officer".
async function writeSecretWithElevation(
keyVaultClient,
vaultName,
secretName,
secretValue,
) {
try {
return await keyVaultClient.writeSecret(
vaultName,
secretName,
secretValue,
);
} catch (e) {
if (!isForbiddenByRbac(e)) {
throw e;
}

try {
await elevate("Key Vault Administrator");
return await keyVaultClient.writeSecret(
vaultName,
secretName,
secretValue,
);
} catch {
console.warn(
chalk.yellow(
"Elevation to key vault admin failed...trying key vault secrets officer.",
),
);
}

try {
await elevate("Key Vault Secrets Officer");
} catch {
console.warn(
chalk.yellow(
"Elevation failed...attempting to write without elevation.",
),
);
}

return await keyVaultClient.writeSecret(
vaultName,
secretName,
secretValue,
);
}
}

async function getSecrets(keyVaultClient, vaultName, shared) {
const overallStart = Date.now();
console.log(
Expand Down Expand Up @@ -576,7 +624,12 @@ async function pushYamlConfig() {
}

try {
await keyVaultClient.writeSecret(vaultName, secretName, yamlContent);
await writeSecretWithElevation(
keyVaultClient,
vaultName,
secretName,
yamlContent,
);
console.log(
chalk.green(
`\nSecret '${secretName}' updated in vault '${vaultName}'.`,
Expand Down
Loading
Loading