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
14 changes: 13 additions & 1 deletion .devcontainer/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Handle smoke-test dispatch failures with a targeted issue while avoiding destructive rollback after publish artifacts are already released
- **Redesigned smoke-test dispatch release orchestration** ([#358](https://github.com/vig-os/devcontainer/issues/358))
- Replace premature `publish-release` behavior with full downstream orchestration: deploy-to-dev merge gate, `prepare-release.yml`, release PR readiness/approval, and `release.yml` dispatch polling
- Add release-branch CHANGELOG sync so smoke-test `main` ends with the same `CHANGELOG.md` content as `vig-os/devcontainer` at the dispatched tag
- Add upstream failure issue reporting with job-phase results and cleanup guidance when dispatch orchestration fails
- **Smoke-test release orchestration now runs as two phases** ([#402](https://github.com/vig-os/devcontainer/issues/402))
- Keep `repository-dispatch.yml` focused on deploy/prepare/release-PR readiness and move release dispatch to a dedicated merged-PR workflow (`on-release-pr-merge.yml`)
- Add release-kind labeling and auto-merge enablement for release PRs, and keep upstream failure notifications in both phases
- Remove release-branch upstream `CHANGELOG.md` sync from `repository-dispatch.yml` (previously added in [#358](https://github.com/vig-os/devcontainer/issues/358))

### Fixed

- **Release app permission docs now include downstream workflow dispatch requirements** ([#397](https://github.com/vig-os/devcontainer/issues/397))
- Update `docs/RELEASE_CYCLE.md` to require `Actions` read/write for `RELEASE_APP` on the validation repository
- Clarify this is required so downstream `repository-dispatch.yml` can trigger release orchestration workflows via `workflow_dispatch`
- **Smoke-test dispatch no longer fails on release PR self-approval** ([#402](https://github.com/vig-os/devcontainer/issues/402))
- Remove bot self-approval from `repository-dispatch.yml` and replace with release-kind labeling plus auto-merge enablement
- Remove in-job polling for release PR merge and downstream release execution from phase 1 orchestration
- Phase 2 (`on-release-pr-merge.yml`) fails validation unless the merged release PR has `release-kind:final` or `release-kind:candidate`
- **Sync-main-to-dev PRs now trigger CI reliably in downstream repos** ([#398](https://github.com/vig-os/devcontainer/issues/398))
- Replace API-based sync branch creation with `git push` in `assets/workspace/.github/workflows/sync-main-to-dev.yml`
- **Sync-main-to-dev no longer dispatches CI via workflow_dispatch** ([#405](https://github.com/vig-os/devcontainer/issues/405))
- `workflow_dispatch` runs are omitted from the PR status check rollup, so they do not satisfy branch protection on the sync PR
- Remove the post-PR `gh workflow run ci.yml` step and drop `actions: write` from the sync job in `.github/workflows/sync-main-to-dev.yml` and `assets/workspace/.github/workflows/sync-main-to-dev.yml`

- **Release finalization now commits generated docs and refreshes PR content** ([#300](https://github.com/vig-os/devcontainer/issues/300))
- Final release automation regenerates docs before committing so pre-commit `generate-docs` does not fail CI with tracked file diffs
Expand Down
67 changes: 63 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,32 @@ permissions:
contents: read

jobs:
resolve-image:
name: Resolve image tag
runs-on: ubuntu-22.04
timeout-minutes: 2
outputs:
image-tag: ${{ steps.resolve.outputs.image-tag }}
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
sparse-checkout: |
.vig-os
.github/actions/resolve-image
sparse-checkout-cone-mode: false

- name: Resolve container image
id: resolve
uses: ./.github/actions/resolve-image

core:
name: Release Core
uses: ./.github/workflows/release-core.yml
permissions:
actions: write
contents: write
pull-requests: read
with:
version: ${{ inputs.version }}
release_kind: ${{ inputs.release-kind }}
Expand All @@ -65,6 +88,8 @@ jobs:
needs: [core, extension]
if: ${{ inputs.dry-run != true }}
uses: ./.github/workflows/release-publish.yml
permissions:
contents: write
with:
version: ${{ needs.core.outputs.version }}
finalize_sha: ${{ needs.core.outputs.finalize_sha }}
Expand All @@ -77,21 +102,55 @@ jobs:

rollback:
name: Rollback on Failure
needs: [core, extension, publish]
needs: [resolve-image, core, extension, publish]
runs-on: ubuntu-22.04
container:
image: ghcr.io/vig-os/devcontainer:${{ needs.core.outputs.image_tag }}
image: ghcr.io/vig-os/devcontainer:${{ needs.resolve-image.outputs.image-tag }}
timeout-minutes: 10
if: ${{ failure() && inputs.dry-run != true }}
defaults:
run:
shell: bash
if: >-
${{
always() &&
inputs.dry-run != true &&
needs.resolve-image.result == 'success' &&
(
needs.core.result == 'failure' ||
needs.extension.result == 'failure' ||
needs.publish.result == 'failure'
)
}}
permissions:
contents: write
issues: write
steps:
- name: Generate release app token
id: release_app_token
if: ${{ needs.core.outputs.version != '' }}
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3
with:
app-id: ${{ secrets.RELEASE_APP_ID }}
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}

- name: Generate commit app token
id: commit_app_token
if: ${{ needs.core.outputs.pre_finalize_sha != '' }}
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3
with:
app-id: ${{ secrets.COMMIT_APP_ID }}
private-key: ${{ secrets.COMMIT_APP_PRIVATE_KEY }}

- name: Checkout repository
if: ${{ needs.core.outputs.pre_finalize_sha != '' }}
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
token: ${{ steps.commit_app_token.outputs.token }}

- name: Fix git safe.directory
if: ${{ needs.core.outputs.pre_finalize_sha != '' }}
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"

- name: Configure git
if: ${{ needs.core.outputs.pre_finalize_sha != '' }}
Expand Down Expand Up @@ -134,7 +193,7 @@ jobs:
env:
VERSION: ${{ needs.core.outputs.version }}
PR_NUMBER: ${{ needs.core.outputs.pr_number }}
GH_TOKEN: ${{ github.token }}
GH_TOKEN: ${{ steps.release_app_token.outputs.token }}
run: |
set -euo pipefail
WORKFLOW_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
Expand Down
167 changes: 16 additions & 151 deletions .github/workflows/repository-dispatch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -482,7 +482,7 @@ jobs:
exit 1

ready-release-pr:
name: Sync changelog and prepare release PR
name: Prepare release PR
runs-on: ubuntu-22.04
timeout-minutes: 35
env:
Expand All @@ -492,7 +492,7 @@ jobs:
release_pr: ${{ steps.locate_release_pr.outputs.release_pr }}
release_pr_url: ${{ steps.locate_release_pr.outputs.release_pr_url }}
steps:
- name: Generate release app token for PR and contents operations
- name: Generate release app token for PR operations
id: generate_release_token
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3
with:
Expand All @@ -501,41 +501,6 @@ jobs:
owner: ${{ github.repository_owner }}
repositories: ${{ github.event.repository.name }}

- name: Sync upstream CHANGELOG onto release branch
env:
GH_TOKEN: ${{ steps.generate_release_token.outputs.token }}
TAG: ${{ needs.validate.outputs.tag }}
BASE_VERSION: ${{ needs.validate.outputs.base_version }}
run: |
set -euo pipefail
CHANGELOG_URL="https://raw.githubusercontent.com/vig-os/devcontainer/${TAG}/CHANGELOG.md"
ATTEMPT=1
MAX_ATTEMPTS=3
until [ "${ATTEMPT}" -gt "${MAX_ATTEMPTS}" ]; do
if curl -sSf "${CHANGELOG_URL}" -o /tmp/upstream-changelog.md; then
break
fi
if [ "${ATTEMPT}" -eq "${MAX_ATTEMPTS}" ]; then
echo "ERROR: failed to download upstream changelog after ${MAX_ATTEMPTS} attempts: ${CHANGELOG_URL}"
exit 1
fi
echo "Changelog download attempt ${ATTEMPT}/${MAX_ATTEMPTS} failed; retrying in 5s..."
ATTEMPT=$((ATTEMPT + 1))
sleep 5
done

FILE_SHA="$(gh api \
"repos/${GITHUB_REPOSITORY}/contents/CHANGELOG.md?ref=release/${BASE_VERSION}" \
--jq '.sha')"
CONTENT="$(base64 -w0 < /tmp/upstream-changelog.md)"

gh api -X PUT "repos/${GITHUB_REPOSITORY}/contents/CHANGELOG.md" \
-f message="chore: sync CHANGELOG from devcontainer ${TAG}" \
-f content="${CONTENT}" \
-f sha="${FILE_SHA}" \
-f branch="release/${BASE_VERSION}" >/dev/null
echo "Synced upstream CHANGELOG to release/${BASE_VERSION}"

- name: Locate release PR
id: locate_release_pr
env:
Expand All @@ -552,7 +517,7 @@ jobs:
echo "release_pr=${PR_NUMBER}" >> "${GITHUB_OUTPUT}"
echo "release_pr_url=${PR_URL}" >> "${GITHUB_OUTPUT}"

- name: Mark release PR ready and approve
- name: Mark release PR ready
env:
GH_TOKEN: ${{ steps.generate_release_token.outputs.token }}
PR_NUMBER: ${{ steps.locate_release_pr.outputs.release_pr }}
Expand All @@ -562,121 +527,30 @@ jobs:
if [ "${IS_DRAFT}" = "true" ]; then
gh pr ready "${PR_NUMBER}"
fi
gh pr review "${PR_NUMBER}" --approve

- name: Wait for release PR CI and merge
- name: Label release PR with release kind
env:
GH_TOKEN: ${{ steps.generate_release_token.outputs.token }}
PR_NUMBER: ${{ steps.locate_release_pr.outputs.release_pr }}
run: |
set -euo pipefail
TIMEOUT=1800
INTERVAL=30
ELAPSED=0
AUTO_MERGE_SET=false

while [ "${ELAPSED}" -lt "${TIMEOUT}" ]; do
PR_STATE="$(gh pr view "${PR_NUMBER}" --json state --jq '.state' 2>/dev/null || echo unknown)"
if [ "${PR_STATE}" = "MERGED" ]; then
echo "Release PR merged: #${PR_NUMBER}"
exit 0
fi
if [ "${PR_STATE}" = "CLOSED" ]; then
echo "ERROR: release PR closed without merge: #${PR_NUMBER}"
exit 1
fi

MERGE_STATE="$(gh pr view "${PR_NUMBER}" --json mergeStateStatus --jq '.mergeStateStatus' 2>/dev/null || echo unknown)"
if [ "${MERGE_STATE}" = "CLEAN" ] && [ "${AUTO_MERGE_SET}" = "false" ]; then
if gh pr merge "${PR_NUMBER}" --auto --merge; then
AUTO_MERGE_SET=true
else
echo "Warning: could not enable auto-merge yet; will retry"
fi
fi

sleep "${INTERVAL}"
ELAPSED=$((ELAPSED + INTERVAL))
echo "Waiting for release PR merge... (${ELAPSED}s/${TIMEOUT}s)"
done

echo "ERROR: timed out waiting for release PR merge"
exit 1

trigger-release:
name: Trigger and wait for release workflow
runs-on: ubuntu-22.04
timeout-minutes: 35
env:
GH_REPO: ${{ github.repository }}
needs: [validate, ready-release-pr]
outputs:
before_run_id: ${{ steps.capture_release_before.outputs.before_run_id }}
steps:
- name: Generate release app token for release workflow dispatch
id: generate_release_token
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3
with:
app-id: ${{ secrets.RELEASE_APP_ID }}
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
repositories: ${{ github.event.repository.name }}

- name: Capture latest release run id
id: capture_release_before
env:
GH_TOKEN: ${{ steps.generate_release_token.outputs.token }}
run: |
set -euo pipefail
BEFORE_RUN_ID="$(
gh run list --workflow release.yml --branch "${WORKFLOW_REF}" --limit 1 --json databaseId --jq '.[0].databaseId // 0' 2>/dev/null || echo 0
)"
echo "before_run_id=${BEFORE_RUN_ID}" >> "${GITHUB_OUTPUT}"

- name: Trigger release workflow
env:
GH_TOKEN: ${{ steps.generate_release_token.outputs.token }}
BASE_VERSION: ${{ needs.validate.outputs.base_version }}
RELEASE_KIND: ${{ needs.validate.outputs.release_kind }}
run: |
set -euo pipefail
gh workflow run release.yml \
--ref "${WORKFLOW_REF}" \
-f version="${BASE_VERSION}" \
-f release-kind="${RELEASE_KIND}"

- name: Wait for release workflow completion
LABEL="release-kind:${RELEASE_KIND}"
gh label create "${LABEL}" --color "5319E7" \
--description "Automated release kind label for dispatch orchestration" \
--force >/dev/null 2>&1 || true
gh pr edit "${PR_NUMBER}" --remove-label "release-kind:candidate" >/dev/null 2>&1 || true
gh pr edit "${PR_NUMBER}" --remove-label "release-kind:final" >/dev/null 2>&1 || true
gh pr edit "${PR_NUMBER}" --add-label "${LABEL}"

- name: Enable release PR auto-merge
env:
GH_TOKEN: ${{ steps.generate_release_token.outputs.token }}
BEFORE_RUN_ID: ${{ steps.capture_release_before.outputs.before_run_id }}
PR_NUMBER: ${{ steps.locate_release_pr.outputs.release_pr }}
run: |
set -euo pipefail
TIMEOUT=1800
INTERVAL=30
ELAPSED=0

while [ "${ELAPSED}" -lt "${TIMEOUT}" ]; do
RUN_ID="$(gh run list --workflow release.yml --branch "${WORKFLOW_REF}" --limit 1 --json databaseId --jq '.[0].databaseId // empty' 2>/dev/null || true)"
if [ -n "${RUN_ID}" ] && [ "${RUN_ID}" -gt "${BEFORE_RUN_ID}" ]; then
STATUS="$(gh run view "${RUN_ID}" --json status --jq '.status' 2>/dev/null || echo unknown)"
if [ "${STATUS}" = "completed" ]; then
CONCLUSION="$(gh run view "${RUN_ID}" --json conclusion --jq '.conclusion' 2>/dev/null || echo unknown)"
if [ "${CONCLUSION}" != "success" ]; then
echo "ERROR: release workflow concluded with '${CONCLUSION}'"
exit 1
fi
echo "release workflow completed successfully"
exit 0
fi
fi

sleep "${INTERVAL}"
ELAPSED=$((ELAPSED + INTERVAL))
echo "Waiting for release workflow... (${ELAPSED}s/${TIMEOUT}s)"
done

echo "ERROR: timed out waiting for release workflow completion"
exit 1
gh pr merge "${PR_NUMBER}" --auto --merge || \
echo "Warning: could not enable auto-merge yet"

summary:
name: Dispatch summary
Expand All @@ -689,7 +563,6 @@ jobs:
- cleanup-release
- trigger-prepare-release
- ready-release-pr
- trigger-release
if: always()
steps:
- name: Write source context summary
Expand Down Expand Up @@ -725,7 +598,6 @@ jobs:
echo "Cleanup: ${{ needs.cleanup-release.result }}"
echo "Prepare: ${{ needs.trigger-prepare-release.result }}"
echo "Release PR: ${{ needs.ready-release-pr.result }}"
echo "Release: ${{ needs.trigger-release.result }}"
echo "Deploy PR: ${{ needs.deploy.outputs.pr_url }}"
echo "Release PR: ${{ needs.ready-release-pr.outputs.release_pr_url }}"
echo ""
Expand Down Expand Up @@ -762,11 +634,6 @@ jobs:
FAILED=true
fi

if [ "${{ needs.trigger-release.result }}" != "success" ]; then
echo "ERROR: Release workflow orchestration job failed"
FAILED=true
fi

if [ "${FAILED}" = "true" ]; then
echo ""
echo "Dispatch orchestration failed"
Expand All @@ -788,7 +655,6 @@ jobs:
- cleanup-release
- trigger-prepare-release
- ready-release-pr
- trigger-release
- summary
steps:
- name: Generate release app token for upstream issue creation
Expand Down Expand Up @@ -843,7 +709,6 @@ jobs:
- cleanup-release: \`${{ needs.cleanup-release.result }}\`
- trigger-prepare-release: \`${{ needs.trigger-prepare-release.result }}\`
- ready-release-pr: \`${{ needs.ready-release-pr.result }}\`
- trigger-release: \`${{ needs.trigger-release.result }}\`
- summary: \`${{ needs.summary.result }}\`

## Manual cleanup guidance
Expand Down
Loading
Loading