From fe296a057c9b742ba77ac23d661a0b5c2f2d42e3 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 Jan 2026 11:15:52 -0800 Subject: [PATCH 1/2] Document trial limit per payment method for Copilot (#59278) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: sunbrye <56200261+sunbrye@users.noreply.github.com> --- .../billing/concepts/product-billing/github-copilot-licenses.md | 1 + 1 file changed, 1 insertion(+) diff --git a/content/billing/concepts/product-billing/github-copilot-licenses.md b/content/billing/concepts/product-billing/github-copilot-licenses.md index 21d8bc691652..401115bd1b36 100644 --- a/content/billing/concepts/product-billing/github-copilot-licenses.md +++ b/content/billing/concepts/product-billing/github-copilot-licenses.md @@ -47,6 +47,7 @@ There are several ways to use {% data variables.product.prodname_copilot_short % * You must choose a monthly or yearly billing cycle and provide a payment method. * If you do not cancel before the end of the trial, it automatically converts to a paid plan. * You can cancel any time during the 30 days. If you cancel, you will not be charged and will keep access until the trial ends. +* Free trials are limited to three per payment method. Additional trials will continue as paid subscriptions. ### Educational and open source benefits From 511825e3c9f0997db9f7784dc2966ed583596738 Mon Sep 17 00:00:00 2001 From: Kevin Heis Date: Fri, 23 Jan 2026 12:13:22 -0800 Subject: [PATCH 2/2] Add LiquidError class for better type safety (#59315) --- src/languages/lib/render-with-fallback.ts | 17 +++- .../scripts/count-translation-corruptions.ts | 16 +++- .../tests/translation-error-comments.ts | 96 +++++++------------ 3 files changed, 62 insertions(+), 67 deletions(-) diff --git a/src/languages/lib/render-with-fallback.ts b/src/languages/lib/render-with-fallback.ts index 30c3686e5439..fe9d53bc5ffe 100644 --- a/src/languages/lib/render-with-fallback.ts +++ b/src/languages/lib/render-with-fallback.ts @@ -5,14 +5,27 @@ import type { Context } from '@/types' export class EmptyTitleError extends Error {} -interface LiquidToken { +export interface LiquidToken { file?: string getPosition?: () => [number, number] } -interface LiquidError extends Error { +/** + * Custom error class for Liquid rendering errors with proper type safety. + * Use this instead of creating Error objects and mutating them with type assertions. + * + * @example + * const error = new LiquidError('Unknown tag', 'ParseError') + * error.token = { file: '/content/test.md', getPosition: () => [1, 5] } + */ +export class LiquidError extends Error { token?: LiquidToken originalError?: Error + + constructor(message: string, name: 'ParseError' | 'RenderError' | 'TokenizationError') { + super(message) + this.name = name + } } interface RenderOptions { diff --git a/src/languages/scripts/count-translation-corruptions.ts b/src/languages/scripts/count-translation-corruptions.ts index a46a1bc2ed5f..bc8e89007e18 100644 --- a/src/languages/scripts/count-translation-corruptions.ts +++ b/src/languages/scripts/count-translation-corruptions.ts @@ -77,11 +77,19 @@ function run(languageCode: string, site: Site, englishReusables: Reusables) { const illegalTags = new Map() function countError(error: TokenizationError, where: string) { - const originalError = (error as { originalError?: Error }).originalError + // TokenizationError from liquidjs may have originalError and token.content + // but these aren't in the public type definitions + const errorWithExtras = error as TokenizationError & { + originalError?: Error + token?: { content?: string } + } + const originalError = errorWithExtras.originalError const errorString = originalError ? originalError.message : error.message - if (errorString.includes('illegal tag syntax')) { - const illegalTag = (error as unknown as { token: { content: string } }).token.content - illegalTags.set(illegalTag, (illegalTags.get(illegalTag) || 0) + 1) + if (errorString.includes('illegal tag syntax') && errorWithExtras.token?.content) { + illegalTags.set( + errorWithExtras.token.content, + (illegalTags.get(errorWithExtras.token.content) || 0) + 1, + ) } errors.set(errorString, (errors.get(errorString) || 0) + 1) wheres.set(where, (wheres.get(where) || 0) + 1) diff --git a/src/languages/tests/translation-error-comments.ts b/src/languages/tests/translation-error-comments.ts index 7052e8023f42..af5986944f66 100644 --- a/src/languages/tests/translation-error-comments.ts +++ b/src/languages/tests/translation-error-comments.ts @@ -4,19 +4,11 @@ import { EmptyTitleError, renderContentWithFallback, executeWithFallback, + LiquidError, } from '../lib/render-with-fallback' import { TitleFromAutotitleError } from '@/content-render/unified/rewrite-local-links' import Page from '@/frame/lib/page' -// Type aliases for error objects with token information -type ErrorWithToken = Error & { token: { file: string; getPosition: () => number[] } } -type ErrorWithTokenNoFile = Error & { token: { getPosition: () => number[] } } -type ErrorWithTokenNoPosition = Error & { token: { file: string } } -type ErrorWithTokenAndOriginal = Error & { - token: { file: string; getPosition: () => number[] } - originalError: Error -} - describe('Translation Error Comments', () => { // Mock renderContent for integration tests let mockRenderContent: MockedFunction< @@ -35,9 +27,8 @@ describe('Translation Error Comments', () => { describe('createTranslationFallbackComment', () => { describe('Liquid ParseError', () => { test('includes all fields when token information is available', () => { - const error = new Error("Unknown tag 'badtag', line:1, col:3") - error.name = 'ParseError' - ;(error as unknown as ErrorWithToken).token = { + const error = new LiquidError("Unknown tag 'badtag', line:1, col:3", 'ParseError') + error.token = { file: '/content/test/article.md', getPosition: () => [1, 3], } @@ -57,15 +48,15 @@ describe('Translation Error Comments', () => { describe('Liquid RenderError', () => { test('includes original error message when available', () => { - const error = new Error("Unknown variable 'variables.nonexistent.value'") - error.name = 'RenderError' - ;(error as unknown as ErrorWithToken).token = { + const error = new LiquidError( + "Unknown variable 'variables.nonexistent.value'", + 'RenderError', + ) + error.token = { file: '/content/test/intro.md', getPosition: () => [3, 15], } - ;(error as unknown as ErrorWithTokenAndOriginal).originalError = new Error( - 'Variable not found: variables.nonexistent.value', - ) + error.originalError = new Error('Variable not found: variables.nonexistent.value') const result = createTranslationFallbackComment(error, 'rawIntro') @@ -78,9 +69,8 @@ describe('Translation Error Comments', () => { }) test('falls back to main error message when no originalError', () => { - const error = new Error('Main error message') - error.name = 'RenderError' - ;(error as unknown as ErrorWithToken).token = { + const error = new LiquidError('Main error message', 'RenderError') + error.token = { file: '/content/test.md', getPosition: () => [1, 1], } @@ -93,9 +83,8 @@ describe('Translation Error Comments', () => { describe('Liquid TokenizationError', () => { test('includes tokenization error details', () => { - const error = new Error('Unexpected token, line:1, col:10') - error.name = 'TokenizationError' - ;(error as unknown as ErrorWithToken).token = { + const error = new LiquidError('Unexpected token, line:1, col:10', 'TokenizationError') + error.token = { file: '/content/test/page.md', getPosition: () => [1, 10], } @@ -147,9 +136,8 @@ describe('Translation Error Comments', () => { describe('Error handling edge cases', () => { test('handles error with no token information gracefully', () => { - const error = new Error('Generic liquid error without token info') - error.name = 'RenderError' - // No token property + const error = new LiquidError('Generic liquid error without token info', 'RenderError') + // No token property set const result = createTranslationFallbackComment(error, 'rawIntro') @@ -163,9 +151,8 @@ describe('Translation Error Comments', () => { }) test('handles error with token but no file', () => { - const error = new Error('Error message') - error.name = 'ParseError' - ;(error as unknown as ErrorWithTokenNoFile).token = { + const error = new LiquidError('Error message', 'ParseError') + error.token = { // No file property getPosition: () => [5, 10], } @@ -178,9 +165,8 @@ describe('Translation Error Comments', () => { }) test('handles error with token but no getPosition method', () => { - const error = new Error('Error message') - error.name = 'ParseError' - ;(error as unknown as ErrorWithTokenNoPosition).token = { + const error = new LiquidError('Error message', 'ParseError') + error.token = { file: '/content/test.md', // No getPosition method } @@ -194,8 +180,7 @@ describe('Translation Error Comments', () => { test('truncates very long error messages', () => { const longMessage = 'A'.repeat(300) // Very long error message - const error = new Error(longMessage) - error.name = 'ParseError' + const error = new LiquidError(longMessage, 'ParseError') const result = createTranslationFallbackComment(error, 'rawTitle') @@ -211,8 +196,7 @@ describe('Translation Error Comments', () => { }) test('properly escapes quotes in error messages', () => { - const error = new Error('Error with "double quotes" and more') - error.name = 'RenderError' + const error = new LiquidError('Error with "double quotes" and more', 'RenderError') const result = createTranslationFallbackComment(error, 'rawTitle') @@ -233,9 +217,7 @@ describe('Translation Error Comments', () => { }) test('handles error with no message', () => { - const error = new Error() - error.name = 'ParseError' - // Message will be empty string by default + const error = new LiquidError('', 'ParseError') const result = createTranslationFallbackComment(error, 'title') @@ -245,8 +227,7 @@ describe('Translation Error Comments', () => { }) test('cleans up multiline messages', () => { - const error = new Error('Line 1\nLine 2\n Line 3 \n\nLine 5') - error.name = 'RenderError' + const error = new LiquidError('Line 1\nLine 2\n Line 3 \n\nLine 5', 'RenderError') const result = createTranslationFallbackComment(error, 'content') @@ -257,9 +238,8 @@ describe('Translation Error Comments', () => { describe('Comment format validation', () => { test('comment format is valid HTML', () => { - const error = new Error('Test error') - error.name = 'ParseError' - ;(error as unknown as ErrorWithToken).token = { + const error = new LiquidError('Test error', 'ParseError') + error.token = { file: '/content/test.md', getPosition: () => [1, 1], } @@ -275,9 +255,8 @@ describe('Translation Error Comments', () => { }) test('contains all required fields when available', () => { - const error = new Error('Detailed error message') - error.name = 'RenderError' - ;(error as unknown as ErrorWithToken).token = { + const error = new LiquidError('Detailed error message', 'RenderError') + error.token = { file: '/content/detailed-test.md', getPosition: () => [42, 15], } @@ -294,9 +273,8 @@ describe('Translation Error Comments', () => { }) test('maintains consistent field order', () => { - const error = new Error('Test message') - error.name = 'ParseError' - ;(error as unknown as ErrorWithToken).token = { + const error = new LiquidError('Test message', 'ParseError') + error.token = { file: '/content/test.md', getPosition: () => [1, 1], } @@ -336,9 +314,8 @@ describe('Translation Error Comments', () => { mockRenderContent.mockImplementation( (template: string, innerContext: Record) => { if (innerContext.currentLanguage !== 'en' && template.includes('badtag')) { - const error = new Error("Unknown tag 'badtag'") - error.name = 'ParseError' - ;(error as unknown as ErrorWithToken).token = { + const error = new LiquidError("Unknown tag 'badtag'", 'ParseError') + error.token = { file: '/content/test.md', getPosition: () => [1, 5], } @@ -375,8 +352,7 @@ describe('Translation Error Comments', () => { mockRenderContent.mockImplementation( (template: string, innerContext: Record) => { if (innerContext.currentLanguage !== 'en' && template.includes('badtag')) { - const error = new Error("Unknown tag 'badtag'") - error.name = 'ParseError' + const error = new LiquidError("Unknown tag 'badtag'", 'ParseError') throw error } return 'English Title' @@ -399,9 +375,8 @@ describe('Translation Error Comments', () => { } const failingCallable = async () => { - const error = new Error("Unknown variable 'variables.bad'") - error.name = 'RenderError' - ;(error as unknown as ErrorWithToken).token = { + const error = new LiquidError("Unknown variable 'variables.bad'", 'RenderError') + error.token = { file: '/content/article.md', getPosition: () => [10, 20], } @@ -427,8 +402,7 @@ describe('Translation Error Comments', () => { } const failingCallable = async () => { - const error = new Error('Test error') - error.name = 'RenderError' + const error = new LiquidError('Test error', 'RenderError') throw error }