Skip to content

Commit

Permalink
ci: add deprecate workflow
Browse files Browse the repository at this point in the history
  • Loading branch information
dpilch committed May 17, 2024
1 parent 0ffe64f commit 176a9ef
Show file tree
Hide file tree
Showing 15 changed files with 530 additions and 3 deletions.
8 changes: 8 additions & 0 deletions .codebuild/deprecate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
version: 0.2
env:
shell: bash
git-credential-helper: yes
phases:
build:
commands:
- source ./shared-scripts.sh && _deprecate
14 changes: 14 additions & 0 deletions .codebuild/deprecate_workflow.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
version: 0.2
env:
shell: bash
compute-type: BUILD_GENERAL1_SMALL

batch:
fast-fail: false
build-graph:
- identifier: install_linux
buildspec: .codebuild/install_linux.yml
- identifier: deprecate
buildspec: .codebuild/deprecate.yml
depend-on:
- install_linux
10 changes: 10 additions & 0 deletions .codebuild/install_linux.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
version: 0.2
env:
shell: bash
phases:
build:
commands:
- source ./shared-scripts.sh && _installLinux
artifacts:
files:
- 'shared-scripts.sh'
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,6 @@ package-lock.json
.idea
scripts/.env
.codebuild/debug_workflow.yml
.npmrc
verdaccio-logs.txt
scripts/components/private_packages.ts
3 changes: 2 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@
**/package-lock.json
**/.eslintrc.js
**/tsconfig.json
packages/*/CHANGELOG.md
packages/*/CHANGELOG.md
scripts/components/private_packages.ts
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"lint-fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix",
"clean": "lerna run clean && lerna exec yarn rimraf tsconfig.tsbuildinfo && lerna clean --yes && yarn rimraf node_modules",
"build": "lerna run build",
"production-build": "yarn --ignore-engines --frozen-lockfile && lerna run build --concurrency 3 --stream",
"production-install": "yarn --ignore-engines --frozen-lockfile",
"production-build": "yarn production-install && lerna run build --concurrency 3 --stream",
"publish:main": "lerna publish --canary --exact --force-publish --preid=dev --dist-tag=dev --include-merged-tags --conventional-prerelease --no-verify-access --yes",
"publish:release": "lerna publish --exact --conventional-commits --message 'chore(release): Publish [ci skip]' --no-verify-access --yes",
"publish:tag": "lerna publish --exact --dist-tag=$NPM_TAG --preid=$NPM_TAG --conventional-commits --conventional-prerelease --message 'chore(release): Publish tagged release $NPM_TAG [ci skip]' --no-verify-access --yes",
Expand All @@ -35,6 +36,7 @@
"verify-api-extract": "yarn extract-api && ./scripts/verify-extract-api.sh",
"trigger-release": "source ./scripts/cloud-release.sh && triggerRelease",
"trigger-tag-release": "source ./scripts/cloud-release.sh && triggerTagRelease",
"trigger-deprecate-release": "source ./scripts/cloud-release.sh && deprecateRelease",
"view-test-artifact": "./scripts/view-test-artifacts.sh",
"cleanup-stale-resources": "source ./scripts/cloud-utils.sh && cleanupStaleResources",
"cloud-e2e": "source scripts/cloud-utils.sh && cloudE2E",
Expand All @@ -44,7 +46,9 @@
"cloud-e2e-debug": "source scripts/cloud-utils.sh && cloudE2EDebug",
"authenticate-e2e-profile": "source scripts/cloud-utils.sh && authenticateWithE2EProfile",
"extract-dependency-licenses": "./scripts/extract-dependency-licenses.sh",
"verify-dependency-licenses-extract": "yarn extract-dependency-licenses && ./scripts/verify-dependency-licenses.sh"
"verify-dependency-licenses-extract": "yarn extract-dependency-licenses && ./scripts/verify-dependency-licenses.sh",
"deprecate": "ts-node scripts/deprecate_release.ts",
"postinstall": "echo 'export default [' > scripts/components/private_packages.ts && grep -l packages/*/package.json -e '\"private\": \"\\?true\"\\?' | xargs cat | jq .name | tr -s '\\n' ',' >> scripts/components/private_packages.ts && echo '];' >> scripts/components/private_packages.ts"
},
"bugs": {
"url": "https://github.com/aws-amplify/amplify-codegen/issues"
Expand Down
11 changes: 11 additions & 0 deletions scripts/cloud-release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ source ./scripts/cloud-utils.sh
export RELEASE_ROLE_NAME=CodebuildDeveloper
export RELEASE_PROFILE_NAME=AmplifyAPIPluginRelease
export RELEASE_PROJECT_NAME=amplify-codegen-release-workflow
export DEPRECATE_PROJECT_NAME=amplify-codegen-deprecate-workflow

function triggerRelease {
triggerProjectBatch $RELEASE_ACCOUNT_PROD $RELEASE_ROLE_NAME "${RELEASE_PROFILE_NAME}Prod" $RELEASE_PROJECT_NAME "release"
Expand All @@ -20,3 +21,13 @@ function triggerTagRelease {
fi
triggerProjectBatch $RELEASE_ACCOUNT_PROD $RELEASE_ROLE_NAME "${RELEASE_PROFILE_NAME}Prod" $RELEASE_PROJECT_NAME $branch_name
}

function deprecateRelease {
DEPRECATION_MESSAGE=$1
SEARCH_FOR_RELEASE_STARTING_FROM=$2
USE_NPM_REGISTRY=$3
triggerProjectBatchWithEnvOverrides $RELEASE_ACCOUNT_PROD $RELEASE_ROLE_NAME "${RELEASE_PROFILE_NAME}Prod" $DEPRECATE_PROJECT_NAME "release" \
name=DEPRECATION_MESSAGE,value=\""$DEPRECATION_MESSAGE"\",type=PLAINTEXT \
name=SEARCH_FOR_RELEASE_STARTING_FROM,value=$SEARCH_FOR_RELEASE_STARTING_FROM,type=PLAINTEXT \
name=USE_NPM_REGISTRY,value=$USE_NPM_REGISTRY,type=PLAINTEXT
}
17 changes: 17 additions & 0 deletions scripts/cloud-utils.sh
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,23 @@ function triggerProjectBatch {
echo "https://$REGION.console.aws.amazon.com/codesuite/codebuild/$account_number/projects/$project_name/batch/$RESULT?region=$REGION"
}

function triggerProjectBatchWithEnvOverrides {
account_number=$1
role_name=$2
profile_name=$3
project_name=$4
target_branch=$5
shift 5
authenticate $account_number $role_name $profile_name
echo AWS Account: $account_number
echo Project: $project_name
echo Target Branch: $target_branch
RESULT=$(aws codebuild start-build-batch --region=$REGION --profile="${profile_name}" --project-name $project_name --source-version=$target_branch \
--environment-variables-override name=BRANCH_NAME,value=$target_branch,type=PLAINTEXT "$@" \
--query 'buildBatch.id' --output text)
echo "https://$REGION.console.aws.amazon.com/codesuite/codebuild/$account_number/projects/$project_name/batch/$RESULT?region=$REGION"
}

function triggerProject {
account_number=$1
role_name=$2
Expand Down
59 changes: 59 additions & 0 deletions scripts/components/dist_tag_mover.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { EOL } from 'os';
import { NpmClient } from './npm_client.js';
import { releaseTagToNameAndVersion } from './release_tag_to_name_and_version';

type DistTagMoveAction = {
/**
* An NPM dist-tag
*/
distTag: string;
/**
* This is a string of the form <packageName>@<version>
*/
releaseTag: string;
};

/**
* Handles moving npm dist-tags from one package version to another
*/
export class DistTagMover {
/**
* Initialize with an npmClient
*/
constructor(private readonly npmClient: NpmClient) {}

/**
* Given a list of sourceReleaseTags and destReleaseTags,
* any npm dist-tags that are pointing to a sourceReleaseTag will be moved to point to the corresponding destReleaseTag
*/
moveDistTags = async (sourceReleaseTags: string[], destReleaseTags: string[]) => {
const moveActions: DistTagMoveAction[] = [];

for (const sourceReleaseTag of sourceReleaseTags) {
const { packageName, version: sourceVersion } = releaseTagToNameAndVersion(sourceReleaseTag);

const { 'dist-tags': distTags } = await this.npmClient.getPackageInfo(sourceReleaseTag);

Object.entries(distTags).forEach(([tagName, versionAtTag]) => {
if (versionAtTag !== sourceVersion) {
return;
}
const destReleaseTag = destReleaseTags.find(releaseTag => releaseTag.includes(packageName));
if (!destReleaseTag) {
console.warn(`No corresponding destination release tag found for ${sourceReleaseTag}. latest tag not moved.`);
} else {
moveActions.push({
releaseTag: destReleaseTag,
distTag: tagName,
});
}
});
}

for (const { distTag, releaseTag } of moveActions) {
console.log(`Moving dist tag "${distTag}" to release tag ${releaseTag}`);
await this.npmClient.setDistTag(releaseTag, distTag);
console.log(`Done!${EOL}`);
}
};
}
209 changes: 209 additions & 0 deletions scripts/components/git_client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
const execa = require('execa');
import { writeFile } from 'fs/promises';
import { EOL } from 'os';
import * as path from 'path';
import { releaseTagToNameAndVersion } from './release_tag_to_name_and_version';
import privatePackages from './private_packages';

/**
* Client for programmatically interacting with the local git cli
*/
export class GitClient {
private readonly gitignorePath: string;

/**
* execaCommand that allows us to capture stdout
*/
private readonly exec;

/**
* execaCommand that pipes buffers to process buffers
*/
private readonly execWithIO;

/**
* Initialize with an optional directory to operate in.
* Defaults to the process cwd.
*/
constructor(cwd?: string) {
this.exec = (command: string, args: string[]) => execa.sync(command, args, { cwd });
this.execWithIO = (command: string, args: string[]) => execa.sync(command, args, { cwd, stdio: 'inherit' });
this.gitignorePath = cwd ? path.join(cwd, '.gitignore') : '.gitignore';
}

init = async () => {
await this.exec('git', ['init']);
await writeFile(this.gitignorePath, `node_modules${EOL}`);
};

/**
* Throws if there are uncommitted changes in the repo
*/
ensureWorkingTreeIsClean = async () => {
const { stdout } = await this.exec('git', ['status', '--porcelain']);
const isDirty = stdout.trim();
if (isDirty) {
throw new Error('Dirty working tree detected. Commit or stash changes to continue.');
}
};

getCurrentBranch = async () => {
const { stdout: currentBranch } = await this.exec('git', ['branch', '--show-current']);
return currentBranch;
};

/**
* Switches to branchName. Creates the branch if it does not exist.
*/
switchToBranch = async (branchName: string) => {
const { stdout: branchResult } = await this.exec('git', ['branch', '-l', branchName]);
const branchExists = branchResult.trim().length > 0;
if (branchExists) {
await this.execWithIO('git', ['switch', branchName]);
} else {
await this.execWithIO('git', ['switch', '-c', branchName]);
}
};

/**
* Stages and commits all current changes
*/
commitAllChanges = async (message: string) => {
await this.execWithIO('git', ['add', '.']);
await this.execWithIO('git', ['commit', '--message', message, '--allow-empty']);
};

/**
* Push to the remote
*/
push = async ({ force }: { force: boolean } = { force: false }) => {
await this.execWithIO('git', ['push', force ? '--force' : '']);
};

fetchTags = async () => {
await this.execWithIO('git', ['fetch', '--tags']);
};

checkout = async (ref: string, paths: string[] = []) => {
const additionalArgs = paths.length > 0 ? ['--', ...paths] : [];
await this.execWithIO('git', ['checkout', ref, ...additionalArgs]);
};

status = async () => {
await this.execWithIO('git', ['status']);
};

/**
* Returns a list of tags that point to the given commit
*/
getTagsAtCommit = async (commitHash: string) => {
const { stdout: tagsString } = await this.exec('git', ['tag', '--points-at', commitHash]);
return (
tagsString
.split(EOL)
.filter((line: string) => line.trim().length > 0)
// filter out packages not published to npm
.filter((tag: string) => !privatePackages.some(name => tag.includes(name)))
);
};

/**
* Gets the most recent release commit that is reachable from the input commitHash
* If no commitHash is specified, HEAD is used as the default
* By default, the input commitHash is considered in the search (ie if commitHash is a release commit, that commit will be returned)
* To search for the most recent release commit EXCLUDING commitHash, set inclusive=false
*/
getNearestReleaseCommit = async (commitHash: string = 'HEAD', { inclusive }: { inclusive: boolean } = { inclusive: true }) => {
// get the most recent tag before (or at if inclusive=false) the current release tag
const { stdout: previousReleaseTag } = await this.exec('git', ['describe', `${commitHash}${inclusive ? '' : '^'}`, '--abbrev=0']);

// get the commit hash associated with the previous release tag
const { stdout: previousReleaseCommitHash } = await this.exec('git', ['log', '-1', previousReleaseTag, '--pretty=%H']);

// run some sanity checks on the release commit
await this.validateReleaseCommitHash(previousReleaseCommitHash);

return previousReleaseCommitHash;
};

/**
* Given a release commit hash A that has tags for one or more package versions,
* walk through release history and find the previous release tags of all of the packages that were released in commit A
*
* Note that this does not mean just looking up the previous release tags.
* It may be the case that package-A was released in release-5 but the previous release of package-A happened in release-2.
* This method will walk through past release tags until it finds the previous version of all of the input package versions
* If a previous version of some package cannot be found, a warning is printed.
*/
getPreviousReleaseTags = async (releaseCommitHash: string) => {
await this.validateReleaseCommitHash(releaseCommitHash);
const releaseTags = await this.getTagsAtCommit(releaseCommitHash);

// create a set of just the package names (strip off the version suffix) associated with this release commit
const packageNamesRemaining = new Set(releaseTags.map(releaseTagToNameAndVersion).map(nameAndVersion => nameAndVersion.packageName));

let releaseCommitCursor = releaseCommitHash;

// the method return value that we will append release tags to in the loop
const previousReleaseTags: string[] = [];

while (packageNamesRemaining.size > 0) {
try {
releaseCommitCursor = await this.getNearestReleaseCommit(releaseCommitCursor, { inclusive: false });
const releaseTagsAtCursor = await this.getTagsAtCommit(releaseCommitCursor);
releaseTagsAtCursor.forEach(releaseTag => {
const { packageName } = releaseTagToNameAndVersion(releaseTag);
if (packageNamesRemaining.has(packageName)) {
// this means we've found the previous version of "packageNameRemaining" that was released in releaseCommitHash
// so we add it to the return list and remove it from the search set
previousReleaseTags.push(releaseTag);
packageNamesRemaining.delete(packageName);
}
});
} catch {
console.warn(`Previous release not found for ${packageNamesRemaining}.`);
packageNamesRemaining.clear();
}
}

return previousReleaseTags;
};

private validateReleaseCommitHash = async (releaseCommitHash: string) => {
// check that the hash points to a valid commit
const { stdout: hashType } = await this.exec('git', ['cat-file', '-t', releaseCommitHash]);
if (hashType !== 'commit') {
throw new Error(`Hash ${releaseCommitHash} does not point to a commit in the git tree`);
}

// check that the commit hash points to a release commit
const { stdout: commitMessage } = await this.exec('git', ['log', '-1', '--pretty="%s"', releaseCommitHash]);
if (!commitMessage.includes('chore(release)')) {
throw new Error(`
Expected release commit message to include "chore(release)".
Instead found ${commitMessage}.
Make sure commit ${releaseCommitHash} points to a release commit.
`);
}

// check that this commit was made by the amplify-data-ci bot
const { stdout: commitAuthor } = await this.exec('git', ['log', '-1', '--pretty="%an"', releaseCommitHash]);
if (!commitAuthor.includes('amplify-data-ci')) {
throw new Error(`
Expected release commit commit to be authored by amplify-data-ci.
Instead found ${commitAuthor}.
Make sure commit ${releaseCommitHash} points to a release commit.
`);
}

// get the release tags associated with the commit
const releaseTags = await this.getTagsAtCommit(releaseCommitHash);

if (releaseTags.length === 0) {
throw new Error(`
Expected release commit to have associated git tags but none found.
Make sure commit ${releaseCommitHash} points to a release commit.
`);
}
};
}
Loading

0 comments on commit 176a9ef

Please sign in to comment.