Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
258 changes: 255 additions & 3 deletions .github/workflows/release-cloud.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ on:
required: true
default: false
type: boolean
promote_to_production:
description: 'Promote the Beta-tested artifact to Production after E2E'
required: true
default: false
type: boolean
reason:
description: 'Optional reason for manually deploying to Beta'
required: false
Expand All @@ -35,6 +40,7 @@ env:
# existing wire-webapp-staging Elastic Beanstalk environment and URL.
BETA_ENVIRONMENT_NAME: wire-webapp-staging
BETA_WEBAPP_URL: https://wire-webapp-staging.zinfra.io/
PRODUCTION_ENVIRONMENT_NAME: wire-webapp-prod

jobs:
validate_request:
Expand Down Expand Up @@ -238,10 +244,219 @@ jobs:
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
TESTINY_API_KEY: ${{ secrets.TESTINY_API_KEY }}

production_preflight:
name: Production preflight
runs-on: ubuntu-24.04
needs: [build_artifact, create_beta_tag, run_e2e]
permissions:
contents: read
outputs:
production_preflight_result: ${{ steps.production_preflight.outputs.production_preflight_result }}
production_skipped_reason: ${{ steps.production_preflight.outputs.production_skipped_reason }}
production_tag_name: ${{ steps.production_preflight.outputs.production_tag_name }}
should_deploy: ${{ steps.production_preflight.outputs.should_deploy }}

steps:
- name: Checkout release commit
if: ${{ inputs.promote_to_production == true }}
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
with:
persist-credentials: false
fetch-depth: 0
ref: ${{ needs.build_artifact.outputs.release_commit_sha }}

- name: Add repository Yarn wrapper to PATH
if: ${{ inputs.promote_to_production == true }}
run: echo "$GITHUB_WORKSPACE/bin" >> "$GITHUB_PATH"

- name: Setup Node.js
if: ${{ inputs.promote_to_production == true }}
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f
with:
node-version-file: '.nvmrc'
cache: 'yarn'

- name: Install dependencies
if: ${{ inputs.promote_to_production == true }}
run: ./bin/yarn --immutable

- name: Check Production promotion eligibility
id: production_preflight
env:
BETA_TAG_NAME: ${{ needs.create_beta_tag.outputs.beta_tag_name }}
PROMOTE_TO_PRODUCTION: ${{ inputs.promote_to_production }}
RELEASE_COMMIT_SHA: ${{ needs.build_artifact.outputs.release_commit_sha }}
RELEASE_IDENTIFIER: ${{ needs.build_artifact.outputs.release_identifier }}
run: |
set -euo pipefail

if [[ "${PROMOTE_TO_PRODUCTION}" != 'true' ]]; then
production_tag_name="${RELEASE_IDENTIFIER}-production"
production_skipped_reason='Production promotion was not requested'

{
echo 'should_deploy=false'
echo 'production_preflight_result=skipped'
echo "production_skipped_reason=${production_skipped_reason}"
echo "production_tag_name=${production_tag_name}"
} >> "$GITHUB_OUTPUT"

{
echo '## Production promotion'
echo "- Production promotion requested: false"
echo "- Production deployment skipped: true"
echo "- Skip reason: ${production_skipped_reason}"
echo "- Production tag: ${production_tag_name}"
} >> "$GITHUB_STEP_SUMMARY"

exit 0
fi

git fetch --tags origin

production_tag_name="$(./bin/yarn ts-node --project ./tsconfig.bin.json ./bin/releaseMetadataCli.ts production-tag "${RELEASE_IDENTIFIER}")"
beta_tag_commit_sha="$(git rev-parse "${BETA_TAG_NAME}^{commit}")"

if [[ "${beta_tag_commit_sha}" != "${RELEASE_COMMIT_SHA}" ]]; then
echo "Beta tag ${BETA_TAG_NAME} points to ${beta_tag_commit_sha}, expected ${RELEASE_COMMIT_SHA}." >&2
exit 1
fi

if git rev-parse --quiet --verify "refs/tags/${production_tag_name}" >/dev/null; then
production_tag_commit_sha="$(git rev-parse "${production_tag_name}^{commit}")"

if [[ "${production_tag_commit_sha}" != "${RELEASE_COMMIT_SHA}" ]]; then
echo "Production tag ${production_tag_name} points to ${production_tag_commit_sha}, expected ${RELEASE_COMMIT_SHA}." >&2
exit 1
fi

production_skipped_reason="Release is already tagged as Production with ${production_tag_name}"

{
echo 'should_deploy=false'
echo 'production_preflight_result=already_tagged'
echo "production_skipped_reason=${production_skipped_reason}"
echo "production_tag_name=${production_tag_name}"
} >> "$GITHUB_OUTPUT"

{
echo '## Production promotion'
echo "- Production promotion requested: true"
echo "- Production deployment skipped: true"
echo "- Skip reason: ${production_skipped_reason}"
echo "- Production tag: ${production_tag_name}"
} >> "$GITHUB_STEP_SUMMARY"

exit 0
fi

{
echo 'should_deploy=true'
echo 'production_preflight_result=ready'
echo 'production_skipped_reason='
echo "production_tag_name=${production_tag_name}"
} >> "$GITHUB_OUTPUT"

{
echo '## Production promotion'
echo "- Production promotion requested: true"
echo "- Production deployment skipped: false"
echo "- Production tag: ${production_tag_name}"
echo "- Approval gate: ${PRODUCTION_ENVIRONMENT_NAME} GitHub Environment settings"
} >> "$GITHUB_STEP_SUMMARY"

deploy_to_production:
name: Deploy to Production
runs-on: ubuntu-24.04
needs: [build_artifact, production_preflight]
if: ${{ needs.production_preflight.outputs.should_deploy == 'true' }}
permissions: {}
# Production approval is enforced by the wire-webapp-prod GitHub Environment settings.
environment: wire-webapp-prod
concurrency:
group: release-cloud-production
cancel-in-progress: false

steps:
- name: Download build artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
with:
name: ${{ needs.build_artifact.outputs.artifact_name }}

- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37
with:
aws-access-key-id: ${{ secrets.WEBTEAM_AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.WEBTEAM_AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}

- name: Deploy to Production
id: deploy
uses: aws-actions/aws-elasticbeanstalk-deploy@1f56e4e813ae4eb167e69ca324234c336c1df573
with:
aws-region: ${{ env.AWS_REGION }}
application-name: Webapp
environment-name: ${{ env.PRODUCTION_ENVIRONMENT_NAME }}
version-label: ${{ needs.build_artifact.outputs.release_identifier }}-${{ github.run_id }}-${{ github.run_attempt }}-production
deployment-package-path: ebs.zip
wait-for-deployment: true
wait-for-environment-recovery: true
deployment-timeout: 150
use-existing-application-version-if-available: true

create_production_tag:
name: Create Production tag
runs-on: ubuntu-24.04
needs: [build_artifact, production_preflight, deploy_to_production]
if: ${{ needs.production_preflight.outputs.should_deploy == 'true' && needs.deploy_to_production.result == 'success' }}
permissions:
contents: write
outputs:
production_tag_name: ${{ steps.production_tag.outputs.production_tag_name }}

steps:
- name: Checkout release commit
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
with:
fetch-depth: 0
ref: ${{ needs.build_artifact.outputs.release_commit_sha }}

- name: Create and push Production tag
id: production_tag
env:
PRODUCTION_TAG_NAME: ${{ needs.production_preflight.outputs.production_tag_name }}
RELEASE_COMMIT_SHA: ${{ needs.build_artifact.outputs.release_commit_sha }}
run: |
set -euo pipefail

git fetch --tags origin

if git rev-parse --quiet --verify "refs/tags/${PRODUCTION_TAG_NAME}" >/dev/null; then
existing_production_tag_commit_sha="$(git rev-parse "${PRODUCTION_TAG_NAME}^{commit}")"
echo "Production tag ${PRODUCTION_TAG_NAME} already exists at ${existing_production_tag_commit_sha}." >&2
exit 1
fi

git config user.email "zebot@users.noreply.github.com"
git config user.name "Zebot"
git tag --annotate "$PRODUCTION_TAG_NAME" --message "Release $PRODUCTION_TAG_NAME" "$RELEASE_COMMIT_SHA"
git push origin "refs/tags/$PRODUCTION_TAG_NAME"

echo "production_tag_name=${PRODUCTION_TAG_NAME}" >> "$GITHUB_OUTPUT"

summarize_release:
name: Summarize release
runs-on: ubuntu-24.04
needs: [build_artifact, deploy_to_beta, create_beta_tag, run_e2e]
needs:
[
build_artifact,
deploy_to_beta,
create_beta_tag,
run_e2e,
production_preflight,
deploy_to_production,
create_production_tag,
]
if: ${{ always() }}
permissions: {}

Expand All @@ -253,6 +468,16 @@ jobs:
BETA_TAG_NAME: ${{ needs.create_beta_tag.outputs.beta_tag_name }}
E2E_REPORT_URL: ${{ needs.run_e2e.outputs.reportUrl }}
E2E_RESULT: ${{ needs.run_e2e.result }}
PRODUCTION_ARTIFACT_CHECKSUM: ${{ needs.build_artifact.outputs.artifact_checksum }}
PRODUCTION_ARTIFACT_NAME: ${{ needs.build_artifact.outputs.artifact_name }}
PRODUCTION_DEPLOYMENT_RESULT: ${{ needs.deploy_to_production.result }}
PRODUCTION_DEPLOYMENT_SKIPPED: ${{ needs.production_preflight.outputs.should_deploy != 'true' }}
PRODUCTION_ENVIRONMENT_NAME: ${{ env.PRODUCTION_ENVIRONMENT_NAME }}
PRODUCTION_PREFLIGHT_RESULT: ${{ needs.production_preflight.outputs.production_preflight_result }}
PRODUCTION_PROMOTION_REQUESTED: ${{ inputs.promote_to_production }}
PRODUCTION_SKIPPED_REASON: ${{ needs.production_preflight.outputs.production_skipped_reason }}
PRODUCTION_TAG_CREATION_RESULT: ${{ needs.create_production_tag.result }}
PRODUCTION_TAG_NAME: ${{ needs.production_preflight.outputs.production_tag_name }}
RELEASE_BRANCH: ${{ needs.build_artifact.outputs.release_branch }}
RELEASE_COMMIT_SHA: ${{ needs.build_artifact.outputs.release_commit_sha }}
RELEASE_IDENTIFIER: ${{ needs.build_artifact.outputs.release_identifier }}
Expand Down Expand Up @@ -282,12 +507,39 @@ jobs:
fi

echo "- Testiny run name: ${TESTINY_RUN_NAME}"

echo
echo '## Release Cloud Production'
echo "- Production promotion requested: ${PRODUCTION_PROMOTION_REQUESTED}"
echo "- Production preflight result: ${PRODUCTION_PREFLIGHT_RESULT}"
echo "- Production tag: ${PRODUCTION_TAG_NAME}"
echo "- Production deployment skipped: ${PRODUCTION_DEPLOYMENT_SKIPPED}"

if [[ -n "${PRODUCTION_SKIPPED_REASON}" ]]; then
echo "- Production skip reason: ${PRODUCTION_SKIPPED_REASON}"
fi

echo "- Production deployment result: ${PRODUCTION_DEPLOYMENT_RESULT}"
echo "- Production tag creation result: ${PRODUCTION_TAG_CREATION_RESULT}"
echo "- Production environment: ${PRODUCTION_ENVIRONMENT_NAME}"
echo "- Production artifact name: ${PRODUCTION_ARTIFACT_NAME}"
echo "- Production artifact checksum: ${PRODUCTION_ARTIFACT_CHECKSUM}"
echo "- Production approval gate: ${PRODUCTION_ENVIRONMENT_NAME} GitHub Environment settings"
} >> "$GITHUB_STEP_SUMMARY"

notify_release_failure:
name: Notify release failure
runs-on: ubuntu-24.04
needs: [build_artifact, deploy_to_beta, create_beta_tag, run_e2e]
needs:
[
build_artifact,
deploy_to_beta,
create_beta_tag,
run_e2e,
production_preflight,
deploy_to_production,
create_production_tag,
]
if: ${{ always() && contains(join(needs.*.result, ','), 'failure') }}
permissions: {}

Expand All @@ -299,6 +551,6 @@ jobs:
with:
status: failure
text: |
Release Cloud Beta failed for ${{ needs.build_artifact.outputs.release_branch || inputs.release_branch }} (${{ needs.build_artifact.outputs.release_identifier || needs.build_artifact.outputs.release_branch || inputs.release_branch }})
Release Cloud failed for ${{ needs.build_artifact.outputs.release_branch || inputs.release_branch }} (${{ needs.build_artifact.outputs.release_identifier || needs.build_artifact.outputs.release_branch || inputs.release_branch }})
Triggered by: ${{ github.actor }}
Build log: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
20 changes: 20 additions & 0 deletions bin/releaseMetadataCli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,26 @@ describe('releaseMetadataCli', () => {
});
});

it('prints the production tag name for the release identifier', () => {
const actualResult = runCommand(['production-tag', '2026-06-19.1']);

expect(actualResult).toEqual({
errors: [],
exitCode: 0,
outputs: ['2026-06-19.1-production'],
});
});

it('rejects an invalid production tag release identifier', () => {
const actualResult = runCommand(['production-tag', '2026-06-19']);

expect(actualResult).toEqual({
errors: ['Invalid release identifier: 2026-06-19'],
exitCode: 1,
outputs: [],
});
});

it('prints usage text for missing command arguments', () => {
const actualResult = runCommand(['next-beta-tag']);

Expand Down
20 changes: 17 additions & 3 deletions bin/releaseMetadataCli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,31 @@

import process from 'node:process';

import {createNextBetaTagName, extractReleaseIdentifierFromBranchName} from './releaseMetadata';
import {
createNextBetaTagName,
createProductionTagName,
extractReleaseIdentifierFromBranchName,
} from './releaseMetadata';

type ReleaseMetadataCliDependencies = {
readonly writeError: (message: string) => void;
readonly writeOutput: (message: string) => void;
};

const nodeExecutableAndScriptPathArgumentCount = 2;

const usageText = [
'Usage:',
' releaseMetadataCli.ts release-identifier-from-branch <release/YYYY-MM-DD.N>',
' releaseMetadataCli.ts next-beta-tag <YYYY-MM-DD.N> [existing-tag ...]',
' releaseMetadataCli.ts production-tag <YYYY-MM-DD.N>',
].join('\n');

function writeResult(
result: ReturnType<typeof extractReleaseIdentifierFromBranchName> | ReturnType<typeof createNextBetaTagName>,
result:
| ReturnType<typeof extractReleaseIdentifierFromBranchName>
| ReturnType<typeof createNextBetaTagName>
| ReturnType<typeof createProductionTagName>,
dependencies: ReleaseMetadataCliDependencies,
): number {
if (result.isErr) {
Expand All @@ -59,12 +69,16 @@ export function runReleaseMetadataCli(
return writeResult(createNextBetaTagName(primaryValue, remainingValues), dependencies);
}

if (commandName === 'production-tag' && primaryValue !== undefined) {
return writeResult(createProductionTagName(primaryValue), dependencies);
}

dependencies.writeError(usageText);
return 1;
}

if (require.main === module) {
process.exitCode = runReleaseMetadataCli(process.argv.slice(2), {
process.exitCode = runReleaseMetadataCli(process.argv.slice(nodeExecutableAndScriptPathArgumentCount), {
writeError(message): void {
console.error(message);
},
Expand Down
Loading