diff --git a/.github/nightly-allowed-files.sh b/.github/nightly-allowed-files.sh new file mode 100644 index 0000000..0b98e14 --- /dev/null +++ b/.github/nightly-allowed-files.sh @@ -0,0 +1,8 @@ +# Nightly scan output files โ€” shared allowlist used by: +# - nightly-scan.yml (inline path validation) +# - validate-nightly-pr.yml (PR file check) +NIGHTLY_ALLOWED_FILES=( + "azure_linux_images.db" + "docs/daily_recommendations.md" + "docs/daily_recommendations.json" +) diff --git a/.github/workflows/nightly-scan.yml b/.github/workflows/nightly-scan.yml index a985776..1e1e1cb 100644 --- a/.github/workflows/nightly-scan.yml +++ b/.github/workflows/nightly-scan.yml @@ -20,21 +20,37 @@ jobs: timeout-minutes: 360 permissions: contents: write + pull-requests: write steps: - - name: ๐Ÿ”‘ Generate App Token - id: app-token - uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 - with: - app-id: ${{ secrets.APP_ID }} - private-key: ${{ secrets.APP_PRIVATE_KEY }} + # TODO: Replace GITHUB_TOKEN with a GitHub App token once available. + # See https://github.com/microsoft/sbi/issues/6 - name: โคต๏ธ Checkout (with LFS) uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: - token: ${{ steps.app-token.outputs.token }} lfs: true fetch-depth: 0 + - name: โœ… Require default branch for manual runs + if: github.event_name == 'workflow_dispatch' + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + run: | + if [ "${GITHUB_REF}" != "refs/heads/${DEFAULT_BRANCH}" ]; then + echo "::error::Run workflow_dispatch from ${DEFAULT_BRANCH} only." + exit 1 + fi + + - name: ๐Ÿ”„ Fetch default branch explicitly + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + run: git fetch --no-tags origin "${DEFAULT_BRANCH}:refs/remotes/origin/${DEFAULT_BRANCH}" + + - name: ๐ŸŒฟ Prepare nightly results branch + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + run: git checkout -B nightly-scan-results "origin/${DEFAULT_BRANCH}" + - name: ๐Ÿšง Setup Go uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: @@ -85,19 +101,110 @@ jobs: ls -lh azure_linux_images.db || true sqlite3 azure_linux_images.db 'SELECT COUNT(*) as images FROM images;' || true - - name: ๐Ÿ“ Commit & push updates + - name: ๐Ÿ“ Create PR with scan results if: success() + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + GH_TOKEN: ${{ github.token }} run: | - git config user.name 'sbi-nightly-bot[bot]' - git config user.email '${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com' - git add -f azure_linux_images.db docs/daily_recommendations.md docs/daily_recommendations.json || true - if [ -n "$(git diff --cached --name-only)" ]; then - CHANGED_FILES=$(git diff --cached --name-only | tr '\n' ' ') - echo "Changed files: $CHANGED_FILES" - git commit -m "chore: update nightly scan results [skip ci]" - git push - else + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + + BRANCH="nightly-scan-results" + PR_TITLE="chore: update nightly scan results" + + # Source the shared allowlist from the trusted triggering commit + # so earlier tooling cannot taint the file list. + git restore --source='${{ github.sha }}' -- .github/nightly-allowed-files.sh + source .github/nightly-allowed-files.sh + + is_allowed_file() { + local candidate="$1" + local allowed_file + + for allowed_file in "${NIGHTLY_ALLOWED_FILES[@]}"; do + if [ "$candidate" = "$allowed_file" ]; then + return 0 + fi + done + + return 1 + } + + # Validate expected scan artifacts exist + MISSING=() + for f in "${NIGHTLY_ALLOWED_FILES[@]}"; do + [ -f "$f" ] || MISSING+=("$f") + done + if [ ${#MISSING[@]} -gt 0 ]; then + echo "::error::Missing expected scan artifacts:" + printf ' - %s\n' "${MISSING[@]}" + exit 1 + fi + + # Validate the full working tree so unexpected files cannot be + # hidden by selectively staging only the allowlisted outputs. + UNEXPECTED=() + while IFS= read -r -d '' file; do + if ! is_allowed_file "$file"; then + UNEXPECTED+=("$file") + fi + done < <(git diff --name-only -z) + while IFS= read -r -d '' file; do + if ! is_allowed_file "$file"; then + UNEXPECTED+=("$file") + fi + done < <(git ls-files --others --exclude-standard -z) + if [ ${#UNEXPECTED[@]} -gt 0 ]; then + echo "::error::Nightly scan produced unexpected file changes:" + printf ' - %s\n' "${UNEXPECTED[@]}" + exit 1 + fi + + git add -f "${NIGHTLY_ALLOWED_FILES[@]}" + if [ -z "$(git diff --cached --name-only)" ]; then echo 'No changes to commit.' + exit 0 + fi + + # Inline path validation: ensure only expected files are staged + while IFS= read -r -d '' file; do + if ! is_allowed_file "$file"; then + echo "::error::Unexpected file staged: $file" + exit 1 + fi + done < <(git diff --cached --name-only -z) + + CHANGED_FILES=$(git diff --cached --name-only | tr '\n' ' ') + echo "Changed files: $CHANGED_FILES" + git commit -m "chore: update nightly scan results" + git push --force-with-lease origin "$BRANCH" + + printf '%s\n' \ + "Automated nightly scan results update." \ + "" \ + "This PR was created by the nightly scan workflow and contains updated:" \ + '- `azure_linux_images.db` โ€” scan database' \ + '- `docs/daily_recommendations.md` โ€” markdown report' \ + '- `docs/daily_recommendations.json` โ€” JSON report' \ + "" \ + "> **Note:** PRs created by GITHUB_TOKEN do not trigger other workflows (GitHub limitation)." \ + "> If required status checks are configured, you may need to manually re-push or close/reopen" \ + "> the PR to trigger them. See https://github.com/microsoft/sbi/issues/6 for details." \ + > /tmp/nightly-pr-body.md + + # Create PR if one doesn't already exist (scoped to this repo, not forks) + EXISTING_PR=$(gh pr list --head "${{ github.repository_owner }}:$BRANCH" --base "$DEFAULT_BRANCH" --state open --json number --jq '.[0].number // empty') + if [ -z "$EXISTING_PR" ]; then + gh pr create \ + --base "$DEFAULT_BRANCH" \ + --head "$BRANCH" \ + --title "$PR_TITLE" \ + --body-file /tmp/nightly-pr-body.md + else + gh pr edit "$EXISTING_PR" \ + --title "$PR_TITLE" \ + --body-file /tmp/nightly-pr-body.md fi - name: ๐Ÿ“ค Upload database artifact diff --git a/.github/workflows/validate-nightly-pr.yml b/.github/workflows/validate-nightly-pr.yml new file mode 100644 index 0000000..7543dd5 --- /dev/null +++ b/.github/workflows/validate-nightly-pr.yml @@ -0,0 +1,71 @@ +# yaml-language-server: $schema=https://www.schemastore.org/github-workflow.json +--- +name: Validate Nightly PR + +on: + pull_request: + +permissions: {} + +jobs: + validate-nightly-pr: + name: ๐Ÿ”’ Validate Nightly PR Files + if: >- + github.event.pull_request.base.ref == github.event.repository.default_branch && + github.head_ref == 'nightly-scan-results' && + github.event.pull_request.head.repo.full_name == github.repository && + github.event.pull_request.user.login == 'github-actions[bot]' + runs-on: ubuntu-24.04 + timeout-minutes: 5 + permissions: + contents: read + pull-requests: read + steps: + - name: โคต๏ธ Checkout allowlist from base branch + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ github.event.pull_request.base.sha }} + sparse-checkout: .github/nightly-allowed-files.sh + sparse-checkout-cone-mode: false + persist-credentials: false + + - name: ๐Ÿ“‹ Check modified files + env: + GH_TOKEN: ${{ github.token }} + run: | + source .github/nightly-allowed-files.sh + + # Get the list of changed files in this PR + CHANGED_FILES=$(gh api \ + "repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files" \ + --paginate --jq '.[].filename') + + UNEXPECTED=() + while IFS= read -r file; do + [ -z "$file" ] && continue + allowed=false + for af in "${NIGHTLY_ALLOWED_FILES[@]}"; do + if [ "$file" = "$af" ]; then + allowed=true + break + fi + done + if [ "$allowed" = "false" ]; then + UNEXPECTED+=("$file") + fi + done <<< "$CHANGED_FILES" + + if [ ${#UNEXPECTED[@]} -gt 0 ]; then + echo "::error::Nightly scan PR contains unexpected files:" + printf ' - %s\n' "${UNEXPECTED[@]}" + echo "" + echo "Only the following files are allowed:" + printf ' - %s\n' "${NIGHTLY_ALLOWED_FILES[@]}" + exit 1 + fi + + echo "โœ… All changed files are in the allowed set:" + while IFS= read -r file; do + [ -z "$file" ] && continue + printf ' - %s\n' "$file" + done <<< "$CHANGED_FILES"