diff --git a/.github/workflows/add-to-project.yaml b/.github/workflows/add-to-project.yaml index 7d05ff54..2ba57b4d 100644 --- a/.github/workflows/add-to-project.yaml +++ b/.github/workflows/add-to-project.yaml @@ -43,13 +43,21 @@ jobs: if: github.event_name == 'issues' || github.event_name == 'pull_request' || github.event_name == 'workflow_call' name: Add issue/PR to a project runs-on: ubuntu-latest - permissions: write-all + permissions: + contents: read + pull-requests: read + issues: read steps: - uses: actions/create-github-app-token@v3 id: generate-token with: app-id: ${{ secrets.APP_ID }} private-key: ${{ secrets.APP_SECRET }} + permission-issues: write + permission-pull-requests: write + permission-members: read + permission-organization-projects: write + permission-administration: write - uses: actions/add-to-project@v1.0.2 id: add-project with: @@ -58,27 +66,177 @@ jobs: project-url: ${{ inputs.project-url != '' && inputs.project-url || secrets.PLATFORM_PROJECT_URL }} github-token: ${{ steps.generate-token.outputs.token }} labeled: ${{ inputs.labeled }} - - if: ${{ github.event_name == 'pull_request' && github.event.pull_request.draft == true && steps.add-project.outputs.itemId }} - uses: titoportas/update-project-fields@v0.1.0 + - if: ${{ github.event_name == 'pull_request' && steps.add-project.outputs.itemId }} + uses: actions/github-script@v7 with: - project-url: ${{ inputs.project-url != '' && inputs.project-url || secrets.PLATFORM_PROJECT_URL }} github-token: ${{ steps.generate-token.outputs.token }} - item-id: ${{ steps.add-project.outputs.itemId }} # Use the item-id output of the previous step - field-keys: Status - field-values: 🏗 In progress - - if: ${{ github.event_name == 'pull_request' && github.event.pull_request.draft == false && steps.add-project.outputs.itemId }} - uses: titoportas/update-project-fields@v0.1.0 - with: - project-url: ${{ inputs.project-url != '' && inputs.project-url || secrets.PLATFORM_PROJECT_URL }} - github-token: ${{ steps.generate-token.outputs.token }} - item-id: ${{ steps.add-project.outputs.itemId }} # Use the item-id output of the previous step - field-keys: Status - field-values: 🔖 Ready + script: | + const projectUrl = '${{ inputs.project-url != '' && inputs.project-url || secrets.PLATFORM_PROJECT_URL }}'; + const itemId = '${{ steps.add-project.outputs.itemId }}'; + const isDraft = ${{ github.event.pull_request.draft }}; + const statusValue = isDraft ? '🏗 In progress' : '🔖 Ready'; + + // Parse project URL, supports both: + // - https://github.com/orgs//projects/ + // - https://github.com/users//projects/ + const projectMatch = projectUrl.match(/github\.com\/(?:(orgs|users)\/)?([^/]+)\/projects\/(\d+)/i); + if (!projectMatch) { + core.setFailed(`Could not parse project URL: ${projectUrl}`); + return; + } + const projectOwnerType = (projectMatch[1] || '').toLowerCase(); + const projectOwnerLogin = projectMatch[2]; + const projectNumber = Number(projectMatch[3]); + const isUserProject = projectOwnerType === 'users'; + + // Get project fields + const fieldsQuery = isUserProject ? ` + query($number: Int!, $first: Int) { + user(login: "${projectOwnerLogin}") { + projectV2(number: $number) { + id + fields(first: $first) { + nodes { + ... on ProjectV2Field { + id + name + } + ... on ProjectV2SingleSelectField { + id + name + options { + id + name + } + } + } + } + } + } + } + ` : ` + query($number: Int!, $first: Int) { + organization(login: "${projectOwnerLogin}") { + projectV2(number: $number) { + id + fields(first: $first) { + nodes { + ... on ProjectV2Field { + id + name + } + ... on ProjectV2SingleSelectField { + id + name + options { + id + name + } + } + } + } + } + } + } + `; + + try { + const fieldsResult = await github.graphql(fieldsQuery, { + number: projectNumber, + first: 20 + }); + + const projectNode = isUserProject + ? fieldsResult.user?.projectV2 + : fieldsResult.organization?.projectV2; + + const statusField = projectNode?.fields?.nodes?.find( + f => f.name === 'Status' + ); + + if (!statusField) { + core.setFailed('Status field not found in project'); + return; + } + + const normalize = (value) => + String(value || '') + .replace(/^\s*[^A-Za-z0-9]+\s*/, '') + .trim() + .toLowerCase(); + + const statusOptions = statusField.options || []; + const isSingleSelect = Array.isArray(statusField.options); + const statusOption = + statusOptions.find((opt) => opt.name === statusValue) || + statusOptions.find((opt) => normalize(opt.name) === normalize(statusValue)); + + if (isSingleSelect && !statusOption) { + core.setFailed( + `Status option not found for "${statusValue}". Available options: ${statusOptions + .map((opt) => opt.name) + .join(', ')}` + ); + return; + } + + const projectId = projectNode?.id; + if (!projectId) { + core.setFailed('Could not resolve project id from project URL and owner'); + return; + } + + // Update field value using the correct value type + const valueFragment = statusOption + ? `singleSelectOptionId: "${statusOption.id}"` + : `text: "${statusValue.replace(/"/g, '\\"')}"`; + const updateMutation = ` + mutation { + updateProjectV2ItemFieldValue(input: { + projectId: "${projectId}" + itemId: "${itemId}" + fieldId: "${statusField.id}" + value: { + ${valueFragment} + } + }) { + projectV2Item { + id + } + } + } + `; + + await github.graphql(updateMutation); + console.log(`Updated Status to: ${statusValue}`); + } catch (error) { + core.setFailed(`Failed to update project field: ${error.message}`); + } - name: Assign PR to creator - if: ${{ github.event_name == 'pull_request' && github.event.pull_request.draft == false && steps.add-project.outputs.itemId }} - uses: thomaseizinger/assign-pr-creator-action@v1.0.0 + if: ${{ github.event_name == 'pull_request' && !github.event.pull_request.draft && steps.add-project.outputs.itemId }} + uses: actions/github-script@v7 with: - repo-token: ${{ secrets.GITHUB_TOKEN }} + github-token: ${{ steps.generate-token.outputs.token }} + script: | + // Check if PR already has assignees + const pr = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number + }); + + // Only assign PR creator if no assignees exist + if (!pr.data.assignees || pr.data.assignees.length === 0) { + await github.rest.issues.addAssignees({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + assignees: [context.payload.pull_request.user.login] + }); + console.log('Assigned PR creator as assignee'); + } else { + console.log('PR already has assignees, skipping assignment'); + } - name: Add permissions if: ${{ github.event_name == 'pull_request' && github.event.pull_request.draft == false && steps.add-project.outputs.itemId }} run: | @@ -98,11 +256,35 @@ jobs: REPO: ${{ github.event.repository.name }} TEAMS: ${{ inputs.reviewers-team != '' && inputs.reviewers-team || 'backend-devs,ops' }} - name: Add reviewers - if: ${{ github.event_name == 'pull_request' && github.event.pull_request.draft == false && steps.add-project.outputs.itemId }} - uses: rowi1de/auto-assign-review-teams@v1.1.3 + if: ${{ github.event_name == 'pull_request' && github.event.pull_request.draft == false && steps.add-project.outputs.itemId }} + uses: actions/github-script@v7 with: - repo-token: ${{ steps.generate-token.outputs.token }} - teams: ${{ inputs.reviewers-team != '' && inputs.reviewers-team || 'backend-devs,ops' }} # only works for GitHub Organisation/Teams - persons: ${{ inputs.reviewers-individuals }} # add individual persons here - include-draft: false # Draft PRs will be skipped (default: false) - skip-with-manual-reviewers: 1 # Skip this action, if the number of reviwers was already assigned (default: 0) + github-token: ${{ steps.generate-token.outputs.token }} + script: | + const teams = '${{ inputs.reviewers-team != '' && inputs.reviewers-team || 'backend-devs,ops' }}' + .split(',') + .map(t => t.trim()) + .filter(t => t); + + const persons = '${{ inputs.reviewers-individuals }}' + .split(',') + .map(p => p.trim()) + .filter(p => p); + + if (teams.length === 0 && persons.length === 0) { + console.log('No reviewers configured'); + return; + } + + try { + await github.rest.pulls.requestReviewers({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + reviewers: persons, + team_reviewers: teams + }); + console.log(`Requested reviewers: teams=[${teams}], persons=[${persons}]`); + } catch (error) { + core.warning(`Failed to request reviewers: ${error.message}`); + } diff --git a/.github/workflows/check-pr.yaml b/.github/workflows/check-pr.yaml index 52031a08..0b7722f8 100644 --- a/.github/workflows/check-pr.yaml +++ b/.github/workflows/check-pr.yaml @@ -40,9 +40,18 @@ jobs: pr-label-check: if: ${{ github.event.pull_request.draft == false }} runs-on: ubuntu-latest + permissions: + pull-requests: read steps: - - uses: docker://agilepathway/pull-request-label-checker:latest + - uses: actions/github-script@v7 with: - github_enterprise_graphql_url: https://api.github.com/graphql - one_of: ${{ inputs.labels !='' && inputs.labels || 'feature,bug,ci,refactor,security,documentation,dependencies,customer,skip-changelog' }} - repo_token: ${{ secrets.GITHUB_TOKEN }} + script: | + const labels = context.payload.pull_request.labels.map(l => l.name); + const required = '${{ inputs.labels != '' && inputs.labels || 'feature,bug,ci,refactor,security,documentation,dependencies,customer,skip-changelog' }}' + .split(',') + .map(s => s.trim()) + .filter(Boolean); + const hasLabel = required.some(label => labels.includes(label)); + if (!hasLabel) { + core.setFailed(`PR must have one of: ${required.join(', ')}`); + } diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index c0e8301c..45070fb8 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -62,16 +62,11 @@ on: type: string default: "" description: Use caching mechanism for a given language - submodules: - required: false - type: string - default: "" - description: True or Recursive to initialize git submodules repositories: required: false type: string default: "" - description: Comma or newline-separated list of repositories to grant access to during the docker build. + description: Comma or newline-separated list of additional repositories needed for Docker build (e.g., private dependencies) jobs: check: @@ -92,15 +87,15 @@ jobs: app-id: ${{ secrets.APP_ID }} private-key: ${{ secrets.APP_SECRET }} repositories: ${{ github.event.repository.name }}${{ inputs.repositories && format(',{0}', inputs.repositories) || '' }} + permission-contents: read - name: Checkout uses: actions/checkout@v6 with: - submodules: ${{ inputs.submodules }} - token: ${{ inputs.submodules != '' && steps.generate-token.outputs.token || github.token }} + token: ${{ steps.generate-token.outputs.token }} - name: Run pre-build env: - GH_TOKEN: ${{ steps.generate-token.outputs.token }} BUILD_SECRET: ${{ secrets.BUILD_SECRET }} + GH_TOKEN: ${{ steps.generate-token.outputs.token }} run: | if test -f "Makefile"; then make ci-pre-build @@ -175,8 +170,6 @@ jobs: labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max - secrets: | - "github_token=${{ steps.generate-token.outputs.token }}" build-args: | GIT_REV=${{fromJson(steps.meta.outputs.json).labels['org.opencontainers.image.revision']}} GIT_VERSION=${{fromJson(steps.meta.outputs.json).labels['org.opencontainers.image.version']}} @@ -192,7 +185,7 @@ jobs: run: | echo "tag=$(echo '${{ needs.build.outputs.tags }}' | head -n1)" >> "$GITHUB_OUTPUT" - name: Trivy vulnerability scanner - # trivy-action v0.35.0 (safe version) + # trivy-action v0.35.0 (safe version) - pinned to full commit SHA uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 with: scan-type: image diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 1ec3b604..65252169 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,6 +1,6 @@ # GitHub Workflows Release Notes -## 0.0.3-dev - 2026-03-24 +## 0.0.3-dev - 2026-03-27 ### Features @@ -80,6 +80,7 @@ ### Security +- Workflow hardening: check-pr / docker / add-to-project (PR #269 by @chicco785) - Fix trivy action to a secure version (PR #266 by @chicco785) - add-to-project wf: add job to sync priority in projects with labels for Vanta (PR #229 by @chicco785) @@ -87,6 +88,8 @@ ### Dependencies +- Bump dawidd6/action-download-artifact from 18 to 19 (PR #265 by + @dependabot[bot]) - Bump actions/create-github-app-token from 2 to 3 (PR #261 by @dependabot[bot]) - Bump dorny/paths-filter from 3 to 4 (PR #259 by @dependabot[bot]) - Bump docker/login-action from 3 to 4 (PR #262 by @dependabot[bot])