@codyswann/lisa 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +867 -0
- package/all/copy-overwrite/.claude/README.md +205 -0
- package/all/copy-overwrite/.claude/agents/agent-architect.md +311 -0
- package/all/copy-overwrite/.claude/agents/codebase-analyzer.md +146 -0
- package/all/copy-overwrite/.claude/agents/codebase-locator.md +125 -0
- package/all/copy-overwrite/.claude/agents/codebase-pattern-finder.md +237 -0
- package/all/copy-overwrite/.claude/agents/git-history-analyzer.md +183 -0
- package/all/copy-overwrite/.claude/agents/hooks-expert.md +74 -0
- package/all/copy-overwrite/.claude/agents/skill-evaluator.md +246 -0
- package/all/copy-overwrite/.claude/agents/slash-command-architect.md +87 -0
- package/all/copy-overwrite/.claude/agents/web-search-researcher.md +112 -0
- package/all/copy-overwrite/.claude/commands/git/commit-and-submit-pr.md +8 -0
- package/all/copy-overwrite/.claude/commands/git/commit.md +44 -0
- package/all/copy-overwrite/.claude/commands/git/prune.md +34 -0
- package/all/copy-overwrite/.claude/commands/git/submit-pr.md +50 -0
- package/all/copy-overwrite/.claude/commands/jira/create.md +50 -0
- package/all/copy-overwrite/.claude/commands/jira/verify.md +34 -0
- package/all/copy-overwrite/.claude/commands/project/archive.md +8 -0
- package/all/copy-overwrite/.claude/commands/project/bootstrap.md +49 -0
- package/all/copy-overwrite/.claude/commands/project/complete-task.md +7 -0
- package/all/copy-overwrite/.claude/commands/project/debrief.md +65 -0
- package/all/copy-overwrite/.claude/commands/project/execute.md +94 -0
- package/all/copy-overwrite/.claude/commands/project/implement.md +42 -0
- package/all/copy-overwrite/.claude/commands/project/local-code-review.md +88 -0
- package/all/copy-overwrite/.claude/commands/project/lower-code-complexity.md +74 -0
- package/all/copy-overwrite/.claude/commands/project/plan.md +314 -0
- package/all/copy-overwrite/.claude/commands/project/research.md +248 -0
- package/all/copy-overwrite/.claude/commands/project/review.md +63 -0
- package/all/copy-overwrite/.claude/commands/project/setup.md +19 -0
- package/all/copy-overwrite/.claude/commands/project/verify.md +38 -0
- package/all/copy-overwrite/.claude/commands/pull-request/review.md +12 -0
- package/all/copy-overwrite/.claude/commands/rules/format-md.md +72 -0
- package/all/copy-overwrite/.claude/commands/sonarqube/check.md +6 -0
- package/all/copy-overwrite/.claude/commands/sonarqube/fix.md +3 -0
- package/all/copy-overwrite/.claude/hooks/README.md +301 -0
- package/all/copy-overwrite/.claude/hooks/notify-ntfy.sh +181 -0
- package/all/copy-overwrite/.claude/settings.json +41 -0
- package/all/copy-overwrite/.claude/settings.local.json.example +14 -0
- package/all/copy-overwrite/.claude/skills/coding-philosophy/SKILL.md +405 -0
- package/all/copy-overwrite/.claude/skills/coding-philosophy/references/function-structure.md +416 -0
- package/all/copy-overwrite/.claude/skills/coding-philosophy/references/immutable-patterns.md +316 -0
- package/all/copy-overwrite/.claude/skills/prompt-complexity-scorer/SKILL.md +118 -0
- package/all/copy-overwrite/.claude/skills/skill-creator/LICENSE.txt +202 -0
- package/all/copy-overwrite/.claude/skills/skill-creator/SKILL.md +210 -0
- package/all/copy-overwrite/.claude/skills/skill-creator/scripts/__pycache__/quick_validate.cpython-312.pyc +0 -0
- package/all/copy-overwrite/.claude/skills/skill-creator/scripts/init_skill.py +303 -0
- package/all/copy-overwrite/.claude/skills/skill-creator/scripts/package_skill.py +110 -0
- package/all/copy-overwrite/.claude/skills/skill-creator/scripts/quick_validate.py +65 -0
- package/all/copy-overwrite/CLAUDE.md +77 -0
- package/all/copy-overwrite/HUMAN.md +17 -0
- package/all/copy-overwrite/specs/.keep +0 -0
- package/all/create-only/PROJECT_RULES.md +0 -0
- package/cdk/merge/package.json +20 -0
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +107 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/prompts.d.ts +45 -0
- package/dist/cli/prompts.d.ts.map +1 -0
- package/dist/cli/prompts.js +58 -0
- package/dist/cli/prompts.js.map +1 -0
- package/dist/core/config.d.ts +73 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +36 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/index.d.ts +4 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +4 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/lisa.d.ts +81 -0
- package/dist/core/lisa.d.ts.map +1 -0
- package/dist/core/lisa.js +459 -0
- package/dist/core/lisa.js.map +1 -0
- package/dist/core/manifest.d.ts +58 -0
- package/dist/core/manifest.d.ts.map +1 -0
- package/dist/core/manifest.js +104 -0
- package/dist/core/manifest.js.map +1 -0
- package/dist/detection/detector.interface.d.ts +15 -0
- package/dist/detection/detector.interface.d.ts.map +1 -0
- package/dist/detection/detector.interface.js +2 -0
- package/dist/detection/detector.interface.js.map +1 -0
- package/dist/detection/detectors/cdk.d.ts +10 -0
- package/dist/detection/detectors/cdk.d.ts.map +1 -0
- package/dist/detection/detectors/cdk.js +34 -0
- package/dist/detection/detectors/cdk.js.map +1 -0
- package/dist/detection/detectors/expo.d.ts +10 -0
- package/dist/detection/detectors/expo.d.ts.map +1 -0
- package/dist/detection/detectors/expo.js +30 -0
- package/dist/detection/detectors/expo.js.map +1 -0
- package/dist/detection/detectors/nestjs.d.ts +10 -0
- package/dist/detection/detectors/nestjs.d.ts.map +1 -0
- package/dist/detection/detectors/nestjs.js +34 -0
- package/dist/detection/detectors/nestjs.js.map +1 -0
- package/dist/detection/detectors/npm-package.d.ts +13 -0
- package/dist/detection/detectors/npm-package.d.ts.map +1 -0
- package/dist/detection/detectors/npm-package.js +30 -0
- package/dist/detection/detectors/npm-package.js.map +1 -0
- package/dist/detection/detectors/typescript.d.ts +10 -0
- package/dist/detection/detectors/typescript.d.ts.map +1 -0
- package/dist/detection/detectors/typescript.js +25 -0
- package/dist/detection/detectors/typescript.js.map +1 -0
- package/dist/detection/index.d.ts +24 -0
- package/dist/detection/index.d.ts.map +1 -0
- package/dist/detection/index.js +57 -0
- package/dist/detection/index.js.map +1 -0
- package/dist/errors/index.d.ts +69 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +110 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/logging/console-logger.d.ts +12 -0
- package/dist/logging/console-logger.d.ts.map +1 -0
- package/dist/logging/console-logger.js +22 -0
- package/dist/logging/console-logger.js.map +1 -0
- package/dist/logging/index.d.ts +4 -0
- package/dist/logging/index.d.ts.map +1 -0
- package/dist/logging/index.js +3 -0
- package/dist/logging/index.js.map +1 -0
- package/dist/logging/logger.interface.d.ts +20 -0
- package/dist/logging/logger.interface.d.ts.map +1 -0
- package/dist/logging/logger.interface.js +2 -0
- package/dist/logging/logger.interface.js.map +1 -0
- package/dist/logging/silent-logger.d.ts +12 -0
- package/dist/logging/silent-logger.d.ts.map +1 -0
- package/dist/logging/silent-logger.js +21 -0
- package/dist/logging/silent-logger.js.map +1 -0
- package/dist/strategies/copy-contents.d.ts +14 -0
- package/dist/strategies/copy-contents.d.ts.map +1 -0
- package/dist/strategies/copy-contents.js +69 -0
- package/dist/strategies/copy-contents.js.map +1 -0
- package/dist/strategies/copy-overwrite.d.ts +14 -0
- package/dist/strategies/copy-overwrite.d.ts.map +1 -0
- package/dist/strategies/copy-overwrite.js +47 -0
- package/dist/strategies/copy-overwrite.js.map +1 -0
- package/dist/strategies/create-only.d.ts +13 -0
- package/dist/strategies/create-only.d.ts.map +1 -0
- package/dist/strategies/create-only.js +30 -0
- package/dist/strategies/create-only.js.map +1 -0
- package/dist/strategies/index.d.ts +31 -0
- package/dist/strategies/index.d.ts.map +1 -0
- package/dist/strategies/index.js +52 -0
- package/dist/strategies/index.js.map +1 -0
- package/dist/strategies/merge.d.ts +13 -0
- package/dist/strategies/merge.d.ts.map +1 -0
- package/dist/strategies/merge.js +60 -0
- package/dist/strategies/merge.js.map +1 -0
- package/dist/strategies/strategy.interface.d.ts +31 -0
- package/dist/strategies/strategy.interface.d.ts.map +1 -0
- package/dist/strategies/strategy.interface.js +2 -0
- package/dist/strategies/strategy.interface.js.map +1 -0
- package/dist/transaction/backup.d.ts +38 -0
- package/dist/transaction/backup.d.ts.map +1 -0
- package/dist/transaction/backup.js +97 -0
- package/dist/transaction/backup.js.map +1 -0
- package/dist/transaction/index.d.ts +4 -0
- package/dist/transaction/index.d.ts.map +1 -0
- package/dist/transaction/index.js +3 -0
- package/dist/transaction/index.js.map +1 -0
- package/dist/transaction/transaction.d.ts +34 -0
- package/dist/transaction/transaction.d.ts.map +1 -0
- package/dist/transaction/transaction.js +68 -0
- package/dist/transaction/transaction.js.map +1 -0
- package/dist/utils/file-operations.d.ts +29 -0
- package/dist/utils/file-operations.d.ts.map +1 -0
- package/dist/utils/file-operations.js +84 -0
- package/dist/utils/file-operations.js.map +1 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +4 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/json-utils.d.ts +22 -0
- package/dist/utils/json-utils.d.ts.map +1 -0
- package/dist/utils/json-utils.js +57 -0
- package/dist/utils/json-utils.js.map +1 -0
- package/dist/utils/path-utils.d.ts +21 -0
- package/dist/utils/path-utils.d.ts.map +1 -0
- package/dist/utils/path-utils.js +35 -0
- package/dist/utils/path-utils.js.map +1 -0
- package/eslint-plugin-code-organization/README.md +149 -0
- package/eslint-plugin-code-organization/__tests__/enforce-statement-order.test.js +468 -0
- package/eslint-plugin-code-organization/index.js +23 -0
- package/eslint-plugin-code-organization/package.json +10 -0
- package/eslint-plugin-code-organization/rules/enforce-statement-order.js +157 -0
- package/expo/copy-overwrite/.claude/skills/apollo-client/SKILL.md +238 -0
- package/expo/copy-overwrite/.claude/skills/apollo-client/references/mutation-patterns.md +360 -0
- package/expo/copy-overwrite/.claude/skills/atomic-design-gluestack/SKILL.md +360 -0
- package/expo/copy-overwrite/.claude/skills/atomic-design-gluestack/references/atomic-levels.md +417 -0
- package/expo/copy-overwrite/.claude/skills/atomic-design-gluestack/references/folder-structure.md +257 -0
- package/expo/copy-overwrite/.claude/skills/atomic-design-gluestack/references/gluestack-mapping.md +233 -0
- package/expo/copy-overwrite/.claude/skills/atomic-design-gluestack/scripts/validate_atomic_structure.py +327 -0
- package/expo/copy-overwrite/.claude/skills/container-view-pattern/SKILL.md +299 -0
- package/expo/copy-overwrite/.claude/skills/container-view-pattern/references/examples.md +749 -0
- package/expo/copy-overwrite/.claude/skills/container-view-pattern/references/patterns.md +318 -0
- package/expo/copy-overwrite/.claude/skills/container-view-pattern/scripts/create_component.py +198 -0
- package/expo/copy-overwrite/.claude/skills/container-view-pattern/scripts/validate_component.py +207 -0
- package/expo/copy-overwrite/.claude/skills/cross-platform-compatibility/SKILL.md +268 -0
- package/expo/copy-overwrite/.claude/skills/cross-platform-compatibility/references/common-issues.md +619 -0
- package/expo/copy-overwrite/.claude/skills/cross-platform-compatibility/references/file-extensions.md +340 -0
- package/expo/copy-overwrite/.claude/skills/cross-platform-compatibility/references/platform-api.md +276 -0
- package/expo/copy-overwrite/.claude/skills/cross-platform-compatibility/scripts/validate_cross_platform.py +414 -0
- package/expo/copy-overwrite/.claude/skills/directory-structure/SKILL.md +202 -0
- package/expo/copy-overwrite/.claude/skills/directory-structure/scripts/validate_structure.py +443 -0
- package/expo/copy-overwrite/.claude/skills/expo-env-config/SKILL.md +309 -0
- package/expo/copy-overwrite/.claude/skills/expo-env-config/references/validation-patterns.md +417 -0
- package/expo/copy-overwrite/.claude/skills/expo-router-best-practices/SKILL.md +431 -0
- package/expo/copy-overwrite/.claude/skills/expo-router-best-practices/references/official-docs.md +290 -0
- package/expo/copy-overwrite/.claude/skills/expo-router-best-practices/scripts/generate-route.py +169 -0
- package/expo/copy-overwrite/.claude/skills/gluestack-nativewind/SKILL.md +411 -0
- package/expo/copy-overwrite/.claude/skills/gluestack-nativewind/references/color-tokens.md +343 -0
- package/expo/copy-overwrite/.claude/skills/gluestack-nativewind/references/component-mapping.md +307 -0
- package/expo/copy-overwrite/.claude/skills/gluestack-nativewind/references/spacing-scale.md +300 -0
- package/expo/copy-overwrite/.claude/skills/gluestack-nativewind/scripts/validate_styling.py +354 -0
- package/expo/copy-overwrite/.claude/skills/local-state/SKILL.md +362 -0
- package/expo/copy-overwrite/.claude/skills/local-state/references/async-storage.md +505 -0
- package/expo/copy-overwrite/.claude/skills/local-state/references/persistence-patterns.md +711 -0
- package/expo/copy-overwrite/.claude/skills/local-state/references/reactive-variables.md +446 -0
- package/expo/copy-overwrite/.claude/skills/playwright-selectors/SKILL.md +223 -0
- package/expo/copy-overwrite/.claude/skills/testing-library/SKILL.md +319 -0
- package/expo/copy-overwrite/.claude/skills/testing-library/references/async-patterns.md +420 -0
- package/expo/copy-overwrite/.claude/skills/testing-library/references/expo-router-testing.md +556 -0
- package/expo/copy-overwrite/.claude/skills/testing-library/references/mocking-patterns.md +590 -0
- package/expo/copy-overwrite/.claude/skills/testing-library/references/query-priority.md +291 -0
- package/expo/copy-overwrite/.easignore.extra +2 -0
- package/expo/copy-overwrite/.mcp.json +33 -0
- package/expo/copy-overwrite/eslint-plugin-component-structure/README.md +234 -0
- package/expo/copy-overwrite/eslint-plugin-component-structure/__tests__/plugin-index.test.js +84 -0
- package/expo/copy-overwrite/eslint-plugin-component-structure/__tests__/require-memo-in-view.test.js +196 -0
- package/expo/copy-overwrite/eslint-plugin-component-structure/__tests__/single-component-per-file.test.js +289 -0
- package/expo/copy-overwrite/eslint-plugin-component-structure/index.js +32 -0
- package/expo/copy-overwrite/eslint-plugin-component-structure/package.json +10 -0
- package/expo/copy-overwrite/eslint-plugin-component-structure/rules/enforce-component-structure.js +230 -0
- package/expo/copy-overwrite/eslint-plugin-component-structure/rules/no-return-in-view.js +91 -0
- package/expo/copy-overwrite/eslint-plugin-component-structure/rules/require-memo-in-view.js +178 -0
- package/expo/copy-overwrite/eslint-plugin-component-structure/rules/single-component-per-file.js +238 -0
- package/expo/copy-overwrite/eslint-plugin-ui-standards/README.md +260 -0
- package/expo/copy-overwrite/eslint-plugin-ui-standards/index.js +29 -0
- package/expo/copy-overwrite/eslint-plugin-ui-standards/package.json +10 -0
- package/expo/copy-overwrite/eslint-plugin-ui-standards/rules/no-classname-outside-ui.js +51 -0
- package/expo/copy-overwrite/eslint-plugin-ui-standards/rules/no-direct-rn-imports.js +55 -0
- package/expo/copy-overwrite/eslint-plugin-ui-standards/rules/no-inline-styles.js +73 -0
- package/expo/copy-overwrite/eslint.config.mjs +560 -0
- package/expo/copy-overwrite/lighthouserc.js +194 -0
- package/expo/create-only/lighthouserc-config.json +28 -0
- package/expo/merge/package.json +132 -0
- package/lisa.sh +35 -0
- package/nestjs/copy-overwrite/.claude/skills/nestjs-graphql/SKILL.md +176 -0
- package/nestjs/copy-overwrite/.claude/skills/nestjs-graphql/references/advanced-features.md +527 -0
- package/nestjs/copy-overwrite/.claude/skills/nestjs-graphql/references/project-patterns.md +483 -0
- package/nestjs/copy-overwrite/.claude/skills/nestjs-graphql/references/quick-start.md +257 -0
- package/nestjs/copy-overwrite/.claude/skills/nestjs-graphql/references/resolvers-mutations.md +413 -0
- package/nestjs/copy-overwrite/.claude/skills/nestjs-graphql/references/types-scalars.md +513 -0
- package/nestjs/copy-overwrite/.claude/skills/nestjs-rules/SKILL.md +536 -0
- package/nestjs/copy-overwrite/.claude/skills/typeorm-patterns/SKILL.md +275 -0
- package/nestjs/copy-overwrite/.claude/skills/typeorm-patterns/references/configuration-patterns.md +487 -0
- package/nestjs/copy-overwrite/.claude/skills/typeorm-patterns/references/entity-patterns.md +450 -0
- package/nestjs/copy-overwrite/.claude/skills/typeorm-patterns/references/observability-patterns.md +536 -0
- package/nestjs/merge/package.json +75 -0
- package/package.json +124 -0
- package/typescript/copy-contents/.husky/commit-msg +91 -0
- package/typescript/copy-contents/.husky/pre-commit +96 -0
- package/typescript/copy-contents/.husky/pre-push +211 -0
- package/typescript/copy-overwrite/.claude/hooks/format-on-edit.sh +74 -0
- package/typescript/copy-overwrite/.claude/hooks/install_pkgs.sh +59 -0
- package/typescript/copy-overwrite/.claude/hooks/lint-on-edit.sh +103 -0
- package/typescript/copy-overwrite/.claude/skills/jsdoc-best-practices/SKILL.md +388 -0
- package/typescript/copy-overwrite/.github/README.md +455 -0
- package/typescript/copy-overwrite/.github/dependabot.yml +40 -0
- package/typescript/copy-overwrite/.github/k6/BROWSER_TESTING_NOTE.md +129 -0
- package/typescript/copy-overwrite/.github/k6/INTEGRATION_GUIDE.md +354 -0
- package/typescript/copy-overwrite/.github/k6/README.md +386 -0
- package/typescript/copy-overwrite/.github/k6/SCENARIO_SELECTION_GUIDE.md +264 -0
- package/typescript/copy-overwrite/.github/k6/examples/customer-deploy-integration.yml +115 -0
- package/typescript/copy-overwrite/.github/k6/examples/data-driven-test.js +268 -0
- package/typescript/copy-overwrite/.github/k6/scenarios/load.js +142 -0
- package/typescript/copy-overwrite/.github/k6/scenarios/load.json +27 -0
- package/typescript/copy-overwrite/.github/k6/scenarios/smoke.js +26 -0
- package/typescript/copy-overwrite/.github/k6/scenarios/smoke.json +20 -0
- package/typescript/copy-overwrite/.github/k6/scenarios/soak.js +244 -0
- package/typescript/copy-overwrite/.github/k6/scenarios/soak.json +29 -0
- package/typescript/copy-overwrite/.github/k6/scenarios/spike.js +180 -0
- package/typescript/copy-overwrite/.github/k6/scenarios/spike.json +32 -0
- package/typescript/copy-overwrite/.github/k6/scenarios/stress.js +206 -0
- package/typescript/copy-overwrite/.github/k6/scenarios/stress.json +38 -0
- package/typescript/copy-overwrite/.github/k6/scripts/api-test.js +452 -0
- package/typescript/copy-overwrite/.github/k6/scripts/default-test.js +185 -0
- package/typescript/copy-overwrite/.github/k6/thresholds/normal.json +30 -0
- package/typescript/copy-overwrite/.github/k6/thresholds/relaxed.json +21 -0
- package/typescript/copy-overwrite/.github/k6/thresholds/strict.json +29 -0
- package/typescript/copy-overwrite/.github/workflows/build.yml +72 -0
- package/typescript/copy-overwrite/.github/workflows/ci.yml +49 -0
- package/typescript/copy-overwrite/.github/workflows/claude.yml +51 -0
- package/typescript/copy-overwrite/.github/workflows/create-github-issue-on-failure.yml +113 -0
- package/typescript/copy-overwrite/.github/workflows/create-jira-issue-on-failure.yml +195 -0
- package/typescript/copy-overwrite/.github/workflows/create-sentry-issue-on-failure.yml +267 -0
- package/typescript/copy-overwrite/.github/workflows/deploy.yml +228 -0
- package/typescript/copy-overwrite/.github/workflows/k6-load-test-README.md +230 -0
- package/typescript/copy-overwrite/.github/workflows/lighthouse.yml +68 -0
- package/typescript/copy-overwrite/.github/workflows/load-test.yml +282 -0
- package/typescript/copy-overwrite/.github/workflows/quality.yml +1737 -0
- package/typescript/copy-overwrite/.github/workflows/release.yml +1599 -0
- package/typescript/copy-overwrite/.gitleaksignore +28 -0
- package/typescript/copy-overwrite/.nvmrc +1 -0
- package/typescript/copy-overwrite/.prettierignore +23 -0
- package/typescript/copy-overwrite/.prettierrc.json +22 -0
- package/typescript/copy-overwrite/.versionrc +42 -0
- package/typescript/copy-overwrite/.yamllint +20 -0
- package/typescript/copy-overwrite/commitlint.config.js +11 -0
- package/typescript/copy-overwrite/eslint-plugin-code-organization/README.md +149 -0
- package/typescript/copy-overwrite/eslint-plugin-code-organization/__tests__/enforce-statement-order.test.js +468 -0
- package/typescript/copy-overwrite/eslint-plugin-code-organization/index.js +23 -0
- package/typescript/copy-overwrite/eslint-plugin-code-organization/package.json +10 -0
- package/typescript/copy-overwrite/eslint-plugin-code-organization/rules/enforce-statement-order.js +157 -0
- package/typescript/copy-overwrite/eslint.config.mjs +390 -0
- package/typescript/copy-overwrite/eslint.ignore.config.json +57 -0
- package/typescript/copy-overwrite/eslint.thresholds.config.json +5 -0
- package/typescript/github-rulesets/base.json +106 -0
- package/typescript/merge/.claude/settings.json +28 -0
- package/typescript/merge/package.json +71 -0
|
@@ -0,0 +1,1599 @@
|
|
|
1
|
+
name: Enhanced Release Workflow v4 (with Enterprise Features)
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_call:
|
|
5
|
+
inputs:
|
|
6
|
+
environment:
|
|
7
|
+
description: 'Target environment for the release'
|
|
8
|
+
required: true
|
|
9
|
+
type: string
|
|
10
|
+
skip_jobs:
|
|
11
|
+
description: 'Comma-separated list of jobs to skip (test,type-check,lint,format,audit,cdk-synth)'
|
|
12
|
+
required: false
|
|
13
|
+
type: string
|
|
14
|
+
default: ''
|
|
15
|
+
force_release:
|
|
16
|
+
description: 'Force create a release even if no changes detected'
|
|
17
|
+
required: false
|
|
18
|
+
type: boolean
|
|
19
|
+
default: false
|
|
20
|
+
release_strategy:
|
|
21
|
+
description: 'Version strategy: standard-version, semantic, calendar, custom'
|
|
22
|
+
required: false
|
|
23
|
+
type: string
|
|
24
|
+
default: 'standard-version'
|
|
25
|
+
prerelease:
|
|
26
|
+
description: 'Create pre-release (alpha, beta, rc)'
|
|
27
|
+
required: false
|
|
28
|
+
type: string
|
|
29
|
+
default: ''
|
|
30
|
+
custom_version:
|
|
31
|
+
description: 'Custom version number (only for custom strategy)'
|
|
32
|
+
required: false
|
|
33
|
+
type: string
|
|
34
|
+
default: ''
|
|
35
|
+
jira_project_key:
|
|
36
|
+
description: 'JIRA project key for release creation'
|
|
37
|
+
required: false
|
|
38
|
+
type: string
|
|
39
|
+
default: ''
|
|
40
|
+
release_notes_template:
|
|
41
|
+
description: 'Template for release notes (default, detailed, security)'
|
|
42
|
+
required: false
|
|
43
|
+
type: string
|
|
44
|
+
default: 'default'
|
|
45
|
+
debug:
|
|
46
|
+
description: 'Enable debug logging'
|
|
47
|
+
required: false
|
|
48
|
+
type: boolean
|
|
49
|
+
default: false
|
|
50
|
+
require_approval:
|
|
51
|
+
description: 'Require approval before creating release'
|
|
52
|
+
required: false
|
|
53
|
+
type: boolean
|
|
54
|
+
default: false
|
|
55
|
+
approval_environment:
|
|
56
|
+
description: 'GitHub environment for approval (if require_approval is true)'
|
|
57
|
+
required: false
|
|
58
|
+
type: string
|
|
59
|
+
default: ''
|
|
60
|
+
require_signatures:
|
|
61
|
+
description: 'Require signed releases (needs RELEASE_SIGNING_KEY secret)'
|
|
62
|
+
required: false
|
|
63
|
+
type: boolean
|
|
64
|
+
default: false
|
|
65
|
+
check_dependencies:
|
|
66
|
+
description: 'Check service dependencies before release'
|
|
67
|
+
required: false
|
|
68
|
+
type: boolean
|
|
69
|
+
default: false
|
|
70
|
+
override_blackout:
|
|
71
|
+
description: 'Override blackout period restrictions'
|
|
72
|
+
required: false
|
|
73
|
+
type: boolean
|
|
74
|
+
default: false
|
|
75
|
+
emergency_release:
|
|
76
|
+
description: 'Mark as emergency release'
|
|
77
|
+
required: false
|
|
78
|
+
type: boolean
|
|
79
|
+
default: false
|
|
80
|
+
generate_sbom:
|
|
81
|
+
description: 'Generate Software Bill of Materials'
|
|
82
|
+
required: false
|
|
83
|
+
type: boolean
|
|
84
|
+
default: false
|
|
85
|
+
node_version:
|
|
86
|
+
description: 'Node.js version to use'
|
|
87
|
+
required: true
|
|
88
|
+
type: string
|
|
89
|
+
package_manager:
|
|
90
|
+
description: 'Package manager to use (npm, yarn, pnpm, bun)'
|
|
91
|
+
required: false
|
|
92
|
+
type: string
|
|
93
|
+
default: 'npm'
|
|
94
|
+
sourcemaps:
|
|
95
|
+
description: 'Path to source maps for Sentry release (optional)'
|
|
96
|
+
required: false
|
|
97
|
+
type: string
|
|
98
|
+
default: ''
|
|
99
|
+
outputs:
|
|
100
|
+
version:
|
|
101
|
+
description: 'The new version number'
|
|
102
|
+
value: ${{ jobs.version.outputs.version }}
|
|
103
|
+
tag:
|
|
104
|
+
description: 'The new version tag'
|
|
105
|
+
value: ${{ jobs.version.outputs.tag }}
|
|
106
|
+
release_url:
|
|
107
|
+
description: 'URL of the created GitHub release'
|
|
108
|
+
value: ${{ jobs.github_release.outputs.release_url }}
|
|
109
|
+
release_id:
|
|
110
|
+
description: 'Release correlation ID for tracking'
|
|
111
|
+
value: ${{ jobs.release_init.outputs.correlation_id }}
|
|
112
|
+
release_metrics_url:
|
|
113
|
+
description: 'URL to release metrics dashboard'
|
|
114
|
+
value: ${{ jobs.release_analytics.outputs.dashboard_url }}
|
|
115
|
+
signed:
|
|
116
|
+
description: 'Whether the release was signed'
|
|
117
|
+
value: ${{ jobs.release_signing.outputs.signed }}
|
|
118
|
+
attestation_url:
|
|
119
|
+
description: 'URL to release attestation'
|
|
120
|
+
value: ${{ jobs.release_attestation.outputs.attestation_url }}
|
|
121
|
+
compliance_status:
|
|
122
|
+
description: 'Compliance validation status'
|
|
123
|
+
value: ${{ jobs.release_compliance.outputs.status }}
|
|
124
|
+
sentry_release_url:
|
|
125
|
+
description: 'URL to Sentry release'
|
|
126
|
+
value: ${{ jobs.sentry_release.outputs.sentry_release_url }}
|
|
127
|
+
jira_release_created:
|
|
128
|
+
description: 'Whether the Jira release was created'
|
|
129
|
+
value: ${{ jobs.jira_release.outputs.jira_release_created }}
|
|
130
|
+
jira_release_url:
|
|
131
|
+
description: 'URL to Jira release'
|
|
132
|
+
value: ${{ jobs.jira_release.outputs.jira_release_url }}
|
|
133
|
+
secrets:
|
|
134
|
+
DEPLOY_KEY:
|
|
135
|
+
description: 'SSH deploy key with write access to bypass branch protection (add to ruleset bypass list)'
|
|
136
|
+
required: false
|
|
137
|
+
RELEASE_SIGNING_KEY:
|
|
138
|
+
required: false
|
|
139
|
+
SIGNING_KEY_ID:
|
|
140
|
+
required: false
|
|
141
|
+
SIGNING_KEY_PASSPHRASE:
|
|
142
|
+
required: false
|
|
143
|
+
SENTRY_AUTH_TOKEN:
|
|
144
|
+
required: false
|
|
145
|
+
SENTRY_ORG:
|
|
146
|
+
required: false
|
|
147
|
+
SENTRY_PROJECT:
|
|
148
|
+
required: false
|
|
149
|
+
|
|
150
|
+
jobs:
|
|
151
|
+
# Initialize release with correlation ID and logging
|
|
152
|
+
release_init:
|
|
153
|
+
name: 🚀 Initialize Release
|
|
154
|
+
runs-on: ubuntu-latest
|
|
155
|
+
outputs:
|
|
156
|
+
correlation_id: ${{ steps.init.outputs.correlation_id }}
|
|
157
|
+
start_time: ${{ steps.init.outputs.start_time }}
|
|
158
|
+
release_logger: ${{ steps.init.outputs.logger_created }}
|
|
159
|
+
steps:
|
|
160
|
+
- name: Generate Release Correlation ID
|
|
161
|
+
id: init
|
|
162
|
+
run: |
|
|
163
|
+
# Generate unique correlation ID
|
|
164
|
+
CORRELATION_ID="rel-$(date +%Y%m%d%H%M%S)-${{ github.run_id }}"
|
|
165
|
+
echo "correlation_id=$CORRELATION_ID" >> $GITHUB_OUTPUT
|
|
166
|
+
echo "start_time=$(date +%s)" >> $GITHUB_OUTPUT
|
|
167
|
+
echo "logger_created=true" >> $GITHUB_OUTPUT
|
|
168
|
+
|
|
169
|
+
# Initialize release context
|
|
170
|
+
echo "## 🚀 Release Workflow Started" >> $GITHUB_STEP_SUMMARY
|
|
171
|
+
echo "- **Correlation ID**: $CORRELATION_ID" >> $GITHUB_STEP_SUMMARY
|
|
172
|
+
echo "- **Environment**: ${{ inputs.environment }}" >> $GITHUB_STEP_SUMMARY
|
|
173
|
+
echo "- **Strategy**: ${{ inputs.release_strategy }}" >> $GITHUB_STEP_SUMMARY
|
|
174
|
+
echo "- **Approval Required**: ${{ inputs.require_approval }}" >> $GITHUB_STEP_SUMMARY
|
|
175
|
+
echo "- **Signatures Required**: ${{ inputs.require_signatures }}" >> $GITHUB_STEP_SUMMARY
|
|
176
|
+
echo "- **Emergency Release**: ${{ inputs.emergency_release }}" >> $GITHUB_STEP_SUMMARY
|
|
177
|
+
|
|
178
|
+
# Check if this is a promotion merge (env branch to env branch)
|
|
179
|
+
check_promotion:
|
|
180
|
+
name: 🔍 Check Promotion
|
|
181
|
+
runs-on: ubuntu-latest
|
|
182
|
+
outputs:
|
|
183
|
+
is_promotion: ${{ steps.check.outputs.is_promotion }}
|
|
184
|
+
skip_reason: ${{ steps.check.outputs.skip_reason }}
|
|
185
|
+
steps:
|
|
186
|
+
- uses: actions/checkout@v4
|
|
187
|
+
with:
|
|
188
|
+
fetch-depth: 10 # Need some history to analyze merge commits
|
|
189
|
+
|
|
190
|
+
- name: Check if this is a promotion merge
|
|
191
|
+
id: check
|
|
192
|
+
run: |
|
|
193
|
+
COMMIT_MSG=$(git log -1 --pretty=%B)
|
|
194
|
+
IS_PROMOTION=false
|
|
195
|
+
SKIP_REASON=""
|
|
196
|
+
|
|
197
|
+
# Environment branches that we track
|
|
198
|
+
ENV_BRANCHES="dev|staging|main|master|production"
|
|
199
|
+
|
|
200
|
+
# Check for merge commits from environment branches to environment branches
|
|
201
|
+
# Pattern 1: "Merge branch 'staging' into main"
|
|
202
|
+
if echo "$COMMIT_MSG" | grep -qiE "^Merge branch ['\"]?($ENV_BRANCHES)['\"]? into ($ENV_BRANCHES)"; then
|
|
203
|
+
IS_PROMOTION=true
|
|
204
|
+
SKIP_REASON="Merge from environment branch detected"
|
|
205
|
+
fi
|
|
206
|
+
|
|
207
|
+
# Pattern 2: "Merge pull request #123 from org/staging" or similar
|
|
208
|
+
if echo "$COMMIT_MSG" | grep -qiE "^Merge pull request.*from .*/($ENV_BRANCHES)$"; then
|
|
209
|
+
IS_PROMOTION=true
|
|
210
|
+
SKIP_REASON="PR merge from environment branch detected"
|
|
211
|
+
fi
|
|
212
|
+
|
|
213
|
+
# Pattern 3: Check parent commits for promotion patterns
|
|
214
|
+
# If both parents are from environment branches, it's likely a promotion
|
|
215
|
+
PARENT_COUNT=$(git rev-list --parents -n 1 HEAD | wc -w)
|
|
216
|
+
if [ "$PARENT_COUNT" -gt 2 ]; then
|
|
217
|
+
# This is a merge commit, check if parent branches are environment branches
|
|
218
|
+
PARENT_BRANCHES=$(git log -1 --pretty=%P | xargs -n1 git branch -a --contains 2>/dev/null | grep -E "($ENV_BRANCHES)" | head -2)
|
|
219
|
+
PARENT_ENV_COUNT=$(echo "$PARENT_BRANCHES" | grep -cE "($ENV_BRANCHES)" || true)
|
|
220
|
+
if [ "$PARENT_ENV_COUNT" -ge 2 ]; then
|
|
221
|
+
IS_PROMOTION=true
|
|
222
|
+
SKIP_REASON="Merge between environment branches detected"
|
|
223
|
+
fi
|
|
224
|
+
fi
|
|
225
|
+
|
|
226
|
+
echo "is_promotion=$IS_PROMOTION" >> $GITHUB_OUTPUT
|
|
227
|
+
echo "skip_reason=$SKIP_REASON" >> $GITHUB_OUTPUT
|
|
228
|
+
|
|
229
|
+
if [ "$IS_PROMOTION" = "true" ]; then
|
|
230
|
+
echo "## ⏭️ Promotion Detected" >> $GITHUB_STEP_SUMMARY
|
|
231
|
+
echo "" >> $GITHUB_STEP_SUMMARY
|
|
232
|
+
echo "**Reason**: $SKIP_REASON" >> $GITHUB_STEP_SUMMARY
|
|
233
|
+
echo "" >> $GITHUB_STEP_SUMMARY
|
|
234
|
+
echo "Version bump will be skipped to avoid duplicate versioning." >> $GITHUB_STEP_SUMMARY
|
|
235
|
+
echo "" >> $GITHUB_STEP_SUMMARY
|
|
236
|
+
echo "**Commit message**:" >> $GITHUB_STEP_SUMMARY
|
|
237
|
+
echo '```' >> $GITHUB_STEP_SUMMARY
|
|
238
|
+
echo "$COMMIT_MSG" | head -5 >> $GITHUB_STEP_SUMMARY
|
|
239
|
+
echo '```' >> $GITHUB_STEP_SUMMARY
|
|
240
|
+
else
|
|
241
|
+
echo "✅ Not a promotion merge - version bump will proceed" >> $GITHUB_STEP_SUMMARY
|
|
242
|
+
fi
|
|
243
|
+
|
|
244
|
+
# Check release schedule and blackout periods
|
|
245
|
+
release_schedule:
|
|
246
|
+
name: 📅 Release Schedule Check
|
|
247
|
+
needs: [release_init]
|
|
248
|
+
runs-on: ubuntu-latest
|
|
249
|
+
outputs:
|
|
250
|
+
allowed: ${{ steps.schedule.outputs.allowed }}
|
|
251
|
+
blackout_reason: ${{ steps.schedule.outputs.reason }}
|
|
252
|
+
steps:
|
|
253
|
+
- name: Check Release Schedule
|
|
254
|
+
id: schedule
|
|
255
|
+
run: |
|
|
256
|
+
# Check if we're in a blackout period
|
|
257
|
+
CURRENT_DATE=$(date +%Y-%m-%d)
|
|
258
|
+
CURRENT_TIME=$(date +%H:%M)
|
|
259
|
+
CURRENT_DAY=$(date +%A)
|
|
260
|
+
CURRENT_HOUR=$(date +%H)
|
|
261
|
+
|
|
262
|
+
IS_BLACKOUT=false
|
|
263
|
+
BLACKOUT_REASON=""
|
|
264
|
+
|
|
265
|
+
# Check environment-specific blackout rules
|
|
266
|
+
case "${{ inputs.environment }}" in
|
|
267
|
+
production|prod|main)
|
|
268
|
+
# No weekend releases for production
|
|
269
|
+
if [[ "$CURRENT_DAY" == "Saturday" || "$CURRENT_DAY" == "Sunday" ]]; then
|
|
270
|
+
IS_BLACKOUT=true
|
|
271
|
+
BLACKOUT_REASON="Weekend release restriction for production"
|
|
272
|
+
fi
|
|
273
|
+
|
|
274
|
+
# No late night releases (10 PM - 6 AM)
|
|
275
|
+
if [[ $CURRENT_HOUR -ge 22 || $CURRENT_HOUR -lt 6 ]]; then
|
|
276
|
+
IS_BLACKOUT=true
|
|
277
|
+
BLACKOUT_REASON="Late night release restriction (10 PM - 6 AM)"
|
|
278
|
+
fi
|
|
279
|
+
|
|
280
|
+
# Holiday blackouts
|
|
281
|
+
HOLIDAYS=(
|
|
282
|
+
"2024-12-24:2025-01-02:Holiday Season"
|
|
283
|
+
"2025-07-03:2025-07-05:Independence Day"
|
|
284
|
+
"2025-11-27:2025-11-29:Thanksgiving"
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
for holiday in "${HOLIDAYS[@]}"; do
|
|
288
|
+
IFS=':' read -r start end name <<< "$holiday"
|
|
289
|
+
if [[ "$CURRENT_DATE" > "$start" && "$CURRENT_DATE" < "$end" ]]; then
|
|
290
|
+
IS_BLACKOUT=true
|
|
291
|
+
BLACKOUT_REASON="$name blackout period"
|
|
292
|
+
break
|
|
293
|
+
fi
|
|
294
|
+
done
|
|
295
|
+
;;
|
|
296
|
+
|
|
297
|
+
staging|stage)
|
|
298
|
+
# Staging has fewer restrictions
|
|
299
|
+
if [[ "$CURRENT_DAY" == "Sunday" && $CURRENT_HOUR -ge 20 ]]; then
|
|
300
|
+
IS_BLACKOUT=true
|
|
301
|
+
BLACKOUT_REASON="Sunday evening restriction for staging"
|
|
302
|
+
fi
|
|
303
|
+
;;
|
|
304
|
+
esac
|
|
305
|
+
|
|
306
|
+
# Handle blackout
|
|
307
|
+
if [ "$IS_BLACKOUT" == "true" ]; then
|
|
308
|
+
if [ "${{ inputs.override_blackout }}" == "true" ]; then
|
|
309
|
+
echo "⚠️ Blackout override enabled: $BLACKOUT_REASON"
|
|
310
|
+
echo "allowed=true" >> $GITHUB_OUTPUT
|
|
311
|
+
echo "reason=Override: $BLACKOUT_REASON" >> $GITHUB_OUTPUT
|
|
312
|
+
else
|
|
313
|
+
echo "❌ Release blocked: $BLACKOUT_REASON" >> $GITHUB_STEP_SUMMARY
|
|
314
|
+
echo "Use override_blackout=true to force release" >> $GITHUB_STEP_SUMMARY
|
|
315
|
+
echo "allowed=false" >> $GITHUB_OUTPUT
|
|
316
|
+
echo "reason=$BLACKOUT_REASON" >> $GITHUB_OUTPUT
|
|
317
|
+
fi
|
|
318
|
+
else
|
|
319
|
+
echo "✅ Release schedule check passed" >> $GITHUB_STEP_SUMMARY
|
|
320
|
+
echo "allowed=true" >> $GITHUB_OUTPUT
|
|
321
|
+
echo "reason=No blackout restrictions" >> $GITHUB_OUTPUT
|
|
322
|
+
fi
|
|
323
|
+
|
|
324
|
+
- name: Enforce Schedule
|
|
325
|
+
if: steps.schedule.outputs.allowed != 'true'
|
|
326
|
+
run: |
|
|
327
|
+
echo "::error::Release blocked due to blackout period: ${{ steps.schedule.outputs.reason }}"
|
|
328
|
+
exit 1
|
|
329
|
+
|
|
330
|
+
# Optional approval gate
|
|
331
|
+
release_approval:
|
|
332
|
+
name: 🚦 Release Approval
|
|
333
|
+
needs: [release_init, release_schedule]
|
|
334
|
+
if: ${{ inputs.require_approval == true }}
|
|
335
|
+
runs-on: ubuntu-latest
|
|
336
|
+
environment:
|
|
337
|
+
name: ${{ inputs.approval_environment || inputs.environment }}
|
|
338
|
+
outputs:
|
|
339
|
+
approved: ${{ steps.approval.outputs.approved }}
|
|
340
|
+
approver: ${{ steps.approval.outputs.approver }}
|
|
341
|
+
approval_time: ${{ steps.approval.outputs.time }}
|
|
342
|
+
steps:
|
|
343
|
+
- name: Request Approval
|
|
344
|
+
id: approval
|
|
345
|
+
run: |
|
|
346
|
+
echo "## 🚦 Release Approval Required" >> $GITHUB_STEP_SUMMARY
|
|
347
|
+
echo "" >> $GITHUB_STEP_SUMMARY
|
|
348
|
+
echo "**Environment**: ${{ inputs.environment }}" >> $GITHUB_STEP_SUMMARY
|
|
349
|
+
echo "**Requester**: @${{ github.actor }}" >> $GITHUB_STEP_SUMMARY
|
|
350
|
+
echo "**Correlation ID**: ${{ needs.release_init.outputs.correlation_id }}" >> $GITHUB_STEP_SUMMARY
|
|
351
|
+
echo "" >> $GITHUB_STEP_SUMMARY
|
|
352
|
+
echo "This release requires manual approval to proceed." >> $GITHUB_STEP_SUMMARY
|
|
353
|
+
|
|
354
|
+
# If we reach here, approval was granted (environment protection rules)
|
|
355
|
+
echo "approved=true" >> $GITHUB_OUTPUT
|
|
356
|
+
echo "approver=${{ github.actor }}" >> $GITHUB_OUTPUT
|
|
357
|
+
echo "time=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> $GITHUB_OUTPUT
|
|
358
|
+
|
|
359
|
+
echo "✅ Release approved by environment protection rules" >> $GITHUB_STEP_SUMMARY
|
|
360
|
+
|
|
361
|
+
# Run quality checks
|
|
362
|
+
quality:
|
|
363
|
+
name: 🔍 Quality Checks
|
|
364
|
+
needs: [release_init, release_schedule]
|
|
365
|
+
uses: ./.github/workflows/quality.yml
|
|
366
|
+
with:
|
|
367
|
+
skip_jobs: ${{ inputs.skip_jobs }}
|
|
368
|
+
node_version: ${{ inputs.node_version }}
|
|
369
|
+
package_manager: ${{ inputs.package_manager }}
|
|
370
|
+
secrets: inherit
|
|
371
|
+
|
|
372
|
+
# Version management with observability
|
|
373
|
+
version:
|
|
374
|
+
name: 📦 Version Management
|
|
375
|
+
needs: [release_init, quality, release_approval, check_promotion]
|
|
376
|
+
if: |
|
|
377
|
+
always() && !cancelled() &&
|
|
378
|
+
needs.quality.result == 'success' &&
|
|
379
|
+
(needs.release_approval.result == 'success' || needs.release_approval.result == 'skipped') &&
|
|
380
|
+
needs.check_promotion.outputs.is_promotion != 'true'
|
|
381
|
+
runs-on: ubuntu-latest
|
|
382
|
+
permissions:
|
|
383
|
+
contents: write
|
|
384
|
+
env:
|
|
385
|
+
HUSKY: '0'
|
|
386
|
+
outputs:
|
|
387
|
+
version: ${{ steps.version.outputs.version }}
|
|
388
|
+
tag: ${{ steps.version.outputs.tag }}
|
|
389
|
+
prerelease: ${{ steps.version.outputs.prerelease }}
|
|
390
|
+
bump_type: ${{ steps.version.outputs.bump_type }}
|
|
391
|
+
strategy: ${{ inputs.release_strategy }}
|
|
392
|
+
changelog_generated: ${{ steps.changelog.outputs.generated }}
|
|
393
|
+
steps:
|
|
394
|
+
# Use SSH deploy key for pushing to protected branches
|
|
395
|
+
# Setup: Add DEPLOY_KEY secret and add the deploy key to ruleset bypass list
|
|
396
|
+
- uses: actions/checkout@v4
|
|
397
|
+
with:
|
|
398
|
+
fetch-depth: 0
|
|
399
|
+
ssh-key: ${{ secrets.DEPLOY_KEY }}
|
|
400
|
+
|
|
401
|
+
- name: Setup Release Logger
|
|
402
|
+
id: logger
|
|
403
|
+
run: |
|
|
404
|
+
# Create logging utilities
|
|
405
|
+
cat > release-logger.sh << 'EOF'
|
|
406
|
+
#!/bin/bash
|
|
407
|
+
|
|
408
|
+
# Correlation ID from init job
|
|
409
|
+
RELEASE_CORRELATION_ID="${{ needs.release_init.outputs.correlation_id }}"
|
|
410
|
+
export RELEASE_CORRELATION_ID
|
|
411
|
+
|
|
412
|
+
# Structured log function
|
|
413
|
+
log_release_event() {
|
|
414
|
+
local event_type="$1"
|
|
415
|
+
local event_status="$2"
|
|
416
|
+
local event_message="$3"
|
|
417
|
+
shift 3
|
|
418
|
+
|
|
419
|
+
# Additional key-value pairs
|
|
420
|
+
local additional_fields=""
|
|
421
|
+
while [ $# -gt 0 ] && [ $# -ge 2 ]; do
|
|
422
|
+
additional_fields="$additional_fields,\"$1\":\"$2\""
|
|
423
|
+
shift 2
|
|
424
|
+
done
|
|
425
|
+
|
|
426
|
+
# Create structured log entry
|
|
427
|
+
local log_entry=$(jq -n \
|
|
428
|
+
--arg correlation_id "$RELEASE_CORRELATION_ID" \
|
|
429
|
+
--arg workflow_id "${{ github.run_id }}" \
|
|
430
|
+
--arg event_type "$event_type" \
|
|
431
|
+
--arg event_status "$event_status" \
|
|
432
|
+
--arg event_message "$event_message" \
|
|
433
|
+
--arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)" \
|
|
434
|
+
--arg environment "${{ inputs.environment }}" \
|
|
435
|
+
--arg version "${VERSION:-unknown}" \
|
|
436
|
+
--arg tag "${TAG:-unknown}" \
|
|
437
|
+
--arg actor "${{ github.actor }}" \
|
|
438
|
+
--arg repository "${{ github.repository }}" \
|
|
439
|
+
--arg ref "${{ github.ref }}" \
|
|
440
|
+
--arg sha "${{ github.sha }}" \
|
|
441
|
+
"{
|
|
442
|
+
\"correlation_id\": \$correlation_id,
|
|
443
|
+
\"workflow_id\": \$workflow_id,
|
|
444
|
+
\"event_type\": \$event_type,
|
|
445
|
+
\"event_status\": \$event_status,
|
|
446
|
+
\"event_message\": \$event_message,
|
|
447
|
+
\"timestamp\": \$timestamp,
|
|
448
|
+
\"release\": {
|
|
449
|
+
\"environment\": \$environment,
|
|
450
|
+
\"version\": \$version,
|
|
451
|
+
\"tag\": \$tag
|
|
452
|
+
},
|
|
453
|
+
\"context\": {
|
|
454
|
+
\"actor\": \$actor,
|
|
455
|
+
\"repository\": \$repository,
|
|
456
|
+
\"ref\": \$ref,
|
|
457
|
+
\"sha\": \$sha
|
|
458
|
+
}${additional_fields}
|
|
459
|
+
}")
|
|
460
|
+
|
|
461
|
+
# Output to stdout and file
|
|
462
|
+
echo "$log_entry" | tee -a release-events.jsonl
|
|
463
|
+
|
|
464
|
+
# Also log to GitHub Actions if debug enabled
|
|
465
|
+
if [ "${{ inputs.debug }}" == "true" ]; then
|
|
466
|
+
echo "::notice title=Release Event::$event_type - $event_status: $event_message"
|
|
467
|
+
fi
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
export -f log_release_event
|
|
471
|
+
EOF
|
|
472
|
+
|
|
473
|
+
chmod +x release-logger.sh
|
|
474
|
+
source ./release-logger.sh
|
|
475
|
+
|
|
476
|
+
# Log version process start
|
|
477
|
+
log_release_event "version_management" "started" "Version determination process started" \
|
|
478
|
+
"strategy" "${{ inputs.release_strategy }}" \
|
|
479
|
+
"approval_required" "${{ inputs.require_approval }}" \
|
|
480
|
+
"signatures_required" "${{ inputs.require_signatures }}"
|
|
481
|
+
|
|
482
|
+
- name: Setup Node.js
|
|
483
|
+
uses: actions/setup-node@v4
|
|
484
|
+
with:
|
|
485
|
+
node-version: ${{ inputs.node_version }}
|
|
486
|
+
cache: ${{ inputs.package_manager != 'bun' && inputs.package_manager || '' }}
|
|
487
|
+
|
|
488
|
+
- name: Setup Bun
|
|
489
|
+
if: inputs.package_manager == 'bun'
|
|
490
|
+
uses: oven-sh/setup-bun@v2
|
|
491
|
+
with:
|
|
492
|
+
bun-version: latest
|
|
493
|
+
|
|
494
|
+
- name: Install dependencies
|
|
495
|
+
run: |
|
|
496
|
+
if [ "${{ inputs.package_manager }}" = "npm" ]; then
|
|
497
|
+
npm ci
|
|
498
|
+
elif [ "${{ inputs.package_manager }}" = "yarn" ]; then
|
|
499
|
+
yarn install --frozen-lockfile
|
|
500
|
+
elif [ "${{ inputs.package_manager }}" = "pnpm" ]; then
|
|
501
|
+
pnpm install --frozen-lockfile
|
|
502
|
+
elif [ "${{ inputs.package_manager }}" = "bun" ]; then
|
|
503
|
+
bun install --frozen-lockfile
|
|
504
|
+
else
|
|
505
|
+
npm ci
|
|
506
|
+
fi
|
|
507
|
+
|
|
508
|
+
- name: Determine Version
|
|
509
|
+
id: version
|
|
510
|
+
run: |
|
|
511
|
+
source ./release-logger.sh
|
|
512
|
+
|
|
513
|
+
# Version determination logic (same as v3)
|
|
514
|
+
case "${{ inputs.release_strategy }}" in
|
|
515
|
+
"standard-version")
|
|
516
|
+
VERSION=$(npx standard-version --dry-run | grep "tagging release" | awk '{print $4}')
|
|
517
|
+
;;
|
|
518
|
+
"semantic")
|
|
519
|
+
if git log --format=%B -n 50 --no-merges | grep -qE "^(BREAKING CHANGE:|.*!:)"; then
|
|
520
|
+
BUMP_TYPE="major"
|
|
521
|
+
elif git log --format=%B -n 50 --no-merges | grep -qE "^feat(\(.+\))?:"; then
|
|
522
|
+
BUMP_TYPE="minor"
|
|
523
|
+
else
|
|
524
|
+
BUMP_TYPE="patch"
|
|
525
|
+
fi
|
|
526
|
+
|
|
527
|
+
CURRENT_VERSION=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
|
|
528
|
+
VERSION=$(npx semver -i $BUMP_TYPE ${CURRENT_VERSION#v})
|
|
529
|
+
;;
|
|
530
|
+
"calendar")
|
|
531
|
+
VERSION=$(date +%Y.%m.%d)
|
|
532
|
+
if [ -n "${{ inputs.prerelease }}" ]; then
|
|
533
|
+
VERSION="${VERSION}-${{ inputs.prerelease }}.$(date +%H%M)"
|
|
534
|
+
fi
|
|
535
|
+
;;
|
|
536
|
+
"custom")
|
|
537
|
+
VERSION="${{ inputs.custom_version }}"
|
|
538
|
+
if [ -z "$VERSION" ]; then
|
|
539
|
+
log_release_event "version_error" "failed" "Custom version not provided"
|
|
540
|
+
exit 1
|
|
541
|
+
fi
|
|
542
|
+
;;
|
|
543
|
+
esac
|
|
544
|
+
|
|
545
|
+
# Add prerelease suffix if specified
|
|
546
|
+
if [ -n "${{ inputs.prerelease }}" ] && [ "${{ inputs.release_strategy }}" != "calendar" ]; then
|
|
547
|
+
VERSION="${VERSION}-${{ inputs.prerelease }}.$(date +%s)"
|
|
548
|
+
echo "prerelease=true" >> $GITHUB_OUTPUT
|
|
549
|
+
fi
|
|
550
|
+
|
|
551
|
+
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
|
552
|
+
echo "tag=v$VERSION" >> $GITHUB_OUTPUT
|
|
553
|
+
echo "bump_type=${BUMP_TYPE:-custom}" >> $GITHUB_OUTPUT
|
|
554
|
+
|
|
555
|
+
export VERSION TAG="v$VERSION"
|
|
556
|
+
|
|
557
|
+
log_release_event "version_determination" "completed" "Version determined successfully" \
|
|
558
|
+
"version" "$VERSION" \
|
|
559
|
+
"bump_type" "${BUMP_TYPE:-custom}"
|
|
560
|
+
|
|
561
|
+
- name: Configure Git
|
|
562
|
+
if: inputs.release_strategy == 'standard-version'
|
|
563
|
+
run: |
|
|
564
|
+
git config user.name "github-actions[bot]"
|
|
565
|
+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
566
|
+
|
|
567
|
+
- name: Generate Changelog
|
|
568
|
+
id: changelog
|
|
569
|
+
run: |
|
|
570
|
+
source ./release-logger.sh
|
|
571
|
+
|
|
572
|
+
log_release_event "changelog_generation" "started" "Generating changelog and release notes"
|
|
573
|
+
|
|
574
|
+
# Generate changelog based on strategy
|
|
575
|
+
if [ "${{ inputs.release_strategy }}" == "standard-version" ]; then
|
|
576
|
+
npx standard-version --release-as ${{ steps.version.outputs.version }} --skip.tag --releaseCommitMessageFormat "chore(release): {{currentTag}} [skip ci]"
|
|
577
|
+
else
|
|
578
|
+
# Custom changelog generation
|
|
579
|
+
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
|
|
580
|
+
if [ -n "$LAST_TAG" ]; then
|
|
581
|
+
COMMIT_RANGE="$LAST_TAG..HEAD"
|
|
582
|
+
else
|
|
583
|
+
COMMIT_RANGE="HEAD"
|
|
584
|
+
fi
|
|
585
|
+
|
|
586
|
+
# Generate changelog with categories
|
|
587
|
+
cat > CHANGELOG_ENTRY.md << EOF
|
|
588
|
+
## [${{ steps.version.outputs.version }}] - $(date +%Y-%m-%d)
|
|
589
|
+
|
|
590
|
+
### Release Information
|
|
591
|
+
- **Environment**: ${{ inputs.environment }}
|
|
592
|
+
- **Release Type**: ${{ inputs.release_strategy }}
|
|
593
|
+
- **Correlation ID**: ${{ needs.release_init.outputs.correlation_id }}
|
|
594
|
+
- **Emergency Release**: ${{ inputs.emergency_release }}
|
|
595
|
+
- **Approved By**: ${{ needs.release_approval.outputs.approver || 'N/A' }}
|
|
596
|
+
|
|
597
|
+
EOF
|
|
598
|
+
|
|
599
|
+
# Add commit categories
|
|
600
|
+
if git log $COMMIT_RANGE --format=%B | grep -qE "^(feat|feature)(\(.+\))?:"; then
|
|
601
|
+
echo "### ✨ Features" >> CHANGELOG_ENTRY.md
|
|
602
|
+
git log $COMMIT_RANGE --format="- %s (%h)" --grep="^feat" --grep="^feature" >> CHANGELOG_ENTRY.md
|
|
603
|
+
echo "" >> CHANGELOG_ENTRY.md
|
|
604
|
+
fi
|
|
605
|
+
|
|
606
|
+
if git log $COMMIT_RANGE --format=%B | grep -qE "^fix(\(.+\))?:"; then
|
|
607
|
+
echo "### 🐛 Bug Fixes" >> CHANGELOG_ENTRY.md
|
|
608
|
+
git log $COMMIT_RANGE --format="- %s (%h)" --grep="^fix" >> CHANGELOG_ENTRY.md
|
|
609
|
+
echo "" >> CHANGELOG_ENTRY.md
|
|
610
|
+
fi
|
|
611
|
+
|
|
612
|
+
# Update CHANGELOG.md
|
|
613
|
+
if [ -f CHANGELOG.md ]; then
|
|
614
|
+
cp CHANGELOG.md CHANGELOG.md.bak
|
|
615
|
+
echo -e "# Changelog\n" > CHANGELOG.md
|
|
616
|
+
cat CHANGELOG_ENTRY.md >> CHANGELOG.md
|
|
617
|
+
echo "" >> CHANGELOG.md
|
|
618
|
+
tail -n +2 CHANGELOG.md.bak >> CHANGELOG.md
|
|
619
|
+
else
|
|
620
|
+
echo -e "# Changelog\n" > CHANGELOG.md
|
|
621
|
+
cat CHANGELOG_ENTRY.md >> CHANGELOG.md
|
|
622
|
+
fi
|
|
623
|
+
fi
|
|
624
|
+
|
|
625
|
+
echo "generated=true" >> $GITHUB_OUTPUT
|
|
626
|
+
|
|
627
|
+
log_release_event "changelog_generation" "completed" "Changelog generated successfully"
|
|
628
|
+
|
|
629
|
+
- name: Push Changelog Changes
|
|
630
|
+
if: inputs.release_strategy == 'standard-version'
|
|
631
|
+
run: |
|
|
632
|
+
git push origin HEAD:${{ github.ref_name }}
|
|
633
|
+
|
|
634
|
+
- name: Generate SBOM
|
|
635
|
+
if: ${{ inputs.generate_sbom == true }}
|
|
636
|
+
continue-on-error: true
|
|
637
|
+
run: |
|
|
638
|
+
source ./release-logger.sh
|
|
639
|
+
|
|
640
|
+
log_release_event "sbom_generation" "started" "Generating Software Bill of Materials"
|
|
641
|
+
|
|
642
|
+
# Install SBOM generator
|
|
643
|
+
npm install -g @cyclonedx/cyclonedx-npm
|
|
644
|
+
|
|
645
|
+
# Generate SBOM in CycloneDX format
|
|
646
|
+
# Note: Using || true to prevent failure on warnings
|
|
647
|
+
cyclonedx-npm --output-format json --output-file sbom.json || {
|
|
648
|
+
echo "::warning::SBOM generation encountered issues but continuing"
|
|
649
|
+
echo '{"components":[],"metadata":{"timestamp":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}}' > sbom.json
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
# Add release metadata to SBOM
|
|
653
|
+
jq --arg version "${{ steps.version.outputs.version }}" \
|
|
654
|
+
--arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
655
|
+
--arg sha "${{ github.sha }}" \
|
|
656
|
+
'.metadata.component.version = $version |
|
|
657
|
+
.metadata.timestamp = $timestamp |
|
|
658
|
+
.metadata.component["bom-ref"] = $sha' \
|
|
659
|
+
sbom.json > sbom-release.json
|
|
660
|
+
|
|
661
|
+
mv sbom-release.json sbom.json
|
|
662
|
+
|
|
663
|
+
log_release_event "sbom_generation" "completed" "SBOM generated successfully" \
|
|
664
|
+
"format" "CycloneDX" \
|
|
665
|
+
"components" "$(jq '.components | length' sbom.json)"
|
|
666
|
+
|
|
667
|
+
- name: Upload Version Artifacts
|
|
668
|
+
uses: actions/upload-artifact@v4
|
|
669
|
+
with:
|
|
670
|
+
name: version-artifacts-${{ github.run_id }}
|
|
671
|
+
path: |
|
|
672
|
+
release-events.jsonl
|
|
673
|
+
CHANGELOG.md
|
|
674
|
+
CHANGELOG_ENTRY.md
|
|
675
|
+
sbom.json
|
|
676
|
+
retention-days: 90
|
|
677
|
+
|
|
678
|
+
# Release signing
|
|
679
|
+
release_signing:
|
|
680
|
+
name: 🔏 Release Signing
|
|
681
|
+
needs: [release_init, version]
|
|
682
|
+
if: inputs.require_signatures == true
|
|
683
|
+
runs-on: ubuntu-latest
|
|
684
|
+
outputs:
|
|
685
|
+
signed: ${{ steps.sign.outputs.signed }}
|
|
686
|
+
signature_files: ${{ steps.sign.outputs.files }}
|
|
687
|
+
steps:
|
|
688
|
+
- uses: actions/checkout@v4
|
|
689
|
+
with:
|
|
690
|
+
fetch-depth: 0
|
|
691
|
+
ref: ${{ github.sha }}
|
|
692
|
+
|
|
693
|
+
- name: Download Version Artifacts
|
|
694
|
+
uses: actions/download-artifact@v4
|
|
695
|
+
with:
|
|
696
|
+
name: version-artifacts-${{ github.run_id }}
|
|
697
|
+
|
|
698
|
+
- name: Setup Signing Environment
|
|
699
|
+
id: setup
|
|
700
|
+
run: |
|
|
701
|
+
# Install signing tools
|
|
702
|
+
if [ "${{ runner.os }}" == "Linux" ]; then
|
|
703
|
+
sudo apt-get update
|
|
704
|
+
sudo apt-get install -y gnupg2
|
|
705
|
+
fi
|
|
706
|
+
|
|
707
|
+
# Check if signing key is available
|
|
708
|
+
if [ -n "${{ secrets.RELEASE_SIGNING_KEY }}" ]; then
|
|
709
|
+
echo "✅ Signing key available"
|
|
710
|
+
echo "signing_available=true" >> $GITHUB_OUTPUT
|
|
711
|
+
else
|
|
712
|
+
echo "⚠️ No signing key available"
|
|
713
|
+
if [ "${{ inputs.require_signatures }}" == "true" ]; then
|
|
714
|
+
echo "::error::Signatures required but RELEASE_SIGNING_KEY not configured"
|
|
715
|
+
exit 1
|
|
716
|
+
fi
|
|
717
|
+
echo "signing_available=false" >> $GITHUB_OUTPUT
|
|
718
|
+
fi
|
|
719
|
+
|
|
720
|
+
- name: Import Signing Key
|
|
721
|
+
if: steps.setup.outputs.signing_available == 'true'
|
|
722
|
+
run: |
|
|
723
|
+
# Import GPG key
|
|
724
|
+
echo "${{ secrets.RELEASE_SIGNING_KEY }}" | base64 -d > signing.key
|
|
725
|
+
|
|
726
|
+
if [ -n "${{ secrets.SIGNING_KEY_PASSPHRASE }}" ]; then
|
|
727
|
+
echo "${{ secrets.SIGNING_KEY_PASSPHRASE }}" | \
|
|
728
|
+
gpg --batch --yes --passphrase-fd 0 --import signing.key
|
|
729
|
+
else
|
|
730
|
+
gpg --batch --yes --import signing.key
|
|
731
|
+
fi
|
|
732
|
+
|
|
733
|
+
rm -f signing.key
|
|
734
|
+
|
|
735
|
+
# List imported keys
|
|
736
|
+
gpg --list-secret-keys
|
|
737
|
+
|
|
738
|
+
# Configure git
|
|
739
|
+
git config --global user.signingkey "${{ secrets.SIGNING_KEY_ID || secrets.RELEASE_SIGNING_KEY_ID }}"
|
|
740
|
+
git config --global commit.gpgsign true
|
|
741
|
+
git config --global tag.gpgsign true
|
|
742
|
+
|
|
743
|
+
- name: Sign Release Artifacts
|
|
744
|
+
id: sign
|
|
745
|
+
if: steps.setup.outputs.signing_available == 'true'
|
|
746
|
+
run: |
|
|
747
|
+
# Function to sign files
|
|
748
|
+
sign_artifact() {
|
|
749
|
+
local file="$1"
|
|
750
|
+
local sig_file="${file}.sig"
|
|
751
|
+
local sha_file="${file}.sha256"
|
|
752
|
+
|
|
753
|
+
if [ -f "$file" ]; then
|
|
754
|
+
# Create detached signature
|
|
755
|
+
gpg --armor --detach-sign \
|
|
756
|
+
--local-user "${{ secrets.SIGNING_KEY_ID || secrets.RELEASE_SIGNING_KEY_ID }}" \
|
|
757
|
+
--output "$sig_file" \
|
|
758
|
+
"$file"
|
|
759
|
+
|
|
760
|
+
# Generate checksum
|
|
761
|
+
sha256sum "$file" | cut -d' ' -f1 > "$sha_file"
|
|
762
|
+
|
|
763
|
+
echo "✅ Signed: $file"
|
|
764
|
+
return 0
|
|
765
|
+
else
|
|
766
|
+
echo "⚠️ File not found: $file"
|
|
767
|
+
return 1
|
|
768
|
+
fi
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
# Sign release artifacts
|
|
772
|
+
SIGNED_FILES=()
|
|
773
|
+
|
|
774
|
+
# Sign changelog
|
|
775
|
+
if sign_artifact "CHANGELOG.md"; then
|
|
776
|
+
SIGNED_FILES+=("CHANGELOG.md")
|
|
777
|
+
fi
|
|
778
|
+
|
|
779
|
+
# Sign SBOM if generated
|
|
780
|
+
if [ -f "sbom.json" ] && sign_artifact "sbom.json"; then
|
|
781
|
+
SIGNED_FILES+=("sbom.json")
|
|
782
|
+
fi
|
|
783
|
+
|
|
784
|
+
# Sign package.json
|
|
785
|
+
if sign_artifact "package.json"; then
|
|
786
|
+
SIGNED_FILES+=("package.json")
|
|
787
|
+
fi
|
|
788
|
+
|
|
789
|
+
# Create release manifest
|
|
790
|
+
cat > release-manifest.json << EOF
|
|
791
|
+
{
|
|
792
|
+
"version": "${{ needs.version.outputs.version }}",
|
|
793
|
+
"tag": "${{ needs.version.outputs.tag }}",
|
|
794
|
+
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
|
795
|
+
"sha": "${{ github.sha }}",
|
|
796
|
+
"signed_by": "${{ secrets.SIGNING_KEY_ID || 'release-bot' }}",
|
|
797
|
+
"artifacts": [
|
|
798
|
+
EOF
|
|
799
|
+
|
|
800
|
+
# Add artifact entries
|
|
801
|
+
first=true
|
|
802
|
+
for file in "${SIGNED_FILES[@]}"; do
|
|
803
|
+
if [ "$first" != "true" ]; then
|
|
804
|
+
echo "," >> release-manifest.json
|
|
805
|
+
fi
|
|
806
|
+
first=false
|
|
807
|
+
|
|
808
|
+
cat >> release-manifest.json << EOF
|
|
809
|
+
{
|
|
810
|
+
"name": "$file",
|
|
811
|
+
"sha256": "$(cat ${file}.sha256)",
|
|
812
|
+
"signature": "$(cat ${file}.sig | base64 -w0)"
|
|
813
|
+
}
|
|
814
|
+
EOF
|
|
815
|
+
done
|
|
816
|
+
|
|
817
|
+
cat >> release-manifest.json << EOF
|
|
818
|
+
]
|
|
819
|
+
}
|
|
820
|
+
EOF
|
|
821
|
+
|
|
822
|
+
# Sign the manifest
|
|
823
|
+
sign_artifact "release-manifest.json"
|
|
824
|
+
|
|
825
|
+
echo "signed=true" >> $GITHUB_OUTPUT
|
|
826
|
+
echo "files=${SIGNED_FILES[*]}" >> $GITHUB_OUTPUT
|
|
827
|
+
|
|
828
|
+
- name: Create Signed Git Tag
|
|
829
|
+
if: steps.setup.outputs.signing_available == 'true'
|
|
830
|
+
run: |
|
|
831
|
+
# Create signed tag
|
|
832
|
+
if [ -n "${{ secrets.SIGNING_KEY_PASSPHRASE }}" ]; then
|
|
833
|
+
echo "${{ secrets.SIGNING_KEY_PASSPHRASE }}" | \
|
|
834
|
+
git tag -s "${{ needs.version.outputs.tag }}" \
|
|
835
|
+
-m "Release ${{ needs.version.outputs.version }}" \
|
|
836
|
+
-m "Signed-off-by: ${{ github.actor }}" \
|
|
837
|
+
-m "Correlation ID: ${{ needs.release_init.outputs.correlation_id }}"
|
|
838
|
+
else
|
|
839
|
+
git tag -s "${{ needs.version.outputs.tag }}" \
|
|
840
|
+
-m "Release ${{ needs.version.outputs.version }}" \
|
|
841
|
+
-m "Signed-off-by: ${{ github.actor }}" \
|
|
842
|
+
-m "Correlation ID: ${{ needs.release_init.outputs.correlation_id }}"
|
|
843
|
+
fi
|
|
844
|
+
|
|
845
|
+
echo "✅ Created signed tag: ${{ needs.version.outputs.tag }}"
|
|
846
|
+
|
|
847
|
+
- name: Upload Signed Artifacts
|
|
848
|
+
if: steps.sign.outputs.signed == 'true'
|
|
849
|
+
uses: actions/upload-artifact@v4
|
|
850
|
+
with:
|
|
851
|
+
name: signed-artifacts-${{ github.run_id }}
|
|
852
|
+
path: |
|
|
853
|
+
*.sig
|
|
854
|
+
*.sha256
|
|
855
|
+
release-manifest.json
|
|
856
|
+
retention-days: 365
|
|
857
|
+
|
|
858
|
+
# Release attestation
|
|
859
|
+
release_attestation:
|
|
860
|
+
name: 📜 Release Attestation
|
|
861
|
+
needs: [release_init, version, quality, release_signing]
|
|
862
|
+
if: |
|
|
863
|
+
always() &&
|
|
864
|
+
!cancelled() &&
|
|
865
|
+
needs.version.result == 'success' &&
|
|
866
|
+
(needs.release_signing.result == 'success' || needs.release_signing.result == 'skipped')
|
|
867
|
+
runs-on: ubuntu-latest
|
|
868
|
+
outputs:
|
|
869
|
+
attestation_created: ${{ steps.attestation.outputs.created }}
|
|
870
|
+
attestation_url: ${{ steps.attestation.outputs.url }}
|
|
871
|
+
steps:
|
|
872
|
+
- name: Generate SLSA Provenance
|
|
873
|
+
id: provenance
|
|
874
|
+
run: |
|
|
875
|
+
# Create SLSA provenance attestation
|
|
876
|
+
cat > provenance.json << EOF
|
|
877
|
+
{
|
|
878
|
+
"_type": "https://in-toto.io/Statement/v0.1",
|
|
879
|
+
"predicateType": "https://slsa.dev/provenance/v0.2",
|
|
880
|
+
"subject": [{
|
|
881
|
+
"name": "${{ github.repository }}",
|
|
882
|
+
"digest": {
|
|
883
|
+
"sha256": "${{ github.sha }}"
|
|
884
|
+
}
|
|
885
|
+
}],
|
|
886
|
+
"predicate": {
|
|
887
|
+
"builder": {
|
|
888
|
+
"id": "https://github.com/actions/runner"
|
|
889
|
+
},
|
|
890
|
+
"buildType": "https://github.com/slsa-framework/slsa-github-generator/generic@v1",
|
|
891
|
+
"invocation": {
|
|
892
|
+
"configSource": {
|
|
893
|
+
"uri": "git+https://github.com/${{ github.repository }}@${{ github.ref }}",
|
|
894
|
+
"digest": {
|
|
895
|
+
"sha1": "${{ github.sha }}"
|
|
896
|
+
},
|
|
897
|
+
"entryPoint": ".github/workflows/release.yml"
|
|
898
|
+
},
|
|
899
|
+
"parameters": {
|
|
900
|
+
"environment": "${{ inputs.environment }}",
|
|
901
|
+
"version": "${{ needs.version.outputs.version }}",
|
|
902
|
+
"release_strategy": "${{ inputs.release_strategy }}",
|
|
903
|
+
"emergency_release": ${{ inputs.emergency_release }},
|
|
904
|
+
"approval_required": ${{ inputs.require_approval }},
|
|
905
|
+
"signatures_required": ${{ inputs.require_signatures }}
|
|
906
|
+
},
|
|
907
|
+
"environment": {
|
|
908
|
+
"github_run_id": "${{ github.run_id }}",
|
|
909
|
+
"github_run_number": "${{ github.run_number }}",
|
|
910
|
+
"github_actor": "${{ github.actor }}",
|
|
911
|
+
"github_event_name": "${{ github.event_name }}"
|
|
912
|
+
}
|
|
913
|
+
},
|
|
914
|
+
"buildConfig": {
|
|
915
|
+
"version": 1,
|
|
916
|
+
"steps": [
|
|
917
|
+
{
|
|
918
|
+
"command": ["quality-checks"],
|
|
919
|
+
"env": null
|
|
920
|
+
},
|
|
921
|
+
{
|
|
922
|
+
"command": ["version-determination"],
|
|
923
|
+
"env": null
|
|
924
|
+
},
|
|
925
|
+
{
|
|
926
|
+
"command": ["release-creation"],
|
|
927
|
+
"env": null
|
|
928
|
+
}
|
|
929
|
+
]
|
|
930
|
+
},
|
|
931
|
+
"metadata": {
|
|
932
|
+
"completeness": {
|
|
933
|
+
"parameters": true,
|
|
934
|
+
"environment": true,
|
|
935
|
+
"materials": false
|
|
936
|
+
},
|
|
937
|
+
"reproducible": false,
|
|
938
|
+
"buildStartedOn": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
|
939
|
+
"buildFinishedOn": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
EOF
|
|
944
|
+
|
|
945
|
+
- name: Create Release Attestation
|
|
946
|
+
id: attestation
|
|
947
|
+
run: |
|
|
948
|
+
# Create comprehensive release attestation
|
|
949
|
+
cat > release-attestation.json << EOF
|
|
950
|
+
{
|
|
951
|
+
"version": "${{ needs.version.outputs.version }}",
|
|
952
|
+
"tag": "${{ needs.version.outputs.tag }}",
|
|
953
|
+
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
|
954
|
+
"correlation_id": "${{ needs.release_init.outputs.correlation_id }}",
|
|
955
|
+
"environment": "${{ inputs.environment }}",
|
|
956
|
+
"attestations": {
|
|
957
|
+
"quality": {
|
|
958
|
+
"passed": ${{ needs.quality.result == 'success' }},
|
|
959
|
+
"skipped_checks": "${{ inputs.skip_jobs }}",
|
|
960
|
+
"workflow_result": "${{ needs.quality.result }}"
|
|
961
|
+
},
|
|
962
|
+
"approvals": {
|
|
963
|
+
"required": ${{ inputs.require_approval }},
|
|
964
|
+
"received": ${{ needs.release_approval.outputs.approved == 'true' || inputs.require_approval == false }},
|
|
965
|
+
"approver": "${{ needs.release_approval.outputs.approver || 'N/A' }}",
|
|
966
|
+
"approval_time": "${{ needs.release_approval.outputs.approval_time || 'N/A' }}"
|
|
967
|
+
},
|
|
968
|
+
"signatures": {
|
|
969
|
+
"required": ${{ inputs.require_signatures }},
|
|
970
|
+
"artifacts_signed": ${{ needs.release_signing.outputs.signed == 'true' }},
|
|
971
|
+
"signed_files": "${{ needs.release_signing.outputs.signature_files || 'none' }}"
|
|
972
|
+
},
|
|
973
|
+
"compliance": {
|
|
974
|
+
"frameworks": ["SOC2", "ISO27001"],
|
|
975
|
+
"emergency_release": ${{ inputs.emergency_release }},
|
|
976
|
+
"blackout_override": ${{ inputs.override_blackout }},
|
|
977
|
+
"dependency_check": ${{ inputs.check_dependencies }}
|
|
978
|
+
},
|
|
979
|
+
"integrity": {
|
|
980
|
+
"source_sha": "${{ github.sha }}",
|
|
981
|
+
"repository": "${{ github.repository }}",
|
|
982
|
+
"ref": "${{ github.ref }}",
|
|
983
|
+
"actor": "${{ github.actor }}"
|
|
984
|
+
},
|
|
985
|
+
"sbom": {
|
|
986
|
+
"generated": ${{ inputs.generate_sbom }},
|
|
987
|
+
"format": "CycloneDX",
|
|
988
|
+
"version": "1.4"
|
|
989
|
+
}
|
|
990
|
+
},
|
|
991
|
+
"metadata": {
|
|
992
|
+
"workflow_id": "${{ github.run_id }}",
|
|
993
|
+
"workflow_url": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}",
|
|
994
|
+
"initiated_by": "${{ github.actor }}",
|
|
995
|
+
"event_type": "${{ github.event_name }}"
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
EOF
|
|
999
|
+
|
|
1000
|
+
echo "created=true" >> $GITHUB_OUTPUT
|
|
1001
|
+
echo "url=https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts" >> $GITHUB_OUTPUT
|
|
1002
|
+
|
|
1003
|
+
- name: Store Attestations
|
|
1004
|
+
uses: actions/upload-artifact@v4
|
|
1005
|
+
with:
|
|
1006
|
+
name: release-attestations-${{ needs.version.outputs.version }}
|
|
1007
|
+
path: |
|
|
1008
|
+
provenance.json
|
|
1009
|
+
release-attestation.json
|
|
1010
|
+
retention-days: 365
|
|
1011
|
+
|
|
1012
|
+
# Create GitHub Release with enterprise features
|
|
1013
|
+
github_release:
|
|
1014
|
+
name: 🎉 Create GitHub Release
|
|
1015
|
+
needs: [release_init, version, release_signing, release_attestation]
|
|
1016
|
+
runs-on: ubuntu-latest
|
|
1017
|
+
if: |
|
|
1018
|
+
always() &&
|
|
1019
|
+
!cancelled() &&
|
|
1020
|
+
needs.version.result == 'success' &&
|
|
1021
|
+
(needs.release_signing.result == 'success' || needs.release_signing.result == 'skipped') &&
|
|
1022
|
+
(needs.release_attestation.result == 'success' || needs.release_attestation.result == 'skipped')
|
|
1023
|
+
outputs:
|
|
1024
|
+
release_url: ${{ steps.create_release.outputs.html_url }}
|
|
1025
|
+
release_id: ${{ steps.create_release.outputs.id }}
|
|
1026
|
+
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
|
1027
|
+
steps:
|
|
1028
|
+
- uses: actions/checkout@v4
|
|
1029
|
+
with:
|
|
1030
|
+
fetch-depth: 0
|
|
1031
|
+
|
|
1032
|
+
- name: Download All Artifacts
|
|
1033
|
+
uses: actions/download-artifact@v4
|
|
1034
|
+
with:
|
|
1035
|
+
pattern: '*-${{ github.run_id }}'
|
|
1036
|
+
|
|
1037
|
+
- name: Generate Release Notes
|
|
1038
|
+
id: release_notes
|
|
1039
|
+
run: |
|
|
1040
|
+
# Create release notes
|
|
1041
|
+
cat > RELEASE_NOTES.md << 'EOF'
|
|
1042
|
+
# Release ${{ needs.version.outputs.version }}
|
|
1043
|
+
|
|
1044
|
+
## 📊 Release Information
|
|
1045
|
+
- **Version**: ${{ needs.version.outputs.version }}
|
|
1046
|
+
- **Environment**: ${{ inputs.environment }}
|
|
1047
|
+
- **Release Date**: $(date +"%Y-%m-%d %H:%M:%S UTC")
|
|
1048
|
+
- **Release ID**: ${{ needs.release_init.outputs.correlation_id }}
|
|
1049
|
+
- **Emergency Release**: ${{ inputs.emergency_release && '⚠️ Yes' || 'No' }}
|
|
1050
|
+
|
|
1051
|
+
## 🔐 Security & Compliance
|
|
1052
|
+
- **Signed Release**: ${{ needs.release_signing.outputs.signed == 'true' && '✅ Yes' || '❌ No' }}
|
|
1053
|
+
- **Approval Required**: ${{ inputs.require_approval && '✅ Yes' || 'No' }}
|
|
1054
|
+
- **SBOM Generated**: ${{ inputs.generate_sbom && '✅ Yes' || 'No' }}
|
|
1055
|
+
- **Attestation**: ✅ [View Attestation](${{ needs.release_attestation.outputs.attestation_url }})
|
|
1056
|
+
|
|
1057
|
+
## 📝 Changelog
|
|
1058
|
+
EOF
|
|
1059
|
+
|
|
1060
|
+
# Include changelog content
|
|
1061
|
+
if [ -f "version-artifacts-${{ github.run_id }}/CHANGELOG_ENTRY.md" ]; then
|
|
1062
|
+
cat "version-artifacts-${{ github.run_id }}/CHANGELOG_ENTRY.md" >> RELEASE_NOTES.md
|
|
1063
|
+
fi
|
|
1064
|
+
|
|
1065
|
+
# Add verification instructions if signed
|
|
1066
|
+
if [ "${{ needs.release_signing.outputs.signed }}" == "true" ]; then
|
|
1067
|
+
cat >> RELEASE_NOTES.md << 'EOF'
|
|
1068
|
+
|
|
1069
|
+
## 🔒 Verification
|
|
1070
|
+
|
|
1071
|
+
This release is cryptographically signed. To verify:
|
|
1072
|
+
|
|
1073
|
+
```bash
|
|
1074
|
+
# Download signature files
|
|
1075
|
+
curl -L -o release-manifest.json.sig <signature-url>
|
|
1076
|
+
curl -L -o release-manifest.json <manifest-url>
|
|
1077
|
+
|
|
1078
|
+
# Import public key
|
|
1079
|
+
gpg --import release-key.pub
|
|
1080
|
+
|
|
1081
|
+
# Verify signature
|
|
1082
|
+
gpg --verify release-manifest.json.sig release-manifest.json
|
|
1083
|
+
```
|
|
1084
|
+
EOF
|
|
1085
|
+
fi
|
|
1086
|
+
|
|
1087
|
+
cat >> RELEASE_NOTES.md << 'EOF'
|
|
1088
|
+
|
|
1089
|
+
## 🔗 Links
|
|
1090
|
+
- [Workflow Run](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
|
1091
|
+
- [Compare Changes](https://github.com/${{ github.repository }}/compare/${{ github.event.before }}...${{ needs.version.outputs.tag }})
|
|
1092
|
+
EOF
|
|
1093
|
+
|
|
1094
|
+
- name: Create GitHub Release
|
|
1095
|
+
id: create_release
|
|
1096
|
+
env:
|
|
1097
|
+
GITHUB_TOKEN: ${{ github.token }}
|
|
1098
|
+
run: |
|
|
1099
|
+
# Prepare release data
|
|
1100
|
+
RELEASE_DATA=$(jq -n \
|
|
1101
|
+
--arg tag "${{ needs.version.outputs.tag }}" \
|
|
1102
|
+
--arg name "Release ${{ needs.version.outputs.version }}" \
|
|
1103
|
+
--arg body "$(cat RELEASE_NOTES.md)" \
|
|
1104
|
+
--arg target "${{ github.sha }}" \
|
|
1105
|
+
--argjson prerelease "${{ needs.version.outputs.prerelease == 'true' }}" \
|
|
1106
|
+
--argjson draft false \
|
|
1107
|
+
'{
|
|
1108
|
+
tag_name: $tag,
|
|
1109
|
+
name: $name,
|
|
1110
|
+
body: $body,
|
|
1111
|
+
target_commitish: $target,
|
|
1112
|
+
prerelease: $prerelease,
|
|
1113
|
+
draft: $draft
|
|
1114
|
+
}')
|
|
1115
|
+
|
|
1116
|
+
# Create release
|
|
1117
|
+
RESPONSE=$(curl -X POST \
|
|
1118
|
+
-H "Authorization: token $GITHUB_TOKEN" \
|
|
1119
|
+
-H "Accept: application/vnd.github.v3+json" \
|
|
1120
|
+
-d "$RELEASE_DATA" \
|
|
1121
|
+
"https://api.github.com/repos/${{ github.repository }}/releases")
|
|
1122
|
+
|
|
1123
|
+
# Extract release information
|
|
1124
|
+
echo "$RESPONSE" | jq -r '.html_url' > .release_url
|
|
1125
|
+
echo "$RESPONSE" | jq -r '.id' > .release_id
|
|
1126
|
+
echo "$RESPONSE" | jq -r '.upload_url' > .upload_url
|
|
1127
|
+
|
|
1128
|
+
echo "html_url=$(cat .release_url)" >> $GITHUB_OUTPUT
|
|
1129
|
+
echo "id=$(cat .release_id)" >> $GITHUB_OUTPUT
|
|
1130
|
+
echo "upload_url=$(cat .upload_url)" >> $GITHUB_OUTPUT
|
|
1131
|
+
|
|
1132
|
+
- name: Upload Release Assets
|
|
1133
|
+
if: needs.release_signing.outputs.signed == 'true'
|
|
1134
|
+
env:
|
|
1135
|
+
GITHUB_TOKEN: ${{ github.token }}
|
|
1136
|
+
run: |
|
|
1137
|
+
UPLOAD_URL=$(cat .upload_url | sed 's/{?name,label}//')
|
|
1138
|
+
|
|
1139
|
+
# Upload signature files
|
|
1140
|
+
for sig_file in signed-artifacts-${{ github.run_id }}/*.sig; do
|
|
1141
|
+
if [ -f "$sig_file" ]; then
|
|
1142
|
+
filename=$(basename "$sig_file")
|
|
1143
|
+
curl -X POST \
|
|
1144
|
+
-H "Authorization: token $GITHUB_TOKEN" \
|
|
1145
|
+
-H "Content-Type: application/pgp-signature" \
|
|
1146
|
+
--data-binary @"$sig_file" \
|
|
1147
|
+
"${UPLOAD_URL}?name=${filename}"
|
|
1148
|
+
fi
|
|
1149
|
+
done
|
|
1150
|
+
|
|
1151
|
+
# Upload checksums
|
|
1152
|
+
for sha_file in signed-artifacts-${{ github.run_id }}/*.sha256; do
|
|
1153
|
+
if [ -f "$sha_file" ]; then
|
|
1154
|
+
filename=$(basename "$sha_file")
|
|
1155
|
+
curl -X POST \
|
|
1156
|
+
-H "Authorization: token $GITHUB_TOKEN" \
|
|
1157
|
+
-H "Content-Type: text/plain" \
|
|
1158
|
+
--data-binary @"$sha_file" \
|
|
1159
|
+
"${UPLOAD_URL}?name=${filename}"
|
|
1160
|
+
fi
|
|
1161
|
+
done
|
|
1162
|
+
|
|
1163
|
+
# Upload release manifest
|
|
1164
|
+
if [ -f "signed-artifacts-${{ github.run_id }}/release-manifest.json" ]; then
|
|
1165
|
+
curl -X POST \
|
|
1166
|
+
-H "Authorization: token $GITHUB_TOKEN" \
|
|
1167
|
+
-H "Content-Type: application/json" \
|
|
1168
|
+
--data-binary @"signed-artifacts-${{ github.run_id }}/release-manifest.json" \
|
|
1169
|
+
"${UPLOAD_URL}?name=release-manifest.json"
|
|
1170
|
+
fi
|
|
1171
|
+
|
|
1172
|
+
# Upload SBOM if generated
|
|
1173
|
+
if [ -f "version-artifacts-${{ github.run_id }}/sbom.json" ]; then
|
|
1174
|
+
curl -X POST \
|
|
1175
|
+
-H "Authorization: token $GITHUB_TOKEN" \
|
|
1176
|
+
-H "Content-Type: application/json" \
|
|
1177
|
+
--data-binary @"version-artifacts-${{ github.run_id }}/sbom.json" \
|
|
1178
|
+
"${UPLOAD_URL}?name=sbom.json"
|
|
1179
|
+
fi
|
|
1180
|
+
|
|
1181
|
+
# Sentry Release Creation
|
|
1182
|
+
sentry_release:
|
|
1183
|
+
name: 🚨 Create Sentry Release
|
|
1184
|
+
needs: [release_init, version]
|
|
1185
|
+
runs-on: ubuntu-latest
|
|
1186
|
+
if: |
|
|
1187
|
+
always() &&
|
|
1188
|
+
!cancelled() &&
|
|
1189
|
+
needs.version.result == 'success'
|
|
1190
|
+
outputs:
|
|
1191
|
+
sentry_release_created: ${{ steps.set_outputs.outputs.created }}
|
|
1192
|
+
sentry_release_url: ${{ steps.set_outputs.outputs.url }}
|
|
1193
|
+
steps:
|
|
1194
|
+
- uses: actions/checkout@v4
|
|
1195
|
+
with:
|
|
1196
|
+
fetch-depth: 0
|
|
1197
|
+
|
|
1198
|
+
- name: Check Sentry Configuration
|
|
1199
|
+
id: check_config
|
|
1200
|
+
env:
|
|
1201
|
+
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
|
1202
|
+
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
|
|
1203
|
+
SENTRY_PROJECT: ${{ vars.SENTRY_PROJECT }}
|
|
1204
|
+
run: |
|
|
1205
|
+
# Check if all required Sentry secrets are available
|
|
1206
|
+
if [ -z "$SENTRY_AUTH_TOKEN" ] || [ -z "$SENTRY_ORG" ] || [ -z "$SENTRY_PROJECT" ]; then
|
|
1207
|
+
echo "⚠️ Sentry configuration incomplete - skipping Sentry release" >> $GITHUB_STEP_SUMMARY
|
|
1208
|
+
echo "configured=false" >> $GITHUB_OUTPUT
|
|
1209
|
+
# Set default outputs for skipped job
|
|
1210
|
+
echo "created=false" >> $GITHUB_OUTPUT
|
|
1211
|
+
echo "url=" >> $GITHUB_OUTPUT
|
|
1212
|
+
else
|
|
1213
|
+
echo "✅ Sentry configuration verified" >> $GITHUB_STEP_SUMMARY
|
|
1214
|
+
echo "configured=true" >> $GITHUB_OUTPUT
|
|
1215
|
+
fi
|
|
1216
|
+
|
|
1217
|
+
- name: Create Sentry Release
|
|
1218
|
+
id: sentry
|
|
1219
|
+
if: steps.check_config.outputs.configured == 'true'
|
|
1220
|
+
uses: getsentry/action-release@v1
|
|
1221
|
+
env:
|
|
1222
|
+
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
|
1223
|
+
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
|
|
1224
|
+
SENTRY_PROJECT: ${{ vars.SENTRY_PROJECT }}
|
|
1225
|
+
with:
|
|
1226
|
+
environment: ${{ inputs.environment }}
|
|
1227
|
+
version: ${{ needs.version.outputs.version }}
|
|
1228
|
+
sourcemaps: ${{ inputs.sourcemaps }}
|
|
1229
|
+
|
|
1230
|
+
- name: Update Release Summary
|
|
1231
|
+
if: steps.check_config.outputs.configured == 'true' && steps.sentry.outcome == 'success'
|
|
1232
|
+
run: |
|
|
1233
|
+
echo "## 🚨 Sentry Release" >> $GITHUB_STEP_SUMMARY
|
|
1234
|
+
echo "" >> $GITHUB_STEP_SUMMARY
|
|
1235
|
+
echo "✅ Sentry release created successfully" >> $GITHUB_STEP_SUMMARY
|
|
1236
|
+
echo "- **Version**: ${{ needs.version.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
|
1237
|
+
echo "- **Environment**: ${{ inputs.environment }}" >> $GITHUB_STEP_SUMMARY
|
|
1238
|
+
if [ -n "${{ inputs.sourcemaps }}" ]; then
|
|
1239
|
+
echo "- **Source Maps**: Uploaded from \`${{ inputs.sourcemaps }}\`" >> $GITHUB_STEP_SUMMARY
|
|
1240
|
+
fi
|
|
1241
|
+
echo "- **Organization**: ${{ vars.SENTRY_ORG }}" >> $GITHUB_STEP_SUMMARY
|
|
1242
|
+
echo "- **Project**: ${{ vars.SENTRY_PROJECT }}" >> $GITHUB_STEP_SUMMARY
|
|
1243
|
+
|
|
1244
|
+
- name: Set Outputs
|
|
1245
|
+
if: always()
|
|
1246
|
+
id: set_outputs
|
|
1247
|
+
run: |
|
|
1248
|
+
# Set outputs based on whether Sentry was configured and successful
|
|
1249
|
+
if [ "${{ steps.check_config.outputs.configured }}" == "true" ] && [ "${{ steps.sentry.outcome }}" == "success" ]; then
|
|
1250
|
+
echo "created=true" >> $GITHUB_OUTPUT
|
|
1251
|
+
echo "url=https://sentry.io/organizations/${{ vars.SENTRY_ORG }}/releases/${{ needs.version.outputs.version }}/" >> $GITHUB_OUTPUT
|
|
1252
|
+
else
|
|
1253
|
+
echo "created=false" >> $GITHUB_OUTPUT
|
|
1254
|
+
echo "url=" >> $GITHUB_OUTPUT
|
|
1255
|
+
fi
|
|
1256
|
+
|
|
1257
|
+
# Check Jira Setup
|
|
1258
|
+
checks_jira_setup:
|
|
1259
|
+
name: 📋 Check Jira Setup
|
|
1260
|
+
needs: [version]
|
|
1261
|
+
runs-on: ubuntu-latest
|
|
1262
|
+
outputs:
|
|
1263
|
+
has_jira_setup: ${{ steps.check.outputs.has_jira_setup }}
|
|
1264
|
+
steps:
|
|
1265
|
+
- name: Checkout
|
|
1266
|
+
uses: actions/checkout@v4
|
|
1267
|
+
- uses: noliran/branch-based-secrets@v1
|
|
1268
|
+
with:
|
|
1269
|
+
secrets: JIRA_AUTOMATION_WEBHOOK
|
|
1270
|
+
- name: Check Jira Configuration
|
|
1271
|
+
id: check
|
|
1272
|
+
run: |
|
|
1273
|
+
if [[ -z "${JIRA_AUTOMATION_WEBHOOK}" ]] || [[ -z "${{ vars.JIRA_PROJECT_KEY }}" ]]; then
|
|
1274
|
+
echo "⚠️ Jira configuration incomplete - skipping Jira release" >> $GITHUB_STEP_SUMMARY
|
|
1275
|
+
if [[ -z "${JIRA_AUTOMATION_WEBHOOK}" ]]; then
|
|
1276
|
+
echo " - Missing: JIRA_AUTOMATION_WEBHOOK secret" >> $GITHUB_STEP_SUMMARY
|
|
1277
|
+
fi
|
|
1278
|
+
if [[ -z "${{ vars.JIRA_PROJECT_KEY }}" ]]; then
|
|
1279
|
+
echo " - Missing: JIRA_PROJECT_KEY variable" >> $GITHUB_STEP_SUMMARY
|
|
1280
|
+
fi
|
|
1281
|
+
echo "has_jira_setup=false" >> $GITHUB_OUTPUT
|
|
1282
|
+
else
|
|
1283
|
+
echo "✅ Jira configuration verified" >> $GITHUB_STEP_SUMMARY
|
|
1284
|
+
echo "has_jira_setup=true" >> $GITHUB_OUTPUT
|
|
1285
|
+
fi
|
|
1286
|
+
shell: bash
|
|
1287
|
+
env:
|
|
1288
|
+
JIRA_AUTOMATION_WEBHOOK: ${{ secrets[env.JIRA_AUTOMATION_WEBHOOK_NAME] }}
|
|
1289
|
+
|
|
1290
|
+
# Create Jira Release
|
|
1291
|
+
jira_release:
|
|
1292
|
+
name: 📋 Create Jira Release
|
|
1293
|
+
runs-on: ubuntu-latest
|
|
1294
|
+
if: |
|
|
1295
|
+
always() &&
|
|
1296
|
+
!cancelled() &&
|
|
1297
|
+
needs.version.result == 'success' &&
|
|
1298
|
+
needs.checks_jira_setup.outputs.has_jira_setup == 'true'
|
|
1299
|
+
needs: [version, checks_jira_setup]
|
|
1300
|
+
outputs:
|
|
1301
|
+
jira_release_created: ${{ steps.create.outputs.created }}
|
|
1302
|
+
jira_release_url: ${{ steps.create.outputs.url }}
|
|
1303
|
+
steps:
|
|
1304
|
+
- name: Checkout
|
|
1305
|
+
uses: actions/checkout@v4
|
|
1306
|
+
- uses: noliran/branch-based-secrets@v1
|
|
1307
|
+
with:
|
|
1308
|
+
secrets: JIRA_AUTOMATION_WEBHOOK
|
|
1309
|
+
- name: Create Jira Release
|
|
1310
|
+
id: create
|
|
1311
|
+
continue-on-error: true
|
|
1312
|
+
uses: GeoWerkstatt/create-jira-release@v1
|
|
1313
|
+
with:
|
|
1314
|
+
jira-project-key: ${{ vars.JIRA_PROJECT_KEY }}
|
|
1315
|
+
jira-automation-webhook: ${{ secrets[env.JIRA_AUTOMATION_WEBHOOK_NAME] }}
|
|
1316
|
+
build-version: '${{ github.event.repository.name }} v${{ needs.version.outputs.version }}'
|
|
1317
|
+
- name: Update Release Summary
|
|
1318
|
+
if: steps.create.outcome == 'success'
|
|
1319
|
+
run: |
|
|
1320
|
+
echo "## 📋 Jira Release" >> $GITHUB_STEP_SUMMARY
|
|
1321
|
+
echo "" >> $GITHUB_STEP_SUMMARY
|
|
1322
|
+
echo "✅ Jira release created successfully" >> $GITHUB_STEP_SUMMARY
|
|
1323
|
+
echo "- **Version**: ${{ needs.version.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
|
1324
|
+
echo "- **Project**: ${{ vars.JIRA_PROJECT_KEY }}" >> $GITHUB_STEP_SUMMARY
|
|
1325
|
+
echo "- **Build Version**: ${{ github.event.repository.name }} v${{ needs.version.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
|
1326
|
+
- name: Set Outputs
|
|
1327
|
+
if: always()
|
|
1328
|
+
run: |
|
|
1329
|
+
if [ "${{ steps.create.outcome }}" == "success" ]; then
|
|
1330
|
+
echo "created=true" >> $GITHUB_OUTPUT
|
|
1331
|
+
echo "url=https://${{ vars.JIRA_BASE_URL || 'company.atlassian.net' }}/projects/${{ vars.JIRA_PROJECT_KEY }}/versions" >> $GITHUB_OUTPUT
|
|
1332
|
+
else
|
|
1333
|
+
echo "created=false" >> $GITHUB_OUTPUT
|
|
1334
|
+
echo "url=" >> $GITHUB_OUTPUT
|
|
1335
|
+
fi
|
|
1336
|
+
|
|
1337
|
+
# Release compliance validation
|
|
1338
|
+
release_compliance:
|
|
1339
|
+
name: ✅ Release Compliance
|
|
1340
|
+
needs:
|
|
1341
|
+
[
|
|
1342
|
+
release_init,
|
|
1343
|
+
version,
|
|
1344
|
+
quality,
|
|
1345
|
+
release_signing,
|
|
1346
|
+
release_attestation,
|
|
1347
|
+
github_release,
|
|
1348
|
+
sentry_release,
|
|
1349
|
+
jira_release,
|
|
1350
|
+
]
|
|
1351
|
+
if: always()
|
|
1352
|
+
runs-on: ubuntu-latest
|
|
1353
|
+
outputs:
|
|
1354
|
+
status: ${{ steps.compliance.outputs.status }}
|
|
1355
|
+
report_url: ${{ steps.compliance.outputs.report_url }}
|
|
1356
|
+
steps:
|
|
1357
|
+
- name: Download All Artifacts
|
|
1358
|
+
uses: actions/download-artifact@v4
|
|
1359
|
+
with:
|
|
1360
|
+
pattern: '*-${{ github.run_id }}'
|
|
1361
|
+
|
|
1362
|
+
- name: Compliance Validation
|
|
1363
|
+
id: compliance
|
|
1364
|
+
run: |
|
|
1365
|
+
# Initialize compliance checks
|
|
1366
|
+
COMPLIANCE_PASSED=true
|
|
1367
|
+
COMPLIANCE_ISSUES=()
|
|
1368
|
+
|
|
1369
|
+
# Check 1: Required approvals
|
|
1370
|
+
if [ "${{ inputs.require_approval }}" == "true" ]; then
|
|
1371
|
+
if [ "${{ needs.release_approval.outputs.approved }}" != "true" ]; then
|
|
1372
|
+
COMPLIANCE_PASSED=false
|
|
1373
|
+
COMPLIANCE_ISSUES+=("Missing required approval")
|
|
1374
|
+
fi
|
|
1375
|
+
fi
|
|
1376
|
+
|
|
1377
|
+
# Check 2: Required signatures
|
|
1378
|
+
if [ "${{ inputs.require_signatures }}" == "true" ]; then
|
|
1379
|
+
if [ "${{ needs.release_signing.outputs.signed }}" != "true" ]; then
|
|
1380
|
+
COMPLIANCE_PASSED=false
|
|
1381
|
+
COMPLIANCE_ISSUES+=("Missing required signatures")
|
|
1382
|
+
fi
|
|
1383
|
+
fi
|
|
1384
|
+
|
|
1385
|
+
# Check 3: Quality gates
|
|
1386
|
+
if [ "${{ needs.quality.result }}" != "success" ] && [ "${{ needs.quality.result }}" != "skipped" ]; then
|
|
1387
|
+
COMPLIANCE_PASSED=false
|
|
1388
|
+
COMPLIANCE_ISSUES+=("Quality checks failed")
|
|
1389
|
+
fi
|
|
1390
|
+
|
|
1391
|
+
# Check 4: Attestation
|
|
1392
|
+
if [ ! -f "release-attestations-${{ needs.version.outputs.version }}/release-attestation.json" ]; then
|
|
1393
|
+
COMPLIANCE_PASSED=false
|
|
1394
|
+
COMPLIANCE_ISSUES+=("Missing release attestation")
|
|
1395
|
+
fi
|
|
1396
|
+
|
|
1397
|
+
# Check 5: Emergency release documentation
|
|
1398
|
+
if [ "${{ inputs.emergency_release }}" == "true" ]; then
|
|
1399
|
+
echo "⚠️ Emergency release - additional documentation required"
|
|
1400
|
+
fi
|
|
1401
|
+
|
|
1402
|
+
# Generate compliance report
|
|
1403
|
+
cat > compliance-report.json << EOF
|
|
1404
|
+
{
|
|
1405
|
+
"release_version": "${{ needs.version.outputs.version }}",
|
|
1406
|
+
"compliance_status": "$([[ $COMPLIANCE_PASSED == true ]] && echo 'PASSED' || echo 'FAILED')",
|
|
1407
|
+
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
|
1408
|
+
"correlation_id": "${{ needs.release_init.outputs.correlation_id }}",
|
|
1409
|
+
"frameworks": {
|
|
1410
|
+
"SOC2": {
|
|
1411
|
+
"CC8.1_change_management": true,
|
|
1412
|
+
"CC7.2_monitoring": true,
|
|
1413
|
+
"CC7.3_security_monitoring": ${{ needs.release_signing.outputs.signed == 'true' }},
|
|
1414
|
+
"CC6.1_logical_access": ${{ inputs.require_approval }}
|
|
1415
|
+
},
|
|
1416
|
+
"ISO27001": {
|
|
1417
|
+
"A.12.1_operational_procedures": true,
|
|
1418
|
+
"A.14.2_secure_development": true,
|
|
1419
|
+
"A.12.4_logging_monitoring": true,
|
|
1420
|
+
"A.14.2.8_security_testing": ${{ needs.quality.result == 'success' }}
|
|
1421
|
+
},
|
|
1422
|
+
"SLSA": {
|
|
1423
|
+
"level": ${{ needs.release_signing.outputs.signed == 'true' && '2' || '1' }},
|
|
1424
|
+
"source": true,
|
|
1425
|
+
"build": true,
|
|
1426
|
+
"provenance": true,
|
|
1427
|
+
"common_requirements": true
|
|
1428
|
+
}
|
|
1429
|
+
},
|
|
1430
|
+
"checks": {
|
|
1431
|
+
"approvals": {
|
|
1432
|
+
"required": ${{ inputs.require_approval }},
|
|
1433
|
+
"completed": ${{ needs.release_approval.outputs.approved == 'true' || inputs.require_approval == false }}
|
|
1434
|
+
},
|
|
1435
|
+
"signatures": {
|
|
1436
|
+
"required": ${{ inputs.require_signatures }},
|
|
1437
|
+
"completed": ${{ needs.release_signing.outputs.signed == 'true' || inputs.require_signatures == false }}
|
|
1438
|
+
},
|
|
1439
|
+
"quality": {
|
|
1440
|
+
"passed": ${{ needs.quality.result == 'success' || needs.quality.result == 'skipped' }},
|
|
1441
|
+
"skipped_checks": "${{ inputs.skip_jobs }}"
|
|
1442
|
+
},
|
|
1443
|
+
"attestation": {
|
|
1444
|
+
"created": ${{ needs.release_attestation.outputs.attestation_created == 'true' }}
|
|
1445
|
+
},
|
|
1446
|
+
"emergency_release": ${{ inputs.emergency_release }},
|
|
1447
|
+
"blackout_override": ${{ inputs.override_blackout }}
|
|
1448
|
+
},
|
|
1449
|
+
"issues": $(printf '%s\n' "${COMPLIANCE_ISSUES[@]}" | jq -R . | jq -s .)
|
|
1450
|
+
}
|
|
1451
|
+
EOF
|
|
1452
|
+
|
|
1453
|
+
# Set outputs
|
|
1454
|
+
echo "status=$([[ $COMPLIANCE_PASSED == true ]] && echo 'PASSED' || echo 'FAILED')" >> $GITHUB_OUTPUT
|
|
1455
|
+
echo "report_url=https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts" >> $GITHUB_OUTPUT
|
|
1456
|
+
|
|
1457
|
+
# Summary
|
|
1458
|
+
echo "## ✅ Compliance Validation" >> $GITHUB_STEP_SUMMARY
|
|
1459
|
+
echo "" >> $GITHUB_STEP_SUMMARY
|
|
1460
|
+
echo "**Status**: $([[ $COMPLIANCE_PASSED == true ]] && echo '✅ PASSED' || echo '❌ FAILED')" >> $GITHUB_STEP_SUMMARY
|
|
1461
|
+
echo "**Frameworks**: SOC2, ISO27001, SLSA Level ${{ needs.release_signing.outputs.signed == 'true' && '2' || '1' }}" >> $GITHUB_STEP_SUMMARY
|
|
1462
|
+
|
|
1463
|
+
if [ ${#COMPLIANCE_ISSUES[@]} -gt 0 ]; then
|
|
1464
|
+
echo "" >> $GITHUB_STEP_SUMMARY
|
|
1465
|
+
echo "### Issues Found" >> $GITHUB_STEP_SUMMARY
|
|
1466
|
+
printf -- '- %s\n' "${COMPLIANCE_ISSUES[@]}" >> $GITHUB_STEP_SUMMARY
|
|
1467
|
+
fi
|
|
1468
|
+
|
|
1469
|
+
- name: Generate Compliance Evidence Package
|
|
1470
|
+
run: |
|
|
1471
|
+
# Create evidence package
|
|
1472
|
+
mkdir -p compliance-evidence
|
|
1473
|
+
|
|
1474
|
+
# Copy all compliance-related artifacts
|
|
1475
|
+
find . -name "release-attestation*.json" -exec cp {} compliance-evidence/ \;
|
|
1476
|
+
find . -name "provenance.json" -exec cp {} compliance-evidence/ \;
|
|
1477
|
+
find . -name "compliance-report.json" -exec cp {} compliance-evidence/ \;
|
|
1478
|
+
find . -name "release-manifest.json*" -exec cp {} compliance-evidence/ \;
|
|
1479
|
+
find . -name "*.sig" -exec cp {} compliance-evidence/ \;
|
|
1480
|
+
find . -name "*.sha256" -exec cp {} compliance-evidence/ \;
|
|
1481
|
+
|
|
1482
|
+
# Create evidence summary
|
|
1483
|
+
cat > compliance-evidence/README.md << EOF
|
|
1484
|
+
# Release Compliance Evidence Package
|
|
1485
|
+
|
|
1486
|
+
**Release Version**: ${{ needs.version.outputs.version }}
|
|
1487
|
+
**Generated**: $(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
1488
|
+
**Environment**: ${{ inputs.environment }}
|
|
1489
|
+
**Correlation ID**: ${{ needs.release_init.outputs.correlation_id }}
|
|
1490
|
+
|
|
1491
|
+
## Contents
|
|
1492
|
+
|
|
1493
|
+
- Release attestations and provenance
|
|
1494
|
+
- Digital signatures (if applicable)
|
|
1495
|
+
- Compliance validation report
|
|
1496
|
+
- Release manifest with checksums
|
|
1497
|
+
- Approval records (if applicable)
|
|
1498
|
+
|
|
1499
|
+
## Compliance Status
|
|
1500
|
+
|
|
1501
|
+
**Overall Status**: ${{ steps.compliance.outputs.status }}
|
|
1502
|
+
|
|
1503
|
+
This release complies with:
|
|
1504
|
+
- SOC 2 Type II
|
|
1505
|
+
- ISO 27001:2022
|
|
1506
|
+
- SLSA Level ${{ needs.release_signing.outputs.signed == 'true' && '2' || '1' }}
|
|
1507
|
+
|
|
1508
|
+
## Verification Instructions
|
|
1509
|
+
|
|
1510
|
+
1. Verify signatures using the provided .sig files
|
|
1511
|
+
2. Check SHA256 checksums against artifacts
|
|
1512
|
+
3. Review attestation for build provenance
|
|
1513
|
+
4. Validate compliance report status
|
|
1514
|
+
|
|
1515
|
+
## Retention
|
|
1516
|
+
|
|
1517
|
+
This evidence package should be retained for 7 years per compliance requirements.
|
|
1518
|
+
EOF
|
|
1519
|
+
|
|
1520
|
+
# Create tarball
|
|
1521
|
+
tar -czf compliance-evidence-${{ needs.version.outputs.version }}.tar.gz compliance-evidence/
|
|
1522
|
+
|
|
1523
|
+
- name: Store Compliance Evidence
|
|
1524
|
+
uses: actions/upload-artifact@v4
|
|
1525
|
+
with:
|
|
1526
|
+
name: compliance-evidence-${{ needs.version.outputs.version }}
|
|
1527
|
+
path: compliance-evidence-${{ needs.version.outputs.version }}.tar.gz
|
|
1528
|
+
retention-days: 2555 # 7 years for compliance
|
|
1529
|
+
|
|
1530
|
+
# Final release summary
|
|
1531
|
+
release_summary:
|
|
1532
|
+
name: 📋 Release Summary
|
|
1533
|
+
needs:
|
|
1534
|
+
[
|
|
1535
|
+
release_init,
|
|
1536
|
+
quality,
|
|
1537
|
+
version,
|
|
1538
|
+
release_signing,
|
|
1539
|
+
release_attestation,
|
|
1540
|
+
github_release,
|
|
1541
|
+
sentry_release,
|
|
1542
|
+
jira_release,
|
|
1543
|
+
release_compliance,
|
|
1544
|
+
]
|
|
1545
|
+
if: always()
|
|
1546
|
+
runs-on: ubuntu-latest
|
|
1547
|
+
steps:
|
|
1548
|
+
- name: Generate Final Summary
|
|
1549
|
+
run: |
|
|
1550
|
+
# Create comprehensive summary
|
|
1551
|
+
cat >> $GITHUB_STEP_SUMMARY << 'EOF'
|
|
1552
|
+
|
|
1553
|
+
# 🎉 Enterprise Release Complete
|
|
1554
|
+
|
|
1555
|
+
## Release Information
|
|
1556
|
+
- **Version**: ${{ needs.version.outputs.version }}
|
|
1557
|
+
- **Tag**: ${{ needs.version.outputs.tag }}
|
|
1558
|
+
- **Environment**: ${{ inputs.environment }}
|
|
1559
|
+
- **Correlation ID**: ${{ needs.release_init.outputs.correlation_id }}
|
|
1560
|
+
|
|
1561
|
+
## Enterprise Features
|
|
1562
|
+
- **Approval Required**: ${{ inputs.require_approval && '✅ Yes' || '❌ No' }}
|
|
1563
|
+
- **Signatures**: ${{ needs.release_signing.outputs.signed == 'true' && '✅ Signed' || '❌ Not Signed' }}
|
|
1564
|
+
- **SBOM**: ${{ inputs.generate_sbom && '✅ Generated' || '❌ Not Generated' }}
|
|
1565
|
+
- **Compliance**: ${{ needs.release_compliance.outputs.status }}
|
|
1566
|
+
|
|
1567
|
+
## Execution Summary
|
|
1568
|
+
- **Total Duration**: $(($(date +%s) - ${{ needs.release_init.outputs.start_time }}))s
|
|
1569
|
+
- **Quality Checks**: ${{ needs.quality.result }}
|
|
1570
|
+
- **Version Creation**: ${{ needs.version.result }}
|
|
1571
|
+
- **GitHub Release**: ${{ needs.github_release.result }}
|
|
1572
|
+
|
|
1573
|
+
## Outputs
|
|
1574
|
+
- **Release URL**: ${{ needs.github_release.outputs.release_url || 'N/A' }}
|
|
1575
|
+
- **Attestation**: ${{ needs.release_attestation.outputs.attestation_url || 'N/A' }}
|
|
1576
|
+
- **Compliance Report**: ${{ needs.release_compliance.outputs.report_url || 'N/A' }}
|
|
1577
|
+
- **Sentry Release**: ${{ needs.sentry_release.outputs.sentry_release_url || 'Not configured' }}
|
|
1578
|
+
- **Jira Release**: ${{ needs.jira_release.outputs.jira_release_url || 'Not configured' }}
|
|
1579
|
+
|
|
1580
|
+
## Security & Compliance
|
|
1581
|
+
- ✅ Audit trail generated
|
|
1582
|
+
- ✅ Release attestation created
|
|
1583
|
+
- ✅ Compliance evidence packaged
|
|
1584
|
+
- ${{ needs.release_signing.outputs.signed == 'true' && '✅ Release signed' || '⚠️ Release not signed' }}
|
|
1585
|
+
|
|
1586
|
+
## Next Steps
|
|
1587
|
+
1. Review the release at ${{ needs.github_release.outputs.release_url || 'GitHub Releases page' }}
|
|
1588
|
+
2. Verify signatures if applicable
|
|
1589
|
+
3. Review compliance report
|
|
1590
|
+
4. Deploy using your deployment workflow
|
|
1591
|
+
5. Run load tests if needed
|
|
1592
|
+
EOF
|
|
1593
|
+
|
|
1594
|
+
# Set workflow status
|
|
1595
|
+
if [ "${{ needs.github_release.result }}" == "success" ] && [ "${{ needs.release_compliance.outputs.status }}" == "PASSED" ]; then
|
|
1596
|
+
echo "✅ Enterprise release completed successfully!" >> $GITHUB_STEP_SUMMARY
|
|
1597
|
+
else
|
|
1598
|
+
echo "⚠️ Release completed with issues. Please review the compliance report." >> $GITHUB_STEP_SUMMARY
|
|
1599
|
+
fi
|