projenrc/s3-docs-publishing.ts (109 lines of code) (raw):
import type { Monorepo, TypeScriptWorkspace } from 'cdklabs-projen-project-types/lib/yarn';
import { Component, github } from 'projen';
export enum DocType {
/**
* TypeDoc documentation (HTML)
*/
TYPEDOC = 'typedoc',
/**
* API Extractor documentation (JSON)
*/
API_EXTRACTOR = 'api-extractor',
}
export interface S3DocsPublishingProps {
/**
* The docs stream to publish to.
*/
readonly docsStream: string;
/**
* The path to the artifact in the dist folder
*/
readonly artifactPath: string;
/**
* The role arn (or github expression) for OIDC to assume to do the actual publishing.
*/
readonly roleToAssume: string;
/**
* The bucket name (or github expression) to publish to.
*/
readonly bucketName: string;
/**
* The type of documentation to publish.
*
*/
readonly docType: DocType;
}
export class S3DocsPublishing extends Component {
private readonly github: github.GitHub;
private readonly props: S3DocsPublishingProps;
constructor(project: TypeScriptWorkspace, props: S3DocsPublishingProps) {
super(project);
const gh = (project.parent! as Monorepo).github;
if (!gh) {
throw new Error('This workspace does not have a GitHub instance');
}
this.github = gh;
this.props = props;
}
public preSynthesize() {
const releaseWf = this.github.tryFindWorkflow('release');
if (!releaseWf) {
throw new Error('Could not find release workflow');
}
const safeName = this.project.name.replace('@', '').replace('/', '-');
const isApiExtractor = this.props.docType === DocType.API_EXTRACTOR;
// Determine job ID, name suffix, S3 path based on doc type
const jobIdSuffix = isApiExtractor ? '_api_extractor' : '_typedoc';
const nameSuffix = isApiExtractor ? 'api-extractor' : 'typedoc';
const s3PathPrefix = isApiExtractor ? `${safeName}-api-model-v` : `${safeName}-v`;
releaseWf.addJob(`${safeName}_release${jobIdSuffix}`, {
name: `${this.project.name}: Publish ${nameSuffix} to S3`,
environment: 'releasing', // <-- this has the configuration
needs: [`${safeName}_release_npm`],
runsOn: ['ubuntu-latest'],
permissions: {
idToken: github.workflows.JobPermission.WRITE,
contents: github.workflows.JobPermission.READ,
},
steps: [
{
name: 'Download build artifacts',
uses: 'actions/download-artifact@v4',
with: {
name: `${safeName}_build-artifact`,
path: 'dist',
},
},
{
name: 'Authenticate Via OIDC Role',
id: 'creds',
uses: 'aws-actions/configure-aws-credentials@v4',
with: {
'aws-region': 'us-east-1',
'role-to-assume': '${{ vars.AWS_ROLE_TO_ASSUME_FOR_ACCOUNT }}',
'role-session-name': `s3-${isApiExtractor ? 'api-model-' : ''}docs-publishing@aws-cdk-cli`,
'mask-aws-account-id': true,
},
},
{
name: 'Assume the publishing role',
id: 'publishing-creds',
uses: 'aws-actions/configure-aws-credentials@v4',
with: {
'aws-region': 'us-east-1',
'role-to-assume': this.props.roleToAssume,
'role-session-name': `s3-${isApiExtractor ? 'api-model-' : ''}docs-publishing@aws-cdk-cli`,
'mask-aws-account-id': true,
'role-chaining': true,
},
},
{
name: `Publish ${nameSuffix}`,
env: {
BUCKET_NAME: this.props.bucketName,
DOCS_STREAM: this.props.docsStream,
},
run: `echo "Uploading ${nameSuffix} to S3"
echo "::add-mask::$BUCKET_NAME"
S3_PATH="$DOCS_STREAM/${s3PathPrefix}$(cat dist/version.txt).zip"
LATEST="latest-${this.props.docsStream}"
# Capture both stdout and stderr
if OUTPUT=$(aws s3api put-object \\
--bucket "$BUCKET_NAME" \\
--key "$S3_PATH" \\
--body dist/${this.props.artifactPath} \\
--if-none-match "*" 2>&1); then
# File was uploaded successfully, update the latest pointer
echo "New ${nameSuffix} artifact uploaded successfully, updating latest pointer"
echo "$S3_PATH" | aws s3 cp - "s3://$BUCKET_NAME/$LATEST"
elif echo "$OUTPUT" | grep -q "PreconditionFailed"; then
# Check specifically for PreconditionFailed in the error output
echo "::warning::File already exists in S3. Skipping upload."
exit 0
else
# Any other error (permissions, etc)
echo "::error::Failed to upload ${nameSuffix} artifact"
exit 1
fi`,
},
],
});
}
}