Controlled Release #5
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Controlled Release Workflow with Dual Approval Gates | |
| # | |
| # This workflow implements a secure release process with two manual approval | |
| # checkpoints before publishing the package. It ensures that releases are | |
| # thoroughly vetted and approved by multiple maintainers. | |
| # | |
| # SETUP REQUIRED: | |
| # Before using this workflow, you must create two GitHub Environments: | |
| # | |
| # 1. "pre-release-review" environment: | |
| # - Go to Settings → Environments → New environment | |
| # - Name: pre-release-review | |
| # - Enable "Required reviewers" | |
| # - Add 1-2 reviewers who must approve before proceeding | |
| # | |
| # 2. "public-release" environment: | |
| # - Go to Settings → Environments → New environment | |
| # - Name: public-release | |
| # - Enable "Required reviewers" | |
| # - Add 1-2 DIFFERENT reviewers (for dual approval) | |
| # - Optional: Enable "Wait timer" for a cooling-off period | |
| # - NOTE: If you already have this environment, just verify it has required reviewers enabled | |
| # | |
| # USAGE: | |
| # To trigger a release: | |
| # 1. Push a git tag matching v* (e.g., v0.1.0, v1.2.3) | |
| # git tag -a v0.1.0 -m "Release version 0.1.0" | |
| # git push origin v0.1.0 | |
| # 2. The workflow will automatically start | |
| # 3. First approval gate: review validation results | |
| # 4. Second approval gate: approve final release to CRAN/GitHub | |
| # | |
| # WORKFLOW STAGES: | |
| # 1. Validation: Run all checks, tests, and build package tarball | |
| # 2. GATE 1 (pre-release-review): Manual approval after reviewing validation | |
| # 3. Pre-release: Create draft GitHub release with artifacts | |
| # 4. GATE 2 (public-release): Final manual approval before publishing | |
| # 5. Publish: Release to GitHub and optionally submit to CRAN | |
| name: Controlled Release | |
| on: | |
| push: | |
| tags: | |
| - 'v*' | |
| workflow_dispatch: | |
| inputs: | |
| version: | |
| description: 'Version to release (e.g., 0.1.0)' | |
| required: true | |
| type: string | |
| env: | |
| R_VERSION: 'release' | |
| jobs: | |
| # STAGE 1: Validation | |
| # Runs all quality checks without requiring approval | |
| validate: | |
| name: Validation - Quality Checks | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version: ${{ steps.version.outputs.version }} | |
| tarball: ${{ steps.build.outputs.tarball }} | |
| steps: | |
| - name: Extract version from tag or input | |
| id: version | |
| run: | | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | |
| VERSION="${{ inputs.version }}" | |
| else | |
| VERSION="${GITHUB_REF#refs/tags/v}" | |
| fi | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| echo "Release version: $VERSION" | |
| - uses: actions/checkout@v4 | |
| - uses: r-lib/actions/setup-r@v2 | |
| with: | |
| r-version: ${{ env.R_VERSION }} | |
| use-public-rspm: true | |
| - uses: r-lib/actions/setup-r-dependencies@v2 | |
| with: | |
| extra-packages: any::rcmdcheck, any::pkgbuild, any::covr | |
| needs: check | |
| - name: Verify version consistency across all metadata files | |
| run: | | |
| VERSION="${{ steps.version.outputs.version }}" | |
| echo "=== Comprehensive Version Validation ===" | |
| echo "Expected version (from tag): $VERSION" | |
| echo "" | |
| # Run our comprehensive version consistency checker | |
| Rscript .dev/check-version-consistency.R | |
| if [ $? -ne 0 ]; then | |
| echo "" | |
| echo "ERROR: Version consistency check failed!" | |
| echo "All metadata files must have matching versions before release." | |
| exit 1 | |
| fi | |
| # Verify tag matches DESCRIPTION | |
| DESC_VERSION=$(Rscript -e "cat(as.character(desc::desc_get_version()))") | |
| if [ "$VERSION" != "$DESC_VERSION" ]; then | |
| echo "" | |
| echo "ERROR: Git tag version ($VERSION) does not match DESCRIPTION version ($DESC_VERSION)" | |
| echo "Ensure the tag matches the version in DESCRIPTION file." | |
| exit 1 | |
| fi | |
| echo "" | |
| echo "✓ All version validation checks passed" | |
| echo " - DESCRIPTION version matches tag" | |
| echo " - CITATION versions consistent" | |
| echo " - .zenodo.json version consistent" | |
| echo " - All metadata files in sync" | |
| - name: Run R CMD check | |
| uses: r-lib/actions/check-r-package@v2 | |
| with: | |
| build_args: 'c("--no-manual","--compact-vignettes=gs+qpdf")' | |
| error-on: '"error"' | |
| - name: Run test suite with coverage | |
| run: | | |
| Rscript -e ' | |
| covr_results <- covr::package_coverage(quiet = FALSE) | |
| covr_percent <- covr::percent_coverage(covr_results) | |
| cat(sprintf("\n=== Test Coverage: %.1f%% ===\n", covr_percent)) | |
| if (covr_percent < 30) { | |
| stop("Coverage below 30% threshold: ", covr_percent, "%") | |
| } | |
| ' | |
| - name: Build package tarball | |
| id: build | |
| run: | | |
| # Build package and capture only the tarball path (last line of output) | |
| TARBALL=$(Rscript -e 'path <- pkgbuild::build(dest_path = ".", binary = FALSE, vignettes = TRUE, manual = TRUE); cat(path, "\n", sep="", file=stderr()); cat(path)' 2>&1 | tail -1) | |
| echo "tarball=$TARBALL" >> $GITHUB_OUTPUT | |
| echo "Built package tarball: $TARBALL" | |
| # Verify tarball exists and is valid | |
| if [ ! -f "$TARBALL" ]; then | |
| echo "Error: Tarball not found: $TARBALL" | |
| exit 1 | |
| fi | |
| tar -tzf "$TARBALL" > /dev/null | |
| echo "✓ Tarball verification passed" | |
| - name: Upload package tarball | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: package-tarball | |
| path: ${{ steps.build.outputs.tarball }} | |
| retention-days: 30 | |
| - name: Generate validation report | |
| run: | | |
| cat > validation-report.md <<EOF | |
| # Release Validation Report | |
| **Version:** ${{ steps.version.outputs.version }} | |
| **Date:** $(date -u +"%Y-%m-%d %H:%M:%S UTC") | |
| **Commit:** ${{ github.sha }} | |
| ## Quality Checks Status | |
| - ✅ R CMD check passed (0 errors, 0 warnings) | |
| - ✅ Test suite passed with adequate coverage | |
| - ✅ Package tarball built successfully | |
| - ✅ Version consistency verified | |
| ## Package Artifact | |
| - **Tarball:** ${{ steps.build.outputs.tarball }} | |
| ## Next Steps | |
| Review this validation report and approve at the **pre-release-review** gate | |
| if all checks are satisfactory. | |
| EOF | |
| cat validation-report.md | |
| - name: Upload validation report | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: validation-report | |
| path: validation-report.md | |
| # GATE 1: Pre-Release Review | |
| # Manual approval required after reviewing validation results | |
| pre-release-review: | |
| name: Gate 1 - Pre-Release Review | |
| needs: validate | |
| runs-on: ubuntu-latest | |
| environment: pre-release-review | |
| steps: | |
| - name: Download validation report | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: validation-report | |
| - name: Display validation report | |
| run: | | |
| echo "=== VALIDATION REPORT FOR REVIEW ===" | |
| cat validation-report.md | |
| echo "" | |
| echo "=== APPROVAL GATE 1 ===" | |
| echo "If validation looks good, approve this environment to proceed to pre-release." | |
| - name: Record approval | |
| run: | | |
| echo "Pre-release review approved at $(date -u +"%Y-%m-%d %H:%M:%S UTC")" | |
| echo "Approved by: ${{ github.actor }}" | |
| # STAGE 2: Create Pre-Release | |
| # Creates a draft GitHub release with the package tarball | |
| create-pre-release: | |
| name: Create Draft Release | |
| needs: [validate, pre-release-review] | |
| runs-on: ubuntu-latest | |
| outputs: | |
| release-id: ${{ steps.create-release.outputs.id }} | |
| upload-url: ${{ steps.create-release.outputs.upload_url }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Download package tarball | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: package-tarball | |
| - name: Download validation report | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: validation-report | |
| - name: Generate release notes | |
| id: release-notes | |
| run: | | |
| VERSION="${{ needs.validate.outputs.version }}" | |
| # Extract NEWS.md section for this version if it exists | |
| if [ -f "NEWS.md" ]; then | |
| awk "/^# emburden $VERSION/,/^# emburden [0-9]/" NEWS.md | head -n -1 > release-notes.md | |
| else | |
| echo "Release notes not found in NEWS.md" > release-notes.md | |
| fi | |
| # Append validation summary | |
| cat >> release-notes.md <<EOF | |
| --- | |
| ## Package Validation | |
| All quality checks passed: | |
| - ✅ R CMD check (0 errors, 0 warnings) | |
| - ✅ Test coverage above threshold | |
| - ✅ Version consistency verified | |
| See attached validation report for details. | |
| ## Installation | |
| \`\`\`r | |
| # Install from GitHub | |
| # install.packages("remotes") | |
| remotes::install_github("ericscheier/emburden@v$VERSION") | |
| \`\`\` | |
| ## Archive DOI | |
| This release is archived on Zenodo with a permanent DOI for citation. | |
| EOF | |
| cat release-notes.md | |
| - name: Create draft GitHub release | |
| id: create-release | |
| uses: actions/create-release@v1 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.PUBLIC_REPO_TOKEN }} | |
| with: | |
| tag_name: v${{ needs.validate.outputs.version }} | |
| release_name: emburden v${{ needs.validate.outputs.version }} | |
| body_path: release-notes.md | |
| draft: true | |
| prerelease: false | |
| - name: Upload tarball to release | |
| uses: actions/upload-release-asset@v1 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.PUBLIC_REPO_TOKEN }} | |
| with: | |
| upload_url: ${{ steps.create-release.outputs.upload_url }} | |
| asset_path: ${{ needs.validate.outputs.tarball }} | |
| asset_name: ${{ needs.validate.outputs.tarball }} | |
| asset_content_type: application/gzip | |
| - name: Upload validation report to release | |
| uses: actions/upload-release-asset@v1 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.PUBLIC_REPO_TOKEN }} | |
| with: | |
| upload_url: ${{ steps.create-release.outputs.upload_url }} | |
| asset_path: validation-report.md | |
| asset_name: validation-report.md | |
| asset_content_type: text/markdown | |
| # GATE 2: Production Release Approval | |
| # Final manual approval before publishing | |
| production-release-review: | |
| name: Gate 2 - Production Release Approval | |
| needs: [validate, create-pre-release] | |
| runs-on: ubuntu-latest | |
| environment: public-release | |
| steps: | |
| - name: Review release | |
| run: | | |
| echo "=== FINAL APPROVAL GATE ===" | |
| echo "Version: ${{ needs.validate.outputs.version }}" | |
| echo "Draft release created: ${{ needs.create-pre-release.outputs.release-id }}" | |
| echo "" | |
| echo "Review the draft release at:" | |
| echo "https://github.com/${{ github.repository }}/releases" | |
| echo "" | |
| echo "If everything looks good, approve this environment to publish the release." | |
| - name: Record final approval | |
| run: | | |
| echo "Production release approved at $(date -u +"%Y-%m-%d %H:%M:%S UTC")" | |
| echo "Approved by: ${{ github.actor }}" | |
| # STAGE 3: Publish Release | |
| # Publishes the GitHub release (triggers Zenodo archival) | |
| publish-release: | |
| name: Publish Release | |
| needs: [validate, create-pre-release, production-release-review] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Publish GitHub release | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ secrets.PUBLIC_REPO_TOKEN }} | |
| script: | | |
| await github.rest.repos.updateRelease({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| release_id: ${{ needs.create-pre-release.outputs.release-id }}, | |
| draft: false | |
| }); | |
| console.log('✅ Release published successfully'); | |
| - name: Post-release summary | |
| run: | | |
| cat <<EOF | |
| ================================================ | |
| 🎉 RELEASE PUBLISHED SUCCESSFULLY 🎉 | |
| ================================================ | |
| Version: v${{ needs.validate.outputs.version }} | |
| Release URL: https://github.com/${{ github.repository }}/releases/tag/v${{ needs.validate.outputs.version }} | |
| ✅ Dual approval gates passed | |
| ✅ GitHub release published | |
| ✅ Zenodo archival will be triggered automatically | |
| NEXT STEPS (Manual): | |
| 1. Verify Zenodo DOI appears on the release page | |
| 2. If submitting to CRAN: | |
| - Download the package tarball from the release | |
| - Submit via https://cran.r-project.org/submit.html | |
| - Monitor CRAN submission status | |
| 3. Announce the release: | |
| - Update package website if applicable | |
| - Social media announcements | |
| - Notify collaborators | |
| ================================================ | |
| EOF | |
| # Optional: CRAN submission preparation | |
| # This job provides instructions but doesn't auto-submit | |
| # (CRAN submissions must be done manually) | |
| cran-submission-info: | |
| name: CRAN Submission Information | |
| needs: [validate, publish-release] | |
| runs-on: ubuntu-latest | |
| if: success() | |
| steps: | |
| - name: CRAN submission instructions | |
| run: | | |
| cat <<EOF | |
| ======================================== | |
| 📦 CRAN SUBMISSION INSTRUCTIONS | |
| ======================================== | |
| The package is now ready for CRAN submission. | |
| IMPORTANT: CRAN submissions must be done manually by the package maintainer. | |
| Steps to submit: | |
| 1. Download the package tarball: | |
| https://github.com/${{ github.repository }}/releases/download/v${{ needs.validate.outputs.version }}/${{ needs.validate.outputs.tarball }} | |
| 2. Review CRAN submission checklist: | |
| - R CMD check passes with 0 errors, 0 warnings ✅ | |
| - Package builds successfully ✅ | |
| - All tests pass ✅ | |
| - NEWS.md updated with version notes | |
| - DESCRIPTION file has correct maintainer email | |
| - All examples run successfully | |
| 3. Submit to CRAN: | |
| - Go to: https://cran.r-project.org/submit.html | |
| - Upload: ${{ needs.validate.outputs.tarball }} | |
| - Fill in submission form | |
| - Confirm maintainer email | |
| 4. Monitor submission: | |
| - Check email for CRAN automated checks | |
| - Respond to any reviewer feedback | |
| - Typical review time: 2-7 days | |
| 5. After CRAN acceptance: | |
| - Package appears on CRAN within 24 hours | |
| - Update README badges if needed | |
| ======================================== | |
| EOF |