Skip to content

Commit 176a9ef

Browse files
author
Dane Pilcher
committed
ci: add deprecate workflow
1 parent 0ffe64f commit 176a9ef

15 files changed

+530
-3
lines changed

.codebuild/deprecate.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
version: 0.2
2+
env:
3+
shell: bash
4+
git-credential-helper: yes
5+
phases:
6+
build:
7+
commands:
8+
- source ./shared-scripts.sh && _deprecate

.codebuild/deprecate_workflow.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
version: 0.2
2+
env:
3+
shell: bash
4+
compute-type: BUILD_GENERAL1_SMALL
5+
6+
batch:
7+
fast-fail: false
8+
build-graph:
9+
- identifier: install_linux
10+
buildspec: .codebuild/install_linux.yml
11+
- identifier: deprecate
12+
buildspec: .codebuild/deprecate.yml
13+
depend-on:
14+
- install_linux

.codebuild/install_linux.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
version: 0.2
2+
env:
3+
shell: bash
4+
phases:
5+
build:
6+
commands:
7+
- source ./shared-scripts.sh && _installLinux
8+
artifacts:
9+
files:
10+
- 'shared-scripts.sh'

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,6 @@ package-lock.json
2929
.idea
3030
scripts/.env
3131
.codebuild/debug_workflow.yml
32+
.npmrc
33+
verdaccio-logs.txt
34+
scripts/components/private_packages.ts

.prettierignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@
55
**/package-lock.json
66
**/.eslintrc.js
77
**/tsconfig.json
8-
packages/*/CHANGELOG.md
8+
packages/*/CHANGELOG.md
9+
scripts/components/private_packages.ts

package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
"lint-fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix",
1111
"clean": "lerna run clean && lerna exec yarn rimraf tsconfig.tsbuildinfo && lerna clean --yes && yarn rimraf node_modules",
1212
"build": "lerna run build",
13-
"production-build": "yarn --ignore-engines --frozen-lockfile && lerna run build --concurrency 3 --stream",
13+
"production-install": "yarn --ignore-engines --frozen-lockfile",
14+
"production-build": "yarn production-install && lerna run build --concurrency 3 --stream",
1415
"publish:main": "lerna publish --canary --exact --force-publish --preid=dev --dist-tag=dev --include-merged-tags --conventional-prerelease --no-verify-access --yes",
1516
"publish:release": "lerna publish --exact --conventional-commits --message 'chore(release): Publish [ci skip]' --no-verify-access --yes",
1617
"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",
@@ -35,6 +36,7 @@
3536
"verify-api-extract": "yarn extract-api && ./scripts/verify-extract-api.sh",
3637
"trigger-release": "source ./scripts/cloud-release.sh && triggerRelease",
3738
"trigger-tag-release": "source ./scripts/cloud-release.sh && triggerTagRelease",
39+
"trigger-deprecate-release": "source ./scripts/cloud-release.sh && deprecateRelease",
3840
"view-test-artifact": "./scripts/view-test-artifacts.sh",
3941
"cleanup-stale-resources": "source ./scripts/cloud-utils.sh && cleanupStaleResources",
4042
"cloud-e2e": "source scripts/cloud-utils.sh && cloudE2E",
@@ -44,7 +46,9 @@
4446
"cloud-e2e-debug": "source scripts/cloud-utils.sh && cloudE2EDebug",
4547
"authenticate-e2e-profile": "source scripts/cloud-utils.sh && authenticateWithE2EProfile",
4648
"extract-dependency-licenses": "./scripts/extract-dependency-licenses.sh",
47-
"verify-dependency-licenses-extract": "yarn extract-dependency-licenses && ./scripts/verify-dependency-licenses.sh"
49+
"verify-dependency-licenses-extract": "yarn extract-dependency-licenses && ./scripts/verify-dependency-licenses.sh",
50+
"deprecate": "ts-node scripts/deprecate_release.ts",
51+
"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"
4852
},
4953
"bugs": {
5054
"url": "https://github.com/aws-amplify/amplify-codegen/issues"

scripts/cloud-release.sh

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ source ./scripts/cloud-utils.sh
33
export RELEASE_ROLE_NAME=CodebuildDeveloper
44
export RELEASE_PROFILE_NAME=AmplifyAPIPluginRelease
55
export RELEASE_PROJECT_NAME=amplify-codegen-release-workflow
6+
export DEPRECATE_PROJECT_NAME=amplify-codegen-deprecate-workflow
67

78
function triggerRelease {
89
triggerProjectBatch $RELEASE_ACCOUNT_PROD $RELEASE_ROLE_NAME "${RELEASE_PROFILE_NAME}Prod" $RELEASE_PROJECT_NAME "release"
@@ -20,3 +21,13 @@ function triggerTagRelease {
2021
fi
2122
triggerProjectBatch $RELEASE_ACCOUNT_PROD $RELEASE_ROLE_NAME "${RELEASE_PROFILE_NAME}Prod" $RELEASE_PROJECT_NAME $branch_name
2223
}
24+
25+
function deprecateRelease {
26+
DEPRECATION_MESSAGE=$1
27+
SEARCH_FOR_RELEASE_STARTING_FROM=$2
28+
USE_NPM_REGISTRY=$3
29+
triggerProjectBatchWithEnvOverrides $RELEASE_ACCOUNT_PROD $RELEASE_ROLE_NAME "${RELEASE_PROFILE_NAME}Prod" $DEPRECATE_PROJECT_NAME "release" \
30+
name=DEPRECATION_MESSAGE,value=\""$DEPRECATION_MESSAGE"\",type=PLAINTEXT \
31+
name=SEARCH_FOR_RELEASE_STARTING_FROM,value=$SEARCH_FOR_RELEASE_STARTING_FROM,type=PLAINTEXT \
32+
name=USE_NPM_REGISTRY,value=$USE_NPM_REGISTRY,type=PLAINTEXT
33+
}

scripts/cloud-utils.sh

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,23 @@ function triggerProjectBatch {
3434
echo "https://$REGION.console.aws.amazon.com/codesuite/codebuild/$account_number/projects/$project_name/batch/$RESULT?region=$REGION"
3535
}
3636

37+
function triggerProjectBatchWithEnvOverrides {
38+
account_number=$1
39+
role_name=$2
40+
profile_name=$3
41+
project_name=$4
42+
target_branch=$5
43+
shift 5
44+
authenticate $account_number $role_name $profile_name
45+
echo AWS Account: $account_number
46+
echo Project: $project_name
47+
echo Target Branch: $target_branch
48+
RESULT=$(aws codebuild start-build-batch --region=$REGION --profile="${profile_name}" --project-name $project_name --source-version=$target_branch \
49+
--environment-variables-override name=BRANCH_NAME,value=$target_branch,type=PLAINTEXT "$@" \
50+
--query 'buildBatch.id' --output text)
51+
echo "https://$REGION.console.aws.amazon.com/codesuite/codebuild/$account_number/projects/$project_name/batch/$RESULT?region=$REGION"
52+
}
53+
3754
function triggerProject {
3855
account_number=$1
3956
role_name=$2

scripts/components/dist_tag_mover.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { EOL } from 'os';
2+
import { NpmClient } from './npm_client.js';
3+
import { releaseTagToNameAndVersion } from './release_tag_to_name_and_version';
4+
5+
type DistTagMoveAction = {
6+
/**
7+
* An NPM dist-tag
8+
*/
9+
distTag: string;
10+
/**
11+
* This is a string of the form <packageName>@<version>
12+
*/
13+
releaseTag: string;
14+
};
15+
16+
/**
17+
* Handles moving npm dist-tags from one package version to another
18+
*/
19+
export class DistTagMover {
20+
/**
21+
* Initialize with an npmClient
22+
*/
23+
constructor(private readonly npmClient: NpmClient) {}
24+
25+
/**
26+
* Given a list of sourceReleaseTags and destReleaseTags,
27+
* any npm dist-tags that are pointing to a sourceReleaseTag will be moved to point to the corresponding destReleaseTag
28+
*/
29+
moveDistTags = async (sourceReleaseTags: string[], destReleaseTags: string[]) => {
30+
const moveActions: DistTagMoveAction[] = [];
31+
32+
for (const sourceReleaseTag of sourceReleaseTags) {
33+
const { packageName, version: sourceVersion } = releaseTagToNameAndVersion(sourceReleaseTag);
34+
35+
const { 'dist-tags': distTags } = await this.npmClient.getPackageInfo(sourceReleaseTag);
36+
37+
Object.entries(distTags).forEach(([tagName, versionAtTag]) => {
38+
if (versionAtTag !== sourceVersion) {
39+
return;
40+
}
41+
const destReleaseTag = destReleaseTags.find(releaseTag => releaseTag.includes(packageName));
42+
if (!destReleaseTag) {
43+
console.warn(`No corresponding destination release tag found for ${sourceReleaseTag}. latest tag not moved.`);
44+
} else {
45+
moveActions.push({
46+
releaseTag: destReleaseTag,
47+
distTag: tagName,
48+
});
49+
}
50+
});
51+
}
52+
53+
for (const { distTag, releaseTag } of moveActions) {
54+
console.log(`Moving dist tag "${distTag}" to release tag ${releaseTag}`);
55+
await this.npmClient.setDistTag(releaseTag, distTag);
56+
console.log(`Done!${EOL}`);
57+
}
58+
};
59+
}

scripts/components/git_client.ts

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
const execa = require('execa');
2+
import { writeFile } from 'fs/promises';
3+
import { EOL } from 'os';
4+
import * as path from 'path';
5+
import { releaseTagToNameAndVersion } from './release_tag_to_name_and_version';
6+
import privatePackages from './private_packages';
7+
8+
/**
9+
* Client for programmatically interacting with the local git cli
10+
*/
11+
export class GitClient {
12+
private readonly gitignorePath: string;
13+
14+
/**
15+
* execaCommand that allows us to capture stdout
16+
*/
17+
private readonly exec;
18+
19+
/**
20+
* execaCommand that pipes buffers to process buffers
21+
*/
22+
private readonly execWithIO;
23+
24+
/**
25+
* Initialize with an optional directory to operate in.
26+
* Defaults to the process cwd.
27+
*/
28+
constructor(cwd?: string) {
29+
this.exec = (command: string, args: string[]) => execa.sync(command, args, { cwd });
30+
this.execWithIO = (command: string, args: string[]) => execa.sync(command, args, { cwd, stdio: 'inherit' });
31+
this.gitignorePath = cwd ? path.join(cwd, '.gitignore') : '.gitignore';
32+
}
33+
34+
init = async () => {
35+
await this.exec('git', ['init']);
36+
await writeFile(this.gitignorePath, `node_modules${EOL}`);
37+
};
38+
39+
/**
40+
* Throws if there are uncommitted changes in the repo
41+
*/
42+
ensureWorkingTreeIsClean = async () => {
43+
const { stdout } = await this.exec('git', ['status', '--porcelain']);
44+
const isDirty = stdout.trim();
45+
if (isDirty) {
46+
throw new Error('Dirty working tree detected. Commit or stash changes to continue.');
47+
}
48+
};
49+
50+
getCurrentBranch = async () => {
51+
const { stdout: currentBranch } = await this.exec('git', ['branch', '--show-current']);
52+
return currentBranch;
53+
};
54+
55+
/**
56+
* Switches to branchName. Creates the branch if it does not exist.
57+
*/
58+
switchToBranch = async (branchName: string) => {
59+
const { stdout: branchResult } = await this.exec('git', ['branch', '-l', branchName]);
60+
const branchExists = branchResult.trim().length > 0;
61+
if (branchExists) {
62+
await this.execWithIO('git', ['switch', branchName]);
63+
} else {
64+
await this.execWithIO('git', ['switch', '-c', branchName]);
65+
}
66+
};
67+
68+
/**
69+
* Stages and commits all current changes
70+
*/
71+
commitAllChanges = async (message: string) => {
72+
await this.execWithIO('git', ['add', '.']);
73+
await this.execWithIO('git', ['commit', '--message', message, '--allow-empty']);
74+
};
75+
76+
/**
77+
* Push to the remote
78+
*/
79+
push = async ({ force }: { force: boolean } = { force: false }) => {
80+
await this.execWithIO('git', ['push', force ? '--force' : '']);
81+
};
82+
83+
fetchTags = async () => {
84+
await this.execWithIO('git', ['fetch', '--tags']);
85+
};
86+
87+
checkout = async (ref: string, paths: string[] = []) => {
88+
const additionalArgs = paths.length > 0 ? ['--', ...paths] : [];
89+
await this.execWithIO('git', ['checkout', ref, ...additionalArgs]);
90+
};
91+
92+
status = async () => {
93+
await this.execWithIO('git', ['status']);
94+
};
95+
96+
/**
97+
* Returns a list of tags that point to the given commit
98+
*/
99+
getTagsAtCommit = async (commitHash: string) => {
100+
const { stdout: tagsString } = await this.exec('git', ['tag', '--points-at', commitHash]);
101+
return (
102+
tagsString
103+
.split(EOL)
104+
.filter((line: string) => line.trim().length > 0)
105+
// filter out packages not published to npm
106+
.filter((tag: string) => !privatePackages.some(name => tag.includes(name)))
107+
);
108+
};
109+
110+
/**
111+
* Gets the most recent release commit that is reachable from the input commitHash
112+
* If no commitHash is specified, HEAD is used as the default
113+
* By default, the input commitHash is considered in the search (ie if commitHash is a release commit, that commit will be returned)
114+
* To search for the most recent release commit EXCLUDING commitHash, set inclusive=false
115+
*/
116+
getNearestReleaseCommit = async (commitHash: string = 'HEAD', { inclusive }: { inclusive: boolean } = { inclusive: true }) => {
117+
// get the most recent tag before (or at if inclusive=false) the current release tag
118+
const { stdout: previousReleaseTag } = await this.exec('git', ['describe', `${commitHash}${inclusive ? '' : '^'}`, '--abbrev=0']);
119+
120+
// get the commit hash associated with the previous release tag
121+
const { stdout: previousReleaseCommitHash } = await this.exec('git', ['log', '-1', previousReleaseTag, '--pretty=%H']);
122+
123+
// run some sanity checks on the release commit
124+
await this.validateReleaseCommitHash(previousReleaseCommitHash);
125+
126+
return previousReleaseCommitHash;
127+
};
128+
129+
/**
130+
* Given a release commit hash A that has tags for one or more package versions,
131+
* walk through release history and find the previous release tags of all of the packages that were released in commit A
132+
*
133+
* Note that this does not mean just looking up the previous release tags.
134+
* It may be the case that package-A was released in release-5 but the previous release of package-A happened in release-2.
135+
* This method will walk through past release tags until it finds the previous version of all of the input package versions
136+
* If a previous version of some package cannot be found, a warning is printed.
137+
*/
138+
getPreviousReleaseTags = async (releaseCommitHash: string) => {
139+
await this.validateReleaseCommitHash(releaseCommitHash);
140+
const releaseTags = await this.getTagsAtCommit(releaseCommitHash);
141+
142+
// create a set of just the package names (strip off the version suffix) associated with this release commit
143+
const packageNamesRemaining = new Set(releaseTags.map(releaseTagToNameAndVersion).map(nameAndVersion => nameAndVersion.packageName));
144+
145+
let releaseCommitCursor = releaseCommitHash;
146+
147+
// the method return value that we will append release tags to in the loop
148+
const previousReleaseTags: string[] = [];
149+
150+
while (packageNamesRemaining.size > 0) {
151+
try {
152+
releaseCommitCursor = await this.getNearestReleaseCommit(releaseCommitCursor, { inclusive: false });
153+
const releaseTagsAtCursor = await this.getTagsAtCommit(releaseCommitCursor);
154+
releaseTagsAtCursor.forEach(releaseTag => {
155+
const { packageName } = releaseTagToNameAndVersion(releaseTag);
156+
if (packageNamesRemaining.has(packageName)) {
157+
// this means we've found the previous version of "packageNameRemaining" that was released in releaseCommitHash
158+
// so we add it to the return list and remove it from the search set
159+
previousReleaseTags.push(releaseTag);
160+
packageNamesRemaining.delete(packageName);
161+
}
162+
});
163+
} catch {
164+
console.warn(`Previous release not found for ${packageNamesRemaining}.`);
165+
packageNamesRemaining.clear();
166+
}
167+
}
168+
169+
return previousReleaseTags;
170+
};
171+
172+
private validateReleaseCommitHash = async (releaseCommitHash: string) => {
173+
// check that the hash points to a valid commit
174+
const { stdout: hashType } = await this.exec('git', ['cat-file', '-t', releaseCommitHash]);
175+
if (hashType !== 'commit') {
176+
throw new Error(`Hash ${releaseCommitHash} does not point to a commit in the git tree`);
177+
}
178+
179+
// check that the commit hash points to a release commit
180+
const { stdout: commitMessage } = await this.exec('git', ['log', '-1', '--pretty="%s"', releaseCommitHash]);
181+
if (!commitMessage.includes('chore(release)')) {
182+
throw new Error(`
183+
Expected release commit message to include "chore(release)".
184+
Instead found ${commitMessage}.
185+
Make sure commit ${releaseCommitHash} points to a release commit.
186+
`);
187+
}
188+
189+
// check that this commit was made by the amplify-data-ci bot
190+
const { stdout: commitAuthor } = await this.exec('git', ['log', '-1', '--pretty="%an"', releaseCommitHash]);
191+
if (!commitAuthor.includes('amplify-data-ci')) {
192+
throw new Error(`
193+
Expected release commit commit to be authored by amplify-data-ci.
194+
Instead found ${commitAuthor}.
195+
Make sure commit ${releaseCommitHash} points to a release commit.
196+
`);
197+
}
198+
199+
// get the release tags associated with the commit
200+
const releaseTags = await this.getTagsAtCommit(releaseCommitHash);
201+
202+
if (releaseTags.length === 0) {
203+
throw new Error(`
204+
Expected release commit to have associated git tags but none found.
205+
Make sure commit ${releaseCommitHash} points to a release commit.
206+
`);
207+
}
208+
};
209+
}

0 commit comments

Comments
 (0)