{"id":1846,"date":"2025-03-19T16:36:04","date_gmt":"2025-03-19T16:36:04","guid":{"rendered":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/2025\/03\/19\/issueops-automate-ci-cd-and-more-with-github-issues-and-actions\/"},"modified":"2025-03-19T16:36:04","modified_gmt":"2025-03-19T16:36:04","slug":"issueops-automate-ci-cd-and-more-with-github-issues-and-actions","status":"publish","type":"post","link":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/2025\/03\/19\/issueops-automate-ci-cd-and-more-with-github-issues-and-actions\/","title":{"rendered":"IssueOps: Automate CI\/CD (and more!) with GitHub Issues and Actions"},"content":{"rendered":"<p>Software development is filled with repetitive tasks\u2014managing issues, handling approvals, triggering CI\/CD workflows, and more. But what if you could automate these types of tasks directly within GitHub Issues? That\u2019s the promise of <strong>IssueOps<\/strong>, a methodology that turns GitHub Issues into a command center for automation.<\/p>\n<p>Whether you\u2019re a solo developer or part of an engineering team, IssueOps helps you streamline operations without ever leaving your repository.<\/p>\n<p>In this article, I\u2019ll explore the concept of IssueOps using state-machine terminology and strategies to help you work more efficiently on GitHub. After all, who doesn\u2019t love automation?<\/p>\n<h2>What is IssueOps?<a href=\"https:\/\/github.blog\/engineering\/issueops-automate-ci-cd-and-more-with-github-issues-and-actions\/#what-is-issueops\" class=\"heading-link pl-2 text-italic text-bold\"><\/a><\/h2>\n<p>IssueOps is the practice of using GitHub Issues, GitHub Actions, and pull requests (PR) as an interface for automating workflows. Instead of switching between tools or manually triggering actions, you can use issue comments, labels, and state changes to kick off CI\/CD pipelines, assign tasks, and even deploy applications.<\/p>\n<p>Much like the various other <em>*Ops<\/em> paradigms (<a href=\"https:\/\/github.blog\/engineering\/infrastructure\/using-chatops-to-help-actions-on-call-engineers\/\">ChatOps<\/a>, ClickOps, and so on), IssueOps is a collection of tools, workflows, and concepts that, when applied to <a href=\"https:\/\/github.com\/features\/issues\">GitHub Issues<\/a>, can automate mundane, repetitive tasks. The flexibility and power of issues, along with their relationship to pull requests, create a near limitless number of possibilities, such as managing approvals and deployments. All of this can really help to simplify your workflows on GitHub. I\u2019m speaking from personal experience here.<\/p>\n<p>It\u2019s important to note that IssueOps isn\u2019t just a DevOps thing! Where DevOps offers a methodology to bring developers and operations into closer alignment, IssueOps is a workflow automation practice centered around GitHub Issues. IssueOps lets you run anything from <a href=\"https:\/\/github.blog\/engineering\/engineering-principles\/enabling-branch-deployments-through-issueops-with-github-actions\/\">complex CI\/CD pipelines<\/a> to a <a href=\"https:\/\/github.com\/issue-ops\/bear-creek-honey-farm\">bed and breakfast reservation system<\/a>. If you can interact with it via an API, there\u2019s a good chance you can build it with IssueOps!<\/p>\n<h2>So, why use IssueOps?<a href=\"https:\/\/github.blog\/engineering\/issueops-automate-ci-cd-and-more-with-github-issues-and-actions\/#so-why-use-issueops\" class=\"heading-link pl-2 text-italic text-bold\"><\/a><\/h2>\n<p>There are lots of benefits to utilizing IssueOps. Here\u2019s how it\u2019s useful in practice:<\/p>\n<p><strong>It\u2019s event-driven, so you can automate the boring stuff:<\/strong> IssueOps lets you automate workflows directly from GitHub Issues and pull requests, turning everyday interactions\u2014from kicking off a CI\/CD pipeline and managing approvals to updating project boards\u2014into powerful triggers for GitHub Actions.<\/p>\n<p><strong>It\u2019s customizable, so you can tailor workflows to your needs:<\/strong> No two teams work the same way, and IssueOps is flexible enough to adapt. Whether you\u2019re automating bug triage or triggering deployments, you can customize workflows based on event type and data provided.<\/p>\n<p><strong>It\u2019s transparent, so you can keep a record:<\/strong> All actions taken on an issue are logged in its timeline, creating an easy-to-follow record of what happened and when.<\/p>\n<p><strong>It\u2019s immutable, so you do an audit whenever you need:<\/strong> Because IssueOps uses GitHub Issues and pull requests as a source of truth, every action leaves a record. No more chasing approvals in Slack or manually triggering workflows: IssueOps keeps everything structured, automated, and auditable right inside GitHub.<\/p>\n<p class=\"h5-mktg gh-aside-title\">Our quickstart guide to IssueOps<\/p>\n<p><strong>Step 1: Define your triggers<\/strong><br \/>\nIdentify the actions that should kick off your workflows\u2014like opening an issue, adding a label, or merging a pull request. These events can serve as triggers for GitHub Actions.<br \/>\n<strong>Step 2: Configure GitHub Actions<\/strong><\/p>\n<p>Use GitHub Actions to define what happens when an event occurs. For example, if an issue is labeled deploy, you could trigger a deployment script. YAML never looked so good.<\/p>\n<p><strong>Step 3: Test and iterate<\/strong><\/p>\n<p>Like any good automation, IssueOps workflows should be tested and refined. Start small, see what works, and expand from there.<\/p>\n<p><strong>Let\u2019s go<\/strong>: Learn more <a href=\"https:\/\/github.com\/issue-ops\/docs\">in our repository<\/a>.<\/p>\n<h2>Defining IssueOps workflows and how they\u2019re like finite-state machines<a href=\"https:\/\/github.blog\/engineering\/issueops-automate-ci-cd-and-more-with-github-issues-and-actions\/#defining-issueops-workflows-and-how-theyre-like-finite-state-machines\" class=\"heading-link pl-2 text-italic text-bold\"><\/a><\/h2>\n<p>Most IssueOps workflows follow the same basic pattern:<\/p>\n<p>A user opens an issue and provides information about a request<br \/>\nThe issue is validated to ensure it contains the required information<br \/>\nThe issue is submitted for processing<br \/>\nApproval is requested from an authorized user or team<br \/>\nThe request is processed and the issue is closed<\/p>\n<p>Suppose you\u2019re an administrator of an organization and want to reduce the overhead of managing team members. In this instance, you could use IssueOps to build an automated membership request and approval process. Within a workflow like this, you\u2019d have several core steps:<\/p>\n<p>A user creates a request to be added to a team<br \/>\nThe request is validated<br \/>\nThe request is submitted for approval<br \/>\nAn administrator approves or denies this request<br \/>\nThe request is processed<\/p>\n<p>If <em>approved<\/em>, the user is added to the team<br \/>\nIf <em>denied<\/em>, the user is not added to the team  <\/p>\n<p>The user is notified of the outcome<\/p>\n<p>When designing your own IssueOps workflows, it can be very helpful to think of them as a <a href=\"https:\/\/web.stanford.edu\/class\/cs123\/lectures\/CS123_lec07_Finite_State_Machine.pdf\">finite-state machine<\/a>: a model for how objects move through a series of states in response to external events. Depending on certain rules defined within the state machine, a number of different actions can take place in response to state changes. If this is a little too complex, you can also think of it like a flow chart.<\/p>\n<p>To apply this comparison to IssueOps, an issue is the <em>object<\/em> that is processed by a state machine. It changes <em>state<\/em> in response to <em>events<\/em>. As the object changes state, certain <em>actions<\/em> may be performed as part of a <em>transition<\/em>, provided any required conditions (<em>guards<\/em>) are met. Once an <em>end state<\/em> is reached, the issue can be closed.<\/p>\n<p>This breaks down into a few key concepts:<\/p>\n<p><strong>State<\/strong>: A point in an object\u2019s lifecycle that satisfies certain condition(s).<br \/>\n<strong>Event<\/strong>: An external occurrence that triggers a state change.<br \/>\n<strong>Transition<\/strong>: A link between two states that, when traversed by an object, will cause certain action(s) to be performed.<br \/>\n<strong>Action<\/strong>: An atomic task that is performed when a transition is taken.<br \/>\n<strong>Guard<\/strong>: A condition that is evaluated when a trigger event occurs. A transition is taken only if all associated guard condition(s) are met.<\/p>\n<p>Here\u2019s a simple state diagram for the example I discussed above.<\/p>\n\n<p>Now, let\u2019s dive into the state machine in more detail!<\/p>\n<h2>Key concepts behind state machines<a href=\"https:\/\/github.blog\/engineering\/issueops-automate-ci-cd-and-more-with-github-issues-and-actions\/#key-concepts-behind-state-machines\" class=\"heading-link pl-2 text-italic text-bold\"><\/a><\/h2>\n<p>The benefit of breaking your workflow down into these components is that you can look for edge cases, enforce conditions, and create a robust, reliable result.<\/p>\n<h3><strong>States<\/strong><a href=\"https:\/\/github.blog\/engineering\/issueops-automate-ci-cd-and-more-with-github-issues-and-actions\/#states\" class=\"heading-link pl-2 text-italic text-bold\"><\/a><\/h3>\n<p>Within a state machine, a <em>state<\/em> defines the current status of an object. As the object transitions through the state machine, it will change states in response to external events. When building IssueOps workflows, common states for issues include opened, submitted, approved, denied, and closed.<\/p>\n<p>These should suffice as the core states to consider when building our workflows in our team membership example above.<\/p>\n<h3><strong>Events<\/strong><a href=\"https:\/\/github.blog\/engineering\/issueops-automate-ci-cd-and-more-with-github-issues-and-actions\/#events\" class=\"heading-link pl-2 text-italic text-bold\"><\/a><\/h3>\n<p>In a state machine, an <em>event<\/em> can be any form of interaction with the object and its current state. When building your own IssueOps, you should consider events from both the user and GitHub points of view.<\/p>\n<p>In our team membership request example, there are several events that can trigger a change in state. The request can be created, submitted, approved, denied, or processed.<\/p>\n<p>In this example, a user interacting with an issue\u2014such as adding labels, commenting, or updating milestones\u2014can also change its state. In GitHub Actions, there are many events that can trigger your workflows (see <a href=\"https:\/\/docs.github.com\/en\/actions\/writing-workflows\/choosing-when-your-workflow-runs\/events-that-trigger-workflows\">events that trigger workflows<\/a>).<\/p>\n<p>Here are a few interactions, or events, that would affect our example IssueOps workflow when it comes to managing team members: <\/p>\n<div class=\"content-table-wrap\">\n<p><strong>Request<\/strong><br \/>\n<strong>Event<\/strong><br \/>\n<strong>State<\/strong><\/p>\n<p>Request is created<br \/>\nissues<br \/>\nopened<\/p>\n<p>Request is approved<br \/>\nissue_comment<br \/>\ncreated<\/p>\n<p>Request is denied<br \/>\nissue_comment<br \/>\ncreated<\/p>\n<\/div>\n<p>As you can see, the same GitHub workflow trigger can apply to multiple events in our state machine. Because of this, validation is key. Within your workflows, you should check both the type of event and the information provided by the user. In this case, we can conditionally trigger different workflow steps based on the content of the issue_comment event.<\/p>\n<p>jobs:<br \/>\n  approve:<br \/>\n    name: Process Approval<br \/>\n    runs-on: ubuntu-latest<\/p>\n<p>    if: ${{ startsWith(github.event.comment.body, &#8216;.approve&#8217;) }}<\/p>\n<p>    # &#8230;<\/p>\n<p>  deny:<br \/>\n    name: Process Denial<br \/>\n    runs-on: ubuntu-latest<\/p>\n<p>    if: ${{ startsWith(github.event.comment.body, &#8216;.deny&#8217;) }}<\/p>\n<p>    # &#8230;<\/p>\n<h3>Transitions<a href=\"https:\/\/github.blog\/engineering\/issueops-automate-ci-cd-and-more-with-github-issues-and-actions\/#transitions\" class=\"heading-link pl-2 text-italic text-bold\"><\/a><\/h3>\n<p>A <em>transition<\/em> is simply the change from one state to another. In our example, for instance, a transition occurs when someone opens an issue. When a request meets certain conditions, or guards, the change in state can take place. When the transition occurs, some actions or processing may take place, as well.<\/p>\n<p>With our example workflow, you can think of the transitions themselves as the lines connecting different nodes in the state diagram. Or the lines connecting boxes in a flow chart.<\/p>\n<h3>Guards<a href=\"https:\/\/github.blog\/engineering\/issueops-automate-ci-cd-and-more-with-github-issues-and-actions\/#guards\" class=\"heading-link pl-2 text-italic text-bold\"><\/a><\/h3>\n<p><em>Guards<\/em> are conditions that must be verified before an event can trigger a transition to a different state. In our case, we know the following guards must be in place:<\/p>\n<p>A request should not transition to an Approved state unless an administrator comments .approve on the issue.<br \/>\nA request should not transition to a Denied state unless an administrator comments .deny on the issue.<\/p>\n<p>What about after the request is approved and the user is added to the team? This is referred to as an <em>unguarded transition<\/em>. There are no conditions that must be met, so the transition happens immediately!<\/p>\n<h3><strong>Actions<\/strong><a href=\"https:\/\/github.blog\/engineering\/issueops-automate-ci-cd-and-more-with-github-issues-and-actions\/#actions\" class=\"heading-link pl-2 text-italic text-bold\"><\/a><\/h3>\n<p>Lastly, <em>actions<\/em> are specific tasks that are performed during a transition. They may affect the object itself, but this is not a requirement in our state machine. In our example, the following actions may take place at different times:<\/p>\n<p>Administrators are notified that a request has been submitted<br \/>\nThe user is added to the requested team<br \/>\nThe user is notified of the outcome<\/p>\n<h2>A real-world example: Building a team membership workflow with IssueOps<a href=\"https:\/\/github.blog\/engineering\/issueops-automate-ci-cd-and-more-with-github-issues-and-actions\/#a-real-world-example-building-a-team-membership-workflow-with-issueops\" class=\"heading-link pl-2 text-italic text-bold\"><\/a><\/h2>\n<p>Now that all of the explanation is out of the way, let\u2019s dive into building our example! For reference, we\u2019ll focus on the GitHub Actions workflows involved in building this automation. There are some additional repository and permissions settings involved that are discussed in more detail <a href=\"https:\/\/issue-ops.github.io\/docs\/setup\">in these IssueOps docs<\/a>.<\/p>\n<h3>Step 1: Issue form template<a href=\"https:\/\/github.blog\/engineering\/issueops-automate-ci-cd-and-more-with-github-issues-and-actions\/#step-1-issue-form-template\" class=\"heading-link pl-2 text-italic text-bold\"><\/a><\/h3>\n<p><a href=\"https:\/\/docs.github.com\/en\/communities\/using-templates-to-encourage-useful-issues-and-pull-requests\/syntax-for-issue-forms\">GitHub issue forms<\/a> let you create standardized, formatted issues based on a set of form fields. Combined with the <a href=\"https:\/\/github.com\/issue-ops\/parser\">issue-ops\/parser<\/a> action, you can get reliable, machine-readable JSON from issue body Markdown. For our example, we are going to create a simple form that accepts a single input: the team where we want to add the user.<\/p>\n<p>name: Team Membership Request<br \/>\ndescription: Submit a new membership request<br \/>\ntitle: New Team Membership Request<br \/>\nlabels:<br \/>\n  &#8211; team-membership<br \/>\nbody:<br \/>\n  &#8211; type: input<br \/>\n    id: team<br \/>\n    attributes:<br \/>\n      label: Team Name<br \/>\n      description: The team name you would like to join<br \/>\n      placeholder: my-team<br \/>\n    validations:<br \/>\n      required: true<\/p>\n<p>When issues are created using this form, they will be parsed into JSON, which can then be passed to the rest of the IssueOps workflow.<\/p>\n<p>{<br \/>\n  &#8220;team&#8221;: &#8220;my-team&#8221;<br \/>\n}<\/p>\n<h3>Step 2: Issue validation<a href=\"https:\/\/github.blog\/engineering\/issueops-automate-ci-cd-and-more-with-github-issues-and-actions\/#step-2-issue-validation\" class=\"heading-link pl-2 text-italic text-bold\"><\/a><\/h3>\n<p>With a machine-readable issue body, we can run additional validation checks to ensure the information provided follows any rules we might have in place. For example, we can\u2019t automatically add a user to a team if the team doesn\u2019t exist yet! That is where the <a href=\"https:\/\/github.com\/issue-ops\/validator\">issue-ops\/validator<\/a> action comes into play. Using an issue form template and a custom validation script, we can confirm the existence of the team ahead of time.<\/p>\n<p>module.exports = async (field) =&gt; {<br \/>\n  const { Octokit } = require(&#8216;@octokit\/rest&#8217;)<br \/>\n  const core = require(&#8216;@actions\/core&#8217;)<\/p>\n<p>  const github = new Octokit({<br \/>\n    auth: core.getInput(&#8216;github-token&#8217;, { required: true })<br \/>\n  })<\/p>\n<p>  try {<br \/>\n    \/\/ Check if the team exists<br \/>\n    core.info(`Checking if team &#8216;${field}&#8217; exists`)<\/p>\n<p>    await github.rest.teams.getByName({<br \/>\n      org: process.env.GITHUB_REPOSITORY_OWNER ?? &#8221;,<br \/>\n      team_slug: field<br \/>\n    })<\/p>\n<p>    core.info(`Team &#8216;${field}&#8217; exists`)<br \/>\n    return &#8216;success&#8217;<br \/>\n  } catch (error) {<br \/>\n    if (error.status === 404) {<br \/>\n      \/\/ If the team does not exist, return an error message<br \/>\n      core.error(`Team &#8216;${field}&#8217; does not exist`)<br \/>\n      return `Team &#8216;${field}&#8217; does not exist`<br \/>\n    } else {<br \/>\n      \/\/ Otherwise, something else went wrong&#8230;<br \/>\n      throw error<br \/>\n    }<br \/>\n  }<br \/>\n}<\/p>\n<p>When included in our IssueOps workflow, this adds any validation error(s) to the comment on the issue.<\/p>\n<h3>Step 3: Issue workflows<a href=\"https:\/\/github.blog\/engineering\/issueops-automate-ci-cd-and-more-with-github-issues-and-actions\/#step-3-issue-workflows\" class=\"heading-link pl-2 text-italic text-bold\"><\/a><\/h3>\n<p>The main \u201centrypoint\u201d of this workflow occurs when a user creates or edits their team membership request issue. This workflow should focus heavily on validating any user inputs! For example, what should happen if the user inputs a team that does not exist?<\/p>\n<p>In our state machine, this workflow is responsible for handling everything up to the <em>opened<\/em> state. Any time an issue is created, edited, or updated, it will re-run validation to ensure the request is ready to be processed. In this case, an additional guard condition is introduced. Before the request can be submitted, the user must comment with .submit after validation has passed.<\/p>\n<p>name: Process Issue Open\/Edit<\/p>\n<p>on:<br \/>\n  issues:<br \/>\n    types:<br \/>\n      &#8211; opened<br \/>\n      &#8211; edited<br \/>\n      &#8211; reopened<\/p>\n<p>permissions:<br \/>\n  contents: read<br \/>\n  id-token: write<br \/>\n  issues: write<\/p>\n<p>jobs:<br \/>\n  validate:<br \/>\n    name: Validate Request<br \/>\n    runs-on: ubuntu-latest<\/p>\n<p>    # This job should only be run on issues with the `team-membership` label.<br \/>\n    if: ${{ contains(github.event.issue.labels.*.name, &#8216;team-membership&#8217;) }}<\/p>\n<p>    steps:<br \/>\n      # This is required to ensure the issue form template and any validation<br \/>\n      # scripts are included in the workspace.<br \/>\n      &#8211; name: Checkout<br \/>\n        id: checkout<br \/>\n        uses: actions\/checkout@v4<\/p>\n<p>      # Since this workflow includes custom validation scripts, we need to<br \/>\n      # install Node.js and any dependencies.<br \/>\n      &#8211; name: Setup Node.js<br \/>\n        id: setup-node<br \/>\n        uses: actions\/setup-node@v4<\/p>\n<p>      # Install dependencies from `package.json`.<br \/>\n      &#8211; name: Install Dependencies<br \/>\n        id: install<br \/>\n        run: npm install<\/p>\n<p>      # GitHub App authentication is required if you want to interact with any<br \/>\n      # resources outside the scope of the repository this workflow runs in.<br \/>\n      &#8211; name: Get GitHub App Token<br \/>\n        id: token<br \/>\n        uses: actions\/create-github-app-token@v1<br \/>\n        with:<br \/>\n          app-id: ${{ vars.ISSUEOPS_APP_ID }}<br \/>\n          private-key: ${{ secrets.ISSUEOPS_APP_PRIVATE_KEY }}<br \/>\n          owner: ${{ github.repository_owner }}<\/p>\n<p>      # Remove any labels and start fresh. This is important because the<br \/>\n      # issue may have been closed and reopened.<br \/>\n      &#8211; name: Remove Labels<br \/>\n        id: remove-label<br \/>\n        uses: issue-ops\/labeler@v2<br \/>\n        with:<br \/>\n          action: remove<br \/>\n          github_token: ${{ steps.token.outputs.token }}<br \/>\n          labels: |<br \/>\n            validated<br \/>\n            approved<br \/>\n            denied<br \/>\n          issue_number: ${{ github.event.issue.number }}<br \/>\n          repository: ${{ github.repository }}<\/p>\n<p>      # Parse the issue body into machine-readable JSON, so that it can be<br \/>\n      # processed by the rest of the workflow.<br \/>\n      &#8211; name: Parse Issue Body<br \/>\n        id: parse<br \/>\n        uses: issue-ops\/parser@v4<br \/>\n        with:<br \/>\n          body: ${{ github.event.issue.body }}<br \/>\n          issue-form-template: team-membership.yml<br \/>\n          workspace: ${{ github.workspace }}<\/p>\n<p>      # Validate early and often! Validation should be run any time an issue is<br \/>\n      # interacted with, to ensure that any changes to the issue body are valid.<br \/>\n      &#8211; name: Validate Request<br \/>\n        id: validate<br \/>\n        uses: issue-ops\/validator@v3<br \/>\n        with:<br \/>\n          add-comment: true<br \/>\n          github-token: ${{ steps.token.outputs.token }}<br \/>\n          issue-form-template: team-membership.yml<br \/>\n          issue-number: ${{ github.event.issue.number }}<br \/>\n          parsed-issue-body: ${{ steps.parse.outputs.json }}<br \/>\n          workspace: ${{ github.workspace }}<\/p>\n<p>      # If validation passes, add the validated label to the issue.<br \/>\n      &#8211; if: ${{ steps.validate.outputs.result == &#8216;success&#8217; }}<br \/>\n        name: Add Validated Label<br \/>\n        id: add-label<br \/>\n        uses: issue-ops\/labeler@v2<br \/>\n        with:<br \/>\n          action: add<br \/>\n          github_token: ${{ steps.token.outputs.token }}<br \/>\n          labels: |<br \/>\n            validated<br \/>\n          issue_number: ${{ github.event.issue.number }}<br \/>\n          repository: ${{ github.repository }}<\/p>\n<p>      # The `issue-ops\/validator` action will automatically notify the user that<br \/>\n      # the request was validated. However, you can optionally add instruction<br \/>\n      # on what to do next.<br \/>\n      &#8211; if: ${{ steps.validate.outputs.result == &#8216;success&#8217; }}<br \/>\n        name: Notify User (Success)<br \/>\n        id: notify-success<br \/>\n        uses: peter-evans\/create-or-update-comment@v4<br \/>\n        with:<br \/>\n          issue-number: ${{ github.event.issue.number }}<br \/>\n          body: |<br \/>\n            Hello! Your request has been validated successfully!<\/p>\n<p>            Please comment with `.submit` to submit this request.<\/p>\n<h3>Step 4: Issue comment workflows<a href=\"https:\/\/github.blog\/engineering\/issueops-automate-ci-cd-and-more-with-github-issues-and-actions\/#step-4-issue-comment-workflows\" class=\"heading-link pl-2 text-italic text-bold\"><\/a><\/h3>\n<p>Once the issue is created, any further processing is triggered using issue comments\u2014and this can be done with one workflow. However, to make things a bit easier to follow, we\u2019ll break this into a few separate workflows.<\/p>\n<h4>Submit workflow<a href=\"https:\/\/github.blog\/engineering\/issueops-automate-ci-cd-and-more-with-github-issues-and-actions\/#submit-workflow\" class=\"heading-link pl-2 text-italic text-bold\"><\/a><\/h4>\n<p>The first workflow handles the user submitting the request. The main task it performs is validating the issue body against the form template to ensure it hasn\u2019t been modified.<\/p>\n<p>name: Process Submit Comment<\/p>\n<p>on:<br \/>\n  issue_comment:<br \/>\n    types:<br \/>\n      &#8211; created<\/p>\n<p>permissions:<br \/>\n  contents: read<br \/>\n  id-token: write<br \/>\n  issues: write<\/p>\n<p>jobs:<br \/>\n  submit:<br \/>\n    name: Submit Request<br \/>\n    runs-on: ubuntu-latest<\/p>\n<p>    # This job should only be run when the following conditions are true:<br \/>\n    #<br \/>\n    # &#8211; A user comments `.submit` on the issue.<br \/>\n    # &#8211; The issue has the `team-membership` label.<br \/>\n    # &#8211; The issue has the `validated` label.<br \/>\n    # &#8211; The issue does not have the `approved` or `denied` labels.<br \/>\n    # &#8211; The issue is open.<br \/>\n    if: |<br \/>\n      startsWith(github.event.comment.body, &#8216;.submit&#8217;) &amp;&amp;<br \/>\n      contains(github.event.issue.labels.*.name, &#8216;team-membership&#8217;) == true &amp;&amp;<br \/>\n      contains(github.event.issue.labels.*.name, &#8216;approved&#8217;) == false &amp;&amp;<br \/>\n      contains(github.event.issue.labels.*.name, &#8216;denied&#8217;) == false &amp;&amp;<br \/>\n      github.event.issue.state == &#8216;open&#8217;<\/p>\n<p>    steps:<br \/>\n      # First, we are going to re-run validation. This is important because<br \/>\n      # the issue body may have changed since the last time it was validated.<\/p>\n<p>      # This is required to ensure the issue form template and any validation<br \/>\n      # scripts are included in the workspace.<br \/>\n      &#8211; name: Checkout<br \/>\n        id: checkout<br \/>\n        uses: actions\/checkout@v4<\/p>\n<p>      # Since this workflow includes custom validation scripts, we need to<br \/>\n      # install Node.js and any dependencies.<br \/>\n      &#8211; name: Setup Node.js<br \/>\n        id: setup-node<br \/>\n        uses: actions\/setup-node@v4<\/p>\n<p>      # Install dependencies from `package.json`.<br \/>\n      &#8211; name: Install Dependencies<br \/>\n        id: install<br \/>\n        run: npm install<\/p>\n<p>      # GitHub App authentication is required if you want to interact with any<br \/>\n      # resources outside the scope of the repository this workflow runs in.<br \/>\n      &#8211; name: Get GitHub App Token<br \/>\n        id: token<br \/>\n        uses: actions\/create-github-app-token@v1<br \/>\n        with:<br \/>\n          app-id: ${{ vars.ISSUEOPS_APP_ID }}<br \/>\n          private-key: ${{ secrets.ISSUEOPS_APP_PRIVATE_KEY }}<br \/>\n          owner: ${{ github.repository_owner }}<\/p>\n<p>      # Remove the validated label. This will be re-added if validation passes.<br \/>\n      &#8211; name: Remove Validated Label<br \/>\n        id: remove-label<br \/>\n        uses: issue-ops\/labeler@v2<br \/>\n        with:<br \/>\n          action: remove<br \/>\n          github_token: ${{ steps.token.outputs.token }}<br \/>\n          labels: |<br \/>\n            validated<br \/>\n          issue_number: ${{ github.event.issue.number }}<br \/>\n          repository: ${{ github.repository }}<\/p>\n<p>      # Parse the issue body into machine-readable JSON, so that it can be<br \/>\n      # processed by the rest of the workflow.<br \/>\n      &#8211; name: Parse Issue Body<br \/>\n        id: parse<br \/>\n        uses: issue-ops\/parser@v4<br \/>\n        with:<br \/>\n          body: ${{ github.event.issue.body }}<br \/>\n          issue-form-template: team-membership.yml<br \/>\n          workspace: ${{ github.workspace }}<\/p>\n<p>      # Validate early and often! Validation should be run any time an issue is<br \/>\n      # interacted with, to ensure that any changes to the issue body are valid.<br \/>\n      &#8211; name: Validate Request<br \/>\n        id: validate<br \/>\n        uses: issue-ops\/validator@v3<br \/>\n        with:<br \/>\n          add-comment: false # Don&#8217;t add another validation comment.<br \/>\n          github-token: ${{ steps.token.outputs.token }}<br \/>\n          issue-form-template: team-membership.yml<br \/>\n          issue-number: ${{ github.event.issue.number }}<br \/>\n          parsed-issue-body: ${{ steps.parse.outputs.json }}<br \/>\n          workspace: ${{ github.workspace }}<\/p>\n<p>      # If validation passed, add the validated and submitted labels to the issue.<br \/>\n      &#8211; if: ${{ steps.validate.outputs.result == &#8216;success&#8217; }}<br \/>\n        name: Add Validated Label<br \/>\n        id: add-label<br \/>\n        uses: issue-ops\/labeler@v2<br \/>\n        with:<br \/>\n          action: add<br \/>\n          github_token: ${{ steps.token.outputs.token }}<br \/>\n          labels: |<br \/>\n            validated<br \/>\n            submitted<br \/>\n          issue_number: ${{ github.event.issue.number }}<br \/>\n          repository: ${{ github.repository }}<\/p>\n<p>      # If validation succeeded, alert the administrator team so they can<br \/>\n      # approve or deny the request.<br \/>\n      &#8211; if: ${{ steps.validate.outputs.result == &#8216;success&#8217; }}<br \/>\n        name: Notify Admin (Success)<br \/>\n        id: notify-success<br \/>\n        uses: peter-evans\/create-or-update-comment@v4<br \/>\n        with:<br \/>\n          issue-number: ${{ github.event.issue.number }}<br \/>\n          body: |<br \/>\n            \ud83d\udc4b @issue-ops\/admins! The request has been validated and is<br \/>\n            ready for your review. Please comment with `.approve` or `.deny`<br \/>\n            to approve or deny this request.<\/p>\n<h4>Deny workflow<a href=\"https:\/\/github.blog\/engineering\/issueops-automate-ci-cd-and-more-with-github-issues-and-actions\/#deny-workflow\" class=\"heading-link pl-2 text-italic text-bold\"><\/a><\/h4>\n<p>If the request is denied, the user should be notified and the issue should close.<\/p>\n<p>name: Process Denial Comment<\/p>\n<p>on:<br \/>\n  issue_comment:<br \/>\n    types:<br \/>\n      &#8211; created<\/p>\n<p>permissions:<br \/>\n  contents: read<br \/>\n  id-token: write<br \/>\n  issues: write<\/p>\n<p>jobs:<br \/>\n  submit:<br \/>\n    name: Deny Request<br \/>\n    runs-on: ubuntu-latest<\/p>\n<p>    # This job should only be run when the following conditions are true:<br \/>\n    #<br \/>\n    # &#8211; A user comments `.deny` on the issue.<br \/>\n    # &#8211; The issue has the `team-membership` label.<br \/>\n    # &#8211; The issue has the `validated` label.<br \/>\n    # &#8211; The issue has the `submitted` label.<br \/>\n    # &#8211; The issue does not have the `approved` or `denied` labels.<br \/>\n    # &#8211; The issue is open.<br \/>\n    if: |<br \/>\n      startsWith(github.event.comment.body, &#8216;.deny&#8217;) &amp;&amp;<br \/>\n      contains(github.event.issue.labels.*.name, &#8216;team-membership&#8217;) == true &amp;&amp;<br \/>\n      contains(github.event.issue.labels.*.name, &#8216;submitted&#8217;) == true &amp;&amp;<br \/>\n      contains(github.event.issue.labels.*.name, &#8216;validated&#8217;) == true &amp;&amp;<br \/>\n      contains(github.event.issue.labels.*.name, &#8216;approved&#8217;) == false &amp;&amp;<br \/>\n      contains(github.event.issue.labels.*.name, &#8216;denied&#8217;) == false &amp;&amp;<br \/>\n      github.event.issue.state == &#8216;open&#8217;<\/p>\n<p>    steps:<br \/>\n      # This time, we do not need to re-run validation because the request is<br \/>\n      # being denied. It can just be closed.<\/p>\n<p>      # However, we do need to confirm that the user who commented `.deny` is<br \/>\n      # a member of the administrator team.<br \/>\n      # GitHub App authentication is required if you want to interact with any<br \/>\n      # resources outside the scope of the repository this workflow runs in.<br \/>\n      &#8211; name: Get GitHub App Token<br \/>\n        id: token<br \/>\n        uses: actions\/create-github-app-token@v1<br \/>\n        with:<br \/>\n          app-id: ${{ vars.ISSUEOPS_APP_ID }}<br \/>\n          private-key: ${{ secrets.ISSUEOPS_APP_PRIVATE_KEY }}<br \/>\n          owner: ${{ github.repository_owner }}<\/p>\n<p>      # Check if the user who commented `.deny` is a member of the<br \/>\n      # administrator team.<br \/>\n      &#8211; name: Check Admin Membership<br \/>\n        id: check-admin<br \/>\n        uses: actions\/github-script@v7<br \/>\n        with:<br \/>\n          github-token: ${{ steps.token.outputs.token }}<br \/>\n          script: |<br \/>\n            try {<br \/>\n              await github.rest.teams.getMembershipForUserInOrg({<br \/>\n                org: context.repo.owner,<br \/>\n                team_slug: &#8216;admins&#8217;,<br \/>\n                username: context.actor,<br \/>\n              })<br \/>\n              core.setOutput(&#8216;member&#8217;, &#8216;true&#8217;)<br \/>\n            } catch (error) {<br \/>\n              if (error.status === 404) {<br \/>\n                core.setOutput(&#8216;member&#8217;, &#8216;false&#8217;)<br \/>\n              }<br \/>\n              throw error<br \/>\n            }<\/p>\n<p>      # If the user is not a member of the administrator team, exit the<br \/>\n      # workflow.<br \/>\n      &#8211; if: ${{ steps.check-admin.outputs.member == &#8216;false&#8217; }}<br \/>\n        name: Exit<br \/>\n        run: exit 0<\/p>\n<p>      # If the user is a member of the administrator team, add the denied label.<br \/>\n      &#8211; name: Add Denied Label<br \/>\n        id: add-label<br \/>\n        uses: issue-ops\/labeler@v2<br \/>\n        with:<br \/>\n          action: add<br \/>\n          github_token: ${{ steps.token.outputs.token }}<br \/>\n          labels: |<br \/>\n            denied<br \/>\n          issue_number: ${{ github.event.issue.number }}<br \/>\n          repository: ${{ github.repository }}<\/p>\n<p>      # Notify the user that the request was denied.<br \/>\n      &#8211; name: Notify User<br \/>\n        id: notify<br \/>\n        uses: peter-evans\/create-or-update-comment@v4<br \/>\n        with:<br \/>\n          issue-number: ${{ github.event.issue.number }}<br \/>\n          body: |<br \/>\n            This request has been denied and will be closed.<\/p>\n<p>      # Close the issue as not planned.<br \/>\n      &#8211; name: Close Issue<br \/>\n        id: close<br \/>\n        uses: actions\/github-script@v7<br \/>\n        with:<br \/>\n          script: |<br \/>\n            await github.rest.issues.update({<br \/>\n              issue_number: ${{ github.event.issue.number }},<br \/>\n              owner: context.repo.owner,<br \/>\n              repo: context.repo.repo,<br \/>\n              state: &#8216;closed&#8217;,<br \/>\n              state_reason: &#8216;not_planned&#8217;<br \/>\n            })<\/p>\n<h4>Approve workflow<a href=\"https:\/\/github.blog\/engineering\/issueops-automate-ci-cd-and-more-with-github-issues-and-actions\/#approve-workflow\" class=\"heading-link pl-2 text-italic text-bold\"><\/a><\/h4>\n<p>Finally, we need to handle request approval. In this case, we need to add the user to the team, notify them, and close the issue.<\/p>\n<p>name: Process Approval Comment<\/p>\n<p>on:<br \/>\n  issue_comment:<br \/>\n    types:<br \/>\n      &#8211; created<\/p>\n<p>permissions:<br \/>\n  contents: read<br \/>\n  id-token: write<br \/>\n  issues: write<\/p>\n<p>jobs:<br \/>\n  submit:<br \/>\n    name: Approve Request<br \/>\n    runs-on: ubuntu-latest<\/p>\n<p>    # This job should only be run when the following conditions are true:<br \/>\n    #<br \/>\n    # &#8211; A user comments `.approve` on the issue.<br \/>\n    # &#8211; The issue has the `team-membership` label.<br \/>\n    # &#8211; The issue has the `validated` label.<br \/>\n    # &#8211; The issue has the `submitted` label.<br \/>\n    # &#8211; The issue does not have the `approved` or `denied` labels.<br \/>\n    # &#8211; The issue is open.<br \/>\n    if: |<br \/>\n      startsWith(github.event.comment.body, &#8216;.approve&#8217;) &amp;&amp;<br \/>\n      contains(github.event.issue.labels.*.name, &#8216;team-membership&#8217;) == true &amp;&amp;<br \/>\n      contains(github.event.issue.labels.*.name, &#8216;submitted&#8217;) == true &amp;&amp;<br \/>\n      contains(github.event.issue.labels.*.name, &#8216;validated&#8217;) == true &amp;&amp;<br \/>\n      contains(github.event.issue.labels.*.name, &#8216;approved&#8217;) == false &amp;&amp;<br \/>\n      contains(github.event.issue.labels.*.name, &#8216;denied&#8217;) == false &amp;&amp;<br \/>\n      github.event.issue.state == &#8216;open&#8217;<\/p>\n<p>    steps:<br \/>\n      # This time, we do not need to re-run validation because the request is<br \/>\n      # being approved. It can just be processed.<\/p>\n<p>      # This is required to ensure the issue form template is included in the<br \/>\n      # workspace.<br \/>\n      &#8211; name: Checkout<br \/>\n        id: checkout<br \/>\n        uses: actions\/checkout@v4<\/p>\n<p>      # We do need to confirm that the user who commented `.approve` is a member<br \/>\n      # of the administrator team. GitHub App authentication is required if you<br \/>\n      # want to interact with any resources outside the scope of the repository<br \/>\n      # this workflow runs in.<br \/>\n      &#8211; name: Get GitHub App Token<br \/>\n        id: token<br \/>\n        uses: actions\/create-github-app-token@v1<br \/>\n        with:<br \/>\n          app-id: ${{ vars.ISSUEOPS_APP_ID }}<br \/>\n          private-key: ${{ secrets.ISSUEOPS_APP_PRIVATE_KEY }}<br \/>\n          owner: ${{ github.repository_owner }}<\/p>\n<p>      # Check if the user who commented `.approve` is a member of the<br \/>\n      # administrator team.<br \/>\n      &#8211; name: Check Admin Membership<br \/>\n        id: check-admin<br \/>\n        uses: actions\/github-script@v7<br \/>\n        with:<br \/>\n          github-token: ${{ steps.token.outputs.token }}<br \/>\n          script: |<br \/>\n            try {<br \/>\n              await github.rest.teams.getMembershipForUserInOrg({<br \/>\n                org: context.repo.owner,<br \/>\n                team_slug: &#8216;admins&#8217;,<br \/>\n                username: context.actor,<br \/>\n              })<br \/>\n              core.setOutput(&#8216;member&#8217;, &#8216;true&#8217;)<br \/>\n            } catch (error) {<br \/>\n              if (error.status === 404) {<br \/>\n                core.setOutput(&#8216;member&#8217;, &#8216;false&#8217;)<br \/>\n              }<br \/>\n              throw error<br \/>\n            }<\/p>\n<p>      # If the user is not a member of the administrator team, exit the<br \/>\n      # workflow.<br \/>\n      &#8211; if: ${{ steps.check-admin.outputs.member == &#8216;false&#8217; }}<br \/>\n        name: Exit<br \/>\n        run: exit 0<\/p>\n<p>      # Parse the issue body into machine-readable JSON, so that it can be<br \/>\n      # processed by the rest of the workflow.<br \/>\n      &#8211; name: Parse Issue body<br \/>\n        id: parse<br \/>\n        uses: issue-ops\/parser@v4<br \/>\n        with:<br \/>\n          body: ${{ github.event.issue.body }}<br \/>\n          issue-form-template: team-membership.yml<br \/>\n          workspace: ${{ github.workspace }}<\/p>\n<p>      &#8211; name: Add to Team<br \/>\n        id: add<br \/>\n        uses: actions\/github-script@v7<br \/>\n        with:<br \/>\n          github-token: ${{ steps.token.outputs.token }}<br \/>\n          script: |<br \/>\n            const parsedIssue = JSON.parse(&#8216;${{ steps.parse.outputs.json }}&#8217;)<\/p>\n<p>            await github.rest.teams.addOrUpdateMembershipForUserInOrg({<br \/>\n              org: context.repo.owner,<br \/>\n              team_slug: parsedIssue.team,<br \/>\n              username: &#8216;${{ github.event.issue.user.login }}&#8217;,<br \/>\n              role: &#8216;member&#8217;<br \/>\n            })<\/p>\n<p>      &#8211; name: Notify User<br \/>\n        id: notify<br \/>\n        uses: peter-evans\/create-or-update-comment@v4<br \/>\n        with:<br \/>\n          issue-number: ${{ github.event.issue.number }}<br \/>\n          body: |<br \/>\n            This request has been processed successfully!<\/p>\n<p>      &#8211; name: Close Issue<br \/>\n        id: close<br \/>\n        uses: actions\/github-script@v7<br \/>\n        with:<br \/>\n          script: |<br \/>\n            await github.rest.issues.update({<br \/>\n              issue_number: ${{ github.event.issue.number }},<br \/>\n              owner: context.repo.owner,<br \/>\n              repo: context.repo.repo,<br \/>\n              state: &#8216;closed&#8217;,<br \/>\n              state_reason: &#8216;completed&#8217;<br \/>\n            })<\/p>\n<h2>Take this with you<a href=\"https:\/\/github.blog\/engineering\/issueops-automate-ci-cd-and-more-with-github-issues-and-actions\/#take-this-with-you\" class=\"heading-link pl-2 text-italic text-bold\"><\/a><\/h2>\n<p>And there you have it! With a handful of standardized workflows, you have an end-to-end, issue-driven process in place to manage team membership. This can be extended as far as you want, including support for removing users, auditing access, and more. With IssueOps, the sky is the limit!<\/p>\n<p>Here\u2019s the best thing about IssueOps: It brings another level of automation to a surface I\u2019m constantly using\u2014and that\u2019s GitHub. By using issues and pull requests as control centers for workflows, teams can reduce friction, improve efficiency, and keep everything transparent. Whether you want to automate deployments, approvals, or bug triage, IssueOps makes it all possible, without ever leaving your repo.<\/p>\n<p>For more information and examples, check out the open source <a href=\"https:\/\/github.com\/issue-ops\/docs\">IssueOps documentation repository<\/a>, and if you want a deeper dive, you can head over to the open source <a href=\"https:\/\/issue-ops.github.io\/docs\/\">IssueOps documentation<\/a>.<\/p>\n<p>In my experience, it\u2019s always best to start small and experiment with what works best for you. With just a bit of time, you\u2019ll see your workflows get smoother with every commit (I know I have). Happy coding! \u2728<\/p>\n<p>The post <a href=\"https:\/\/github.blog\/engineering\/issueops-automate-ci-cd-and-more-with-github-issues-and-actions\/\">IssueOps: Automate CI\/CD (and more!) with GitHub Issues and Actions<\/a> appeared first on <a href=\"https:\/\/github.blog\/\">The GitHub Blog<\/a>.<\/p>","protected":false},"excerpt":{"rendered":"<p>Software development is filled with repetitive tasks\u2014managing issues, handling approvals, triggering CI\/CD workflows, and more. But what if you could [&hellip;]<\/p>\n","protected":false},"author":0,"featured_media":0,"comment_status":"","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"default","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","ast-disable-related-posts":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"default","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"footnotes":""},"categories":[8],"tags":[],"class_list":["post-1846","post","type-post","status-publish","format-standard","hentry","category-github-engineering"],"_links":{"self":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts\/1846","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/types\/post"}],"replies":[{"embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/comments?post=1846"}],"version-history":[{"count":0,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts\/1846\/revisions"}],"wp:attachment":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/media?parent=1846"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/categories?post=1846"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/tags?post=1846"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}