Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
d0e6ad1
fix(dashboard): repair GitHub config link-existing flow
BilalG1 May 19, 2026
65789a1
fix(dashboard): use npx instead of pnpx in config sync workflow
BilalG1 May 19, 2026
ed25eab
feat(dashboard): improve local CLI step in link-existing flow
BilalG1 May 19, 2026
ebb090e
chore(dashboard): trim helper text on local CLI link step
BilalG1 May 19, 2026
55ff7e3
feat(stack-cli): fall back to STACK_PROJECT_ID env var for project id
BilalG1 May 19, 2026
de9ec19
fix(dashboard): avoid flashing GitHub providerAccountId in account dr…
BilalG1 May 19, 2026
7550eaa
feat(dashboard): searchable combobox for repo + branch in link flow
BilalG1 May 19, 2026
2faffb6
refactor(dashboard): drop unused useTransition around onboarding stat…
BilalG1 May 19, 2026
5ce1b6b
refactor(dashboard): race-safe loadRepositories and simpler combobox API
BilalG1 May 19, 2026
34db0d5
Merge branch 'dev' into fix/github-config-link-flow
BilalG1 May 19, 2026
03e8a5e
feat(dashboard): scope repo search, surface rate limits, branch refresh
BilalG1 May 19, 2026
08c8356
chore(dashboard): bump generated workflow to actions/{checkout,setup-…
BilalG1 May 19, 2026
08bbba5
Merge remote-tracking branch 'origin/dev' into fix/github-config-link…
BilalG1 May 19, 2026
cdf4c68
fix(dashboard): throw if config path normalizes to empty in workflow …
BilalG1 May 19, 2026
b6783b2
fix(dashboard): escape project.id via JSON.stringify in copy-paste CL…
BilalG1 May 19, 2026
815560c
Merge branch 'dev' into fix/github-config-link-flow
BilalG1 May 19, 2026
c80c89f
fix(dashboard): throw on invalid matching-refs response
BilalG1 May 20, 2026
22e9f63
Merge branch 'dev' into fix/github-config-link-flow
BilalG1 May 20, 2026
4d415a5
Merge branch 'dev' into fix/github-config-link-flow
BilalG1 May 20, 2026
337cd9e
fix(dashboard): tighten repo combobox row + handle empty-tree repos
BilalG1 May 20, 2026
2563126
Merge branch 'dev' into fix/github-config-link-flow
BilalG1 May 20, 2026
c8b750b
feat(stack-cli): add --source flags to `config push`
BilalG1 May 20, 2026
453ba35
Merge branch 'dev' into fix/github-config-link-flow
BilalG1 May 20, 2026
f195b3f
Merge branch 'fix/github-config-link-flow' into feat/config-push-sour…
BilalG1 May 20, 2026
89754cc
fix(stack-cli): reject empty-string --source-path / --source-workflow…
BilalG1 May 20, 2026
9950674
Merge branch 'dev' into feat/config-push-source-flags
BilalG1 May 20, 2026
2876c82
Merge branch 'dev' into fix/github-config-link-flow
BilalG1 May 20, 2026
e1a3cd1
Merge branch 'fix/github-config-link-flow' into feat/config-push-sour…
BilalG1 May 20, 2026
c7cfbd8
Merge branch 'feat/config-push-source-flags' of https://github.com/he…
BilalG1 May 20, 2026
6b8d508
feat(dashboard): push config edits to GitHub from "Push to GitHub" di…
BilalG1 May 21, 2026
0a81649
chore: address PR review feedback
BilalG1 May 21, 2026
f129bc9
Merge remote-tracking branch 'origin/dev' into feat/config-push-sourc…
BilalG1 May 21, 2026
e0013df
chore: address config push review comments
BilalG1 May 21, 2026
02d635a
fix: normalize github source paths in config push
BilalG1 May 21, 2026
cb85d12
chore: avoid non-null assertions in config push source
BilalG1 May 21, 2026
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
9 changes: 9 additions & 0 deletions .claude/CLAUDE-KNOWLEDGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

This file contains knowledge learned while working on the codebase in Q&A format.

## Q: How should GitHub Contents API request-body assertions be written in Stack Auth tests?
A: Prefer inline snapshots over individual field selectors. For request bodies that contain base64 file content, parse the JSON body, assert it is an object, decode the `content` field back to UTF-8, and snapshot the normalized call object so the test verifies the path, method, headers, branch, message, sha, and rendered file content together.

## Q: How should Stack CLI GitHub source paths be stored?
A: Explicit `stack config push --source github` paths should be normalized as repo-relative paths before storing source metadata. Trim whitespace and strip leading `./`, repeated `./`, and leading `/` segments, matching the dashboard workflow generator's normalization for `STACK_AUTH_CONFIG_PATH` and workflow paths.

## Q: How should Stack CLI code handle flags proven present by nearby validation?
A: Avoid non-null assertions even when an earlier missing-flags check proves presence. Use `flags.foo ?? throwErr("Expected ...; this should have been caught by ...")` so the type system receives a definite value and future refactors fail loudly with the violated assumption.

## Q: How do anonymous users work in Stack Auth?
A: Anonymous users are a special type of user that can be created without any authentication. They have `isAnonymous: true` in the database and use different JWT signing keys with a `role: 'anon'` claim. Anonymous JWTs use a prefixed secret ("anon-" + audience) for signing and verification.

Expand Down
1 change: 1 addition & 0 deletions apps/backend/src/lib/seed-dummy-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2076,6 +2076,7 @@ export async function seedDummyProject(options: SeedDummyProjectOptions): Promis
branch: "main",
commit_hash: "abc123def456789",
config_file_path: "stack.config.json",
workflow_path: ".github/workflows/stack-auth-config-sync.yml",
},
})],
globalPrismaClient.project.update({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
buildWorkflowYaml,
GITHUB_PROJECT_ID_SECRET_NAME,
GITHUB_SECRET_SERVER_KEY_SECRET_NAME,
normalizeConfigPath,
WORKFLOW_FILE_PATH,
} from "./link-existing-onboarding-workflow";

Expand All @@ -17,7 +18,9 @@ describe("buildWorkflowYaml", () => {
expect(workflowYaml).toContain(` - ${JSON.stringify(configPath)}`);
expect(workflowYaml).toContain(` - ${JSON.stringify(WORKFLOW_FILE_PATH)}`);
expect(workflowYaml).toContain(` STACK_AUTH_CONFIG_PATH: ${JSON.stringify(configPath)}`);
expect(workflowYaml).toContain("run: npx --yes @stackframe/stack-cli@latest config push --config-file \"$STACK_AUTH_CONFIG_PATH\"");
expect(workflowYaml).toContain(` STACK_AUTH_SOURCE_REPO: \${{ github.repository }}`);
expect(workflowYaml).toContain(` STACK_AUTH_SOURCE_WORKFLOW_PATH: ${JSON.stringify(WORKFLOW_FILE_PATH)}`);
expect(workflowYaml).toContain("run: npx --yes @stackframe/stack-cli@latest config push --config-file \"$STACK_AUTH_CONFIG_PATH\" --source github --source-repo \"$STACK_AUTH_SOURCE_REPO\" --source-path \"$STACK_AUTH_CONFIG_PATH\" --source-workflow-path \"$STACK_AUTH_SOURCE_WORKFLOW_PATH\"");
expect(workflowYaml).not.toContain(`--config-file "${configPath}"`);
});

Expand All @@ -27,4 +30,36 @@ describe("buildWorkflowYaml", () => {
expect(workflowYaml).toContain(`\${{ secrets.${GITHUB_PROJECT_ID_SECRET_NAME} }}`);
expect(workflowYaml).toContain(`\${{ secrets.${GITHUB_SECRET_SERVER_KEY_SECRET_NAME} }}`);
});

it("uses the GitHub Actions runtime repository context for --source-repo", () => {
const workflowYaml = buildWorkflowYaml("main", "stack.config.ts");
expect(workflowYaml).toContain("STACK_AUTH_SOURCE_REPO: ${{ github.repository }}");
expect(workflowYaml).not.toMatch(/STACK_AUTH_SOURCE_REPO:\s+"[^$]/);
});
});

describe("normalizeConfigPath", () => {
it("strips a single leading ./", () => {
expect(normalizeConfigPath("./stack.config.ts")).toBe("stack.config.ts");
});

it("strips repeated leading ./", () => {
expect(normalizeConfigPath("././stack.config.ts")).toBe("stack.config.ts");
});

it("strips a mix of leading ./ and extra slashes", () => {
expect(normalizeConfigPath(".//src/stack.config.ts")).toBe("src/stack.config.ts");
});

it("strips a single leading /", () => {
expect(normalizeConfigPath("/src/stack.config.ts")).toBe("src/stack.config.ts");
});

it("leaves a repo-relative path alone", () => {
expect(normalizeConfigPath("src/stack.config.ts")).toBe("src/stack.config.ts");
});

it("trims whitespace before normalization", () => {
expect(normalizeConfigPath(" ./stack.config.ts ")).toBe("stack.config.ts");
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ function encodeYamlScalar(value: string): string {
}

// GitHub Actions `on.push.paths` filters are repo-relative and do not match a
// leading `./`. Config-path suggestions and manual input may include one, so
// strip it to keep the push trigger (and the checked-out file path) canonical.
// leading `./` or `/`. Config-path suggestions and manual input may include
// either, possibly repeated (e.g. `.//src/...`), so strip any combination of
// leading `./` and `/` segments to keep the push trigger and checked-out path
// canonical.
export function normalizeConfigPath(configPath: string): string {
return configPath.trim().replace(/^(?:\.\/)+/, "");
return configPath.trim().replace(/^(?:\.?\/+)+/, "");
}

export function buildWorkflowYaml(branch: string, configPath: string): string {
Expand All @@ -23,6 +25,10 @@ export function buildWorkflowYaml(branch: string, configPath: string): string {
const encodedConfigPath = encodeYamlScalar(normalizedConfigPath);
const encodedWorkflowPath = encodeYamlScalar(WORKFLOW_FILE_PATH);

// `actions/checkout` lands the repo at the runner cwd, so `$STACK_AUTH_CONFIG_PATH`
// (repo-relative) is also the local path on disk — that's why the same env var is
// safe to use for both `--config-file` and `--source-path`. If a future workflow
// checks out with `with: path: <subdir>`, these would diverge.
return `name: Stack Auth Config Sync

on:
Expand Down Expand Up @@ -51,6 +57,8 @@ jobs:
STACK_PROJECT_ID: \${{ secrets.${GITHUB_PROJECT_ID_SECRET_NAME} }}
STACK_SECRET_SERVER_KEY: \${{ secrets.${GITHUB_SECRET_SERVER_KEY_SECRET_NAME} }}
STACK_AUTH_CONFIG_PATH: ${encodedConfigPath}
run: npx --yes @stackframe/stack-cli@latest config push --config-file "$STACK_AUTH_CONFIG_PATH"
STACK_AUTH_SOURCE_REPO: \${{ github.repository }}
STACK_AUTH_SOURCE_WORKFLOW_PATH: ${encodedWorkflowPath}
run: npx --yes @stackframe/stack-cli@latest config push --config-file "$STACK_AUTH_CONFIG_PATH" --source github --source-repo "$STACK_AUTH_SOURCE_REPO" --source-path "$STACK_AUTH_CONFIG_PATH" --source-workflow-path "$STACK_AUTH_SOURCE_WORKFLOW_PATH"
`;
}
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ type PersistedLinkExistingState = {
selectedRepositoryFullName: string,
selectedBranch: string,
configPathInput: string,
packageRunner: PackageRunner,
};

function createRepositoryReference(fullName: string, defaultBranch: string): GithubRepository {
Expand Down Expand Up @@ -135,12 +136,15 @@ function readPersistedLinkExistingState(projectId: string): PersistedLinkExistin
return null;
}
const selectedGithubAccountIdField = parsed.selectedGithubAccountId;
const packageRunnerField = getObjectString(parsed, "packageRunner");
const packageRunner: PackageRunner = PACKAGE_RUNNERS.find((entry) => entry === packageRunnerField) ?? "npx";
return {
step: parsePersistedLinkExistingStep(parsed.step),
selectedGithubAccountId: typeof selectedGithubAccountIdField === "string" ? selectedGithubAccountIdField : null,
selectedRepositoryFullName: getObjectString(parsed, "selectedRepositoryFullName") ?? "",
selectedBranch: getObjectString(parsed, "selectedBranch") ?? "",
configPathInput: getObjectString(parsed, "configPathInput") ?? "stack.config.ts",
packageRunner,
};
} catch {
return null;
Expand Down Expand Up @@ -494,7 +498,7 @@ export function LinkExistingOnboarding(props: Props) {
const repositoriesLoadedAccountRef = useRef<string | null>(null);
const loadRepositoriesRunIdRef = useRef(0);
const [configPathInput, setConfigPathInput] = useState<string>(persistedState?.configPathInput ?? "stack.config.ts");
const [packageRunner, setPackageRunner] = useState<PackageRunner>("npx");
const [packageRunner, setPackageRunner] = useState<PackageRunner>(persistedState?.packageRunner ?? "npx");
const [repoSearchQuery, setRepoSearchQuery] = useState("");
const [repoSearchResults, setRepoSearchResults] = useState<GithubRepository[]>([]);
const [loadingRepoSearch, setLoadingRepoSearch] = useState(false);
Expand All @@ -514,9 +518,10 @@ export function LinkExistingOnboarding(props: Props) {
selectedRepositoryFullName: partial.selectedRepositoryFullName ?? existingState?.selectedRepositoryFullName ?? selectedRepositoryFullName,
selectedBranch: partial.selectedBranch ?? existingState?.selectedBranch ?? selectedBranch,
configPathInput: partial.configPathInput ?? existingState?.configPathInput ?? configPathInput,
packageRunner: partial.packageRunner ?? existingState?.packageRunner ?? packageRunner,
...partial,
});
}, [configPathInput, project.id, selectedBranch, selectedGithubAccountId, selectedRepositoryFullName, step]);
}, [configPathInput, packageRunner, project.id, selectedBranch, selectedGithubAccountId, selectedRepositoryFullName, step]);

const setStepWithPersistence = useCallback((nextStep: LinkExistingStep) => {
if (nextStep !== "github-logs") {
Expand Down Expand Up @@ -971,14 +976,22 @@ export function LinkExistingOnboarding(props: Props) {
branch: string,
path: string,
): Promise<boolean> => {
const normalizedPath = path.trim().replace(/^\.?\/+/, "");
// Same shape as normalizeConfigPath in link-existing-onboarding-workflow.ts:
// strip any combination of leading `./` and `/` segments so inputs like
// `.//src/...` or `/src/...` collapse to a repo-relative path.
const normalizedPath = path.trim().replace(/^(?:\.?\/+)+/, "");
if (normalizedPath.length === 0 || normalizedPath.split("/").includes("..")) {
return false;
}
const refQuery = new URLSearchParams({ ref: branch }).toString();
try {
// `cache: "no-store"` because GitHub's Contents API responds with
// `Cache-Control: private, max-age=60` for authenticated reads. Without
// this, a user who just pushed the config file and immediately clicks
// "Create GitHub Action" can see a cached 404 from before the push.
const response = await githubFetch(
githubRepositoryApiPath(owner, repo, `/contents/${encodeGitHubPath(normalizedPath)}?${refQuery}`),
{ cache: "no-store" },
);
if (!isObject(response) || Array.isArray(response)) {
return false;
Expand Down Expand Up @@ -1418,6 +1431,7 @@ export function LinkExistingOnboarding(props: Props) {
const runner = PACKAGE_RUNNERS.find((entry) => entry === id);
if (runner != null) {
setPackageRunner(runner);
persistState({ packageRunner: runner });
}
}}
size="sm"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,19 @@ export default function PageClient() {
<div><strong>Repository:</strong> {configSource.owner}/{configSource.repo}</div>
<div><strong>Branch:</strong> {configSource.branch}</div>
<div><strong>Config file:</strong> {configSource.configFilePath}</div>
{configSource.workflowPath ? (
<div>
<strong>Workflow file:</strong>{" "}
<a
className="underline"
target="_blank"
rel="noreferrer noopener"
href={`https://github.com/${encodeURIComponent(configSource.owner)}/${encodeURIComponent(configSource.repo)}/blob/${configSource.branch.split("/").map(encodeURIComponent).join("/")}/${configSource.workflowPath.split("/").map(encodeURIComponent).join("/")}`}
>
{configSource.workflowPath}
</a>
</div>
) : null}
<div><strong>Last commit:</strong> <code className="text-xs">{configSource.commitHash.substring(0, 7)}</code></div>
</div>
</div>
Expand Down
Loading
Loading