Repository: spotify/backstage
File: plugins/techdocs-node/src/stages/publish/awsS3.ts (TypeScript)

Code before:
        ? AwsS3Publish.buildStaticCredentials(accessKeyId, secretAccessKey)
        : (await credsManager.getCredentialProvider()).sdkCredentialProvider;

    const roleArn = config.getOptionalString('roleArn');
    if (roleArn) {
      return fromTemporaryCredentials({
        masterCredentials: explicitCredentials,
        params: {
          RoleSessionName: 'backstage-aws-techdocs-s3-publisher',
          RoleArn: roleArn,
        },
        clientConfig: { region },
      });
    }

    return explicitCredentials;
  }
  /**
   * Custom retry wrapper for S3 operations with detailed error handling.
   */
  private async retryOperation<TOutput>(
    operation: () => Promise<TOutput>,
    operationName: string,
    maxAttempts: number = 3,
    shouldRetry: (error: S3ServiceException) => boolean = this
      .defaultShouldRetry,
  ): Promise<TOutput> {
    for (let attempt = 1; attempt < maxAttempts; attempt++) {
      try {
        return await operation();
      } catch (error) {
        const e = error as S3ServiceException;
        if (!shouldRetry(e)) {
          this.logger.error(`${operationName} failed: ${e.message}`);
          throw e;
        }

        this.logger.warn(`${operationName} failed, retrying...`, {
          attempt,
          maxAttempts,
          error: e.message,
          errorCode: e.name,
          httpStatusCode: e.$metadata?.httpStatusCode,
        });

        // Enhanced exponential backoff with jitter
        const baseDelay = operationName.startsWith('Upload-') ? 2000 : 1000;
        const backoffDelay = Math.min(baseDelay * Math.pow(2, attempt), 30000);
        const jitter = Math.random() * 1000;
        await new Promise(resolve =>
          setTimeout(resolve, backoffDelay + jitter),

Review comment (suggestion):
```suggestion
    shouldRetry: (error: S3ServiceException) => boolean = this
      .defaultShouldRetry.bind(this),
```

to not get surprising errors some time down the line when it actually accesses `this`

Code after:
        params: {
          RoleSessionName: 'backstage-aws-techdocs-s3-publisher',
          RoleArn: roleArn,
        },
        clientConfig: { region },
      });
    }

    return explicitCredentials;
  }
  /**
   * Custom retry wrapper for S3 operations with detailed error handling.
   */
  public async retryOperation<TOutput>(
    operation: () => Promise<TOutput>,
    operationName: string,
    maxAttempts: number = 3,
    shouldRetry: (
      error: S3ServiceException,
    ) => boolean = this.defaultShouldRetry.bind(this),
  ): Promise<TOutput> {
    for (let attempt = 1; attempt < maxAttempts; attempt++) {
      try {
        return await operation();
      } catch (error) {
        const e = error as S3ServiceException;
        if (!shouldRetry(e)) {
          this.logger.error(`${operationName} failed: ${e.message}`);
          throw e;
        }

        this.logger.warn(`${operationName} failed, retrying...`, {
          attempt,
          maxAttempts,
          error: e.message,
          errorCode: e.name,
          httpStatusCode: e.$metadata?.httpStatusCode,
        });

        // Enhanced exponential backoff with jitter
        const baseDelay = operationName.startsWith('Upload-') ? 2000 : 1000;
        const backoffDelay = Math.min(
          baseDelay * Math.pow(2, attempt - 1),
          30000,
        );
        const jitter = Math.random() * 1000;
        await new Promise(resolve =>
          setTimeout(resolve, backoffDelay + jitter),
        );
      }
    }