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
5 changes: 5 additions & 0 deletions .changeset/tracked-token-warning.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@taskless/cli": patch
---

When `.taskless/.env.local.json` is tracked by git, the warning now gives the steps that help: `git rm --cached` to untrack it, check `.taskless/.gitignore`, and if the commit was pushed, replace the token with `auth logout` then `auth login`. Before, it only said to gitignore the file, which does nothing for a file git already tracks.
14 changes: 11 additions & 3 deletions packages/cli/src/auth/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export async function saveToken(

const tasklessDirectory = join(cwd, ".taskless");
await mkdir(tasklessDirectory, { recursive: true });
await addToGitignore(cwd, [".env.local.json"]);
await addToGitignore(cwd, [PER_REPO_AUTH_FILE]);
await writeFile(join(tasklessDirectory, PER_REPO_AUTH_FILE), content, {
mode: 0o600,
});
Expand Down Expand Up @@ -126,7 +126,7 @@ function warnIfLegacyToken(): void {

/** Warn if .env.local.json is tracked by git */
async function warnIfTracked(cwd: string): Promise<void> {
const relativePath = ".taskless/.env.local.json";
const relativePath = `.taskless/${PER_REPO_AUTH_FILE}`;
try {
const output = await new Promise<string>((resolve, reject) => {
execFile("git", ["ls-files", relativePath], { cwd }, (error, stdout) => {
Expand All @@ -138,8 +138,16 @@ async function warnIfTracked(cwd: string): Promise<void> {
});
});
if (output.length > 0) {
// Already in the index, so a .gitignore entry alone changes nothing:
// git keeps tracking a file it already tracks.
const cli = getCliPrefix();
console.error(
"Warning: .taskless/.env.local.json is tracked by git. This file contains authentication tokens and should be gitignored."
[
`Warning: ${relativePath} is tracked by git. It contains an authentication token.`,
` 1. Untrack it and keep your local copy: git rm --cached ${relativePath}`,
` 2. Make sure .taskless/.gitignore lists ${PER_REPO_AUTH_FILE}, then commit.`,
` 3. If a commit containing it was pushed, treat the token as exposed and replace it: \`${cli} auth logout\`, then \`${cli} auth login\`. Logout only deletes the local copy, so it does not revoke the old token.`,
].join("\n")
);
}
} catch {
Expand Down
35 changes: 35 additions & 0 deletions packages/cli/test/token.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { execFileSync } from "node:child_process";
import { mkdtemp, readFile, rm, writeFile, mkdir } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
Expand All @@ -14,6 +15,7 @@ beforeEach(async () => {

afterEach(async () => {
vi.useRealTimers();
vi.restoreAllMocks();
vi.unstubAllEnvs();
await rm(temporaryDirectory, { recursive: true, force: true });
});
Expand Down Expand Up @@ -169,3 +171,36 @@ describe("removeToken", () => {
expect(await removeToken()).toBe(false);
});
});

describe("tracked token warning", () => {
it("tells the user to untrack the file, not just gitignore it", async () => {
await writeAuthFile({ access_token: "tracked-token" });
execFileSync("git", ["init", "-q"], { cwd: temporaryDirectory });
execFileSync("git", ["add", ".taskless/.env.local.json"], {
cwd: temporaryDirectory,
});
const errors: string[] = [];
vi.spyOn(console, "error").mockImplementation((message: unknown) => {
errors.push(String(message));
});

expect(await getToken(temporaryDirectory)).toBe("tracked-token");

const warning = errors.find((line) => line.includes("tracked by git"));
expect(warning).toContain("git rm --cached .taskless/.env.local.json");
expect(warning).toContain("auth logout");
expect(warning).toContain("auth login");
expect(warning).toContain("does not revoke");
});

it("stays quiet when the file is not tracked", async () => {
await writeAuthFile({ access_token: "untracked-token" });
execFileSync("git", ["init", "-q"], { cwd: temporaryDirectory });
const spy = vi.spyOn(console, "error").mockImplementation(() => {});

expect(await getToken(temporaryDirectory)).toBe("untracked-token");
expect(
spy.mock.calls.some((call) => String(call[0]).includes("tracked by git"))
).toBe(false);
});
});
Loading